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