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