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