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