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