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