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