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