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