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