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