First round of fixes of log scaling based on code review from danvk.
[dygraphs.git] / dygraph.js
CommitLineData
6a1aa64f
DV
1// Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2// All Rights Reserved.
3
4/**
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
285a6bda
DV
6 * string. Dygraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. Dygraph uses the
6a1aa64f
DV
8 * <canvas> tag, so it only works in FF1.5+.
9 * @author danvdk@gmail.com (Dan Vanderkam)
10
11 Usage:
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
285a6bda
DV
14 new Dygraph(document.getElementById("graphdiv"),
15 "datafile.csv", // CSV file with headers
16 { }); // options
6a1aa64f
DV
17 </script>
18
19 The CSV file is of the form
20
285a6bda 21 Date,SeriesA,SeriesB,SeriesC
6a1aa64f
DV
22 YYYYMMDD,A1,B1,C1
23 YYYYMMDD,A2,B2,C2
24
6a1aa64f
DV
25 If the 'errorBars' option is set in the constructor, the input should be of
26 the form
27
285a6bda 28 Date,SeriesA,SeriesB,...
6a1aa64f
DV
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
31
32 If the 'fractions' option is set, the input should be of the form:
33
285a6bda 34 Date,SeriesA,SeriesB,...
6a1aa64f
DV
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
37
38 And error bars will be calculated automatically using a binomial distribution.
39
ee672584 40 For further documentation and examples, see http://dygraphs.com/
6a1aa64f
DV
41
42 */
43
44/**
45 * An interactive, zoomable graph
46 * @param {String | Function} file A file containing CSV data or a function that
47 * returns this data. The expected format for each line is
48 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
49 * YYYYMMDD,val1,stddev1,val2,stddev2,...
6a1aa64f
DV
50 * @param {Object} attrs Various other attributes, e.g. errorBars determines
51 * whether the input data contains error ranges.
52 */
285a6bda
DV
53Dygraph = function(div, data, opts) {
54 if (arguments.length > 0) {
55 if (arguments.length == 4) {
56 // Old versions of dygraphs took in the series labels as a constructor
57 // parameter. This doesn't make sense anymore, but it's easy to continue
58 // to support this usage.
59 this.warn("Using deprecated four-argument dygraph constructor");
60 this.__old_init__(div, data, arguments[2], arguments[3]);
61 } else {
62 this.__init__(div, data, opts);
63 }
64 }
6a1aa64f
DV
65};
66
285a6bda
DV
67Dygraph.NAME = "Dygraph";
68Dygraph.VERSION = "1.2";
69Dygraph.__repr__ = function() {
6a1aa64f
DV
70 return "[" + this.NAME + " " + this.VERSION + "]";
71};
285a6bda 72Dygraph.toString = function() {
6a1aa64f
DV
73 return this.__repr__();
74};
75
76// Various default values
285a6bda
DV
77Dygraph.DEFAULT_ROLL_PERIOD = 1;
78Dygraph.DEFAULT_WIDTH = 480;
79Dygraph.DEFAULT_HEIGHT = 320;
80Dygraph.AXIS_LINE_WIDTH = 0.3;
6a1aa64f 81
d59b6f34 82Dygraph.LOG_SCALE = 10;
0037b2a4 83Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
d59b6f34 84Dygraph.log10 = function(x) {
0037b2a4 85 return Math.log(x) / Dygraph.LN_TEN;
d59b6f34 86}
885c13e4 87
8e4a6af3 88// Default attribute values.
285a6bda 89Dygraph.DEFAULT_ATTRS = {
a9fc39ab 90 highlightCircleSize: 3,
8e4a6af3 91 pixelsPerXLabel: 60,
c6336f04 92 pixelsPerYLabel: 30,
285a6bda 93
8e4a6af3
DV
94 labelsDivWidth: 250,
95 labelsDivStyles: {
96 // TODO(danvk): move defaults from createStatusMessage_ here.
285a6bda
DV
97 },
98 labelsSeparateLines: false,
bcd3ebf0 99 labelsShowZeroValues: true,
285a6bda 100 labelsKMB: false,
afefbcdb 101 labelsKMG2: false,
d160cc3b 102 showLabelsOnHighlight: true,
12e4c741 103
029da4b6 104 yValueFormatter: function(x) { return Dygraph.round_(x, 2); },
285a6bda
DV
105
106 strokeWidth: 1.0,
8e4a6af3 107
8846615a
DV
108 axisTickSize: 3,
109 axisLabelFontSize: 14,
110 xAxisLabelWidth: 50,
111 yAxisLabelWidth: 50,
bf640e56 112 xAxisLabelFormatter: Dygraph.dateAxisFormatter,
8846615a 113 rightGap: 5,
285a6bda
DV
114
115 showRoller: false,
116 xValueFormatter: Dygraph.dateString_,
117 xValueParser: Dygraph.dateParser,
118 xTicker: Dygraph.dateTicker,
119
3d67f03b
DV
120 delimiter: ',',
121
ff00d3e2 122 logScale: false,
285a6bda
DV
123 sigma: 2.0,
124 errorBars: false,
125 fractions: false,
126 wilsonInterval: true, // only relevant if fractions is true
5954ef32 127 customBars: false,
43af96e7
NK
128 fillGraph: false,
129 fillAlpha: 0.15,
f032c51d 130 connectSeparatedPoints: false,
43af96e7
NK
131
132 stackedGraph: false,
afdc483f
NN
133 hideOverlayOnMouseOut: true,
134
00c281d4 135 stepPlot: false,
ee672584 136 avoidMinZero: false,
4f0c5aa0
DV
137
138 interactionModel: null // will be set to Dygraph.defaultInteractionModel.
285a6bda
DV
139};
140
141// Various logging levels.
142Dygraph.DEBUG = 1;
143Dygraph.INFO = 2;
144Dygraph.WARNING = 3;
145Dygraph.ERROR = 3;
146
39b0e098
RK
147// Directions for panning and zooming. Use bit operations when combined
148// values are possible.
149Dygraph.HORIZONTAL = 1;
150Dygraph.VERTICAL = 2;
151
5c528fa2
DV
152// Used for initializing annotation CSS rules only once.
153Dygraph.addedAnnotationCSS = false;
154
285a6bda
DV
155Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
156 // Labels is no longer a constructor parameter, since it's typically set
157 // directly from the data source. It also conains a name for the x-axis,
158 // which the previous constructor form did not.
159 if (labels != null) {
160 var new_labels = ["Date"];
161 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
fc80a396 162 Dygraph.update(attrs, { 'labels': new_labels });
285a6bda
DV
163 }
164 this.__init__(div, file, attrs);
8e4a6af3
DV
165};
166
6a1aa64f 167/**
285a6bda 168 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
885c13e4 169 * and context &lt;canvas&gt; inside of it. See the constructor for details.
6a1aa64f 170 * on the parameters.
12e4c741 171 * @param {Element} div the Element to render the graph into.
6a1aa64f 172 * @param {String | Function} file Source data
6a1aa64f
DV
173 * @param {Object} attrs Miscellaneous other options
174 * @private
175 */
285a6bda 176Dygraph.prototype.__init__ = function(div, file, attrs) {
a2c8fff4
DV
177 // Hack for IE: if we're using excanvas and the document hasn't finished
178 // loading yet (and hence may not have initialized whatever it needs to
179 // initialize), then keep calling this routine periodically until it has.
180 if (/MSIE/.test(navigator.userAgent) && !window.opera &&
181 typeof(G_vmlCanvasManager) != 'undefined' &&
182 document.readyState != 'complete') {
183 var self = this;
184 setTimeout(function() { self.__init__(div, file, attrs) }, 100);
185 }
186
285a6bda
DV
187 // Support two-argument constructor
188 if (attrs == null) { attrs = {}; }
189
6a1aa64f 190 // Copy the important bits into the object
32988383 191 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
6a1aa64f 192 this.maindiv_ = div;
6a1aa64f 193 this.file_ = file;
285a6bda 194 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
6a1aa64f 195 this.previousVerticalX_ = -1;
6a1aa64f 196 this.fractions_ = attrs.fractions || false;
6a1aa64f 197 this.dateWindow_ = attrs.dateWindow || null;
8b83c6cc 198
6a1aa64f 199 this.wilsonInterval_ = attrs.wilsonInterval || true;
fe0b7c03 200 this.is_initial_draw_ = true;
5c528fa2 201 this.annotations_ = [];
8e4a6af3 202
f7d6278e
DV
203 // Clear the div. This ensure that, if multiple dygraphs are passed the same
204 // div, then only one will be drawn.
205 div.innerHTML = "";
206
c21d2c2d 207 // If the div isn't already sized then inherit from our attrs or
208 // give it a default size.
285a6bda 209 if (div.style.width == '') {
ddd1b11f 210 div.style.width = (attrs.width || Dygraph.DEFAULT_WIDTH) + "px";
285a6bda
DV
211 }
212 if (div.style.height == '') {
ddd1b11f 213 div.style.height = (attrs.height || Dygraph.DEFAULT_HEIGHT) + "px";
32988383 214 }
285a6bda
DV
215 this.width_ = parseInt(div.style.width, 10);
216 this.height_ = parseInt(div.style.height, 10);
c21d2c2d 217 // The div might have been specified as percent of the current window size,
218 // convert that to an appropriate number of pixels.
219 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
c6f45033 220 this.width_ = div.offsetWidth;
c21d2c2d 221 }
222 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
c6f45033 223 this.height_ = div.offsetHeight;
c21d2c2d 224 }
32988383 225
10a6456d
DV
226 if (this.width_ == 0) {
227 this.error("dygraph has zero width. Please specify a width in pixels.");
228 }
229 if (this.height_ == 0) {
230 this.error("dygraph has zero height. Please specify a height in pixels.");
231 }
232
344ba8c0 233 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
43af96e7
NK
234 if (attrs['stackedGraph']) {
235 attrs['fillGraph'] = true;
236 // TODO(nikhilk): Add any other stackedGraph checks here.
237 }
238
285a6bda
DV
239 // Dygraphs has many options, some of which interact with one another.
240 // To keep track of everything, we maintain two sets of options:
241 //
c21d2c2d 242 // this.user_attrs_ only options explicitly set by the user.
285a6bda
DV
243 // this.attrs_ defaults, options derived from user_attrs_, data.
244 //
245 // Options are then accessed this.attr_('attr'), which first looks at
246 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
247 // defaults without overriding behavior that the user specifically asks for.
248 this.user_attrs_ = {};
fc80a396 249 Dygraph.update(this.user_attrs_, attrs);
6a1aa64f 250
285a6bda 251 this.attrs_ = {};
fc80a396 252 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
6a1aa64f 253
16269f6e 254 this.boundaryIds_ = [];
6a1aa64f 255
285a6bda
DV
256 // Make a note of whether labels will be pulled from the CSV file.
257 this.labelsFromCSV_ = (this.attr_("labels") == null);
6a1aa64f
DV
258
259 // Create the containing DIV and other interactive elements
260 this.createInterface_();
261
738fc797 262 this.start_();
6a1aa64f
DV
263};
264
227b93cc
DV
265Dygraph.prototype.attr_ = function(name, seriesName) {
266 if (seriesName &&
267 typeof(this.user_attrs_[seriesName]) != 'undefined' &&
268 this.user_attrs_[seriesName] != null &&
269 typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
270 return this.user_attrs_[seriesName][name];
450fe64b 271 } else if (typeof(this.user_attrs_[name]) != 'undefined') {
285a6bda
DV
272 return this.user_attrs_[name];
273 } else if (typeof(this.attrs_[name]) != 'undefined') {
274 return this.attrs_[name];
275 } else {
276 return null;
277 }
278};
279
280// TODO(danvk): any way I can get the line numbers to be this.warn call?
281Dygraph.prototype.log = function(severity, message) {
282 if (typeof(console) != 'undefined') {
283 switch (severity) {
284 case Dygraph.DEBUG:
285 console.debug('dygraphs: ' + message);
286 break;
287 case Dygraph.INFO:
288 console.info('dygraphs: ' + message);
289 break;
290 case Dygraph.WARNING:
291 console.warn('dygraphs: ' + message);
292 break;
293 case Dygraph.ERROR:
294 console.error('dygraphs: ' + message);
295 break;
296 }
297 }
298}
299Dygraph.prototype.info = function(message) {
300 this.log(Dygraph.INFO, message);
301}
302Dygraph.prototype.warn = function(message) {
303 this.log(Dygraph.WARNING, message);
304}
305Dygraph.prototype.error = function(message) {
306 this.log(Dygraph.ERROR, message);
307}
308
6a1aa64f
DV
309/**
310 * Returns the current rolling period, as set by the user or an option.
311 * @return {Number} The number of days in the rolling window
312 */
285a6bda 313Dygraph.prototype.rollPeriod = function() {
6a1aa64f 314 return this.rollPeriod_;
76171648
DV
315};
316
599fb4ad
DV
317/**
318 * Returns the currently-visible x-range. This can be affected by zooming,
319 * panning or a call to updateOptions.
320 * Returns a two-element array: [left, right].
321 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
322 */
323Dygraph.prototype.xAxisRange = function() {
324 if (this.dateWindow_) return this.dateWindow_;
325
326 // The entire chart is visible.
327 var left = this.rawData_[0][0];
328 var right = this.rawData_[this.rawData_.length - 1][0];
329 return [left, right];
330};
331
3230c662 332/**
d58ae307
DV
333 * Returns the currently-visible y-range for an axis. This can be affected by
334 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
335 * called with no arguments, returns the range of the first axis.
3230c662
DV
336 * Returns a two-element array: [bottom, top].
337 */
d58ae307 338Dygraph.prototype.yAxisRange = function(idx) {
d63e6799 339 if (typeof(idx) == "undefined") idx = 0;
d58ae307
DV
340 if (idx < 0 || idx >= this.axes_.length) return null;
341 return [ this.axes_[idx].computedValueRange[0],
342 this.axes_[idx].computedValueRange[1] ];
343};
344
345/**
346 * Returns the currently-visible y-ranges for each axis. This can be affected by
347 * zooming, panning, calls to updateOptions, etc.
348 * Returns an array of [bottom, top] pairs, one for each y-axis.
349 */
350Dygraph.prototype.yAxisRanges = function() {
351 var ret = [];
352 for (var i = 0; i < this.axes_.length; i++) {
353 ret.push(this.yAxisRange(i));
354 }
355 return ret;
3230c662
DV
356};
357
d58ae307 358// TODO(danvk): use these functions throughout dygraphs.
3230c662
DV
359/**
360 * Convert from data coordinates to canvas/div X/Y coordinates.
d58ae307
DV
361 * If specified, do this conversion for the coordinate system of a particular
362 * axis. Uses the first axis by default.
3230c662 363 * Returns a two-element array: [X, Y]
ff022deb 364 *
0747928a 365 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
ff022deb 366 * instead of toDomCoords(null, y, axis).
3230c662 367 */
d58ae307 368Dygraph.prototype.toDomCoords = function(x, y, axis) {
ff022deb
RK
369 return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
370};
371
372/**
373 * Convert from data x coordinates to canvas/div X coordinate.
374 * If specified, do this conversion for the coordinate system of a particular
0037b2a4
RK
375 * axis.
376 * Returns a single value or null if x is null.
ff022deb
RK
377 */
378Dygraph.prototype.toDomXCoord = function(x) {
379 if (x == null) {
380 return null;
381 };
382
3230c662 383 var area = this.plotter_.area;
ff022deb
RK
384 var xRange = this.xAxisRange();
385 return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
386}
3230c662 387
ff022deb
RK
388/**
389 * Convert from data x coordinates to canvas/div Y coordinate and optional
390 * axis. Uses the first axis by default.
391 *
392 * returns a single value or null if y is null.
393 */
394Dygraph.prototype.toDomYCoord = function(y, axis) {
0747928a 395 var pct = this.toPercentYCoord(y, axis);
3230c662 396
ff022deb
RK
397 if (pct == null) {
398 return null;
399 }
e4416fb9 400 var area = this.plotter_.area;
ff022deb
RK
401 return area.y + pct * area.h;
402}
3230c662
DV
403
404/**
405 * Convert from canvas/div coords to data coordinates.
d58ae307
DV
406 * If specified, do this conversion for the coordinate system of a particular
407 * axis. Uses the first axis by default.
ff022deb
RK
408 * Returns a two-element array: [X, Y].
409 *
0747928a 410 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
ff022deb 411 * instead of toDataCoords(null, y, axis).
3230c662 412 */
d58ae307 413Dygraph.prototype.toDataCoords = function(x, y, axis) {
ff022deb
RK
414 return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
415};
416
417/**
418 * Convert from canvas/div x coordinate to data coordinate.
419 *
420 * If x is null, this returns null.
421 */
422Dygraph.prototype.toDataXCoord = function(x) {
423 if (x == null) {
424 return null;
3230c662
DV
425 }
426
ff022deb
RK
427 var area = this.plotter_.area;
428 var xRange = this.xAxisRange();
429 return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
430};
431
432/**
433 * Convert from canvas/div y coord to value.
434 *
435 * If y is null, this returns null.
436 * if axis is null, this uses the first axis.
437 */
438Dygraph.prototype.toDataYCoord = function(y, axis) {
439 if (y == null) {
440 return null;
3230c662
DV
441 }
442
ff022deb
RK
443 var area = this.plotter_.area;
444 var yRange = this.yAxisRange(axis);
445
446 if (!this.attr_("logscale")) {
447 return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
448 } else {
449 // Computing the inverse of toDomCoord.
450 var pct = (y - area.y) / area.h
451
452 // Computing the inverse of toPercentYCoord. The function was arrived at with
453 // the following steps:
454 //
455 // Original calcuation:
d59b6f34 456 // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
ff022deb
RK
457 //
458 // Move denominator to both sides:
d59b6f34 459 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
ff022deb
RK
460 //
461 // subtract logr1, and take the negative value.
d59b6f34 462 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
ff022deb
RK
463 //
464 // Swap both sides of the equation, and we can compute the log of the
465 // return value. Which means we just need to use that as the exponent in
466 // e^exponent.
d59b6f34 467 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
ff022deb 468
d59b6f34
RK
469 var logr1 = Dygraph.log10(yRange[1]);
470 var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
471 var value = Math.pow(Dygraph.LOG_SCALE, exponent);
ff022deb
RK
472 return value;
473 }
3230c662
DV
474};
475
e99fde05 476/**
ff022deb
RK
477 * Converts a y for an axis to a percentage from the top to the
478 * bottom of the div.
479 *
480 * If the coordinate represents a value visible on the canvas, then
481 * the value will be between 0 and 1, where 0 is the top of the canvas.
482 * However, this method will return values outside the range, as
483 * values can fall outside the canvas.
484 *
485 * If y is null, this returns null.
486 * if axis is null, this uses the first axis.
487 */
488Dygraph.prototype.toPercentYCoord = function(y, axis) {
489 if (y == null) {
490 return null;
491 }
492
493 var area = this.plotter_.area;
494 var yRange = this.yAxisRange(axis);
495
496 var pct;
497 if (!this.attr_("logscale")) {
498 // yrange[1] - y is unit distance from the bottom.
499 // yrange[1] - yrange[0] is the scale of the range.
500 // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
501 pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
502 } else {
d59b6f34
RK
503 var logr1 = Dygraph.log10(yRange[1]);
504 pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
ff022deb
RK
505 }
506 return pct;
507}
508
509/**
e99fde05
DV
510 * Returns the number of columns (including the independent variable).
511 */
512Dygraph.prototype.numColumns = function() {
513 return this.rawData_[0].length;
514};
515
516/**
517 * Returns the number of rows (excluding any header/label row).
518 */
519Dygraph.prototype.numRows = function() {
520 return this.rawData_.length;
521};
522
523/**
524 * Returns the value in the given row and column. If the row and column exceed
525 * the bounds on the data, returns null. Also returns null if the value is
526 * missing.
527 */
528Dygraph.prototype.getValue = function(row, col) {
529 if (row < 0 || row > this.rawData_.length) return null;
530 if (col < 0 || col > this.rawData_[row].length) return null;
531
532 return this.rawData_[row][col];
533};
534
76171648
DV
535Dygraph.addEvent = function(el, evt, fn) {
536 var normed_fn = function(e) {
537 if (!e) var e = window.event;
538 fn(e);
539 };
540 if (window.addEventListener) { // Mozilla, Netscape, Firefox
541 el.addEventListener(evt, normed_fn, false);
542 } else { // IE
543 el.attachEvent('on' + evt, normed_fn);
544 }
545};
6a1aa64f 546
ee672584 547
ee672584
RK
548// Based on the article at
549// http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
ee672584
RK
550Dygraph.cancelEvent = function(e) {
551 e = e ? e : window.event;
552 if (e.stopPropagation) {
553 e.stopPropagation();
554 }
555 if (e.preventDefault) {
556 e.preventDefault();
557 }
558 e.cancelBubble = true;
559 e.cancel = true;
560 e.returnValue = false;
561 return false;
562}
563
6a1aa64f 564/**
285a6bda 565 * Generates interface elements for the Dygraph: a containing div, a div to
6a1aa64f 566 * display the current point, and a textbox to adjust the rolling average
697e70b2 567 * period. Also creates the Renderer/Layout elements.
6a1aa64f
DV
568 * @private
569 */
285a6bda 570Dygraph.prototype.createInterface_ = function() {
6a1aa64f
DV
571 // Create the all-enclosing graph div
572 var enclosing = this.maindiv_;
573
b0c3b730
DV
574 this.graphDiv = document.createElement("div");
575 this.graphDiv.style.width = this.width_ + "px";
576 this.graphDiv.style.height = this.height_ + "px";
577 enclosing.appendChild(this.graphDiv);
578
579 // Create the canvas for interactive parts of the chart.
f8cfec73 580 this.canvas_ = Dygraph.createCanvas();
b0c3b730
DV
581 this.canvas_.style.position = "absolute";
582 this.canvas_.width = this.width_;
583 this.canvas_.height = this.height_;
f8cfec73
DV
584 this.canvas_.style.width = this.width_ + "px"; // for IE
585 this.canvas_.style.height = this.height_ + "px"; // for IE
b0c3b730
DV
586
587 // ... and for static parts of the chart.
6a1aa64f 588 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
76171648 589
eb7bf005
EC
590 // The interactive parts of the graph are drawn on top of the chart.
591 this.graphDiv.appendChild(this.hidden_);
592 this.graphDiv.appendChild(this.canvas_);
593 this.mouseEventElement_ = this.canvas_;
594
76171648 595 var dygraph = this;
eb7bf005 596 Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
76171648
DV
597 dygraph.mouseMove_(e);
598 });
eb7bf005 599 Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
76171648
DV
600 dygraph.mouseOut_(e);
601 });
697e70b2
DV
602
603 // Create the grapher
604 // TODO(danvk): why does the Layout need its own set of options?
605 this.layoutOptions_ = { 'xOriginIsZero': false };
606 Dygraph.update(this.layoutOptions_, this.attrs_);
607 Dygraph.update(this.layoutOptions_, this.user_attrs_);
608 Dygraph.update(this.layoutOptions_, {
609 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
610
611 this.layout_ = new DygraphLayout(this, this.layoutOptions_);
612
613 // TODO(danvk): why does the Renderer need its own set of options?
614 this.renderOptions_ = { colorScheme: this.colors_,
615 strokeColor: null,
616 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
617 Dygraph.update(this.renderOptions_, this.attrs_);
618 Dygraph.update(this.renderOptions_, this.user_attrs_);
697e70b2
DV
619
620 this.createStatusMessage_();
697e70b2 621 this.createDragInterface_();
4cfcc38c
DV
622};
623
624/**
625 * Detach DOM elements in the dygraph and null out all data references.
626 * Calling this when you're done with a dygraph can dramatically reduce memory
627 * usage. See, e.g., the tests/perf.html example.
628 */
629Dygraph.prototype.destroy = function() {
630 var removeRecursive = function(node) {
631 while (node.hasChildNodes()) {
632 removeRecursive(node.firstChild);
633 node.removeChild(node.firstChild);
634 }
635 };
636 removeRecursive(this.maindiv_);
637
638 var nullOut = function(obj) {
639 for (var n in obj) {
640 if (typeof(obj[n]) === 'object') {
641 obj[n] = null;
642 }
643 }
644 };
645
646 // These may not all be necessary, but it can't hurt...
647 nullOut(this.layout_);
648 nullOut(this.plotter_);
649 nullOut(this);
650};
6a1aa64f
DV
651
652/**
653 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
285a6bda 654 * this particular canvas. All Dygraph work is done on this.canvas_.
8846615a 655 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
6a1aa64f
DV
656 * @return {Object} The newly-created canvas
657 * @private
658 */
285a6bda 659Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
f8cfec73 660 var h = Dygraph.createCanvas();
6a1aa64f 661 h.style.position = "absolute";
9ac5e4ae
DV
662 // TODO(danvk): h should be offset from canvas. canvas needs to include
663 // some extra area to make it easier to zoom in on the far left and far
664 // right. h needs to be precisely the plot area, so that clipping occurs.
6a1aa64f
DV
665 h.style.top = canvas.style.top;
666 h.style.left = canvas.style.left;
667 h.width = this.width_;
668 h.height = this.height_;
f8cfec73
DV
669 h.style.width = this.width_ + "px"; // for IE
670 h.style.height = this.height_ + "px"; // for IE
6a1aa64f
DV
671 return h;
672};
673
f474c2a3
DV
674// Taken from MochiKit.Color
675Dygraph.hsvToRGB = function (hue, saturation, value) {
676 var red;
677 var green;
678 var blue;
679 if (saturation === 0) {
680 red = value;
681 green = value;
682 blue = value;
683 } else {
684 var i = Math.floor(hue * 6);
685 var f = (hue * 6) - i;
686 var p = value * (1 - saturation);
687 var q = value * (1 - (saturation * f));
688 var t = value * (1 - (saturation * (1 - f)));
689 switch (i) {
690 case 1: red = q; green = value; blue = p; break;
691 case 2: red = p; green = value; blue = t; break;
692 case 3: red = p; green = q; blue = value; break;
693 case 4: red = t; green = p; blue = value; break;
694 case 5: red = value; green = p; blue = q; break;
695 case 6: // fall through
696 case 0: red = value; green = t; blue = p; break;
697 }
698 }
699 red = Math.floor(255 * red + 0.5);
700 green = Math.floor(255 * green + 0.5);
701 blue = Math.floor(255 * blue + 0.5);
702 return 'rgb(' + red + ',' + green + ',' + blue + ')';
703};
704
705
6a1aa64f
DV
706/**
707 * Generate a set of distinct colors for the data series. This is done with a
708 * color wheel. Saturation/Value are customizable, and the hue is
709 * equally-spaced around the color wheel. If a custom set of colors is
710 * specified, that is used instead.
6a1aa64f
DV
711 * @private
712 */
285a6bda
DV
713Dygraph.prototype.setColors_ = function() {
714 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
715 // away with this.renderOptions_.
716 var num = this.attr_("labels").length - 1;
6a1aa64f 717 this.colors_ = [];
285a6bda
DV
718 var colors = this.attr_('colors');
719 if (!colors) {
720 var sat = this.attr_('colorSaturation') || 1.0;
721 var val = this.attr_('colorValue') || 0.5;
2aa21213 722 var half = Math.ceil(num / 2);
6a1aa64f 723 for (var i = 1; i <= num; i++) {
ec1959eb 724 if (!this.visibility()[i-1]) continue;
43af96e7 725 // alternate colors for high contrast.
2aa21213 726 var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
43af96e7
NK
727 var hue = (1.0 * idx/ (1 + num));
728 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
6a1aa64f
DV
729 }
730 } else {
731 for (var i = 0; i < num; i++) {
ec1959eb 732 if (!this.visibility()[i]) continue;
285a6bda 733 var colorStr = colors[i % colors.length];
f474c2a3 734 this.colors_.push(colorStr);
6a1aa64f
DV
735 }
736 }
285a6bda 737
c21d2c2d 738 // TODO(danvk): update this w/r/t/ the new options system.
285a6bda 739 this.renderOptions_.colorScheme = this.colors_;
fc80a396
DV
740 Dygraph.update(this.plotter_.options, this.renderOptions_);
741 Dygraph.update(this.layoutOptions_, this.user_attrs_);
742 Dygraph.update(this.layoutOptions_, this.attrs_);
6a1aa64f
DV
743}
744
43af96e7
NK
745/**
746 * Return the list of colors. This is either the list of colors passed in the
747 * attributes, or the autogenerated list of rgb(r,g,b) strings.
748 * @return {Array<string>} The list of colors.
749 */
750Dygraph.prototype.getColors = function() {
751 return this.colors_;
752};
753
5e60386d
DV
754// The following functions are from quirksmode.org with a modification for Safari from
755// http://blog.firetree.net/2005/07/04/javascript-find-position/
3df0ccf0
DV
756// http://www.quirksmode.org/js/findpos.html
757Dygraph.findPosX = function(obj) {
758 var curleft = 0;
5e60386d 759 if(obj.offsetParent)
50360fd0 760 while(1)
5e60386d 761 {
3df0ccf0 762 curleft += obj.offsetLeft;
5e60386d
DV
763 if(!obj.offsetParent)
764 break;
3df0ccf0
DV
765 obj = obj.offsetParent;
766 }
5e60386d 767 else if(obj.x)
3df0ccf0
DV
768 curleft += obj.x;
769 return curleft;
770};
c21d2c2d 771
3df0ccf0
DV
772Dygraph.findPosY = function(obj) {
773 var curtop = 0;
5e60386d
DV
774 if(obj.offsetParent)
775 while(1)
776 {
3df0ccf0 777 curtop += obj.offsetTop;
5e60386d
DV
778 if(!obj.offsetParent)
779 break;
3df0ccf0
DV
780 obj = obj.offsetParent;
781 }
5e60386d 782 else if(obj.y)
3df0ccf0
DV
783 curtop += obj.y;
784 return curtop;
785};
786
5e60386d 787
71a11a8e 788
6a1aa64f
DV
789/**
790 * Create the div that contains information on the selected point(s)
791 * This goes in the top right of the canvas, unless an external div has already
792 * been specified.
793 * @private
794 */
fedbd797 795Dygraph.prototype.createStatusMessage_ = function() {
796 var userLabelsDiv = this.user_attrs_["labelsDiv"];
797 if (userLabelsDiv && null != userLabelsDiv
798 && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
799 this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
800 }
285a6bda
DV
801 if (!this.attr_("labelsDiv")) {
802 var divWidth = this.attr_('labelsDivWidth');
b0c3b730 803 var messagestyle = {
6a1aa64f
DV
804 "position": "absolute",
805 "fontSize": "14px",
806 "zIndex": 10,
807 "width": divWidth + "px",
808 "top": "0px",
8846615a 809 "left": (this.width_ - divWidth - 2) + "px",
6a1aa64f
DV
810 "background": "white",
811 "textAlign": "left",
b0c3b730 812 "overflow": "hidden"};
fc80a396 813 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
b0c3b730
DV
814 var div = document.createElement("div");
815 for (var name in messagestyle) {
85b99f0b
DV
816 if (messagestyle.hasOwnProperty(name)) {
817 div.style[name] = messagestyle[name];
818 }
b0c3b730
DV
819 }
820 this.graphDiv.appendChild(div);
285a6bda 821 this.attrs_.labelsDiv = div;
6a1aa64f
DV
822 }
823};
824
825/**
0abfbd7e
DV
826 * Position the labels div so that its right edge is flush with the right edge
827 * of the charting area.
828 */
829Dygraph.prototype.positionLabelsDiv_ = function() {
830 // Don't touch a user-specified labelsDiv.
831 if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
832
833 var area = this.plotter_.area;
834 var div = this.attr_("labelsDiv");
8c21adcf 835 div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
0abfbd7e
DV
836};
837
838/**
6a1aa64f 839 * Create the text box to adjust the averaging period
6a1aa64f
DV
840 * @private
841 */
285a6bda 842Dygraph.prototype.createRollInterface_ = function() {
8c69de65
DV
843 // Create a roller if one doesn't exist already.
844 if (!this.roller_) {
845 this.roller_ = document.createElement("input");
846 this.roller_.type = "text";
847 this.roller_.style.display = "none";
848 this.graphDiv.appendChild(this.roller_);
849 }
850
851 var display = this.attr_('showRoller') ? 'block' : 'none';
26ca7938 852
b0c3b730
DV
853 var textAttr = { "position": "absolute",
854 "zIndex": 10,
855 "top": (this.plotter_.area.h - 25) + "px",
856 "left": (this.plotter_.area.x + 1) + "px",
857 "display": display
6a1aa64f 858 };
8c69de65
DV
859 this.roller_.size = "2";
860 this.roller_.value = this.rollPeriod_;
b0c3b730 861 for (var name in textAttr) {
85b99f0b 862 if (textAttr.hasOwnProperty(name)) {
8c69de65 863 this.roller_.style[name] = textAttr[name];
85b99f0b 864 }
b0c3b730
DV
865 }
866
76171648 867 var dygraph = this;
8c69de65 868 this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
76171648
DV
869};
870
871// These functions are taken from MochiKit.Signal
872Dygraph.pageX = function(e) {
873 if (e.pageX) {
874 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
875 } else {
876 var de = document;
877 var b = document.body;
878 return e.clientX +
879 (de.scrollLeft || b.scrollLeft) -
880 (de.clientLeft || 0);
881 }
882};
883
884Dygraph.pageY = function(e) {
885 if (e.pageY) {
886 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
887 } else {
888 var de = document;
889 var b = document.body;
890 return e.clientY +
891 (de.scrollTop || b.scrollTop) -
892 (de.clientTop || 0);
893 }
894};
6a1aa64f 895
ee672584
RK
896Dygraph.prototype.dragGetX_ = function(e, context) {
897 return Dygraph.pageX(e) - context.px
898};
6a1aa64f 899
ee672584
RK
900Dygraph.prototype.dragGetY_ = function(e, context) {
901 return Dygraph.pageY(e) - context.py
902};
e3489f4f 903
885c13e4
RK
904// Called in response to an interaction model operation that
905// should start the default panning behavior.
906//
907// It's used in the default callback for "mousedown" operations.
908// Custom interaction model builders can use it to provide the default
909// panning behavior.
910//
ee672584 911Dygraph.startPan = function(event, g, context) {
ee672584
RK
912 context.isPanning = true;
913 var xRange = g.xAxisRange();
914 context.dateRange = xRange[1] - xRange[0];
915
916 // Record the range of each y-axis at the start of the drag.
917 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
918 context.is2DPan = false;
919 for (var i = 0; i < g.axes_.length; i++) {
920 var axis = g.axes_[i];
921 var yRange = g.yAxisRange(i);
922 axis.dragValueRange = yRange[1] - yRange[0];
ff022deb 923 axis.draggingValue = g.toDataYCoord(context.dragStartY, i);
ee672584
RK
924 if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
925 }
926
927 // TODO(konigsberg): Switch from all this math to toDataCoords?
928 // Seems to work for the dragging value.
929 context.draggingDate = (context.dragStartX / g.width_) * context.dateRange + xRange[0];
930};
931
885c13e4
RK
932// Called in response to an interaction model operation that
933// responds to an event that pans the view.
934//
935// It's used in the default callback for "mousemove" operations.
936// Custom interaction model builders can use it to provide the default
937// panning behavior.
938//
ee672584
RK
939Dygraph.movePan = function(event, g, context) {
940 context.dragEndX = g.dragGetX_(event, context);
941 context.dragEndY = g.dragGetY_(event, context);
e3489f4f 942
d58ae307 943 // TODO(danvk): update this comment
ee672584
RK
944 // Want to have it so that:
945 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
946 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
947 // 3. draggingValue appears at dragEndY.
948 // 4. valueRange is unaltered.
949
950 var minDate = context.draggingDate - (context.dragEndX / g.width_) * context.dateRange;
951 var maxDate = minDate + context.dateRange;
952 g.dateWindow_ = [minDate, maxDate];
953
954 // y-axis scaling is automatic unless this is a full 2D pan.
955 if (context.is2DPan) {
956 // Adjust each axis appropriately.
d59b6f34
RK
957 // NOTE(konigsberg): I don't think this computation for y_frac is correct.
958 // I think it doesn't take into account the display of the x axis.
959 // See, when I tested this with console.log(y_frac), and move the mouse
960 // cursor to the botom, the largest y_frac was 0.94, and not 1.0. That
961 // could also explain why panning tends to start with a small jumpy shift.
ee672584 962 var y_frac = context.dragEndY / g.height_;
d59b6f34 963
ee672584
RK
964 for (var i = 0; i < g.axes_.length; i++) {
965 var axis = g.axes_[i];
966 var maxValue = axis.draggingValue + y_frac * axis.dragValueRange;
967 var minValue = maxValue - axis.dragValueRange;
968 axis.valueWindow = [ minValue, maxValue ];
969 }
970 }
971
972 g.drawGraph_();
973}
974
885c13e4
RK
975// Called in response to an interaction model operation that
976// responds to an event that ends panning.
977//
978// It's used in the default callback for "mouseup" operations.
979// Custom interaction model builders can use it to provide the default
980// panning behavior.
981//
ee672584
RK
982Dygraph.endPan = function(event, g, context) {
983 context.isPanning = false;
984 context.is2DPan = false;
985 context.draggingDate = null;
986 context.dateRange = null;
987 context.valueRange = null;
988}
989
885c13e4
RK
990// Called in response to an interaction model operation that
991// responds to an event that starts zooming.
992//
993// It's used in the default callback for "mousedown" operations.
994// Custom interaction model builders can use it to provide the default
995// zooming behavior.
996//
ee672584
RK
997Dygraph.startZoom = function(event, g, context) {
998 context.isZooming = true;
999}
1000
885c13e4
RK
1001// Called in response to an interaction model operation that
1002// responds to an event that defines zoom boundaries.
1003//
1004// It's used in the default callback for "mousemove" operations.
1005// Custom interaction model builders can use it to provide the default
1006// zooming behavior.
1007//
ee672584
RK
1008Dygraph.moveZoom = function(event, g, context) {
1009 context.dragEndX = g.dragGetX_(event, context);
1010 context.dragEndY = g.dragGetY_(event, context);
1011
1012 var xDelta = Math.abs(context.dragStartX - context.dragEndX);
1013 var yDelta = Math.abs(context.dragStartY - context.dragEndY);
1014
1015 // drag direction threshold for y axis is twice as large as x axis
1016 context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
1017
1018 g.drawZoomRect_(
1019 context.dragDirection,
1020 context.dragStartX,
1021 context.dragEndX,
1022 context.dragStartY,
1023 context.dragEndY,
1024 context.prevDragDirection,
1025 context.prevEndX,
1026 context.prevEndY);
1027
1028 context.prevEndX = context.dragEndX;
1029 context.prevEndY = context.dragEndY;
1030 context.prevDragDirection = context.dragDirection;
1031}
1032
885c13e4
RK
1033// Called in response to an interaction model operation that
1034// responds to an event that performs a zoom based on previously defined
1035// bounds..
1036//
1037// It's used in the default callback for "mouseup" operations.
1038// Custom interaction model builders can use it to provide the default
1039// zooming behavior.
1040//
ee672584
RK
1041Dygraph.endZoom = function(event, g, context) {
1042 context.isZooming = false;
1043 context.dragEndX = g.dragGetX_(event, context);
1044 context.dragEndY = g.dragGetY_(event, context);
1045 var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
1046 var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
1047
1048 if (regionWidth < 2 && regionHeight < 2 &&
1049 g.lastx_ != undefined && g.lastx_ != -1) {
1050 // TODO(danvk): pass along more info about the points, e.g. 'x'
1051 if (g.attr_('clickCallback') != null) {
1052 g.attr_('clickCallback')(event, g.lastx_, g.selPoints_);
1053 }
1054 if (g.attr_('pointClickCallback')) {
1055 // check if the click was on a particular point.
1056 var closestIdx = -1;
1057 var closestDistance = 0;
1058 for (var i = 0; i < g.selPoints_.length; i++) {
1059 var p = g.selPoints_[i];
1060 var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
1061 Math.pow(p.canvasy - context.dragEndY, 2);
1062 if (closestIdx == -1 || distance < closestDistance) {
1063 closestDistance = distance;
1064 closestIdx = i;
d58ae307 1065 }
74f09e22 1066 }
bce01b0f 1067
ee672584
RK
1068 // Allow any click within two pixels of the dot.
1069 var radius = g.attr_('highlightCircleSize') + 2;
1070 if (closestDistance <= 5 * 5) {
1071 g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]);
1072 }
6a1aa64f 1073 }
ee672584 1074 }
6a1aa64f 1075
ee672584
RK
1076 if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
1077 g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
1078 Math.max(context.dragStartX, context.dragEndX));
1079 } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
1080 g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
1081 Math.max(context.dragStartY, context.dragEndY));
1082 } else {
1083 g.canvas_.getContext("2d").clearRect(0, 0,
1084 g.canvas_.width,
1085 g.canvas_.height);
1086 }
1087 context.dragStartX = null;
1088 context.dragStartY = null;
1089}
79b3ee42 1090
0a52ab7a
RK
1091Dygraph.defaultInteractionModel = {
1092 // Track the beginning of drag events
4f0c5aa0
DV
1093 mousedown: function(event, g, context) {
1094 context.initializeMouseDown(event, g, context);
0a52ab7a 1095
4f0c5aa0
DV
1096 if (event.altKey || event.shiftKey) {
1097 Dygraph.startPan(event, g, context);
1098 } else {
1099 Dygraph.startZoom(event, g, context);
1100 }
1101 },
0a52ab7a
RK
1102
1103 // Draw zoom rectangles when the mouse is down and the user moves around
4f0c5aa0
DV
1104 mousemove: function(event, g, context) {
1105 if (context.isZooming) {
1106 Dygraph.moveZoom(event, g, context);
1107 } else if (context.isPanning) {
1108 Dygraph.movePan(event, g, context);
1109 }
1110 },
0a52ab7a 1111
4f0c5aa0
DV
1112 mouseup: function(event, g, context) {
1113 if (context.isZooming) {
1114 Dygraph.endZoom(event, g, context);
1115 } else if (context.isPanning) {
1116 Dygraph.endPan(event, g, context);
1117 }
1118 },
0a52ab7a
RK
1119
1120 // Temporarily cancel the dragging event when the mouse leaves the graph
4f0c5aa0
DV
1121 mouseout: function(event, g, context) {
1122 if (context.isZooming) {
1123 context.dragEndX = null;
1124 context.dragEndY = null;
1125 }
1126 },
0a52ab7a
RK
1127
1128 // Disable zooming out if panning.
4f0c5aa0
DV
1129 dblclick: function(event, g, context) {
1130 if (event.altKey || event.shiftKey) {
1131 return;
0a52ab7a 1132 }
4f0c5aa0
DV
1133 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1134 // friendlier to public use.
1135 g.doUnzoom_();
1136 }
0a52ab7a
RK
1137};
1138
1139Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
ee672584
RK
1140
1141/**
1142 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1143 * events.
1144 * @private
1145 */
1146Dygraph.prototype.createDragInterface_ = function() {
1147 var context = {
1148 // Tracks whether the mouse is down right now
4f0c5aa0
DV
1149 isZooming: false,
1150 isPanning: false, // is this drag part of a pan?
1151 is2DPan: false, // if so, is that pan 1- or 2-dimensional?
1152 dragStartX: null,
1153 dragStartY: null,
1154 dragEndX: null,
1155 dragEndY: null,
1156 dragDirection: null,
1157 prevEndX: null,
1158 prevEndY: null,
1159 prevDragDirection: null,
ee672584
RK
1160
1161 // TODO(danvk): update this comment
1162 // draggingDate and draggingValue represent the [date,value] point on the
1163 // graph at which the mouse was pressed. As the mouse moves while panning,
1164 // the viewport must pan so that the mouse position points to
1165 // [draggingDate, draggingValue]
4f0c5aa0 1166 draggingDate: null,
ee672584
RK
1167
1168 // TODO(danvk): update this comment
1169 // The range in second/value units that the viewport encompasses during a
1170 // panning operation.
4f0c5aa0 1171 dateRange: null,
ee672584
RK
1172
1173 // Utility function to convert page-wide coordinates to canvas coords
4f0c5aa0
DV
1174 px: 0,
1175 py: 0,
ee672584 1176
4f0c5aa0 1177 initializeMouseDown: function(event, g, context) {
885c13e4 1178 // prevents mouse drags from selecting page text.
ee672584
RK
1179 if (event.preventDefault) {
1180 event.preventDefault(); // Firefox, Chrome, etc.
1181 } else {
1182 event.returnValue = false; // IE
4f0c5aa0 1183 event.cancelBubble = true;
ee672584 1184 }
4f0c5aa0 1185
ee672584
RK
1186 context.px = Dygraph.findPosX(g.canvas_);
1187 context.py = Dygraph.findPosY(g.canvas_);
1188 context.dragStartX = g.dragGetX_(event, context);
1189 context.dragStartY = g.dragGetY_(event, context);
bce01b0f 1190 }
ee672584
RK
1191 };
1192
885c13e4
RK
1193 var interactionModel = this.attr_("interactionModel");
1194
2b188b3d
RK
1195 // Self is the graph.
1196 var self = this;
1197
1198 // Function that binds the graph and context to the handler.
1199 var bindHandler = function(handler) {
ee672584 1200 return function(event) {
2b188b3d 1201 handler(event, self, context);
ee672584
RK
1202 };
1203 };
1204
885c13e4
RK
1205 for (var eventName in interactionModel) {
1206 if (!interactionModel.hasOwnProperty(eventName)) continue;
ee672584 1207 Dygraph.addEvent(this.mouseEventElement_, eventName,
2b188b3d 1208 bindHandler(interactionModel[eventName]));
ee672584
RK
1209 }
1210
6a1aa64f
DV
1211 // If the user releases the mouse button during a drag, but not over the
1212 // canvas, then it doesn't count as a zooming action.
76171648 1213 Dygraph.addEvent(document, 'mouseup', function(event) {
ee672584
RK
1214 if (context.isZooming || context.isPanning) {
1215 context.isZooming = false;
1216 context.dragStartX = null;
1217 context.dragStartY = null;
6a1aa64f 1218 }
bce01b0f 1219
ee672584
RK
1220 if (context.isPanning) {
1221 context.isPanning = false;
1222 context.draggingDate = null;
1223 context.dateRange = null;
d58ae307 1224 for (var i = 0; i < self.axes_.length; i++) {
d63e6799
DV
1225 delete self.axes_[i].draggingValue;
1226 delete self.axes_[i].dragValueRange;
d58ae307 1227 }
bce01b0f 1228 }
6a1aa64f 1229 });
6a1aa64f
DV
1230};
1231
1232/**
1233 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1234 * up any previous zoom rectangles that were drawn. This could be optimized to
885c13e4 1235 * avoid extra redrawing, but it's tricky to avoid interactions with the status
6a1aa64f 1236 * dots.
8b83c6cc 1237 *
39b0e098
RK
1238 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1239 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
6a1aa64f
DV
1240 * @param {Number} startX The X position where the drag started, in canvas
1241 * coordinates.
1242 * @param {Number} endX The current X position of the drag, in canvas coords.
8b83c6cc
RK
1243 * @param {Number} startY The Y position where the drag started, in canvas
1244 * coordinates.
1245 * @param {Number} endY The current Y position of the drag, in canvas coords.
39b0e098 1246 * @param {Number} prevDirection the value of direction on the previous call to
8b83c6cc 1247 * this function. Used to avoid excess redrawing
6a1aa64f
DV
1248 * @param {Number} prevEndX The value of endX on the previous call to this
1249 * function. Used to avoid excess redrawing
8b83c6cc
RK
1250 * @param {Number} prevEndY The value of endY on the previous call to this
1251 * function. Used to avoid excess redrawing
6a1aa64f
DV
1252 * @private
1253 */
8b83c6cc
RK
1254Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY,
1255 prevDirection, prevEndX, prevEndY) {
6a1aa64f
DV
1256 var ctx = this.canvas_.getContext("2d");
1257
1258 // Clean up from the previous rect if necessary
39b0e098 1259 if (prevDirection == Dygraph.HORIZONTAL) {
6a1aa64f
DV
1260 ctx.clearRect(Math.min(startX, prevEndX), 0,
1261 Math.abs(startX - prevEndX), this.height_);
39b0e098 1262 } else if (prevDirection == Dygraph.VERTICAL){
8b83c6cc
RK
1263 ctx.clearRect(0, Math.min(startY, prevEndY),
1264 this.width_, Math.abs(startY - prevEndY));
6a1aa64f
DV
1265 }
1266
1267 // Draw a light-grey rectangle to show the new viewing area
39b0e098 1268 if (direction == Dygraph.HORIZONTAL) {
8b83c6cc
RK
1269 if (endX && startX) {
1270 ctx.fillStyle = "rgba(128,128,128,0.33)";
1271 ctx.fillRect(Math.min(startX, endX), 0,
1272 Math.abs(endX - startX), this.height_);
1273 }
1274 }
39b0e098 1275 if (direction == Dygraph.VERTICAL) {
8b83c6cc
RK
1276 if (endY && startY) {
1277 ctx.fillStyle = "rgba(128,128,128,0.33)";
1278 ctx.fillRect(0, Math.min(startY, endY),
1279 this.width_, Math.abs(endY - startY));
1280 }
6a1aa64f
DV
1281 }
1282};
1283
1284/**
8b83c6cc
RK
1285 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1286 * the canvas. The exact zoom window may be slightly larger if there are no data
1287 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1288 * which accepts dates that match the raw data. This function redraws the graph.
d58ae307 1289 *
6a1aa64f
DV
1290 * @param {Number} lowX The leftmost pixel value that should be visible.
1291 * @param {Number} highX The rightmost pixel value that should be visible.
1292 * @private
1293 */
8b83c6cc 1294Dygraph.prototype.doZoomX_ = function(lowX, highX) {
6a1aa64f 1295 // Find the earliest and latest dates contained in this canvasx range.
8b83c6cc 1296 // Convert the call to date ranges of the raw data.
ff022deb
RK
1297 var minDate = this.toDataXCoord(lowX);
1298 var maxDate = this.toDataXCoord(highX);
8b83c6cc
RK
1299 this.doZoomXDates_(minDate, maxDate);
1300};
6a1aa64f 1301
8b83c6cc
RK
1302/**
1303 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1304 * method with doZoomX which accepts pixel coordinates. This function redraws
1305 * the graph.
d58ae307 1306 *
8b83c6cc
RK
1307 * @param {Number} minDate The minimum date that should be visible.
1308 * @param {Number} maxDate The maximum date that should be visible.
1309 * @private
1310 */
1311Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
6a1aa64f 1312 this.dateWindow_ = [minDate, maxDate];
26ca7938 1313 this.drawGraph_();
285a6bda 1314 if (this.attr_("zoomCallback")) {
dea5c743 1315 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
8b83c6cc
RK
1316 }
1317};
1318
1319/**
1320 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
d58ae307
DV
1321 * the canvas. This function redraws the graph.
1322 *
8b83c6cc
RK
1323 * @param {Number} lowY The topmost pixel value that should be visible.
1324 * @param {Number} highY The lowest pixel value that should be visible.
1325 * @private
1326 */
1327Dygraph.prototype.doZoomY_ = function(lowY, highY) {
d58ae307
DV
1328 // Find the highest and lowest values in pixel range for each axis.
1329 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1330 // This is because pixels increase as you go down on the screen, whereas data
1331 // coordinates increase as you go up the screen.
1332 var valueRanges = [];
1333 for (var i = 0; i < this.axes_.length; i++) {
ff022deb
RK
1334 var hi = this.toDataYCoord(lowY, i);
1335 var low = this.toDataYCoord(highY, i);
1336 this.axes_[i].valueWindow = [low, hi];
1337 valueRanges.push([low, hi]);
d58ae307 1338 }
8b83c6cc 1339
66c380c4 1340 this.drawGraph_();
8b83c6cc 1341 if (this.attr_("zoomCallback")) {
d58ae307
DV
1342 var xRange = this.xAxisRange();
1343 this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
8b83c6cc
RK
1344 }
1345};
1346
1347/**
1348 * Reset the zoom to the original view coordinates. This is the same as
1349 * double-clicking on the graph.
d58ae307 1350 *
8b83c6cc
RK
1351 * @private
1352 */
1353Dygraph.prototype.doUnzoom_ = function() {
d58ae307 1354 var dirty = false;
8b83c6cc 1355 if (this.dateWindow_ != null) {
d58ae307 1356 dirty = true;
8b83c6cc
RK
1357 this.dateWindow_ = null;
1358 }
d58ae307
DV
1359
1360 for (var i = 0; i < this.axes_.length; i++) {
1361 if (this.axes_[i].valueWindow != null) {
1362 dirty = true;
1363 delete this.axes_[i].valueWindow;
1364 }
8b83c6cc
RK
1365 }
1366
1367 if (dirty) {
437c0979
RK
1368 // Putting the drawing operation before the callback because it resets
1369 // yAxisRange.
66c380c4 1370 this.drawGraph_();
8b83c6cc
RK
1371 if (this.attr_("zoomCallback")) {
1372 var minDate = this.rawData_[0][0];
1373 var maxDate = this.rawData_[this.rawData_.length - 1][0];
d58ae307 1374 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
8b83c6cc 1375 }
67e650dc 1376 }
6a1aa64f
DV
1377};
1378
1379/**
1380 * When the mouse moves in the canvas, display information about a nearby data
1381 * point and draw dots over those points in the data series. This function
1382 * takes care of cleanup of previously-drawn dots.
1383 * @param {Object} event The mousemove event from the browser.
1384 * @private
1385 */
285a6bda 1386Dygraph.prototype.mouseMove_ = function(event) {
eb7bf005 1387 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
6a1aa64f
DV
1388 var points = this.layout_.points;
1389
1390 var lastx = -1;
1391 var lasty = -1;
1392
1393 // Loop through all the points and find the date nearest to our current
1394 // location.
1395 var minDist = 1e+100;
1396 var idx = -1;
1397 for (var i = 0; i < points.length; i++) {
8a7cc60e
RK
1398 var point = points[i];
1399 if (point == null) continue;
ee672584 1400 var dist = Math.abs(point.canvasx - canvasx);
f032c51d 1401 if (dist > minDist) continue;
6a1aa64f
DV
1402 minDist = dist;
1403 idx = i;
1404 }
1405 if (idx >= 0) lastx = points[idx].xval;
1406 // Check that you can really highlight the last day's data
8a7cc60e
RK
1407 var last = points[points.length-1];
1408 if (last != null && canvasx > last.canvasx)
6a1aa64f
DV
1409 lastx = points[points.length-1].xval;
1410
1411 // Extract the points we've selected
b258a3da 1412 this.selPoints_ = [];
50360fd0 1413 var l = points.length;
416b05ad
NK
1414 if (!this.attr_("stackedGraph")) {
1415 for (var i = 0; i < l; i++) {
1416 if (points[i].xval == lastx) {
1417 this.selPoints_.push(points[i]);
1418 }
1419 }
1420 } else {
354e15ab
DE
1421 // Need to 'unstack' points starting from the bottom
1422 var cumulative_sum = 0;
416b05ad
NK
1423 for (var i = l - 1; i >= 0; i--) {
1424 if (points[i].xval == lastx) {
354e15ab 1425 var p = {}; // Clone the point since we modify it
d4139cd8
NK
1426 for (var k in points[i]) {
1427 p[k] = points[i][k];
50360fd0
NK
1428 }
1429 p.yval -= cumulative_sum;
1430 cumulative_sum += p.yval;
d4139cd8 1431 this.selPoints_.push(p);
12e4c741 1432 }
6a1aa64f 1433 }
354e15ab 1434 this.selPoints_.reverse();
6a1aa64f
DV
1435 }
1436
b258a3da 1437 if (this.attr_("highlightCallback")) {
a4c6a67c 1438 var px = this.lastx_;
dd082dda 1439 if (px !== null && lastx != px) {
344ba8c0 1440 // only fire if the selected point has changed.
2ddb1197 1441 this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
43af96e7 1442 }
12e4c741 1443 }
43af96e7 1444
239c712d
NAG
1445 // Save last x position for callbacks.
1446 this.lastx_ = lastx;
50360fd0 1447
239c712d
NAG
1448 this.updateSelection_();
1449};
b258a3da 1450
239c712d 1451/**
1903f1e4 1452 * Transforms layout_.points index into data row number.
2ddb1197 1453 * @param int layout_.points index
1903f1e4 1454 * @return int row number, or -1 if none could be found.
2ddb1197
SC
1455 * @private
1456 */
1457Dygraph.prototype.idxToRow_ = function(idx) {
1903f1e4 1458 if (idx < 0) return -1;
2ddb1197 1459
1903f1e4
DV
1460 for (var i in this.layout_.datasets) {
1461 if (idx < this.layout_.datasets[i].length) {
1462 return this.boundaryIds_[0][0]+idx;
1463 }
1464 idx -= this.layout_.datasets[i].length;
1465 }
1466 return -1;
1467};
2ddb1197
SC
1468
1469/**
239c712d
NAG
1470 * Draw dots over the selectied points in the data series. This function
1471 * takes care of cleanup of previously-drawn dots.
1472 * @private
1473 */
1474Dygraph.prototype.updateSelection_ = function() {
6a1aa64f 1475 // Clear the previously drawn vertical, if there is one
6a1aa64f
DV
1476 var ctx = this.canvas_.getContext("2d");
1477 if (this.previousVerticalX_ >= 0) {
46dde5f9
DV
1478 // Determine the maximum highlight circle size.
1479 var maxCircleSize = 0;
227b93cc
DV
1480 var labels = this.attr_('labels');
1481 for (var i = 1; i < labels.length; i++) {
1482 var r = this.attr_('highlightCircleSize', labels[i]);
46dde5f9
DV
1483 if (r > maxCircleSize) maxCircleSize = r;
1484 }
6a1aa64f 1485 var px = this.previousVerticalX_;
46dde5f9
DV
1486 ctx.clearRect(px - maxCircleSize - 1, 0,
1487 2 * maxCircleSize + 2, this.height_);
6a1aa64f
DV
1488 }
1489
584ceeaa
DV
1490 var isOK = function(x) { return x && !isNaN(x); };
1491
d160cc3b 1492 if (this.selPoints_.length > 0) {
b258a3da 1493 var canvasx = this.selPoints_[0].canvasx;
6a1aa64f
DV
1494
1495 // Set the status message to indicate the selected point(s)
239c712d 1496 var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
50360fd0 1497 var fmtFunc = this.attr_('yValueFormatter');
6a1aa64f 1498 var clen = this.colors_.length;
d160cc3b
NK
1499
1500 if (this.attr_('showLabelsOnHighlight')) {
1501 // Set the status message to indicate the selected point(s)
d160cc3b 1502 for (var i = 0; i < this.selPoints_.length; i++) {
129569a5 1503 if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
d160cc3b
NK
1504 if (!isOK(this.selPoints_[i].canvasy)) continue;
1505 if (this.attr_("labelsSeparateLines")) {
1506 replace += "<br/>";
1507 }
1508 var point = this.selPoints_[i];
8fb6dc24 1509 var c = new RGBColor(this.plotter_.colors[point.name]);
029da4b6 1510 var yval = fmtFunc(point.yval);
d160cc3b
NK
1511 replace += " <b><font color='" + c.toHex() + "'>"
1512 + point.name + "</font></b>:"
1513 + yval;
6a1aa64f 1514 }
50360fd0 1515
d160cc3b 1516 this.attr_("labelsDiv").innerHTML = replace;
6a1aa64f 1517 }
6a1aa64f 1518
6a1aa64f 1519 // Draw colored circles over the center of each selected point
43af96e7 1520 ctx.save();
b258a3da 1521 for (var i = 0; i < this.selPoints_.length; i++) {
f032c51d 1522 if (!isOK(this.selPoints_[i].canvasy)) continue;
227b93cc
DV
1523 var circleSize =
1524 this.attr_('highlightCircleSize', this.selPoints_[i].name);
6a1aa64f 1525 ctx.beginPath();
563c70ca 1526 ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
f032c51d 1527 ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
7bf6a9fe 1528 0, 2 * Math.PI, false);
6a1aa64f
DV
1529 ctx.fill();
1530 }
1531 ctx.restore();
1532
1533 this.previousVerticalX_ = canvasx;
1534 }
1535};
1536
1537/**
239c712d
NAG
1538 * Set manually set selected dots, and display information about them
1539 * @param int row number that should by highlighted
1540 * false value clears the selection
1541 * @public
1542 */
1543Dygraph.prototype.setSelection = function(row) {
1544 // Extract the points we've selected
1545 this.selPoints_ = [];
1546 var pos = 0;
50360fd0 1547
239c712d 1548 if (row !== false) {
16269f6e
NAG
1549 row = row-this.boundaryIds_[0][0];
1550 }
50360fd0 1551
16269f6e 1552 if (row !== false && row >= 0) {
239c712d 1553 for (var i in this.layout_.datasets) {
16269f6e 1554 if (row < this.layout_.datasets[i].length) {
38f33a44 1555 var point = this.layout_.points[pos+row];
1556
1557 if (this.attr_("stackedGraph")) {
8c03ba63 1558 point = this.layout_.unstackPointAtIndex(pos+row);
38f33a44 1559 }
1560
1561 this.selPoints_.push(point);
16269f6e 1562 }
239c712d
NAG
1563 pos += this.layout_.datasets[i].length;
1564 }
16269f6e 1565 }
50360fd0 1566
16269f6e 1567 if (this.selPoints_.length) {
239c712d
NAG
1568 this.lastx_ = this.selPoints_[0].xval;
1569 this.updateSelection_();
1570 } else {
1571 this.lastx_ = -1;
1572 this.clearSelection();
1573 }
1574
1575};
1576
1577/**
6a1aa64f
DV
1578 * The mouse has left the canvas. Clear out whatever artifacts remain
1579 * @param {Object} event the mouseout event from the browser.
1580 * @private
1581 */
285a6bda 1582Dygraph.prototype.mouseOut_ = function(event) {
a4c6a67c
AV
1583 if (this.attr_("unhighlightCallback")) {
1584 this.attr_("unhighlightCallback")(event);
1585 }
1586
43af96e7 1587 if (this.attr_("hideOverlayOnMouseOut")) {
239c712d 1588 this.clearSelection();
43af96e7 1589 }
6a1aa64f
DV
1590};
1591
239c712d
NAG
1592/**
1593 * Remove all selection from the canvas
1594 * @public
1595 */
1596Dygraph.prototype.clearSelection = function() {
1597 // Get rid of the overlay data
1598 var ctx = this.canvas_.getContext("2d");
1599 ctx.clearRect(0, 0, this.width_, this.height_);
1600 this.attr_("labelsDiv").innerHTML = "";
1601 this.selPoints_ = [];
1602 this.lastx_ = -1;
1603}
1604
103b7292
NAG
1605/**
1606 * Returns the number of the currently selected row
1607 * @return int row number, of -1 if nothing is selected
1608 * @public
1609 */
1610Dygraph.prototype.getSelection = function() {
1611 if (!this.selPoints_ || this.selPoints_.length < 1) {
1612 return -1;
1613 }
50360fd0 1614
103b7292
NAG
1615 for (var row=0; row<this.layout_.points.length; row++ ) {
1616 if (this.layout_.points[row].x == this.selPoints_[0].x) {
16269f6e 1617 return row + this.boundaryIds_[0][0];
103b7292
NAG
1618 }
1619 }
1620 return -1;
1621}
1622
285a6bda 1623Dygraph.zeropad = function(x) {
32988383
DV
1624 if (x < 10) return "0" + x; else return "" + x;
1625}
1626
6a1aa64f 1627/**
6b8e33dd
DV
1628 * Return a string version of the hours, minutes and seconds portion of a date.
1629 * @param {Number} date The JavaScript date (ms since epoch)
1630 * @return {String} A time of the form "HH:MM:SS"
1631 * @private
1632 */
bf640e56 1633Dygraph.hmsString_ = function(date) {
285a6bda 1634 var zeropad = Dygraph.zeropad;
6b8e33dd
DV
1635 var d = new Date(date);
1636 if (d.getSeconds()) {
1637 return zeropad(d.getHours()) + ":" +
1638 zeropad(d.getMinutes()) + ":" +
1639 zeropad(d.getSeconds());
6b8e33dd 1640 } else {
054531ca 1641 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
6b8e33dd
DV
1642 }
1643}
1644
1645/**
bf640e56
AV
1646 * Convert a JS date to a string appropriate to display on an axis that
1647 * is displaying values at the stated granularity.
1648 * @param {Date} date The date to format
1649 * @param {Number} granularity One of the Dygraph granularity constants
1650 * @return {String} The formatted date
1651 * @private
1652 */
1653Dygraph.dateAxisFormatter = function(date, granularity) {
5f70132f
DV
1654 if (granularity >= Dygraph.DECADAL) {
1655 return date.strftime('%Y');
1656 } else if (granularity >= Dygraph.MONTHLY) {
bf640e56
AV
1657 return date.strftime('%b %y');
1658 } else {
31eddad3 1659 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
bf640e56
AV
1660 if (frac == 0 || granularity >= Dygraph.DAILY) {
1661 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
1662 } else {
1663 return Dygraph.hmsString_(date.getTime());
1664 }
1665 }
1666}
1667
1668/**
6a1aa64f
DV
1669 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1670 * @param {Number} date The JavaScript date (ms since epoch)
1671 * @return {String} A date of the form "YYYY/MM/DD"
1672 * @private
1673 */
285a6bda
DV
1674Dygraph.dateString_ = function(date, self) {
1675 var zeropad = Dygraph.zeropad;
6a1aa64f
DV
1676 var d = new Date(date);
1677
1678 // Get the year:
1679 var year = "" + d.getFullYear();
1680 // Get a 0 padded month string
6b8e33dd 1681 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
6a1aa64f 1682 // Get a 0 padded day string
6b8e33dd 1683 var day = zeropad(d.getDate());
6a1aa64f 1684
6b8e33dd
DV
1685 var ret = "";
1686 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
bf640e56 1687 if (frac) ret = " " + Dygraph.hmsString_(date);
6b8e33dd
DV
1688
1689 return year + "/" + month + "/" + day + ret;
6a1aa64f
DV
1690};
1691
1692/**
1693 * Round a number to the specified number of digits past the decimal point.
1694 * @param {Number} num The number to round
1695 * @param {Number} places The number of decimals to which to round
1696 * @return {Number} The rounded number
1697 * @private
1698 */
029da4b6 1699Dygraph.round_ = function(num, places) {
6a1aa64f
DV
1700 var shift = Math.pow(10, places);
1701 return Math.round(num * shift)/shift;
1702};
1703
1704/**
1705 * Fires when there's data available to be graphed.
1706 * @param {String} data Raw CSV data to be plotted
1707 * @private
1708 */
285a6bda 1709Dygraph.prototype.loadedEvent_ = function(data) {
6a1aa64f 1710 this.rawData_ = this.parseCSV_(data);
26ca7938 1711 this.predraw_();
6a1aa64f
DV
1712};
1713
285a6bda 1714Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
8846615a 1715 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
285a6bda 1716Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
6a1aa64f
DV
1717
1718/**
1719 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1720 * @private
1721 */
285a6bda 1722Dygraph.prototype.addXTicks_ = function() {
6a1aa64f
DV
1723 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1724 var startDate, endDate;
1725 if (this.dateWindow_) {
1726 startDate = this.dateWindow_[0];
1727 endDate = this.dateWindow_[1];
1728 } else {
1729 startDate = this.rawData_[0][0];
1730 endDate = this.rawData_[this.rawData_.length - 1][0];
1731 }
1732
285a6bda 1733 var xTicks = this.attr_('xTicker')(startDate, endDate, this);
6a1aa64f 1734 this.layout_.updateOptions({xTicks: xTicks});
32988383
DV
1735};
1736
1737// Time granularity enumeration
285a6bda 1738Dygraph.SECONDLY = 0;
20a41c17
DV
1739Dygraph.TWO_SECONDLY = 1;
1740Dygraph.FIVE_SECONDLY = 2;
1741Dygraph.TEN_SECONDLY = 3;
1742Dygraph.THIRTY_SECONDLY = 4;
1743Dygraph.MINUTELY = 5;
1744Dygraph.TWO_MINUTELY = 6;
1745Dygraph.FIVE_MINUTELY = 7;
1746Dygraph.TEN_MINUTELY = 8;
1747Dygraph.THIRTY_MINUTELY = 9;
1748Dygraph.HOURLY = 10;
1749Dygraph.TWO_HOURLY = 11;
1750Dygraph.SIX_HOURLY = 12;
1751Dygraph.DAILY = 13;
1752Dygraph.WEEKLY = 14;
1753Dygraph.MONTHLY = 15;
1754Dygraph.QUARTERLY = 16;
1755Dygraph.BIANNUAL = 17;
1756Dygraph.ANNUAL = 18;
1757Dygraph.DECADAL = 19;
3db4c464 1758Dygraph.CENTENNIAL = 20;
5f70132f 1759Dygraph.NUM_GRANULARITIES = 21;
285a6bda
DV
1760
1761Dygraph.SHORT_SPACINGS = [];
1762Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
20a41c17
DV
1763Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
1764Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
285a6bda
DV
1765Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
1766Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
1767Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
20a41c17
DV
1768Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
1769Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
285a6bda
DV
1770Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
1771Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
1772Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
20a41c17 1773Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
805d5519 1774Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
285a6bda
DV
1775Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
1776Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
32988383
DV
1777
1778// NumXTicks()
1779//
1780// If we used this time granularity, how many ticks would there be?
1781// This is only an approximation, but it's generally good enough.
1782//
285a6bda
DV
1783Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1784 if (granularity < Dygraph.MONTHLY) {
32988383 1785 // Generate one tick mark for every fixed interval of time.
285a6bda 1786 var spacing = Dygraph.SHORT_SPACINGS[granularity];
32988383
DV
1787 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1788 } else {
1789 var year_mod = 1; // e.g. to only print one point every 10 years.
1790 var num_months = 12;
285a6bda
DV
1791 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1792 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1793 if (granularity == Dygraph.ANNUAL) num_months = 1;
1794 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
3db4c464 1795 if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
32988383
DV
1796
1797 var msInYear = 365.2524 * 24 * 3600 * 1000;
1798 var num_years = 1.0 * (end_time - start_time) / msInYear;
1799 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1800 }
1801};
1802
1803// GetXAxis()
1804//
1805// Construct an x-axis of nicely-formatted times on meaningful boundaries
1806// (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1807//
1808// Returns an array containing {v: millis, label: label} dictionaries.
1809//
285a6bda 1810Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
bf640e56 1811 var formatter = this.attr_("xAxisLabelFormatter");
32988383 1812 var ticks = [];
285a6bda 1813 if (granularity < Dygraph.MONTHLY) {
32988383 1814 // Generate one tick mark for every fixed interval of time.
285a6bda 1815 var spacing = Dygraph.SHORT_SPACINGS[granularity];
3d29302c 1816 var format = '%d%b'; // e.g. "1Jan"
076c9622
DV
1817
1818 // Find a time less than start_time which occurs on a "nice" time boundary
1819 // for this granularity.
1820 var g = spacing / 1000;
076c9622
DV
1821 var d = new Date(start_time);
1822 if (g <= 60) { // seconds
1823 var x = d.getSeconds(); d.setSeconds(x - x % g);
1824 } else {
1825 d.setSeconds(0);
1826 g /= 60;
1827 if (g <= 60) { // minutes
1828 var x = d.getMinutes(); d.setMinutes(x - x % g);
1829 } else {
1830 d.setMinutes(0);
1831 g /= 60;
1832
1833 if (g <= 24) { // days
1834 var x = d.getHours(); d.setHours(x - x % g);
1835 } else {
1836 d.setHours(0);
1837 g /= 24;
1838
1839 if (g == 7) { // one week
20a41c17 1840 d.setDate(d.getDate() - d.getDay());
076c9622
DV
1841 }
1842 }
1843 }
328bb812 1844 }
076c9622
DV
1845 start_time = d.getTime();
1846
32988383 1847 for (var t = start_time; t <= end_time; t += spacing) {
bf640e56 1848 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
1849 }
1850 } else {
1851 // Display a tick mark on the first of a set of months of each year.
1852 // Years get a tick mark iff y % year_mod == 0. This is useful for
1853 // displaying a tick mark once every 10 years, say, on long time scales.
1854 var months;
1855 var year_mod = 1; // e.g. to only print one point every 10 years.
1856
285a6bda 1857 if (granularity == Dygraph.MONTHLY) {
32988383 1858 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
285a6bda 1859 } else if (granularity == Dygraph.QUARTERLY) {
32988383 1860 months = [ 0, 3, 6, 9 ];
285a6bda 1861 } else if (granularity == Dygraph.BIANNUAL) {
32988383 1862 months = [ 0, 6 ];
285a6bda 1863 } else if (granularity == Dygraph.ANNUAL) {
32988383 1864 months = [ 0 ];
285a6bda 1865 } else if (granularity == Dygraph.DECADAL) {
32988383
DV
1866 months = [ 0 ];
1867 year_mod = 10;
3db4c464 1868 } else if (granularity == Dygraph.CENTENNIAL) {
5f70132f
DV
1869 months = [ 0 ];
1870 year_mod = 100;
1871 } else {
1872 this.warn("Span of dates is too long");
32988383
DV
1873 }
1874
1875 var start_year = new Date(start_time).getFullYear();
1876 var end_year = new Date(end_time).getFullYear();
285a6bda 1877 var zeropad = Dygraph.zeropad;
32988383
DV
1878 for (var i = start_year; i <= end_year; i++) {
1879 if (i % year_mod != 0) continue;
1880 for (var j = 0; j < months.length; j++) {
1881 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1882 var t = Date.parse(date_str);
1883 if (t < start_time || t > end_time) continue;
bf640e56 1884 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
32988383
DV
1885 }
1886 }
1887 }
1888
1889 return ticks;
1890};
1891
6a1aa64f
DV
1892
1893/**
1894 * Add ticks to the x-axis based on a date range.
1895 * @param {Number} startDate Start of the date window (millis since epoch)
1896 * @param {Number} endDate End of the date window (millis since epoch)
1897 * @return {Array.<Object>} Array of {label, value} tuples.
1898 * @public
1899 */
285a6bda 1900Dygraph.dateTicker = function(startDate, endDate, self) {
32988383 1901 var chosen = -1;
285a6bda
DV
1902 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1903 var num_ticks = self.NumXTicks(startDate, endDate, i);
1904 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
32988383
DV
1905 chosen = i;
1906 break;
2769de62 1907 }
6a1aa64f
DV
1908 }
1909
32988383 1910 if (chosen >= 0) {
285a6bda 1911 return self.GetXAxis(startDate, endDate, chosen);
6a1aa64f 1912 } else {
32988383 1913 // TODO(danvk): signal error.
6a1aa64f 1914 }
6a1aa64f
DV
1915};
1916
1917/**
1918 * Add ticks when the x axis has numbers on it (instead of dates)
ff022deb
RK
1919 * TODO(konigsberg): Update comment.
1920 *
6a1aa64f
DV
1921 * @param {Number} startDate Start of the date window (millis since epoch)
1922 * @param {Number} endDate End of the date window (millis since epoch)
84fc6aa7 1923 * @param self
f30cf740 1924 * @param {function} attribute accessor function.
6a1aa64f
DV
1925 * @return {Array.<Object>} Array of {label, value} tuples.
1926 * @public
1927 */
0d64e596 1928Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
70c80071
DV
1929 var attr = function(k) {
1930 if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
1931 return self.attr_(k);
1932 };
f09fc545 1933
0d64e596
DV
1934 var ticks = [];
1935 if (vals) {
1936 for (var i = 0; i < vals.length; i++) {
1937 ticks.push({v: vals[i]});
1938 }
f09e46d4 1939 } else {
ff022deb
RK
1940 if (self.attr_("logscale")) {
1941 // As opposed to the other ways for computing ticks, we're just going
1942 // for nearby values. There's no reasonable way to scale the values
1943 // (unless we want to show strings like "log(" + x + ")") in which case
1944 // x can be integer values.
1945
1946 // so compute height / pixelsPerTick and move on.
1947 var pixelsPerTick = attr('pixelsPerYLabel');
1948 var nTicks = Math.floor(self.height_ / pixelsPerTick);
1949 var vv = minV;
1950
1951 // Construct the set of ticks.
1952 for (var i = 0; i < nTicks; i++) {
1953 ticks.push( {v: vv} );
d59b6f34 1954 vv = vv * Dygraph.LOG_SCALE;
ff022deb 1955 }
f09e46d4 1956 } else {
ff022deb
RK
1957 // Basic idea:
1958 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1959 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
1960 // The first spacing greater than pixelsPerYLabel is what we use.
1961 // TODO(danvk): version that works on a log scale.
0d64e596 1962 if (attr("labelsKMG2")) {
ff022deb 1963 var mults = [1, 2, 4, 8];
0d64e596 1964 } else {
ff022deb 1965 var mults = [1, 2, 5];
0d64e596 1966 }
ff022deb
RK
1967 var scale, low_val, high_val, nTicks;
1968 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1969 var pixelsPerTick = attr('pixelsPerYLabel');
1970 for (var i = -10; i < 50; i++) {
1971 if (attr("labelsKMG2")) {
1972 var base_scale = Math.pow(16, i);
1973 } else {
1974 var base_scale = Math.pow(10, i);
1975 }
1976 for (var j = 0; j < mults.length; j++) {
1977 scale = base_scale * mults[j];
1978 low_val = Math.floor(minV / scale) * scale;
1979 high_val = Math.ceil(maxV / scale) * scale;
1980 nTicks = Math.abs(high_val - low_val) / scale;
1981 var spacing = self.height_ / nTicks;
1982 // wish I could break out of both loops at once...
1983 if (spacing > pixelsPerTick) break;
1984 }
0d64e596
DV
1985 if (spacing > pixelsPerTick) break;
1986 }
0d64e596 1987
ff022deb
RK
1988 // Construct the set of ticks.
1989 // Allow reverse y-axis if it's explicitly requested.
1990 if (low_val > high_val) scale *= -1;
1991 for (var i = 0; i < nTicks; i++) {
1992 var tickV = low_val + i * scale;
1993 ticks.push( {v: tickV} );
1994 }
0d64e596 1995 }
6a1aa64f
DV
1996 }
1997
0d64e596 1998 // Add formatted labels to the ticks.
ed11be50
DV
1999 var k;
2000 var k_labels = [];
f09fc545 2001 if (attr("labelsKMB")) {
ed11be50
DV
2002 k = 1000;
2003 k_labels = [ "K", "M", "B", "T" ];
2004 }
f09fc545 2005 if (attr("labelsKMG2")) {
ed11be50
DV
2006 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2007 k = 1024;
2008 k_labels = [ "k", "M", "G", "T" ];
2009 }
c94eee24 2010 var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
ed11be50 2011
0d64e596
DV
2012 for (var i = 0; i < ticks.length; i++) {
2013 var tickV = ticks[i].v;
0af6e346 2014 var absTickV = Math.abs(tickV);
84fc6aa7
NN
2015 var label;
2016 if (formatter != undefined) {
2017 label = formatter(tickV);
2018 } else {
2019 label = Dygraph.round_(tickV, 2);
2020 }
ed11be50
DV
2021 if (k_labels.length) {
2022 // Round up to an appropriate unit.
2023 var n = k*k*k*k;
2024 for (var j = 3; j >= 0; j--, n /= k) {
2025 if (absTickV >= n) {
029da4b6 2026 label = Dygraph.round_(tickV / n, 1) + k_labels[j];
ed11be50
DV
2027 break;
2028 }
afefbcdb 2029 }
6a1aa64f 2030 }
0d64e596 2031 ticks[i].label = label;
6a1aa64f
DV
2032 }
2033 return ticks;
2034};
2035
5011e7a1
DV
2036// Computes the range of the data series (including confidence intervals).
2037// series is either [ [x1, y1], [x2, y2], ... ] or
2038// [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2039// Returns [low, high]
2040Dygraph.prototype.extremeValues_ = function(series) {
2041 var minY = null, maxY = null;
2042
9922b78b 2043 var bars = this.attr_("errorBars") || this.attr_("customBars");
5011e7a1
DV
2044 if (bars) {
2045 // With custom bars, maxY is the max of the high values.
2046 for (var j = 0; j < series.length; j++) {
2047 var y = series[j][1][0];
2048 if (!y) continue;
2049 var low = y - series[j][1][1];
2050 var high = y + series[j][1][2];
2051 if (low > y) low = y; // this can happen with custom bars,
2052 if (high < y) high = y; // e.g. in tests/custom-bars.html
2053 if (maxY == null || high > maxY) {
2054 maxY = high;
2055 }
2056 if (minY == null || low < minY) {
2057 minY = low;
2058 }
2059 }
2060 } else {
2061 for (var j = 0; j < series.length; j++) {
2062 var y = series[j][1];
d12999d3 2063 if (y === null || isNaN(y)) continue;
5011e7a1
DV
2064 if (maxY == null || y > maxY) {
2065 maxY = y;
2066 }
2067 if (minY == null || y < minY) {
2068 minY = y;
2069 }
2070 }
2071 }
2072
2073 return [minY, maxY];
2074};
2075
6a1aa64f 2076/**
26ca7938
DV
2077 * This function is called once when the chart's data is changed or the options
2078 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2079 * idea is that values derived from the chart's data can be computed here,
2080 * rather than every time the chart is drawn. This includes things like the
2081 * number of axes, rolling averages, etc.
2082 */
2083Dygraph.prototype.predraw_ = function() {
2084 // TODO(danvk): move more computations out of drawGraph_ and into here.
2085 this.computeYAxes_();
2086
2087 // Create a new plotter.
70c80071 2088 if (this.plotter_) this.plotter_.clear();
26ca7938
DV
2089 this.plotter_ = new DygraphCanvasRenderer(this,
2090 this.hidden_, this.layout_,
2091 this.renderOptions_);
2092
0abfbd7e
DV
2093 // The roller sits in the bottom left corner of the chart. We don't know where
2094 // this will be until the options are available, so it's positioned here.
8c69de65 2095 this.createRollInterface_();
26ca7938 2096
0abfbd7e
DV
2097 // Same thing applies for the labelsDiv. It's right edge should be flush with
2098 // the right edge of the charting area (which may not be the same as the right
2099 // edge of the div, if we have two y-axes.
2100 this.positionLabelsDiv_();
2101
26ca7938
DV
2102 // If the data or options have changed, then we'd better redraw.
2103 this.drawGraph_();
2104};
2105
2106/**
2107 * Update the graph with new data. This method is called when the viewing area
2108 * has changed. If the underlying data or options have changed, predraw_ will
2109 * be called before drawGraph_ is called.
6a1aa64f
DV
2110 * @private
2111 */
26ca7938
DV
2112Dygraph.prototype.drawGraph_ = function() {
2113 var data = this.rawData_;
2114
fe0b7c03
DV
2115 // This is used to set the second parameter to drawCallback, below.
2116 var is_initial_draw = this.is_initial_draw_;
2117 this.is_initial_draw_ = false;
2118
3bd9c228 2119 var minY = null, maxY = null;
6a1aa64f 2120 this.layout_.removeAllDatasets();
285a6bda 2121 this.setColors_();
9317362d 2122 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
285a6bda 2123
354e15ab
DE
2124 // Loop over the fields (series). Go from the last to the first,
2125 // because if they're stacked that's how we accumulate the values.
43af96e7 2126
354e15ab
DE
2127 var cumulative_y = []; // For stacked series.
2128 var datasets = [];
2129
f09fc545
DV
2130 var extremes = {}; // series name -> [low, high]
2131
354e15ab
DE
2132 // Loop over all fields and create datasets
2133 for (var i = data[0].length - 1; i >= 1; i--) {
1cf11047
DV
2134 if (!this.visibility()[i - 1]) continue;
2135
f09fc545 2136 var seriesName = this.attr_("labels")[i];
450fe64b
DV
2137 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
2138
6a1aa64f
DV
2139 var series = [];
2140 for (var j = 0; j < data.length; j++) {
4a634fc7 2141 if (data[j][i] != null || !connectSeparatedPoints) {
f032c51d 2142 var date = data[j][0];
563c70ca 2143 series.push([date, data[j][i]]);
f032c51d 2144 }
6a1aa64f 2145 }
2f5e7e1a
DV
2146
2147 // TODO(danvk): move this into predraw_. It's insane to do it here.
6a1aa64f
DV
2148 series = this.rollingAverage(series, this.rollPeriod_);
2149
2150 // Prune down to the desired range, if necessary (for zooming)
1a26f3fb
DV
2151 // Because there can be lines going to points outside of the visible area,
2152 // we actually prune to visible points, plus one on either side.
9922b78b 2153 var bars = this.attr_("errorBars") || this.attr_("customBars");
6a1aa64f
DV
2154 if (this.dateWindow_) {
2155 var low = this.dateWindow_[0];
2156 var high= this.dateWindow_[1];
2157 var pruned = [];
1a26f3fb
DV
2158 // TODO(danvk): do binary search instead of linear search.
2159 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2160 var firstIdx = null, lastIdx = null;
6a1aa64f 2161 for (var k = 0; k < series.length; k++) {
1a26f3fb
DV
2162 if (series[k][0] >= low && firstIdx === null) {
2163 firstIdx = k;
2164 }
2165 if (series[k][0] <= high) {
2166 lastIdx = k;
6a1aa64f
DV
2167 }
2168 }
1a26f3fb
DV
2169 if (firstIdx === null) firstIdx = 0;
2170 if (firstIdx > 0) firstIdx--;
2171 if (lastIdx === null) lastIdx = series.length - 1;
2172 if (lastIdx < series.length - 1) lastIdx++;
16269f6e 2173 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1a26f3fb
DV
2174 for (var k = firstIdx; k <= lastIdx; k++) {
2175 pruned.push(series[k]);
6a1aa64f
DV
2176 }
2177 series = pruned;
16269f6e
NAG
2178 } else {
2179 this.boundaryIds_[i-1] = [0, series.length-1];
6a1aa64f
DV
2180 }
2181
f09fc545 2182 var seriesExtremes = this.extremeValues_(series);
5011e7a1 2183
6a1aa64f 2184 if (bars) {
354e15ab
DE
2185 for (var j=0; j<series.length; j++) {
2186 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
2187 series[j] = val;
2188 }
43af96e7 2189 } else if (this.attr_("stackedGraph")) {
43af96e7
NK
2190 var l = series.length;
2191 var actual_y;
2192 for (var j = 0; j < l; j++) {
354e15ab
DE
2193 // If one data set has a NaN, let all subsequent stacked
2194 // sets inherit the NaN -- only start at 0 for the first set.
2195 var x = series[j][0];
41b0f691 2196 if (cumulative_y[x] === undefined) {
354e15ab 2197 cumulative_y[x] = 0;
41b0f691 2198 }
43af96e7
NK
2199
2200 actual_y = series[j][1];
354e15ab 2201 cumulative_y[x] += actual_y;
43af96e7 2202
354e15ab 2203 series[j] = [x, cumulative_y[x]]
43af96e7 2204
41b0f691
DV
2205 if (cumulative_y[x] > seriesExtremes[1]) {
2206 seriesExtremes[1] = cumulative_y[x];
2207 }
2208 if (cumulative_y[x] < seriesExtremes[0]) {
2209 seriesExtremes[0] = cumulative_y[x];
2210 }
43af96e7 2211 }
6a1aa64f 2212 }
41b0f691 2213 extremes[seriesName] = seriesExtremes;
354e15ab
DE
2214
2215 datasets[i] = series;
6a1aa64f
DV
2216 }
2217
354e15ab 2218 for (var i = 1; i < datasets.length; i++) {
4523c1f6 2219 if (!this.visibility()[i - 1]) continue;
354e15ab 2220 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
43af96e7
NK
2221 }
2222
26ca7938
DV
2223 // TODO(danvk): this method doesn't need to return anything.
2224 var out = this.computeYAxisRanges_(extremes);
f09fc545
DV
2225 var axes = out[0];
2226 var seriesToAxisMap = out[1];
9012dd21 2227 this.layout_.updateOptions( { yAxes: axes,
ea4942ed 2228 seriesToAxisMap: seriesToAxisMap
9012dd21 2229 } );
f09fc545 2230
6a1aa64f
DV
2231 this.addXTicks_();
2232
2233 // Tell PlotKit to use this new data and render itself
d033ae1c 2234 this.layout_.updateOptions({dateWindow: this.dateWindow_});
6a1aa64f
DV
2235 this.layout_.evaluateWithError();
2236 this.plotter_.clear();
2237 this.plotter_.render();
f6401bf6 2238 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
2f5e7e1a 2239 this.canvas_.height);
599fb4ad
DV
2240
2241 if (this.attr_("drawCallback") !== null) {
fe0b7c03 2242 this.attr_("drawCallback")(this, is_initial_draw);
599fb4ad 2243 }
6a1aa64f
DV
2244};
2245
2246/**
26ca7938
DV
2247 * Determine properties of the y-axes which are independent of the data
2248 * currently being displayed. This includes things like the number of axes and
2249 * the style of the axes. It does not include the range of each axis and its
2250 * tick marks.
2251 * This fills in this.axes_ and this.seriesToAxisMap_.
2252 * axes_ = [ { options } ]
2253 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2254 * indices are into the axes_ array.
f09fc545 2255 */
26ca7938
DV
2256Dygraph.prototype.computeYAxes_ = function() {
2257 this.axes_ = [{}]; // always have at least one y-axis.
2258 this.seriesToAxisMap_ = {};
2259
2260 // Get a list of series names.
2261 var labels = this.attr_("labels");
1c77a3a1 2262 var series = {};
26ca7938 2263 for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
f09fc545
DV
2264
2265 // all options which could be applied per-axis:
2266 var axisOptions = [
2267 'includeZero',
2268 'valueRange',
2269 'labelsKMB',
2270 'labelsKMG2',
2271 'pixelsPerYLabel',
2272 'yAxisLabelWidth',
2273 'axisLabelFontSize',
2274 'axisTickSize'
2275 ];
2276
2277 // Copy global axis options over to the first axis.
2278 for (var i = 0; i < axisOptions.length; i++) {
2279 var k = axisOptions[i];
2280 var v = this.attr_(k);
26ca7938 2281 if (v) this.axes_[0][k] = v;
f09fc545
DV
2282 }
2283
2284 // Go through once and add all the axes.
26ca7938
DV
2285 for (var seriesName in series) {
2286 if (!series.hasOwnProperty(seriesName)) continue;
f09fc545
DV
2287 var axis = this.attr_("axis", seriesName);
2288 if (axis == null) {
26ca7938 2289 this.seriesToAxisMap_[seriesName] = 0;
f09fc545
DV
2290 continue;
2291 }
2292 if (typeof(axis) == 'object') {
2293 // Add a new axis, making a copy of its per-axis options.
2294 var opts = {};
26ca7938 2295 Dygraph.update(opts, this.axes_[0]);
f09fc545
DV
2296 Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
2297 Dygraph.update(opts, axis);
26ca7938
DV
2298 this.axes_.push(opts);
2299 this.seriesToAxisMap_[seriesName] = this.axes_.length - 1;
f09fc545
DV
2300 }
2301 }
2302
2303 // Go through one more time and assign series to an axis defined by another
2304 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
26ca7938
DV
2305 for (var seriesName in series) {
2306 if (!series.hasOwnProperty(seriesName)) continue;
f09fc545
DV
2307 var axis = this.attr_("axis", seriesName);
2308 if (typeof(axis) == 'string') {
26ca7938 2309 if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
f09fc545
DV
2310 this.error("Series " + seriesName + " wants to share a y-axis with " +
2311 "series " + axis + ", which does not define its own axis.");
2312 return null;
2313 }
26ca7938
DV
2314 var idx = this.seriesToAxisMap_[axis];
2315 this.seriesToAxisMap_[seriesName] = idx;
f09fc545
DV
2316 }
2317 }
1c77a3a1
DV
2318
2319 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2320 // this last so that hiding the first series doesn't destroy the axis
2321 // properties of the primary axis.
2322 var seriesToAxisFiltered = {};
2323 var vis = this.visibility();
2324 for (var i = 1; i < labels.length; i++) {
2325 var s = labels[i];
2326 if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
2327 }
2328 this.seriesToAxisMap_ = seriesToAxisFiltered;
26ca7938
DV
2329};
2330
2331/**
2332 * Returns the number of y-axes on the chart.
2333 * @return {Number} the number of axes.
2334 */
2335Dygraph.prototype.numAxes = function() {
2336 var last_axis = 0;
2337 for (var series in this.seriesToAxisMap_) {
2338 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2339 var idx = this.seriesToAxisMap_[series];
2340 if (idx > last_axis) last_axis = idx;
2341 }
2342 return 1 + last_axis;
2343};
2344
2345/**
2346 * Determine the value range and tick marks for each axis.
2347 * @param {Object} extremes A mapping from seriesName -> [low, high]
2348 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2349 */
2350Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2351 // Build a map from axis number -> [list of series names]
2352 var seriesForAxis = [];
2353 for (var series in this.seriesToAxisMap_) {
2354 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2355 var idx = this.seriesToAxisMap_[series];
2356 while (seriesForAxis.length <= idx) seriesForAxis.push([]);
2357 seriesForAxis[idx].push(series);
2358 }
f09fc545
DV
2359
2360 // Compute extreme values, a span and tick marks for each axis.
26ca7938 2361 for (var i = 0; i < this.axes_.length; i++) {
ff022deb 2362 var isLogScale = this.attr_("logscale");
26ca7938 2363 var axis = this.axes_[i];
d58ae307
DV
2364 if (axis.valueWindow) {
2365 // This is only set if the user has zoomed on the y-axis. It is never set
2366 // by a user. It takes precedence over axis.valueRange because, if you set
2367 // valueRange, you'd still expect to be able to pan.
2368 axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
2369 } else if (axis.valueRange) {
2370 // This is a user-set value range for this axis.
26ca7938
DV
2371 axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
2372 } else {
1c77a3a1 2373 // Calculate the extremes of extremes.
f09fc545
DV
2374 var series = seriesForAxis[i];
2375 var minY = Infinity; // extremes[series[0]][0];
2376 var maxY = -Infinity; // extremes[series[0]][1];
2377 for (var j = 0; j < series.length; j++) {
2378 minY = Math.min(extremes[series[j]][0], minY);
e3b6727e 2379 maxY = Math.max(extremes[series[j]][1], maxY);
f09fc545
DV
2380 }
2381 if (axis.includeZero && minY > 0) minY = 0;
2382
2383 // Add some padding and round up to an integer to be human-friendly.
2384 var span = maxY - minY;
2385 // special case: if we have no sense of scale, use +/-10% of the sole value.
2386 if (span == 0) { span = maxY; }
f09fc545 2387
ff022deb
RK
2388 var maxAxisY;
2389 var minAxisY;
2390 if (isLogScale) {
2391 var maxAxisY = maxY + 0.1 * span;
2392 var minAxisY = minY;
2393 } else {
2394 var maxAxisY = maxY + 0.1 * span;
2395 var minAxisY = minY - 0.1 * span;
2396
2397 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2398 if (!this.attr_("avoidMinZero")) {
2399 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2400 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2401 }
f09fc545 2402
ff022deb
RK
2403 if (this.attr_("includeZero")) {
2404 if (maxY < 0) maxAxisY = 0;
2405 if (minY > 0) minAxisY = 0;
2406 }
f09fc545
DV
2407 }
2408
26ca7938 2409 axis.computedValueRange = [minAxisY, maxAxisY];
f09fc545
DV
2410 }
2411
0d64e596
DV
2412 // Add ticks. By default, all axes inherit the tick positions of the
2413 // primary axis. However, if an axis is specifically marked as having
2414 // independent ticks, then that is permissible as well.
2415 if (i == 0 || axis.independentTicks) {
2416 axis.ticks =
2417 Dygraph.numericTicks(axis.computedValueRange[0],
2418 axis.computedValueRange[1],
2419 this,
2420 axis);
2421 } else {
2422 var p_axis = this.axes_[0];
2423 var p_ticks = p_axis.ticks;
2424 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
2425 var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
2426 var tick_values = [];
2427 for (var i = 0; i < p_ticks.length; i++) {
2428 var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
2429 var y_val = axis.computedValueRange[0] + y_frac * scale;
2430 tick_values.push(y_val);
2431 }
2432
2433 axis.ticks =
2434 Dygraph.numericTicks(axis.computedValueRange[0],
2435 axis.computedValueRange[1],
2436 this, axis, tick_values);
2437 }
f09fc545
DV
2438 }
2439
26ca7938 2440 return [this.axes_, this.seriesToAxisMap_];
f09fc545
DV
2441};
2442
2443/**
6a1aa64f
DV
2444 * Calculates the rolling average of a data set.
2445 * If originalData is [label, val], rolls the average of those.
2446 * If originalData is [label, [, it's interpreted as [value, stddev]
2447 * and the roll is returned in the same form, with appropriately reduced
2448 * stddev for each value.
2449 * Note that this is where fractional input (i.e. '5/10') is converted into
2450 * decimal values.
2451 * @param {Array} originalData The data in the appropriate format (see above)
2452 * @param {Number} rollPeriod The number of days over which to average the data
2453 */
285a6bda 2454Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
6a1aa64f
DV
2455 if (originalData.length < 2)
2456 return originalData;
2457 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
2458 var rollingData = [];
285a6bda 2459 var sigma = this.attr_("sigma");
6a1aa64f
DV
2460
2461 if (this.fractions_) {
2462 var num = 0;
2463 var den = 0; // numerator/denominator
2464 var mult = 100.0;
2465 for (var i = 0; i < originalData.length; i++) {
2466 num += originalData[i][1][0];
2467 den += originalData[i][1][1];
2468 if (i - rollPeriod >= 0) {
2469 num -= originalData[i - rollPeriod][1][0];
2470 den -= originalData[i - rollPeriod][1][1];
2471 }
2472
2473 var date = originalData[i][0];
2474 var value = den ? num / den : 0.0;
285a6bda 2475 if (this.attr_("errorBars")) {
6a1aa64f
DV
2476 if (this.wilsonInterval_) {
2477 // For more details on this confidence interval, see:
2478 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
2479 if (den) {
2480 var p = value < 0 ? 0 : value, n = den;
2481 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
2482 var denom = 1 + sigma * sigma / den;
2483 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
2484 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
2485 rollingData[i] = [date,
2486 [p * mult, (p - low) * mult, (high - p) * mult]];
2487 } else {
2488 rollingData[i] = [date, [0, 0, 0]];
2489 }
2490 } else {
2491 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
2492 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
2493 }
2494 } else {
2495 rollingData[i] = [date, mult * value];
2496 }
2497 }
9922b78b 2498 } else if (this.attr_("customBars")) {
f6885d6a
DV
2499 var low = 0;
2500 var mid = 0;
2501 var high = 0;
2502 var count = 0;
6a1aa64f
DV
2503 for (var i = 0; i < originalData.length; i++) {
2504 var data = originalData[i][1];
2505 var y = data[1];
2506 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
f6885d6a 2507
8b91c51f 2508 if (y != null && !isNaN(y)) {
49a7d0d5
DV
2509 low += data[0];
2510 mid += y;
2511 high += data[2];
2512 count += 1;
2513 }
f6885d6a
DV
2514 if (i - rollPeriod >= 0) {
2515 var prev = originalData[i - rollPeriod];
8b91c51f 2516 if (prev[1][1] != null && !isNaN(prev[1][1])) {
49a7d0d5
DV
2517 low -= prev[1][0];
2518 mid -= prev[1][1];
2519 high -= prev[1][2];
2520 count -= 1;
2521 }
f6885d6a
DV
2522 }
2523 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
2524 1.0 * (mid - low) / count,
2525 1.0 * (high - mid) / count ]];
2769de62 2526 }
6a1aa64f
DV
2527 } else {
2528 // Calculate the rolling average for the first rollPeriod - 1 points where
2529 // there is not enough data to roll over the full number of days
2530 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
285a6bda 2531 if (!this.attr_("errorBars")){
5011e7a1
DV
2532 if (rollPeriod == 1) {
2533 return originalData;
2534 }
2535
2847c1cf 2536 for (var i = 0; i < originalData.length; i++) {
6a1aa64f 2537 var sum = 0;
5011e7a1 2538 var num_ok = 0;
2847c1cf
DV
2539 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
2540 var y = originalData[j][1];
8b91c51f 2541 if (y == null || isNaN(y)) continue;
5011e7a1 2542 num_ok++;
2847c1cf 2543 sum += originalData[j][1];
6a1aa64f 2544 }
5011e7a1 2545 if (num_ok) {
2847c1cf 2546 rollingData[i] = [originalData[i][0], sum / num_ok];
5011e7a1 2547 } else {
2847c1cf 2548 rollingData[i] = [originalData[i][0], null];
5011e7a1 2549 }
6a1aa64f 2550 }
2847c1cf
DV
2551
2552 } else {
2553 for (var i = 0; i < originalData.length; i++) {
6a1aa64f
DV
2554 var sum = 0;
2555 var variance = 0;
5011e7a1 2556 var num_ok = 0;
2847c1cf 2557 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
5011e7a1 2558 var y = originalData[j][1][0];
8b91c51f 2559 if (y == null || isNaN(y)) continue;
5011e7a1 2560 num_ok++;
6a1aa64f
DV
2561 sum += originalData[j][1][0];
2562 variance += Math.pow(originalData[j][1][1], 2);
2563 }
5011e7a1
DV
2564 if (num_ok) {
2565 var stddev = Math.sqrt(variance) / num_ok;
2566 rollingData[i] = [originalData[i][0],
2567 [sum / num_ok, sigma * stddev, sigma * stddev]];
2568 } else {
2569 rollingData[i] = [originalData[i][0], [null, null, null]];
2570 }
6a1aa64f
DV
2571 }
2572 }
2573 }
2574
2575 return rollingData;
2576};
2577
2578/**
2579 * Parses a date, returning the number of milliseconds since epoch. This can be
285a6bda
DV
2580 * passed in as an xValueParser in the Dygraph constructor.
2581 * TODO(danvk): enumerate formats that this understands.
6a1aa64f
DV
2582 * @param {String} A date in YYYYMMDD format.
2583 * @return {Number} Milliseconds since epoch.
2584 * @public
2585 */
285a6bda 2586Dygraph.dateParser = function(dateStr, self) {
6a1aa64f 2587 var dateStrSlashed;
285a6bda 2588 var d;
986a5026 2589 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
6a1aa64f 2590 dateStrSlashed = dateStr.replace("-", "/", "g");
353a0294
DV
2591 while (dateStrSlashed.search("-") != -1) {
2592 dateStrSlashed = dateStrSlashed.replace("-", "/");
2593 }
285a6bda 2594 d = Date.parse(dateStrSlashed);
2769de62 2595 } else if (dateStr.length == 8) { // e.g. '20090712'
285a6bda 2596 // TODO(danvk): remove support for this format. It's confusing.
6a1aa64f
DV
2597 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
2598 + "/" + dateStr.substr(6,2);
285a6bda 2599 d = Date.parse(dateStrSlashed);
2769de62
DV
2600 } else {
2601 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2602 // "2009/07/12 12:34:56"
285a6bda
DV
2603 d = Date.parse(dateStr);
2604 }
2605
2606 if (!d || isNaN(d)) {
2607 self.error("Couldn't parse " + dateStr + " as a date");
2608 }
2609 return d;
2610};
2611
2612/**
2613 * Detects the type of the str (date or numeric) and sets the various
2614 * formatting attributes in this.attrs_ based on this type.
2615 * @param {String} str An x value.
2616 * @private
2617 */
2618Dygraph.prototype.detectTypeFromString_ = function(str) {
2619 var isDate = false;
2620 if (str.indexOf('-') >= 0 ||
2621 str.indexOf('/') >= 0 ||
2622 isNaN(parseFloat(str))) {
2623 isDate = true;
2624 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
2625 // TODO(danvk): remove support for this format.
2626 isDate = true;
2627 }
2628
2629 if (isDate) {
2630 this.attrs_.xValueFormatter = Dygraph.dateString_;
2631 this.attrs_.xValueParser = Dygraph.dateParser;
2632 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 2633 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda
DV
2634 } else {
2635 this.attrs_.xValueFormatter = function(x) { return x; };
2636 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2637 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 2638 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
6a1aa64f 2639 }
6a1aa64f
DV
2640};
2641
2642/**
2643 * Parses a string in a special csv format. We expect a csv file where each
2644 * line is a date point, and the first field in each line is the date string.
2645 * We also expect that all remaining fields represent series.
285a6bda 2646 * if the errorBars attribute is set, then interpret the fields as:
6a1aa64f
DV
2647 * date, series1, stddev1, series2, stddev2, ...
2648 * @param {Array.<Object>} data See above.
2649 * @private
285a6bda
DV
2650 *
2651 * @return Array.<Object> An array with one entry for each row. These entries
2652 * are an array of cells in that row. The first entry is the parsed x-value for
2653 * the row. The second, third, etc. are the y-values. These can take on one of
2654 * three forms, depending on the CSV and constructor parameters:
2655 * 1. numeric value
2656 * 2. [ value, stddev ]
2657 * 3. [ low value, center value, high value ]
6a1aa64f 2658 */
285a6bda 2659Dygraph.prototype.parseCSV_ = function(data) {
6a1aa64f
DV
2660 var ret = [];
2661 var lines = data.split("\n");
3d67f03b
DV
2662
2663 // Use the default delimiter or fall back to a tab if that makes sense.
2664 var delim = this.attr_('delimiter');
2665 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
2666 delim = '\t';
2667 }
2668
285a6bda 2669 var start = 0;
6a1aa64f 2670 if (this.labelsFromCSV_) {
285a6bda 2671 start = 1;
3d67f03b 2672 this.attrs_.labels = lines[0].split(delim);
6a1aa64f
DV
2673 }
2674
03b522a4
DV
2675 // Parse the x as a float or return null if it's not a number.
2676 var parseFloatOrNull = function(x) {
41333ec0 2677 var val = parseFloat(x);
1f7f664b
DV
2678 // isFinite() returns false for NaN and +/-Infinity.
2679 return isFinite(val) ? val : null;
03b522a4
DV
2680 };
2681
285a6bda
DV
2682 var xParser;
2683 var defaultParserSet = false; // attempt to auto-detect x value type
2684 var expectedCols = this.attr_("labels").length;
987840a2 2685 var outOfOrder = false;
6a1aa64f
DV
2686 for (var i = start; i < lines.length; i++) {
2687 var line = lines[i];
2688 if (line.length == 0) continue; // skip blank lines
3d67f03b
DV
2689 if (line[0] == '#') continue; // skip comment lines
2690 var inFields = line.split(delim);
285a6bda 2691 if (inFields.length < 2) continue;
6a1aa64f
DV
2692
2693 var fields = [];
285a6bda
DV
2694 if (!defaultParserSet) {
2695 this.detectTypeFromString_(inFields[0]);
2696 xParser = this.attr_("xValueParser");
2697 defaultParserSet = true;
2698 }
2699 fields[0] = xParser(inFields[0], this);
6a1aa64f
DV
2700
2701 // If fractions are expected, parse the numbers as "A/B"
2702 if (this.fractions_) {
2703 for (var j = 1; j < inFields.length; j++) {
2704 // TODO(danvk): figure out an appropriate way to flag parse errors.
2705 var vals = inFields[j].split("/");
03b522a4 2706 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
6a1aa64f 2707 }
285a6bda 2708 } else if (this.attr_("errorBars")) {
6a1aa64f
DV
2709 // If there are error bars, values are (value, stddev) pairs
2710 for (var j = 1; j < inFields.length; j += 2)
03b522a4
DV
2711 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2712 parseFloatOrNull(inFields[j + 1])];
9922b78b 2713 } else if (this.attr_("customBars")) {
6a1aa64f
DV
2714 // Bars are a low;center;high tuple
2715 for (var j = 1; j < inFields.length; j++) {
2716 var vals = inFields[j].split(";");
03b522a4
DV
2717 fields[j] = [ parseFloatOrNull(vals[0]),
2718 parseFloatOrNull(vals[1]),
2719 parseFloatOrNull(vals[2]) ];
6a1aa64f
DV
2720 }
2721 } else {
2722 // Values are just numbers
285a6bda 2723 for (var j = 1; j < inFields.length; j++) {
03b522a4 2724 fields[j] = parseFloatOrNull(inFields[j]);
285a6bda 2725 }
6a1aa64f 2726 }
987840a2
DV
2727 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2728 outOfOrder = true;
2729 }
6a1aa64f 2730 ret.push(fields);
285a6bda
DV
2731
2732 if (fields.length != expectedCols) {
2733 this.error("Number of columns in line " + i + " (" + fields.length +
2734 ") does not agree with number of labels (" + expectedCols +
2735 ") " + line);
2736 }
6a1aa64f 2737 }
987840a2
DV
2738
2739 if (outOfOrder) {
2740 this.warn("CSV is out of order; order it correctly to speed loading.");
2741 ret.sort(function(a,b) { return a[0] - b[0] });
2742 }
2743
6a1aa64f
DV
2744 return ret;
2745};
2746
2747/**
285a6bda
DV
2748 * The user has provided their data as a pre-packaged JS array. If the x values
2749 * are numeric, this is the same as dygraphs' internal format. If the x values
2750 * are dates, we need to convert them from Date objects to ms since epoch.
2751 * @param {Array.<Object>} data
2752 * @return {Array.<Object>} data with numeric x values.
2753 */
2754Dygraph.prototype.parseArray_ = function(data) {
2755 // Peek at the first x value to see if it's numeric.
2756 if (data.length == 0) {
2757 this.error("Can't plot empty data set");
2758 return null;
2759 }
2760 if (data[0].length == 0) {
2761 this.error("Data set cannot contain an empty row");
2762 return null;
2763 }
2764
2765 if (this.attr_("labels") == null) {
2766 this.warn("Using default labels. Set labels explicitly via 'labels' " +
2767 "in the options parameter");
2768 this.attrs_.labels = [ "X" ];
2769 for (var i = 1; i < data[0].length; i++) {
2770 this.attrs_.labels.push("Y" + i);
2771 }
2772 }
2773
2dda3850 2774 if (Dygraph.isDateLike(data[0][0])) {
285a6bda
DV
2775 // Some intelligent defaults for a date x-axis.
2776 this.attrs_.xValueFormatter = Dygraph.dateString_;
bf640e56 2777 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
285a6bda
DV
2778 this.attrs_.xTicker = Dygraph.dateTicker;
2779
2780 // Assume they're all dates.
e3ab7b40 2781 var parsedData = Dygraph.clone(data);
285a6bda
DV
2782 for (var i = 0; i < data.length; i++) {
2783 if (parsedData[i].length == 0) {
a323ff4a 2784 this.error("Row " + (1 + i) + " of data is empty");
285a6bda
DV
2785 return null;
2786 }
2787 if (parsedData[i][0] == null
3a909ec5
DV
2788 || typeof(parsedData[i][0].getTime) != 'function'
2789 || isNaN(parsedData[i][0].getTime())) {
be96a1f5 2790 this.error("x value in row " + (1 + i) + " is not a Date");
285a6bda
DV
2791 return null;
2792 }
2793 parsedData[i][0] = parsedData[i][0].getTime();
2794 }
2795 return parsedData;
2796 } else {
2797 // Some intelligent defaults for a numeric x-axis.
2798 this.attrs_.xValueFormatter = function(x) { return x; };
2799 this.attrs_.xTicker = Dygraph.numericTicks;
2800 return data;
2801 }
2802};
2803
2804/**
79420a1e
DV
2805 * Parses a DataTable object from gviz.
2806 * The data is expected to have a first column that is either a date or a
2807 * number. All subsequent columns must be numbers. If there is a clear mismatch
2808 * between this.xValueParser_ and the type of the first column, it will be
a685723c 2809 * fixed. Fills out rawData_.
79420a1e
DV
2810 * @param {Array.<Object>} data See above.
2811 * @private
2812 */
285a6bda 2813Dygraph.prototype.parseDataTable_ = function(data) {
79420a1e
DV
2814 var cols = data.getNumberOfColumns();
2815 var rows = data.getNumberOfRows();
2816
d955e223 2817 var indepType = data.getColumnType(0);
4440f6c8 2818 if (indepType == 'date' || indepType == 'datetime') {
285a6bda
DV
2819 this.attrs_.xValueFormatter = Dygraph.dateString_;
2820 this.attrs_.xValueParser = Dygraph.dateParser;
2821 this.attrs_.xTicker = Dygraph.dateTicker;
bf640e56 2822 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
33127159 2823 } else if (indepType == 'number') {
285a6bda
DV
2824 this.attrs_.xValueFormatter = function(x) { return x; };
2825 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2826 this.attrs_.xTicker = Dygraph.numericTicks;
bf640e56 2827 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
285a6bda 2828 } else {
987840a2
DV
2829 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2830 "column 1 of DataTable input (Got '" + indepType + "')");
79420a1e
DV
2831 return null;
2832 }
2833
a685723c
DV
2834 // Array of the column indices which contain data (and not annotations).
2835 var colIdx = [];
2836 var annotationCols = {}; // data index -> [annotation cols]
2837 var hasAnnotations = false;
2838 for (var i = 1; i < cols; i++) {
2839 var type = data.getColumnType(i);
2840 if (type == 'number') {
2841 colIdx.push(i);
2842 } else if (type == 'string' && this.attr_('displayAnnotations')) {
2843 // This is OK -- it's an annotation column.
2844 var dataIdx = colIdx[colIdx.length - 1];
2845 if (!annotationCols.hasOwnProperty(dataIdx)) {
2846 annotationCols[dataIdx] = [i];
2847 } else {
2848 annotationCols[dataIdx].push(i);
2849 }
2850 hasAnnotations = true;
2851 } else {
2852 this.error("Only 'number' is supported as a dependent type with Gviz." +
2853 " 'string' is only supported if displayAnnotations is true");
2854 }
2855 }
2856
2857 // Read column labels
2858 // TODO(danvk): add support back for errorBars
2859 var labels = [data.getColumnLabel(0)];
2860 for (var i = 0; i < colIdx.length; i++) {
2861 labels.push(data.getColumnLabel(colIdx[i]));
f9348814 2862 if (this.attr_("errorBars")) i += 1;
a685723c
DV
2863 }
2864 this.attrs_.labels = labels;
2865 cols = labels.length;
2866
79420a1e 2867 var ret = [];
987840a2 2868 var outOfOrder = false;
a685723c 2869 var annotations = [];
79420a1e
DV
2870 for (var i = 0; i < rows; i++) {
2871 var row = [];
debe4434
DV
2872 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2873 data.getValue(i, 0) === null) {
129569a5
FD
2874 this.warn("Ignoring row " + i +
2875 " of DataTable because of undefined or null first column.");
debe4434
DV
2876 continue;
2877 }
2878
c21d2c2d 2879 if (indepType == 'date' || indepType == 'datetime') {
d955e223
DV
2880 row.push(data.getValue(i, 0).getTime());
2881 } else {
2882 row.push(data.getValue(i, 0));
2883 }
3e3f84e4 2884 if (!this.attr_("errorBars")) {
a685723c
DV
2885 for (var j = 0; j < colIdx.length; j++) {
2886 var col = colIdx[j];
2887 row.push(data.getValue(i, col));
2888 if (hasAnnotations &&
2889 annotationCols.hasOwnProperty(col) &&
2890 data.getValue(i, annotationCols[col][0]) != null) {
2891 var ann = {};
2892 ann.series = data.getColumnLabel(col);
2893 ann.xval = row[0];
2894 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
2895 ann.text = '';
2896 for (var k = 0; k < annotationCols[col].length; k++) {
2897 if (k) ann.text += "\n";
2898 ann.text += data.getValue(i, annotationCols[col][k]);
2899 }
2900 annotations.push(ann);
2901 }
3e3f84e4
DV
2902 }
2903 } else {
2904 for (var j = 0; j < cols - 1; j++) {
2905 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2906 }
79420a1e 2907 }
987840a2
DV
2908 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2909 outOfOrder = true;
2910 }
1f7f664b
DV
2911
2912 // Strip out infinities, which give dygraphs problems later on.
2913 for (var j = 0; j < row.length; j++) {
2914 if (!isFinite(row[j])) row[j] = null;
2915 }
243d96e8 2916 ret.push(row);
79420a1e 2917 }
987840a2
DV
2918
2919 if (outOfOrder) {
2920 this.warn("DataTable is out of order; order it correctly to speed loading.");
2921 ret.sort(function(a,b) { return a[0] - b[0] });
2922 }
a685723c
DV
2923 this.rawData_ = ret;
2924
2925 if (annotations.length > 0) {
2926 this.setAnnotations(annotations, true);
2927 }
79420a1e
DV
2928}
2929
24e5350c 2930// These functions are all based on MochiKit.
fc80a396
DV
2931Dygraph.update = function (self, o) {
2932 if (typeof(o) != 'undefined' && o !== null) {
2933 for (var k in o) {
85b99f0b
DV
2934 if (o.hasOwnProperty(k)) {
2935 self[k] = o[k];
2936 }
fc80a396
DV
2937 }
2938 }
2939 return self;
2940};
2941
2dda3850
DV
2942Dygraph.isArrayLike = function (o) {
2943 var typ = typeof(o);
2944 if (
c21d2c2d 2945 (typ != 'object' && !(typ == 'function' &&
2dda3850
DV
2946 typeof(o.item) == 'function')) ||
2947 o === null ||
2948 typeof(o.length) != 'number' ||
2949 o.nodeType === 3
2950 ) {
2951 return false;
2952 }
2953 return true;
2954};
2955
2956Dygraph.isDateLike = function (o) {
2957 if (typeof(o) != "object" || o === null ||
2958 typeof(o.getTime) != 'function') {
2959 return false;
2960 }
2961 return true;
2962};
2963
e3ab7b40
DV
2964Dygraph.clone = function(o) {
2965 // TODO(danvk): figure out how MochiKit's version works
2966 var r = [];
2967 for (var i = 0; i < o.length; i++) {
2968 if (Dygraph.isArrayLike(o[i])) {
2969 r.push(Dygraph.clone(o[i]));
2970 } else {
2971 r.push(o[i]);
2972 }
2973 }
2974 return r;
24e5350c
DV
2975};
2976
2dda3850 2977
79420a1e 2978/**
6a1aa64f
DV
2979 * Get the CSV data. If it's in a function, call that function. If it's in a
2980 * file, do an XMLHttpRequest to get it.
2981 * @private
2982 */
285a6bda 2983Dygraph.prototype.start_ = function() {
6a1aa64f 2984 if (typeof this.file_ == 'function') {
285a6bda 2985 // CSV string. Pretend we got it via XHR.
6a1aa64f 2986 this.loadedEvent_(this.file_());
2dda3850 2987 } else if (Dygraph.isArrayLike(this.file_)) {
285a6bda 2988 this.rawData_ = this.parseArray_(this.file_);
26ca7938 2989 this.predraw_();
79420a1e
DV
2990 } else if (typeof this.file_ == 'object' &&
2991 typeof this.file_.getColumnRange == 'function') {
2992 // must be a DataTable from gviz.
a685723c 2993 this.parseDataTable_(this.file_);
26ca7938 2994 this.predraw_();
285a6bda
DV
2995 } else if (typeof this.file_ == 'string') {
2996 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2997 if (this.file_.indexOf('\n') >= 0) {
2998 this.loadedEvent_(this.file_);
2999 } else {
3000 var req = new XMLHttpRequest();
3001 var caller = this;
3002 req.onreadystatechange = function () {
3003 if (req.readyState == 4) {
3004 if (req.status == 200) {
3005 caller.loadedEvent_(req.responseText);
3006 }
6a1aa64f 3007 }
285a6bda 3008 };
6a1aa64f 3009
285a6bda
DV
3010 req.open("GET", this.file_, true);
3011 req.send(null);
3012 }
3013 } else {
3014 this.error("Unknown data format: " + (typeof this.file_));
6a1aa64f
DV
3015 }
3016};
3017
3018/**
3019 * Changes various properties of the graph. These can include:
3020 * <ul>
3021 * <li>file: changes the source data for the graph</li>
3022 * <li>errorBars: changes whether the data contains stddev</li>
3023 * </ul>
3024 * @param {Object} attrs The new properties and values
3025 */
285a6bda
DV
3026Dygraph.prototype.updateOptions = function(attrs) {
3027 // TODO(danvk): this is a mess. Rethink this function.
c65f2303 3028 if ('rollPeriod' in attrs) {
6a1aa64f
DV
3029 this.rollPeriod_ = attrs.rollPeriod;
3030 }
c65f2303 3031 if ('dateWindow' in attrs) {
6a1aa64f
DV
3032 this.dateWindow_ = attrs.dateWindow;
3033 }
450fe64b
DV
3034
3035 // TODO(danvk): validate per-series options.
46dde5f9
DV
3036 // Supported:
3037 // strokeWidth
3038 // pointSize
3039 // drawPoints
3040 // highlightCircleSize
450fe64b 3041
fc80a396 3042 Dygraph.update(this.user_attrs_, attrs);
87bb7958 3043 Dygraph.update(this.renderOptions_, attrs);
285a6bda
DV
3044
3045 this.labelsFromCSV_ = (this.attr_("labels") == null);
3046
3047 // TODO(danvk): this doesn't match the constructor logic
3048 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
5e50289f 3049 if (attrs['file']) {
6a1aa64f
DV
3050 this.file_ = attrs['file'];
3051 this.start_();
3052 } else {
26ca7938 3053 this.predraw_();
6a1aa64f
DV
3054 }
3055};
3056
3057/**
697e70b2
DV
3058 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3059 * containing div (which has presumably changed size since the dygraph was
3060 * instantiated. If the width/height are specified, the div will be resized.
964f30c6
DV
3061 *
3062 * This is far more efficient than destroying and re-instantiating a
3063 * Dygraph, since it doesn't have to reparse the underlying data.
3064 *
697e70b2
DV
3065 * @param {Number} width Width (in pixels)
3066 * @param {Number} height Height (in pixels)
3067 */
3068Dygraph.prototype.resize = function(width, height) {
e8c7ef86
DV
3069 if (this.resize_lock) {
3070 return;
3071 }
3072 this.resize_lock = true;
3073
697e70b2
DV
3074 if ((width === null) != (height === null)) {
3075 this.warn("Dygraph.resize() should be called with zero parameters or " +
3076 "two non-NULL parameters. Pretending it was zero.");
3077 width = height = null;
3078 }
3079
b16e6369 3080 // TODO(danvk): there should be a clear() method.
697e70b2 3081 this.maindiv_.innerHTML = "";
b16e6369
DV
3082 this.attrs_.labelsDiv = null;
3083
697e70b2
DV
3084 if (width) {
3085 this.maindiv_.style.width = width + "px";
3086 this.maindiv_.style.height = height + "px";
3087 this.width_ = width;
3088 this.height_ = height;
3089 } else {
3090 this.width_ = this.maindiv_.offsetWidth;
3091 this.height_ = this.maindiv_.offsetHeight;
3092 }
3093
3094 this.createInterface_();
26ca7938 3095 this.predraw_();
e8c7ef86
DV
3096
3097 this.resize_lock = false;
697e70b2
DV
3098};
3099
3100/**
6a1aa64f
DV
3101 * Adjusts the number of days in the rolling average. Updates the graph to
3102 * reflect the new averaging period.
3103 * @param {Number} length Number of days over which to average the data.
3104 */
285a6bda 3105Dygraph.prototype.adjustRoll = function(length) {
6a1aa64f 3106 this.rollPeriod_ = length;
26ca7938 3107 this.predraw_();
6a1aa64f 3108};
540d00f1 3109
f8cfec73 3110/**
1cf11047
DV
3111 * Returns a boolean array of visibility statuses.
3112 */
3113Dygraph.prototype.visibility = function() {
3114 // Do lazy-initialization, so that this happens after we know the number of
3115 // data series.
3116 if (!this.attr_("visibility")) {
f38dec01 3117 this.attrs_["visibility"] = [];
1cf11047
DV
3118 }
3119 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
f38dec01 3120 this.attr_("visibility").push(true);
1cf11047
DV
3121 }
3122 return this.attr_("visibility");
3123};
3124
3125/**
3126 * Changes the visiblity of a series.
3127 */
3128Dygraph.prototype.setVisibility = function(num, value) {
3129 var x = this.visibility();
a6c109c1 3130 if (num < 0 || num >= x.length) {
1cf11047
DV
3131 this.warn("invalid series number in setVisibility: " + num);
3132 } else {
3133 x[num] = value;
26ca7938 3134 this.predraw_();
1cf11047
DV
3135 }
3136};
3137
3138/**
5c528fa2
DV
3139 * Update the list of annotations and redraw the chart.
3140 */
a685723c 3141Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3c51ab74
DV
3142 // Only add the annotation CSS rule once we know it will be used.
3143 Dygraph.addAnnotationRule();
5c528fa2
DV
3144 this.annotations_ = ann;
3145 this.layout_.setAnnotations(this.annotations_);
a685723c 3146 if (!suppressDraw) {
26ca7938 3147 this.predraw_();
a685723c 3148 }
5c528fa2
DV
3149};
3150
3151/**
3152 * Return the list of annotations.
3153 */
3154Dygraph.prototype.annotations = function() {
3155 return this.annotations_;
3156};
3157
46dde5f9
DV
3158/**
3159 * Get the index of a series (column) given its name. The first column is the
3160 * x-axis, so the data series start with index 1.
3161 */
3162Dygraph.prototype.indexFromSetName = function(name) {
3163 var labels = this.attr_("labels");
3164 for (var i = 0; i < labels.length; i++) {
3165 if (labels[i] == name) return i;
3166 }
3167 return null;
3168};
3169
5c528fa2
DV
3170Dygraph.addAnnotationRule = function() {
3171 if (Dygraph.addedAnnotationCSS) return;
3172
5c528fa2
DV
3173 var rule = "border: 1px solid black; " +
3174 "background-color: white; " +
3175 "text-align: center;";
22186871
DV
3176
3177 var styleSheetElement = document.createElement("style");
3178 styleSheetElement.type = "text/css";
3179 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
3180
3181 // Find the first style sheet that we can access.
3182 // We may not add a rule to a style sheet from another domain for security
3183 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3184 // adds its own style sheets from google.com.
3185 for (var i = 0; i < document.styleSheets.length; i++) {
3186 if (document.styleSheets[i].disabled) continue;
3187 var mysheet = document.styleSheets[i];
3188 try {
3189 if (mysheet.insertRule) { // Firefox
3190 var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
3191 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
3192 } else if (mysheet.addRule) { // IE
3193 mysheet.addRule(".dygraphDefaultAnnotation", rule);
3194 }
3195 Dygraph.addedAnnotationCSS = true;
3196 return;
3197 } catch(err) {
3198 // Was likely a security exception.
3199 }
5c528fa2
DV
3200 }
3201
22186871 3202 this.warn("Unable to add default annotation CSS rule; display may be off.");
5c528fa2
DV
3203}
3204
3205/**
f8cfec73
DV
3206 * Create a new canvas element. This is more complex than a simple
3207 * document.createElement("canvas") because of IE and excanvas.
3208 */
3209Dygraph.createCanvas = function() {
3210 var canvas = document.createElement("canvas");
3211
3212 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
8b8f2d59 3213 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
f8cfec73
DV
3214 canvas = G_vmlCanvasManager.initElement(canvas);
3215 }
3216
3217 return canvas;
3218};
3219
540d00f1
DV
3220
3221/**
285a6bda 3222 * A wrapper around Dygraph that implements the gviz API.
540d00f1
DV
3223 * @param {Object} container The DOM object the visualization should live in.
3224 */
285a6bda 3225Dygraph.GVizChart = function(container) {
540d00f1
DV
3226 this.container = container;
3227}
3228
285a6bda 3229Dygraph.GVizChart.prototype.draw = function(data, options) {
c91f4ae8
DV
3230 // Clear out any existing dygraph.
3231 // TODO(danvk): would it make more sense to simply redraw using the current
3232 // date_graph object?
540d00f1 3233 this.container.innerHTML = '';
c91f4ae8
DV
3234 if (typeof(this.date_graph) != 'undefined') {
3235 this.date_graph.destroy();
3236 }
3237
285a6bda 3238 this.date_graph = new Dygraph(this.container, data, options);
540d00f1 3239}
285a6bda 3240
239c712d
NAG
3241/**
3242 * Google charts compatible setSelection
50360fd0 3243 * Only row selection is supported, all points in the row will be highlighted
239c712d
NAG
3244 * @param {Array} array of the selected cells
3245 * @public
3246 */
3247Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
3248 var row = false;
3249 if (selection_array.length) {
3250 row = selection_array[0].row;
3251 }
3252 this.date_graph.setSelection(row);
3253}
3254
103b7292
NAG
3255/**
3256 * Google charts compatible getSelection implementation
3257 * @return {Array} array of the selected cells
3258 * @public
3259 */
3260Dygraph.GVizChart.prototype.getSelection = function() {
3261 var selection = [];
50360fd0 3262
103b7292 3263 var row = this.date_graph.getSelection();
50360fd0 3264
103b7292 3265 if (row < 0) return selection;
50360fd0 3266
103b7292
NAG
3267 col = 1;
3268 for (var i in this.date_graph.layout_.datasets) {
3269 selection.push({row: row, column: col});
3270 col++;
3271 }
3272
3273 return selection;
3274}
3275
285a6bda
DV
3276// Older pages may still use this name.
3277DateGraph = Dygraph;