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