8e6d6e04cb3e0d645a69b3ec6148327664705a41
[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 if (!animatedZooms) {
1354 this.dateWindow_ = null;
1355 this.axes_.forEach(axis => {
1356 if (axis.valueRange) delete axis.valueRange;
1357 });
1358
1359 this.drawGraph_();
1360 if (zoomCallback) {
1361 zoomCallback.call(this, minDate, maxDate, this.yAxisRanges());
1362 }
1363 return;
1364 }
1365
1366 var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null;
1367 if (dirtyX) {
1368 oldWindow = this.xAxisRange();
1369 newWindow = [minDate, maxDate];
1370 }
1371
1372 if (dirtyY) {
1373 oldValueRanges = this.yAxisRanges();
1374 newValueRanges = this.yAxisExtremes();
1375 }
1376
1377 this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges,
1378 () => {
1379 this.dateWindow_ = null;
1380 this.axes_.forEach(axis => {
1381 if (axis.valueRange) delete axis.valueRange;
1382 });
1383 if (zoomCallback) {
1384 zoomCallback.call(this, minDate, maxDate, this.yAxisRanges());
1385 }
1386 });
1387 };
1388
1389 /**
1390 * Combined animation logic for all zoom functions.
1391 * either the x parameters or y parameters may be null.
1392 * @private
1393 */
1394 Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) {
1395 var steps = this.getBooleanOption("animatedZooms") ?
1396 Dygraph.ANIMATION_STEPS : 1;
1397
1398 var windows = [];
1399 var valueRanges = [];
1400 var step, frac;
1401
1402 if (oldXRange !== null && newXRange !== null) {
1403 for (step = 1; step <= steps; step++) {
1404 frac = Dygraph.zoomAnimationFunction(step, steps);
1405 windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0],
1406 oldXRange[1]*(1-frac) + frac*newXRange[1]];
1407 }
1408 }
1409
1410 if (oldYRanges !== null && newYRanges !== null) {
1411 for (step = 1; step <= steps; step++) {
1412 frac = Dygraph.zoomAnimationFunction(step, steps);
1413 var thisRange = [];
1414 for (var j = 0; j < this.axes_.length; j++) {
1415 thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0],
1416 oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]);
1417 }
1418 valueRanges[step-1] = thisRange;
1419 }
1420 }
1421
1422 utils.repeatAndCleanup(step => {
1423 if (valueRanges.length) {
1424 for (var i = 0; i < this.axes_.length; i++) {
1425 var w = valueRanges[step][i];
1426 this.axes_[i].valueRange = [w[0], w[1]];
1427 }
1428 }
1429 if (windows.length) {
1430 this.dateWindow_ = windows[step];
1431 }
1432 this.drawGraph_();
1433 }, steps, Dygraph.ANIMATION_DURATION / steps, callback);
1434 };
1435
1436 /**
1437 * Get the current graph's area object.
1438 *
1439 * Returns: {x, y, w, h}
1440 */
1441 Dygraph.prototype.getArea = function() {
1442 return this.plotter_.area;
1443 };
1444
1445 /**
1446 * Convert a mouse event to DOM coordinates relative to the graph origin.
1447 *
1448 * Returns a two-element array: [X, Y].
1449 */
1450 Dygraph.prototype.eventToDomCoords = function(event) {
1451 if (event.offsetX && event.offsetY) {
1452 return [ event.offsetX, event.offsetY ];
1453 } else {
1454 var eventElementPos = utils.findPos(this.mouseEventElement_);
1455 var canvasx = utils.pageX(event) - eventElementPos.x;
1456 var canvasy = utils.pageY(event) - eventElementPos.y;
1457 return [canvasx, canvasy];
1458 }
1459 };
1460
1461 /**
1462 * Given a canvas X coordinate, find the closest row.
1463 * @param {number} domX graph-relative DOM X coordinate
1464 * Returns {number} row number.
1465 * @private
1466 */
1467 Dygraph.prototype.findClosestRow = function(domX) {
1468 var minDistX = Infinity;
1469 var closestRow = -1;
1470 var sets = this.layout_.points;
1471 for (var i = 0; i < sets.length; i++) {
1472 var points = sets[i];
1473 var len = points.length;
1474 for (var j = 0; j < len; j++) {
1475 var point = points[j];
1476 if (!utils.isValidPoint(point, true)) continue;
1477 var dist = Math.abs(point.canvasx - domX);
1478 if (dist < minDistX) {
1479 minDistX = dist;
1480 closestRow = point.idx;
1481 }
1482 }
1483 }
1484
1485 return closestRow;
1486 };
1487
1488 /**
1489 * Given canvas X,Y coordinates, find the closest point.
1490 *
1491 * This finds the individual data point across all visible series
1492 * that's closest to the supplied DOM coordinates using the standard
1493 * Euclidean X,Y distance.
1494 *
1495 * @param {number} domX graph-relative DOM X coordinate
1496 * @param {number} domY graph-relative DOM Y coordinate
1497 * Returns: {row, seriesName, point}
1498 * @private
1499 */
1500 Dygraph.prototype.findClosestPoint = function(domX, domY) {
1501 var minDist = Infinity;
1502 var dist, dx, dy, point, closestPoint, closestSeries, closestRow;
1503 for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) {
1504 var points = this.layout_.points[setIdx];
1505 for (var i = 0; i < points.length; ++i) {
1506 point = points[i];
1507 if (!utils.isValidPoint(point)) continue;
1508 dx = point.canvasx - domX;
1509 dy = point.canvasy - domY;
1510 dist = dx * dx + dy * dy;
1511 if (dist < minDist) {
1512 minDist = dist;
1513 closestPoint = point;
1514 closestSeries = setIdx;
1515 closestRow = point.idx;
1516 }
1517 }
1518 }
1519 var name = this.layout_.setNames[closestSeries];
1520 return {
1521 row: closestRow,
1522 seriesName: name,
1523 point: closestPoint
1524 };
1525 };
1526
1527 /**
1528 * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1529 *
1530 * This first finds the X data point closest to the supplied DOM X coordinate,
1531 * then finds the series which puts the Y coordinate on top of its filled area,
1532 * using linear interpolation between adjacent point pairs.
1533 *
1534 * @param {number} domX graph-relative DOM X coordinate
1535 * @param {number} domY graph-relative DOM Y coordinate
1536 * Returns: {row, seriesName, point}
1537 * @private
1538 */
1539 Dygraph.prototype.findStackedPoint = function(domX, domY) {
1540 var row = this.findClosestRow(domX);
1541 var closestPoint, closestSeries;
1542 for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
1543 var boundary = this.getLeftBoundary_(setIdx);
1544 var rowIdx = row - boundary;
1545 var points = this.layout_.points[setIdx];
1546 if (rowIdx >= points.length) continue;
1547 var p1 = points[rowIdx];
1548 if (!utils.isValidPoint(p1)) continue;
1549 var py = p1.canvasy;
1550 if (domX > p1.canvasx && rowIdx + 1 < points.length) {
1551 // interpolate series Y value using next point
1552 var p2 = points[rowIdx + 1];
1553 if (utils.isValidPoint(p2)) {
1554 var dx = p2.canvasx - p1.canvasx;
1555 if (dx > 0) {
1556 var r = (domX - p1.canvasx) / dx;
1557 py += r * (p2.canvasy - p1.canvasy);
1558 }
1559 }
1560 } else if (domX < p1.canvasx && rowIdx > 0) {
1561 // interpolate series Y value using previous point
1562 var p0 = points[rowIdx - 1];
1563 if (utils.isValidPoint(p0)) {
1564 var dx = p1.canvasx - p0.canvasx;
1565 if (dx > 0) {
1566 var r = (p1.canvasx - domX) / dx;
1567 py += r * (p0.canvasy - p1.canvasy);
1568 }
1569 }
1570 }
1571 // Stop if the point (domX, py) is above this series' upper edge
1572 if (setIdx === 0 || py < domY) {
1573 closestPoint = p1;
1574 closestSeries = setIdx;
1575 }
1576 }
1577 var name = this.layout_.setNames[closestSeries];
1578 return {
1579 row: row,
1580 seriesName: name,
1581 point: closestPoint
1582 };
1583 };
1584
1585 /**
1586 * When the mouse moves in the canvas, display information about a nearby data
1587 * point and draw dots over those points in the data series. This function
1588 * takes care of cleanup of previously-drawn dots.
1589 * @param {Object} event The mousemove event from the browser.
1590 * @private
1591 */
1592 Dygraph.prototype.mouseMove_ = function(event) {
1593 // This prevents JS errors when mousing over the canvas before data loads.
1594 var points = this.layout_.points;
1595 if (points === undefined || points === null) return;
1596
1597 var canvasCoords = this.eventToDomCoords(event);
1598 var canvasx = canvasCoords[0];
1599 var canvasy = canvasCoords[1];
1600
1601 var highlightSeriesOpts = this.getOption("highlightSeriesOpts");
1602 var selectionChanged = false;
1603 if (highlightSeriesOpts && !this.isSeriesLocked()) {
1604 var closest;
1605 if (this.getBooleanOption("stackedGraph")) {
1606 closest = this.findStackedPoint(canvasx, canvasy);
1607 } else {
1608 closest = this.findClosestPoint(canvasx, canvasy);
1609 }
1610 selectionChanged = this.setSelection(closest.row, closest.seriesName);
1611 } else {
1612 var idx = this.findClosestRow(canvasx);
1613 selectionChanged = this.setSelection(idx);
1614 }
1615
1616 var callback = this.getFunctionOption("highlightCallback");
1617 if (callback && selectionChanged) {
1618 callback.call(this, event,
1619 this.lastx_,
1620 this.selPoints_,
1621 this.lastRow_,
1622 this.highlightSet_);
1623 }
1624 };
1625
1626 /**
1627 * Fetch left offset from the specified set index or if not passed, the
1628 * first defined boundaryIds record (see bug #236).
1629 * @private
1630 */
1631 Dygraph.prototype.getLeftBoundary_ = function(setIdx) {
1632 if (this.boundaryIds_[setIdx]) {
1633 return this.boundaryIds_[setIdx][0];
1634 } else {
1635 for (var i = 0; i < this.boundaryIds_.length; i++) {
1636 if (this.boundaryIds_[i] !== undefined) {
1637 return this.boundaryIds_[i][0];
1638 }
1639 }
1640 return 0;
1641 }
1642 };
1643
1644 Dygraph.prototype.animateSelection_ = function(direction) {
1645 var totalSteps = 10;
1646 var millis = 30;
1647 if (this.fadeLevel === undefined) this.fadeLevel = 0;
1648 if (this.animateId === undefined) this.animateId = 0;
1649 var start = this.fadeLevel;
1650 var steps = direction < 0 ? start : totalSteps - start;
1651 if (steps <= 0) {
1652 if (this.fadeLevel) {
1653 this.updateSelection_(1.0);
1654 }
1655 return;
1656 }
1657
1658 var thisId = ++this.animateId;
1659 var that = this;
1660 var cleanupIfClearing = function() {
1661 // if we haven't reached fadeLevel 0 in the max frame time,
1662 // ensure that the clear happens and just go to 0
1663 if (that.fadeLevel !== 0 && direction < 0) {
1664 that.fadeLevel = 0;
1665 that.clearSelection();
1666 }
1667 };
1668 utils.repeatAndCleanup(
1669 function(n) {
1670 // ignore simultaneous animations
1671 if (that.animateId != thisId) return;
1672
1673 that.fadeLevel += direction;
1674 if (that.fadeLevel === 0) {
1675 that.clearSelection();
1676 } else {
1677 that.updateSelection_(that.fadeLevel / totalSteps);
1678 }
1679 },
1680 steps, millis, cleanupIfClearing);
1681 };
1682
1683 /**
1684 * Draw dots over the selectied points in the data series. This function
1685 * takes care of cleanup of previously-drawn dots.
1686 * @private
1687 */
1688 Dygraph.prototype.updateSelection_ = function(opt_animFraction) {
1689 /*var defaultPrevented = */
1690 this.cascadeEvents_('select', {
1691 selectedRow: this.lastRow_ === -1 ? undefined : this.lastRow_,
1692 selectedX: this.lastx_ === -1 ? undefined : this.lastx_,
1693 selectedPoints: this.selPoints_
1694 });
1695 // TODO(danvk): use defaultPrevented here?
1696
1697 // Clear the previously drawn vertical, if there is one
1698 var i;
1699 var ctx = this.canvas_ctx_;
1700 if (this.getOption('highlightSeriesOpts')) {
1701 ctx.clearRect(0, 0, this.width_, this.height_);
1702 var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
1703 var backgroundColor = utils.toRGB_(this.getOption('highlightSeriesBackgroundColor'));
1704
1705 if (alpha) {
1706 // Activating background fade includes an animation effect for a gradual
1707 // fade. TODO(klausw): make this independently configurable if it causes
1708 // issues? Use a shared preference to control animations?
1709 var animateBackgroundFade = true;
1710 if (animateBackgroundFade) {
1711 if (opt_animFraction === undefined) {
1712 // start a new animation
1713 this.animateSelection_(1);
1714 return;
1715 }
1716 alpha *= opt_animFraction;
1717 }
1718 ctx.fillStyle = 'rgba(' + backgroundColor.r + ',' + backgroundColor.g + ',' + backgroundColor.b + ',' + alpha + ')';
1719 ctx.fillRect(0, 0, this.width_, this.height_);
1720 }
1721
1722 // Redraw only the highlighted series in the interactive canvas (not the
1723 // static plot canvas, which is where series are usually drawn).
1724 this.plotter_._renderLineChart(this.highlightSet_, ctx);
1725 } else if (this.previousVerticalX_ >= 0) {
1726 // Determine the maximum highlight circle size.
1727 var maxCircleSize = 0;
1728 var labels = this.attr_('labels');
1729 for (i = 1; i < labels.length; i++) {
1730 var r = this.getNumericOption('highlightCircleSize', labels[i]);
1731 if (r > maxCircleSize) maxCircleSize = r;
1732 }
1733 var px = this.previousVerticalX_;
1734 ctx.clearRect(px - maxCircleSize - 1, 0,
1735 2 * maxCircleSize + 2, this.height_);
1736 }
1737
1738 if (this.selPoints_.length > 0) {
1739 // Draw colored circles over the center of each selected point
1740 var canvasx = this.selPoints_[0].canvasx;
1741 ctx.save();
1742 for (i = 0; i < this.selPoints_.length; i++) {
1743 var pt = this.selPoints_[i];
1744 if (isNaN(pt.canvasy)) continue;
1745
1746 var circleSize = this.getNumericOption('highlightCircleSize', pt.name);
1747 var callback = this.getFunctionOption("drawHighlightPointCallback", pt.name);
1748 var color = this.plotter_.colors[pt.name];
1749 if (!callback) {
1750 callback = utils.Circles.DEFAULT;
1751 }
1752 ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name);
1753 ctx.strokeStyle = color;
1754 ctx.fillStyle = color;
1755 callback.call(this, this, pt.name, ctx, canvasx, pt.canvasy,
1756 color, circleSize, pt.idx);
1757 }
1758 ctx.restore();
1759
1760 this.previousVerticalX_ = canvasx;
1761 }
1762 };
1763
1764 /**
1765 * Manually set the selected points and display information about them in the
1766 * legend. The selection can be cleared using clearSelection() and queried
1767 * using getSelection().
1768 *
1769 * To set a selected series but not a selected point, call setSelection with
1770 * row=false and the selected series name.
1771 *
1772 * @param {number} row Row number that should be highlighted (i.e. appear with
1773 * hover dots on the chart).
1774 * @param {seriesName} optional series name to highlight that series with the
1775 * the highlightSeriesOpts setting.
1776 * @param { locked } optional If true, keep seriesName selected when mousing
1777 * over the graph, disabling closest-series highlighting. Call clearSelection()
1778 * to unlock it.
1779 */
1780 Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) {
1781 // Extract the points we've selected
1782 this.selPoints_ = [];
1783
1784 var changed = false;
1785 if (row !== false && row >= 0) {
1786 if (row != this.lastRow_) changed = true;
1787 this.lastRow_ = row;
1788 for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) {
1789 var points = this.layout_.points[setIdx];
1790 // Check if the point at the appropriate index is the point we're looking
1791 // for. If it is, just use it, otherwise search the array for a point
1792 // in the proper place.
1793 var setRow = row - this.getLeftBoundary_(setIdx);
1794 if (setRow >= 0 && setRow < points.length && points[setRow].idx == row) {
1795 var point = points[setRow];
1796 if (point.yval !== null) this.selPoints_.push(point);
1797 } else {
1798 for (var pointIdx = 0; pointIdx < points.length; ++pointIdx) {
1799 var point = points[pointIdx];
1800 if (point.idx == row) {
1801 if (point.yval !== null) {
1802 this.selPoints_.push(point);
1803 }
1804 break;
1805 }
1806 }
1807 }
1808 }
1809 } else {
1810 if (this.lastRow_ >= 0) changed = true;
1811 this.lastRow_ = -1;
1812 }
1813
1814 if (this.selPoints_.length) {
1815 this.lastx_ = this.selPoints_[0].xval;
1816 } else {
1817 this.lastx_ = -1;
1818 }
1819
1820 if (opt_seriesName !== undefined) {
1821 if (this.highlightSet_ !== opt_seriesName) changed = true;
1822 this.highlightSet_ = opt_seriesName;
1823 }
1824
1825 if (opt_locked !== undefined) {
1826 this.lockedSet_ = opt_locked;
1827 }
1828
1829 if (changed) {
1830 this.updateSelection_(undefined);
1831 }
1832 return changed;
1833 };
1834
1835 /**
1836 * The mouse has left the canvas. Clear out whatever artifacts remain
1837 * @param {Object} event the mouseout event from the browser.
1838 * @private
1839 */
1840 Dygraph.prototype.mouseOut_ = function(event) {
1841 if (this.getFunctionOption("unhighlightCallback")) {
1842 this.getFunctionOption("unhighlightCallback").call(this, event);
1843 }
1844
1845 if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_) {
1846 this.clearSelection();
1847 }
1848 };
1849
1850 /**
1851 * Clears the current selection (i.e. points that were highlighted by moving
1852 * the mouse over the chart).
1853 */
1854 Dygraph.prototype.clearSelection = function() {
1855 this.cascadeEvents_('deselect', {});
1856
1857 this.lockedSet_ = false;
1858 // Get rid of the overlay data
1859 if (this.fadeLevel) {
1860 this.animateSelection_(-1);
1861 return;
1862 }
1863 this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_);
1864 this.fadeLevel = 0;
1865 this.selPoints_ = [];
1866 this.lastx_ = -1;
1867 this.lastRow_ = -1;
1868 this.highlightSet_ = null;
1869 };
1870
1871 /**
1872 * Returns the number of the currently selected row. To get data for this row,
1873 * you can use the getValue method.
1874 * @return {number} row number, or -1 if nothing is selected
1875 */
1876 Dygraph.prototype.getSelection = function() {
1877 if (!this.selPoints_ || this.selPoints_.length < 1) {
1878 return -1;
1879 }
1880
1881 for (var setIdx = 0; setIdx < this.layout_.points.length; setIdx++) {
1882 var points = this.layout_.points[setIdx];
1883 for (var row = 0; row < points.length; row++) {
1884 if (points[row].x == this.selPoints_[0].x) {
1885 return points[row].idx;
1886 }
1887 }
1888 }
1889 return -1;
1890 };
1891
1892 /**
1893 * Returns the name of the currently-highlighted series.
1894 * Only available when the highlightSeriesOpts option is in use.
1895 */
1896 Dygraph.prototype.getHighlightSeries = function() {
1897 return this.highlightSet_;
1898 };
1899
1900 /**
1901 * Returns true if the currently-highlighted series was locked
1902 * via setSelection(..., seriesName, true).
1903 */
1904 Dygraph.prototype.isSeriesLocked = function() {
1905 return this.lockedSet_;
1906 };
1907
1908 /**
1909 * Fires when there's data available to be graphed.
1910 * @param {string} data Raw CSV data to be plotted
1911 * @private
1912 */
1913 Dygraph.prototype.loadedEvent_ = function(data) {
1914 this.rawData_ = this.parseCSV_(data);
1915 this.cascadeDataDidUpdateEvent_();
1916 this.predraw_();
1917 };
1918
1919 /**
1920 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1921 * @private
1922 */
1923 Dygraph.prototype.addXTicks_ = function() {
1924 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1925 var range;
1926 if (this.dateWindow_) {
1927 range = [this.dateWindow_[0], this.dateWindow_[1]];
1928 } else {
1929 range = this.xAxisExtremes();
1930 }
1931
1932 var xAxisOptionsView = this.optionsViewForAxis_('x');
1933 var xTicks = xAxisOptionsView('ticker')(
1934 range[0],
1935 range[1],
1936 this.plotter_.area.w, // TODO(danvk): should be area.width
1937 xAxisOptionsView,
1938 this);
1939 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
1940 // console.log(msg);
1941 this.layout_.setXTicks(xTicks);
1942 };
1943
1944 /**
1945 * Returns the correct handler class for the currently set options.
1946 * @private
1947 */
1948 Dygraph.prototype.getHandlerClass_ = function() {
1949 var handlerClass;
1950 if (this.attr_('dataHandler')) {
1951 handlerClass = this.attr_('dataHandler');
1952 } else if (this.fractions_) {
1953 if (this.getBooleanOption('errorBars')) {
1954 handlerClass = FractionsBarsHandler;
1955 } else {
1956 handlerClass = DefaultFractionHandler;
1957 }
1958 } else if (this.getBooleanOption('customBars')) {
1959 handlerClass = CustomBarsHandler;
1960 } else if (this.getBooleanOption('errorBars')) {
1961 handlerClass = ErrorBarsHandler;
1962 } else {
1963 handlerClass = DefaultHandler;
1964 }
1965 return handlerClass;
1966 };
1967
1968 /**
1969 * @private
1970 * This function is called once when the chart's data is changed or the options
1971 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1972 * idea is that values derived from the chart's data can be computed here,
1973 * rather than every time the chart is drawn. This includes things like the
1974 * number of axes, rolling averages, etc.
1975 */
1976 Dygraph.prototype.predraw_ = function() {
1977 var start = new Date();
1978
1979 // Create the correct dataHandler
1980 this.dataHandler_ = new (this.getHandlerClass_())();
1981
1982 this.layout_.computePlotArea();
1983
1984 // TODO(danvk): move more computations out of drawGraph_ and into here.
1985 this.computeYAxes_();
1986
1987 if (!this.is_initial_draw_) {
1988 this.canvas_ctx_.restore();
1989 this.hidden_ctx_.restore();
1990 }
1991
1992 this.canvas_ctx_.save();
1993 this.hidden_ctx_.save();
1994
1995 // Create a new plotter.
1996 this.plotter_ = new DygraphCanvasRenderer(this,
1997 this.hidden_,
1998 this.hidden_ctx_,
1999 this.layout_);
2000
2001 // The roller sits in the bottom left corner of the chart. We don't know where
2002 // this will be until the options are available, so it's positioned here.
2003 this.createRollInterface_();
2004
2005 this.cascadeEvents_('predraw');
2006
2007 // Convert the raw data (a 2D array) into the internal format and compute
2008 // rolling averages.
2009 this.rolledSeries_ = [null]; // x-axis is the first series and it's special
2010 for (var i = 1; i < this.numColumns(); i++) {
2011 // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too.
2012 var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_);
2013 if (this.rollPeriod_ > 1) {
2014 series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_);
2015 }
2016
2017 this.rolledSeries_.push(series);
2018 }
2019
2020 // If the data or options have changed, then we'd better redraw.
2021 this.drawGraph_();
2022
2023 // This is used to determine whether to do various animations.
2024 var end = new Date();
2025 this.drawingTimeMs_ = (end - start);
2026 };
2027
2028 /**
2029 * Point structure.
2030 *
2031 * xval_* and yval_* are the original unscaled data values,
2032 * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
2033 * yval_stacked is the cumulative Y value used for stacking graphs,
2034 * and bottom/top/minus/plus are used for error bar graphs.
2035 *
2036 * @typedef {{
2037 * idx: number,
2038 * name: string,
2039 * x: ?number,
2040 * xval: ?number,
2041 * y_bottom: ?number,
2042 * y: ?number,
2043 * y_stacked: ?number,
2044 * y_top: ?number,
2045 * yval_minus: ?number,
2046 * yval: ?number,
2047 * yval_plus: ?number,
2048 * yval_stacked
2049 * }}
2050 */
2051 Dygraph.PointType = undefined;
2052
2053 /**
2054 * Calculates point stacking for stackedGraph=true.
2055 *
2056 * For stacking purposes, interpolate or extend neighboring data across
2057 * NaN values based on stackedGraphNaNFill settings. This is for display
2058 * only, the underlying data value as shown in the legend remains NaN.
2059 *
2060 * @param {Array.<Dygraph.PointType>} points Point array for a single series.
2061 * Updates each Point's yval_stacked property.
2062 * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
2063 * values for the series seen so far. Index is the row number. Updated
2064 * based on the current series's values.
2065 * @param {Array.<number>} seriesExtremes Min and max values, updated
2066 * to reflect the stacked values.
2067 * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
2068 * 'none'.
2069 * @private
2070 */
2071 Dygraph.stackPoints_ = function(
2072 points, cumulativeYval, seriesExtremes, fillMethod) {
2073 var lastXval = null;
2074 var prevPoint = null;
2075 var nextPoint = null;
2076 var nextPointIdx = -1;
2077
2078 // Find the next stackable point starting from the given index.
2079 var updateNextPoint = function(idx) {
2080 // If we've previously found a non-NaN point and haven't gone past it yet,
2081 // just use that.
2082 if (nextPointIdx >= idx) return;
2083
2084 // We haven't found a non-NaN point yet or have moved past it,
2085 // look towards the right to find a non-NaN point.
2086 for (var j = idx; j < points.length; ++j) {
2087 // Clear out a previously-found point (if any) since it's no longer
2088 // valid, we shouldn't use it for interpolation anymore.
2089 nextPoint = null;
2090 if (!isNaN(points[j].yval) && points[j].yval !== null) {
2091 nextPointIdx = j;
2092 nextPoint = points[j];
2093 break;
2094 }
2095 }
2096 };
2097
2098 for (var i = 0; i < points.length; ++i) {
2099 var point = points[i];
2100 var xval = point.xval;
2101 if (cumulativeYval[xval] === undefined) {
2102 cumulativeYval[xval] = 0;
2103 }
2104
2105 var actualYval = point.yval;
2106 if (isNaN(actualYval) || actualYval === null) {
2107 if(fillMethod == 'none') {
2108 actualYval = 0;
2109 } else {
2110 // Interpolate/extend for stacking purposes if possible.
2111 updateNextPoint(i);
2112 if (prevPoint && nextPoint && fillMethod != 'none') {
2113 // Use linear interpolation between prevPoint and nextPoint.
2114 actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) *
2115 ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval));
2116 } else if (prevPoint && fillMethod == 'all') {
2117 actualYval = prevPoint.yval;
2118 } else if (nextPoint && fillMethod == 'all') {
2119 actualYval = nextPoint.yval;
2120 } else {
2121 actualYval = 0;
2122 }
2123 }
2124 } else {
2125 prevPoint = point;
2126 }
2127
2128 var stackedYval = cumulativeYval[xval];
2129 if (lastXval != xval) {
2130 // If an x-value is repeated, we ignore the duplicates.
2131 stackedYval += actualYval;
2132 cumulativeYval[xval] = stackedYval;
2133 }
2134 lastXval = xval;
2135
2136 point.yval_stacked = stackedYval;
2137
2138 if (stackedYval > seriesExtremes[1]) {
2139 seriesExtremes[1] = stackedYval;
2140 }
2141 if (stackedYval < seriesExtremes[0]) {
2142 seriesExtremes[0] = stackedYval;
2143 }
2144 }
2145 };
2146
2147
2148 /**
2149 * Loop over all fields and create datasets, calculating extreme y-values for
2150 * each series and extreme x-indices as we go.
2151 *
2152 * dateWindow is passed in as an explicit parameter so that we can compute
2153 * extreme values "speculatively", i.e. without actually setting state on the
2154 * dygraph.
2155 *
2156 * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
2157 * rolledSeries[seriesIndex][row] = raw point, where
2158 * seriesIndex is the column number starting with 1, and
2159 * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
2160 * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
2161 * @return {{
2162 * points: Array.<Array.<Dygraph.PointType>>,
2163 * seriesExtremes: Array.<Array.<number>>,
2164 * boundaryIds: Array.<number>}}
2165 * @private
2166 */
2167 Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) {
2168 var boundaryIds = [];
2169 var points = [];
2170 var cumulativeYval = []; // For stacked series.
2171 var extremes = {}; // series name -> [low, high]
2172 var seriesIdx, sampleIdx;
2173 var firstIdx, lastIdx;
2174 var axisIdx;
2175
2176 // Loop over the fields (series). Go from the last to the first,
2177 // because if they're stacked that's how we accumulate the values.
2178 var num_series = rolledSeries.length - 1;
2179 var series;
2180 for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) {
2181 if (!this.visibility()[seriesIdx - 1]) continue;
2182
2183 // Prune down to the desired range, if necessary (for zooming)
2184 // Because there can be lines going to points outside of the visible area,
2185 // we actually prune to visible points, plus one on either side.
2186 if (dateWindow) {
2187 series = rolledSeries[seriesIdx];
2188 var low = dateWindow[0];
2189 var high = dateWindow[1];
2190
2191 // TODO(danvk): do binary search instead of linear search.
2192 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2193 firstIdx = null;
2194 lastIdx = null;
2195 for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) {
2196 if (series[sampleIdx][0] >= low && firstIdx === null) {
2197 firstIdx = sampleIdx;
2198 }
2199 if (series[sampleIdx][0] <= high) {
2200 lastIdx = sampleIdx;
2201 }
2202 }
2203
2204 if (firstIdx === null) firstIdx = 0;
2205 var correctedFirstIdx = firstIdx;
2206 var isInvalidValue = true;
2207 while (isInvalidValue && correctedFirstIdx > 0) {
2208 correctedFirstIdx--;
2209 // check if the y value is null.
2210 isInvalidValue = series[correctedFirstIdx][1] === null;
2211 }
2212
2213 if (lastIdx === null) lastIdx = series.length - 1;
2214 var correctedLastIdx = lastIdx;
2215 isInvalidValue = true;
2216 while (isInvalidValue && correctedLastIdx < series.length - 1) {
2217 correctedLastIdx++;
2218 isInvalidValue = series[correctedLastIdx][1] === null;
2219 }
2220
2221 if (correctedFirstIdx!==firstIdx) {
2222 firstIdx = correctedFirstIdx;
2223 }
2224 if (correctedLastIdx !== lastIdx) {
2225 lastIdx = correctedLastIdx;
2226 }
2227
2228 boundaryIds[seriesIdx-1] = [firstIdx, lastIdx];
2229
2230 // .slice's end is exclusive, we want to include lastIdx.
2231 series = series.slice(firstIdx, lastIdx + 1);
2232 } else {
2233 series = rolledSeries[seriesIdx];
2234 boundaryIds[seriesIdx-1] = [0, series.length-1];
2235 }
2236
2237 var seriesName = this.attr_("labels")[seriesIdx];
2238 var seriesExtremes = this.dataHandler_.getExtremeYValues(series,
2239 dateWindow, this.getBooleanOption("stepPlot",seriesName));
2240
2241 var seriesPoints = this.dataHandler_.seriesToPoints(series,
2242 seriesName, boundaryIds[seriesIdx-1][0]);
2243
2244 if (this.getBooleanOption("stackedGraph")) {
2245 axisIdx = this.attributes_.axisForSeries(seriesName);
2246 if (cumulativeYval[axisIdx] === undefined) {
2247 cumulativeYval[axisIdx] = [];
2248 }
2249 Dygraph.stackPoints_(seriesPoints, cumulativeYval[axisIdx], seriesExtremes,
2250 this.getBooleanOption("stackedGraphNaNFill"));
2251 }
2252
2253 extremes[seriesName] = seriesExtremes;
2254 points[seriesIdx] = seriesPoints;
2255 }
2256
2257 return { points: points, extremes: extremes, boundaryIds: boundaryIds };
2258 };
2259
2260 /**
2261 * Update the graph with new data. This method is called when the viewing area
2262 * has changed. If the underlying data or options have changed, predraw_ will
2263 * be called before drawGraph_ is called.
2264 *
2265 * @private
2266 */
2267 Dygraph.prototype.drawGraph_ = function() {
2268 var start = new Date();
2269
2270 // This is used to set the second parameter to drawCallback, below.
2271 var is_initial_draw = this.is_initial_draw_;
2272 this.is_initial_draw_ = false;
2273
2274 this.layout_.removeAllDatasets();
2275 this.setColors_();
2276 this.attrs_.pointSize = 0.5 * this.getNumericOption('highlightCircleSize');
2277
2278 var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_);
2279 var points = packed.points;
2280 var extremes = packed.extremes;
2281 this.boundaryIds_ = packed.boundaryIds;
2282
2283 this.setIndexByName_ = {};
2284 var labels = this.attr_("labels");
2285 var dataIdx = 0;
2286 for (var i = 1; i < points.length; i++) {
2287 if (!this.visibility()[i - 1]) continue;
2288 this.layout_.addDataset(labels[i], points[i]);
2289 this.datasetIndex_[i] = dataIdx++;
2290 }
2291 for (var i = 0; i < labels.length; i++) {
2292 this.setIndexByName_[labels[i]] = i;
2293 }
2294
2295 this.computeYAxisRanges_(extremes);
2296 this.layout_.setYAxes(this.axes_);
2297
2298 this.addXTicks_();
2299
2300 // Tell PlotKit to use this new data and render itself
2301 this.layout_.evaluate();
2302 this.renderGraph_(is_initial_draw);
2303
2304 if (this.getStringOption("timingName")) {
2305 var end = new Date();
2306 console.log(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms");
2307 }
2308 };
2309
2310 /**
2311 * This does the work of drawing the chart. It assumes that the layout and axis
2312 * scales have already been set (e.g. by predraw_).
2313 *
2314 * @private
2315 */
2316 Dygraph.prototype.renderGraph_ = function(is_initial_draw) {
2317 this.cascadeEvents_('clearChart');
2318 this.plotter_.clear();
2319
2320 const underlayCallback = this.getFunctionOption('underlayCallback');
2321 if (underlayCallback) {
2322 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
2323 // users who expect a deprecated form of this callback.
2324 underlayCallback.call(this,
2325 this.hidden_ctx_, this.layout_.getPlotArea(), this, this);
2326 }
2327
2328 var e = {
2329 canvas: this.hidden_,
2330 drawingContext: this.hidden_ctx_
2331 };
2332 this.cascadeEvents_('willDrawChart', e);
2333 this.plotter_.render();
2334 this.cascadeEvents_('didDrawChart', e);
2335 this.lastRow_ = -1; // because plugins/legend.js clears the legend
2336
2337 // TODO(danvk): is this a performance bottleneck when panning?
2338 // The interaction canvas should already be empty in that situation.
2339 this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_);
2340
2341 const drawCallback = this.getFunctionOption("drawCallback");
2342 if (drawCallback !== null) {
2343 drawCallback.call(this, this, is_initial_draw);
2344 }
2345 if (is_initial_draw) {
2346 this.readyFired_ = true;
2347 while (this.readyFns_.length > 0) {
2348 var fn = this.readyFns_.pop();
2349 fn(this);
2350 }
2351 }
2352 };
2353
2354 /**
2355 * @private
2356 * Determine properties of the y-axes which are independent of the data
2357 * currently being displayed. This includes things like the number of axes and
2358 * the style of the axes. It does not include the range of each axis and its
2359 * tick marks.
2360 * This fills in this.axes_.
2361 * axes_ = [ { options } ]
2362 * indices are into the axes_ array.
2363 */
2364 Dygraph.prototype.computeYAxes_ = function() {
2365 var axis, index, opts, v;
2366
2367 // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
2368 // data computation as well as options storage.
2369 // Go through once and add all the axes.
2370 this.axes_ = [];
2371
2372 for (axis = 0; axis < this.attributes_.numAxes(); axis++) {
2373 // Add a new axis, making a copy of its per-axis options.
2374 opts = { g : this };
2375 utils.update(opts, this.attributes_.axisOptions(axis));
2376 this.axes_[axis] = opts;
2377 }
2378
2379 for (axis = 0; axis < this.axes_.length; axis++) {
2380 if (axis === 0) {
2381 opts = this.optionsViewForAxis_('y' + (axis ? '2' : ''));
2382 v = opts("valueRange");
2383 if (v) this.axes_[axis].valueRange = v;
2384 } else { // To keep old behavior
2385 var axes = this.user_attrs_.axes;
2386 if (axes && axes.y2) {
2387 v = axes.y2.valueRange;
2388 if (v) this.axes_[axis].valueRange = v;
2389 }
2390 }
2391 }
2392 };
2393
2394 /**
2395 * Returns the number of y-axes on the chart.
2396 * @return {number} the number of axes.
2397 */
2398 Dygraph.prototype.numAxes = function() {
2399 return this.attributes_.numAxes();
2400 };
2401
2402 /**
2403 * @private
2404 * Returns axis properties for the given series.
2405 * @param {string} setName The name of the series for which to get axis
2406 * properties, e.g. 'Y1'.
2407 * @return {Object} The axis properties.
2408 */
2409 Dygraph.prototype.axisPropertiesForSeries = function(series) {
2410 // TODO(danvk): handle errors.
2411 return this.axes_[this.attributes_.axisForSeries(series)];
2412 };
2413
2414 /**
2415 * @private
2416 * Determine the value range and tick marks for each axis.
2417 * @param {Object} extremes A mapping from seriesName -> [low, high]
2418 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2419 */
2420 Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2421 var isNullUndefinedOrNaN = function(num) {
2422 return isNaN(parseFloat(num));
2423 };
2424 var numAxes = this.attributes_.numAxes();
2425 var ypadCompat, span, series, ypad;
2426
2427 var p_axis;
2428
2429 // Compute extreme values, a span and tick marks for each axis.
2430 for (var i = 0; i < numAxes; i++) {
2431 var axis = this.axes_[i];
2432 var logscale = this.attributes_.getForAxis("logscale", i);
2433 var includeZero = this.attributes_.getForAxis("includeZero", i);
2434 var independentTicks = this.attributes_.getForAxis("independentTicks", i);
2435 series = this.attributes_.seriesForAxis(i);
2436
2437 // Add some padding. This supports two Y padding operation modes:
2438 //
2439 // - backwards compatible (yRangePad not set):
2440 // 10% padding for automatic Y ranges, but not for user-supplied
2441 // ranges, and move a close-to-zero edge to zero, since drawing at the edge
2442 // results in invisible lines. Unfortunately lines drawn at the edge of a
2443 // user-supplied range will still be invisible. If logscale is
2444 // set, add a variable amount of padding at the top but
2445 // none at the bottom.
2446 //
2447 // - new-style (yRangePad set by the user):
2448 // always add the specified Y padding.
2449 //
2450 ypadCompat = true;
2451 ypad = 0.1; // add 10%
2452 const yRangePad = this.getNumericOption('yRangePad');
2453 if (yRangePad !== null) {
2454 ypadCompat = false;
2455 // Convert pixel padding to ratio
2456 ypad = yRangePad / this.plotter_.area.h;
2457 }
2458
2459 if (series.length === 0) {
2460 // If no series are defined or visible then use a reasonable default
2461 axis.extremeRange = [0, 1];
2462 } else {
2463 // Calculate the extremes of extremes.
2464 var minY = Infinity; // extremes[series[0]][0];
2465 var maxY = -Infinity; // extremes[series[0]][1];
2466 var extremeMinY, extremeMaxY;
2467
2468 for (var j = 0; j < series.length; j++) {
2469 // this skips invisible series
2470 if (!extremes.hasOwnProperty(series[j])) continue;
2471
2472 // Only use valid extremes to stop null data series' from corrupting the scale.
2473 extremeMinY = extremes[series[j]][0];
2474 if (extremeMinY !== null) {
2475 minY = Math.min(extremeMinY, minY);
2476 }
2477 extremeMaxY = extremes[series[j]][1];
2478 if (extremeMaxY !== null) {
2479 maxY = Math.max(extremeMaxY, maxY);
2480 }
2481 }
2482
2483 // Include zero if requested by the user.
2484 if (includeZero && !logscale) {
2485 if (minY > 0) minY = 0;
2486 if (maxY < 0) maxY = 0;
2487 }
2488
2489 // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2490 if (minY == Infinity) minY = 0;
2491 if (maxY == -Infinity) maxY = 1;
2492
2493 span = maxY - minY;
2494 // special case: if we have no sense of scale, center on the sole value.
2495 if (span === 0) {
2496 if (maxY !== 0) {
2497 span = Math.abs(maxY);
2498 } else {
2499 // ... and if the sole value is zero, use range 0-1.
2500 maxY = 1;
2501 span = 1;
2502 }
2503 }
2504
2505 var maxAxisY = maxY, minAxisY = minY;
2506 if (ypadCompat) {
2507 if (logscale) {
2508 maxAxisY = maxY + ypad * span;
2509 minAxisY = minY;
2510 } else {
2511 maxAxisY = maxY + ypad * span;
2512 minAxisY = minY - ypad * span;
2513
2514 // Backwards-compatible behavior: Move the span to start or end at zero if it's
2515 // close to zero.
2516 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2517 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2518 }
2519 }
2520 axis.extremeRange = [minAxisY, maxAxisY];
2521 }
2522 if (axis.valueRange) {
2523 // This is a user-set value range for this axis.
2524 var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0];
2525 var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1];
2526 axis.computedValueRange = [y0, y1];
2527 } else {
2528 axis.computedValueRange = axis.extremeRange;
2529 }
2530 if (!ypadCompat) {
2531 // When using yRangePad, adjust the upper/lower bounds to add
2532 // padding unless the user has zoomed/panned the Y axis range.
2533 if (logscale) {
2534 y0 = axis.computedValueRange[0];
2535 y1 = axis.computedValueRange[1];
2536 var y0pct = ypad / (2 * ypad - 1);
2537 var y1pct = (ypad - 1) / (2 * ypad - 1);
2538 axis.computedValueRange[0] = utils.logRangeFraction(y0, y1, y0pct);
2539 axis.computedValueRange[1] = utils.logRangeFraction(y0, y1, y1pct);
2540 } else {
2541 y0 = axis.computedValueRange[0];
2542 y1 = axis.computedValueRange[1];
2543 span = y1 - y0;
2544 axis.computedValueRange[0] = y0 - span * ypad;
2545 axis.computedValueRange[1] = y1 + span * ypad;
2546 }
2547 }
2548
2549
2550 if (independentTicks) {
2551 axis.independentTicks = independentTicks;
2552 var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
2553 var ticker = opts('ticker');
2554 axis.ticks = ticker(axis.computedValueRange[0],
2555 axis.computedValueRange[1],
2556 this.plotter_.area.h,
2557 opts,
2558 this);
2559 // Define the first independent axis as primary axis.
2560 if (!p_axis) p_axis = axis;
2561 }
2562 }
2563 if (p_axis === undefined) {
2564 throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
2565 }
2566 // Add ticks. By default, all axes inherit the tick positions of the
2567 // primary axis. However, if an axis is specifically marked as having
2568 // independent ticks, then that is permissible as well.
2569 for (var i = 0; i < numAxes; i++) {
2570 var axis = this.axes_[i];
2571
2572 if (!axis.independentTicks) {
2573 var opts = this.optionsViewForAxis_('y' + (i ? '2' : ''));
2574 var ticker = opts('ticker');
2575 var p_ticks = p_axis.ticks;
2576 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
2577 var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
2578 var tick_values = [];
2579 for (var k = 0; k < p_ticks.length; k++) {
2580 var y_frac = (p_ticks[k].v - p_axis.computedValueRange[0]) / p_scale;
2581 var y_val = axis.computedValueRange[0] + y_frac * scale;
2582 tick_values.push(y_val);
2583 }
2584
2585 axis.ticks = ticker(axis.computedValueRange[0],
2586 axis.computedValueRange[1],
2587 this.plotter_.area.h,
2588 opts,
2589 this,
2590 tick_values);
2591 }
2592 }
2593 };
2594
2595 /**
2596 * Detects the type of the str (date or numeric) and sets the various
2597 * formatting attributes in this.attrs_ based on this type.
2598 * @param {string} str An x value.
2599 * @private
2600 */
2601 Dygraph.prototype.detectTypeFromString_ = function(str) {
2602 var isDate = false;
2603 var dashPos = str.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2
2604 if ((dashPos > 0 && (str[dashPos-1] != 'e' && str[dashPos-1] != 'E')) ||
2605 str.indexOf('/') >= 0 ||
2606 isNaN(parseFloat(str))) {
2607 isDate = true;
2608 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
2609 // TODO(danvk): remove support for this format.
2610 isDate = true;
2611 }
2612
2613 this.setXAxisOptions_(isDate);
2614 };
2615
2616 Dygraph.prototype.setXAxisOptions_ = function(isDate) {
2617 if (isDate) {
2618 this.attrs_.xValueParser = utils.dateParser;
2619 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2620 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2621 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2622 } else {
2623 /** @private (shut up, jsdoc!) */
2624 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2625 // TODO(danvk): use Dygraph.numberValueFormatter here?
2626 /** @private (shut up, jsdoc!) */
2627 this.attrs_.axes.x.valueFormatter = function(x) { return x; };
2628 this.attrs_.axes.x.ticker = DygraphTickers.numericTicks;
2629 this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
2630 }
2631 };
2632
2633 /**
2634 * @private
2635 * Parses a string in a special csv format. We expect a csv file where each
2636 * line is a date point, and the first field in each line is the date string.
2637 * We also expect that all remaining fields represent series.
2638 * if the errorBars attribute is set, then interpret the fields as:
2639 * date, series1, stddev1, series2, stddev2, ...
2640 * @param {[Object]} data See above.
2641 *
2642 * @return [Object] An array with one entry for each row. These entries
2643 * are an array of cells in that row. The first entry is the parsed x-value for
2644 * the row. The second, third, etc. are the y-values. These can take on one of
2645 * three forms, depending on the CSV and constructor parameters:
2646 * 1. numeric value
2647 * 2. [ value, stddev ]
2648 * 3. [ low value, center value, high value ]
2649 */
2650 Dygraph.prototype.parseCSV_ = function(data) {
2651 var ret = [];
2652 var line_delimiter = utils.detectLineDelimiter(data);
2653 var lines = data.split(line_delimiter || "\n");
2654 var vals, j;
2655
2656 // Use the default delimiter or fall back to a tab if that makes sense.
2657 var delim = this.getStringOption('delimiter');
2658 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
2659 delim = '\t';
2660 }
2661
2662 var start = 0;
2663 if (!('labels' in this.user_attrs_)) {
2664 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2665 start = 1;
2666 this.attrs_.labels = lines[0].split(delim); // NOTE: _not_ user_attrs_.
2667 this.attributes_.reparseSeries();
2668 }
2669 var line_no = 0;
2670
2671 var xParser;
2672 var defaultParserSet = false; // attempt to auto-detect x value type
2673 var expectedCols = this.attr_("labels").length;
2674 var outOfOrder = false;
2675 for (var i = start; i < lines.length; i++) {
2676 var line = lines[i];
2677 line_no = i;
2678 if (line.length === 0) continue; // skip blank lines
2679 if (line[0] == '#') continue; // skip comment lines
2680 var inFields = line.split(delim);
2681 if (inFields.length < 2) continue;
2682
2683 var fields = [];
2684 if (!defaultParserSet) {
2685 this.detectTypeFromString_(inFields[0]);
2686 xParser = this.getFunctionOption("xValueParser");
2687 defaultParserSet = true;
2688 }
2689 fields[0] = xParser(inFields[0], this);
2690
2691 // If fractions are expected, parse the numbers as "A/B"
2692 if (this.fractions_) {
2693 for (j = 1; j < inFields.length; j++) {
2694 // TODO(danvk): figure out an appropriate way to flag parse errors.
2695 vals = inFields[j].split("/");
2696 if (vals.length != 2) {
2697 console.error('Expected fractional "num/den" values in CSV data ' +
2698 "but found a value '" + inFields[j] + "' on line " +
2699 (1 + i) + " ('" + line + "') which is not of this form.");
2700 fields[j] = [0, 0];
2701 } else {
2702 fields[j] = [utils.parseFloat_(vals[0], i, line),
2703 utils.parseFloat_(vals[1], i, line)];
2704 }
2705 }
2706 } else if (this.getBooleanOption("errorBars")) {
2707 // If there are error bars, values are (value, stddev) pairs
2708 if (inFields.length % 2 != 1) {
2709 console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2710 'but line ' + (1 + i) + ' has an odd number of values (' +
2711 (inFields.length - 1) + "): '" + line + "'");
2712 }
2713 for (j = 1; j < inFields.length; j += 2) {
2714 fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line),
2715 utils.parseFloat_(inFields[j + 1], i, line)];
2716 }
2717 } else if (this.getBooleanOption("customBars")) {
2718 // Bars are a low;center;high tuple
2719 for (j = 1; j < inFields.length; j++) {
2720 var val = inFields[j];
2721 if (/^ *$/.test(val)) {
2722 fields[j] = [null, null, null];
2723 } else {
2724 vals = val.split(";");
2725 if (vals.length == 3) {
2726 fields[j] = [ utils.parseFloat_(vals[0], i, line),
2727 utils.parseFloat_(vals[1], i, line),
2728 utils.parseFloat_(vals[2], i, line) ];
2729 } else {
2730 console.warn('When using customBars, values must be either blank ' +
2731 'or "low;center;high" tuples (got "' + val +
2732 '" on line ' + (1+i));
2733 }
2734 }
2735 }
2736 } else {
2737 // Values are just numbers
2738 for (j = 1; j < inFields.length; j++) {
2739 fields[j] = utils.parseFloat_(inFields[j], i, line);
2740 }
2741 }
2742 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2743 outOfOrder = true;
2744 }
2745
2746 if (fields.length != expectedCols) {
2747 console.error("Number of columns in line " + i + " (" + fields.length +
2748 ") does not agree with number of labels (" + expectedCols +
2749 ") " + line);
2750 }
2751
2752 // If the user specified the 'labels' option and none of the cells of the
2753 // first row parsed correctly, then they probably double-specified the
2754 // labels. We go with the values set in the option, discard this row and
2755 // log a warning to the JS console.
2756 if (i === 0 && this.attr_('labels')) {
2757 var all_null = true;
2758 for (j = 0; all_null && j < fields.length; j++) {
2759 if (fields[j]) all_null = false;
2760 }
2761 if (all_null) {
2762 console.warn("The dygraphs 'labels' option is set, but the first row " +
2763 "of CSV data ('" + line + "') appears to also contain " +
2764 "labels. Will drop the CSV labels and use the option " +
2765 "labels.");
2766 continue;
2767 }
2768 }
2769 ret.push(fields);
2770 }
2771
2772 if (outOfOrder) {
2773 console.warn("CSV is out of order; order it correctly to speed loading.");
2774 ret.sort(function(a,b) { return a[0] - b[0]; });
2775 }
2776
2777 return ret;
2778 };
2779
2780 // In native format, all values must be dates or numbers.
2781 // This check isn't perfect but will catch most mistaken uses of strings.
2782 function validateNativeFormat(data) {
2783 const firstRow = data[0];
2784 const firstX = firstRow[0];
2785 if (typeof firstX !== 'number' && !utils.isDateLike(firstX)) {
2786 throw new Error(`Expected number or date but got ${typeof firstX}: ${firstX}.`);
2787 }
2788 for (let i = 1; i < firstRow.length; i++) {
2789 const val = firstRow[i];
2790 if (val === null || val === undefined) continue;
2791 if (typeof val === 'number') continue;
2792 if (utils.isArrayLike(val)) continue; // e.g. error bars or custom bars.
2793 throw new Error(`Expected number or array but got ${typeof val}: ${val}.`);
2794 }
2795 }
2796
2797 /**
2798 * The user has provided their data as a pre-packaged JS array. If the x values
2799 * are numeric, this is the same as dygraphs' internal format. If the x values
2800 * are dates, we need to convert them from Date objects to ms since epoch.
2801 * @param {!Array} data
2802 * @return {Object} data with numeric x values.
2803 * @private
2804 */
2805 Dygraph.prototype.parseArray_ = function(data) {
2806 // Peek at the first x value to see if it's numeric.
2807 if (data.length === 0) {
2808 console.error("Can't plot empty data set");
2809 return null;
2810 }
2811 if (data[0].length === 0) {
2812 console.error("Data set cannot contain an empty row");
2813 return null;
2814 }
2815
2816 validateNativeFormat(data);
2817
2818 var i;
2819 if (this.attr_("labels") === null) {
2820 console.warn("Using default labels. Set labels explicitly via 'labels' " +
2821 "in the options parameter");
2822 this.attrs_.labels = [ "X" ];
2823 for (i = 1; i < data[0].length; i++) {
2824 this.attrs_.labels.push("Y" + i); // Not user_attrs_.
2825 }
2826 this.attributes_.reparseSeries();
2827 } else {
2828 var num_labels = this.attr_("labels");
2829 if (num_labels.length != data[0].length) {
2830 console.error("Mismatch between number of labels (" + num_labels + ")" +
2831 " and number of columns in array (" + data[0].length + ")");
2832 return null;
2833 }
2834 }
2835
2836 if (utils.isDateLike(data[0][0])) {
2837 // Some intelligent defaults for a date x-axis.
2838 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2839 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2840 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2841
2842 // Assume they're all dates.
2843 var parsedData = utils.clone(data);
2844 for (i = 0; i < data.length; i++) {
2845 if (parsedData[i].length === 0) {
2846 console.error("Row " + (1 + i) + " of data is empty");
2847 return null;
2848 }
2849 if (parsedData[i][0] === null ||
2850 typeof(parsedData[i][0].getTime) != 'function' ||
2851 isNaN(parsedData[i][0].getTime())) {
2852 console.error("x value in row " + (1 + i) + " is not a Date");
2853 return null;
2854 }
2855 parsedData[i][0] = parsedData[i][0].getTime();
2856 }
2857 return parsedData;
2858 } else {
2859 // Some intelligent defaults for a numeric x-axis.
2860 /** @private (shut up, jsdoc!) */
2861 this.attrs_.axes.x.valueFormatter = function(x) { return x; };
2862 this.attrs_.axes.x.ticker = DygraphTickers.numericTicks;
2863 this.attrs_.axes.x.axisLabelFormatter = utils.numberAxisLabelFormatter;
2864 return data;
2865 }
2866 };
2867
2868 /**
2869 * Parses a DataTable object from gviz.
2870 * The data is expected to have a first column that is either a date or a
2871 * number. All subsequent columns must be numbers. If there is a clear mismatch
2872 * between this.xValueParser_ and the type of the first column, it will be
2873 * fixed. Fills out rawData_.
2874 * @param {!google.visualization.DataTable} data See above.
2875 * @private
2876 */
2877 Dygraph.prototype.parseDataTable_ = function(data) {
2878 var shortTextForAnnotationNum = function(num) {
2879 // converts [0-9]+ [A-Z][a-z]*
2880 // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
2881 // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
2882 var shortText = String.fromCharCode(65 /* A */ + num % 26);
2883 num = Math.floor(num / 26);
2884 while ( num > 0 ) {
2885 shortText = String.fromCharCode(65 /* A */ + (num - 1) % 26 ) + shortText.toLowerCase();
2886 num = Math.floor((num - 1) / 26);
2887 }
2888 return shortText;
2889 };
2890
2891 var cols = data.getNumberOfColumns();
2892 var rows = data.getNumberOfRows();
2893
2894 var indepType = data.getColumnType(0);
2895 if (indepType == 'date' || indepType == 'datetime') {
2896 this.attrs_.xValueParser = utils.dateParser;
2897 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2898 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2899 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2900 } else if (indepType == 'number') {
2901 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2902 this.attrs_.axes.x.valueFormatter = function(x) { return x; };
2903 this.attrs_.axes.x.ticker = DygraphTickers.numericTicks;
2904 this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter;
2905 } else {
2906 throw new Error(
2907 "only 'date', 'datetime' and 'number' types are supported " +
2908 "for column 1 of DataTable input (Got '" + indepType + "')");
2909 }
2910
2911 // Array of the column indices which contain data (and not annotations).
2912 var colIdx = [];
2913 var annotationCols = {}; // data index -> [annotation cols]
2914 var hasAnnotations = false;
2915 var i, j;
2916 for (i = 1; i < cols; i++) {
2917 var type = data.getColumnType(i);
2918 if (type == 'number') {
2919 colIdx.push(i);
2920 } else if (type == 'string' && this.getBooleanOption('displayAnnotations')) {
2921 // This is OK -- it's an annotation column.
2922 var dataIdx = colIdx[colIdx.length - 1];
2923 if (!annotationCols.hasOwnProperty(dataIdx)) {
2924 annotationCols[dataIdx] = [i];
2925 } else {
2926 annotationCols[dataIdx].push(i);
2927 }
2928 hasAnnotations = true;
2929 } else {
2930 throw new Error(
2931 "Only 'number' is supported as a dependent type with Gviz." +
2932 " 'string' is only supported if displayAnnotations is true");
2933 }
2934 }
2935
2936 // Read column labels
2937 // TODO(danvk): add support back for errorBars
2938 var labels = [data.getColumnLabel(0)];
2939 for (i = 0; i < colIdx.length; i++) {
2940 labels.push(data.getColumnLabel(colIdx[i]));
2941 if (this.getBooleanOption("errorBars")) i += 1;
2942 }
2943 this.attrs_.labels = labels;
2944 cols = labels.length;
2945
2946 var ret = [];
2947 var outOfOrder = false;
2948 var annotations = [];
2949 for (i = 0; i < rows; i++) {
2950 var row = [];
2951 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2952 data.getValue(i, 0) === null) {
2953 console.warn("Ignoring row " + i +
2954 " of DataTable because of undefined or null first column.");
2955 continue;
2956 }
2957
2958 if (indepType == 'date' || indepType == 'datetime') {
2959 row.push(data.getValue(i, 0).getTime());
2960 } else {
2961 row.push(data.getValue(i, 0));
2962 }
2963 if (!this.getBooleanOption("errorBars")) {
2964 for (j = 0; j < colIdx.length; j++) {
2965 var col = colIdx[j];
2966 row.push(data.getValue(i, col));
2967 if (hasAnnotations &&
2968 annotationCols.hasOwnProperty(col) &&
2969 data.getValue(i, annotationCols[col][0]) !== null) {
2970 var ann = {};
2971 ann.series = data.getColumnLabel(col);
2972 ann.xval = row[0];
2973 ann.shortText = shortTextForAnnotationNum(annotations.length);
2974 ann.text = '';
2975 for (var k = 0; k < annotationCols[col].length; k++) {
2976 if (k) ann.text += "\n";
2977 ann.text += data.getValue(i, annotationCols[col][k]);
2978 }
2979 annotations.push(ann);
2980 }
2981 }
2982
2983 // Strip out infinities, which give dygraphs problems later on.
2984 for (j = 0; j < row.length; j++) {
2985 if (!isFinite(row[j])) row[j] = null;
2986 }
2987 } else {
2988 for (j = 0; j < cols - 1; j++) {
2989 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2990 }
2991 }
2992 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2993 outOfOrder = true;
2994 }
2995 ret.push(row);
2996 }
2997
2998 if (outOfOrder) {
2999 console.warn("DataTable is out of order; order it correctly to speed loading.");
3000 ret.sort(function(a,b) { return a[0] - b[0]; });
3001 }
3002 this.rawData_ = ret;
3003
3004 if (annotations.length > 0) {
3005 this.setAnnotations(annotations, true);
3006 }
3007 this.attributes_.reparseSeries();
3008 };
3009
3010 /**
3011 * Signals to plugins that the chart data has updated.
3012 * This happens after the data has updated but before the chart has redrawn.
3013 */
3014 Dygraph.prototype.cascadeDataDidUpdateEvent_ = function() {
3015 // TODO(danvk): there are some issues checking xAxisRange() and using
3016 // toDomCoords from handlers of this event. The visible range should be set
3017 // when the chart is drawn, not derived from the data.
3018 this.cascadeEvents_('dataDidUpdate', {});
3019 };
3020
3021 /**
3022 * Get the CSV data. If it's in a function, call that function. If it's in a
3023 * file, do an XMLHttpRequest to get it.
3024 * @private
3025 */
3026 Dygraph.prototype.start_ = function() {
3027 var data = this.file_;
3028
3029 // Functions can return references of all other types.
3030 if (typeof data == 'function') {
3031 data = data();
3032 }
3033
3034 if (utils.isArrayLike(data)) {
3035 this.rawData_ = this.parseArray_(data);
3036 this.cascadeDataDidUpdateEvent_();
3037 this.predraw_();
3038 } else if (typeof data == 'object' &&
3039 typeof data.getColumnRange == 'function') {
3040 // must be a DataTable from gviz.
3041 this.parseDataTable_(data);
3042 this.cascadeDataDidUpdateEvent_();
3043 this.predraw_();
3044 } else if (typeof data == 'string') {
3045 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3046 var line_delimiter = utils.detectLineDelimiter(data);
3047 if (line_delimiter) {
3048 this.loadedEvent_(data);
3049 } else {
3050 // REMOVE_FOR_IE
3051 var req;
3052 if (window.XMLHttpRequest) {
3053 // Firefox, Opera, IE7, and other browsers will use the native object
3054 req = new XMLHttpRequest();
3055 } else {
3056 // IE 5 and 6 will use the ActiveX control
3057 req = new ActiveXObject("Microsoft.XMLHTTP");
3058 }
3059
3060 var caller = this;
3061 req.onreadystatechange = function () {
3062 if (req.readyState == 4) {
3063 if (req.status === 200 || // Normal http
3064 req.status === 0) { // Chrome w/ --allow-file-access-from-files
3065 caller.loadedEvent_(req.responseText);
3066 }
3067 }
3068 };
3069
3070 req.open("GET", data, true);
3071 req.send(null);
3072 }
3073 } else {
3074 console.error("Unknown data format: " + (typeof data));
3075 }
3076 };
3077
3078 /**
3079 * Changes various properties of the graph. These can include:
3080 * <ul>
3081 * <li>file: changes the source data for the graph</li>
3082 * <li>errorBars: changes whether the data contains stddev</li>
3083 * </ul>
3084 *
3085 * There's a huge variety of options that can be passed to this method. For a
3086 * full list, see http://dygraphs.com/options.html.
3087 *
3088 * @param {Object} input_attrs The new properties and values
3089 * @param {boolean} block_redraw Usually the chart is redrawn after every
3090 * call to updateOptions(). If you know better, you can pass true to
3091 * explicitly block the redraw. This can be useful for chaining
3092 * updateOptions() calls, avoiding the occasional infinite loop and
3093 * preventing redraws when it's not necessary (e.g. when updating a
3094 * callback).
3095 */
3096 Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) {
3097 if (typeof(block_redraw) == 'undefined') block_redraw = false;
3098
3099 // copyUserAttrs_ drops the "file" parameter as a convenience to us.
3100 var file = input_attrs.file;
3101 var attrs = Dygraph.copyUserAttrs_(input_attrs);
3102
3103 // TODO(danvk): this is a mess. Move these options into attr_.
3104 if ('rollPeriod' in attrs) {
3105 this.rollPeriod_ = attrs.rollPeriod;
3106 }
3107 if ('dateWindow' in attrs) {
3108 this.dateWindow_ = attrs.dateWindow;
3109 }
3110
3111 // TODO(danvk): validate per-series options.
3112 // Supported:
3113 // strokeWidth
3114 // pointSize
3115 // drawPoints
3116 // highlightCircleSize
3117
3118 // Check if this set options will require new points.
3119 var requiresNewPoints = utils.isPixelChangingOptionList(this.attr_("labels"), attrs);
3120
3121 utils.updateDeep(this.user_attrs_, attrs);
3122
3123 this.attributes_.reparseSeries();
3124
3125 if (file) {
3126 // This event indicates that the data is about to change, but hasn't yet.
3127 // TODO(danvk): support cancelation of the update via this event.
3128 this.cascadeEvents_('dataWillUpdate', {});
3129
3130 this.file_ = file;
3131 if (!block_redraw) this.start_();
3132 } else {
3133 if (!block_redraw) {
3134 if (requiresNewPoints) {
3135 this.predraw_();
3136 } else {
3137 this.renderGraph_(false);
3138 }
3139 }
3140 }
3141 };
3142
3143 /**
3144 * Make a copy of input attributes, removing file as a convenience.
3145 */
3146 Dygraph.copyUserAttrs_ = function(attrs) {
3147 var my_attrs = {};
3148 for (var k in attrs) {
3149 if (!attrs.hasOwnProperty(k)) continue;
3150 if (k == 'file') continue;
3151 if (attrs.hasOwnProperty(k)) my_attrs[k] = attrs[k];
3152 }
3153 return my_attrs;
3154 };
3155
3156 /**
3157 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3158 * containing div (which has presumably changed size since the dygraph was
3159 * instantiated. If the width/height are specified, the div will be resized.
3160 *
3161 * This is far more efficient than destroying and re-instantiating a
3162 * Dygraph, since it doesn't have to reparse the underlying data.
3163 *
3164 * @param {number} width Width (in pixels)
3165 * @param {number} height Height (in pixels)
3166 */
3167 Dygraph.prototype.resize = function(width, height) {
3168 if (this.resize_lock) {
3169 return;
3170 }
3171 this.resize_lock = true;
3172
3173 if ((width === null) != (height === null)) {
3174 console.warn("Dygraph.resize() should be called with zero parameters or " +
3175 "two non-NULL parameters. Pretending it was zero.");
3176 width = height = null;
3177 }
3178
3179 var old_width = this.width_;
3180 var old_height = this.height_;
3181
3182 if (width) {
3183 this.maindiv_.style.width = width + "px";
3184 this.maindiv_.style.height = height + "px";
3185 this.width_ = width;
3186 this.height_ = height;
3187 } else {
3188 this.width_ = this.maindiv_.clientWidth;
3189 this.height_ = this.maindiv_.clientHeight;
3190 }
3191
3192 if (old_width != this.width_ || old_height != this.height_) {
3193 // Resizing a canvas erases it, even when the size doesn't change, so
3194 // any resize needs to be followed by a redraw.
3195 this.resizeElements_();
3196 this.predraw_();
3197 }
3198
3199 this.resize_lock = false;
3200 };
3201
3202 /**
3203 * Adjusts the number of points in the rolling average. Updates the graph to
3204 * reflect the new averaging period.
3205 * @param {number} length Number of points over which to average the data.
3206 */
3207 Dygraph.prototype.adjustRoll = function(length) {
3208 this.rollPeriod_ = length;
3209 this.predraw_();
3210 };
3211
3212 /**
3213 * Returns a boolean array of visibility statuses.
3214 */
3215 Dygraph.prototype.visibility = function() {
3216 // Do lazy-initialization, so that this happens after we know the number of
3217 // data series.
3218 if (!this.getOption("visibility")) {
3219 this.attrs_.visibility = [];
3220 }
3221 // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs.
3222 while (this.getOption("visibility").length < this.numColumns() - 1) {
3223 this.attrs_.visibility.push(true);
3224 }
3225 return this.getOption("visibility");
3226 };
3227
3228 /**
3229 * Changes the visibility of one or more series.
3230 *
3231 * @param {number|number[]|object} num the series index or an array of series indices
3232 * or a boolean array of visibility states by index
3233 * or an object mapping series numbers, as keys, to
3234 * visibility state (boolean values)
3235 * @param {boolean} value the visibility state expressed as a boolean
3236 */
3237 Dygraph.prototype.setVisibility = function(num, value) {
3238 var x = this.visibility();
3239 var numIsObject = false;
3240
3241 if (!Array.isArray(num)) {
3242 if (num !== null && typeof num === 'object') {
3243 numIsObject = true;
3244 } else {
3245 num = [num];
3246 }
3247 }
3248
3249 if (numIsObject) {
3250 for (var i in num) {
3251 if (num.hasOwnProperty(i)) {
3252 if (i < 0 || i >= x.length) {
3253 console.warn("Invalid series number in setVisibility: " + i);
3254 } else {
3255 x[i] = num[i];
3256 }
3257 }
3258 }
3259 } else {
3260 for (var i = 0; i < num.length; i++) {
3261 if (typeof num[i] === 'boolean') {
3262 if (i >= x.length) {
3263 console.warn("Invalid series number in setVisibility: " + i);
3264 } else {
3265 x[i] = num[i];
3266 }
3267 } else {
3268 if (num[i] < 0 || num[i] >= x.length) {
3269 console.warn("Invalid series number in setVisibility: " + num[i]);
3270 } else {
3271 x[num[i]] = value;
3272 }
3273 }
3274 }
3275 }
3276
3277 this.predraw_();
3278 };
3279
3280 /**
3281 * How large of an area will the dygraph render itself in?
3282 * This is used for testing.
3283 * @return A {width: w, height: h} object.
3284 * @private
3285 */
3286 Dygraph.prototype.size = function() {
3287 return { width: this.width_, height: this.height_ };
3288 };
3289
3290 /**
3291 * Update the list of annotations and redraw the chart.
3292 * See dygraphs.com/annotations.html for more info on how to use annotations.
3293 * @param ann {Array} An array of annotation objects.
3294 * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3295 */
3296 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
3297 // Only add the annotation CSS rule once we know it will be used.
3298 this.annotations_ = ann;
3299 if (!this.layout_) {
3300 console.warn("Tried to setAnnotations before dygraph was ready. " +
3301 "Try setting them in a ready() block. See " +
3302 "dygraphs.com/tests/annotation.html");
3303 return;
3304 }
3305
3306 this.layout_.setAnnotations(this.annotations_);
3307 if (!suppressDraw) {
3308 this.predraw_();
3309 }
3310 };
3311
3312 /**
3313 * Return the list of annotations.
3314 */
3315 Dygraph.prototype.annotations = function() {
3316 return this.annotations_;
3317 };
3318
3319 /**
3320 * Get the list of label names for this graph. The first column is the
3321 * x-axis, so the data series names start at index 1.
3322 *
3323 * Returns null when labels have not yet been defined.
3324 */
3325 Dygraph.prototype.getLabels = function() {
3326 var labels = this.attr_("labels");
3327 return labels ? labels.slice() : null;
3328 };
3329
3330 /**
3331 * Get the index of a series (column) given its name. The first column is the
3332 * x-axis, so the data series start with index 1.
3333 */
3334 Dygraph.prototype.indexFromSetName = function(name) {
3335 return this.setIndexByName_[name];
3336 };
3337
3338 /**
3339 * Find the row number corresponding to the given x-value.
3340 * Returns null if there is no such x-value in the data.
3341 * If there are multiple rows with the same x-value, this will return the
3342 * first one.
3343 * @param {number} xVal The x-value to look for (e.g. millis since epoch).
3344 * @return {?number} The row number, which you can pass to getValue(), or null.
3345 */
3346 Dygraph.prototype.getRowForX = function(xVal) {
3347 var low = 0,
3348 high = this.numRows() - 1;
3349
3350 while (low <= high) {
3351 var idx = (high + low) >> 1;
3352 var x = this.getValue(idx, 0);
3353 if (x < xVal) {
3354 low = idx + 1;
3355 } else if (x > xVal) {
3356 high = idx - 1;
3357 } else if (low != idx) { // equal, but there may be an earlier match.
3358 high = idx;
3359 } else {
3360 return idx;
3361 }
3362 }
3363
3364 return null;
3365 };
3366
3367 /**
3368 * Trigger a callback when the dygraph has drawn itself and is ready to be
3369 * manipulated. This is primarily useful when dygraphs has to do an XHR for the
3370 * data (i.e. a URL is passed as the data source) and the chart is drawn
3371 * asynchronously. If the chart has already drawn, the callback will fire
3372 * immediately.
3373 *
3374 * This is a good place to call setAnnotation().
3375 *
3376 * @param {function(!Dygraph)} callback The callback to trigger when the chart
3377 * is ready.
3378 */
3379 Dygraph.prototype.ready = function(callback) {
3380 if (this.is_initial_draw_) {
3381 this.readyFns_.push(callback);
3382 } else {
3383 callback.call(this, this);
3384 }
3385 };
3386
3387 /**
3388 * Add an event handler. This event handler is kept until the graph is
3389 * destroyed with a call to graph.destroy().
3390 *
3391 * @param {!Node} elem The element to add the event to.
3392 * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
3393 * @param {function(Event):(boolean|undefined)} fn The function to call
3394 * on the event. The function takes one parameter: the event object.
3395 * @private
3396 */
3397 Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) {
3398 utils.addEvent(elem, type, fn);
3399 this.registeredEvents_.push({elem, type, fn});
3400 };
3401
3402 Dygraph.prototype.removeTrackedEvents_ = function() {
3403 if (this.registeredEvents_) {
3404 for (var idx = 0; idx < this.registeredEvents_.length; idx++) {
3405 var reg = this.registeredEvents_[idx];
3406 utils.removeEvent(reg.elem, reg.type, reg.fn);
3407 }
3408 }
3409
3410 this.registeredEvents_ = [];
3411 };
3412
3413
3414 // Installed plugins, in order of precedence (most-general to most-specific).
3415 Dygraph.PLUGINS = [
3416 LegendPlugin,
3417 AxesPlugin,
3418 RangeSelectorPlugin, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
3419 ChartLabelsPlugin,
3420 AnnotationsPlugin,
3421 GridPlugin
3422 ];
3423
3424 // There are many symbols which have historically been available through the
3425 // Dygraph class. These are exported here for backwards compatibility.
3426 Dygraph.GVizChart = GVizChart;
3427 Dygraph.DASHED_LINE = utils.DASHED_LINE;
3428 Dygraph.DOT_DASH_LINE = utils.DOT_DASH_LINE;
3429 Dygraph.dateAxisLabelFormatter = utils.dateAxisLabelFormatter;
3430 Dygraph.toRGB_ = utils.toRGB_;
3431 Dygraph.findPos = utils.findPos;
3432 Dygraph.pageX = utils.pageX;
3433 Dygraph.pageY = utils.pageY;
3434 Dygraph.dateString_ = utils.dateString_;
3435 Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel;
3436 Dygraph.nonInteractiveModel = Dygraph.nonInteractiveModel_ = DygraphInteraction.nonInteractiveModel_;
3437 Dygraph.Circles = utils.Circles;
3438
3439 Dygraph.Plugins = {
3440 Legend: LegendPlugin,
3441 Axes: AxesPlugin,
3442 Annotations: AnnotationsPlugin,
3443 ChartLabels: ChartLabelsPlugin,
3444 Grid: GridPlugin,
3445 RangeSelector: RangeSelectorPlugin
3446 };
3447
3448 Dygraph.DataHandlers = {
3449 DefaultHandler,
3450 BarsHandler,
3451 CustomBarsHandler,
3452 DefaultFractionHandler,
3453 ErrorBarsHandler,
3454 FractionsBarsHandler
3455 };
3456
3457 Dygraph.startPan = DygraphInteraction.startPan;
3458 Dygraph.startZoom = DygraphInteraction.startZoom;
3459 Dygraph.movePan = DygraphInteraction.movePan;
3460 Dygraph.moveZoom = DygraphInteraction.moveZoom;
3461 Dygraph.endPan = DygraphInteraction.endPan;
3462 Dygraph.endZoom = DygraphInteraction.endZoom;
3463
3464 Dygraph.numericLinearTicks = DygraphTickers.numericLinearTicks;
3465 Dygraph.numericTicks = DygraphTickers.numericTicks;
3466 Dygraph.dateTicker = DygraphTickers.dateTicker;
3467 Dygraph.Granularity = DygraphTickers.Granularity;
3468 Dygraph.getDateAxis = DygraphTickers.getDateAxis;
3469 Dygraph.floatFormat = utils.floatFormat;
3470
3471 export default Dygraph;