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