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