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    Date,SeriesA,SeriesB,...
 28    YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
 29    YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
 30 
 31  If the 'fractions' option is set, the input should be of the form:
 32 
 33    Date,SeriesA,SeriesB,...
 34    YYYYMMDD,A1/B1,A2/B2,...
 35    YYYYMMDD,A1/B1,A2/B2,...
 36 
 37  And error bars will be calculated automatically using a binomial distribution.
 38 
 39  For further documentation and examples, see http://dygraphs.com/
 40 
 41  */
 42 
 43 /**
 44  * Creates an interactive, zoomable chart.
 45  *
 46  * @constructor
 47  * @param {div | String} div A div or the id of a div into which to construct
 48  * the chart.
 49  * @param {String | Function} file A file containing CSV data or a function
 50  * that returns this data. The most basic expected format for each line is
 51  * "YYYY/MM/DD,val1,val2,...". For more information, see
 52  * http://dygraphs.com/data.html.
 53  * @param {Object} attrs Various other attributes, e.g. errorBars determines
 54  * whether the input data contains error ranges. For a complete list of
 55  * options, see http://dygraphs.com/options.html.
 56  */
 57 Dygraph = function(div, data, opts) {
 58   if (arguments.length > 0) {
 59     if (arguments.length == 4) {
 60       // Old versions of dygraphs took in the series labels as a constructor
 61       // parameter. This doesn't make sense anymore, but it's easy to continue
 62       // to support this usage.
 63       this.warn("Using deprecated four-argument dygraph constructor");
 64       this.__old_init__(div, data, arguments[2], arguments[3]);
 65     } else {
 66       this.__init__(div, data, opts);
 67     }
 68   }
 69 };
 70 
 71 Dygraph.NAME = "Dygraph";
 72 Dygraph.VERSION = "1.2";
 73 Dygraph.__repr__ = function() {
 74   return "[" + this.NAME + " " + this.VERSION + "]";
 75 };
 76 
 77 /**
 78  * Returns information about the Dygraph class.
 79  */
 80 Dygraph.toString = function() {
 81   return this.__repr__();
 82 };
 83 
 84 // Various default values
 85 Dygraph.DEFAULT_ROLL_PERIOD = 1;
 86 Dygraph.DEFAULT_WIDTH = 480;
 87 Dygraph.DEFAULT_HEIGHT = 320;
 88 Dygraph.AXIS_LINE_WIDTH = 0.3;
 89 
 90 Dygraph.LOG_SCALE = 10;
 91 Dygraph.LN_TEN = Math.log(Dygraph.LOG_SCALE);
 92 /**
 93  * @private
 94  */
 95 Dygraph.log10 = function(x) {
 96   return Math.log(x) / Dygraph.LN_TEN;
 97 }
 98 
 99 // Default attribute values.
