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