1 /**
  2  * @license
  3  * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
  4  * MIT-licenced: https://opensource.org/licenses/MIT
  5  */
  6 
  7 /**
  8  * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
  9  * string. Dygraph can handle multiple series with or without error bars. The
 10  * date/value ranges will be automatically set. Dygraph uses the
 11  * <canvas> tag, so it only works in FF1.5+.
 12  * See the source or https://dygraphs.com/ for more information.
 13  * @author danvdk@gmail.com (Dan Vanderkam)
 14  */
 15 
 16 /*
 17   Usage:
 18    <div id="graphdiv" style="width:800px; height:500px;"></div>
 19    <script type="text/javascript"><!--//--><![CDATA[//><!--
 20      new Dygraph(document.getElementById("graphdiv"),
 21                  "datafile.csv",  // CSV file with headers
 22                  { }); // options
 23    //--><!]]></script>
 24 
 25  The CSV file is of the form
 26 
 27    Date,SeriesA,SeriesB,SeriesC
 28    YYYY-MM-DD,A1,B1,C1
 29    YYYY-MM-DD,A2,B2,C2
 30 
 31  If the 'errorBars' option is set in the constructor, the input should be of
 32  the form
 33    Date,SeriesA,SeriesB,...
 34    YYYY-MM-DD,A1,sigmaA1,B1,sigmaB1,...
 35    YYYY-MM-DD,A2,sigmaA2,B2,sigmaB2,...
 36 
 37  If the 'fractions' option is set, the input should be of the form:
 38 
 39    Date,SeriesA,SeriesB,...
 40    YYYY-MM-DD,A1/B1,A2/B2,...
 41    YYYY-MM-DD,A1/B1,A2/B2,...
 42 
 43  And error bars will be calculated automatically using a binomial distribution.
 44 
 45  For further documentation and examples, see http://dygraphs.com/
 46  */
 47 
 48 import DygraphLayout from './dygraph-layout';
 49 import DygraphCanvasRenderer from './dygraph-canvas';
 50 import DygraphOptions from './dygraph-options';
 51 import DygraphInteraction from './dygraph-interaction-model';
 52 import * as DygraphTickers from './dygraph-tickers';
 53 import * as utils from './dygraph-utils';
 54 import DEFAULT_ATTRS from './dygraph-default-attrs';
 55 import OPTIONS_REFERENCE from './dygraph-options-reference';
 56 import IFrameTarp from './iframe-tarp';
 57 
 58 import DefaultHandler from './datahandler/default';
 59 import ErrorBarsHandler from './datahandler/bars-error';
 60 import CustomBarsHandler from './datahandler/bars-custom';
 61 import DefaultFractionHandler from './datahandler/default-fractions';
 62 import FractionsBarsHandler from './datahandler/bars-fractions';
 63 import BarsHandler from './datahandler/bars';
 64 
 65 import AnnotationsPlugin from './plugins/annotations';
 66 import AxesPlugin from './plugins/axes';
 67 import ChartLabelsPlugin from './plugins/chart-labels';
 68 import GridPlugin from './plugins/grid';
 69 import LegendPlugin from './plugins/legend';
 70 import RangeSelectorPlugin from './plugins/range-selector';
 71 
 72 import GVizChart from './dygraph-gviz';
 73 
 74 "use strict";
 75 
 76 /**
 77  * Creates an interactive, zoomable chart.
 78  *
 79  * @constructor
 80  * @param {div | String} div A div or the id of a div into which to construct
 81  * the chart.
 82  * @param {String | Function} file A file containing CSV data or a function
 83  * that returns this data. The most basic expected format for each line is
 84  * "YYYY/MM/DD,val1,val2,...". For more information, see
 85  * http://dygraphs.com/data.html.
 86  * @param {Object} attrs Various other attributes, e.g. errorBars determines
 87  * whether the input data contains error ranges. For a complete list of
 88  * options, see http://dygraphs.com/options.html.
 89  */
 90 var Dygraph = function(div, data, opts) {
 91   this.__init__(div, data, opts);
 92 };
 93 
 94 Dygraph.NAME = "Dygraph";
 95 Dygraph.VERSION = "2.1.2";
 96 
 97 // Various default values
 98 Dygraph.DEFAULT_ROLL_PERIOD = 1;
 99 Dygraph.DEFAULT_WIDTH = 480;
