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