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