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