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