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