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