UNPKG

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