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