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