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