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