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