100 Dygraph.DEFAULT_ATTRS = {
101   highlightCircleSize: 3,
102   pixelsPerXLabel: 60,
103   pixelsPerYLabel: 30,
104 
105   labelsDivWidth: 250,
106   labelsDivStyles: {
107     // TODO(danvk): move defaults from createStatusMessage_ here.
108   },
109   labelsSeparateLines: false,
110   labelsShowZeroValues: true,
111   labelsKMB: false,
112   labelsKMG2: false,
113   showLabelsOnHighlight: true,
114 
115   yValueFormatter: function(a,b) { return Dygraph.numberFormatter(a,b); },
116   digitsAfterDecimal: 2,
117   maxNumberWidth: 6,
118   sigFigs: null,
119 
120   strokeWidth: 1.0,
121 
122   axisTickSize: 3,
123   axisLabelFontSize: 14,
124   xAxisLabelWidth: 50,
125   yAxisLabelWidth: 50,
126   xAxisLabelFormatter: Dygraph.dateAxisFormatter,
127   rightGap: 5,
128 
129   showRoller: false,
130   xValueFormatter: Dygraph.dateString_,
131   xValueParser: Dygraph.dateParser,
132   xTicker: Dygraph.dateTicker,
133 
134   delimiter: ',',
135 
136   sigma: 2.0,
137   errorBars: false,
138   fractions: false,
139   wilsonInterval: true,  // only relevant if fractions is true
140   customBars: false,
141   fillGraph: false,
142   fillAlpha: 0.15,
143   connectSeparatedPoints: false,
144 
145   stackedGraph: false,
146   hideOverlayOnMouseOut: true,
147 
148   // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
149   legend: 'onmouseover',  // the only relevant value at the moment is 'always'.
150 
151   stepPlot: false,
152   avoidMinZero: false,
153 
154   // Sizes of the various chart labels.
155   titleHeight: 28,
156   xLabelHeight: 18,
157   yLabelWidth: 18,
158 
159   interactionModel: null  // will be set to Dygraph.defaultInteractionModel.
160 };
161 
162 // Various logging levels.
163 Dygraph.DEBUG = 1;
164 Dygraph.INFO = 2;
165 Dygraph.WARNING = 3;
166 Dygraph.ERROR = 3;
167 
168 // Directions for panning and zooming. Use bit operations when combined
169 // values are possible.
170 Dygraph.HORIZONTAL = 1;
171 Dygraph.VERTICAL = 2;
172 
173 // Used for initializing annotation CSS rules only once.
174 Dygraph.addedAnnotationCSS = false;
175 
176 /**
177  * @private
178  * Return the 2d context for a dygraph canvas.
179  *
180  * This method is only exposed for the sake of replacing the function in
181  * automated tests, e.g.
182  *
183  * var oldFunc = Dygraph.getContext();
184  * Dygraph.getContext = function(canvas) {
185  *   var realContext = oldFunc(canvas);
186  *   return new Proxy(realContext);
187  * };
188  */
189 Dygraph.getContext = function(canvas) {
190   return canvas.getContext("2d");
191 };
192 
193 Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
194   // Labels is no longer a constructor parameter, since it's typically set
195   // directly from the data source. It also conains a name for the x-axis,
196   // which the previous constructor form did not.
197   if (labels != null) {
198     var new_labels = ["Date"];
199     for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
200     Dygraph.update(attrs, { 'labels': new_labels });
201   }
202   this.__init__(div, file, attrs);
203 };
204 
205 /**
206  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
207  * and context <canvas> inside of it. See the constructor for details.
208  * on the parameters.
209  * @param {Element} div the Element to render the graph into.
210  * @param {String | Function} file Source data
211  * @param {Object} attrs Miscellaneous other options
212  * @private
213  */
214 Dygraph.prototype.__init__ = function(div, file, attrs) {
215   // Hack for IE: if we're using excanvas and the document hasn't finished
216   // loading yet (and hence may not have initialized whatever it needs to
217   // initialize), then keep calling this routine periodically until it has.
218   if (/MSIE/.test(navigator.userAgent) && !window.opera &&
219       typeof(G_vmlCanvasManager) != 'undefined' &&
220       document.readyState != 'complete') {
221     var self = this;
222     setTimeout(function() { self.__init__(div, file, attrs) }, 100);
223   }
224 
225   // Support two-argument constructor
226   if (attrs == null) { attrs = {}; }
227 
228   // Copy the important bits into the object
229   // TODO(danvk): most of these should just stay in the attrs_ dictionary.
230   this.maindiv_ = div;
231   this.file_ = file;
232   this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
233   this.previousVerticalX_ = -1;
234   this.fractions_ = attrs.fractions || false;
235   this.dateWindow_ = attrs.dateWindow || null;
236 
237   this.wilsonInterval_ = attrs.wilsonInterval || true;
238   this.is_initial_draw_ = true;
239   this.annotations_ = [];
240 
241   // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
242   this.zoomed_x_ = false;
243   this.zoomed_y_ = false;
244 
245   // Clear the div. This ensure that, if multiple dygraphs are passed the same
246   // div, then only one will be drawn.
247   div.innerHTML = "";
248 
249   // If the div isn't already sized then inherit from our attrs or
250   // give it a default size.
251   if (div.style.width == '') {
252     div.style.width = (attrs.width || Dygraph.DEFAULT_WIDTH) + "px";
253   }
254   if (div.style.height == '') {
255     div.style.height = (attrs.height || Dygraph.DEFAULT_HEIGHT) + "px";
256   }
257   this.width_ = parseInt(div.style.width, 10);
258   this.height_ = parseInt(div.style.height, 10);
259   // The div might have been specified as percent of the current window size,
260   // convert that to an appropriate number of pixels.
261   if (div.style.width.indexOf("%") == div.style.width.length - 1) {
262     this.width_ = div.offsetWidth;
263   }
264   if (div.style.height.indexOf("%") == div.style.height.length - 1) {
265     this.height_ = div.offsetHeight;
266   }
267 
268   if (this.width_ == 0) {
269     this.error("dygraph has zero width. Please specify a width in pixels.");
270   }
271   if (this.height_ == 0) {
272     this.error("dygraph has zero height. Please specify a height in pixels.");
273   }
274 
275   // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
276   if (attrs['stackedGraph']) {
277     attrs['fillGraph'] = true;
278     // TODO(nikhilk): Add any other stackedGraph checks here.
279   }
280 
281   // Dygraphs has many options, some of which interact with one another.
282   // To keep track of everything, we maintain two sets of options:
283   //
284   //  this.user_attrs_   only options explicitly set by the user.
285   //  this.attrs_        defaults, options derived from user_attrs_, data.
286   //
287   // Options are then accessed this.attr_('attr'), which first looks at
288   // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
289   // defaults without overriding behavior that the user specifically asks for.
290   this.user_attrs_ = {};
291   Dygraph.update(this.user_attrs_, attrs);
292 
293   this.attrs_ = {};
294   Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
295 
296   this.boundaryIds_ = [];
297 
298   // Make a note of whether labels will be pulled from the CSV file.
299   this.labelsFromCSV_ = (this.attr_("labels") == null);
300 
301   // Create the containing DIV and other interactive elements
302   this.createInterface_();
303 
304   this.start_();
305 };
306 
307 /**
308  * Returns the zoomed status of the chart for one or both axes.
309  *
310  * Axis is an optional parameter. Can be set to 'x' or 'y'.
311  *
312  * The zoomed status for an axis is set whenever a user zooms using the mouse
313  * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
314  * option is also specified).
315  */
316 Dygraph.prototype.isZoomed = function(axis) {
317   if (axis == null) return this.zoomed_x_ || this.zoomed_y_;
318   if (axis == 'x') return this.zoomed_x_;
319   if (axis == 'y') return this.zoomed_y_;
320   throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
321 };
322 
323 /**
324  * Returns information about the Dygraph object, including its containing ID.
325  */
326 Dygraph.prototype.toString = function() {
327   var maindiv = this.maindiv_;
328   var id = (maindiv && maindiv.id) ? maindiv.id : maindiv
329   return "[Dygraph " + id + "]";
330 }
331 
332 /**
333  * @private
334  * Returns the value of an option. This may be set by the user (either in the
335  * constructor or by calling updateOptions) or by dygraphs, and may be set to a
336  * per-series value.
337  * @param { String } name The name of the option, e.g. 'rollPeriod'.
338  * @param { String } [seriesName] The name of the series to which the option
339  * will be applied. If no per-series value of this option is available, then
340  * the global value is returned. This is optional.
341  * @return { ... } The value of the option.
342  */
343 Dygraph.prototype.attr_ = function(name, seriesName) {
344 // <REMOVE_FOR_COMBINED>
345   if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
346     this.error('Must include options reference JS for testing');
347   } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
348     this.error('Dygraphs is using property ' + name + ', which has no entry ' +
349                'in the Dygraphs.OPTIONS_REFERENCE listing.');
350     // Only log this error once.
351     Dygraph.OPTIONS_REFERENCE[name] = true;
352   }
353 // </REMOVE_FOR_COMBINED>
354   if (seriesName &&
355       typeof(this.user_attrs_[seriesName]) != 'undefined' &&
356       this.user_attrs_[seriesName] != null &&
357       typeof(this.user_attrs_[seriesName][name]) != 'undefined') {
358     return this.user_attrs_[seriesName][name];
359   } else if (typeof(this.user_attrs_[name]) != 'undefined') {
360     return this.user_attrs_[name];
361   } else if (typeof(this.attrs_[name]) != 'undefined') {
362     return this.attrs_[name];
363   } else {
364     return null;
365   }
366 };
367 
368 // TODO(danvk): any way I can get the line numbers to be this.warn call?
369 /**
370  * @private
371  * Log an error on the JS console at the given severity.
372  * @param { Integer } severity One of Dygraph.{DEBUG,INFO,WARNING,ERROR}
373  * @param { String } The message to log.
374  */
375 Dygraph.prototype.log = function(severity, message) {
376   if (typeof(console) != 'undefined') {
377     switch (severity) {
378       case Dygraph.DEBUG:
379         console.debug('dygraphs: ' + message);
380         break;
381       case Dygraph.INFO:
382         console.info('dygraphs: ' + message);
383         break;
384       case Dygraph.WARNING:
385         console.warn('dygraphs: ' + message);
386         break;
387       case Dygraph.ERROR:
388         console.error('dygraphs: ' + message);
389         break;
390     }
391   }
392 };
393 
394 /**
395  * @private
396  */
397 Dygraph.prototype.info = function(message) {
398   this.log(Dygraph.INFO, message);
399 };
400 
401 /**
402  * @private
403  */
404 Dygraph.prototype.warn = function(message) {
405   this.log(Dygraph.WARNING, message);
406 };
407 
408 /**
409  * @private
410  */
411 Dygraph.prototype.error = function(message) {
412   this.log(Dygraph.ERROR, message);
413 };
414 
415 /**
416  * Returns the current rolling period, as set by the user or an option.
417  * @return {Number} The number of points in the rolling window
418  */
419 Dygraph.prototype.rollPeriod = function() {
420   return this.rollPeriod_;
421 };
422 
423 /**
424  * Returns the currently-visible x-range. This can be affected by zooming,
425  * panning or a call to updateOptions.
426  * Returns a two-element array: [left, right].
427  * If the Dygraph has dates on the x-axis, these will be millis since epoch.
428  */
429 Dygraph.prototype.xAxisRange = function() {
430   return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
431 };
432 
433 /**
434  * Returns the lower- and upper-bound x-axis values of the
435  * data set.
436  */
437 Dygraph.prototype.xAxisExtremes = function() {
438   var left = this.rawData_[0][0];
439   var right = this.rawData_[this.rawData_.length - 1][0];
440   return [left, right];
441 };
442 
443 /**
444  * Returns the currently-visible y-range for an axis. This can be affected by
445  * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
446  * called with no arguments, returns the range of the first axis.
447  * Returns a two-element array: [bottom, top].
448  */
449 Dygraph.prototype.yAxisRange = function(idx) {
450   if (typeof(idx) == "undefined") idx = 0;
451   if (idx < 0 || idx >= this.axes_.length) return null;
452   return [ this.axes_[idx].computedValueRange[0],
453            this.axes_[idx].computedValueRange[1] ];
454 };
455 
456 /**
457  * Returns the currently-visible y-ranges for each axis. This can be affected by
458  * zooming, panning, calls to updateOptions, etc.
459  * Returns an array of [bottom, top] pairs, one for each y-axis.
460  */
461 Dygraph.prototype.yAxisRanges = function() {
462   var ret = [];
463   for (var i = 0; i < this.axes_.length; i++) {
464     ret.push(this.yAxisRange(i));
465   }
466   return ret;
467 };
468 
469 // TODO(danvk): use these functions throughout dygraphs.
470 /**
471  * Convert from data coordinates to canvas/div X/Y coordinates.
472  * If specified, do this conversion for the coordinate system of a particular
473  * axis. Uses the first axis by default.
474  * Returns a two-element array: [X, Y]
475  *
476  * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
477  * instead of toDomCoords(null, y, axis).
478  */
479 Dygraph.prototype.toDomCoords = function(x, y, axis) {
480   return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
481 };
482 
483 /**
484  * Convert from data x coordinates to canvas/div X coordinate.
485  * If specified, do this conversion for the coordinate system of a particular
486  * axis.
487  * Returns a single value or null if x is null.
488  */
489 Dygraph.prototype.toDomXCoord = function(x) {
490   if (x == null) {
491     return null;
492   };
493 
494   var area = this.plotter_.area;
495   var xRange = this.xAxisRange();
496   return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
497 }
498 
499 /**
500  * Convert from data x coordinates to canvas/div Y coordinate and optional
501  * axis. Uses the first axis by default.
502  *
503  * returns a single value or null if y is null.
504  */
505 Dygraph.prototype.toDomYCoord = function(y, axis) {
506   var pct = this.toPercentYCoord(y, axis);
507 
508   if (pct == null) {
509     return null;
510   }
511   var area = this.plotter_.area;
512   return area.y + pct * area.h;
513 }
514 
515 /**
516  * Convert from canvas/div coords to data coordinates.
517  * If specified, do this conversion for the coordinate system of a particular
518  * axis. Uses the first axis by default.
519  * Returns a two-element array: [X, Y].
520  *
521  * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
522  * instead of toDataCoords(null, y, axis).
523  */
524 Dygraph.prototype.toDataCoords = function(x, y, axis) {
525   return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
526 };
527 
528 /**
529  * Convert from canvas/div x coordinate to data coordinate.
530  *
531  * If x is null, this returns null.
532  */
533 Dygraph.prototype.toDataXCoord = function(x) {
534   if (x == null) {
535     return null;
536   }
537 
538   var area = this.plotter_.area;
539   var xRange = this.xAxisRange();
540   return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
541 };
542 
543 /**
544  * Convert from canvas/div y coord to value.
545  *
546  * If y is null, this returns null.
547  * if axis is null, this uses the first axis.
548  */
549 Dygraph.prototype.toDataYCoord = function(y, axis) {
550   if (y == null) {
551     return null;
552   }
553 
554   var area = this.plotter_.area;
555   var yRange = this.yAxisRange(axis);
556 
557   if (typeof(axis) == "undefined") axis = 0;
558   if (!this.axes_[axis].logscale) {
559     return yRange[0] + (area.h - y) / area.h * (yRange[1] - yRange[0]);
560   } else {
561     // Computing the inverse of toDomCoord.
562     var pct = (y - area.y) / area.h
563 
564     // Computing the inverse of toPercentYCoord. The function was arrived at with
565     // the following steps:
566     //
567     // Original calcuation:
568     // pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
569     //
570     // Move denominator to both sides:
571     // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
572     //
573     // subtract logr1, and take the negative value.
574     // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
575     //
576     // Swap both sides of the equation, and we can compute the log of the
577     // return value. Which means we just need to use that as the exponent in
578     // e^exponent.
579     // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
580 
581     var logr1 = Dygraph.log10(yRange[1]);
582     var exponent = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
583     var value = Math.pow(Dygraph.LOG_SCALE, exponent);
584     return value;
585   }
586 };
587 
588 /**
589  * Converts a y for an axis to a percentage from the top to the
590  * bottom of the drawing area.
591  *
592  * If the coordinate represents a value visible on the canvas, then
593  * the value will be between 0 and 1, where 0 is the top of the canvas.
594  * However, this method will return values outside the range, as
595  * values can fall outside the canvas.
596  *
597  * If y is null, this returns null.
598  * if axis is null, this uses the first axis.
599  *
600  * @param { Number } y The data y-coordinate.
601  * @param { Number } [axis] The axis number on which the data coordinate lives.
602  * @return { Number } A fraction in [0, 1] where 0 = the top edge.
603  */
604 Dygraph.prototype.toPercentYCoord = function(y, axis) {
605   if (y == null) {
606     return null;
607   }
608   if (typeof(axis) == "undefined") axis = 0;
609 
610   var area = this.plotter_.area;
611   var yRange = this.yAxisRange(axis);
612 
613   var pct;
614   if (!this.axes_[axis].logscale) {
615     // yRange[1] - y is unit distance from the bottom.
616     // yRange[1] - yRange[0] is the scale of the range.
617     // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
618     pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
619   } else {
620     var logr1 = Dygraph.log10(yRange[1]);
621     pct = (logr1 - Dygraph.log10(y)) / (logr1 - Dygraph.log10(yRange[0]));
622   }
623   return pct;
624 }
625 
626 /**
627  * Converts an x value to a percentage from the left to the right of
628  * the drawing area.
629  *
630  * If the coordinate represents a value visible on the canvas, then
631  * the value will be between 0 and 1, where 0 is the left of the canvas.
632  * However, this method will return values outside the range, as
633  * values can fall outside the canvas.
634  *
635  * If x is null, this returns null.
636  * @param { Number } x The data x-coordinate.
637  * @return { Number } A fraction in [0, 1] where 0 = the left edge.
638  */
639 Dygraph.prototype.toPercentXCoord = function(x) {
640   if (x == null) {
641     return null;
642   }
643 
644   var xRange = this.xAxisRange();
645   return (x - xRange[0]) / (xRange[1] - xRange[0]);
646 };
647 
648 /**
649  * Returns the number of columns (including the independent variable).
650  * @return { Integer } The number of columns.
651  */
652 Dygraph.prototype.numColumns = function() {
653   return this.rawData_[0].length;
654 };
655 
656 /**
657  * Returns the number of rows (excluding any header/label row).
658  * @return { Integer } The number of rows, less any header.
659  */
660 Dygraph.prototype.numRows = function() {
661   return this.rawData_.length;
662 };
663 
664 /**
665  * Returns the value in the given row and column. If the row and column exceed
666  * the bounds on the data, returns null. Also returns null if the value is
667  * missing.
668  * @param { Number} row The row number of the data (0-based). Row 0 is the
669  * first row of data, not a header row.
670  * @param { Number} col The column number of the data (0-based)
671  * @return { Number } The value in the specified cell or null if the row/col
672  * were out of range.
673  */
674 Dygraph.prototype.getValue = function(row, col) {
675   if (row < 0 || row > this.rawData_.length) return null;
676   if (col < 0 || col > this.rawData_[row].length) return null;
677 
678   return this.rawData_[row][col];
679 };
680 
681 /**
682  * @private
683  * Add an event handler. This smooths a difference between IE and the rest of
684  * the world.
685  * @param { DOM element } el The element to add the event to.
686  * @param { String } evt The name of the event, e.g. 'click' or 'mousemove'.
687  * @param { Function } fn The function to call on the event. The function takes
688  * one parameter: the event object.
689  */
690 Dygraph.addEvent = function(el, evt, fn) {
691   var normed_fn = function(e) {
692     if (!e) var e = window.event;
693     fn(e);
694   };
695   if (window.addEventListener) {  // Mozilla, Netscape, Firefox
696     el.addEventListener(evt, normed_fn, false);
697   } else {  // IE
698     el.attachEvent('on' + evt, normed_fn);
699   }
700 };
701 
702 
703 /**
704  * @private
705  * Cancels further processing of an event. This is useful to prevent default
706  * browser actions, e.g. highlighting text on a double-click.
707  * Based on the article at
708  * http://www.switchonthecode.com/tutorials/javascript-tutorial-the-scroll-wheel
709  * @param { Event } e The event whose normal behavior should be canceled.
710  */
711 Dygraph.cancelEvent = function(e) {
712   e = e ? e : window.event;
713   if (e.stopPropagation) {
714     e.stopPropagation();
715   }
716   if (e.preventDefault) {
717     e.preventDefault();
718   }
719   e.cancelBubble = true;
720   e.cancel = true;
721   e.returnValue = false;
722   return false;
723 };
724 
725 
726 /**
727  * Generates interface elements for the Dygraph: a containing div, a div to
728  * display the current point, and a textbox to adjust the rolling average
729  * period. Also creates the Renderer/Layout elements.
730  * @private
731  */
732 Dygraph.prototype.createInterface_ = function() {
733   // Create the all-enclosing graph div
734   var enclosing = this.maindiv_;
735 
736   this.graphDiv = document.createElement("div");
737   this.graphDiv.style.width = this.width_ + "px";
738   this.graphDiv.style.height = this.height_ + "px";
739   enclosing.appendChild(this.graphDiv);
740 
741   // Create the canvas for interactive parts of the chart.
742   this.canvas_ = Dygraph.createCanvas();
743   this.canvas_.style.position = "absolute";
744   this.canvas_.width = this.width_;
745   this.canvas_.height = this.height_;
746   this.canvas_.style.width = this.width_ + "px";    // for IE
747   this.canvas_.style.height = this.height_ + "px";  // for IE
748 
749   this.canvas_ctx_ = Dygraph.getContext(this.canvas_);
750 
751   // ... and for static parts of the chart.
752   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
753   this.hidden_ctx_ = Dygraph.getContext(this.hidden_);
754 
755   // The interactive parts of the graph are drawn on top of the chart.
756   this.graphDiv.appendChild(this.hidden_);
757   this.graphDiv.appendChild(this.canvas_);
758   this.mouseEventElement_ = this.canvas_;
759 
760   var dygraph = this;
761   Dygraph.addEvent(this.mouseEventElement_, 'mousemove', function(e) {
762     dygraph.mouseMove_(e);
763   });
764   Dygraph.addEvent(this.mouseEventElement_, 'mouseout', function(e) {
765     dygraph.mouseOut_(e);
766   });
767 
768   // Create the grapher
769   // TODO(danvk): why does the Layout need its own set of options?
770   this.layoutOptions_ = { 'xOriginIsZero': false };
771   Dygraph.update(this.layoutOptions_, this.attrs_);
772   Dygraph.update(this.layoutOptions_, this.user_attrs_);
773   Dygraph.update(this.layoutOptions_, {
774     'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
775 
776   this.layout_ = new DygraphLayout(this, this.layoutOptions_);
777 
778   // TODO(danvk): why does the Renderer need its own set of options?
779   this.renderOptions_ = { colorScheme: this.colors_,
780                           strokeColor: null,
781                           axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
782   Dygraph.update(this.renderOptions_, this.attrs_);
783   Dygraph.update(this.renderOptions_, this.user_attrs_);
784 
785   this.createStatusMessage_();
786   this.createDragInterface_();
787 };
788 
789 /**
790  * Detach DOM elements in the dygraph and null out all data references.
791  * Calling this when you're done with a dygraph can dramatically reduce memory
792  * usage. See, e.g., the tests/perf.html example.
793  */
794 Dygraph.prototype.destroy = function() {
795   var removeRecursive = function(node) {
796     while (node.hasChildNodes()) {
797       removeRecursive(node.firstChild);
798       node.removeChild(node.firstChild);
799     }
800   };
801   removeRecursive(this.maindiv_);
802 
803   var nullOut = function(obj) {
804     for (var n in obj) {
805       if (typeof(obj[n]) === 'object') {
806         obj[n] = null;
807       }
808     }
809   };
810 
811   // These may not all be necessary, but it can't hurt...
812   nullOut(this.layout_);
813   nullOut(this.plotter_);
814   nullOut(this);
815 };
816 
817 /**
818  * Creates the canvas on which the chart will be drawn. Only the Renderer ever
819  * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
820  * or the zoom rectangles) is done on this.canvas_.
821  * @param {Object} canvas The Dygraph canvas over which to overlay the plot
822  * @return {Object} The newly-created canvas
823  * @private
824  */
825 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
826   var h = Dygraph.createCanvas();
827   h.style.position = "absolute";
828   // TODO(danvk): h should be offset from canvas. canvas needs to include
829   // some extra area to make it easier to zoom in on the far left and far
830   // right. h needs to be precisely the plot area, so that clipping occurs.
831   h.style.top = canvas.style.top;
832   h.style.left = canvas.style.left;
833   h.width = this.width_;
834   h.height = this.height_;
835   h.style.width = this.width_ + "px";    // for IE
836   h.style.height = this.height_ + "px";  // for IE
837   return h;
838 };
839 
840 /**
841  * Convert hsv values to an rgb(r,g,b) string. Taken from MochiKit.Color. This
842  * is used to generate default series colors which are evenly spaced on the
843  * color wheel.
844  * @param { Number } hue Range is 0.0-1.0.
845  * @param { Number } saturation Range is 0.0-1.0.
846  * @param { Number } value Range is 0.0-1.0.
847  * @return { String } "rgb(r,g,b)" where r, g and b range from 0-255.
848  * @private
849  */
850 Dygraph.hsvToRGB = function (hue, saturation, value) {
851   var red;
852   var green;
853   var blue;
854   if (saturation === 0) {
855     red = value;
856     green = value;
857     blue = value;
858   } else {
859     var i = Math.floor(hue * 6);
860     var f = (hue * 6) - i;
861     var p = value * (1 - saturation);
862     var q = value * (1 - (saturation * f));
863     var t = value * (1 - (saturation * (1 - f)));
864     switch (i) {
865       case 1: red = q; green = value; blue = p; break;
866       case 2: red = p; green = value; blue = t; break;
867       case 3: red = p; green = q; blue = value; break;
868       case 4: red = t; green = p; blue = value; break;
869       case 5: red = value; green = p; blue = q; break;
870       case 6: // fall through
871       case 0: red = value; green = t; blue = p; break;
872     }
873   }
874   red = Math.floor(255 * red + 0.5);
875   green = Math.floor(255 * green + 0.5);
876   blue = Math.floor(255 * blue + 0.5);
877   return 'rgb(' + red + ',' + green + ',' + blue + ')';
878 };
879 
880 
881 /**
882  * Generate a set of distinct colors for the data series. This is done with a
883  * color wheel. Saturation/Value are customizable, and the hue is
884  * equally-spaced around the color wheel. If a custom set of colors is
885  * specified, that is used instead.
886  * @private
887  */
888 Dygraph.prototype.setColors_ = function() {
889   // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
890   // away with this.renderOptions_.
891   var num = this.attr_("labels").length - 1;
892   this.colors_ = [];
893   var colors = this.attr_('colors');
894   if (!colors) {
895     var sat = this.attr_('colorSaturation') || 1.0;
896     var val = this.attr_('colorValue') || 0.5;
897     var half = Math.ceil(num / 2);
898     for (var i = 1; i <= num; i++) {
899       if (!this.visibility()[i-1]) continue;
900       // alternate colors for high contrast.
901       var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2);
902       var hue = (1.0 * idx/ (1 + num));
903       this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
904     }
905   } else {
906     for (var i = 0; i < num; i++) {
907       if (!this.visibility()[i]) continue;
908       var colorStr = colors[i % colors.length];
909       this.colors_.push(colorStr);
910     }
911   }
912 
913   // TODO(danvk): update this w/r/t/ the new options system.
914   this.renderOptions_.colorScheme = this.colors_;
915   Dygraph.update(this.plotter_.options, this.renderOptions_);
916   Dygraph.update(this.layoutOptions_, this.user_attrs_);
917   Dygraph.update(this.layoutOptions_, this.attrs_);
918 };
919 
920 /**
921  * Return the list of colors. This is either the list of colors passed in the
922  * attributes or the autogenerated list of rgb(r,g,b) strings.
923  * @return {Array<string>} The list of colors.
924  */
925 Dygraph.prototype.getColors = function() {
926   return this.colors_;
927 };
928 
929 // The following functions are from quirksmode.org with a modification for Safari from
930 // http://blog.firetree.net/2005/07/04/javascript-find-position/
931 // http://www.quirksmode.org/js/findpos.html
932 
933 /**
934  * @private
935  */
936 Dygraph.findPosX = function(obj) {
937   var curleft = 0;
938   if(obj.offsetParent)
939     while(1)
940     {
941       curleft += obj.offsetLeft;
942       if(!obj.offsetParent)
943         break;
944       obj = obj.offsetParent;
945     }
946   else if(obj.x)
947     curleft += obj.x;
948   return curleft;
949 };
950 
951 
952 /**
953  * @private
954  */
955 Dygraph.findPosY = function(obj) {
956   var curtop = 0;
957   if(obj.offsetParent)
958     while(1)
959     {
960       curtop += obj.offsetTop;
961       if(!obj.offsetParent)
962         break;
963       obj = obj.offsetParent;
964     }
965   else if(obj.y)
966     curtop += obj.y;
967   return curtop;
968 };
969 
970 
971 /**
972  * Create the div that contains information on the selected point(s)
973  * This goes in the top right of the canvas, unless an external div has already
974  * been specified.
975  * @private
976  */
977 Dygraph.prototype.createStatusMessage_ = function() {
978   var userLabelsDiv = this.user_attrs_["labelsDiv"];
979   if (userLabelsDiv && null != userLabelsDiv
980     && (typeof(userLabelsDiv) == "string" || userLabelsDiv instanceof String)) {
981     this.user_attrs_["labelsDiv"] = document.getElementById(userLabelsDiv);
982   }
983   if (!this.attr_("labelsDiv")) {
984     var divWidth = this.attr_('labelsDivWidth');
985     var messagestyle = {
986       "position": "absolute",
987       "fontSize": "14px",
988       "zIndex": 10,
989       "width": divWidth + "px",
990       "top": "0px",
991       "left": (this.width_ - divWidth - 2) + "px",
992       "background": "white",
993       "textAlign": "left",
994       "overflow": "hidden"};
995     Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
996     var div = document.createElement("div");
997     for (var name in messagestyle) {
998       if (messagestyle.hasOwnProperty(name)) {
999         div.style[name] = messagestyle[name];
1000       }
1001     }
1002     this.graphDiv.appendChild(div);
1003     this.attrs_.labelsDiv = div;
1004   }
1005 };
1006 
1007 /**
1008  * Position the labels div so that:
1009  * - its right edge is flush with the right edge of the charting area
1010  * - its top edge is flush with the top edge of the charting area
1011  * @private
1012  */
1013 Dygraph.prototype.positionLabelsDiv_ = function() {
1014   // Don't touch a user-specified labelsDiv.
1015   if (this.user_attrs_.hasOwnProperty("labelsDiv")) return;
1016 
1017   var area = this.plotter_.area;
1018   var div = this.attr_("labelsDiv");
1019   div.style.left = area.x + area.w - this.attr_("labelsDivWidth") - 1 + "px";
1020   div.style.top = area.y + "px";
1021 };
1022 
1023 /**
1024  * Create the text box to adjust the averaging period
1025  * @private
1026  */
1027 Dygraph.prototype.createRollInterface_ = function() {
1028   // Create a roller if one doesn't exist already.
1029   if (!this.roller_) {
1030     this.roller_ = document.createElement("input");
1031     this.roller_.type = "text";
1032     this.roller_.style.display = "none";
1033     this.graphDiv.appendChild(this.roller_);
1034   }
1035 
1036   var display = this.attr_('showRoller') ? 'block' : 'none';
1037 
1038   var area = this.plotter_.area;
1039   var textAttr = { "position": "absolute",
1040                    "zIndex": 10,
1041                    "top": (area.y + area.h - 25) + "px",
1042                    "left": (area.x + 1) + "px",
1043                    "display": display
1044                   };
1045   this.roller_.size = "2";
1046   this.roller_.value = this.rollPeriod_;
1047   for (var name in textAttr) {
1048     if (textAttr.hasOwnProperty(name)) {
1049       this.roller_.style[name] = textAttr[name];
1050     }
1051   }
1052 
1053   var dygraph = this;
1054   this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); };
1055 };
1056 
1057 /**
1058  * @private
1059  * Returns the x-coordinate of the event in a coordinate system where the
1060  * top-left corner of the page (not the window) is (0,0).
1061  * Taken from MochiKit.Signal
1062  */
1063 Dygraph.pageX = function(e) {
1064   if (e.pageX) {
1065     return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
1066   } else {
1067     var de = document;
1068     var b = document.body;
1069     return e.clientX +
1070         (de.scrollLeft || b.scrollLeft) -
1071         (de.clientLeft || 0);
1072   }
1073 };
1074 
1075 /**
1076  * @private
1077  * Returns the y-coordinate of the event in a coordinate system where the
1078  * top-left corner of the page (not the window) is (0,0).
1079  * Taken from MochiKit.Signal
1080  */
1081 Dygraph.pageY = function(e) {
1082   if (e.pageY) {
1083     return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
1084   } else {
1085     var de = document;
1086     var b = document.body;
1087     return e.clientY +
1088         (de.scrollTop || b.scrollTop) -
1089         (de.clientTop || 0);
1090   }
1091 };
1092 
1093 /**
1094  * @private
1095  * Converts page the x-coordinate of the event to pixel x-coordinates on the
1096  * canvas (i.e. DOM Coords).
1097  */
1098 Dygraph.prototype.dragGetX_ = function(e, context) {
1099   return Dygraph.pageX(e) - context.px
1100 };
1101 
1102 /**
1103  * @private
1104  * Converts page the y-coordinate of the event to pixel y-coordinates on the
1105  * canvas (i.e. DOM Coords).
1106  */
1107 Dygraph.prototype.dragGetY_ = function(e, context) {
1108   return Dygraph.pageY(e) - context.py
1109 };
1110 
1111 /**
1112  * Called in response to an interaction model operation that
1113  * should start the default panning behavior.
1114  *
1115  * It's used in the default callback for "mousedown" operations.
1116  * Custom interaction model builders can use it to provide the default
1117  * panning behavior.
1118  *
1119  * @param { Event } event the event object which led to the startPan call.
1120  * @param { Dygraph} g The dygraph on which to act.
1121  * @param { Object} context The dragging context object (with
1122  * dragStartX/dragStartY/etc. properties). This function modifies the context.
1123  */
1124 Dygraph.startPan = function(event, g, context) {
1125   context.isPanning = true;
1126   var xRange = g.xAxisRange();
1127   context.dateRange = xRange[1] - xRange[0];
1128   context.initialLeftmostDate = xRange[0];
1129   context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);
1130 
1131   if (g.attr_("panEdgeFraction")) {
1132     var maxXPixelsToDraw = g.width_ * g.attr_("panEdgeFraction");
1133     var xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!
1134 
1135     var boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
1136     var boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;
1137 
1138     var boundedLeftDate = g.toDataXCoord(boundedLeftX);
1139     var boundedRightDate = g.toDataXCoord(boundedRightX);
1140     context.boundedDates = [boundedLeftDate, boundedRightDate];
1141 
1142     var boundedValues = [];
1143     var maxYPixelsToDraw = g.height_ * g.attr_("panEdgeFraction");
1144 
1145     for (var i = 0; i < g.axes_.length; i++) {
1146       var axis = g.axes_[i];
1147       var yExtremes = axis.extremeRange;
1148 
1149       var boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
1150       var boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;
1151 
1152       var boundedTopValue = g.toDataYCoord(boundedTopY);
1153       var boundedBottomValue = g.toDataYCoord(boundedBottomY);
1154 
1155       boundedValues[i] = [boundedTopValue, boundedBottomValue];
1156     }
1157     context.boundedValues = boundedValues;
1158   }
1159 
1160   // Record the range of each y-axis at the start of the drag.
1161   // If any axis has a valueRange or valueWindow, then we want a 2D pan.
1162   context.is2DPan = false;
1163   for (var i = 0; i < g.axes_.length; i++) {
1164     var axis = g.axes_[i];
1165     var yRange = g.yAxisRange(i);
1166     // TODO(konigsberg): These values should be in |context|.
1167     // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
1168     if (axis.logscale) {
1169       axis.initialTopValue = Dygraph.log10(yRange[1]);
1170       axis.dragValueRange = Dygraph.log10(yRange[1]) - Dygraph.log10(yRange[0]);
1171     } else {
1172       axis.initialTopValue = yRange[1];
1173       axis.dragValueRange = yRange[1] - yRange[0];
1174     }
1175     axis.unitsPerPixel = axis.dragValueRange / (g.plotter_.area.h - 1);
1176 
1177     // While calculating axes, set 2dpan.
1178     if (axis.valueWindow || axis.valueRange) context.is2DPan = true;
1179   }
1180 };
1181 
1182 /**
1183  * Called in response to an interaction model operation that
1184  * responds to an event that pans the view.
1185  *
1186  * It's used in the default callback for "mousemove" operations.
1187  * Custom interaction model builders can use it to provide the default
1188  * panning behavior.
1189  *
1190  * @param { Event } event the event object which led to the movePan call.
1191  * @param { Dygraph} g The dygraph on which to act.
1192  * @param { Object} context The dragging context object (with
1193  * dragStartX/dragStartY/etc. properties). This function modifies the context.
1194  */
1195 Dygraph.movePan = function(event, g, context) {
1196   context.dragEndX = g.dragGetX_(event, context);
1197   context.dragEndY = g.dragGetY_(event, context);
1198 
1199   var minDate = context.initialLeftmostDate -
1200     (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
1201   if (context.boundedDates) {
1202     minDate = Math.max(minDate, context.boundedDates[0]);
1203   }
1204   var maxDate = minDate + context.dateRange;
1205   if (context.boundedDates) {
1206     if (maxDate > context.boundedDates[1]) {
1207       // Adjust minDate, and recompute maxDate.
1208       minDate = minDate - (maxDate - context.boundedDates[1]);
1209       maxDate = minDate + context.dateRange;
1210     }
1211   }
1212 
1213   g.dateWindow_ = [minDate, maxDate];
1214 
1215   // y-axis scaling is automatic unless this is a full 2D pan.
1216   if (context.is2DPan) {
1217     // Adjust each axis appropriately.
1218     for (var i = 0; i < g.axes_.length; i++) {
1219       var axis = g.axes_[i];
1220 
1221       var pixelsDragged = context.dragEndY - context.dragStartY;
1222       var unitsDragged = pixelsDragged * axis.unitsPerPixel;
1223  
1224       var boundedValue = context.boundedValues ? context.boundedValues[i] : null;
1225 
1226       // In log scale, maxValue and minValue are the logs of those values.
1227       var maxValue = axis.initialTopValue + unitsDragged;
1228       if (boundedValue) {
1229         maxValue = Math.min(maxValue, boundedValue[1]);
1230       }
1231       var minValue = maxValue - axis.dragValueRange;
1232       if (boundedValue) {
1233         if (minValue < boundedValue[0]) {
1234           // Adjust maxValue, and recompute minValue.
1235           maxValue = maxValue - (minValue - boundedValue[0]);
1236           minValue = maxValue - axis.dragValueRange;
1237         }
1238       }
1239       if (axis.logscale) {
1240         axis.valueWindow = [ Math.pow(Dygraph.LOG_SCALE, minValue),
1241                              Math.pow(Dygraph.LOG_SCALE, maxValue) ];
1242       } else {
1243         axis.valueWindow = [ minValue, maxValue ];
1244       }
1245     }
1246   }
1247 
1248   g.drawGraph_();
1249 };
1250 
1251 /**
1252  * Called in response to an interaction model operation that
1253  * responds to an event that ends panning.
1254  *
1255  * It's used in the default callback for "mouseup" operations.
1256  * Custom interaction model builders can use it to provide the default
1257  * panning behavior.
1258  *
1259  * @param { Event } event the event object which led to the startZoom call.
1260  * @param { Dygraph} g The dygraph on which to act.
1261  * @param { Object} context The dragging context object (with
1262  * dragStartX/dragStartY/etc. properties). This function modifies the context.
1263  */
1264 Dygraph.endPan = function(event, g, context) {
1265   // TODO(konigsberg): Clear the context data from the axis.
1266   // TODO(konigsberg): mouseup should just delete the
1267   // context object, and mousedown should create a new one.
1268   context.isPanning = false;
1269   context.is2DPan = false;
1270   context.initialLeftmostDate = null;
1271   context.dateRange = null;
1272   context.valueRange = null;
1273   context.boundedDates = null;
1274   context.boundedValues = null;
1275 };
1276 
1277 /**
1278  * Called in response to an interaction model operation that
1279  * responds to an event that starts zooming.
1280  *
1281  * It's used in the default callback for "mousedown" operations.
1282  * Custom interaction model builders can use it to provide the default
1283  * zooming behavior.
1284  *
1285  * @param { Event } event the event object which led to the startZoom call.
1286  * @param { Dygraph} g The dygraph on which to act.
1287  * @param { Object} context The dragging context object (with
1288  * dragStartX/dragStartY/etc. properties). This function modifies the context.
1289  */
1290 Dygraph.startZoom = function(event, g, context) {
1291   context.isZooming = true;
1292 };
1293 
1294 /**
1295  * Called in response to an interaction model operation that
1296  * responds to an event that defines zoom boundaries.
1297  *
1298  * It's used in the default callback for "mousemove" operations.
1299  * Custom interaction model builders can use it to provide the default
1300  * zooming behavior.
1301  *
1302  * @param { Event } event the event object which led to the moveZoom call.
1303  * @param { Dygraph} g The dygraph on which to act.
1304  * @param { Object} context The dragging context object (with
1305  * dragStartX/dragStartY/etc. properties). This function modifies the context.
1306  */
1307 Dygraph.moveZoom = function(event, g, context) {
1308   context.dragEndX = g.dragGetX_(event, context);
1309   context.dragEndY = g.dragGetY_(event, context);
1310 
1311   var xDelta = Math.abs(context.dragStartX - context.dragEndX);
1312   var yDelta = Math.abs(context.dragStartY - context.dragEndY);
1313 
1314   // drag direction threshold for y axis is twice as large as x axis
1315   context.dragDirection = (xDelta < yDelta / 2) ? Dygraph.VERTICAL : Dygraph.HORIZONTAL;
1316 
1317   g.drawZoomRect_(
1318       context.dragDirection,
1319       context.dragStartX,
1320       context.dragEndX,
1321       context.dragStartY,
1322       context.dragEndY,
1323       context.prevDragDirection,
1324       context.prevEndX,
1325       context.prevEndY);
1326 
1327   context.prevEndX = context.dragEndX;
1328   context.prevEndY = context.dragEndY;
1329   context.prevDragDirection = context.dragDirection;
1330 };
1331 
1332 /**
1333  * Called in response to an interaction model operation that
1334  * responds to an event that performs a zoom based on previously defined
1335  * bounds..
1336  *
1337  * It's used in the default callback for "mouseup" operations.
1338  * Custom interaction model builders can use it to provide the default
1339  * zooming behavior.
1340  *
1341  * @param { Event } event the event object which led to the endZoom call.
1342  * @param { Dygraph} g The dygraph on which to end the zoom.
1343  * @param { Object} context The dragging context object (with
1344  * dragStartX/dragStartY/etc. properties). This function modifies the context.
1345  */
1346 Dygraph.endZoom = function(event, g, context) {
1347   // TODO(konigsberg): Refactor or rename this fn -- it deals with clicks, too.
1348   context.isZooming = false;
1349   context.dragEndX = g.dragGetX_(event, context);
1350   context.dragEndY = g.dragGetY_(event, context);
1351   var regionWidth = Math.abs(context.dragEndX - context.dragStartX);
1352   var regionHeight = Math.abs(context.dragEndY - context.dragStartY);
1353 
1354   if (regionWidth < 2 && regionHeight < 2 &&
1355       g.lastx_ != undefined && g.lastx_ != -1) {
1356     // TODO(danvk): pass along more info about the points, e.g. 'x'
1357     if (g.attr_('clickCallback') != null) {
1358       g.attr_('clickCallback')(event, g.lastx_, g.selPoints_);
1359     }
1360     if (g.attr_('pointClickCallback')) {
1361       // check if the click was on a particular point.
1362       var closestIdx = -1;
1363       var closestDistance = 0;
1364       for (var i = 0; i < g.selPoints_.length; i++) {
1365         var p = g.selPoints_[i];
1366         var distance = Math.pow(p.canvasx - context.dragEndX, 2) +
1367                        Math.pow(p.canvasy - context.dragEndY, 2);
1368         if (closestIdx == -1 || distance < closestDistance) {
1369           closestDistance = distance;
1370           closestIdx = i;
1371         }
1372       }
1373 
1374       // Allow any click within two pixels of the dot.
1375       var radius = g.attr_('highlightCircleSize') + 2;
1376       if (closestDistance <= 5 * 5) {
1377         g.attr_('pointClickCallback')(event, g.selPoints_[closestIdx]);
1378       }
1379     }
1380   }
1381 
1382   if (regionWidth >= 10 && context.dragDirection == Dygraph.HORIZONTAL) {
1383     g.doZoomX_(Math.min(context.dragStartX, context.dragEndX),
1384                Math.max(context.dragStartX, context.dragEndX));
1385   } else if (regionHeight >= 10 && context.dragDirection == Dygraph.VERTICAL) {
1386     g.doZoomY_(Math.min(context.dragStartY, context.dragEndY),
1387                Math.max(context.dragStartY, context.dragEndY));
1388   } else {
1389     g.canvas_ctx_.clearRect(0, 0, g.canvas_.width, g.canvas_.height);
1390   }
1391   context.dragStartX = null;
1392   context.dragStartY = null;
1393 };
1394 
1395 /**
1396  * Default interation model for dygraphs. You can refer to specific elements of
1397  * this when constructing your own interaction model, e.g.:
1398  * g.updateOptions( {
1399  *   interactionModel: {
1400  *     mousedown: Dygraph.defaultInteractionModel.mousedown
1401  *   }
1402  * } );
1403  */
1404 Dygraph.defaultInteractionModel = {
1405   // Track the beginning of drag events
1406   mousedown: function(event, g, context) {
1407     context.initializeMouseDown(event, g, context);
1408 
1409     if (event.altKey || event.shiftKey) {
1410       Dygraph.startPan(event, g, context);
1411     } else {
1412       Dygraph.startZoom(event, g, context);
1413     }
1414   },
1415 
1416   // Draw zoom rectangles when the mouse is down and the user moves around
1417   mousemove: function(event, g, context) {
1418     if (context.isZooming) {
1419       Dygraph.moveZoom(event, g, context);
1420     } else if (context.isPanning) {
1421       Dygraph.movePan(event, g, context);
1422     }
1423   },
1424 
1425   mouseup: function(event, g, context) {
1426     if (context.isZooming) {
1427       Dygraph.endZoom(event, g, context);
1428     } else if (context.isPanning) {
1429       Dygraph.endPan(event, g, context);
1430     }
1431   },
1432 
1433   // Temporarily cancel the dragging event when the mouse leaves the graph
1434   mouseout: function(event, g, context) {
1435     if (context.isZooming) {
1436       context.dragEndX = null;
1437       context.dragEndY = null;
1438     }
1439   },
1440 
1441   // Disable zooming out if panning.
1442   dblclick: function(event, g, context) {
1443     if (event.altKey || event.shiftKey) {
1444       return;
1445     }
1446     // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1447     // friendlier to public use.
1448     g.doUnzoom_();
1449   }
1450 };
1451 
1452 Dygraph.DEFAULT_ATTRS.interactionModel = Dygraph.defaultInteractionModel;
1453 
1454 /**
1455  * Set up all the mouse handlers needed to capture dragging behavior for zoom
1456  * events.
1457  * @private
1458  */
1459 Dygraph.prototype.createDragInterface_ = function() {
1460   var context = {
1461     // Tracks whether the mouse is down right now
1462     isZooming: false,
1463     isPanning: false,  // is this drag part of a pan?
1464     is2DPan: false,    // if so, is that pan 1- or 2-dimensional?
1465     dragStartX: null,
1466     dragStartY: null,
1467     dragEndX: null,
1468     dragEndY: null,
1469     dragDirection: null,
1470     prevEndX: null,
1471     prevEndY: null,
1472     prevDragDirection: null,
1473 
1474     // The value on the left side of the graph when a pan operation starts.
1475     initialLeftmostDate: null,
1476 
1477     // The number of units each pixel spans. (This won't be valid for log
1478     // scales)
1479     xUnitsPerPixel: null,
1480 
1481     // TODO(danvk): update this comment
1482     // The range in second/value units that the viewport encompasses during a
1483     // panning operation.
1484     dateRange: null,
1485 
1486     // Utility function to convert page-wide coordinates to canvas coords
1487     px: 0,
1488     py: 0,
1489 
1490     // Values for use with panEdgeFraction, which limit how far outside the
1491     // graph's data boundaries it can be panned.
1492     boundedDates: null, // [minDate, maxDate]
1493     boundedValues: null, // [[minValue, maxValue] ...]
1494 
1495     initializeMouseDown: function(event, g, context) {
1496       // prevents mouse drags from selecting page text.
1497       if (event.preventDefault) {
1498         event.preventDefault();  // Firefox, Chrome, etc.
1499       } else {
1500         event.returnValue = false;  // IE
1501         event.cancelBubble = true;
1502       }
1503 
1504       context.px = Dygraph.findPosX(g.canvas_);
1505       context.py = Dygraph.findPosY(g.canvas_);
1506       context.dragStartX = g.dragGetX_(event, context);
1507       context.dragStartY = g.dragGetY_(event, context);
1508     }
1509   };
1510 
1511   var interactionModel = this.attr_("interactionModel");
1512 
1513   // Self is the graph.
1514   var self = this;
1515 
1516   // Function that binds the graph and context to the handler.
1517   var bindHandler = function(handler) {
1518     return function(event) {
1519       handler(event, self, context);
1520     };
1521   };
1522 
1523   for (var eventName in interactionModel) {
1524     if (!interactionModel.hasOwnProperty(eventName)) continue;
1525     Dygraph.addEvent(this.mouseEventElement_, eventName,
1526         bindHandler(interactionModel[eventName]));
1527   }
1528 
1529   // If the user releases the mouse button during a drag, but not over the
1530   // canvas, then it doesn't count as a zooming action.
1531   Dygraph.addEvent(document, 'mouseup', function(event) {
1532     if (context.isZooming || context.isPanning) {
1533       context.isZooming = false;
1534       context.dragStartX = null;
1535       context.dragStartY = null;
1536     }
1537 
1538     if (context.isPanning) {
1539       context.isPanning = false;
1540       context.draggingDate = null;
1541       context.dateRange = null;
1542       for (var i = 0; i < self.axes_.length; i++) {
1543         delete self.axes_[i].draggingValue;
1544         delete self.axes_[i].dragValueRange;
1545       }
1546     }
1547   });
1548 };
1549 
1550 
1551 /**
1552  * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1553  * up any previous zoom rectangles that were drawn. This could be optimized to
1554  * avoid extra redrawing, but it's tricky to avoid interactions with the status
1555  * dots.
1556  * 
1557  * @param {Number} direction the direction of the zoom rectangle. Acceptable
1558  * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1559  * @param {Number} startX The X position where the drag started, in canvas
1560  * coordinates.
1561  * @param {Number} endX The current X position of the drag, in canvas coords.
1562  * @param {Number} startY The Y position where the drag started, in canvas
1563  * coordinates.
1564  * @param {Number} endY The current Y position of the drag, in canvas coords.
1565  * @param {Number} prevDirection the value of direction on the previous call to
1566  * this function. Used to avoid excess redrawing
1567  * @param {Number} prevEndX The value of endX on the previous call to this
1568  * function. Used to avoid excess redrawing
1569  * @param {Number} prevEndY The value of endY on the previous call to this
1570  * function. Used to avoid excess redrawing
1571  * @private
1572  */
1573 Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
1574                                            endY, prevDirection, prevEndX,
1575                                            prevEndY) {
1576   var ctx = this.canvas_ctx_;
1577 
1578   // Clean up from the previous rect if necessary
1579   if (prevDirection == Dygraph.HORIZONTAL) {
1580     ctx.clearRect(Math.min(startX, prevEndX), 0,
1581                   Math.abs(startX - prevEndX), this.height_);
1582   } else if (prevDirection == Dygraph.VERTICAL){
1583     ctx.clearRect(0, Math.min(startY, prevEndY),
1584                   this.width_, Math.abs(startY - prevEndY));
1585   }
1586 
1587   // Draw a light-grey rectangle to show the new viewing area
1588   if (direction == Dygraph.HORIZONTAL) {
1589     if (endX && startX) {
1590       ctx.fillStyle = "rgba(128,128,128,0.33)";
1591       ctx.fillRect(Math.min(startX, endX), 0,
1592                    Math.abs(endX - startX), this.height_);
1593     }
1594   }
1595   if (direction == Dygraph.VERTICAL) {
1596     if (endY && startY) {
1597       ctx.fillStyle = "rgba(128,128,128,0.33)";
1598       ctx.fillRect(0, Math.min(startY, endY),
1599                    this.width_, Math.abs(endY - startY));
1600     }
1601   }
1602 };
1603 
1604 /**
1605  * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1606  * the canvas. The exact zoom window may be slightly larger if there are no data
1607  * points near lowX or highX. Don't confuse this function with doZoomXDates,
1608  * which accepts dates that match the raw data. This function redraws the graph.
1609  *
1610  * @param {Number} lowX The leftmost pixel value that should be visible.
1611  * @param {Number} highX The rightmost pixel value that should be visible.
1612  * @private
1613  */
1614 Dygraph.prototype.doZoomX_ = function(lowX, highX) {
1615   // Find the earliest and latest dates contained in this canvasx range.
1616   // Convert the call to date ranges of the raw data.
1617   var minDate = this.toDataXCoord(lowX);
1618   var maxDate = this.toDataXCoord(highX);
1619   this.doZoomXDates_(minDate, maxDate);
1620 };
1621 
1622 /**
1623  * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1624  * method with doZoomX which accepts pixel coordinates. This function redraws
1625  * the graph.
1626  *
1627  * @param {Number} minDate The minimum date that should be visible.
1628  * @param {Number} maxDate The maximum date that should be visible.
1629  * @private
1630  */
1631 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
1632   this.dateWindow_ = [minDate, maxDate];
1633   this.zoomed_x_ = true;
1634   this.drawGraph_();
1635   if (this.attr_("zoomCallback")) {
1636     this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
1637   }
1638 };
1639 
1640 /**
1641  * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1642  * the canvas. This function redraws the graph.
1643  *
1644  * @param {Number} lowY The topmost pixel value that should be visible.
1645  * @param {Number} highY The lowest pixel value that should be visible.
1646  * @private
1647  */
1648 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
1649   // Find the highest and lowest values in pixel range for each axis.
1650   // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1651   // This is because pixels increase as you go down on the screen, whereas data
1652   // coordinates increase as you go up the screen.
1653   var valueRanges = [];
1654   for (var i = 0; i < this.axes_.length; i++) {
1655     var hi = this.toDataYCoord(lowY, i);
1656     var low = this.toDataYCoord(highY, i);
1657     this.axes_[i].valueWindow = [low, hi];
1658     valueRanges.push([low, hi]);
1659   }
1660 
1661   this.zoomed_y_ = true;
1662   this.drawGraph_();
1663   if (this.attr_("zoomCallback")) {
1664     var xRange = this.xAxisRange();
1665     var yRange = this.yAxisRange();
1666     this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
1667   }
1668 };
1669 
1670 /**
1671  * Reset the zoom to the original view coordinates. This is the same as
1672  * double-clicking on the graph.
1673  *
1674  * @private
1675  */
1676 Dygraph.prototype.doUnzoom_ = function() {
1677   var dirty = false;
1678   if (this.dateWindow_ != null) {
1679     dirty = true;
1680     this.dateWindow_ = null;
1681   }
1682 
1683   for (var i = 0; i < this.axes_.length; i++) {
1684     if (this.axes_[i].valueWindow != null) {
1685       dirty = true;
1686       delete this.axes_[i].valueWindow;
1687     }
1688   }
1689 
1690   if (dirty) {
1691     // Putting the drawing operation before the callback because it resets
1692     // yAxisRange.
1693     this.zoomed_x_ = false;
1694     this.zoomed_y_ = false;
1695     this.drawGraph_();
1696     if (this.attr_("zoomCallback")) {
1697       var minDate = this.rawData_[0][0];
1698       var maxDate = this.rawData_[this.rawData_.length - 1][0];
1699       this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
1700     }
1701   }
1702 };
1703 
1704 /**
1705  * When the mouse moves in the canvas, display information about a nearby data
1706  * point and draw dots over those points in the data series. This function
1707  * takes care of cleanup of previously-drawn dots.
1708  * @param {Object} event The mousemove event from the browser.
1709  * @private
1710  */
1711 Dygraph.prototype.mouseMove_ = function(event) {
1712   // This prevents JS errors when mousing over the canvas before data loads.
1713   var points = this.layout_.points;
1714   if (points === undefined) return;
1715 
1716   var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
1717 
1718   var lastx = -1;
1719   var lasty = -1;
1720 
1721   // Loop through all the points and find the date nearest to our current
1722   // location.
1723   var minDist = 1e+100;
1724   var idx = -1;
1725   for (var i = 0; i < points.length; i++) {
1726     var point = points[i];
1727     if (point == null) continue;
1728     var dist = Math.abs(point.canvasx - canvasx);
1729     if (dist > minDist) continue;
1730     minDist = dist;
1731     idx = i;
1732   }
1733   if (idx >= 0) lastx = points[idx].xval;
1734 
1735   // Extract the points we've selected
1736   this.selPoints_ = [];
1737   var l = points.length;
1738   if (!this.attr_("stackedGraph")) {
1739     for (var i = 0; i < l; i++) {
1740       if (points[i].xval == lastx) {
1741         this.selPoints_.push(points[i]);
1742       }
1743     }
1744   } else {
1745     // Need to 'unstack' points starting from the bottom
1746     var cumulative_sum = 0;
1747     for (var i = l - 1; i >= 0; i--) {
1748       if (points[i].xval == lastx) {
1749         var p = {};  // Clone the point since we modify it
1750         for (var k in points[i]) {
1751           p[k] = points[i][k];
1752         }
1753         p.yval -= cumulative_sum;
1754         cumulative_sum += p.yval;
1755         this.selPoints_.push(p);
1756       }
1757     }
1758     this.selPoints_.reverse();
1759   }
1760 
1761   if (this.attr_("highlightCallback")) {
1762     var px = this.lastx_;
1763     if (px !== null && lastx != px) {
1764       // only fire if the selected point has changed.
1765       this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
1766     }
1767   }
1768 
1769   // Save last x position for callbacks.
1770   this.lastx_ = lastx;
1771 
1772   this.updateSelection_();
1773 };
1774 
1775 /**
1776  * Transforms layout_.points index into data row number.
1777  * @param int layout_.points index
1778  * @return int row number, or -1 if none could be found.
1779  * @private
1780  */
1781 Dygraph.prototype.idxToRow_ = function(idx) {
1782   if (idx < 0) return -1;
1783 
1784   for (var i in this.layout_.datasets) {
1785     if (idx < this.layout_.datasets[i].length) {
1786       return this.boundaryIds_[0][0]+idx;
1787     }
1788     idx -= this.layout_.datasets[i].length;
1789   }
1790   return -1;
1791 };
1792 
1793 /**
1794  * @private
1795  * @param { Number } x The number to consider.
1796  * @return { Boolean } Whether the number is zero or NaN.
1797  */
1798 // TODO(danvk): rename this function to something like 'isNonZeroNan'.
1799 Dygraph.isOK = function(x) {
1800   return x && !isNaN(x);
1801 };
1802 
1803 /**
1804  * @private
1805  * Generates HTML for the legend which is displayed when hovering over the
1806  * chart. If no selected points are specified, a default legend is returned
1807  * (this may just be the empty string).
1808  * @param { Number } [x] The x-value of the selected points.
1809  * @param { [Object] } [sel_points] List of selected points for the given
1810  * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1811  */
1812 Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) {
1813   // If no points are selected, we display a default legend. Traditionally,
1814   // this has been blank. But a better default would be a conventional legend,
1815   // which provides essential information for a non-interactive chart.
1816   if (typeof(x) === 'undefined') {
1817     if (this.attr_('legend') != 'always') return '';
1818 
1819     var sepLines = this.attr_('labelsSeparateLines');
1820     var labels = this.attr_('labels');
1821     var html = '';
1822     for (var i = 1; i < labels.length; i++) {
1823       if (!this.visibility()[i - 1]) continue;
1824       var c = this.plotter_.colors[labels[i]];
1825       if (html != '') html += (sepLines ? '<br/>' : ' ');
1826       html += "<b><span style='color: " + c + ";'>—" + labels[i] +
1827         "</span></b>";
1828     }
1829     return html;
1830   }
1831 
1832   var html = this.attr_('xValueFormatter')(x) + ":";
1833 
1834   var fmtFunc = this.attr_('yValueFormatter');
1835   var showZeros = this.attr_("labelsShowZeroValues");
1836   var sepLines = this.attr_("labelsSeparateLines");
1837   for (var i = 0; i < this.selPoints_.length; i++) {
1838     var pt = this.selPoints_[i];
1839     if (pt.yval == 0 && !showZeros) continue;
1840     if (!Dygraph.isOK(pt.canvasy)) continue;
1841     if (sepLines) html += "<br/>";
1842 
1843     var c = this.plotter_.colors[pt.name];
1844     var yval = fmtFunc(pt.yval, this);
1845     // TODO(danvk): use a template string here and make it an attribute.
1846     html += " <b><span style='color: " + c + ";'>"
1847       + pt.name + "</span></b>:"
1848       + yval;
1849   }
1850   return html;
1851 };
1852 
1853 /**
1854  * @private
1855  * Displays information about the selected points in the legend. If there is no
1856  * selection, the legend will be cleared.
1857  * @param { Number } [x] The x-value of the selected points.
1858  * @param { [Object] } [sel_points] List of selected points for the given
1859  * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1860  */
1861 Dygraph.prototype.setLegendHTML_ = function(x, sel_points) {
1862   var html = this.generateLegendHTML_(x, sel_points);
1863   var labelsDiv = this.attr_("labelsDiv");
1864   if (labelsDiv !== null) {
1865     labelsDiv.innerHTML = html;
1866   } else {
1867     if (typeof(this.shown_legend_error_) == 'undefined') {
1868       this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
1869       this.shown_legend_error_ = true;
1870     }
1871   }
1872 };
1873 
1874 /**
1875  * Draw dots over the selectied points in the data series. This function
1876  * takes care of cleanup of previously-drawn dots.
1877  * @private
1878  */
1879 Dygraph.prototype.updateSelection_ = function() {
1880   // Clear the previously drawn vertical, if there is one
1881   var ctx = this.canvas_ctx_;
1882   if (this.previousVerticalX_ >= 0) {
1883     // Determine the maximum highlight circle size.
1884     var maxCircleSize = 0;
1885     var labels = this.attr_('labels');
1886     for (var i = 1; i < labels.length; i++) {
1887       var r = this.attr_('highlightCircleSize', labels[i]);
1888       if (r > maxCircleSize) maxCircleSize = r;
1889     }
1890     var px = this.previousVerticalX_;
1891     ctx.clearRect(px - maxCircleSize - 1, 0,
1892                   2 * maxCircleSize + 2, this.height_);
1893   }
1894 
1895   if (this.selPoints_.length > 0) {
1896     // Set the status message to indicate the selected point(s)
1897     if (this.attr_('showLabelsOnHighlight')) {
1898       this.setLegendHTML_(this.lastx_, this.selPoints_);
1899     }
1900 
1901     // Draw colored circles over the center of each selected point
1902     var canvasx = this.selPoints_[0].canvasx;
1903     ctx.save();
1904     for (var i = 0; i < this.selPoints_.length; i++) {
1905       var pt = this.selPoints_[i];
1906       if (!Dygraph.isOK(pt.canvasy)) continue;
1907 
1908       var circleSize = this.attr_('highlightCircleSize', pt.name);
1909       ctx.beginPath();
1910       ctx.fillStyle = this.plotter_.colors[pt.name];
1911       ctx.arc(canvasx, pt.canvasy, circleSize, 0, 2 * Math.PI, false);
1912       ctx.fill();
1913     }
1914     ctx.restore();
1915 
1916     this.previousVerticalX_ = canvasx;
1917   }
1918 };
1919 
1920 /**
1921  * Manually set the selected points and display information about them in the
1922  * legend. The selection can be cleared using clearSelection() and queried
1923  * using getSelection().
1924  * @param { Integer } row number that should be highlighted (i.e. appear with
1925  * hover dots on the chart). Set to false to clear any selection.
1926  */
1927 Dygraph.prototype.setSelection = function(row) {
1928   // Extract the points we've selected
1929   this.selPoints_ = [];
1930   var pos = 0;
1931 
1932   if (row !== false) {
1933     row = row-this.boundaryIds_[0][0];
1934   }
1935 
1936   if (row !== false && row >= 0) {
1937     for (var i in this.layout_.datasets) {
1938       if (row < this.layout_.datasets[i].length) {
1939         var point = this.layout_.points[pos+row];
1940         
1941         if (this.attr_("stackedGraph")) {
1942           point = this.layout_.unstackPointAtIndex(pos+row);
1943         }
1944         
1945         this.selPoints_.push(point);
1946       }
1947       pos += this.layout_.datasets[i].length;
1948     }
1949   }
1950 
1951   if (this.selPoints_.length) {
1952     this.lastx_ = this.selPoints_[0].xval;
1953     this.updateSelection_();
1954   } else {
1955     this.clearSelection();
1956   }
1957 
1958 };
1959 
1960 /**
1961  * The mouse has left the canvas. Clear out whatever artifacts remain
1962  * @param {Object} event the mouseout event from the browser.
1963  * @private
1964  */
1965 Dygraph.prototype.mouseOut_ = function(event) {
1966   if (this.attr_("unhighlightCallback")) {
1967     this.attr_("unhighlightCallback")(event);
1968   }
1969 
1970   if (this.attr_("hideOverlayOnMouseOut")) {
1971     this.clearSelection();
1972   }
1973 };
1974 
1975 /**
1976  * Clears the current selection (i.e. points that were highlighted by moving
1977  * the mouse over the chart).
1978  */
1979 Dygraph.prototype.clearSelection = function() {
1980   // Get rid of the overlay data
1981   this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
1982   this.setLegendHTML_();
1983   this.selPoints_ = [];
1984   this.lastx_ = -1;
1985 }
1986 
1987 /**
1988  * Returns the number of the currently selected row. To get data for this row,
1989  * you can use the getValue method.
1990  * @return { Integer } row number, or -1 if nothing is selected
1991  */
1992 Dygraph.prototype.getSelection = function() {
1993   if (!this.selPoints_ || this.selPoints_.length < 1) {
1994     return -1;
1995   }
1996 
1997   for (var row=0; row<this.layout_.points.length; row++ ) {
1998     if (this.layout_.points[row].x == this.selPoints_[0].x) {
1999       return row + this.boundaryIds_[0][0];
2000     }
2001   }
2002   return -1;
2003 };
2004 
2005 /**
2006  * Number formatting function which mimicks the behavior of %g in printf, i.e.
2007  * either exponential or fixed format (without trailing 0s) is used depending on
2008  * the length of the generated string.  The advantage of this format is that
2009  * there is a predictable upper bound on the resulting string length,
2010  * significant figures are not dropped, and normal numbers are not displayed in
2011  * exponential notation.
2012  *
2013  * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
2014  * It creates strings which are too long for absolute values between 10^-4 and
2015  * 10^-6, e.g. '0.00001' instead of '1e-5'. See tests/number-format.html for
2016  * output examples.
2017  *
2018  * @param {Number} x The number to format
2019  * @param {Number} opt_precision The precision to use, default 2.
2020  * @return {String} A string formatted like %g in printf.  The max generated
2021  *                  string length should be precision + 6 (e.g 1.123e+300).
2022  */
2023 Dygraph.floatFormat = function(x, opt_precision) {
2024   // Avoid invalid precision values; [1, 21] is the valid range.
2025   var p = Math.min(Math.max(1, opt_precision || 2), 21);
2026 
2027   // This is deceptively simple.  The actual algorithm comes from:
2028   //
2029   // Max allowed length = p + 4
2030   // where 4 comes from 'e+n' and '.'.
2031   //
2032   // Length of fixed format = 2 + y + p
2033   // where 2 comes from '0.' and y = # of leading zeroes.
2034   //
2035   // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
2036   // 1.0e-3.
2037   //
2038   // Since the behavior of toPrecision() is identical for larger numbers, we
2039   // don't have to worry about the other bound.
2040   //
2041   // Finally, the argument for toExponential() is the number of trailing digits,
2042   // so we take off 1 for the value before the '.'.
2043   return (Math.abs(x) < 1.0e-3 && x != 0.0) ?
2044       x.toExponential(p - 1) : x.toPrecision(p);
2045 };
2046 
2047 /**
2048  * @private
2049  * Return a string version of a number. This respects the digitsAfterDecimal
2050  * and maxNumberWidth options.
2051  * @param {Number} x The number to be formatted
2052  * @param {Dygraph} g The dygraph object
2053  */
2054 Dygraph.numberFormatter = function(x, g) {
2055   var sigFigs = g.attr_('sigFigs');
2056 
2057   if (sigFigs !== null) {
2058     // User has opted for a fixed number of significant figures.
2059     return Dygraph.floatFormat(x, sigFigs);
2060   }
2061 
2062   var digits = g.attr_('digitsAfterDecimal');
2063   var maxNumberWidth = g.attr_('maxNumberWidth');
2064 
2065   // switch to scientific notation if we underflow or overflow fixed display.
2066   if (x !== 0.0 &&
2067       (Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
2068        Math.abs(x) < Math.pow(10, -digits))) {
2069     return x.toExponential(digits);
2070   } else {
2071     return '' + Dygraph.round_(x, digits);
2072   }
2073 };
2074 
2075 /**
2076  * @private
2077  * Converts '9' to '09' (useful for dates)
2078  */
2079 Dygraph.zeropad = function(x) {
2080   if (x < 10) return "0" + x; else return "" + x;
2081 };
2082 
2083 /**
2084  * Return a string version of the hours, minutes and seconds portion of a date.
2085  * @param {Number} date The JavaScript date (ms since epoch)
2086  * @return {String} A time of the form "HH:MM:SS"
2087  * @private
2088  */
2089 Dygraph.hmsString_ = function(date) {
2090   var zeropad = Dygraph.zeropad;
2091   var d = new Date(date);
2092   if (d.getSeconds()) {
2093     return zeropad(d.getHours()) + ":" +
2094            zeropad(d.getMinutes()) + ":" +
2095            zeropad(d.getSeconds());
2096   } else {
2097     return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
2098   }
2099 };
2100 
2101 /**
2102  * Convert a JS date to a string appropriate to display on an axis that
2103  * is displaying values at the stated granularity.
2104  * @param {Date} date The date to format
2105  * @param {Number} granularity One of the Dygraph granularity constants
2106  * @return {String} The formatted date
2107  * @private
2108  */
2109 Dygraph.dateAxisFormatter = function(date, granularity) {
2110   if (granularity >= Dygraph.DECADAL) {
2111     return date.strftime('%Y');
2112   } else if (granularity >= Dygraph.MONTHLY) {
2113     return date.strftime('%b %y');
2114   } else {
2115     var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
2116     if (frac == 0 || granularity >= Dygraph.DAILY) {
2117       return new Date(date.getTime() + 3600*1000).strftime('%d%b');
2118     } else {
2119       return Dygraph.hmsString_(date.getTime());
2120     }
2121   }
2122 };
2123 
2124 /**
2125  * Convert a JS date (millis since epoch) to YYYY/MM/DD
2126  * @param {Number} date The JavaScript date (ms since epoch)
2127  * @return {String} A date of the form "YYYY/MM/DD"
2128  * @private
2129  */
2130 Dygraph.dateString_ = function(date) {
2131   var zeropad = Dygraph.zeropad;
2132   var d = new Date(date);
2133 
2134   // Get the year:
2135   var year = "" + d.getFullYear();
2136   // Get a 0 padded month string
2137   var month = zeropad(d.getMonth() + 1);  //months are 0-offset, sigh
2138   // Get a 0 padded day string
2139   var day = zeropad(d.getDate());
2140 
2141   var ret = "";
2142   var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
2143   if (frac) ret = " " + Dygraph.hmsString_(date);
2144 
2145   return year + "/" + month + "/" + day + ret;
2146 };
2147 
2148 /**
2149  * Round a number to the specified number of digits past the decimal point.
2150  * @param {Number} num The number to round
2151  * @param {Number} places The number of decimals to which to round
2152  * @return {Number} The rounded number
2153  * @private
2154  */
2155 Dygraph.round_ = function(num, places) {
2156   var shift = Math.pow(10, places);
2157   return Math.round(num * shift)/shift;
2158 };
2159 
2160 /**
2161  * Fires when there's data available to be graphed.
2162  * @param {String} data Raw CSV data to be plotted
2163  * @private
2164  */
2165 Dygraph.prototype.loadedEvent_ = function(data) {
2166   this.rawData_ = this.parseCSV_(data);
2167   this.predraw_();
2168 };
2169 
2170 Dygraph.prototype.months =  ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
2171                              "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
2172 Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
2173 
2174 /**
2175  * Add ticks on the x-axis representing years, months, quarters, weeks, or days
2176  * @private
2177  */
2178 Dygraph.prototype.addXTicks_ = function() {
2179   // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
2180   var range;
2181   if (this.dateWindow_) {
2182     range = [this.dateWindow_[0], this.dateWindow_[1]];
2183   } else {
2184     range = [this.rawData_[0][0], this.rawData_[this.rawData_.length - 1][0]];
2185   }
2186 
2187   var xTicks = this.attr_('xTicker')(range[0], range[1], this);
2188   this.layout_.updateOptions({xTicks: xTicks});
2189 };
2190 
2191 // Time granularity enumeration
2192 Dygraph.SECONDLY = 0;
2193 Dygraph.TWO_SECONDLY = 1;
2194 Dygraph.FIVE_SECONDLY = 2;
2195 Dygraph.TEN_SECONDLY = 3;
2196 Dygraph.THIRTY_SECONDLY  = 4;
2197 Dygraph.MINUTELY = 5;
2198 Dygraph.TWO_MINUTELY = 6;
2199 Dygraph.FIVE_MINUTELY = 7;
2200 Dygraph.TEN_MINUTELY = 8;
2201 Dygraph.THIRTY_MINUTELY = 9;
2202 Dygraph.HOURLY = 10;
2203 Dygraph.TWO_HOURLY = 11;
2204 Dygraph.SIX_HOURLY = 12;
2205 Dygraph.DAILY = 13;
2206 Dygraph.WEEKLY = 14;
2207 Dygraph.MONTHLY = 15;
2208 Dygraph.QUARTERLY = 16;
2209 Dygraph.BIANNUAL = 17;
2210 Dygraph.ANNUAL = 18;
2211 Dygraph.DECADAL = 19;
2212 Dygraph.CENTENNIAL = 20;
2213 Dygraph.NUM_GRANULARITIES = 21;
2214 
2215 Dygraph.SHORT_SPACINGS = [];
2216 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]        = 1000 * 1;
2217 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]    = 1000 * 2;
2218 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]   = 1000 * 5;
2219 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]    = 1000 * 10;
2220 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
2221 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]        = 1000 * 60;
2222 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]    = 1000 * 60 * 2;
2223 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]   = 1000 * 60 * 5;
2224 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]    = 1000 * 60 * 10;
2225 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
2226 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]          = 1000 * 3600;
2227 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]      = 1000 * 3600 * 2;
2228 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]      = 1000 * 3600 * 6;
2229 Dygraph.SHORT_SPACINGS[Dygraph.DAILY]           = 1000 * 86400;
2230 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]          = 1000 * 604800;
2231 
2232 /**
2233  * @private
2234  * If we used this time granularity, how many ticks would there be?
2235  * This is only an approximation, but it's generally good enough.
2236  */
2237 Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
2238   if (granularity < Dygraph.MONTHLY) {
2239     // Generate one tick mark for every fixed interval of time.
2240     var spacing = Dygraph.SHORT_SPACINGS[granularity];
2241     return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
2242   } else {
2243     var year_mod = 1;  // e.g. to only print one point every 10 years.
2244     var num_months = 12;
2245     if (granularity == Dygraph.QUARTERLY) num_months = 3;
2246     if (granularity == Dygraph.BIANNUAL) num_months = 2;
2247     if (granularity == Dygraph.ANNUAL) num_months = 1;
2248     if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
2249     if (granularity == Dygraph.CENTENNIAL) { num_months = 1; year_mod = 100; }
2250 
2251     var msInYear = 365.2524 * 24 * 3600 * 1000;
2252     var num_years = 1.0 * (end_time - start_time) / msInYear;
2253     return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
2254   }
2255 };
2256 
2257 /**
2258  * @private
2259  *
2260  * Construct an x-axis of nicely-formatted times on meaningful boundaries
2261  * (e.g. 'Jan 09' rather than 'Jan 22, 2009').
2262  *
2263  * Returns an array containing {v: millis, label: label} dictionaries.
2264  */
2265 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
2266   var formatter = this.attr_("xAxisLabelFormatter");
2267   var ticks = [];
2268   if (granularity < Dygraph.MONTHLY) {
2269     // Generate one tick mark for every fixed interval of time.
2270     var spacing = Dygraph.SHORT_SPACINGS[granularity];
2271     var format = '%d%b';  // e.g. "1Jan"
2272 
2273     // Find a time less than start_time which occurs on a "nice" time boundary
2274     // for this granularity.
2275     var g = spacing / 1000;
2276     var d = new Date(start_time);
2277     if (g <= 60) {  // seconds
2278       var x = d.getSeconds(); d.setSeconds(x - x % g);
2279     } else {
2280       d.setSeconds(0);
2281       g /= 60;
2282       if (g <= 60) {  // minutes
2283         var x = d.getMinutes(); d.setMinutes(x - x % g);
2284       } else {
2285         d.setMinutes(0);
2286         g /= 60;
2287 
2288         if (g <= 24) {  // days
2289           var x = d.getHours(); d.setHours(x - x % g);
2290         } else {
2291           d.setHours(0);
2292           g /= 24;
2293 
2294           if (g == 7) {  // one week
2295             d.setDate(d.getDate() - d.getDay());
2296           }
2297         }
2298       }
2299     }
2300     start_time = d.getTime();
2301 
2302     for (var t = start_time; t <= end_time; t += spacing) {
2303       ticks.push({ v:t, label: formatter(new Date(t), granularity) });
2304     }
2305   } else {
2306     // Display a tick mark on the first of a set of months of each year.
2307     // Years get a tick mark iff y % year_mod == 0. This is useful for
2308     // displaying a tick mark once every 10 years, say, on long time scales.
2309     var months;
2310     var year_mod = 1;  // e.g. to only print one point every 10 years.
2311 
2312     if (granularity == Dygraph.MONTHLY) {
2313       months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
2314     } else if (granularity == Dygraph.QUARTERLY) {
2315       months = [ 0, 3, 6, 9 ];
2316     } else if (granularity == Dygraph.BIANNUAL) {
2317       months = [ 0, 6 ];
2318     } else if (granularity == Dygraph.ANNUAL) {
2319       months = [ 0 ];
2320     } else if (granularity == Dygraph.DECADAL) {
2321       months = [ 0 ];
2322       year_mod = 10;
2323     } else if (granularity == Dygraph.CENTENNIAL) {
2324       months = [ 0 ];
2325       year_mod = 100;
2326     } else {
2327       this.warn("Span of dates is too long");
2328     }
2329 
2330     var start_year = new Date(start_time).getFullYear();
2331     var end_year   = new Date(end_time).getFullYear();
2332     var zeropad = Dygraph.zeropad;
2333     for (var i = start_year; i <= end_year; i++) {
2334       if (i % year_mod != 0) continue;
2335       for (var j = 0; j < months.length; j++) {
2336         var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
2337         var t = Dygraph.dateStrToMillis(date_str);
2338         if (t < start_time || t > end_time) continue;
2339         ticks.push({ v:t, label: formatter(new Date(t), granularity) });
2340       }
2341     }
2342   }
2343 
2344   return ticks;
2345 };
2346 
2347 
2348 /**
2349  * Add ticks to the x-axis based on a date range.
2350  * @param {Number} startDate Start of the date window (millis since epoch)
2351  * @param {Number} endDate End of the date window (millis since epoch)
2352  * @param {Dygraph} self The dygraph object
2353  * @return { [Object] } Array of {label, value} tuples.
2354  * @public
2355  */
2356 Dygraph.dateTicker = function(startDate, endDate, self) {
2357   // TODO(danvk): why does this take 'self' as a param?
2358   var chosen = -1;
2359   for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
2360     var num_ticks = self.NumXTicks(startDate, endDate, i);
2361     if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
2362       chosen = i;
2363       break;
2364     }
2365   }
2366 
2367   if (chosen >= 0) {
2368     return self.GetXAxis(startDate, endDate, chosen);
2369   } else {
2370     // TODO(danvk): signal error.
2371   }
2372 };
2373 
2374 /**
2375  * @private
2376  * This is a list of human-friendly values at which to show tick marks on a log
2377  * scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
2378  * ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
2379  * NOTE: this assumes that Dygraph.LOG_SCALE = 10.
2380  */
2381 Dygraph.PREFERRED_LOG_TICK_VALUES = function() {
2382   var vals = [];
2383   for (var power = -39; power <= 39; power++) {
2384     var range = Math.pow(10, power);
2385     for (var mult = 1; mult <= 9; mult++) {
2386       var val = range * mult;
2387       vals.push(val);
2388     }
2389   }
2390   return vals;
2391 }();
2392 
2393 /**
2394  * @private
2395  * Implementation of binary search over an array.
2396  * Currently does not work when val is outside the range of arry's values.
2397  * @param { Integer } val the value to search for
2398  * @param { Integer[] } arry is the value over which to search
2399  * @param { Integer } abs If abs > 0, find the lowest entry greater than val
2400  * If abs < 0, find the highest entry less than val.
2401  * if abs == 0, find the entry that equals val.
2402  * @param { Integer } [low] The first index in arry to consider (optional)
2403  * @param { Integer } [high] The last index in arry to consider (optional)
2404  */
2405 Dygraph.binarySearch = function(val, arry, abs, low, high) {
2406   if (low == null || high == null) {
2407     low = 0;
2408     high = arry.length - 1;
2409   }
2410   if (low > high) {
2411     return -1;
2412   }
2413   if (abs == null) {
2414     abs = 0;
2415   }
2416   var validIndex = function(idx) {
2417     return idx >= 0 && idx < arry.length;
2418   }
2419   var mid = parseInt((low + high) / 2);
2420   var element = arry[mid];
2421   if (element == val) {
2422     return mid;
2423   }
2424   if (element > val) {
2425     if (abs > 0) {
2426       // Accept if element > val, but also if prior element < val.
2427       var idx = mid - 1;
2428       if (validIndex(idx) && arry[idx] < val) {
2429         return mid;
2430       }
2431     }
2432     return Dygraph.binarySearch(val, arry, abs, low, mid - 1);
2433   }
2434   if (element < val) {
2435     if (abs < 0) {
2436       // Accept if element < val, but also if prior element > val.
2437       var idx = mid + 1;
2438       if (validIndex(idx) && arry[idx] > val) {
2439         return mid;
2440       }
2441     }
2442     return Dygraph.binarySearch(val, arry, abs, mid + 1, high);
2443   }
2444 };
2445 
2446 // TODO(konigsberg): Update comment.
2447 /**
2448  * Add ticks when the x axis has numbers on it (instead of dates)
2449  *
2450  * @param {Number} minV minimum value
2451  * @param {Number} maxV maximum value
2452  * @param self
2453  * @param {function} attribute accessor function.
2454  * @return {[Object]} Array of {label, value} tuples.
2455  */
2456 Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
2457   var attr = function(k) {
2458     if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
2459     return self.attr_(k);
2460   };
2461 
2462   var ticks = [];
2463   if (vals) {
2464     for (var i = 0; i < vals.length; i++) {
2465       ticks.push({v: vals[i]});
2466     }
2467   } else {
2468     if (axis_props && attr("logscale")) {
2469       var pixelsPerTick = attr('pixelsPerYLabel');
2470       // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
2471       var nTicks  = Math.floor(self.height_ / pixelsPerTick);
2472       var minIdx = Dygraph.binarySearch(minV, Dygraph.PREFERRED_LOG_TICK_VALUES, 1);
2473       var maxIdx = Dygraph.binarySearch(maxV, Dygraph.PREFERRED_LOG_TICK_VALUES, -1);
2474       if (minIdx == -1) {
2475         minIdx = 0;
2476       }
2477       if (maxIdx == -1) {
2478         maxIdx = Dygraph.PREFERRED_LOG_TICK_VALUES.length - 1;
2479       }
2480       // Count the number of tick values would appear, if we can get at least
2481       // nTicks / 4 accept them.
2482       var lastDisplayed = null;
2483       if (maxIdx - minIdx >= nTicks / 4) {
2484         var axisId = axis_props.yAxisId;
2485         for (var idx = maxIdx; idx >= minIdx; idx--) {
2486           var tickValue = Dygraph.PREFERRED_LOG_TICK_VALUES[idx];
2487           var domCoord = axis_props.g.toDomYCoord(tickValue, axisId);
2488           var tick = { v: tickValue };
2489           if (lastDisplayed == null) {
2490             lastDisplayed = {
2491               tickValue : tickValue,
2492               domCoord : domCoord
2493             };
2494           } else {
2495             if (domCoord - lastDisplayed.domCoord >= pixelsPerTick) {
2496               lastDisplayed = {
2497                 tickValue : tickValue,
2498                 domCoord : domCoord
2499               };
2500             } else {
2501               tick.label = "";
2502             }
2503           }
2504           ticks.push(tick);
2505         }
2506         // Since we went in backwards order.
2507         ticks.reverse();
2508       }
2509     }
2510 
2511     // ticks.length won't be 0 if the log scale function finds values to insert.
2512     if (ticks.length == 0) {
2513       // Basic idea:
2514       // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
2515       // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
2516       // The first spacing greater than pixelsPerYLabel is what we use.
2517       // TODO(danvk): version that works on a log scale.
2518       if (attr("labelsKMG2")) {
2519         var mults = [1, 2, 4, 8];
2520       } else {
2521         var mults = [1, 2, 5];
2522       }
2523       var scale, low_val, high_val, nTicks;
2524       // TODO(danvk): make it possible to set this for x- and y-axes independently.
2525       var pixelsPerTick = attr('pixelsPerYLabel');
2526       for (var i = -10; i < 50; i++) {
2527         if (attr("labelsKMG2")) {
2528           var base_scale = Math.pow(16, i);
2529         } else {
2530           var base_scale = Math.pow(10, i);
2531         }
2532         for (var j = 0; j < mults.length; j++) {
2533           scale = base_scale * mults[j];
2534           low_val = Math.floor(minV / scale) * scale;
2535           high_val = Math.ceil(maxV / scale) * scale;
2536           nTicks = Math.abs(high_val - low_val) / scale;
2537           var spacing = self.height_ / nTicks;
2538           // wish I could break out of both loops at once...
2539           if (spacing > pixelsPerTick) break;
2540         }
2541         if (spacing > pixelsPerTick) break;
2542       }
2543 
2544       // Construct the set of ticks.
2545       // Allow reverse y-axis if it's explicitly requested.
2546       if (low_val > high_val) scale *= -1;
2547       for (var i = 0; i < nTicks; i++) {
2548         var tickV = low_val + i * scale;
2549         ticks.push( {v: tickV} );
2550       }
2551     }
2552   }
2553 
2554   // Add formatted labels to the ticks.
2555   var k;
2556   var k_labels = [];
2557   if (attr("labelsKMB")) {
2558     k = 1000;
2559     k_labels = [ "K", "M", "B", "T" ];
2560   }
2561   if (attr("labelsKMG2")) {
2562     if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2563     k = 1024;
2564     k_labels = [ "k", "M", "G", "T" ];
2565   }
2566   var formatter = attr('yAxisLabelFormatter') ?
2567       attr('yAxisLabelFormatter') : attr('yValueFormatter');
2568 
2569   // Add labels to the ticks.
2570   for (var i = 0; i < ticks.length; i++) {
2571     if (ticks[i].label !== undefined) continue;  // Use current label.
2572     var tickV = ticks[i].v;
2573     var absTickV = Math.abs(tickV);
2574     var label = formatter(tickV, self);
2575     if (k_labels.length > 0) {
2576       // Round up to an appropriate unit.
2577       var n = k*k*k*k;
2578       for (var j = 3; j >= 0; j--, n /= k) {
2579         if (absTickV >= n) {
2580           label = Dygraph.round_(tickV / n, attr('digitsAfterDecimal')) + k_labels[j];
2581           break;
2582         }
2583       }
2584     }
2585     ticks[i].label = label;
2586   }
2587 
2588   return ticks;
2589 };
2590 
2591 /**
2592  * @private
2593  * Computes the range of the data series (including confidence intervals).
2594  * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
2595  * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2596  * @return [low, high]
2597  */
2598 Dygraph.prototype.extremeValues_ = function(series) {
2599   var minY = null, maxY = null;
2600 
2601   var bars = this.attr_("errorBars") || this.attr_("customBars");
2602   if (bars) {
2603     // With custom bars, maxY is the max of the high values.
2604     for (var j = 0; j < series.length; j++) {
2605       var y = series[j][1][0];
2606       if (!y) continue;
2607       var low = y - series[j][1][1];
2608       var high = y + series[j][1][2];
2609       if (low > y) low = y;    // this can happen with custom bars,
2610       if (high < y) high = y;  // e.g. in tests/custom-bars.html
2611       if (maxY == null || high > maxY) {
2612         maxY = high;
2613       }
2614       if (minY == null || low < minY) {
2615         minY = low;
2616       }
2617     }
2618   } else {
2619     for (var j = 0; j < series.length; j++) {
2620       var y = series[j][1];
2621       if (y === null || isNaN(y)) continue;
2622       if (maxY == null || y > maxY) {
2623         maxY = y;
2624       }
2625       if (minY == null || y < minY) {
2626         minY = y;
2627       }
2628     }
2629   }
2630 
2631   return [minY, maxY];
2632 };
2633 
2634 /**
2635  * @private
2636  * This function is called once when the chart's data is changed or the options
2637  * dictionary is updated. It is _not_ called when the user pans or zooms. The
2638  * idea is that values derived from the chart's data can be computed here,
2639  * rather than every time the chart is drawn. This includes things like the
2640  * number of axes, rolling averages, etc.
2641  */
2642 Dygraph.prototype.predraw_ = function() {
2643   // TODO(danvk): move more computations out of drawGraph_ and into here.
2644   this.computeYAxes_();
2645 
2646   // Create a new plotter.
2647   if (this.plotter_) this.plotter_.clear();
2648   this.plotter_ = new DygraphCanvasRenderer(this,
2649                                             this.hidden_,
2650                                             this.hidden_ctx_,
2651                                             this.layout_,
2652                                             this.renderOptions_);
2653 
2654   // The roller sits in the bottom left corner of the chart. We don't know where
2655   // this will be until the options are available, so it's positioned here.
2656   this.createRollInterface_();
2657 
2658   // Same thing applies for the labelsDiv. It's right edge should be flush with
2659   // the right edge of the charting area (which may not be the same as the right
2660   // edge of the div, if we have two y-axes.
2661   this.positionLabelsDiv_();
2662 
2663   // If the data or options have changed, then we'd better redraw.
2664   this.drawGraph_();
2665 };
2666 
2667 /**
2668  * Update the graph with new data. This method is called when the viewing area
2669  * has changed. If the underlying data or options have changed, predraw_ will
2670  * be called before drawGraph_ is called.
2671  * @private
2672  */
2673 Dygraph.prototype.drawGraph_ = function() {
2674   var data = this.rawData_;
2675 
2676   // This is used to set the second parameter to drawCallback, below.
2677   var is_initial_draw = this.is_initial_draw_;
2678   this.is_initial_draw_ = false;
2679 
2680   var minY = null, maxY = null;
2681   this.layout_.removeAllDatasets();
2682   this.setColors_();
2683   this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
2684 
2685   // Loop over the fields (series).  Go from the last to the first,
2686   // because if they're stacked that's how we accumulate the values.
2687 
2688   var cumulative_y = [];  // For stacked series.
2689   var datasets = [];
2690 
2691   var extremes = {};  // series name -> [low, high]
2692 
2693   // Loop over all fields and create datasets
2694   for (var i = data[0].length - 1; i >= 1; i--) {
2695     if (!this.visibility()[i - 1]) continue;
2696 
2697     var seriesName = this.attr_("labels")[i];
2698     var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
2699     var logScale = this.attr_('logscale', i);
2700 
2701     var series = [];
2702     for (var j = 0; j < data.length; j++) {
2703       var date = data[j][0];
2704       var point = data[j][i];
2705       if (logScale) {
2706         // On the log scale, points less than zero do not exist.
2707         // This will create a gap in the chart. Note that this ignores
2708         // connectSeparatedPoints.
2709         if (point <= 0) {
2710           point = null;
2711         }
2712         series.push([date, point]);
2713       } else {
2714         if (point != null || !connectSeparatedPoints) {
2715           series.push([date, point]);
2716         }
2717       }
2718     }
2719 
2720     // TODO(danvk): move this into predraw_. It's insane to do it here.
2721     series = this.rollingAverage(series, this.rollPeriod_);
2722 
2723     // Prune down to the desired range, if necessary (for zooming)
2724     // Because there can be lines going to points outside of the visible area,
2725     // we actually prune to visible points, plus one on either side.
2726     var bars = this.attr_("errorBars") || this.attr_("customBars");
2727     if (this.dateWindow_) {
2728       var low = this.dateWindow_[0];
2729       var high= this.dateWindow_[1];
2730       var pruned = [];
2731       // TODO(danvk): do binary search instead of linear search.
2732       // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2733       var firstIdx = null, lastIdx = null;
2734       for (var k = 0; k < series.length; k++) {
2735         if (series[k][0] >= low && firstIdx === null) {
2736           firstIdx = k;
2737         }
2738         if (series[k][0] <= high) {
2739           lastIdx = k;
2740         }
2741       }
2742       if (firstIdx === null) firstIdx = 0;
2743       if (firstIdx > 0) firstIdx--;
2744       if (lastIdx === null) lastIdx = series.length - 1;
2745       if (lastIdx < series.length - 1) lastIdx++;
2746       this.boundaryIds_[i-1] = [firstIdx, lastIdx];
2747       for (var k = firstIdx; k <= lastIdx; k++) {
2748         pruned.push(series[k]);
2749       }
2750       series = pruned;
2751     } else {
2752       this.boundaryIds_[i-1] = [0, series.length-1];
2753     }
2754 
2755     var seriesExtremes = this.extremeValues_(series);
2756 
2757     if (bars) {
2758       for (var j=0; j<series.length; j++) {
2759         val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
2760         series[j] = val;
2761       }
2762     } else if (this.attr_("stackedGraph")) {
2763       var l = series.length;
2764       var actual_y;
2765       for (var j = 0; j < l; j++) {
2766         // If one data set has a NaN, let all subsequent stacked
2767         // sets inherit the NaN -- only start at 0 for the first set.
2768         var x = series[j][0];
2769         if (cumulative_y[x] === undefined) {
2770           cumulative_y[x] = 0;
2771         }
2772 
2773         actual_y = series[j][1];
2774         cumulative_y[x] += actual_y;
2775 
2776         series[j] = [x, cumulative_y[x]]
2777 
2778         if (cumulative_y[x] > seriesExtremes[1]) {
2779           seriesExtremes[1] = cumulative_y[x];
2780         }
2781         if (cumulative_y[x] < seriesExtremes[0]) {
2782           seriesExtremes[0] = cumulative_y[x];
2783         }
2784       }
2785     }
2786     extremes[seriesName] = seriesExtremes;
2787 
2788     datasets[i] = series;
2789   }
2790 
2791   for (var i = 1; i < datasets.length; i++) {
2792     if (!this.visibility()[i - 1]) continue;
2793     this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
2794   }
2795 
2796   this.computeYAxisRanges_(extremes);
2797   this.layout_.updateOptions( { yAxes: this.axes_,
2798                                 seriesToAxisMap: this.seriesToAxisMap_
2799                               } );
2800   this.addXTicks_();
2801 
2802   // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously
2803   var tmp_zoomed_x = this.zoomed_x_;
2804   // Tell PlotKit to use this new data and render itself
2805   this.layout_.updateOptions({dateWindow: this.dateWindow_});
2806   this.zoomed_x_ = tmp_zoomed_x;
2807   this.layout_.evaluateWithError();
2808   this.plotter_.clear();
2809   this.plotter_.render();
2810   this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
2811                                           this.canvas_.height);
2812 
2813   if (is_initial_draw) {
2814     // Generate a static legend before any particular point is selected.
2815     this.setLegendHTML_();
2816   } else {
2817     if (typeof(this.selPoints_) !== 'undefined' && this.selPoints_.length) {
2818       this.lastx_ = this.selPoints_[0].xval;
2819       this.updateSelection_();
2820     } else {
2821       this.clearSelection();
2822     }
2823   }
2824 
2825   if (this.attr_("drawCallback") !== null) {
2826     this.attr_("drawCallback")(this, is_initial_draw);
2827   }
2828 };
2829 
2830 /**
2831  * @private
2832  * Determine properties of the y-axes which are independent of the data
2833  * currently being displayed. This includes things like the number of axes and
2834  * the style of the axes. It does not include the range of each axis and its
2835  * tick marks.
2836  * This fills in this.axes_ and this.seriesToAxisMap_.
2837  * axes_ = [ { options } ]
2838  * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2839  *   indices are into the axes_ array.
2840  */
2841 Dygraph.prototype.computeYAxes_ = function() {
2842   this.axes_ = [{ yAxisId : 0, g : this }];  // always have at least one y-axis.
2843   this.seriesToAxisMap_ = {};
2844 
2845   // Get a list of series names.
2846   var labels = this.attr_("labels");
2847   var series = {};
2848   for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
2849 
2850   // all options which could be applied per-axis:
2851   var axisOptions = [
2852     'includeZero',
2853     'valueRange',
2854     'labelsKMB',
2855     'labelsKMG2',
2856     'pixelsPerYLabel',
2857     'yAxisLabelWidth',
2858     'axisLabelFontSize',
2859     'axisTickSize',
2860     'logscale'
2861   ];
2862 
2863   // Copy global axis options over to the first axis.
2864   for (var i = 0; i < axisOptions.length; i++) {
2865     var k = axisOptions[i];
2866     var v = this.attr_(k);
2867     if (v) this.axes_[0][k] = v;
2868   }
2869 
2870   // Go through once and add all the axes.
2871   for (var seriesName in series) {
2872     if (!series.hasOwnProperty(seriesName)) continue;
2873     var axis = this.attr_("axis", seriesName);
2874     if (axis == null) {
2875       this.seriesToAxisMap_[seriesName] = 0;
2876       continue;
2877     }
2878     if (typeof(axis) == 'object') {
2879       // Add a new axis, making a copy of its per-axis options.
2880       var opts = {};
2881       Dygraph.update(opts, this.axes_[0]);
2882       Dygraph.update(opts, { valueRange: null });  // shouldn't inherit this.
2883       var yAxisId = this.axes_.length;
2884       opts.yAxisId = yAxisId;
2885       opts.g = this;
2886       Dygraph.update(opts, axis);
2887       this.axes_.push(opts);
2888       this.seriesToAxisMap_[seriesName] = yAxisId;
2889     }
2890   }
2891 
2892   // Go through one more time and assign series to an axis defined by another
2893   // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2894   for (var seriesName in series) {
2895     if (!series.hasOwnProperty(seriesName)) continue;
2896     var axis = this.attr_("axis", seriesName);
2897     if (typeof(axis) == 'string') {
2898       if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
2899         this.error("Series " + seriesName + " wants to share a y-axis with " +
2900                    "series " + axis + ", which does not define its own axis.");
2901         return null;
2902       }
2903       var idx = this.seriesToAxisMap_[axis];
2904       this.seriesToAxisMap_[seriesName] = idx;
2905     }
2906   }
2907 
2908   // Now we remove series from seriesToAxisMap_ which are not visible. We do
2909   // this last so that hiding the first series doesn't destroy the axis
2910   // properties of the primary axis.
2911   var seriesToAxisFiltered = {};
2912   var vis = this.visibility();
2913   for (var i = 1; i < labels.length; i++) {
2914     var s = labels[i];
2915     if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
2916   }
2917   this.seriesToAxisMap_ = seriesToAxisFiltered;
2918 };
2919 
2920 /**
2921  * Returns the number of y-axes on the chart.
2922  * @return {Number} the number of axes.
2923  */
2924 Dygraph.prototype.numAxes = function() {
2925   var last_axis = 0;
2926   for (var series in this.seriesToAxisMap_) {
2927     if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2928     var idx = this.seriesToAxisMap_[series];
2929     if (idx > last_axis) last_axis = idx;
2930   }
2931   return 1 + last_axis;
2932 };
2933 
2934 /**
2935  * @private
2936  * Determine the value range and tick marks for each axis.
2937  * @param {Object} extremes A mapping from seriesName -> [low, high]
2938  * This fills in the valueRange and ticks fields in each entry of this.axes_.
2939  */
2940 Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2941   // Build a map from axis number -> [list of series names]
2942   var seriesForAxis = [];
2943   for (var series in this.seriesToAxisMap_) {
2944     if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2945     var idx = this.seriesToAxisMap_[series];
2946     while (seriesForAxis.length <= idx) seriesForAxis.push([]);
2947     seriesForAxis[idx].push(series);
2948   }
2949 
2950   // Compute extreme values, a span and tick marks for each axis.
2951   for (var i = 0; i < this.axes_.length; i++) {
2952     var axis = this.axes_[i];
2953  
2954     if (!seriesForAxis[i]) {
2955       // If no series are defined or visible then use a reasonable default
2956       axis.extremeRange = [0, 1];
2957     } else {
2958       // Calculate the extremes of extremes.
2959       var series = seriesForAxis[i];
2960       var minY = Infinity;  // extremes[series[0]][0];
2961       var maxY = -Infinity;  // extremes[series[0]][1];
2962       var extremeMinY, extremeMaxY;
2963       for (var j = 0; j < series.length; j++) {
2964         // Only use valid extremes to stop null data series' from corrupting the scale.
2965         extremeMinY = extremes[series[j]][0];
2966         if (extremeMinY != null) {
2967           minY = Math.min(extremeMinY, minY);
2968         }
2969         extremeMaxY = extremes[series[j]][1];
2970         if (extremeMaxY != null) {
2971           maxY = Math.max(extremeMaxY, maxY);
2972         }
2973       }
2974       if (axis.includeZero && minY > 0) minY = 0;
2975 
2976       // Ensure we have a valid scale, otherwise defualt to zero for safety.
2977       if (minY == Infinity) minY = 0;
2978       if (maxY == -Infinity) maxY = 0;
2979 
2980       // Add some padding and round up to an integer to be human-friendly.
2981       var span = maxY - minY;
2982       // special case: if we have no sense of scale, use +/-10% of the sole value.
2983       if (span == 0) { span = maxY; }
2984 
2985       var maxAxisY;
2986       var minAxisY;
2987       if (axis.logscale) {
2988         var maxAxisY = maxY + 0.1 * span;
2989         var minAxisY = minY;
2990       } else {
2991         var maxAxisY = maxY + 0.1 * span;
2992         var minAxisY = minY - 0.1 * span;
2993 
2994         // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2995         if (!this.attr_("avoidMinZero")) {
2996           if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2997           if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2998         }
2999 
3000         if (this.attr_("includeZero")) {
3001           if (maxY < 0) maxAxisY = 0;
3002           if (minY > 0) minAxisY = 0;
3003         }
3004       }
3005       axis.extremeRange = [minAxisY, maxAxisY];
3006     }
3007     if (axis.valueWindow) {
3008       // This is only set if the user has zoomed on the y-axis. It is never set
3009       // by a user. It takes precedence over axis.valueRange because, if you set
3010       // valueRange, you'd still expect to be able to pan.
3011       axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
3012     } else if (axis.valueRange) {
3013       // This is a user-set value range for this axis.
3014       axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
3015     } else {
3016       axis.computedValueRange = axis.extremeRange;
3017     }
3018 
3019     // Add ticks. By default, all axes inherit the tick positions of the
3020     // primary axis. However, if an axis is specifically marked as having
3021     // independent ticks, then that is permissible as well.
3022     if (i == 0 || axis.independentTicks) {
3023       axis.ticks =
3024         Dygraph.numericTicks(axis.computedValueRange[0],
3025                              axis.computedValueRange[1],
3026                              this,
3027                              axis);
3028     } else {
3029       var p_axis = this.axes_[0];
3030       var p_ticks = p_axis.ticks;
3031       var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
3032       var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
3033       var tick_values = [];
3034       for (var i = 0; i < p_ticks.length; i++) {
3035         var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
3036         var y_val = axis.computedValueRange[0] + y_frac * scale;
3037         tick_values.push(y_val);
3038       }
3039 
3040       axis.ticks =
3041         Dygraph.numericTicks(axis.computedValueRange[0],
3042                              axis.computedValueRange[1],
3043                              this, axis, tick_values);
3044     }
3045   }
3046 };
3047  
3048 /**
3049  * @private
3050  * Calculates the rolling average of a data set.
3051  * If originalData is [label, val], rolls the average of those.
3052  * If originalData is [label, [, it's interpreted as [value, stddev]
3053  *   and the roll is returned in the same form, with appropriately reduced
3054  *   stddev for each value.
3055  * Note that this is where fractional input (i.e. '5/10') is converted into
3056  *   decimal values.
3057  * @param {Array} originalData The data in the appropriate format (see above)
3058  * @param {Number} rollPeriod The number of points over which to average the
3059  *                            data
3060  */
3061 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
3062   if (originalData.length < 2)
3063     return originalData;
3064   var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
3065   var rollingData = [];
3066   var sigma = this.attr_("sigma");
3067 
3068   if (this.fractions_) {
3069     var num = 0;
3070     var den = 0;  // numerator/denominator
3071     var mult = 100.0;
3072     for (var i = 0; i < originalData.length; i++) {
3073       num += originalData[i][1][0];
3074       den += originalData[i][1][1];
3075       if (i - rollPeriod >= 0) {
3076         num -= originalData[i - rollPeriod][1][0];
3077         den -= originalData[i - rollPeriod][1][1];
3078       }
3079 
3080       var date = originalData[i][0];
3081       var value = den ? num / den : 0.0;
3082       if (this.attr_("errorBars")) {
3083         if (this.wilsonInterval_) {
3084           // For more details on this confidence interval, see:
3085           // http://en.wikipedia.org/wiki/Binomial_confidence_interval
3086           if (den) {
3087             var p = value < 0 ? 0 : value, n = den;
3088             var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
3089             var denom = 1 + sigma * sigma / den;
3090             var low  = (p + sigma * sigma / (2 * den) - pm) / denom;
3091             var high = (p + sigma * sigma / (2 * den) + pm) / denom;
3092             rollingData[i] = [date,
3093                               [p * mult, (p - low) * mult, (high - p) * mult]];
3094           } else {
3095             rollingData[i] = [date, [0, 0, 0]];
3096           }
3097         } else {
3098           var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
3099           rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
3100         }
3101       } else {
3102         rollingData[i] = [date, mult * value];
3103       }
3104     }
3105   } else if (this.attr_("customBars")) {
3106     var low = 0;
3107     var mid = 0;
3108     var high = 0;
3109     var count = 0;
3110     for (var i = 0; i < originalData.length; i++) {
3111       var data = originalData[i][1];
3112       var y = data[1];
3113       rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
3114 
3115       if (y != null && !isNaN(y)) {
3116         low += data[0];
3117         mid += y;
3118         high += data[2];
3119         count += 1;
3120       }
3121       if (i - rollPeriod >= 0) {
3122         var prev = originalData[i - rollPeriod];
3123         if (prev[1][1] != null && !isNaN(prev[1][1])) {
3124           low -= prev[1][0];
3125           mid -= prev[1][1];
3126           high -= prev[1][2];
3127           count -= 1;
3128         }
3129       }
3130       rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
3131                                               1.0 * (mid - low) / count,
3132                                               1.0 * (high - mid) / count ]];
3133     }
3134   } else {
3135     // Calculate the rolling average for the first rollPeriod - 1 points where
3136     // there is not enough data to roll over the full number of points
3137     var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
3138     if (!this.attr_("errorBars")){
3139       if (rollPeriod == 1) {
3140         return originalData;
3141       }
3142 
3143       for (var i = 0; i < originalData.length; i++) {
3144         var sum = 0;
3145         var num_ok = 0;
3146         for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3147           var y = originalData[j][1];
3148           if (y == null || isNaN(y)) continue;
3149           num_ok++;
3150           sum += originalData[j][1];
3151         }
3152         if (num_ok) {
3153           rollingData[i] = [originalData[i][0], sum / num_ok];
3154         } else {
3155           rollingData[i] = [originalData[i][0], null];
3156         }
3157       }
3158 
3159     } else {
3160       for (var i = 0; i < originalData.length; i++) {
3161         var sum = 0;
3162         var variance = 0;
3163         var num_ok = 0;
3164         for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
3165           var y = originalData[j][1][0];
3166           if (y == null || isNaN(y)) continue;
3167           num_ok++;
3168           sum += originalData[j][1][0];
3169           variance += Math.pow(originalData[j][1][1], 2);
3170         }
3171         if (num_ok) {
3172           var stddev = Math.sqrt(variance) / num_ok;
3173           rollingData[i] = [originalData[i][0],
3174                             [sum / num_ok, sigma * stddev, sigma * stddev]];
3175         } else {
3176           rollingData[i] = [originalData[i][0], [null, null, null]];
3177         }
3178       }
3179     }
3180   }
3181 
3182   return rollingData;
3183 };
3184 
3185 /**
3186  * @private
3187  * Parses a date, returning the number of milliseconds since epoch. This can be
3188  * passed in as an xValueParser in the Dygraph constructor.
3189  * TODO(danvk): enumerate formats that this understands.
3190  * @param {String} A date in YYYYMMDD format.
3191  * @return {Number} Milliseconds since epoch.
3192  */
3193 Dygraph.dateParser = function(dateStr, self) {
3194   var dateStrSlashed;
3195   var d;
3196   if (dateStr.search("-") != -1) {  // e.g. '2009-7-12' or '2009-07-12'
3197     dateStrSlashed = dateStr.replace("-", "/", "g");
3198     while (dateStrSlashed.search("-") != -1) {
3199       dateStrSlashed = dateStrSlashed.replace("-", "/");
3200     }
3201     d = Dygraph.dateStrToMillis(dateStrSlashed);
3202   } else if (dateStr.length == 8) {  // e.g. '20090712'
3203     // TODO(danvk): remove support for this format. It's confusing.
3204     dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
3205                        + "/" + dateStr.substr(6,2);
3206     d = Dygraph.dateStrToMillis(dateStrSlashed);
3207   } else {
3208     // Any format that Date.parse will accept, e.g. "2009/07/12" or
3209     // "2009/07/12 12:34:56"
3210     d = Dygraph.dateStrToMillis(dateStr);
3211   }
3212 
3213   if (!d || isNaN(d)) {
3214     self.error("Couldn't parse " + dateStr + " as a date");
3215   }
3216   return d;
3217 };
3218 
3219 /**
3220  * Detects the type of the str (date or numeric) and sets the various
3221  * formatting attributes in this.attrs_ based on this type.
3222  * @param {String} str An x value.
3223  * @private
3224  */
3225 Dygraph.prototype.detectTypeFromString_ = function(str) {
3226   var isDate = false;
3227   if (str.indexOf('-') > 0 ||
3228       str.indexOf('/') >= 0 ||
3229       isNaN(parseFloat(str))) {
3230     isDate = true;
3231   } else if (str.length == 8 && str > '19700101' && str < '20371231') {
3232     // TODO(danvk): remove support for this format.
3233     isDate = true;
3234   }
3235 
3236   if (isDate) {
3237     this.attrs_.xValueFormatter = Dygraph.dateString_;
3238     this.attrs_.xValueParser = Dygraph.dateParser;
3239     this.attrs_.xTicker = Dygraph.dateTicker;
3240     this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
3241   } else {
3242     // TODO(danvk): use Dygraph.numberFormatter here?
3243     this.attrs_.xValueFormatter = function(x) { return x; };
3244     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3245     this.attrs_.xTicker = Dygraph.numericTicks;
3246     this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
3247   }
3248 };
3249 
3250 /**
3251  * Parses the value as a floating point number. This is like the parseFloat()
3252  * built-in, but with a few differences:
3253  * - the empty string is parsed as null, rather than NaN.
3254  * - if the string cannot be parsed at all, an error is logged.
3255  * If the string can't be parsed, this method returns null.
3256  * @param {String} x The string to be parsed
3257  * @param {Number} opt_line_no The line number from which the string comes.
3258  * @param {String} opt_line The text of the line from which the string comes.
3259  * @private
3260  */
3261 
3262 // Parse the x as a float or return null if it's not a number.
3263 Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) {
3264   var val = parseFloat(x);
3265   if (!isNaN(val)) return val;
3266 
3267   // Try to figure out what happeend.
3268   // If the value is the empty string, parse it as null.
3269   if (/^ *$/.test(x)) return null;
3270 
3271   // If it was actually "NaN", return it as NaN.
3272   if (/^ *nan *$/i.test(x)) return NaN;
3273 
3274   // Looks like a parsing error.
3275   var msg = "Unable to parse '" + x + "' as a number";
3276   if (opt_line !== null && opt_line_no !== null) {
3277     msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV.";
3278   }
3279   this.error(msg);
3280 
3281   return null;
3282 };
3283 
3284 /**
3285  * @private
3286  * Parses a string in a special csv format.  We expect a csv file where each
3287  * line is a date point, and the first field in each line is the date string.
3288  * We also expect that all remaining fields represent series.
3289  * if the errorBars attribute is set, then interpret the fields as:
3290  * date, series1, stddev1, series2, stddev2, ...
3291  * @param {[Object]} data See above.
3292  *
3293  * @return [Object] An array with one entry for each row. These entries
3294  * are an array of cells in that row. The first entry is the parsed x-value for
3295  * the row. The second, third, etc. are the y-values. These can take on one of
3296  * three forms, depending on the CSV and constructor parameters:
3297  * 1. numeric value
3298  * 2. [ value, stddev ]
3299  * 3. [ low value, center value, high value ]
3300  */
3301 Dygraph.prototype.parseCSV_ = function(data) {
3302   var ret = [];
3303   var lines = data.split("\n");
3304 
3305   // Use the default delimiter or fall back to a tab if that makes sense.
3306   var delim = this.attr_('delimiter');
3307   if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
3308     delim = '\t';
3309   }
3310 
3311   var start = 0;
3312   if (this.labelsFromCSV_) {
3313     start = 1;
3314     this.attrs_.labels = lines[0].split(delim);
3315   }
3316   var line_no = 0;
3317 
3318   var xParser;
3319   var defaultParserSet = false;  // attempt to auto-detect x value type
3320   var expectedCols = this.attr_("labels").length;
3321   var outOfOrder = false;
3322   for (var i = start; i < lines.length; i++) {
3323     var line = lines[i];
3324     line_no = i;
3325     if (line.length == 0) continue;  // skip blank lines
3326     if (line[0] == '#') continue;    // skip comment lines
3327     var inFields = line.split(delim);
3328     if (inFields.length < 2) continue;
3329 
3330     var fields = [];
3331     if (!defaultParserSet) {
3332       this.detectTypeFromString_(inFields[0]);
3333       xParser = this.attr_("xValueParser");
3334       defaultParserSet = true;
3335     }
3336     fields[0] = xParser(inFields[0], this);
3337 
3338     // If fractions are expected, parse the numbers as "A/B"
3339     if (this.fractions_) {
3340       for (var j = 1; j < inFields.length; j++) {
3341         // TODO(danvk): figure out an appropriate way to flag parse errors.
3342         var vals = inFields[j].split("/");
3343         if (vals.length != 2) {
3344           this.error('Expected fractional "num/den" values in CSV data ' +
3345                      "but found a value '" + inFields[j] + "' on line " +
3346                      (1 + i) + " ('" + line + "') which is not of this form.");
3347           fields[j] = [0, 0];
3348         } else {
3349           fields[j] = [this.parseFloat_(vals[0], i, line),
3350                        this.parseFloat_(vals[1], i, line)];
3351         }
3352       }
3353     } else if (this.attr_("errorBars")) {
3354       // If there are error bars, values are (value, stddev) pairs
3355       if (inFields.length % 2 != 1) {
3356         this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
3357                    'but line ' + (1 + i) + ' has an odd number of values (' +
3358                    (inFields.length - 1) + "): '" + line + "'");
3359       }
3360       for (var j = 1; j < inFields.length; j += 2) {
3361         fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
3362                                this.parseFloat_(inFields[j + 1], i, line)];
3363       }
3364     } else if (this.attr_("customBars")) {
3365       // Bars are a low;center;high tuple
3366       for (var j = 1; j < inFields.length; j++) {
3367         var val = inFields[j];
3368         if (/^ *$/.test(val)) {
3369           fields[j] = [null, null, null];
3370         } else {
3371           var vals = val.split(";");
3372           if (vals.length == 3) {
3373             fields[j] = [ this.parseFloat_(vals[0], i, line),
3374                           this.parseFloat_(vals[1], i, line),
3375                           this.parseFloat_(vals[2], i, line) ];
3376           } else {
3377             this.warning('When using customBars, values must be either blank ' +
3378                          'or "low;center;high" tuples (got "' + val +
3379                          '" on line ' + (1+i));
3380           }
3381         }
3382       }
3383     } else {
3384       // Values are just numbers
3385       for (var j = 1; j < inFields.length; j++) {
3386         fields[j] = this.parseFloat_(inFields[j], i, line);
3387       }
3388     }
3389     if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
3390       outOfOrder = true;
3391     }
3392 
3393     if (fields.length != expectedCols) {
3394       this.error("Number of columns in line " + i + " (" + fields.length +
3395                  ") does not agree with number of labels (" + expectedCols +
3396                  ") " + line);
3397     }
3398 
3399     // If the user specified the 'labels' option and none of the cells of the
3400     // first row parsed correctly, then they probably double-specified the
3401     // labels. We go with the values set in the option, discard this row and
3402     // log a warning to the JS console.
3403     if (i == 0 && this.attr_('labels')) {
3404       var all_null = true;
3405       for (var j = 0; all_null && j < fields.length; j++) {
3406         if (fields[j]) all_null = false;
3407       }
3408       if (all_null) {
3409         this.warn("The dygraphs 'labels' option is set, but the first row of " +
3410                   "CSV data ('" + line + "') appears to also contain labels. " +
3411                   "Will drop the CSV labels and use the option labels.");
3412         continue;
3413       }
3414     }
3415     ret.push(fields);
3416   }
3417 
3418   if (outOfOrder) {
3419     this.warn("CSV is out of order; order it correctly to speed loading.");
3420     ret.sort(function(a,b) { return a[0] - b[0] });
3421   }
3422 
3423   return ret;
3424 };
3425 
3426 /**
3427  * @private
3428  * The user has provided their data as a pre-packaged JS array. If the x values
3429  * are numeric, this is the same as dygraphs' internal format. If the x values
3430  * are dates, we need to convert them from Date objects to ms since epoch.
3431  * @param {[Object]} data
3432  * @return {[Object]} data with numeric x values.
3433  */
3434 Dygraph.prototype.parseArray_ = function(data) {
3435   // Peek at the first x value to see if it's numeric.
3436   if (data.length == 0) {
3437     this.error("Can't plot empty data set");
3438     return null;
3439   }
3440   if (data[0].length == 0) {
3441     this.error("Data set cannot contain an empty row");
3442     return null;
3443   }
3444 
3445   if (this.attr_("labels") == null) {
3446     this.warn("Using default labels. Set labels explicitly via 'labels' " +
3447               "in the options parameter");
3448     this.attrs_.labels = [ "X" ];
3449     for (var i = 1; i < data[0].length; i++) {
3450       this.attrs_.labels.push("Y" + i);
3451     }
3452   }
3453 
3454   if (Dygraph.isDateLike(data[0][0])) {
3455     // Some intelligent defaults for a date x-axis.
3456     this.attrs_.xValueFormatter = Dygraph.dateString_;
3457     this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
3458     this.attrs_.xTicker = Dygraph.dateTicker;
3459 
3460     // Assume they're all dates.
3461     var parsedData = Dygraph.clone(data);
3462     for (var i = 0; i < data.length; i++) {
3463       if (parsedData[i].length == 0) {
3464         this.error("Row " + (1 + i) + " of data is empty");
3465         return null;
3466       }
3467       if (parsedData[i][0] == null
3468           || typeof(parsedData[i][0].getTime) != 'function'
3469           || isNaN(parsedData[i][0].getTime())) {
3470         this.error("x value in row " + (1 + i) + " is not a Date");
3471         return null;
3472       }
3473       parsedData[i][0] = parsedData[i][0].getTime();
3474     }
3475     return parsedData;
3476   } else {
3477     // Some intelligent defaults for a numeric x-axis.
3478     this.attrs_.xValueFormatter = function(x) { return x; };
3479     this.attrs_.xTicker = Dygraph.numericTicks;
3480     return data;
3481   }
3482 };
3483 
3484 /**
3485  * Parses a DataTable object from gviz.
3486  * The data is expected to have a first column that is either a date or a
3487  * number. All subsequent columns must be numbers. If there is a clear mismatch
3488  * between this.xValueParser_ and the type of the first column, it will be
3489  * fixed. Fills out rawData_.
3490  * @param {[Object]} data See above.
3491  * @private
3492  */
3493 Dygraph.prototype.parseDataTable_ = function(data) {
3494   var cols = data.getNumberOfColumns();
3495   var rows = data.getNumberOfRows();
3496 
3497   var indepType = data.getColumnType(0);
3498   if (indepType == 'date' || indepType == 'datetime') {
3499     this.attrs_.xValueFormatter = Dygraph.dateString_;
3500     this.attrs_.xValueParser = Dygraph.dateParser;
3501     this.attrs_.xTicker = Dygraph.dateTicker;
3502     this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
3503   } else if (indepType == 'number') {
3504     this.attrs_.xValueFormatter = function(x) { return x; };
3505     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
3506     this.attrs_.xTicker = Dygraph.numericTicks;
3507     this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
3508   } else {
3509     this.error("only 'date', 'datetime' and 'number' types are supported for " +
3510                "column 1 of DataTable input (Got '" + indepType + "')");
3511     return null;
3512   }
3513 
3514   // Array of the column indices which contain data (and not annotations).
3515   var colIdx = [];
3516   var annotationCols = {};  // data index -> [annotation cols]
3517   var hasAnnotations = false;
3518   for (var i = 1; i < cols; i++) {
3519     var type = data.getColumnType(i);
3520     if (type == 'number') {
3521       colIdx.push(i);
3522     } else if (type == 'string' && this.attr_('displayAnnotations')) {
3523       // This is OK -- it's an annotation column.
3524       var dataIdx = colIdx[colIdx.length - 1];
3525       if (!annotationCols.hasOwnProperty(dataIdx)) {
3526         annotationCols[dataIdx] = [i];
3527       } else {
3528         annotationCols[dataIdx].push(i);
3529       }
3530       hasAnnotations = true;
3531     } else {
3532       this.error("Only 'number' is supported as a dependent type with Gviz." +
3533                  " 'string' is only supported if displayAnnotations is true");
3534     }
3535   }
3536 
3537   // Read column labels
3538   // TODO(danvk): add support back for errorBars
3539   var labels = [data.getColumnLabel(0)];
3540   for (var i = 0; i < colIdx.length; i++) {
3541     labels.push(data.getColumnLabel(colIdx[i]));
3542     if (this.attr_("errorBars")) i += 1;
3543   }
3544   this.attrs_.labels = labels;
3545   cols = labels.length;
3546 
3547   var ret = [];
3548   var outOfOrder = false;
3549   var annotations = [];
3550   for (var i = 0; i < rows; i++) {
3551     var row = [];
3552     if (typeof(data.getValue(i, 0)) === 'undefined' ||
3553         data.getValue(i, 0) === null) {
3554       this.warn("Ignoring row " + i +
3555                 " of DataTable because of undefined or null first column.");
3556       continue;
3557     }
3558 
3559     if (indepType == 'date' || indepType == 'datetime') {
3560       row.push(data.getValue(i, 0).getTime());
3561     } else {
3562       row.push(data.getValue(i, 0));
3563     }
3564     if (!this.attr_("errorBars")) {
3565       for (var j = 0; j < colIdx.length; j++) {
3566         var col = colIdx[j];
3567         row.push(data.getValue(i, col));
3568         if (hasAnnotations &&
3569             annotationCols.hasOwnProperty(col) &&
3570             data.getValue(i, annotationCols[col][0]) != null) {
3571           var ann = {};
3572           ann.series = data.getColumnLabel(col);
3573           ann.xval = row[0];
3574           ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
3575           ann.text = '';
3576           for (var k = 0; k < annotationCols[col].length; k++) {
3577             if (k) ann.text += "\n";
3578             ann.text += data.getValue(i, annotationCols[col][k]);
3579           }
3580           annotations.push(ann);
3581         }
3582       }
3583 
3584       // Strip out infinities, which give dygraphs problems later on.
3585       for (var j = 0; j < row.length; j++) {
3586         if (!isFinite(row[j])) row[j] = null;
3587       }
3588     } else {
3589       for (var j = 0; j < cols - 1; j++) {
3590         row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
3591       }
3592     }
3593     if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
3594       outOfOrder = true;
3595     }
3596     ret.push(row);
3597   }
3598 
3599   if (outOfOrder) {
3600     this.warn("DataTable is out of order; order it correctly to speed loading.");
3601     ret.sort(function(a,b) { return a[0] - b[0] });
3602   }
3603   this.rawData_ = ret;
3604 
3605   if (annotations.length > 0) {
3606     this.setAnnotations(annotations, true);
3607   }
3608 }
3609 
3610 /**
3611  * @private
3612  * This is identical to JavaScript's built-in Date.parse() method, except that
3613  * it doesn't get replaced with an incompatible method by aggressive JS
3614  * libraries like MooTools or Joomla.
3615  * @param { String } str The date string, e.g. "2011/05/06"
3616  * @return { Integer } millis since epoch
3617  */
3618 Dygraph.dateStrToMillis = function(str) {
3619   return new Date(str).getTime();
3620 };
3621 
3622 // These functions are all based on MochiKit.
3623 /**
3624  * @private
3625  */
3626 Dygraph.update = function (self, o) {
3627   if (typeof(o) != 'undefined' && o !== null) {
3628     for (var k in o) {
3629       if (o.hasOwnProperty(k)) {
3630         self[k] = o[k];
3631       }
3632     }
3633   }
3634   return self;
3635 };
3636 
3637 /**
3638  * @private
3639  */
3640 Dygraph.isArrayLike = function (o) {
3641   var typ = typeof(o);
3642   if (
3643       (typ != 'object' && !(typ == 'function' &&
3644         typeof(o.item) == 'function')) ||
3645       o === null ||
3646       typeof(o.length) != 'number' ||
3647       o.nodeType === 3
3648      ) {
3649     return false;
3650   }
3651   return true;
3652 };
3653 
3654 /**
3655  * @private
3656  */
3657 Dygraph.isDateLike = function (o) {
3658   if (typeof(o) != "object" || o === null ||
3659       typeof(o.getTime) != 'function') {
3660     return false;
3661   }
3662   return true;
3663 };
3664 
3665 /**
3666  * @private
3667  */
3668 Dygraph.clone = function(o) {
3669   // TODO(danvk): figure out how MochiKit's version works
3670   var r = [];
3671   for (var i = 0; i < o.length; i++) {
3672     if (Dygraph.isArrayLike(o[i])) {
3673       r.push(Dygraph.clone(o[i]));
3674     } else {
3675       r.push(o[i]);
3676     }
3677   }
3678   return r;
3679 };
3680 
3681 
3682 /**
3683  * Get the CSV data. If it's in a function, call that function. If it's in a
3684  * file, do an XMLHttpRequest to get it.
3685  * @private
3686  */
3687 Dygraph.prototype.start_ = function() {
3688   if (typeof this.file_ == 'function') {
3689     // CSV string. Pretend we got it via XHR.
3690     this.loadedEvent_(this.file_());
3691   } else if (Dygraph.isArrayLike(this.file_)) {
3692     this.rawData_ = this.parseArray_(this.file_);
3693     this.predraw_();
3694   } else if (typeof this.file_ == 'object' &&
3695              typeof this.file_.getColumnRange == 'function') {
3696     // must be a DataTable from gviz.
3697     this.parseDataTable_(this.file_);
3698     this.predraw_();
3699   } else if (typeof this.file_ == 'string') {
3700     // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3701     if (this.file_.indexOf('\n') >= 0) {
3702       this.loadedEvent_(this.file_);
3703     } else {
3704       var req = new XMLHttpRequest();
3705       var caller = this;
3706       req.onreadystatechange = function () {
3707         if (req.readyState == 4) {
3708           if (req.status == 200) {
3709             caller.loadedEvent_(req.responseText);
3710           }
3711         }
3712       };
3713 
3714       req.open("GET", this.file_, true);
3715       req.send(null);
3716     }
3717   } else {
3718     this.error("Unknown data format: " + (typeof this.file_));
3719   }
3720 };
3721 
3722 /**
3723  * Changes various properties of the graph. These can include:
3724  * <ul>
3725  * <li>file: changes the source data for the graph</li>
3726  * <li>errorBars: changes whether the data contains stddev</li>
3727  * </ul>
3728  *
3729  * @param {Object} attrs The new properties and values
3730  */
3731 Dygraph.prototype.updateOptions = function(attrs) {
3732   // TODO(danvk): this is a mess. Rethink this function.
3733   if ('rollPeriod' in attrs) {
3734     this.rollPeriod_ = attrs.rollPeriod;
3735   }
3736   if ('dateWindow' in attrs) {
3737     this.dateWindow_ = attrs.dateWindow;
3738     if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) {
3739       this.zoomed_x_ = attrs.dateWindow != null;
3740     }
3741   }
3742   if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) {
3743     this.zoomed_y_ = attrs.valueRange != null;
3744   }
3745 
3746   // TODO(danvk): validate per-series options.
3747   // Supported:
3748   // strokeWidth
3749   // pointSize
3750   // drawPoints
3751   // highlightCircleSize
3752 
3753   Dygraph.update(this.user_attrs_, attrs);
3754   Dygraph.update(this.renderOptions_, attrs);
3755 
3756   this.labelsFromCSV_ = (this.attr_("labels") == null);
3757 
3758   // TODO(danvk): this doesn't match the constructor logic
3759   this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
3760   if (attrs['file']) {
3761     this.file_ = attrs['file'];
3762     this.start_();
3763   } else {
3764     this.predraw_();
3765   }
3766 };
3767 
3768 /**
3769  * Resizes the dygraph. If no parameters are specified, resizes to fill the
3770  * containing div (which has presumably changed size since the dygraph was
3771  * instantiated. If the width/height are specified, the div will be resized.
3772  *
3773  * This is far more efficient than destroying and re-instantiating a
3774  * Dygraph, since it doesn't have to reparse the underlying data.
3775  *
3776  * @param {Number} [width] Width (in pixels)
3777  * @param {Number} [height] Height (in pixels)
3778  */
3779 Dygraph.prototype.resize = function(width, height) {
3780   if (this.resize_lock) {
3781     return;
3782   }
3783   this.resize_lock = true;
3784 
3785   if ((width === null) != (height === null)) {
3786     this.warn("Dygraph.resize() should be called with zero parameters or " +
3787               "two non-NULL parameters. Pretending it was zero.");
3788     width = height = null;
3789   }
3790 
3791   // TODO(danvk): there should be a clear() method.
3792   this.maindiv_.innerHTML = "";
3793   this.attrs_.labelsDiv = null;
3794 
3795   if (width) {
3796     this.maindiv_.style.width = width + "px";
3797     this.maindiv_.style.height = height + "px";
3798     this.width_ = width;
3799     this.height_ = height;
3800   } else {
3801     this.width_ = this.maindiv_.offsetWidth;
3802     this.height_ = this.maindiv_.offsetHeight;
3803   }
3804 
3805   this.createInterface_();
3806   this.predraw_();
3807 
3808   this.resize_lock = false;
3809 };
3810 
3811 /**
3812  * Adjusts the number of points in the rolling average. Updates the graph to
3813  * reflect the new averaging period.
3814  * @param {Number} length Number of points over which to average the data.
3815  */
3816 Dygraph.prototype.adjustRoll = function(length) {
3817   this.rollPeriod_ = length;
3818   this.predraw_();
3819 };
3820 
3821 /**
3822  * Returns a boolean array of visibility statuses.
3823  */
3824 Dygraph.prototype.visibility = function() {
3825   // Do lazy-initialization, so that this happens after we know the number of
3826   // data series.
3827   if (!this.attr_("visibility")) {
3828     this.attrs_["visibility"] = [];
3829   }
3830   while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
3831     this.attr_("visibility").push(true);
3832   }
3833   return this.attr_("visibility");
3834 };
3835 
3836 /**
3837  * Changes the visiblity of a series.
3838  */
3839 Dygraph.prototype.setVisibility = function(num, value) {
3840   var x = this.visibility();
3841   if (num < 0 || num >= x.length) {
3842     this.warn("invalid series number in setVisibility: " + num);
3843   } else {
3844     x[num] = value;
3845     this.predraw_();
3846   }
3847 };
3848 
3849 /**
3850  * Update the list of annotations and redraw the chart.
3851  */
3852 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3853   // Only add the annotation CSS rule once we know it will be used.
3854   Dygraph.addAnnotationRule();
3855   this.annotations_ = ann;
3856   this.layout_.setAnnotations(this.annotations_);
3857   if (!suppressDraw) {
3858     this.predraw_();
3859   }
3860 };
3861 
3862 /**
3863  * Return the list of annotations.
3864  */
3865 Dygraph.prototype.annotations = function() {
3866   return this.annotations_;
3867 };
3868 
3869 /**
3870  * Get the index of a series (column) given its name. The first column is the
3871  * x-axis, so the data series start with index 1.
3872  */
3873 Dygraph.prototype.indexFromSetName = function(name) {
3874   var labels = this.attr_("labels");
3875   for (var i = 0; i < labels.length; i++) {
3876     if (labels[i] == name) return i;
3877   }
3878   return null;
3879 };
3880 
3881 /**
3882  * @private
3883  * Adds a default style for the annotation CSS classes to the document. This is
3884  * only executed when annotations are actually used. It is designed to only be
3885  * called once -- all calls after the first will return immediately.
3886  */
3887 Dygraph.addAnnotationRule = function() {
3888   if (Dygraph.addedAnnotationCSS) return;
3889 
3890   var rule = "border: 1px solid black; " +
3891              "background-color: white; " +
3892              "text-align: center;";
3893 
3894   var styleSheetElement = document.createElement("style");
3895   styleSheetElement.type = "text/css";
3896   document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
3897 
3898   // Find the first style sheet that we can access.
3899   // We may not add a rule to a style sheet from another domain for security
3900   // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3901   // adds its own style sheets from google.com.
3902   for (var i = 0; i < document.styleSheets.length; i++) {
3903     if (document.styleSheets[i].disabled) continue;
3904     var mysheet = document.styleSheets[i];
3905     try {
3906       if (mysheet.insertRule) {  // Firefox
3907         var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
3908         mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
3909       } else if (mysheet.addRule) {  // IE
3910         mysheet.addRule(".dygraphDefaultAnnotation", rule);
3911       }
3912       Dygraph.addedAnnotationCSS = true;
3913       return;
3914     } catch(err) {
3915       // Was likely a security exception.
3916     }
3917   }
3918 
3919   this.warn("Unable to add default annotation CSS rule; display may be off.");
3920 }
3921 
3922 /**
3923  * @private
3924  * Create a new canvas element. This is more complex than a simple
3925  * document.createElement("canvas") because of IE and excanvas.
3926  */
3927 Dygraph.createCanvas = function() {
3928   var canvas = document.createElement("canvas");
3929 
3930   isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
3931   if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
3932     canvas = G_vmlCanvasManager.initElement(canvas);
3933   }
3934 
3935   return canvas;
3936 };
3937 
3938 
3939 /**
3940  * A wrapper around Dygraph that implements the gviz API.
3941  * @param {Object} container The DOM object the visualization should live in.
3942  */
3943 Dygraph.GVizChart = function(container) {
3944   this.container = container;
3945 }
3946 
3947 Dygraph.GVizChart.prototype.draw = function(data, options) {
3948   // Clear out any existing dygraph.
3949   // TODO(danvk): would it make more sense to simply redraw using the current
3950   // date_graph object?
3951   this.container.innerHTML = '';
3952   if (typeof(this.date_graph) != 'undefined') {
3953     this.date_graph.destroy();
3954   }
3955 
3956   this.date_graph = new Dygraph(this.container, data, options);
3957 }
3958 
3959 /**
3960  * Google charts compatible setSelection
3961  * Only row selection is supported, all points in the row will be highlighted
3962  * @param {Array} array of the selected cells
3963  * @public
3964  */
3965 Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
3966   var row = false;
3967   if (selection_array.length) {
3968     row = selection_array[0].row;
3969   }
3970   this.date_graph.setSelection(row);
3971 }
3972 
3973 /**
3974  * Google charts compatible getSelection implementation
3975  * @return {Array} array of the selected cells
3976  * @public
3977  */
3978 Dygraph.GVizChart.prototype.getSelection = function() {
3979   var selection = [];
3980 
3981   var row = this.date_graph.getSelection();
3982 
3983   if (row < 0) return selection;
3984 
3985   col = 1;
3986   for (var i in this.date_graph.layout_.datasets) {
3987     selection.push({row: row, column: col});
3988     col++;
3989   }
3990 
3991   return selection;
3992 }
3993 
3994 // Older pages may still use this name.
3995 DateGraph = Dygraph;
3996 
3997 // <REMOVE_FOR_COMBINED>
3998 Dygraph.OPTIONS_REFERENCE =  // <JSON>
3999 {
4000   "xValueParser": {
4001     "default": "parseFloat() or Date.parse()*",
4002     "labels": ["CSV parsing"],
4003     "type": "function(str) -> number",
4004     "description": "A function which parses x-values (i.e. the dependent series). Must return a number, even when the values are dates. In this case, millis since epoch are used. This is used primarily for parsing CSV data. *=Dygraphs is slightly more accepting in the dates which it will parse. See code for details."
4005   },
4006   "stackedGraph": {
4007     "default": "false",
4008     "labels": ["Data Line display"],
4009     "type": "boolean",
4010     "description": "If set, stack series on top of one another rather than drawing them independently."
4011   },
4012   "pointSize": {
4013     "default": "1",
4014     "labels": ["Data Line display"],
4015     "type": "integer",
4016     "description": "The size of the dot to draw on each point in pixels (see drawPoints). A dot is always drawn when a point is \"isolated\", i.e. there is a missing point on either side of it. This also controls the size of those dots."
4017   },
4018   "labelsDivStyles": {
4019     "default": "null",
4020     "labels": ["Legend"],
4021     "type": "{}",
4022     "description": "Additional styles to apply to the currently-highlighted points div. For example, { 'font-weight': 'bold' } will make the labels bold."
4023   },
4024   "drawPoints": {
4025     "default": "false",
4026     "labels": ["Data Line display"],
4027     "type": "boolean",
4028     "description": "Draw a small dot at each point, in addition to a line going through the point. This makes the individual data points easier to see, but can increase visual clutter in the chart."
4029   },
4030   "height": {
4031     "default": "320",
4032     "labels": ["Overall display"],
4033     "type": "integer",
4034     "description": "Height, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
4035   },
4036   "zoomCallback": {
4037     "default": "null",
4038     "labels": ["Callbacks"],
4039     "type": "function(minDate, maxDate, yRanges)",
4040     "description": "A function to call when the zoom window is changed (either by zooming in or out). minDate and maxDate are milliseconds since epoch. yRanges is an array of [bottom, top] pairs, one for each y-axis."
4041   },
4042   "pointClickCallback": {
4043     "default": "",
4044     "labels": ["Callbacks", "Interactive Elements"],
4045     "type": "",
4046     "description": ""
4047   },
4048   "colors": {
4049     "default": "(see description)",
4050     "labels": ["Data Series Colors"],
4051     "type": "array<string>",
4052     "example": "['red', '#00FF00']",
4053     "description": "List of colors for the data series. These can be of the form \"#AABBCC\" or \"rgb(255,100,200)\" or \"yellow\", etc. If not specified, equally-spaced points around a color wheel are used."
4054   },
4055   "connectSeparatedPoints": {
4056     "default": "false",
4057     "labels": ["Data Line display"],
4058     "type": "boolean",
4059     "description": "Usually, when Dygraphs encounters a missing value in a data series, it interprets this as a gap and draws it as such. If, instead, the missing values represents an x-value for which only a different series has data, then you'll want to connect the dots by setting this to true. To explicitly include a gap with this option set, use a value of NaN."
4060   },
4061   "highlightCallback": {
4062     "default": "null",
4063     "labels": ["Callbacks"],
4064     "type": "function(event, x, points,row)",
4065     "description": "When set, this callback gets called every time a new point is highlighted. The parameters are the JavaScript mousemove event, the x-coordinate of the highlighted points and an array of highlighted points: <code>[ {name: 'series', yval: y-value}, … ]</code>"
4066   },
4067   "includeZero": {
4068     "default": "false",
4069     "labels": ["Axis display"],
4070     "type": "boolean",
4071     "description": "Usually, dygraphs will use the range of the data plus some padding to set the range of the y-axis. If this option is set, the y-axis will always include zero, typically as the lowest value. This can be used to avoid exaggerating the variance in the data"
4072   },
4073   "rollPeriod": {
4074     "default": "1",
4075     "labels": ["Error Bars", "Rolling Averages"],
4076     "type": "integer >= 1",
4077     "description": "Number of days over which to average data. Discussed extensively above."
4078   },
4079   "unhighlightCallback": {
4080     "default": "null",
4081     "labels": ["Callbacks"],
4082     "type": "function(event)",
4083     "description": "When set, this callback gets called every time the user stops highlighting any point by mousing out of the graph.  The parameter is the mouseout event."
4084   },
4085   "axisTickSize": {
4086     "default": "3.0",
4087     "labels": ["Axis display"],
4088     "type": "number",
4089     "description": "The size of the line to display next to each tick mark on x- or y-axes."
4090   },
4091   "labelsSeparateLines": {
4092     "default": "false",
4093     "labels": ["Legend"],
4094     "type": "boolean",
4095     "description": "Put <code><br/></code> between lines in the label string. Often used in conjunction with <strong>labelsDiv</strong>."
4096   },
4097   "xValueFormatter": {
4098     "default": "(Round to 2 decimal places)",
4099     "labels": ["Axis display"],
4100     "type": "function(x)",
4101     "description": "Function to provide a custom display format for the X value for mouseover."
4102   },
4103   "pixelsPerYLabel": {
4104     "default": "30",
4105     "labels": ["Axis display", "Grid"],
4106     "type": "integer",
4107     "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks."
4108   },
4109   "annotationMouseOverHandler": {
4110     "default": "null",
4111     "labels": ["Annotations"],
4112     "type": "function(annotation, point, dygraph, event)",
4113     "description": "If provided, this function is called whenever the user mouses over an annotation."
4114   },
4115   "annotationMouseOutHandler": {
4116     "default": "null",
4117     "labels": ["Annotations"],
4118     "type": "function(annotation, point, dygraph, event)",
4119     "description": "If provided, this function is called whenever the user mouses out of an annotation."
4120   },
4121   "annotationClickHandler": {
4122     "default": "null",
4123     "labels": ["Annotations"],
4124     "type": "function(annotation, point, dygraph, event)",
4125     "description": "If provided, this function is called whenever the user clicks on an annotation."
4126   },
4127   "annotationDblClickHandler": {
4128     "default": "null",
4129     "labels": ["Annotations"],
4130     "type": "function(annotation, point, dygraph, event)",
4131     "description": "If provided, this function is called whenever the user double-clicks on an annotation."
4132   },
4133   "drawCallback": {
4134     "default": "null",
4135     "labels": ["Callbacks"],
4136     "type": "function(dygraph, is_initial)",
4137     "description": "When set, this callback gets called every time the dygraph is drawn. This includes the initial draw, after zooming and repeatedly while panning. The first parameter is the dygraph being drawn. The second is a boolean value indicating whether this is the initial draw."
4138   },
4139   "labelsKMG2": {
4140     "default": "false",
4141     "labels": ["Value display/formatting"],
4142     "type": "boolean",
4143     "description": "Show k/M/G for kilo/Mega/Giga on y-axis. This is different than <code>labelsKMB</code> in that it uses base 2, not 10."
4144   },
4145   "delimiter": {
4146     "default": ",",
4147     "labels": ["CSV parsing"],
4148     "type": "string",
4149     "description": "The delimiter to look for when separating fields of a CSV file. Setting this to a tab is not usually necessary, since tab-delimited data is auto-detected."
4150   },
4151   "axisLabelFontSize": {
4152     "default": "14",
4153     "labels": ["Axis display"],
4154     "type": "integer",
4155     "description": "Size of the font (in pixels) to use in the axis labels, both x- and y-axis."
4156   },
4157   "underlayCallback": {
4158     "default": "null",
4159     "labels": ["Callbacks"],
4160     "type": "function(canvas, area, dygraph)",
4161     "description": "When set, this callback gets called before the chart is drawn. It details on how to use this."
4162   },
4163   "width": {
4164     "default": "480",
4165     "labels": ["Overall display"],
4166     "type": "integer",
4167     "description": "Width, in pixels, of the chart. If the container div has been explicitly sized, this will be ignored."
4168   },
4169   "interactionModel": {
4170     "default": "...",
4171     "labels": ["Interactive Elements"],
4172     "type": "Object",
4173     "description": "TODO(konigsberg): document this"
4174   },
4175   "xTicker": {
4176     "default": "Dygraph.dateTicker or Dygraph.numericTicks",
4177     "labels": ["Axis display"],
4178     "type": "function(min, max, dygraph) -> [{v: ..., label: ...}, ...]",
4179     "description": "This lets you specify an arbitrary function to generate tick marks on an axis. The tick marks are an array of (value, label) pairs. The built-in functions go to great lengths to choose good tick marks so, if you set this option, you'll most likely want to call one of them and modify the result."
4180   },
4181   "xAxisLabelWidth": {
4182     "default": "50",
4183     "labels": ["Axis display"],
4184     "type": "integer",
4185     "description": "Width, in pixels, of the x-axis labels."
4186   },
4187   "showLabelsOnHighlight": {
4188     "default": "true",
4189     "labels": ["Interactive Elements", "Legend"],
4190     "type": "boolean",
4191     "description": "Whether to show the legend upon mouseover."
4192   },
4193   "axis": {
4194     "default": "(none)",
4195     "labels": ["Axis display"],
4196     "type": "string or object",
4197     "description": "Set to either an object ({}) filled with options for this axis or to the name of an existing data series with its own axis to re-use that axis. See tests for usage."
4198   },
4199   "pixelsPerXLabel": {
4200     "default": "60",
4201     "labels": ["Axis display", "Grid"],
4202     "type": "integer",
4203     "description": "Number of pixels to require between each x- and y-label. Larger values will yield a sparser axis with fewer ticks."
4204   },
4205   "labelsDiv": {
4206     "default": "null",
4207     "labels": ["Legend"],
4208     "type": "DOM element or string",
4209     "example": "<code style='font-size: small'>document.getElementById('foo')</code>or<code>'foo'",
4210     "description": "Show data labels in an external div, rather than on the graph.  This value can either be a div element or a div id."
4211   },
4212   "fractions": {
4213     "default": "false",
4214     "labels": ["CSV parsing", "Error Bars"],
4215     "type": "boolean",
4216     "description": "When set, attempt to parse each cell in the CSV file as \"a/b\", where a and b are integers. The ratio will be plotted. This allows computation of Wilson confidence intervals (see below)."
4217   },
4218   "logscale": {
4219     "default": "false",
4220     "labels": ["Axis display"],
4221     "type": "boolean",
4222     "description": "When set for a y-axis, the graph shows that axis in log scale. Any values less than or equal to zero are not displayed.\n\nNot compatible with showZero, and ignores connectSeparatedPoints. Also, showing log scale with valueRanges that are less than zero will result in an unviewable graph."
4223   },
4224   "strokeWidth": {
4225     "default": "1.0",
4226     "labels": ["Data Line display"],
4227     "type": "integer",
4228     "example": "0.5, 2.0",
4229     "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs."
4230   },
4231   "wilsonInterval": {
4232     "default": "true",
4233     "labels": ["Error Bars"],
4234     "type": "boolean",
4235     "description": "Use in conjunction with the \"fractions\" option. Instead of plotting +/- N standard deviations, dygraphs will compute a Wilson confidence interval and plot that. This has more reasonable behavior for ratios close to 0 or 1."
4236   },
4237   "fillGraph": {
4238     "default": "false",
4239     "labels": ["Data Line display"],
4240     "type": "boolean",
4241     "description": "Should the area underneath the graph be filled? This option is not compatible with error bars."
4242   },
4243   "highlightCircleSize": {
4244     "default": "3",
4245     "labels": ["Interactive Elements"],
4246     "type": "integer",
4247     "description": "The size in pixels of the dot drawn over highlighted points."
4248   },
4249   "gridLineColor": {
4250     "default": "rgb(128,128,128)",
4251     "labels": ["Grid"],
4252     "type": "red, blue",
4253     "description": "The color of the gridlines."
4254   },
4255   "visibility": {
4256     "default": "[true, true, ...]",
4257     "labels": ["Data Line display"],
4258     "type": "Array of booleans",
4259     "description": "Which series should initially be visible? Once the Dygraph has been constructed, you can access and modify the visibility of each series using the <code>visibility</code> and <code>setVisibility</code> methods."
4260   },
4261   "valueRange": {
4262     "default": "Full range of the input is shown",
4263     "labels": ["Axis display"],
4264     "type": "Array of two numbers",
4265     "example": "[10, 110]",
4266     "description": "Explicitly set the vertical range of the graph to [low, high]."
4267   },
4268   "labelsDivWidth": {
4269     "default": "250",
4270     "labels": ["Legend"],
4271     "type": "integer",
4272     "description": "Width (in pixels) of the div which shows information on the currently-highlighted points."
4273   },
4274   "colorSaturation": {
4275     "default": "1.0",
4276     "labels": ["Data Series Colors"],
4277     "type": "0.0 - 1.0",
4278     "description": "If <strong>colors</strong> is not specified, saturation of the automatically-generated data series colors."
4279   },
4280   "yAxisLabelWidth": {
4281     "default": "50",
4282     "labels": ["Axis display"],
4283     "type": "integer",
4284     "description": "Width, in pixels, of the y-axis labels."
4285   },
4286   "hideOverlayOnMouseOut": {
4287     "default": "true",
4288     "labels": ["Interactive Elements", "Legend"],
4289     "type": "boolean",
4290     "description": "Whether to hide the legend when the mouse leaves the chart area."
4291   },
4292   "yValueFormatter": {
4293     "default": "(Round to 2 decimal places)",
4294     "labels": ["Axis display"],
4295     "type": "function(x)",
4296     "description": "Function to provide a custom display format for the Y value for mouseover."
4297   },
4298   "legend": {
4299     "default": "onmouseover",
4300     "labels": ["Legend"],
4301     "type": "string",
4302     "description": "When to display the legend. By default, it only appears when a user mouses over the chart. Set it to \"always\" to always display a legend of some sort."
4303   },
4304   "labelsShowZeroValues": {
4305     "default": "true",
4306     "labels": ["Legend"],
4307     "type": "boolean",
4308     "description": "Show zero value labels in the labelsDiv."
4309   },
4310   "stepPlot": {
4311     "default": "false",
4312     "labels": ["Data Line display"],
4313     "type": "boolean",
4314     "description": "When set, display the graph as a step plot instead of a line plot."
4315   },
4316   "labelsKMB": {
4317     "default": "false",
4318     "labels": ["Value display/formatting"],
4319     "type": "boolean",
4320     "description": "Show K/M/B for thousands/millions/billions on y-axis."
4321   },
4322   "rightGap": {
4323     "default": "5",
4324     "labels": ["Overall display"],
4325     "type": "integer",
4326     "description": "Number of pixels to leave blank at the right edge of the Dygraph. This makes it easier to highlight the right-most data point."
4327   },
4328   "avoidMinZero": {
4329     "default": "false",
4330     "labels": ["Axis display"],
4331     "type": "boolean",
4332     "description": "When set, the heuristic that fixes the Y axis at zero for a data set with the minimum Y value of zero is disabled. \nThis is particularly useful for data sets that contain many zero values, especially for step plots which may otherwise have lines not visible running along the bottom axis."
4333   },
4334   "xAxisLabelFormatter": {
4335     "default": "Dygraph.dateAxisFormatter",
4336     "labels": ["Axis display", "Value display/formatting"],
4337     "type": "function(date, granularity)",
4338     "description": "Function to call to format values along the x axis."
4339   },
4340   "clickCallback": {
4341     "snippet": "function(e, date){<br>  alert(date);<br>}",
4342     "default": "null",
4343     "labels": ["Callbacks"],
4344     "type": "function(e, date)",
4345     "description": "A function to call when a data point is clicked. The function should take two arguments, the event object for the click and the date that was clicked."
4346   },
4347   "yAxisLabelFormatter": {
4348     "default": "yValueFormatter",
4349     "labels": ["Axis display", "Value display/formatting"],
4350     "type": "function(x)",
4351     "description": "Function used to format values along the Y axis. By default it uses the same as the <code>yValueFormatter</code> unless specified."
4352   },
4353   "labels": {
4354     "default": "[\"X\", \"Y1\", \"Y2\", ...]*",
4355     "labels": ["Legend"],
4356     "type": "array<string>",
4357     "description": "A name for each data series, including the independent (X) series. For CSV files and DataTable objections, this is determined by context. For raw data, this must be specified. If it is not, default values are supplied and a warning is logged."
4358   },
4359   "dateWindow": {
4360     "default": "Full range of the input is shown",
4361     "labels": ["Axis display"],
4362     "type": "Array of two Dates or numbers",
4363     "example": "[<br>  Date.parse('2006-01-01'),<br>  (new Date()).valueOf()<br>]",
4364     "description": "Initially zoom in on a section of the graph. Is of the form [earliest, latest], where earliest/latest are milliseconds since epoch. If the data for the x-axis is numeric, the values in dateWindow must also be numbers."
4365   },
4366   "showRoller": {
4367     "default": "false",
4368     "labels": ["Interactive Elements", "Rolling Averages"],
4369     "type": "boolean",
4370     "description": "If the rolling average period text box should be shown."
4371   },
4372   "sigma": {
4373     "default": "2.0",
4374     "labels": ["Error Bars"],
4375     "type": "float",
4376     "description": "When errorBars is set, shade this many standard deviations above/below each point."
4377   },
4378   "customBars": {
4379     "default": "false",
4380     "labels": ["CSV parsing", "Error Bars"],
4381     "type": "boolean",
4382     "description": "When set, parse each CSV cell as \"low;middle;high\". Error bars will be drawn for each point between low and high, with the series itself going through middle."
4383   },
4384   "colorValue": {
4385     "default": "1.0",
4386     "labels": ["Data Series Colors"],
4387     "type": "float (0.0 - 1.0)",
4388     "description": "If colors is not specified, value of the data series colors, as in hue/saturation/value. (0.0-1.0, default 0.5)"
4389   },
4390   "errorBars": {
4391     "default": "false",
4392     "labels": ["CSV parsing", "Error Bars"],
4393     "type": "boolean",
4394     "description": "Does the data contain standard deviations? Setting this to true alters the input format (see above)."
4395   },
4396   "displayAnnotations": {
4397     "default": "false",
4398     "labels": ["Annotations"],
4399     "type": "boolean",
4400     "description": "Only applies when Dygraphs is used as a GViz chart. Causes string columns following a data series to be interpreted as annotations on points in that series. This is the same format used by Google's AnnotatedTimeLine chart."
4401   },
4402   "panEdgeFraction": {
4403     "default": "null",
4404     "labels": ["Axis Display", "Interactive Elements"],
4405     "type": "float",
4406     "default": "null",
4407     "description": "A value representing the farthest a graph may be panned, in percent of the display. For example, a value of 0.1 means that the graph can only be panned 10% pased the edges of the displayed values. null means no bounds."
4408   },
4409   "title": {
4410     "labels": ["Chart labels"],
4411     "type": "string",
4412     "default": "null",
4413     "description": "Text to display above the chart. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-title' classes."
4414   },
4415   "titleHeight": {
4416     "default": "18",
4417     "labels": ["Chart labels"],
4418     "type": "integer",
4419     "description": "Height of the chart title, in pixels. This also controls the default font size of the title. If you style the title on your own, this controls how much space is set aside above the chart for the title's div."
4420   },
4421   "xlabel": {
4422     "labels": ["Chart labels"],
4423     "type": "string",
4424     "default": "null",
4425     "description": "Text to display below the chart's x-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-xlabel' classes."
4426   },
4427   "xLabelHeight": {
4428     "labels": ["Chart labels"],
4429     "type": "integer",
4430     "default": "18",
4431     "description": "Height of the x-axis label, in pixels. This also controls the default font size of the x-axis label. If you style the label on your own, this controls how much space is set aside below the chart for the x-axis label's div."
4432   },
4433   "ylabel": {
4434     "labels": ["Chart labels"],
4435     "type": "string",
4436     "default": "null",
4437     "description": "Text to display to the left of the chart's y-axis. You can supply any HTML for this value, not just text. If you wish to style it using CSS, use the 'dygraph-label' or 'dygraph-ylabel' classes. The text will be rotated 90 degrees by default, so CSS rules may behave in unintuitive ways. No additional space is set aside for a y-axis label. If you need more space, increase the width of the y-axis tick labels using the yAxisLabelWidth option. If you need a wider div for the y-axis label, either style it that way with CSS (but remember that it's rotated, so width is controlled by the 'height' property) or set the yLabelWidth option."
4438   },
4439   "yLabelWidth": {
4440     "labels": ["Chart labels"],
4441     "type": "integer",
4442     "default": "18",
4443     "description": "Width of the div which contains the y-axis label. Since the y-axis label appears rotated 90 degrees, this actually affects the height of its div."
4444   },
4445   "isZoomedIgnoreProgrammaticZoom" : {
4446     "default": "false",
4447     "labels": ["Zooming"],
4448     "type": "boolean",
4449     "description" : "When this option is passed to updateOptions() along with either the <code>dateWindow</code> or <code>valueRange</code> options, the zoom flags are not changed to reflect a zoomed state. This is primarily useful for when the display area of a chart is changed programmatically and also where manual zooming is allowed and use is made of the <code>isZoomed</code> method to determine this."
4450   },
4451   "sigFigs" : {
4452     "default": "null",
4453     "labels": ["Value display/formatting"],
4454     "type": "integer",
4455     "description": "By default, dygraphs displays numbers with a fixed number of digits after the decimal point. If you'd prefer to have a fixed number of significant figures, set this option to that number of sig figs. A value of 2, for instance, would cause 1 to be display as 1.0 and 1234 to be displayed as 1.23e+3."
4456   },
4457   "digitsAfterDecimal" : {
4458     "default": "2",
4459     "labels": ["Value display/formatting"],
4460     "type": "integer",
4461     "description": "Unless it's run in scientific mode (see the <code>sigFigs</code> option), dygraphs displays numbers with <code>digitsAfterDecimal</code> digits after the decimal point. Trailing zeros are not displayed, so with a value of 2 you'll get '0', '0.1', '0.12', '123.45' but not '123.456' (it will be rounded to '123.46'). Numbers with absolute value less than 0.1^digitsAfterDecimal (i.e. those which would show up as '0.00') will be displayed in scientific notation."
4462   },
4463   "maxNumberWidth" : {
4464     "default": "6",
4465     "labels": ["Value display/formatting"],
4466     "type": "integer",
4467     "description": "When displaying numbers in normal (not scientific) mode, large numbers will be displayed with many trailing zeros (e.g. 100000000 instead of 1e9). This can lead to unwieldy y-axis labels. If there are more than <code>maxNumberWidth</code> digits to the left of the decimal in a number, dygraphs will switch to scientific notation, even when not operating in scientific mode. If you'd like to see all those digits, set this to something large, like 20 or 30."
4468   }
4469 }
4470 ;  // </JSON>
4471 // NOTE: in addition to parsing as JS, this snippet is expected to be valid
4472 // JSON. This assumption cannot be checked in JS, but it will be checked when
4473 // documentation is generated by the generate-documentation.py script. For the
4474 // most part, this just means that you should always use double quotes.
4475 
4476 // Do a quick sanity check on the options reference.
4477 (function() {
4478   var warn = function(msg) { if (console) console.warn(msg); };
4479   var flds = ['type', 'default', 'description'];
4480   var valid_cats = [ 
4481    'Annotations',
4482    'Axis display',
4483    'Chart labels',
4484    'CSV parsing',
4485    'Callbacks',
4486    'Data Line display',
4487    'Data Series Colors',
4488    'Error Bars',
4489    'Grid',
4490    'Interactive Elements',
4491    'Legend',
4492    'Overall display',
4493    'Rolling Averages',
4494    'Value display/formatting',
4495    'Zooming'
4496   ];
4497   var cats = {};
4498   for (var i = 0; i < valid_cats.length; i++) cats[valid_cats[i]] = true;
4499 
4500   for (var k in Dygraph.OPTIONS_REFERENCE) {
4501     if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(k)) continue;
4502     var op = Dygraph.OPTIONS_REFERENCE[k];
4503     for (var i = 0; i < flds.length; i++) {
4504       if (!op.hasOwnProperty(flds[i])) {
4505         warn('Option ' + k + ' missing "' + flds[i] + '" property');
4506       } else if (typeof(op[flds[i]]) != 'string') {
4507         warn(k + '.' + flds[i] + ' must be of type string');
4508       }
4509     }
4510     var labels = op['labels'];
4511     if (typeof(labels) !== 'object') {
4512       warn('Option "' + k + '" is missing a "labels": [...] option');
4513       for (var i = 0; i < labels.length; i++) {
4514         if (!cats.hasOwnProperty(labels[i])) {
4515           warn('Option "' + k + '" has label "' + labels[i] +
4516                '", which is invalid.');
4517         }
4518       }
4519     }
4520   }
4521 })();
4522 // </REMOVE_FOR_COMBINED>
4523