100 Dygraph.DEFAULT_HEIGHT = 320;
101 
102 // For max 60 Hz. animation:
103 Dygraph.ANIMATION_STEPS = 12;
104 Dygraph.ANIMATION_DURATION = 200;
105 
106 /**
107  * Standard plotters. These may be used by clients.
108  * Available plotters are:
109  * - Dygraph.Plotters.linePlotter: draws central lines (most common)
110  * - Dygraph.Plotters.errorPlotter: draws error bars
111  * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
112  *
113  * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
114  * This causes all the lines to be drawn over all the fills/error bars.
115  */
116 Dygraph.Plotters = DygraphCanvasRenderer._Plotters;
117 
118 // Used for initializing annotation CSS rules only once.
119 Dygraph.addedAnnotationCSS = false;
120 
121 /**
122  * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
123  * and context <canvas> inside of it. See the constructor for details.
124  * on the parameters.
125  * @param {Element} div the Element to render the graph into.
126  * @param {string | Function} file Source data
127  * @param {Object} attrs Miscellaneous other options
128  * @private
129  */
130 Dygraph.prototype.__init__ = function(div, file, attrs) {
131   this.is_initial_draw_ = true;
132   this.readyFns_ = [];
133 
134   // Support two-argument constructor
135   if (attrs === null || attrs === undefined) { attrs = {}; }
136 
137   attrs = Dygraph.copyUserAttrs_(attrs);
138 
139   if (typeof(div) == 'string') {
140     div = document.getElementById(div);
141   }
142 
143   if (!div) {
144     throw new Error('Constructing dygraph with a non-existent div!');
145   }
146 
147   // Copy the important bits into the object
148   // TODO(danvk): most of these should just stay in the attrs_ dictionary.
149   this.maindiv_ = div;
150   this.file_ = file;
151   this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
152   this.previousVerticalX_ = -1;
153   this.fractions_ = attrs.fractions || false;
154   this.dateWindow_ = attrs.dateWindow || null;
155 
156   this.annotations_ = [];
157 
158   // Clear the div. This ensure that, if multiple dygraphs are passed the same
159   // div, then only one will be drawn.
160   div.innerHTML = "";
161 
162   // For historical reasons, the 'width' and 'height' options trump all CSS
163   // rules _except_ for an explicit 'width' or 'height' on the div.
164   // As an added convenience, if the div has zero height (like <div></div> does
165   // without any styles), then we use a default height/width.
166   if (div.style.width === '' && attrs.width) {
167     div.style.width = attrs.width + "px";
168   }
169   if (div.style.height === '' && attrs.height) {
170     div.style.height = attrs.height + "px";
171   }
172   if (div.style.height === '' && div.clientHeight === 0) {
173     div.style.height = Dygraph.DEFAULT_HEIGHT + "px";
174     if (div.style.width === '') {
175       div.style.width = Dygraph.DEFAULT_WIDTH + "px";
176     }
177   }
178   // These will be zero if the dygraph's div is hidden. In that case,
179   // use the user-specified attributes if present. If not, use zero
180   // and assume the user will call resize to fix things later.
181   this.width_ = div.clientWidth || attrs.width || 0;
182   this.height_ = div.clientHeight || attrs.height || 0;
183 
184   // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
185   if (attrs.stackedGraph) {
186     attrs.fillGraph = true;
187     // TODO(nikhilk): Add any other stackedGraph checks here.
188   }
189 
190   // DEPRECATION WARNING: All option processing should be moved from
191   // attrs_ and user_attrs_ to options_, which holds all this information.
192   //
193   // Dygraphs has many options, some of which interact with one another.
194   // To keep track of everything, we maintain two sets of options:
195   //
196   //  this.user_attrs_   only options explicitly set by the user.
197   //  this.attrs_        defaults, options derived from user_attrs_, data.
198   //
199   // Options are then accessed this.attr_('attr'), which first looks at
200   // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
201   // defaults without overriding behavior that the user specifically asks for.
202   this.user_attrs_ = {};
203   utils.update(this.user_attrs_, attrs);
204 
205   // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
206   this.attrs_ = {};
207   utils.updateDeep(this.attrs_, DEFAULT_ATTRS);
208 
209   this.boundaryIds_ = [];
210   this.setIndexByName_ = {};
211   this.datasetIndex_ = [];
212 
213   this.registeredEvents_ = [];
214   this.eventListeners_ = {};
215 
216   this.attributes_ = new DygraphOptions(this);
217 
218   // Create the containing DIV and other interactive elements
219   this.createInterface_();
220 
221   // Activate plugins.
222   this.plugins_ = [];
223   var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins'));
224   for (var i = 0; i < plugins.length; i++) {
225     // the plugins option may contain either plugin classes or instances.
226     // Plugin instances contain an activate method.
227     var Plugin = plugins[i];  // either a constructor or an instance.
228     var pluginInstance;
229     if (typeof(Plugin.activate) !== 'undefined') {
230       pluginInstance = Plugin;
231     } else {
232       pluginInstance = new Plugin();
233     }
234 
235     var pluginDict = {
236       plugin: pluginInstance,
237       events: {},
238       options: {},
239       pluginOptions: {}
240     };
241 
242     var handlers = pluginInstance.activate(this);
243     for (var eventName in handlers) {
244       if (!handlers.hasOwnProperty(eventName)) continue;
245       // TODO(danvk): validate eventName.
246       pluginDict.events[eventName] = handlers[eventName];
247     }
248 
249     this.plugins_.push(pluginDict);
250   }
251 
252   // At this point, plugins can no longer register event handlers.
253   // Construct a map from event -> ordered list of [callback, plugin].
254   for (var i = 0; i < this.plugins_.length; i++) {
255     var plugin_dict = this.plugins_[i];
256     for (var eventName in plugin_dict.events) {
257       if (!plugin_dict.events.hasOwnProperty(eventName)) continue;
258       var callback = plugin_dict.events[eventName];
259 
260       var pair = [plugin_dict.plugin, callback];
261       if (!(eventName in this.eventListeners_)) {
262         this.eventListeners_[eventName] = [pair];
263       } else {
264         this.eventListeners_[eventName].push(pair);
265       }
266     }
267   }
268 
269   this.createDragInterface_();
270 
271   this.start_();
272 };
273 
274 /**
275  * Triggers a cascade of events to the various plugins which are interested in them.
276  * Returns true if the "default behavior" should be prevented, i.e. if one
277  * of the event listeners called event.preventDefault().
278  * @private
279  */
280 Dygraph.prototype.cascadeEvents_ = function(name, extra_props) {
281   if (!(name in this.eventListeners_)) return false;
282 
283   // QUESTION: can we use objects & prototypes to speed this up?
284   var e = {
285     dygraph: this,
286     cancelable: false,
287     defaultPrevented: false,
288     preventDefault: function() {
289       if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event.";
290       e.defaultPrevented = true;
291     },
292     propagationStopped: false,
293     stopPropagation: function() {
294       e.propagationStopped = true;
295     }
296   };
297   utils.update(e, extra_props);
298 
299   var callback_plugin_pairs = this.eventListeners_[name];
300   if (callback_plugin_pairs) {
301     for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) {
302       var plugin = callback_plugin_pairs[i][0];
303       var callback = callback_plugin_pairs[i][1];
304       callback.call(plugin, e);
305       if (e.propagationStopped) break;
306     }
307   }
308   return e.defaultPrevented;
309 };
310 
311 /**
312  * Fetch a plugin instance of a particular class. Only for testing.
313  * @private
314  * @param {!Class} type The type of the plugin.
315  * @return {Object} Instance of the plugin, or null if there is none.
316  */
317 Dygraph.prototype.getPluginInstance_ = function(type) {
318   for (var i = 0; i < this.plugins_.length; i++) {
319     var p = this.plugins_[i];
320     if (p.plugin instanceof type) {
321       return p.plugin;
322     }
323   }
324   return null;
325 };
326 
327 /**
328  * Returns the zoomed status of the chart for one or both axes.
329  *
330  * Axis is an optional parameter. Can be set to 'x' or 'y'.
331  *
332  * The zoomed status for an axis is set whenever a user zooms using the mouse
333  * or when the dateWindow or valueRange are updated. Double-clicking or calling
334  * resetZoom() resets the zoom status for the chart.
335  */
336 Dygraph.prototype.isZoomed = function(axis) {
337   const isZoomedX = !!this.dateWindow_;
338   if (axis === 'x') return isZoomedX;
339 
340   const isZoomedY = this.axes_.map(axis => !!axis.valueRange).indexOf(true) >= 0;
341   if (axis === null || axis === undefined) {
342     return isZoomedX || isZoomedY;
343   }
344   if (axis === 'y') return isZoomedY;
345 
346   throw new Error(`axis parameter is [${axis}] must be null, 'x' or 'y'.`);
347 };
348 
349 /**
350  * Returns information about the Dygraph object, including its containing ID.
351  */
352 Dygraph.prototype.toString = function() {
353   var maindiv = this.maindiv_;
354   var id = (maindiv && maindiv.id) ? maindiv.id : maindiv;
355   return "[Dygraph " + id + "]";
356 };
357 
358 /**
359  * @private
360  * Returns the value of an option. This may be set by the user (either in the
361  * constructor or by calling updateOptions) or by dygraphs, and may be set to a
362  * per-series value.
363  * @param {string} name The name of the option, e.g. 'rollPeriod'.
364  * @param {string} [seriesName] The name of the series to which the option
365  * will be applied. If no per-series value of this option is available, then
366  * the global value is returned. This is optional.
367  * @return { ... } The value of the option.
368  */
369 Dygraph.prototype.attr_ = function(name, seriesName) {
370   if (typeof process !== 'undefined' && process.env.NODE_ENV != 'production') {
371     // For "production" code, this gets removed by uglifyjs.
372     if (typeof(OPTIONS_REFERENCE) === 'undefined') {
373       console.error('Must include options reference JS for testing');
374     } else if (!OPTIONS_REFERENCE.hasOwnProperty(name)) {
375       console.error('Dygraphs is using property ' + name + ', which has no ' +
376                     'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
377       // Only log this error once.
378       OPTIONS_REFERENCE[name] = true;
379     }
380   }
381   return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name);
382 };
383 
384 /**
385  * Returns the current value for an option, as set in the constructor or via
386  * updateOptions. You may pass in an (optional) series name to get per-series
387  * values for the option.
388  *
389  * All values returned by this method should be considered immutable. If you
390  * modify them, there is no guarantee that the changes will be honored or that
391  * dygraphs will remain in a consistent state. If you want to modify an option,
392  * use updateOptions() instead.
393  *
394  * @param {string} name The name of the option (e.g. 'strokeWidth')
395  * @param {string=} opt_seriesName Series name to get per-series values.
396  * @return {*} The value of the option.
397  */
398 Dygraph.prototype.getOption = function(name, opt_seriesName) {
399   return this.attr_(name, opt_seriesName);
400 };
401 
402 /**
403  * Like getOption(), but specifically returns a number.
404  * This is a convenience function for working with the Closure Compiler.
405  * @param {string} name The name of the option (e.g. 'strokeWidth')
406  * @param {string=} opt_seriesName Series name to get per-series values.
407  * @return {number} The value of the option.
408  * @private
409  */
410 Dygraph.prototype.getNumericOption = function(name, opt_seriesName) {
411   return /** @type{number} */(this.getOption(name, opt_seriesName));
412 };
413 
414 /**
415  * Like getOption(), but specifically returns a string.
416  * This is a convenience function for working with the Closure Compiler.
417  * @param {string} name The name of the option (e.g. 'strokeWidth')
418  * @param {string=} opt_seriesName Series name to get per-series values.
419  * @return {string} The value of the option.
420  * @private
421  */
422 Dygraph.prototype.getStringOption = function(name, opt_seriesName) {
423   return /** @type{string} */(this.getOption(name, opt_seriesName));
424 };
425 
426 /**
427  * Like getOption(), but specifically returns a boolean.
428  * This is a convenience function for working with the Closure Compiler.
429  * @param {string} name The name of the option (e.g. 'strokeWidth')
430  * @param {string=} opt_seriesName Series name to get per-series values.
431  * @return {boolean} The value of the option.
432  * @private
433  */
434 Dygraph.prototype.getBooleanOption = function(name, opt_seriesName) {
435   return /** @type{boolean} */(this.getOption(name, opt_seriesName));
436 };
437 
438 /**
439  * Like getOption(), but specifically returns a function.
440  * This is a convenience function for working with the Closure Compiler.
441  * @param {string} name The name of the option (e.g. 'strokeWidth')
442  * @param {string=} opt_seriesName Series name to get per-series values.
443  * @return {function(...)} The value of the option.
444  * @private
445  */
446 Dygraph.prototype.getFunctionOption = function(name, opt_seriesName) {
447   return /** @type{function(...)} */(this.getOption(name, opt_seriesName));
448 };
449 
450 Dygraph.prototype.getOptionForAxis = function(name, axis) {
451   return this.attributes_.getForAxis(name, axis);
452 };
453 
454 /**
455  * @private
456  * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2')
457  * @return { ... } A function mapping string -> option value
458  */
459 Dygraph.prototype.optionsViewForAxis_ = function(axis) {
460   var self = this;
461   return function(opt) {
462     var axis_opts = self.user_attrs_.axes;
463     if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
464       return axis_opts[axis][opt];
465     }
466 
467     // I don't like that this is in a second spot.
468     if (axis === 'x' && opt === 'logscale') {
469       // return the default value.
470       // TODO(konigsberg): pull the default from a global default.
471       return false;
472     }
473 
474     // user-specified attributes always trump defaults, even if they're less
475     // specific.
476     if (typeof(self.user_attrs_[opt]) != 'undefined') {
477       return self.user_attrs_[opt];
478     }
479 
480     axis_opts = self.attrs_.axes;
481     if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) {
482       return axis_opts[axis][opt];
483     }
484     // check old-style axis options
485     // TODO(danvk): add a deprecation warning if either of these match.
486     if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) {
487       return self.axes_[0][opt];
488     } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) {
489       return self.axes_[1][opt];
490     }
491     return self.attr_(opt);
492   };
493 };
494 
495 /**
496  * Returns the current rolling period, as set by the user or an option.
497  * @return {number} The number of points in the rolling window
498  */
499 Dygraph.prototype.rollPeriod = function() {
500   return this.rollPeriod_;
501 };
502 
503 /**
504  * Returns the currently-visible x-range. This can be affected by zooming,
505  * panning or a call to updateOptions.
506  * Returns a two-element array: [left, right].
507  * If the Dygraph has dates on the x-axis, these will be millis since epoch.
508  */
509 Dygraph.prototype.xAxisRange = function() {
510   return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes();
511 };
512 
513 /**
514  * Returns the lower- and upper-bound x-axis values of the data set.
515  */
516 Dygraph.prototype.xAxisExtremes = function() {
517   var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w;
518   if (this.numRows() === 0) {
519     return [0 - pad, 1 + pad];
520   }
521   var left = this.rawData_[0][0];
522   var right = this.rawData_[this.rawData_.length - 1][0];
523   if (pad) {
524     // Must keep this in sync with dygraph-layout _evaluateLimits()
525     var range = right - left;
526     left -= range * pad;
527     right += range * pad;
528   }
529   return [left, right];
530 };
531 
532 /**
533  * Returns the lower- and upper-bound y-axis values for each axis. These are
534  * the ranges you'll get if you double-click to zoom out or call resetZoom().
535  * The return value is an array of [low, high] tuples, one for each y-axis.
536  */
537 Dygraph.prototype.yAxisExtremes = function() {
538   // TODO(danvk): this is pretty inefficient
539   const packed = this.gatherDatasets_(this.rolledSeries_, null);
540   const { extremes } = packed;
541   const saveAxes = this.axes_;
542   this.computeYAxisRanges_(extremes);
543   const newAxes = this.axes_;
544   this.axes_ = saveAxes;
545   return newAxes.map(axis => axis.extremeRange);
546 }
547 
548 /**
549  * Returns the currently-visible y-range for an axis. This can be affected by
550  * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
551  * called with no arguments, returns the range of the first axis.
552  * Returns a two-element array: [bottom, top].
553  */
554 Dygraph.prototype.yAxisRange = function(idx) {
555   if (typeof(idx) == "undefined") idx = 0;
556   if (idx < 0 || idx >= this.axes_.length) {
557     return null;
558   }
559   var axis = this.axes_[idx];
560   return [ axis.computedValueRange[0], axis.computedValueRange[1] ];
561 };
562 
563 /**
564  * Returns the currently-visible y-ranges for each axis. This can be affected by
565  * zooming, panning, calls to updateOptions, etc.
566  * Returns an array of [bottom, top] pairs, one for each y-axis.
567  */
568 Dygraph.prototype.yAxisRanges = function() {
569   var ret = [];
570   for (var i = 0; i < this.axes_.length; i++) {
571     ret.push(this.yAxisRange(i));
572   }
573   return ret;
574 };
575 
576 // TODO(danvk): use these functions throughout dygraphs.
577 /**
578  * Convert from data coordinates to canvas/div X/Y coordinates.
579  * If specified, do this conversion for the coordinate system of a particular
580  * axis. Uses the first axis by default.
581  * Returns a two-element array: [X, Y]
582  *
583  * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
584  * instead of toDomCoords(null, y, axis).
585  */
586 Dygraph.prototype.toDomCoords = function(x, y, axis) {
587   return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ];
588 };
589 
590 /**
591  * Convert from data x coordinates to canvas/div X coordinate.
592  * If specified, do this conversion for the coordinate system of a particular
593  * axis.
594  * Returns a single value or null if x is null.
595  */
596 Dygraph.prototype.toDomXCoord = function(x) {
597   if (x === null) {
598     return null;
599   }
600 
601   var area = this.plotter_.area;
602   var xRange = this.xAxisRange();
603   return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w;
604 };
605 
606 /**
607  * Convert from data x coordinates to canvas/div Y coordinate and optional
608  * axis. Uses the first axis by default.
609  *
610  * returns a single value or null if y is null.
611  */
612 Dygraph.prototype.toDomYCoord = function(y, axis) {
613   var pct = this.toPercentYCoord(y, axis);
614 
615   if (pct === null) {
616     return null;
617   }
618   var area = this.plotter_.area;
619   return area.y + pct * area.h;
620 };
621 
622 /**
623  * Convert from canvas/div coords to data coordinates.
624  * If specified, do this conversion for the coordinate system of a particular
625  * axis. Uses the first axis by default.
626  * Returns a two-element array: [X, Y].
627  *
628  * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
629  * instead of toDataCoords(null, y, axis).
630  */
631 Dygraph.prototype.toDataCoords = function(x, y, axis) {
632   return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ];
633 };
634 
635 /**
636  * Convert from canvas/div x coordinate to data coordinate.
637  *
638  * If x is null, this returns null.
639  */
640 Dygraph.prototype.toDataXCoord = function(x) {
641   if (x === null) {
642     return null;
643   }
644 
645   var area = this.plotter_.area;
646   var xRange = this.xAxisRange();
647 
648   if (!this.attributes_.getForAxis("logscale", 'x')) {
649     return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]);
650   } else {
651     var pct = (x - area.x) / area.w;
652     return utils.logRangeFraction(xRange[0], xRange[1], pct);
653   }
654 };
655 
656 /**
657  * Convert from canvas/div y coord to value.
658  *
659  * If y is null, this returns null.
660  * if axis is null, this uses the first axis.
661  */
662 Dygraph.prototype.toDataYCoord = function(y, axis) {
663   if (y === null) {
664     return null;
665   }
666 
667   var area = this.plotter_.area;
668   var yRange = this.yAxisRange(axis);
669 
670   if (typeof(axis) == "undefined") axis = 0;
671   if (!this.attributes_.getForAxis("logscale", axis)) {
672     return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]);
673   } else {
674     // Computing the inverse of toDomCoord.
675     var pct = (y - area.y) / area.h;
676     // Note reversed yRange, y1 is on top with pct==0.
677     return utils.logRangeFraction(yRange[1], yRange[0], pct);
678   }
679 };
680 
681 /**
682  * Converts a y for an axis to a percentage from the top to the
683  * bottom of the drawing area.
684  *
685  * If the coordinate represents a value visible on the canvas, then
686  * the value will be between 0 and 1, where 0 is the top of the canvas.
687  * However, this method will return values outside the range, as
688  * values can fall outside the canvas.
689  *
690  * If y is null, this returns null.
691  * if axis is null, this uses the first axis.
692  *
693  * @param {number} y The data y-coordinate.
694  * @param {number} [axis] The axis number on which the data coordinate lives.
695  * @return {number} A fraction in [0, 1] where 0 = the top edge.
696  */
697 Dygraph.prototype.toPercentYCoord = function(y, axis) {
698   if (y === null) {
699     return null;
700   }
701   if (typeof(axis) == "undefined") axis = 0;
702 
703   var yRange = this.yAxisRange(axis);
704 
705   var pct;
706   var logscale = this.attributes_.getForAxis("logscale", axis);
707   if (logscale) {
708     var logr0 = utils.log10(yRange[0]);
709     var logr1 = utils.log10(yRange[1]);
710     pct = (logr1 - utils.log10(y)) / (logr1 - logr0);
711   } else {
712     // yRange[1] - y is unit distance from the bottom.
713     // yRange[1] - yRange[0] is the scale of the range.
714     // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom.
715     pct = (yRange[1] - y) / (yRange[1] - yRange[0]);
716   }
717   return pct;
718 };
719 
720 /**
721  * Converts an x value to a percentage from the left to the right of
722  * the drawing area.
723  *
724  * If the coordinate represents a value visible on the canvas, then
725  * the value will be between 0 and 1, where 0 is the left of the canvas.
726  * However, this method will return values outside the range, as
727  * values can fall outside the canvas.
728  *
729  * If x is null, this returns null.
730  * @param {number} x The data x-coordinate.
731  * @return {number} A fraction in [0, 1] where 0 = the left edge.
732  */
733 Dygraph.prototype.toPercentXCoord = function(x) {
734   if (x === null) {
735     return null;
736   }
737 
738   var xRange = this.xAxisRange();
739   var pct;
740   var logscale = this.attributes_.getForAxis("logscale", 'x') ;
741   if (logscale === true) {  // logscale can be null so we test for true explicitly.
742     var logr0 = utils.log10(xRange[0]);
743     var logr1 = utils.log10(xRange[1]);
744     pct = (utils.log10(x) - logr0) / (logr1 - logr0);
745   } else {
746     // x - xRange[0] is unit distance from the left.
747     // xRange[1] - xRange[0] is the scale of the range.
748     // The full expression below is the % from the left.
749     pct = (x - xRange[0]) / (xRange[1] - xRange[0]);
750   }
751   return pct;
752 };
753 
754 /**
755  * Returns the number of columns (including the independent variable).
756  * @return {number} The number of columns.
757  */
758 Dygraph.prototype.numColumns = function() {
759   if (!this.rawData_) return 0;
760   return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length;
761 };
762 
763 /**
764  * Returns the number of rows (excluding any header/label row).
765  * @return {number} The number of rows, less any header.
766  */
767 Dygraph.prototype.numRows = function() {
768   if (!this.rawData_) return 0;
769   return this.rawData_.length;
770 };
771 
772 /**
773  * Returns the value in the given row and column. If the row and column exceed
774  * the bounds on the data, returns null. Also returns null if the value is
775  * missing.
776  * @param {number} row The row number of the data (0-based). Row 0 is the
777  *     first row of data, not a header row.
778  * @param {number} col The column number of the data (0-based)
779  * @return {number} The value in the specified cell or null if the row/col
780  *     were out of range.
781  */
782 Dygraph.prototype.getValue = function(row, col) {
783   if (row < 0 || row >= this.rawData_.length) return null;
784   if (col < 0 || col >= this.rawData_[row].length) return null;
785 
786   return this.rawData_[row][col];
787 };
788 
789 /**
790  * Generates interface elements for the Dygraph: a containing div, a div to
791  * display the current point, and a textbox to adjust the rolling average
792  * period. Also creates the Renderer/Layout elements.
793  * @private
794  */
795 Dygraph.prototype.createInterface_ = function() {
796   // Create the all-enclosing graph div
797   var enclosing = this.maindiv_;
798 
799   this.graphDiv = document.createElement("div");
800 
801   // TODO(danvk): any other styles that are useful to set here?
802   this.graphDiv.style.textAlign = 'left';  // This is a CSS "reset"
803   this.graphDiv.style.position = 'relative';
804   enclosing.appendChild(this.graphDiv);
805 
806   // Create the canvas for interactive parts of the chart.
807   this.canvas_ = utils.createCanvas();
808   this.canvas_.style.position = "absolute";
809   this.canvas_.style.top = 0;
810   this.canvas_.style.left = 0;
811 
812   // ... and for static parts of the chart.
813   this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
814 
815   this.canvas_ctx_ = utils.getContext(this.canvas_);
816   this.hidden_ctx_ = utils.getContext(this.hidden_);
817 
818   this.resizeElements_();
819 
820   // The interactive parts of the graph are drawn on top of the chart.
821   this.graphDiv.appendChild(this.hidden_);
822   this.graphDiv.appendChild(this.canvas_);
823   this.mouseEventElement_ = this.createMouseEventElement_();
824 
825   // Create the grapher
826   this.layout_ = new DygraphLayout(this);
827 
828   var dygraph = this;
829 
830   this.mouseMoveHandler_ = function(e) {
831     dygraph.mouseMove_(e);
832   };
833 
834   this.mouseOutHandler_ = function(e) {
835     // The mouse has left the chart if:
836     // 1. e.target is inside the chart
837     // 2. e.relatedTarget is outside the chart
838     var target = e.target || e.fromElement;
839     var relatedTarget = e.relatedTarget || e.toElement;
840     if (utils.isNodeContainedBy(target, dygraph.graphDiv) &&
841         !utils.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) {
842       dygraph.mouseOut_(e);
843     }
844   };
845 
846   this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_);
847   this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
848 
849   // Don't recreate and register the resize handler on subsequent calls.
850   // This happens when the graph is resized.
851   if (!this.resizeHandler_) {
852     this.resizeHandler_ = function(e) {
853       dygraph.resize();
854     };
855 
856     // Update when the window is resized.
857     // TODO(danvk): drop frames depending on complexity of the chart.
858     this.addAndTrackEvent(window, 'resize', this.resizeHandler_);
859   }
860 };
861 
862 Dygraph.prototype.resizeElements_ = function() {
863   this.graphDiv.style.width = this.width_ + "px";
864   this.graphDiv.style.height = this.height_ + "px";
865 
866   var pixelRatioOption = this.getNumericOption('pixelRatio')
867 
868   var canvasScale = pixelRatioOption || utils.getContextPixelRatio(this.canvas_ctx_);
869   this.canvas_.width = this.width_ * canvasScale;
870   this.canvas_.height = this.height_ * canvasScale;
871   this.canvas_.style.width = this.width_ + "px";    // for IE
872   this.canvas_.style.height = this.height_ + "px";  // for IE
873   if (canvasScale !== 1) {
874     this.canvas_ctx_.scale(canvasScale, canvasScale);
875   }
876 
877   var hiddenScale = pixelRatioOption || utils.getContextPixelRatio(this.hidden_ctx_);
878   this.hidden_.width = this.width_ * hiddenScale;
879   this.hidden_.height = this.height_ * hiddenScale;
880   this.hidden_.style.width = this.width_ + "px";    // for IE
881   this.hidden_.style.height = this.height_ + "px";  // for IE
882   if (hiddenScale !== 1) {
883     this.hidden_ctx_.scale(hiddenScale, hiddenScale);
884   }
885 };
886 
887 /**
888  * Detach DOM elements in the dygraph and null out all data references.
889  * Calling this when you're done with a dygraph can dramatically reduce memory
890  * usage. See, e.g., the tests/perf.html example.
891  */
892 Dygraph.prototype.destroy = function() {
893   this.canvas_ctx_.restore();
894   this.hidden_ctx_.restore();
895 
896   // Destroy any plugins, in the reverse order that they were registered.
897   for (var i = this.plugins_.length - 1; i >= 0; i--) {
898     var p = this.plugins_.pop();
899     if (p.plugin.destroy) p.plugin.destroy();
900   }
901 
902   var removeRecursive = function(node) {
903     while (node.hasChildNodes()) {
904       removeRecursive(node.firstChild);
905       node.removeChild(node.firstChild);
906     }
907   };
908 
909   this.removeTrackedEvents_();
910 
911   // remove mouse event handlers (This may not be necessary anymore)
912   utils.removeEvent(window, 'mouseout', this.mouseOutHandler_);
913   utils.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_);
914 
915   // remove window handlers
916   utils.removeEvent(window,'resize', this.resizeHandler_);
917   this.resizeHandler_ = null;
918 
919   removeRecursive(this.maindiv_);
920 
921   var nullOut = function(obj) {
922     for (var n in obj) {
923       if (typeof(obj[n]) === 'object') {
924         obj[n] = null;
925       }
926     }
927   };
928   // These may not all be necessary, but it can't hurt...
929   nullOut(this.layout_);
930   nullOut(this.plotter_);
931   nullOut(this);
932 };
933 
934 /**
935  * Creates the canvas on which the chart will be drawn. Only the Renderer ever
936  * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
937  * or the zoom rectangles) is done on this.canvas_.
938  * @param {Object} canvas The Dygraph canvas over which to overlay the plot
939  * @return {Object} The newly-created canvas
940  * @private
941  */
942 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
943   var h = utils.createCanvas();
944   h.style.position = "absolute";
945   // TODO(danvk): h should be offset from canvas. canvas needs to include
946   // some extra area to make it easier to zoom in on the far left and far
947   // right. h needs to be precisely the plot area, so that clipping occurs.
948   h.style.top = canvas.style.top;
949   h.style.left = canvas.style.left;
950   h.width = this.width_;
951   h.height = this.height_;
952   h.style.width = this.width_ + "px";    // for IE
953   h.style.height = this.height_ + "px";  // for IE
954   return h;
955 };
956 
957 /**
958  * Creates an overlay element used to handle mouse events.
959  * @return {Object} The mouse event element.
960  * @private
961  */
962 Dygraph.prototype.createMouseEventElement_ = function() {
963   return this.canvas_;
964 };
965 
966 /**
967  * Generate a set of distinct colors for the data series. This is done with a
968  * color wheel. Saturation/Value are customizable, and the hue is
969  * equally-spaced around the color wheel. If a custom set of colors is
970  * specified, that is used instead.
971  * @private
972  */
973 Dygraph.prototype.setColors_ = function() {
974   var labels = this.getLabels();
975   var num = labels.length - 1;
976   this.colors_ = [];
977   this.colorsMap_ = {};
978 
979   // These are used for when no custom colors are specified.
980   var sat = this.getNumericOption('colorSaturation') || 1.0;
981   var val = this.getNumericOption('colorValue') || 0.5;
982   var half = Math.ceil(num / 2);
983 
984   var colors = this.getOption('colors');
985   var visibility = this.visibility();
986   for (var i = 0; i < num; i++) {
987     if (!visibility[i]) {
988       continue;
989     }
990     var label = labels[i + 1];
991     var colorStr = this.attributes_.getForSeries('color', label);
992     if (!colorStr) {
993       if (colors) {
994         colorStr = colors[i % colors.length];
995       } else {
996         // alternate colors for high contrast.
997         var idx = i % 2 ? (half + (i + 1)/ 2) : Math.ceil((i + 1) / 2);
998         var hue = (1.0 * idx / (1 + num));
999         colorStr = utils.hsvToRGB(hue, sat, val);
1000       }
1001     }
1002     this.colors_.push(colorStr);
1003     this.colorsMap_[label] = colorStr;
1004   }
1005 };
1006 
1007 /**
1008  * Return the list of colors. This is either the list of colors passed in the
1009  * attributes or the autogenerated list of rgb(r,g,b) strings.
1010  * This does not return colors for invisible series.
1011  * @return {Array.<string>} The list of colors.
1012  */
1013 Dygraph.prototype.getColors = function() {
1014   return this.colors_;
1015 };
1016 
1017 /**
1018  * Returns a few attributes of a series, i.e. its color, its visibility, which
1019  * axis it's assigned to, and its column in the original data.
1020  * Returns null if the series does not exist.
1021  * Otherwise, returns an object with column, visibility, color and axis properties.
1022  * The "axis" property will be set to 1 for y1 and 2 for y2.
1023  * The "column" property can be fed back into getValue(row, column) to get
1024  * values for this series.
1025  */
1026 Dygraph.prototype.getPropertiesForSeries = function(series_name) {
1027   var idx = -1;
1028   var labels = this.getLabels();
1029   for (var i = 1; i < labels.length; i++) {
1030     if (labels[i] == series_name) {
1031       idx = i;
1032       break;
1033     }
1034   }
1035   if (idx == -1) return null;
1036 
1037   return {
1038     name: series_name,
1039     column: idx,
1040     visible: this.visibility()[idx - 1],
1041     color: this.colorsMap_[series_name],
1042     axis: 1 + this.attributes_.axisForSeries(series_name)
1043   };
1044 };
1045 
1046 /**
1047  * Create the text box to adjust the averaging period
1048  * @private
1049  */
1050 Dygraph.prototype.createRollInterface_ = function() {
1051   // Create a roller if one doesn't exist already.
1052   var roller = this.roller_;
1053   if (!roller) {
1054     this.roller_ = roller = document.createElement("input");
1055     roller.type = "text";
1056     roller.style.display = "none";
1057     roller.className = 'dygraph-roller';
1058     this.graphDiv.appendChild(roller);
1059   }
1060 
1061   var display = this.getBooleanOption('showRoller') ? 'block' : 'none';
1062 
1063   var area = this.getArea();
1064   var textAttr = {
1065                    "top": (area.y + area.h - 25) + "px",
1066                    "left": (area.x + 1) + "px",
1067                    "display": display
1068                  };
1069   roller.size = "2";
1070   roller.value = this.rollPeriod_;
1071   utils.update(roller.style, textAttr);
1072 
1073   roller.onchange = () => this.adjustRoll(roller.value);
1074 };
1075 
1076 /**
1077  * Set up all the mouse handlers needed to capture dragging behavior for zoom
1078  * events.
1079  * @private
1080  */
1081 Dygraph.prototype.createDragInterface_ = function() {
1082   var context = {
1083     // Tracks whether the mouse is down right now
1084     isZooming: false,
1085     isPanning: false,  // is this drag part of a pan?
1086     is2DPan: false,    // if so, is that pan 1- or 2-dimensional?
1087     dragStartX: null, // pixel coordinates
1088     dragStartY: null, // pixel coordinates
1089     dragEndX: null, // pixel coordinates
1090     dragEndY: null, // pixel coordinates
1091     dragDirection: null,
1092     prevEndX: null, // pixel coordinates
1093     prevEndY: null, // pixel coordinates
1094     prevDragDirection: null,
1095     cancelNextDblclick: false,  // see comment in dygraph-interaction-model.js
1096 
1097     // The value on the left side of the graph when a pan operation starts.
1098     initialLeftmostDate: null,
1099 
1100     // The number of units each pixel spans. (This won't be valid for log
1101     // scales)
1102     xUnitsPerPixel: null,
1103 
1104     // TODO(danvk): update this comment
1105     // The range in second/value units that the viewport encompasses during a
1106     // panning operation.
1107     dateRange: null,
1108 
1109     // Top-left corner of the canvas, in DOM coords
1110     // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1111     px: 0,
1112     py: 0,
1113 
1114     // Values for use with panEdgeFraction, which limit how far outside the
1115     // graph's data boundaries it can be panned.
1116     boundedDates: null, // [minDate, maxDate]
1117     boundedValues: null, // [[minValue, maxValue] ...]
1118 
1119     // We cover iframes during mouse interactions. See comments in
1120     // dygraph-utils.js for more info on why this is a good idea.
1121     tarp: new IFrameTarp(),
1122 
1123     // contextB is the same thing as this context object but renamed.
1124     initializeMouseDown: function(event, g, contextB) {
1125       // prevents mouse drags from selecting page text.
1126       if (event.preventDefault) {
1127         event.preventDefault();  // Firefox, Chrome, etc.
1128       } else {
1129         event.returnValue = false;  // IE
1130         event.cancelBubble = true;
1131       }
1132 
1133       var canvasPos = utils.findPos(g.canvas_);
1134       contextB.px = canvasPos.x;
1135       contextB.py = canvasPos.y;
1136       contextB.dragStartX = utils.dragGetX_(event, contextB);
1137       contextB.dragStartY = utils.dragGetY_(event, contextB);
1138       contextB.cancelNextDblclick = false;
1139       contextB.tarp.cover();
1140     },
1141     destroy: function() {
1142       var context = this;
1143       if (context.isZooming || context.isPanning) {
1144         context.isZooming = false;
1145         context.dragStartX = null;
1146         context.dragStartY = null;
1147       }
1148 
1149       if (context.isPanning) {
1150         context.isPanning = false;
1151         context.draggingDate = null;
1152         context.dateRange = null;
1153         for (var i = 0; i < self.axes_.length; i++) {
1154           delete self.axes_[i].draggingValue;
1155           delete self.axes_[i].dragValueRange;
1156         }
1157       }
1158 
1159       context.tarp.uncover();
1160     }
1161   };
1162 
1163   var interactionModel = this.getOption("interactionModel");
1164 
1165   // Self is the graph.
1166   var self = this;
1167 
1168   // Function that binds the graph and context to the handler.
1169   var bindHandler = function(handler) {
1170     return function(event) {
1171       handler(event, self, context);
1172     };
1173   };
1174 
1175   for (var eventName in interactionModel) {
1176     if (!interactionModel.hasOwnProperty(eventName)) continue;
1177     this.addAndTrackEvent(this.mouseEventElement_, eventName,
1178         bindHandler(interactionModel[eventName]));
1179   }
1180 
1181   // If the user releases the mouse button during a drag, but not over the
1182   // canvas, then it doesn't count as a zooming action.
1183   if (!interactionModel.willDestroyContextMyself) {
1184     var mouseUpHandler = function(event) {
1185       context.destroy();
1186     };
1187 
1188     this.addAndTrackEvent(document, 'mouseup', mouseUpHandler);
1189   }
1190 };
1191 
1192 /**
1193  * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1194  * up any previous zoom rectangles that were drawn. This could be optimized to
1195  * avoid extra redrawing, but it's tricky to avoid interactions with the status
1196  * dots.
1197  *
1198  * @param {number} direction the direction of the zoom rectangle. Acceptable
1199  *     values are utils.HORIZONTAL and utils.VERTICAL.
1200  * @param {number} startX The X position where the drag started, in canvas
1201  *     coordinates.
1202  * @param {number} endX The current X position of the drag, in canvas coords.
1203  * @param {number} startY The Y position where the drag started, in canvas
1204  *     coordinates.
1205  * @param {number} endY The current Y position of the drag, in canvas coords.
1206  * @param {number} prevDirection the value of direction on the previous call to
1207  *     this function. Used to avoid excess redrawing
1208  * @param {number} prevEndX The value of endX on the previous call to this
1209  *     function. Used to avoid excess redrawing
1210  * @param {number} prevEndY The value of endY on the previous call to this
1211  *     function. Used to avoid excess redrawing
1212  * @private
1213  */
1214 Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY,
1215                                            endY, prevDirection, prevEndX,
1216                                            prevEndY) {
1217   var ctx = this.canvas_ctx_;
1218 
1219   // Clean up from the previous rect if necessary
1220   if (prevDirection == utils.HORIZONTAL) {
1221     ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y,
1222                   Math.abs(startX - prevEndX), this.layout_.getPlotArea().h);
1223   } else if (prevDirection == utils.VERTICAL) {
1224     ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY),
1225                   this.layout_.getPlotArea().w, Math.abs(startY - prevEndY));
1226   }
1227 
1228   // Draw a light-grey rectangle to show the new viewing area
1229   if (direction == utils.HORIZONTAL) {
1230     if (endX && startX) {
1231       ctx.fillStyle = "rgba(128,128,128,0.33)";
1232       ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y,
1233                    Math.abs(endX - startX), this.layout_.getPlotArea().h);
1234     }
1235   } else if (direction == utils.VERTICAL) {
1236     if (endY && startY) {
1237       ctx.fillStyle = "rgba(128,128,128,0.33)";
1238       ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY),
1239                    this.layout_.getPlotArea().w, Math.abs(endY - startY));
1240     }
1241   }
1242 };
1243 
1244 /**
1245  * Clear the zoom rectangle (and perform no zoom).
1246  * @private
1247  */
1248 Dygraph.prototype.clearZoomRect_ = function() {
1249   this.currentZoomRectArgs_ = null;
1250   this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
1251 };
1252 
1253 /**
1254  * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1255  * the canvas. The exact zoom window may be slightly larger if there are no data
1256  * points near lowX or highX. Don't confuse this function with doZoomXDates,
1257  * which accepts dates that match the raw data. This function redraws the graph.
1258  *
1259  * @param {number} lowX The leftmost pixel value that should be visible.
1260  * @param {number} highX The rightmost pixel value that should be visible.
1261  * @private
1262  */
1263 Dygraph.prototype.doZoomX_ = function(lowX, highX) {
1264   this.currentZoomRectArgs_ = null;
1265   // Find the earliest and latest dates contained in this canvasx range.
1266   // Convert the call to date ranges of the raw data.
1267   var minDate = this.toDataXCoord(lowX);
1268   var maxDate = this.toDataXCoord(highX);
1269   this.doZoomXDates_(minDate, maxDate);
1270 };
1271 
1272 /**
1273  * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1274  * method with doZoomX which accepts pixel coordinates. This function redraws
1275  * the graph.
1276  *
1277  * @param {number} minDate The minimum date that should be visible.
1278  * @param {number} maxDate The maximum date that should be visible.
1279  * @private
1280  */
1281 Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) {
1282   // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation
1283   // can produce strange effects. Rather than the x-axis transitioning slowly
1284   // between values, it can jerk around.)
1285   var old_window = this.xAxisRange();
1286   var new_window = [minDate, maxDate];
1287   const zoomCallback = this.getFunctionOption('zoomCallback');
1288   this.doAnimatedZoom(old_window, new_window, null, null, () => {
1289     if (zoomCallback) {
1290       zoomCallback.call(this, minDate, maxDate, this.yAxisRanges());
1291     }
1292   });
1293 };
1294 
1295 /**
1296  * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1297  * the canvas. This function redraws the graph.
1298  *
1299  * @param {number} lowY The topmost pixel value that should be visible.
1300  * @param {number} highY The lowest pixel value that should be visible.
1301  * @private
1302  */
1303 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
1304   this.currentZoomRectArgs_ = null;
1305   // Find the highest and lowest values in pixel range for each axis.
1306   // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1307   // This is because pixels increase as you go down on the screen, whereas data
1308   // coordinates increase as you go up the screen.
1309   var oldValueRanges = this.yAxisRanges();
1310   var newValueRanges = [];
1311   for (var i = 0; i < this.axes_.length; i++) {
1312     var hi = this.toDataYCoord(lowY, i);
1313     var low = this.toDataYCoord(highY, i);
1314     newValueRanges.push([low, hi]);
1315   }
1316 
1317   const zoomCallback = this.getFunctionOption('zoomCallback');
1318   this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, () => {
1319     if (zoomCallback) {
1320       const [minX, maxX] = this.xAxisRange();
1321       zoomCallback.call(this, minX, maxX, this.yAxisRanges());
1322     }
1323   });
1324 };
1325 
1326 /**
1327  * Transition function to use in animations. Returns values between 0.0
1328  * (totally old values) and 1.0 (totally new values) for each frame.
1329  * @private
1330  */
1331 Dygraph.zoomAnimationFunction = function(frame, numFrames) {
1332   var k = 1.5;
1333   return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames));
1334 };
1335 
1336 /**
1337  * Reset the zoom to the original view coordinates. This is the same as
1338  * double-clicking on the graph.
1339  */
1340 Dygraph.prototype.resetZoom = function() {
1341   const dirtyX = this.isZoomed('x');
1342   const dirtyY = this.isZoomed('y');
1343   const dirty = dirtyX || dirtyY;
1344 
1345   // Clear any selection, since it's likely to be drawn in the wrong place.
1346   this.clearSelection();
1347 
1348   if (!dirty) return;
1349 
1350   // Calculate extremes to avoid lack of padding on reset.
1351   const [minDate, maxDate] = this.xAxisExtremes();
1352 
1353   const animatedZooms = this.getBooleanOption('animatedZooms');
1354   const zoomCallback = this.getFunctionOption('zoomCallback');
1355 
1356   // TODO(danvk): merge this block w/ the code below.
1357   // TODO(danvk): factor out a generic, public zoomTo method.
1358   if (!animatedZooms) {
1359     this.dateWindow_ = null;
1360     this.axes_.forEach(axis => {
1361       if (axis.valueRange) delete axis.valueRange;
1362     });
1363 
1364     this.drawGraph_();
1365     if (zoomCallback) {
1366       zoomCallback.call(this, minDate, maxDate, this.yAxisRanges());
1367     }
1368     return;
1369   }
1370 
1371   var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null;
1372   if (dirtyX) {
1373     oldWindow = this.xAxisRange();
1374     newWindow = [minDate, maxDate];
1375   }
1376 
1377   if (dirtyY) {
1378     oldValueRanges = this.yAxisRanges();
1379     newValueRanges = this.yAxisExtremes();
1380   }
1381 
1382   this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges,
1383       () => {
1384         this.dateWindow_ = null;
1385         this.axes_.forEach(axis => {
1386           if (axis.valueRange) delete axis.valueRange;
1387         });
1388         if (zoomCallback) {
1389           zoomCallback.call(this, minDate, maxDate, this.yAxisRanges());
1390         }
1391       });
1392 };
1393 
1394 /**
1395  * Combined animation logic for all zoom functions.
1396  * either the x parameters or y parameters may be null.
1397  * @private
1398  */
1399 Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) {
1400   var steps = this.getBooleanOption("animatedZooms") ?
1401       Dygraph.ANIMATION_STEPS : 1;
1402 
1403   var windows = [];
1404   var valueRanges = [];
1405   var step, frac;
1406 
1407   if (oldXRange !== null && newXRange !== null) {
1408     for (step = 1; step <= steps; step++) {
1409       frac = Dygraph.zoomAnimationFunction(step, steps);
1410       windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0],
1411                          oldXRange[1]*(1-frac) + frac*newXRange[1]];
1412     }
1413   }
1414 
1415   if (oldYRanges !== null && newYRanges !== null) {
1416     for (step = 1; step <= steps; step++) {
1417       frac = Dygraph.zoomAnimationFunction(step, steps);
1418       var thisRange = [];
1419       for (var j = 0; j < this.axes_.length; j++) {
1420         thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0],
1421                         oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]);
1422       }
1423       valueRanges[step-1] = thisRange;
1424     }
1425   }
1426 
1427   utils.repeatAndCleanup(step => {
1428     if (valueRanges.length) {
1429       for (var i = 0; i < this.axes_.length; i++) {
1430         var w = valueRanges[step][i];
1431         this.axes_[i].valueRange = [w[0], w[1]];
1432       }
1433     }
1434     if (windows.length) {
1435       this.dateWindow_ = windows[step];
1436     }
1437     this.drawGraph_();
1438   }, steps, Dygraph.ANIMATION_DURATION / steps, callback);
1439 };
1440 
1441 /**
1442  * Get the current graph's area object.
1443  *
1444  * Returns: {x, y, w, h}
1445  */
1446 Dygraph.prototype.getArea = function() {
1447   return this.plotter_.area;
1448 };
1449 
1450 /**
1451  * Convert a mouse event to DOM coordinates relative to the graph origin.
1452  *
1453  * Returns a two-element array: [X, Y].
1454  */
1455 Dygraph.prototype.eventToDomCoords = function(event) {
1456   if (event.offsetX && event.offsetY) {
1457     return [ event.offsetX, event.offsetY ];
1458   } else {
1459     var eventElementPos = utils.findPos(this.mouseEventElement_);
1460     var canvasx = utils.pageX(event) - eventElementPos.x;
1461     var canvasy = utils.pageY(event) - eventElementPos.y;
1462     return [canvasx, canvasy];
1463   }
1464 };
1465 
1466 /**
1467  * Given a canvas X coordinate, find the closest row.
1468  * @param {number} domX graph-relative DOM X coordinate
1469  * Returns {number} row number.
1470  * @private
1471  */
1472 Dygraph.prototype.findClosestRow = function(domX) {
1473   var minDistX = Infinity;
1474   var closestRow = -1;
1475   var sets = this.layout_.points;
1476   for (var i = 0; i < sets.length; i++) {
1477     var points = sets[i];
1478     var len = points.length;
1479     for (var j = 0; j < len; j++) {
1480       var point = points[j];
1481       if (!utils.isValidPoint(point, true)) continue;
1482       var dist = Math.abs(point.canvasx - domX);
1483       if (dist < minDistX) {
1484         minDistX = dist;
1485         closestRow = point.idx;
1486       }
1487     }
1488   }
1489 
1490   return closestRow;
1491 };
1492 
1493 /**
1494  * Given canvas X,Y coordinates, find the closest point.
1495  *
1496  * This finds the individual data point across all visible series
1497  * that's closest to the supplied DOM coordinates using the standard
1498  * Euclidean X,Y distance.
1499  *
1500  * @param {number} domX graph-relative DOM X coordinate
1501  * @param {number} domY graph-relative DOM Y coordinate
1502  * Returns: {row, seriesName, point}
1503  * @private
1504  */
1505 Dygraph.prototype.findClosestPoint = function(domX, domY) {
1506   var minDist = Infinity;
1507   var dist, dx, dy, point, closestPoint, closestSeries, closestRow;
1508   for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) {
1509     var points = this.layout_.points[setIdx];
1510     for (var i = 0; i < points.length; ++i) {
1511       point = points[i];
1512       if (!utils.isValidPoint(point)) continue;
1513       dx = point.canvasx - domX;
1514       dy = point.canvasy - domY;
1515       dist = dx * dx + dy * dy;
1516       if (dist < minDist) {
1517         minDist = dist;
1518         closestPoint = point;
1519         closestSeries = setIdx;
1520         closestRow = point.idx;
1521       }
1522     }
1523   }
1524   var name = this.layout_.setNames[closestSeries];
1525   return {
1526     row: closestRow,
1527     seriesName: name,
1528     point: closestPoint
1529   };
1530 };
1531 
1532 /**
1533  * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1534  *
1535  * This first finds the X data point closest to the supplied DOM X coordinate,
1536  * then finds the series which puts the Y coordinate on top of its filled area,
1537  * using linear interpolation between adjacent point pairs.
1538  *
1539  * @param {number} domX graph-relative DOM X coordinate
1540  * @param {number} domY graph-relative DOM Y coordinate
1541  * Returns: {row, seriesName, point}
1542  * @private
1543  */
1544 Dygraph.prototype.findStackedPoint = function(domX, domY) {
1545   var row = this.findClosestRow(domX);
1546   var closestPoint, closestSeries;
1547   for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
1548     var boundary = this.getLeftBoundary_(setIdx);
1549     var rowIdx = row - boundary;
1550     var points = this.layout_.points[setIdx];
1551     if (rowIdx >= points.length) continue;
1552     var p1 = points[rowIdx];
1553     if (!utils.isValidPoint(p1)) continue;
1554     var py = p1.canvasy;
1555     if (domX > p1.canvasx && rowIdx + 1 < points.length) {
1556       // interpolate series Y value using next point
1557       var p2 = points[rowIdx + 1];
1558       if (utils.isValidPoint(p2)) {
1559         var dx = p2.canvasx - p1.canvasx;
1560         if (dx > 0) {
1561           var r = (domX - p1.canvasx) / dx;
1562           py += r * (p2.canvasy - p1.canvasy);
1563         }
1564       }
1565     } else if (domX < p1.canvasx && rowIdx > 0) {
1566       // interpolate series Y value using previous point
1567       var p0 = points[rowIdx - 1];
1568       if (utils.isValidPoint(p0)) {
1569         var dx = p1.canvasx - p0.canvasx;
1570         if (dx > 0) {
1571           var r = (p1.canvasx - domX) / dx;
1572           py += r * (p0.canvasy - p1.canvasy);
1573         }
1574       }
1575     }
1576     // Stop if the point (domX, py) is above this series' upper edge
1577     if (setIdx === 0 || py < domY) {
1578       closestPoint = p1;
1579       closestSeries = setIdx;
1580     }
1581   }
1582   var name = this.layout_.setNames[closestSeries];
1583   return {
1584     row: row,
1585     seriesName: name,
1586     point: closestPoint
1587   };
1588 };
1589 
1590 /**
1591  * When the mouse moves in the canvas, display information about a nearby data
1592  * point and draw dots over those points in the data series. This function
1593  * takes care of cleanup of previously-drawn dots.
1594  * @param {Object} event The mousemove event from the browser.
1595  * @private
1596  */
1597 Dygraph.prototype.mouseMove_ = function(event) {
1598   // This prevents JS errors when mousing over the canvas before data loads.
1599   var points = this.layout_.points;
1600   if (points === undefined || points === null) return;
1601 
1602   var canvasCoords = this.eventToDomCoords(event);
1603   var canvasx = canvasCoords[0];
1604   var canvasy = canvasCoords[1];
1605 
1606   var highlightSeriesOpts = this.getOption("highlightSeriesOpts");
1607   var selectionChanged = false;
1608   if (highlightSeriesOpts && !this.isSeriesLocked()) {
1609     var closest;
1610     if (this.getBooleanOption("stackedGraph")) {
1611       closest = this.findStackedPoint(canvasx, canvasy);
1612     } else {
1613       closest = this.findClosestPoint(canvasx, canvasy);
1614     }
1615     selectionChanged = this.setSelection(closest.row, closest.seriesName);
1616   } else {
1617     var idx = this.findClosestRow(canvasx);
1618     selectionChanged = this.setSelection(idx);
1619   }
1620 
1621   var callback = this.getFunctionOption("highlightCallback");
1622   if (callback && selectionChanged) {
1623     callback.call(this, event,
1624         this.lastx_,
1625         this.selPoints_,
1626         this.lastRow_,
1627         this.highlightSet_);
1628   }
1629 };
1630 
1631 /**
1632  * Fetch left offset from the specified set index or if not passed, the
1633  * first defined boundaryIds record (see bug #236).
1634  * @private
1635  */
1636 Dygraph.prototype.getLeftBoundary_ = function(setIdx) {
1637   if (this.boundaryIds_[setIdx]) {
1638       return this.boundaryIds_[setIdx][0];
1639   } else {
1640     for (var i = 0; i < this.boundaryIds_.length; i++) {
1641       if (this.boundaryIds_[i] !== undefined) {
1642         return this.boundaryIds_[i][0];
1643       }
1644     }
1645     return 0;
1646   }
1647 };
1648 
1649 Dygraph.prototype.animateSelection_ = function(direction) {
1650   var totalSteps = 10;
1651   var millis = 30;
1652   if (this.fadeLevel === undefined) this.fadeLevel = 0;
1653   if (this.animateId === undefined) this.animateId = 0;
1654   var start = this.fadeLevel;
1655   var steps = direction < 0 ? start : totalSteps - start;
1656   if (steps <= 0) {
1657     if (this.fadeLevel) {
1658       this.updateSelection_(1.0);
1659     }
1660     return;
1661   }
1662 
1663   var thisId = ++this.animateId;
1664   var that = this;
1665   var cleanupIfClearing = function() {
1666     // if we haven't reached fadeLevel 0 in the max frame time,
1667     // ensure that the clear happens and just go to 0
1668     if (that.fadeLevel !== 0 && direction < 0) {
1669       that.fadeLevel = 0;
1670       that.clearSelection();
1671     }
1672   };
1673   utils.repeatAndCleanup(
1674     function(n) {
1675       // ignore simultaneous animations
1676       if (that.animateId != thisId) return;
1677 
1678       that.fadeLevel += direction;
1679       if (that.fadeLevel === 0) {
1680         that.clearSelection();
1681       } else {
1682         that.updateSelection_(that.fadeLevel / totalSteps);
1683       }
1684     },
1685     steps, millis, cleanupIfClearing);
1686 };
1687 
1688 /**
1689  * Draw dots over the selectied points in the data series. This function
1690  * takes care of cleanup of previously-drawn dots.
1691  * @private
1692  */
1693 Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
1694   /*var defaultPrevented = */
1695   this.cascadeEvents_('select', {
1696     selectedRow: this.lastRow_ === -1 ? undefined : this.lastRow_,
1697     selectedX: this.lastx_ === -1 ? undefined : this.lastx_,
1698     selectedPoints: this.selPoints_
1699   });
1700   // TODO(danvk): use defaultPrevented here?
1701 
1702   // Clear the previously drawn vertical, if there is one
1703   var i;
1704   var ctx = this.canvas_ctx_;
1705   if (this.getOption('highlightSeriesOpts')) {
1706     ctx.clearRect(0, 0, this.width_, this.height_);
1707     var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
1708     var backgroundColor = utils.toRGB_(this.getOption('highlightSeriesBackgroundColor'));
1709 
1710     if (alpha) {
1711       // Activating background fade includes an animation effect for a gradual
1712       // fade. TODO(klausw): make this independently configurable if it causes
1713       // issues? Use a shared preference to control animations?
1714       var animateBackgroundFade = this.getBooleanOption('animateBackgroundFade');
1715       if (animateBackgroundFade) {
1716         if (opt_animFraction === undefined) {
1717           // start a new animation
1718           this.animateSelection_(1);
1719           return;
1720         }
1721         alpha *= opt_animFraction;
1722       }
1723       ctx.fillStyle = 'rgba(' + backgroundColor.r + ',' + backgroundColor.g + ',' + backgroundColor.b + ',' + alpha + ')';
1724       ctx.fillRect(0, 0, this.width_, this.height_);
1725     }
1726 
1727     // Redraw only the highlighted series in the interactive canvas (not the
1728     // static plot canvas, which is where series are usually drawn).
1729     this.plotter_._renderLineChart(this.highlightSet_, ctx);
1730   } else if (this.previousVerticalX_ >= 0) {
1731     // Determine the maximum highlight circle size.
1732     var maxCircleSize = 0;
1733     var labels = this.attr_('labels');
1734     for (i = 1; i < labels.length; i++) {
1735       var r = this.getNumericOption('highlightCircleSize', labels[i]);
1736       if (r > maxCircleSize) maxCircleSize = r;
1737     }
1738     var px = this.previousVerticalX_;
1739     ctx.clearRect(px - maxCircleSize - 1, 0,
1740                   2 * maxCircleSize + 2, this.height_);
1741   }
1742 
1743   if (this.selPoints_.length > 0) {
1744     // Draw colored circles over the center of each selected point
1745     var canvasx = this.selPoints_[0].canvasx;
1746     ctx.save();
1747     for (i = 0; i < this.selPoints_.length; i++) {
1748       var pt = this.selPoints_[i];
1749       if (isNaN(pt.canvasy)) continue;
1750 
1751       var circleSize = this.getNumericOption('highlightCircleSize', pt.name);
1752       var callback = this.getFunctionOption("drawHighlightPointCallback", pt.name);
1753       var color = this.plotter_.colors[pt.name];
1754       if (!callback) {
1755         callback = utils.Circles.DEFAULT;
1756       }
1757       ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name);
1758       ctx.strokeStyle = color;
1759       ctx.fillStyle = color;
1760       callback.call(this, this, pt.name, ctx, canvasx, pt.canvasy,
1761           color, circleSize, pt.idx);
1762     }
1763     ctx.restore();
1764 
1765     this.previousVerticalX_ = canvasx;
1766   }
1767 };
1768 
1769 /**
1770  * Manually set the selected points and display information about them in the
1771  * legend. The selection can be cleared using clearSelection() and queried
1772  * using getSelection().
1773  *
1774  * To set a selected series but not a selected point, call setSelection with
1775  * row=false and the selected series name.
1776  *
1777  * @param {number} row Row number that should be highlighted (i.e. appear with
1778  * hover dots on the chart).
1779  * @param {seriesName} optional series name to highlight that series with the
1780  * the highlightSeriesOpts setting.
1781  * @param { locked } optional If true, keep seriesName selected when mousing
1782  * over the graph, disabling closest-series highlighting. Call clearSelection()
1783  * to unlock it.
1784  */
1785 Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) {
1786   // Extract the points we've selected
1787   this.selPoints_ = [];
1788 
1789   var changed = false;
1790   if (row !== false && row >= 0) {
1791     if (row != this.lastRow_) changed = true;
1792     this.lastRow_ = row;
1793     for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
1794       var points = this.layout_.points[setIdx];
1795       // Check if the point at the appropriate index is the point we're looking
1796       // for.  If it is, just use it, otherwise search the array for a point
1797       // in the proper place.
1798       var setRow = row - this.getLeftBoundary_(setIdx);
1799       if (setRow >= 0 && setRow < points.length && points[setRow].idx == row) {
1800         var point = points[setRow];
1801         if (point.yval !== null) this.selPoints_.push(point);
1802       } else {
1803         for (var pointIdx = 0; pointIdx < points.length; ++pointIdx) {
1804           var point = points[pointIdx];
1805           if (point.idx == row) {
1806             if (point.yval !== null) {
1807               this.selPoints_.push(point);
1808             }
1809             break;
1810           }
1811         }
1812       }
1813     }
1814   } else {
1815     if (this.lastRow_ >= 0) changed = true;
1816     this.lastRow_ = -1;
1817   }
1818 
1819   if (this.selPoints_.length) {
1820     this.lastx_ = this.selPoints_[0].xval;
1821   } else {
1822     this.lastx_ = -1;
1823   }
1824 
1825   if (opt_seriesName !== undefined) {
1826     if (this.highlightSet_ !== opt_seriesName) changed = true;
1827     this.highlightSet_ = opt_seriesName;
1828   }
1829 
1830   if (opt_locked !== undefined) {
1831     this.lockedSet_ = opt_locked;
1832   }
1833 
1834   if (changed) {
1835     this.updateSelection_(undefined);
1836   }
1837   return changed;
1838 };
1839 
1840 /**
1841  * The mouse has left the canvas. Clear out whatever artifacts remain
1842  * @param {Object} event the mouseout event from the browser.
1843  * @private
1844  */
1845 Dygraph.prototype.mouseOut_ = function(event) {
1846   if (this.getFunctionOption("unhighlightCallback")) {
1847     this.getFunctionOption("unhighlightCallback").call(this, event);
1848   }
1849 
1850   if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_) {
1851     this.clearSelection();
1852   }
1853 };
1854 
1855 /**
1856  * Clears the current selection (i.e. points that were highlighted by moving
1857  * the mouse over the chart).
1858  */
1859 Dygraph.prototype.clearSelection = function() {
1860   this.cascadeEvents_('deselect', {});
1861 
1862   this.lockedSet_ = false;
1863   // Get rid of the overlay data
1864   if (this.fadeLevel) {
1865     this.animateSelection_(-1);
1866     return;
1867   }
1868   this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
1869   this.fadeLevel = 0;
1870   this.selPoints_ = [];
1871   this.lastx_ = -1;
1872   this.lastRow_ = -1;
1873   this.highlightSet_ = null;
1874 };
1875 
1876 /**
1877  * Returns the number of the currently selected row. To get data for this row,
1878  * you can use the getValue method.
1879  * @return {number} row number, or -1 if nothing is selected
1880  */
1881 Dygraph.prototype.getSelection = function() {
1882   if (!this.selPoints_ || this.selPoints_.length < 1) {
1883     return -1;
1884   }
1885 
1886   for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) {
1887     var points = this.layout_.points[setIdx];
1888     for (var row = 0; row < points.length; row++) {
1889       if (points[row].x == this.selPoints_[0].x) {
1890         return points[row].idx;
1891       }
1892     }
1893   }
1894   return -1;
1895 };
1896 
1897 /**
1898  * Returns the name of the currently-highlighted series.
1899  * Only available when the highlightSeriesOpts option is in use.
1900  */
1901 Dygraph.prototype.getHighlightSeries = function() {
1902   return this.highlightSet_;
1903 };
1904 
1905 /**
1906  * Returns true if the currently-highlighted series was locked
1907  * via setSelection(..., seriesName, true).
1908  */
1909 Dygraph.prototype.isSeriesLocked = function() {
1910   return this.lockedSet_;
1911 };
1912 
1913 /**
1914  * Fires when there's data available to be graphed.
1915  * @param {string} data Raw CSV data to be plotted
1916  * @private
1917  */
1918 Dygraph.prototype.loadedEvent_ = function(data) {
1919   this.rawData_ = this.parseCSV_(data);
1920   this.cascadeDataDidUpdateEvent_();
1921   this.predraw_();
1922 };
1923 
1924 /**
1925  * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1926  * @private
1927  */
1928 Dygraph.prototype.addXTicks_ = function() {
1929   // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1930   var range;
1931   if (this.dateWindow_) {
1932     range = [this.dateWindow_[0], this.dateWindow_[1]];
1933   } else {
1934     range = this.xAxisExtremes();
1935   }
1936 
1937   var xAxisOptionsView = this.optionsViewForAxis_('x');
1938   var xTicks = xAxisOptionsView('ticker')(
1939       range[0],
1940       range[1],
1941       this.plotter_.area.w,  // TODO(danvk): should be area.width
1942       xAxisOptionsView,
1943       this);
1944   // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
1945   // console.log(msg);
1946   this.layout_.setXTicks(xTicks);
1947 };
1948 
1949 /**
1950  * Returns the correct handler class for the currently set options.
1951  * @private
1952  */
1953 Dygraph.prototype.getHandlerClass_ = function() {
1954   var handlerClass;
1955   if (this.attr_('dataHandler')) {
1956     handlerClass =  this.attr_('dataHandler');
1957   } else if (this.fractions_) {
1958     if (this.getBooleanOption('errorBars')) {
1959       handlerClass = FractionsBarsHandler;
1960     } else {
1961       handlerClass = DefaultFractionHandler;
1962     }
1963   } else if (this.getBooleanOption('customBars')) {
1964     handlerClass = CustomBarsHandler;
1965   } else if (this.getBooleanOption('errorBars')) {
1966     handlerClass = ErrorBarsHandler;
1967   } else {
1968     handlerClass = DefaultHandler;
1969   }
1970   return handlerClass;
1971 };
1972 
1973 /**
1974  * @private
1975  * This function is called once when the chart's data is changed or the options
1976  * dictionary is updated. It is _not_ called when the user pans or zooms. The
1977  * idea is that values derived from the chart's data can be computed here,
1978  * rather than every time the chart is drawn. This includes things like the
1979  * number of axes, rolling averages, etc.
1980  */
1981 Dygraph.prototype.predraw_ = function() {
1982   var start = new Date();
1983 
1984   // Create the correct dataHandler
1985   this.dataHandler_ = new (this.getHandlerClass_())();
1986 
1987   this.layout_.computePlotArea();
1988 
1989   // TODO(danvk): move more computations out of drawGraph_ and into here.
1990   this.computeYAxes_();
1991 
1992   if (!this.is_initial_draw_) {
1993     this.canvas_ctx_.restore();
1994     this.hidden_ctx_.restore();
1995   }
1996 
1997   this.canvas_ctx_.save();
1998   this.hidden_ctx_.save();
1999 
2000   // Create a new plotter.
2001   this.plotter_ = new DygraphCanvasRenderer(this,
2002                                             this.hidden_,
2003                                             this.hidden_ctx_,
2004                                             this.layout_);
2005 
2006   // The roller sits in the bottom left corner of the chart. We don't know where
2007   // this will be until the options are available, so it's positioned here.
2008   this.createRollInterface_();
2009 
2010   this.cascadeEvents_('predraw');
2011 
2012   // Convert the raw data (a 2D array) into the internal format and compute
2013   // rolling averages.
2014   this.rolledSeries_ = [null];  // x-axis is the first series and it's special
2015   for (var i = 1; i < this.numColumns(); i++) {
2016     // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too.
2017     var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_);
2018     if (this.rollPeriod_ > 1) {
2019       series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_);
2020     }
2021 
2022     this.rolledSeries_.push(series);
2023   }
2024 
2025   // If the data or options have changed, then we'd better redraw.
2026   this.drawGraph_();
2027 
2028   // This is used to determine whether to do various animations.
2029   var end = new Date();
2030   this.drawingTimeMs_ = (end - start);
2031 };
2032 
2033 /**
2034  * Point structure.
2035  *
2036  * xval_* and yval_* are the original unscaled data values,
2037  * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
2038  * yval_stacked is the cumulative Y value used for stacking graphs,
2039  * and bottom/top/minus/plus are used for error bar graphs.
2040  *
2041  * @typedef {{
2042  *     idx: number,
2043  *     name: string,
2044  *     x: ?number,
2045  *     xval: ?number,
2046  *     y_bottom: ?number,
2047  *     y: ?number,
2048  *     y_stacked: ?number,
2049  *     y_top: ?number,
2050  *     yval_minus: ?number,
2051  *     yval: ?number,
2052  *     yval_plus: ?number,
2053  *     yval_stacked
2054  * }}
2055  */
2056 Dygraph.PointType = undefined;
2057 
2058 /**
2059  * Calculates point stacking for stackedGraph=true.
2060  *
2061  * For stacking purposes, interpolate or extend neighboring data across
2062  * NaN values based on stackedGraphNaNFill settings. This is for display
2063  * only, the underlying data value as shown in the legend remains NaN.
2064  *
2065  * @param {Array.<Dygraph.PointType>} points Point array for a single series.
2066  *     Updates each Point's yval_stacked property.
2067  * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
2068  *     values for the series seen so far. Index is the row number. Updated
2069  *     based on the current series's values.
2070  * @param {Array.<number>} seriesExtremes Min and max values, updated
2071  *     to reflect the stacked values.
2072  * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
2073  *     'none'.
2074  * @private
2075  */
2076 Dygraph.stackPoints_ = function(
2077     points, cumulativeYval, seriesExtremes, fillMethod) {
2078   var lastXval = null;
2079   var prevPoint = null;
2080   var nextPoint = null;
2081   var nextPointIdx = -1;
2082 
2083   // Find the next stackable point starting from the given index.
2084   var updateNextPoint = function(idx) {
2085     // If we've previously found a non-NaN point and haven't gone past it yet,
2086     // just use that.
2087     if (nextPointIdx >= idx) return;
2088 
2089     // We haven't found a non-NaN point yet or have moved past it,
2090     // look towards the right to find a non-NaN point.
2091     for (var j = idx; j < points.length; ++j) {
2092       // Clear out a previously-found point (if any) since it's no longer
2093       // valid, we shouldn't use it for interpolation anymore.
2094       nextPoint = null;
2095       if (!isNaN(points[j].yval) && points[j].yval !== null) {
2096         nextPointIdx = j;
2097         nextPoint = points[j];
2098         break;
2099       }
2100     }
2101   };
2102 
2103   for (var i = 0; i < points.length; ++i) {
2104     var point = points[i];
2105     var xval = point.xval;
2106     if (cumulativeYval[xval] === undefined) {
2107       cumulativeYval[xval] = 0;
2108     }
2109 
2110     var actualYval = point.yval;
2111     if (isNaN(actualYval) || actualYval === null) {
2112       if(fillMethod == 'none') {
2113         actualYval = 0;
2114       } else {
2115         // Interpolate/extend for stacking purposes if possible.
2116         updateNextPoint(i);
2117         if (prevPoint && nextPoint && fillMethod != 'none') {
2118           // Use linear interpolation between prevPoint and nextPoint.
2119           actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) *
2120               ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval));
2121         } else if (prevPoint && fillMethod == 'all') {
2122           actualYval = prevPoint.yval;
2123         } else if (nextPoint && fillMethod == 'all') {
2124           actualYval = nextPoint.yval;
2125         } else {
2126           actualYval = 0;
2127         }
2128       }
2129     } else {
2130       prevPoint = point;
2131     }
2132 
2133     var stackedYval = cumulativeYval[xval];
2134     if (lastXval != xval) {
2135       // If an x-value is repeated, we ignore the duplicates.
2136       stackedYval += actualYval;
2137       cumulativeYval[xval] = stackedYval;
2138     }
2139     lastXval = xval;
2140 
2141     point.yval_stacked = stackedYval;
2142 
2143     if (stackedYval > seriesExtremes[1]) {
2144       seriesExtremes[1] = stackedYval;
2145     }
2146     if (stackedYval < seriesExtremes[0]) {
2147       seriesExtremes[0] = stackedYval;
2148     }
2149   }
2150 };
2151 
2152 /**
2153  * Loop over all fields and create datasets, calculating extreme y-values for
2154  * each series and extreme x-indices as we go.
2155  *
2156  * dateWindow is passed in as an explicit parameter so that we can compute
2157  * extreme values "speculatively", i.e. without actually setting state on the
2158  * dygraph.
2159  *
2160  * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
2161  *     rolledSeries[seriesIndex][row] = raw point, where
2162  *     seriesIndex is the column number starting with 1, and
2163  *     rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
2164  * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
2165  * @return {{
2166  *     points: Array.<Array.<Dygraph.PointType>>,
2167  *     seriesExtremes: Array.<Array.<number>>,
2168  *     boundaryIds: Array.<number>}}
2169  * @private
2170  */
2171 Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
2172   var boundaryIds = [];
2173   var points = [];
2174   var cumulativeYval = [];  // For stacked series.
2175   var extremes = {};  // series name -> [low, high]
2176   var seriesIdx, sampleIdx;
2177   var firstIdx, lastIdx;
2178   var axisIdx;
2179 
2180   // Loop over the fields (series).  Go from the last to the first,
2181   // because if they're stacked that's how we accumulate the values.
2182   var num_series = rolledSeries.length - 1;
2183   var series;
2184   for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) {
2185     if (!this.visibility()[seriesIdx - 1]) continue;
2186 
2187     // Prune down to the desired range, if necessary (for zooming)
2188     // Because there can be lines going to points outside of the visible area,
2189     // we actually prune to visible points, plus one on either side.
2190     if (dateWindow) {
2191       series = rolledSeries[seriesIdx];
2192       var low = dateWindow[0];
2193       var high = dateWindow[1];
2194 
2195       // TODO(danvk): do binary search instead of linear search.
2196       // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2197       firstIdx = null;
2198       lastIdx = null;
2199       for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) {
2200         if (series[sampleIdx][0] >= low && firstIdx === null) {
2201           firstIdx = sampleIdx;
2202         }
2203         if (series[sampleIdx][0] <= high) {
2204           lastIdx = sampleIdx;
2205         }
2206       }
2207 
2208       if (firstIdx === null) firstIdx = 0;
2209       var correctedFirstIdx = firstIdx;
2210       var isInvalidValue = true;
2211       while (isInvalidValue && correctedFirstIdx > 0) {
2212         correctedFirstIdx--;
2213         // check if the y value is null.
2214         isInvalidValue = series[correctedFirstIdx][1] === null;
2215       }
2216 
2217       if (lastIdx === null) lastIdx = series.length - 1;
2218       var correctedLastIdx = lastIdx;
2219       isInvalidValue = true;
2220       while (isInvalidValue && correctedLastIdx < series.length - 1) {
2221         correctedLastIdx++;
2222         isInvalidValue = series[correctedLastIdx][1] === null;
2223       }
2224 
2225       if (correctedFirstIdx!==firstIdx) {
2226         firstIdx = correctedFirstIdx;
2227       }
2228       if (correctedLastIdx !== lastIdx) {
2229         lastIdx = correctedLastIdx;
2230       }
2231 
2232       boundaryIds[seriesIdx-1] = [firstIdx, lastIdx];
2233 
2234       // .slice's end is exclusive, we want to include lastIdx.
2235       series = series.slice(firstIdx, lastIdx + 1);
2236     } else {
2237       series = rolledSeries[seriesIdx];
2238       boundaryIds[seriesIdx-1] = [0, series.length-1];
2239     }
2240 
2241     var seriesName = this.attr_("labels")[seriesIdx];
2242     var seriesExtremes = this.dataHandler_.getExtremeYValues(series,
2243         dateWindow, this.getBooleanOption("stepPlot",seriesName));
2244 
2245     var seriesPoints = this.dataHandler_.seriesToPoints(series,
2246         seriesName, boundaryIds[seriesIdx-1][0]);
2247 
2248     if (this.getBooleanOption("stackedGraph")) {
2249       axisIdx = this.attributes_.axisForSeries(seriesName);
2250       if (cumulativeYval[axisIdx] === undefined) {
2251         cumulativeYval[axisIdx] = [];
2252       }
2253       Dygraph.stackPoints_(seriesPoints, cumulativeYval[axisIdx], seriesExtremes,
2254                            this.getBooleanOption("stackedGraphNaNFill"));
2255     }
2256 
2257     extremes[seriesName] = seriesExtremes;
2258     points[seriesIdx] = seriesPoints;
2259   }
2260 
2261   return { points: points, extremes: extremes, boundaryIds: boundaryIds };
2262 };
2263 
2264 /**
2265  * Update the graph with new data. This method is called when the viewing area
2266  * has changed. If the underlying data or options have changed, predraw_ will
2267  * be called before drawGraph_ is called.
2268  *
2269  * @private
2270  */
2271 Dygraph.prototype.drawGraph_ = function() {
2272   var start = new Date();
2273 
2274   // This is used to set the second parameter to drawCallback, below.
2275   var is_initial_draw = this.is_initial_draw_;
2276   this.is_initial_draw_ = false;
2277 
2278   this.layout_.removeAllDatasets();
2279   this.setColors_();
2280   this.attrs_.pointSize = 0.5 * this.getNumericOption('highlightCircleSize');
2281 
2282   var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
2283   var points = packed.points;
2284   var extremes = packed.extremes;
2285   this.boundaryIds_ = packed.boundaryIds;
2286 
2287   this.setIndexByName_ = {};
2288   var labels = this.attr_("labels");
2289   var dataIdx = 0;
2290   for (var i = 1; i < points.length; i++) {
2291     if (!this.visibility()[i - 1]) continue;
2292     this.layout_.addDataset(labels[i], points[i]);
2293     this.datasetIndex_[i] = dataIdx++;
2294   }
2295   for (var i = 0; i < labels.length; i++) {
2296     this.setIndexByName_[labels[i]] = i;
2297   }
2298 
2299   this.computeYAxisRanges_(extremes);
2300   this.layout_.setYAxes(this.axes_);
2301 
2302   this.addXTicks_();
2303 
2304   // Tell PlotKit to use this new data and render itself
2305   this.layout_.evaluate();
2306   this.renderGraph_(is_initial_draw);
2307 
2308   if (this.getStringOption("timingName")) {
2309     var end = new Date();
2310     console.log(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms");
2311   }
2312 };
2313 
2314 /**
2315  * This does the work of drawing the chart. It assumes that the layout and axis
2316  * scales have already been set (e.g. by predraw_).
2317  *
2318  * @private
2319  */
2320 Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
2321   this.cascadeEvents_('clearChart');
2322   this.plotter_.clear();
2323 
2324   const underlayCallback = this.getFunctionOption('underlayCallback');
2325   if (underlayCallback) {
2326     // NOTE: we pass the dygraph object to this callback twice to avoid breaking
2327     // users who expect a deprecated form of this callback.
2328     underlayCallback.call(this,
2329         this.hidden_ctx_, this.layout_.getPlotArea(), this, this);
2330   }
2331 
2332   var e = {
2333     canvas: this.hidden_,
2334     drawingContext: this.hidden_ctx_
2335   };
2336   this.cascadeEvents_('willDrawChart', e);
2337   this.plotter_.render();
2338   this.cascadeEvents_('didDrawChart', e);
2339   this.lastRow_ = -1;  // because plugins/legend.js clears the legend
2340 
2341   // TODO(danvk): is this a performance bottleneck when panning?
2342   // The interaction canvas should already be empty in that situation.
2343   this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_);
2344 
2345   const drawCallback = this.getFunctionOption("drawCallback");
2346   if (drawCallback !== null) {
2347     drawCallback.call(this, this, is_initial_draw);
2348   }
2349   if (is_initial_draw) {
2350     this.readyFired_ = true;
2351     while (this.readyFns_.length > 0) {
2352       var fn = this.readyFns_.pop();
2353       fn(this);
2354     }
2355   }
2356 };
2357 
2358 /**
2359  * @private
2360  * Determine properties of the y-axes which are independent of the data
2361  * currently being displayed. This includes things like the number of axes and
2362  * the style of the axes. It does not include the range of each axis and its
2363  * tick marks.
2364  * This fills in this.axes_.
2365  * axes_ = [ { options } ]
2366  *   indices are into the axes_ array.
2367  */
2368 Dygraph.prototype.computeYAxes_ = function() {
2369   var axis, index, opts, v;
2370 
2371   // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
2372   // data computation as well as options storage.
2373   // Go through once and add all the axes.
2374   this.axes_ = [];
2375 
2376   for (axis = 0; axis < this.attributes_.numAxes(); axis++) {
2377     // Add a new axis, making a copy of its per-axis options.
2378     opts = { g : this };
2379     utils.update(opts, this.attributes_.axisOptions(axis));
2380     this.axes_[axis] = opts;
2381   }
2382 
2383   for (axis = 0; axis < this.axes_.length; axis++) {
2384     if (axis === 0) {
2385       opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
2386       v = opts("valueRange");
2387       if (v) this.axes_[axis].valueRange = v;
2388     } else {  // To keep old behavior
2389       var axes = this.user_attrs_.axes;
2390       if (axes && axes.y2) {
2391         v = axes.y2.valueRange;
2392         if (v) this.axes_[axis].valueRange = v;
2393       }
2394     }
2395   }
2396 };
2397 
2398 /**
2399  * Returns the number of y-axes on the chart.
2400  * @return {number} the number of axes.
2401  */
2402 Dygraph.prototype.numAxes = function() {
2403   return this.attributes_.numAxes();
2404 };
2405 
2406 /**
2407  * @private
2408  * Returns axis properties for the given series.
2409  * @param {string} setName The name of the series for which to get axis
2410  * properties, e.g. 'Y1'.
2411  * @return {Object} The axis properties.
2412  */
2413 Dygraph.prototype.axisPropertiesForSeries = function(series) {
2414   // TODO(danvk): handle errors.
2415   return this.axes_[this.attributes_.axisForSeries(series)];
2416 };
2417 
2418 /**
2419  * @private
2420  * Determine the value range and tick marks for each axis.
2421  * @param {Object} extremes A mapping from seriesName -> [low, high]
2422  * This fills in the valueRange and ticks fields in each entry of this.axes_.
2423  */
2424 Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2425   var isNullUndefinedOrNaN = function(num) {
2426     return isNaN(parseFloat(num));
2427   };
2428   var numAxes = this.attributes_.numAxes();
2429   var ypadCompat, span, series, ypad;
2430 
2431   var p_axis;
2432 
2433   // Compute extreme values, a span and tick marks for each axis.
2434   for (var i = 0; i < numAxes; i++) {
2435     var axis = this.axes_[i];
2436     var logscale = this.attributes_.getForAxis("logscale", i);
2437     var includeZero = this.attributes_.getForAxis("includeZero", i);
2438     var independentTicks = this.attributes_.getForAxis("independentTicks", i);
2439     series = this.attributes_.seriesForAxis(i);
2440 
2441     // Add some padding. This supports two Y padding operation modes:
2442     //
2443     // - backwards compatible (yRangePad not set):
2444     //   10% padding for automatic Y ranges, but not for user-supplied
2445     //   ranges, and move a close-to-zero edge to zero, since drawing at the edge
2446     //   results in invisible lines. Unfortunately lines drawn at the edge of a
2447     //   user-supplied range will still be invisible. If logscale is
2448     //   set, add a variable amount of padding at the top but
2449     //   none at the bottom.
2450     //
2451     // - new-style (yRangePad set by the user):
2452     //   always add the specified Y padding.
2453     //
2454     ypadCompat = true;
2455     ypad = 0.1; // add 10%
2456     const yRangePad = this.getNumericOption('yRangePad');
2457     if (yRangePad !== null) {
2458       ypadCompat = false;
2459       // Convert pixel padding to ratio
2460       ypad = yRangePad / this.plotter_.area.h;
2461     }
2462 
2463     if (series.length === 0) {
2464       // If no series are defined or visible then use a reasonable default
2465       axis.extremeRange = [0, 1];
2466     } else {
2467       // Calculate the extremes of extremes.
2468       var minY = Infinity;  // extremes[series[0]][0];
2469       var maxY = -Infinity;  // extremes[series[0]][1];
2470       var extremeMinY, extremeMaxY;
2471 
2472       for (var j = 0; j < series.length; j++) {
2473         // this skips invisible series
2474         if (!extremes.hasOwnProperty(series[j])) continue;
2475 
2476         // Only use valid extremes to stop null data series' from corrupting the scale.
2477         extremeMinY = extremes[series[j]][0];
2478         if (extremeMinY !== null) {
2479           minY = Math.min(extremeMinY, minY);
2480         }
2481         extremeMaxY = extremes[series[j]][1];
2482         if (extremeMaxY !== null) {
2483           maxY = Math.max(extremeMaxY, maxY);
2484         }
2485       }
2486 
2487       // Include zero if requested by the user.
2488       if (includeZero && !logscale) {
2489         if (minY > 0) minY = 0;
2490         if (maxY < 0) maxY = 0;
2491       }
2492 
2493       // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2494       if (minY == Infinity) minY = 0;
2495       if (maxY == -Infinity) maxY = 1;
2496 
2497       span = maxY - minY;
2498       // special case: if we have no sense of scale, center on the sole value.
2499       if (span === 0) {
2500         if (maxY !== 0) {
2501           span = Math.abs(maxY);
2502         } else {
2503           // ... and if the sole value is zero, use range 0-1.
2504           maxY = 1;
2505           span = 1;
2506         }
2507       }
2508 
2509       var maxAxisY = maxY, minAxisY = minY;
2510       if (ypadCompat) {
2511         if (logscale) {
2512           maxAxisY = maxY + ypad * span;
2513           minAxisY = minY;
2514         } else {
2515           maxAxisY = maxY + ypad * span;
2516           minAxisY = minY - ypad * span;
2517 
2518           // Backwards-compatible behavior: Move the span to start or end at zero if it's
2519           // close to zero.
2520           if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2521           if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2522         }
2523       }
2524       axis.extremeRange = [minAxisY, maxAxisY];
2525     }
2526     if (axis.valueRange) {
2527       // This is a user-set value range for this axis.
2528       var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
2529       var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
2530       axis.computedValueRange = [y0, y1];
2531     } else {
2532       axis.computedValueRange = axis.extremeRange;
2533     }
2534     if (!ypadCompat) {
2535       // When using yRangePad, adjust the upper/lower bounds to add
2536       // padding unless the user has zoomed/panned the Y axis range.
2537 
2538       y0 = axis.computedValueRange[0];
2539       y1 = axis.computedValueRange[1];
2540 
2541       // special case #781: if we have no sense of scale, center on the sole value.
2542       if (y0 === y1) {
2543         if(y0 === 0) {
2544           y1 = 1;
2545         } else {
2546           var delta = Math.abs(y0 / 10);
2547           y0 -= delta;
2548           y1 += delta;
2549         }
2550       }
2551 
2552       if (logscale) {
2553         var y0pct = ypad / (2 * ypad - 1);
2554         var y1pct = (ypad - 1) / (2 * ypad - 1);
2555         axis.computedValueRange[0] = utils.logRangeFraction(y0, y1, y0pct);
2556         axis.computedValueRange[1] = utils.logRangeFraction(y0, y1, y1pct);
2557       } else {
2558         span = y1 - y0;
2559         axis.computedValueRange[0] = y0 - span * ypad;
2560         axis.computedValueRange[1] = y1 + span * ypad;
2561       }
2562     }
2563 
2564     if (independentTicks) {
2565       axis.independentTicks = independentTicks;
2566       var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
2567       var ticker = opts('ticker');
2568       axis.ticks = ticker(axis.computedValueRange[0],
2569               axis.computedValueRange[1],
2570               this.plotter_.area.h,
2571               opts,
2572               this);
2573       // Define the first independent axis as primary axis.
2574       if (!p_axis) p_axis = axis;
2575     }
2576   }
2577   if (p_axis === undefined) {
2578     throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
2579   }
2580   // Add ticks. By default, all axes inherit the tick positions of the
2581   // primary axis. However, if an axis is specifically marked as having
2582   // independent ticks, then that is permissible as well.
2583   for (var i = 0; i < numAxes; i++) {
2584     var axis = this.axes_[i];
2585 
2586     if (!axis.independentTicks) {
2587       var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
2588       var ticker = opts('ticker');
2589       var p_ticks = p_axis.ticks;
2590       var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
2591       var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
2592       var tick_values = [];
2593       for (var k = 0; k < p_ticks.length; k++) {
2594         var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale;
2595         var y_val = axis.computedValueRange[0] + y_frac * scale;
2596         tick_values.push(y_val);
2597       }
2598 
2599       axis.ticks = ticker(axis.computedValueRange[0],
2600                           axis.computedValueRange[1],
2601                           this.plotter_.area.h,
2602                           opts,
2603                           this,
2604                           tick_values);
2605     }
2606   }
2607 };
2608 
2609 /**
2610  * Detects the type of the str (date or numeric) and sets the various
2611  * formatting attributes in this.attrs_ based on this type.
2612  * @param {string} str An x value.
2613  * @private
2614  */
2615 Dygraph.prototype.detectTypeFromString_ = function(str) {
2616   var isDate = false;
2617   var dashPos = str.indexOf('-');  // could be 2006-01-01 _or_ 1.0e-2
2618   if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) ||
2619       str.indexOf('/') >= 0 ||
2620       isNaN(parseFloat(str))) {
2621     isDate = true;
2622   }
2623 
2624   this.setXAxisOptions_(isDate);
2625 };
2626 
2627 Dygraph.prototype.setXAxisOptions_ = function(isDate) {
2628   if (isDate) {
2629     this.attrs_.xValueParser = utils.dateParser;
2630     this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2631     this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2632     this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2633   } else {
2634     /** @private (shut up, jsdoc!) */
2635     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2636     // TODO(danvk): use Dygraph.numberValueFormatter here?
2637     /** @private (shut up, jsdoc!) */
2638     this.attrs_.axes.x.valueFormatter = function(x) { return x; };
2639     this.attrs_.axes.x.ticker = DygraphTickers.numericTicks;
2640     this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
2641   }
2642 };
2643 
2644 /**
2645  * @private
2646  * Parses a string in a special csv format.  We expect a csv file where each
2647  * line is a date point, and the first field in each line is the date string.
2648  * We also expect that all remaining fields represent series.
2649  * if the errorBars attribute is set, then interpret the fields as:
2650  * date, series1, stddev1, series2, stddev2, ...
2651  * @param {[Object]} data See above.
2652  *
2653  * @return [Object] An array with one entry for each row. These entries
2654  * are an array of cells in that row. The first entry is the parsed x-value for
2655  * the row. The second, third, etc. are the y-values. These can take on one of
2656  * three forms, depending on the CSV and constructor parameters:
2657  * 1. numeric value
2658  * 2. [ value, stddev ]
2659  * 3. [ low value, center value, high value ]
2660  */
2661 Dygraph.prototype.parseCSV_ = function(data) {
2662   var ret = [];
2663   var line_delimiter = utils.detectLineDelimiter(data);
2664   var lines = data.split(line_delimiter || "\n");
2665   var vals, j;
2666 
2667   // Use the default delimiter or fall back to a tab if that makes sense.
2668   var delim = this.getStringOption('delimiter');
2669   if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
2670     delim = '\t';
2671   }
2672 
2673   var start = 0;
2674   if (!('labels' in this.user_attrs_)) {
2675     // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2676     start = 1;
2677     this.attrs_.labels = lines[0].split(delim);  // NOTE: _not_ user_attrs_.
2678     this.attributes_.reparseSeries();
2679   }
2680   var line_no = 0;
2681 
2682   var xParser;
2683   var defaultParserSet = false;  // attempt to auto-detect x value type
2684   var expectedCols = this.attr_("labels").length;
2685   var outOfOrder = false;
2686   for (var i = start; i < lines.length; i++) {
2687     var line = lines[i];
2688     line_no = i;
2689     if (line.length === 0) continue;  // skip blank lines
2690     if (line[0] == '#') continue;    // skip comment lines
2691     var inFields = line.split(delim);
2692     if (inFields.length < 2) continue;
2693 
2694     var fields = [];
2695     if (!defaultParserSet) {
2696       this.detectTypeFromString_(inFields[0]);
2697       xParser = this.getFunctionOption("xValueParser");
2698       defaultParserSet = true;
2699     }
2700     fields[0] = xParser(inFields[0], this);
2701 
2702     // If fractions are expected, parse the numbers as "A/B"
2703     if (this.fractions_) {
2704       for (j = 1; j < inFields.length; j++) {
2705         // TODO(danvk): figure out an appropriate way to flag parse errors.
2706         vals = inFields[j].split("/");
2707         if (vals.length != 2) {
2708           console.error('Expected fractional "num/den" values in CSV data ' +
2709                         "but found a value '" + inFields[j] + "' on line " +
2710                         (1 + i) + " ('" + line + "') which is not of this form.");
2711           fields[j] = [0, 0];
2712         } else {
2713           fields[j] = [utils.parseFloat_(vals[0], i, line),
2714                        utils.parseFloat_(vals[1], i, line)];
2715         }
2716       }
2717     } else if (this.getBooleanOption("errorBars")) {
2718       // If there are error bars, values are (value, stddev) pairs
2719       if (inFields.length % 2 != 1) {
2720         console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2721                       'but line ' + (1 + i) + ' has an odd number of values (' +
2722                       (inFields.length - 1) + "): '" + line + "'");
2723       }
2724       for (j = 1; j < inFields.length; j += 2) {
2725         fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line),
2726                                utils.parseFloat_(inFields[j + 1], i, line)];
2727       }
2728     } else if (this.getBooleanOption("customBars")) {
2729       // Bars are a low;center;high tuple
2730       for (j = 1; j < inFields.length; j++) {
2731         var val = inFields[j];
2732         if (/^ *$/.test(val)) {
2733           fields[j] = [null, null, null];
2734         } else {
2735           vals = val.split(";");
2736           if (vals.length == 3) {
2737             fields[j] = [ utils.parseFloat_(vals[0], i, line),
2738                           utils.parseFloat_(vals[1], i, line),
2739                           utils.parseFloat_(vals[2], i, line) ];
2740           } else {
2741             console.warn('When using customBars, values must be either blank ' +
2742                          'or "low;center;high" tuples (got "' + val +
2743                          '" on line ' + (1+i));
2744           }
2745         }
2746       }
2747     } else {
2748       // Values are just numbers
2749       for (j = 1; j < inFields.length; j++) {
2750         fields[j] = utils.parseFloat_(inFields[j], i, line);
2751       }
2752     }
2753     if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2754       outOfOrder = true;
2755     }
2756 
2757     if (fields.length != expectedCols) {
2758       console.error("Number of columns in line " + i + " (" + fields.length +
2759                     ") does not agree with number of labels (" + expectedCols +
2760                     ") " + line);
2761     }
2762 
2763     // If the user specified the 'labels' option and none of the cells of the
2764     // first row parsed correctly, then they probably double-specified the
2765     // labels. We go with the values set in the option, discard this row and
2766     // log a warning to the JS console.
2767     if (i === 0 && this.attr_('labels')) {
2768       var all_null = true;
2769       for (j = 0; all_null && j < fields.length; j++) {
2770         if (fields[j]) all_null = false;
2771       }
2772       if (all_null) {
2773         console.warn("The dygraphs 'labels' option is set, but the first row " +
2774                      "of CSV data ('" + line + "') appears to also contain " +
2775                      "labels. Will drop the CSV labels and use the option " +
2776                      "labels.");
2777         continue;
2778       }
2779     }
2780     ret.push(fields);
2781   }
2782 
2783   if (outOfOrder) {
2784     console.warn("CSV is out of order; order it correctly to speed loading.");
2785     ret.sort(function(a,b) { return a[0] - b[0]; });
2786   }
2787 
2788   return ret;
2789 };
2790 
2791 // In native format, all values must be dates or numbers.
2792 // This check isn't perfect but will catch most mistaken uses of strings.
2793 function validateNativeFormat(data) {
2794   const firstRow = data[0];
2795   const firstX = firstRow[0];
2796   if (typeof firstX !== 'number' && !utils.isDateLike(firstX)) {
2797     throw new Error(`Expected number or date but got ${typeof firstX}: ${firstX}.`);
2798   }
2799   for (let i = 1; i < firstRow.length; i++) {
2800     const val = firstRow[i];
2801     if (val === null || val === undefined) continue;
2802     if (typeof val === 'number') continue;
2803     if (utils.isArrayLike(val)) continue;  // e.g. error bars or custom bars.
2804     throw new Error(`Expected number or array but got ${typeof val}: ${val}.`);
2805   }
2806 }
2807 
2808 /**
2809  * The user has provided their data as a pre-packaged JS array. If the x values
2810  * are numeric, this is the same as dygraphs' internal format. If the x values
2811  * are dates, we need to convert them from Date objects to ms since epoch.
2812  * @param {!Array} data
2813  * @return {Object} data with numeric x values.
2814  * @private
2815  */
2816 Dygraph.prototype.parseArray_ = function(data) {
2817   // Peek at the first x value to see if it's numeric.
2818   if (data.length === 0) {
2819     console.error("Can't plot empty data set");
2820     return null;
2821   }
2822   if (data[0].length === 0) {
2823     console.error("Data set cannot contain an empty row");
2824     return null;
2825   }
2826 
2827   validateNativeFormat(data);
2828 
2829   var i;
2830   if (this.attr_("labels") === null) {
2831     console.warn("Using default labels. Set labels explicitly via 'labels' " +
2832                  "in the options parameter");
2833     this.attrs_.labels = [ "X" ];
2834     for (i = 1; i < data[0].length; i++) {
2835       this.attrs_.labels.push("Y" + i); // Not user_attrs_.
2836     }
2837     this.attributes_.reparseSeries();
2838   } else {
2839     var num_labels = this.attr_("labels");
2840     if (num_labels.length != data[0].length) {
2841       console.error("Mismatch between number of labels (" + num_labels + ")" +
2842                     " and number of columns in array (" + data[0].length + ")");
2843       return null;
2844     }
2845   }
2846 
2847   if (utils.isDateLike(data[0][0])) {
2848     // Some intelligent defaults for a date x-axis.
2849     this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2850     this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2851     this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2852 
2853     // Assume they're all dates.
2854     var parsedData = utils.clone(data);
2855     for (i = 0; i < data.length; i++) {
2856       if (parsedData[i].length === 0) {
2857         console.error("Row " + (1 + i) + " of data is empty");
2858         return null;
2859       }
2860       if (parsedData[i][0] === null ||
2861           typeof(parsedData[i][0].getTime) != 'function' ||
2862           isNaN(parsedData[i][0].getTime())) {
2863         console.error("x value in row " + (1 + i) + " is not a Date");
2864         return null;
2865       }
2866       parsedData[i][0] = parsedData[i][0].getTime();
2867     }
2868     return parsedData;
2869   } else {
2870     // Some intelligent defaults for a numeric x-axis.
2871     /** @private (shut up, jsdoc!) */
2872     this.attrs_.axes.x.valueFormatter = function(x) { return x; };
2873     this.attrs_.axes.x.ticker = DygraphTickers.numericTicks;
2874     this.attrs_.axes.x.axisLabelFormatter = utils.numberAxisLabelFormatter;
2875     return data;
2876   }
2877 };
2878 
2879 /**
2880  * Parses a DataTable object from gviz.
2881  * The data is expected to have a first column that is either a date or a
2882  * number. All subsequent columns must be numbers. If there is a clear mismatch
2883  * between this.xValueParser_ and the type of the first column, it will be
2884  * fixed. Fills out rawData_.
2885  * @param {!google.visualization.DataTable} data See above.
2886  * @private
2887  */
2888 Dygraph.prototype.parseDataTable_ = function(data) {
2889   var shortTextForAnnotationNum = function(num) {
2890     // converts [0-9]+ [A-Z][a-z]*
2891     // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
2892     // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
2893     var shortText = String.fromCharCode(65 /* A */ + num % 26);
2894     num = Math.floor(num / 26);
2895     while ( num > 0 ) {
2896       shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
2897       num = Math.floor((num - 1) / 26);
2898     }
2899     return shortText;
2900   };
2901 
2902   var cols = data.getNumberOfColumns();
2903   var rows = data.getNumberOfRows();
2904 
2905   var indepType = data.getColumnType(0);
2906   if (indepType == 'date' || indepType == 'datetime') {
2907     this.attrs_.xValueParser = utils.dateParser;
2908     this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2909     this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2910     this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2911   } else if (indepType == 'number') {
2912     this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2913     this.attrs_.axes.x.valueFormatter = function(x) { return x; };
2914     this.attrs_.axes.x.ticker = DygraphTickers.numericTicks;
2915     this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
2916   } else {
2917     throw new Error(
2918           "only 'date', 'datetime' and 'number' types are supported " +
2919           "for column 1 of DataTable input (Got '" + indepType + "')");
2920   }
2921 
2922   // Array of the column indices which contain data (and not annotations).
2923   var colIdx = [];
2924   var annotationCols = {};  // data index -> [annotation cols]
2925   var hasAnnotations = false;
2926   var i, j;
2927   for (i = 1; i < cols; i++) {
2928     var type = data.getColumnType(i);
2929     if (type == 'number') {
2930       colIdx.push(i);
2931     } else if (type == 'string' && this.getBooleanOption('displayAnnotations')) {
2932       // This is OK -- it's an annotation column.
2933       var dataIdx = colIdx[colIdx.length - 1];
2934       if (!annotationCols.hasOwnProperty(dataIdx)) {
2935         annotationCols[dataIdx] = [i];
2936       } else {
2937         annotationCols[dataIdx].push(i);
2938       }
2939       hasAnnotations = true;
2940     } else {
2941       throw new Error(
2942           "Only 'number' is supported as a dependent type with Gviz." +
2943           " 'string' is only supported if displayAnnotations is true");
2944     }
2945   }
2946 
2947   // Read column labels
2948   // TODO(danvk): add support back for errorBars
2949   var labels = [data.getColumnLabel(0)];
2950   for (i = 0; i < colIdx.length; i++) {
2951     labels.push(data.getColumnLabel(colIdx[i]));
2952     if (this.getBooleanOption("errorBars")) i += 1;
2953   }
2954   this.attrs_.labels = labels;
2955   cols = labels.length;
2956 
2957   var ret = [];
2958   var outOfOrder = false;
2959   var annotations = [];
2960   for (i = 0; i < rows; i++) {
2961     var row = [];
2962     if (typeof(data.getValue(i, 0)) === 'undefined' ||
2963         data.getValue(i, 0) === null) {
2964       console.warn("Ignoring row " + i +
2965                    " of DataTable because of undefined or null first column.");
2966       continue;
2967     }
2968 
2969     if (indepType == 'date' || indepType == 'datetime') {
2970       row.push(data.getValue(i, 0).getTime());
2971     } else {
2972       row.push(data.getValue(i, 0));
2973     }
2974     if (!this.getBooleanOption("errorBars")) {
2975       for (j = 0; j < colIdx.length; j++) {
2976         var col = colIdx[j];
2977         row.push(data.getValue(i, col));
2978         if (hasAnnotations &&
2979             annotationCols.hasOwnProperty(col) &&
2980             data.getValue(i, annotationCols[col][0]) !== null) {
2981           var ann = {};
2982           ann.series = data.getColumnLabel(col);
2983           ann.xval = row[0];
2984           ann.shortText = shortTextForAnnotationNum(annotations.length);
2985           ann.text = '';
2986           for (var k = 0; k < annotationCols[col].length; k++) {
2987             if (k) ann.text += "\n";
2988             ann.text += data.getValue(i, annotationCols[col][k]);
2989           }
2990           annotations.push(ann);
2991         }
2992       }
2993 
2994       // Strip out infinities, which give dygraphs problems later on.
2995       for (j = 0; j < row.length; j++) {
2996         if (!isFinite(row[j])) row[j] = null;
2997       }
2998     } else {
2999       for (j = 0; j < cols - 1; j++) {
3000         row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
3001       }
3002     }
3003     if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
3004       outOfOrder = true;
3005     }
3006     ret.push(row);
3007   }
3008 
3009   if (outOfOrder) {
3010     console.warn("DataTable is out of order; order it correctly to speed loading.");
3011     ret.sort(function(a,b) { return a[0] - b[0]; });
3012   }
3013   this.rawData_ = ret;
3014 
3015   if (annotations.length > 0) {
3016     this.setAnnotations(annotations, true);
3017   }
3018   this.attributes_.reparseSeries();
3019 };
3020 
3021 /**
3022  * Signals to plugins that the chart data has updated.
3023  * This happens after the data has updated but before the chart has redrawn.
3024  * @private
3025  */
3026 Dygraph.prototype.cascadeDataDidUpdateEvent_ = function() {
3027   // TODO(danvk): there are some issues checking xAxisRange() and using
3028   // toDomCoords from handlers of this event. The visible range should be set
3029   // when the chart is drawn, not derived from the data.
3030   this.cascadeEvents_('dataDidUpdate', {});
3031 };
3032 
3033 /**
3034  * Get the CSV data. If it's in a function, call that function. If it's in a
3035  * file, do an XMLHttpRequest to get it.
3036  * @private
3037  */
3038 Dygraph.prototype.start_ = function() {
3039   var data = this.file_;
3040 
3041   // Functions can return references of all other types.
3042   if (typeof data == 'function') {
3043     data = data();
3044   }
3045 
3046   if (utils.isArrayLike(data)) {
3047     this.rawData_ = this.parseArray_(data);
3048     this.cascadeDataDidUpdateEvent_();
3049     this.predraw_();
3050   } else if (typeof data == 'object' &&
3051              typeof data.getColumnRange == 'function') {
3052     // must be a DataTable from gviz.
3053     this.parseDataTable_(data);
3054     this.cascadeDataDidUpdateEvent_();
3055     this.predraw_();
3056   } else if (typeof data == 'string') {
3057     // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3058     var line_delimiter = utils.detectLineDelimiter(data);
3059     if (line_delimiter) {
3060       this.loadedEvent_(data);
3061     } else {
3062       // REMOVE_FOR_IE
3063       var req;
3064       if (window.XMLHttpRequest) {
3065         // Firefox, Opera, IE7, and other browsers will use the native object
3066         req = new XMLHttpRequest();
3067       } else {
3068         // IE 5 and 6 will use the ActiveX control
3069         req = new ActiveXObject("Microsoft.XMLHTTP");
3070       }
3071 
3072       var caller = this;
3073       req.onreadystatechange = function () {
3074         if (req.readyState == 4) {
3075           if (req.status === 200 ||  // Normal http
3076               req.status === 0) {    // Chrome w/ --allow-file-access-from-files
3077             caller.loadedEvent_(req.responseText);
3078           }
3079         }
3080       };
3081 
3082       req.open("GET", data, true);
3083       req.send(null);
3084     }
3085   } else {
3086     console.error("Unknown data format: " + (typeof data));
3087   }
3088 };
3089 
3090 /**
3091  * Changes various properties of the graph. These can include:
3092  * <ul>
3093  * <li>file: changes the source data for the graph</li>
3094  * <li>errorBars: changes whether the data contains stddev</li>
3095  * </ul>
3096  *
3097  * There's a huge variety of options that can be passed to this method. For a
3098  * full list, see http://dygraphs.com/options.html.
3099  *
3100  * @param {Object} input_attrs The new properties and values
3101  * @param {boolean} block_redraw Usually the chart is redrawn after every
3102  *     call to updateOptions(). If you know better, you can pass true to
3103  *     explicitly block the redraw. This can be useful for chaining
3104  *     updateOptions() calls, avoiding the occasional infinite loop and
3105  *     preventing redraws when it's not necessary (e.g. when updating a
3106  *     callback).
3107  */
3108 Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
3109   if (typeof(block_redraw) == 'undefined') block_redraw = false;
3110 
3111   // copyUserAttrs_ drops the "file" parameter as a convenience to us.
3112   var file = input_attrs.file;
3113   var attrs = Dygraph.copyUserAttrs_(input_attrs);
3114   var prevNumAxes = this.attributes_.numAxes();
3115 
3116   // TODO(danvk): this is a mess. Move these options into attr_.
3117   if ('rollPeriod' in attrs) {
3118     this.rollPeriod_ = attrs.rollPeriod;
3119   }
3120   if ('dateWindow' in attrs) {
3121     this.dateWindow_ = attrs.dateWindow;
3122   }
3123 
3124   // TODO(danvk): validate per-series options.
3125   // Supported:
3126   // strokeWidth
3127   // pointSize
3128   // drawPoints
3129   // highlightCircleSize
3130 
3131   // Check if this set options will require new points.
3132   var requiresNewPoints = utils.isPixelChangingOptionList(this.attr_("labels"), attrs);
3133 
3134   utils.updateDeep(this.user_attrs_, attrs);
3135 
3136   this.attributes_.reparseSeries();
3137 
3138   if (prevNumAxes < this.attributes_.numAxes()) this.plotter_.clear();
3139   if (file) {
3140     // This event indicates that the data is about to change, but hasn't yet.
3141     // TODO(danvk): support cancellation of the update via this event.
3142     this.cascadeEvents_('dataWillUpdate', {});
3143 
3144     this.file_ = file;
3145     if (!block_redraw) this.start_();
3146   } else {
3147     if (!block_redraw) {
3148       if (requiresNewPoints) {
3149         this.predraw_();
3150       } else {
3151         this.renderGraph_(false);
3152       }
3153     }
3154   }
3155 };
3156 
3157 /**
3158  * Make a copy of input attributes, removing file as a convenience.
3159  * @private
3160  */
3161 Dygraph.copyUserAttrs_ = function(attrs) {
3162   var my_attrs = {};
3163   for (var k in attrs) {
3164     if (!attrs.hasOwnProperty(k)) continue;
3165     if (k == 'file') continue;
3166     if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
3167   }
3168   return my_attrs;
3169 };
3170 
3171 /**
3172  * Resizes the dygraph. If no parameters are specified, resizes to fill the
3173  * containing div (which has presumably changed size since the dygraph was
3174  * instantiated. If the width/height are specified, the div will be resized.
3175  *
3176  * This is far more efficient than destroying and re-instantiating a
3177  * Dygraph, since it doesn't have to reparse the underlying data.
3178  *
3179  * @param {number} width Width (in pixels)
3180  * @param {number} height Height (in pixels)
3181  */
3182 Dygraph.prototype.resize = function(width, height) {
3183   if (this.resize_lock) {
3184     return;
3185   }
3186   this.resize_lock = true;
3187 
3188   if ((width === null) != (height === null)) {
3189     console.warn("Dygraph.resize() should be called with zero parameters or " +
3190                  "two non-NULL parameters. Pretending it was zero.");
3191     width = height = null;
3192   }
3193 
3194   var old_width = this.width_;
3195   var old_height = this.height_;
3196 
3197   if (width) {
3198     this.maindiv_.style.width = width + "px";
3199     this.maindiv_.style.height = height + "px";
3200     this.width_ = width;
3201     this.height_ = height;
3202   } else {
3203     this.width_ = this.maindiv_.clientWidth;
3204     this.height_ = this.maindiv_.clientHeight;
3205   }
3206 
3207   if (old_width != this.width_ || old_height != this.height_) {
3208     // Resizing a canvas erases it, even when the size doesn't change, so
3209     // any resize needs to be followed by a redraw.
3210     this.resizeElements_();
3211     this.predraw_();
3212   }
3213 
3214   this.resize_lock = false;
3215 };
3216 
3217 /**
3218  * Adjusts the number of points in the rolling average. Updates the graph to
3219  * reflect the new averaging period.
3220  * @param {number} length Number of points over which to average the data.
3221  */
3222 Dygraph.prototype.adjustRoll = function(length) {
3223   this.rollPeriod_ = length;
3224   this.predraw_();
3225 };
3226 
3227 /**
3228  * Returns a boolean array of visibility statuses.
3229  */
3230 Dygraph.prototype.visibility = function() {
3231   // Do lazy-initialization, so that this happens after we know the number of
3232   // data series.
3233   if (!this.getOption("visibility")) {
3234     this.attrs_.visibility = [];
3235   }
3236   // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
3237   while (this.getOption("visibility").length < this.numColumns() - 1) {
3238     this.attrs_.visibility.push(true);
3239   }
3240   return this.getOption("visibility");
3241 };
3242 
3243 /**
3244  * Changes the visibility of one or more series.
3245  *
3246  * @param {number|number[]|object} num the series index or an array of series indices
3247  *                                     or a boolean array of visibility states by index
3248  *                                     or an object mapping series numbers, as keys, to
3249  *                                     visibility state (boolean values)
3250  * @param {boolean} value the visibility state expressed as a boolean
3251  */
3252 Dygraph.prototype.setVisibility = function(num, value) {
3253   var x = this.visibility();
3254   var numIsObject = false;
3255 
3256   if (!Array.isArray(num)) {
3257     if (num !== null && typeof num === 'object') {
3258       numIsObject = true;
3259     } else {
3260       num = [num];
3261     }
3262   }
3263 
3264   if (numIsObject) {
3265     for (var i in num) {
3266       if (num.hasOwnProperty(i)) {
3267         if (i < 0 || i >= x.length) {
3268           console.warn("Invalid series number in setVisibility: " + i);
3269         } else {
3270           x[i] = num[i];
3271         }
3272       }
3273     }
3274   } else {
3275     for (var i = 0; i < num.length; i++) {
3276       if (typeof num[i] === 'boolean') {
3277         if (i >= x.length) {
3278           console.warn("Invalid series number in setVisibility: " + i);
3279         } else {
3280           x[i] = num[i];
3281         }
3282       } else {
3283         if (num[i] < 0 || num[i] >= x.length) {
3284           console.warn("Invalid series number in setVisibility: " + num[i]);
3285         } else {
3286           x[num[i]] = value;
3287         }
3288       }
3289     }
3290   }
3291 
3292   this.predraw_();
3293 };
3294 
3295 /**
3296  * How large of an area will the dygraph render itself in?
3297  * This is used for testing.
3298  * @return A {width: w, height: h} object.
3299  * @private
3300  */
3301 Dygraph.prototype.size = function() {
3302   return { width: this.width_, height: this.height_ };
3303 };
3304 
3305 /**
3306  * Update the list of annotations and redraw the chart.
3307  * See dygraphs.com/annotations.html for more info on how to use annotations.
3308  * @param ann {Array} An array of annotation objects.
3309  * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3310  */
3311 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3312   // Only add the annotation CSS rule once we know it will be used.
3313   this.annotations_ = ann;
3314   if (!this.layout_) {
3315     console.warn("Tried to setAnnotations before dygraph was ready. " +
3316                  "Try setting them in a ready() block. See " +
3317                  "dygraphs.com/tests/annotation.html");
3318     return;
3319   }
3320 
3321   this.layout_.setAnnotations(this.annotations_);
3322   if (!suppressDraw) {
3323     this.predraw_();
3324   }
3325 };
3326 
3327 /**
3328  * Return the list of annotations.
3329  */
3330 Dygraph.prototype.annotations = function() {
3331   return this.annotations_;
3332 };
3333 
3334 /**
3335  * Get the list of label names for this graph. The first column is the
3336  * x-axis, so the data series names start at index 1.
3337  *
3338  * Returns null when labels have not yet been defined.
3339  */
3340 Dygraph.prototype.getLabels = function() {
3341   var labels = this.attr_("labels");
3342   return labels ? labels.slice() : null;
3343 };
3344 
3345 /**
3346  * Get the index of a series (column) given its name. The first column is the
3347  * x-axis, so the data series start with index 1.
3348  */
3349 Dygraph.prototype.indexFromSetName = function(name) {
3350   return this.setIndexByName_[name];
3351 };
3352 
3353 /**
3354  * Find the row number corresponding to the given x-value.
3355  * Returns null if there is no such x-value in the data.
3356  * If there are multiple rows with the same x-value, this will return the
3357  * first one.
3358  * @param {number} xVal The x-value to look for (e.g. millis since epoch).
3359  * @return {?number} The row number, which you can pass to getValue(), or null.
3360  */
3361 Dygraph.prototype.getRowForX = function(xVal) {
3362   var low = 0,
3363       high = this.numRows() - 1;
3364 
3365   while (low <= high) {
3366     var idx = (high + low) >> 1;
3367     var x = this.getValue(idx, 0);
3368     if (x < xVal) {
3369       low = idx + 1;
3370     } else if (x > xVal) {
3371       high = idx - 1;
3372     } else if (low != idx) {  // equal, but there may be an earlier match.
3373       high = idx;
3374     } else {
3375       return idx;
3376     }
3377   }
3378 
3379   return null;
3380 };
3381 
3382 /**
3383  * Trigger a callback when the dygraph has drawn itself and is ready to be
3384  * manipulated. This is primarily useful when dygraphs has to do an XHR for the
3385  * data (i.e. a URL is passed as the data source) and the chart is drawn
3386  * asynchronously. If the chart has already drawn, the callback will fire
3387  * immediately.
3388  *
3389  * This is a good place to call setAnnotation().
3390  *
3391  * @param {function(!Dygraph)} callback The callback to trigger when the chart
3392  *     is ready.
3393  */
3394 Dygraph.prototype.ready = function(callback) {
3395   if (this.is_initial_draw_) {
3396     this.readyFns_.push(callback);
3397   } else {
3398     callback.call(this, this);
3399   }
3400 };
3401 
3402 /**
3403  * Add an event handler. This event handler is kept until the graph is
3404  * destroyed with a call to graph.destroy().
3405  *
3406  * @param {!Node} elem The element to add the event to.
3407  * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
3408  * @param {function(Event):(boolean|undefined)} fn The function to call
3409  *     on the event. The function takes one parameter: the event object.
3410  * @private
3411  */
3412 Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) {
3413   utils.addEvent(elem, type, fn);
3414   this.registeredEvents_.push({elem, type, fn});
3415 };
3416 
3417 Dygraph.prototype.removeTrackedEvents_ = function() {
3418   if (this.registeredEvents_) {
3419     for (var idx = 0; idx < this.registeredEvents_.length; idx++) {
3420       var reg = this.registeredEvents_[idx];
3421       utils.removeEvent(reg.elem, reg.type, reg.fn);
3422     }
3423   }
3424 
3425   this.registeredEvents_ = [];
3426 };
3427 
3428 // Installed plugins, in order of precedence (most-general to most-specific).
3429 Dygraph.PLUGINS = [
3430   LegendPlugin,
3431   AxesPlugin,
3432   RangeSelectorPlugin, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
3433   ChartLabelsPlugin,
3434   AnnotationsPlugin,
3435   GridPlugin
3436 ];
3437 
3438 // There are many symbols which have historically been available through the
3439 // Dygraph class. These are exported here for backwards compatibility.
3440 Dygraph.GVizChart = GVizChart;
3441 Dygraph.DASHED_LINE = utils.DASHED_LINE;
3442 Dygraph.DOT_DASH_LINE = utils.DOT_DASH_LINE;
3443 Dygraph.dateAxisLabelFormatter = utils.dateAxisLabelFormatter;
3444 Dygraph.toRGB_ = utils.toRGB_;
3445 Dygraph.findPos = utils.findPos;
3446 Dygraph.pageX = utils.pageX;
3447 Dygraph.pageY = utils.pageY;
3448 Dygraph.dateString_ = utils.dateString_;
3449 Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel;
3450 Dygraph.nonInteractiveModel = Dygraph.nonInteractiveModel_ = DygraphInteraction.nonInteractiveModel_;
3451 Dygraph.Circles = utils.Circles;
3452 
3453 Dygraph.Plugins = {
3454   Legend: LegendPlugin,
3455   Axes: AxesPlugin,
3456   Annotations: AnnotationsPlugin,
3457   ChartLabels: ChartLabelsPlugin,
3458   Grid: GridPlugin,
3459   RangeSelector: RangeSelectorPlugin
3460 };
3461 
3462 Dygraph.DataHandlers = {
3463   DefaultHandler,
3464   BarsHandler,
3465   CustomBarsHandler,
3466   DefaultFractionHandler,
3467   ErrorBarsHandler,
3468   FractionsBarsHandler
3469 };
3470 
3471 Dygraph.startPan = DygraphInteraction.startPan;
3472 Dygraph.startZoom = DygraphInteraction.startZoom;
3473 Dygraph.movePan = DygraphInteraction.movePan;
3474 Dygraph.moveZoom = DygraphInteraction.moveZoom;
3475 Dygraph.endPan = DygraphInteraction.endPan;
3476 Dygraph.endZoom = DygraphInteraction.endZoom;
3477 
3478 Dygraph.numericLinearTicks = DygraphTickers.numericLinearTicks;
3479 Dygraph.numericTicks = DygraphTickers.numericTicks;
3480 Dygraph.dateTicker = DygraphTickers.dateTicker;
3481 Dygraph.Granularity = DygraphTickers.Granularity;
3482 Dygraph.getDateAxis = DygraphTickers.getDateAxis;
3483 Dygraph.floatFormat = utils.floatFormat;
3484 
3485 export default Dygraph;
3486