7d64bb9d8e8ad7e370688db9a4c3a2b06134a9b9
[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 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRange());
1090 }
1091 };
1092
1093 /**
1094 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1095 * the canvas. This function redraws the graph.
1096 *
1097 * @param {Number} lowY The topmost pixel value that should be visible.
1098 * @param {Number} highY The lowest pixel value that should be visible.
1099 * @private
1100 */
1101 Dygraph.prototype.doZoomY_ = function(lowY, highY) {
1102 // Find the highest and lowest values in pixel range for each axis.
1103 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1104 // This is because pixels increase as you go down on the screen, whereas data
1105 // coordinates increase as you go up the screen.
1106 var valueRanges = [];
1107 for (var i = 0; i < this.axes_.length; i++) {
1108 var hi = this.toDataCoords(null, lowY, i);
1109 var low = this.toDataCoords(null, highY, i);
1110 this.axes_[i].valueWindow = [low[1], hi[1]];
1111 valueRanges.push([low[1], hi[1]]);
1112 }
1113
1114 this.drawGraph_();
1115 if (this.attr_("zoomCallback")) {
1116 var xRange = this.xAxisRange();
1117 this.attr_("zoomCallback")(xRange[0], xRange[1], this.yAxisRanges());
1118 }
1119 };
1120
1121 /**
1122 * Reset the zoom to the original view coordinates. This is the same as
1123 * double-clicking on the graph.
1124 *
1125 * @private
1126 */
1127 Dygraph.prototype.doUnzoom_ = function() {
1128 var dirty = false;
1129 if (this.dateWindow_ != null) {
1130 dirty = true;
1131 this.dateWindow_ = null;
1132 }
1133
1134 for (var i = 0; i < this.axes_.length; i++) {
1135 if (this.axes_[i].valueWindow != null) {
1136 dirty = true;
1137 delete this.axes_[i].valueWindow;
1138 }
1139 }
1140
1141 if (dirty) {
1142 // Putting the drawing operation before the callback because it resets
1143 // yAxisRange.
1144 this.drawGraph_();
1145 if (this.attr_("zoomCallback")) {
1146 var minDate = this.rawData_[0][0];
1147 var maxDate = this.rawData_[this.rawData_.length - 1][0];
1148 this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges());
1149 }
1150 }
1151 };
1152
1153 /**
1154 * When the mouse moves in the canvas, display information about a nearby data
1155 * point and draw dots over those points in the data series. This function
1156 * takes care of cleanup of previously-drawn dots.
1157 * @param {Object} event The mousemove event from the browser.
1158 * @private
1159 */
1160 Dygraph.prototype.mouseMove_ = function(event) {
1161 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_);
1162 var points = this.layout_.points;
1163
1164 var lastx = -1;
1165 var lasty = -1;
1166
1167 // Loop through all the points and find the date nearest to our current
1168 // location.
1169 var minDist = 1e+100;
1170 var idx = -1;
1171 for (var i = 0; i < points.length; i++) {
1172 var point = points[i];
1173 if (point == null) continue;
1174 var dist = Math.abs(points[i].canvasx - canvasx);
1175 if (dist > minDist) continue;
1176 minDist = dist;
1177 idx = i;
1178 }
1179 if (idx >= 0) lastx = points[idx].xval;
1180 // Check that you can really highlight the last day's data
1181 var last = points[points.length-1];
1182 if (last != null && canvasx > last.canvasx)
1183 lastx = points[points.length-1].xval;
1184
1185 // Extract the points we've selected
1186 this.selPoints_ = [];
1187 var l = points.length;
1188 if (!this.attr_("stackedGraph")) {
1189 for (var i = 0; i < l; i++) {
1190 if (points[i].xval == lastx) {
1191 this.selPoints_.push(points[i]);
1192 }
1193 }
1194 } else {
1195 // Need to 'unstack' points starting from the bottom
1196 var cumulative_sum = 0;
1197 for (var i = l - 1; i >= 0; i--) {
1198 if (points[i].xval == lastx) {
1199 var p = {}; // Clone the point since we modify it
1200 for (var k in points[i]) {
1201 p[k] = points[i][k];
1202 }
1203 p.yval -= cumulative_sum;
1204 cumulative_sum += p.yval;
1205 this.selPoints_.push(p);
1206 }
1207 }
1208 this.selPoints_.reverse();
1209 }
1210
1211 if (this.attr_("highlightCallback")) {
1212 var px = this.lastx_;
1213 if (px !== null && lastx != px) {
1214 // only fire if the selected point has changed.
1215 this.attr_("highlightCallback")(event, lastx, this.selPoints_, this.idxToRow_(idx));
1216 }
1217 }
1218
1219 // Save last x position for callbacks.
1220 this.lastx_ = lastx;
1221
1222 this.updateSelection_();
1223 };
1224
1225 /**
1226 * Transforms layout_.points index into data row number.
1227 * @param int layout_.points index
1228 * @return int row number, or -1 if none could be found.
1229 * @private
1230 */
1231 Dygraph.prototype.idxToRow_ = function(idx) {
1232 if (idx < 0) return -1;
1233
1234 for (var i in this.layout_.datasets) {
1235 if (idx < this.layout_.datasets[i].length) {
1236 return this.boundaryIds_[0][0]+idx;
1237 }
1238 idx -= this.layout_.datasets[i].length;
1239 }
1240 return -1;
1241 };
1242
1243 /**
1244 * Draw dots over the selectied points in the data series. This function
1245 * takes care of cleanup of previously-drawn dots.
1246 * @private
1247 */
1248 Dygraph.prototype.updateSelection_ = function() {
1249 // Clear the previously drawn vertical, if there is one
1250 var ctx = this.canvas_.getContext("2d");
1251 if (this.previousVerticalX_ >= 0) {
1252 // Determine the maximum highlight circle size.
1253 var maxCircleSize = 0;
1254 var labels = this.attr_('labels');
1255 for (var i = 1; i < labels.length; i++) {
1256 var r = this.attr_('highlightCircleSize', labels[i]);
1257 if (r > maxCircleSize) maxCircleSize = r;
1258 }
1259 var px = this.previousVerticalX_;
1260 ctx.clearRect(px - maxCircleSize - 1, 0,
1261 2 * maxCircleSize + 2, this.height_);
1262 }
1263
1264 var isOK = function(x) { return x && !isNaN(x); };
1265
1266 if (this.selPoints_.length > 0) {
1267 var canvasx = this.selPoints_[0].canvasx;
1268
1269 // Set the status message to indicate the selected point(s)
1270 var replace = this.attr_('xValueFormatter')(this.lastx_, this) + ":";
1271 var fmtFunc = this.attr_('yValueFormatter');
1272 var clen = this.colors_.length;
1273
1274 if (this.attr_('showLabelsOnHighlight')) {
1275 // Set the status message to indicate the selected point(s)
1276 for (var i = 0; i < this.selPoints_.length; i++) {
1277 if (!this.attr_("labelsShowZeroValues") && this.selPoints_[i].yval == 0) continue;
1278 if (!isOK(this.selPoints_[i].canvasy)) continue;
1279 if (this.attr_("labelsSeparateLines")) {
1280 replace += "<br/>";
1281 }
1282 var point = this.selPoints_[i];
1283 var c = new RGBColor(this.plotter_.colors[point.name]);
1284 var yval = fmtFunc(point.yval);
1285 replace += " <b><font color='" + c.toHex() + "'>"
1286 + point.name + "</font></b>:"
1287 + yval;
1288 }
1289
1290 this.attr_("labelsDiv").innerHTML = replace;
1291 }
1292
1293 // Draw colored circles over the center of each selected point
1294 ctx.save();
1295 for (var i = 0; i < this.selPoints_.length; i++) {
1296 if (!isOK(this.selPoints_[i].canvasy)) continue;
1297 var circleSize =
1298 this.attr_('highlightCircleSize', this.selPoints_[i].name);
1299 ctx.beginPath();
1300 ctx.fillStyle = this.plotter_.colors[this.selPoints_[i].name];
1301 ctx.arc(canvasx, this.selPoints_[i].canvasy, circleSize,
1302 0, 2 * Math.PI, false);
1303 ctx.fill();
1304 }
1305 ctx.restore();
1306
1307 this.previousVerticalX_ = canvasx;
1308 }
1309 };
1310
1311 /**
1312 * Set manually set selected dots, and display information about them
1313 * @param int row number that should by highlighted
1314 * false value clears the selection
1315 * @public
1316 */
1317 Dygraph.prototype.setSelection = function(row) {
1318 // Extract the points we've selected
1319 this.selPoints_ = [];
1320 var pos = 0;
1321
1322 if (row !== false) {
1323 row = row-this.boundaryIds_[0][0];
1324 }
1325
1326 if (row !== false && row >= 0) {
1327 for (var i in this.layout_.datasets) {
1328 if (row < this.layout_.datasets[i].length) {
1329 var point = this.layout_.points[pos+row];
1330
1331 if (this.attr_("stackedGraph")) {
1332 point = this.layout_.unstackPointAtIndex(pos+row);
1333 }
1334
1335 this.selPoints_.push(point);
1336 }
1337 pos += this.layout_.datasets[i].length;
1338 }
1339 }
1340
1341 if (this.selPoints_.length) {
1342 this.lastx_ = this.selPoints_[0].xval;
1343 this.updateSelection_();
1344 } else {
1345 this.lastx_ = -1;
1346 this.clearSelection();
1347 }
1348
1349 };
1350
1351 /**
1352 * The mouse has left the canvas. Clear out whatever artifacts remain
1353 * @param {Object} event the mouseout event from the browser.
1354 * @private
1355 */
1356 Dygraph.prototype.mouseOut_ = function(event) {
1357 if (this.attr_("unhighlightCallback")) {
1358 this.attr_("unhighlightCallback")(event);
1359 }
1360
1361 if (this.attr_("hideOverlayOnMouseOut")) {
1362 this.clearSelection();
1363 }
1364 };
1365
1366 /**
1367 * Remove all selection from the canvas
1368 * @public
1369 */
1370 Dygraph.prototype.clearSelection = function() {
1371 // Get rid of the overlay data
1372 var ctx = this.canvas_.getContext("2d");
1373 ctx.clearRect(0, 0, this.width_, this.height_);
1374 this.attr_("labelsDiv").innerHTML = "";
1375 this.selPoints_ = [];
1376 this.lastx_ = -1;
1377 }
1378
1379 /**
1380 * Returns the number of the currently selected row
1381 * @return int row number, of -1 if nothing is selected
1382 * @public
1383 */
1384 Dygraph.prototype.getSelection = function() {
1385 if (!this.selPoints_ || this.selPoints_.length < 1) {
1386 return -1;
1387 }
1388
1389 for (var row=0; row<this.layout_.points.length; row++ ) {
1390 if (this.layout_.points[row].x == this.selPoints_[0].x) {
1391 return row + this.boundaryIds_[0][0];
1392 }
1393 }
1394 return -1;
1395 }
1396
1397 Dygraph.zeropad = function(x) {
1398 if (x < 10) return "0" + x; else return "" + x;
1399 }
1400
1401 /**
1402 * Return a string version of the hours, minutes and seconds portion of a date.
1403 * @param {Number} date The JavaScript date (ms since epoch)
1404 * @return {String} A time of the form "HH:MM:SS"
1405 * @private
1406 */
1407 Dygraph.hmsString_ = function(date) {
1408 var zeropad = Dygraph.zeropad;
1409 var d = new Date(date);
1410 if (d.getSeconds()) {
1411 return zeropad(d.getHours()) + ":" +
1412 zeropad(d.getMinutes()) + ":" +
1413 zeropad(d.getSeconds());
1414 } else {
1415 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
1416 }
1417 }
1418
1419 /**
1420 * Convert a JS date to a string appropriate to display on an axis that
1421 * is displaying values at the stated granularity.
1422 * @param {Date} date The date to format
1423 * @param {Number} granularity One of the Dygraph granularity constants
1424 * @return {String} The formatted date
1425 * @private
1426 */
1427 Dygraph.dateAxisFormatter = function(date, granularity) {
1428 if (granularity >= Dygraph.MONTHLY) {
1429 return date.strftime('%b %y');
1430 } else {
1431 var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds();
1432 if (frac == 0 || granularity >= Dygraph.DAILY) {
1433 return new Date(date.getTime() + 3600*1000).strftime('%d%b');
1434 } else {
1435 return Dygraph.hmsString_(date.getTime());
1436 }
1437 }
1438 }
1439
1440 /**
1441 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1442 * @param {Number} date The JavaScript date (ms since epoch)
1443 * @return {String} A date of the form "YYYY/MM/DD"
1444 * @private
1445 */
1446 Dygraph.dateString_ = function(date, self) {
1447 var zeropad = Dygraph.zeropad;
1448 var d = new Date(date);
1449
1450 // Get the year:
1451 var year = "" + d.getFullYear();
1452 // Get a 0 padded month string
1453 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
1454 // Get a 0 padded day string
1455 var day = zeropad(d.getDate());
1456
1457 var ret = "";
1458 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
1459 if (frac) ret = " " + Dygraph.hmsString_(date);
1460
1461 return year + "/" + month + "/" + day + ret;
1462 };
1463
1464 /**
1465 * Round a number to the specified number of digits past the decimal point.
1466 * @param {Number} num The number to round
1467 * @param {Number} places The number of decimals to which to round
1468 * @return {Number} The rounded number
1469 * @private
1470 */
1471 Dygraph.round_ = function(num, places) {
1472 var shift = Math.pow(10, places);
1473 return Math.round(num * shift)/shift;
1474 };
1475
1476 /**
1477 * Fires when there's data available to be graphed.
1478 * @param {String} data Raw CSV data to be plotted
1479 * @private
1480 */
1481 Dygraph.prototype.loadedEvent_ = function(data) {
1482 this.rawData_ = this.parseCSV_(data);
1483 this.predraw_();
1484 };
1485
1486 Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1487 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1488 Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
1489
1490 /**
1491 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1492 * @private
1493 */
1494 Dygraph.prototype.addXTicks_ = function() {
1495 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1496 var startDate, endDate;
1497 if (this.dateWindow_) {
1498 startDate = this.dateWindow_[0];
1499 endDate = this.dateWindow_[1];
1500 } else {
1501 startDate = this.rawData_[0][0];
1502 endDate = this.rawData_[this.rawData_.length - 1][0];
1503 }
1504
1505 var xTicks = this.attr_('xTicker')(startDate, endDate, this);
1506 this.layout_.updateOptions({xTicks: xTicks});
1507 };
1508
1509 // Time granularity enumeration
1510 Dygraph.SECONDLY = 0;
1511 Dygraph.TWO_SECONDLY = 1;
1512 Dygraph.FIVE_SECONDLY = 2;
1513 Dygraph.TEN_SECONDLY = 3;
1514 Dygraph.THIRTY_SECONDLY = 4;
1515 Dygraph.MINUTELY = 5;
1516 Dygraph.TWO_MINUTELY = 6;
1517 Dygraph.FIVE_MINUTELY = 7;
1518 Dygraph.TEN_MINUTELY = 8;
1519 Dygraph.THIRTY_MINUTELY = 9;
1520 Dygraph.HOURLY = 10;
1521 Dygraph.TWO_HOURLY = 11;
1522 Dygraph.SIX_HOURLY = 12;
1523 Dygraph.DAILY = 13;
1524 Dygraph.WEEKLY = 14;
1525 Dygraph.MONTHLY = 15;
1526 Dygraph.QUARTERLY = 16;
1527 Dygraph.BIANNUAL = 17;
1528 Dygraph.ANNUAL = 18;
1529 Dygraph.DECADAL = 19;
1530 Dygraph.NUM_GRANULARITIES = 20;
1531
1532 Dygraph.SHORT_SPACINGS = [];
1533 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
1534 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
1535 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
1536 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
1537 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
1538 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
1539 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
1540 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
1541 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
1542 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
1543 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
1544 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
1545 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
1546 Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
1547 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
1548
1549 // NumXTicks()
1550 //
1551 // If we used this time granularity, how many ticks would there be?
1552 // This is only an approximation, but it's generally good enough.
1553 //
1554 Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1555 if (granularity < Dygraph.MONTHLY) {
1556 // Generate one tick mark for every fixed interval of time.
1557 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1558 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1559 } else {
1560 var year_mod = 1; // e.g. to only print one point every 10 years.
1561 var num_months = 12;
1562 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1563 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1564 if (granularity == Dygraph.ANNUAL) num_months = 1;
1565 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
1566
1567 var msInYear = 365.2524 * 24 * 3600 * 1000;
1568 var num_years = 1.0 * (end_time - start_time) / msInYear;
1569 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1570 }
1571 };
1572
1573 // GetXAxis()
1574 //
1575 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1576 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1577 //
1578 // Returns an array containing {v: millis, label: label} dictionaries.
1579 //
1580 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
1581 var formatter = this.attr_("xAxisLabelFormatter");
1582 var ticks = [];
1583 if (granularity < Dygraph.MONTHLY) {
1584 // Generate one tick mark for every fixed interval of time.
1585 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1586 var format = '%d%b'; // e.g. "1Jan"
1587
1588 // Find a time less than start_time which occurs on a "nice" time boundary
1589 // for this granularity.
1590 var g = spacing / 1000;
1591 var d = new Date(start_time);
1592 if (g <= 60) { // seconds
1593 var x = d.getSeconds(); d.setSeconds(x - x % g);
1594 } else {
1595 d.setSeconds(0);
1596 g /= 60;
1597 if (g <= 60) { // minutes
1598 var x = d.getMinutes(); d.setMinutes(x - x % g);
1599 } else {
1600 d.setMinutes(0);
1601 g /= 60;
1602
1603 if (g <= 24) { // days
1604 var x = d.getHours(); d.setHours(x - x % g);
1605 } else {
1606 d.setHours(0);
1607 g /= 24;
1608
1609 if (g == 7) { // one week
1610 d.setDate(d.getDate() - d.getDay());
1611 }
1612 }
1613 }
1614 }
1615 start_time = d.getTime();
1616
1617 for (var t = start_time; t <= end_time; t += spacing) {
1618 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
1619 }
1620 } else {
1621 // Display a tick mark on the first of a set of months of each year.
1622 // Years get a tick mark iff y % year_mod == 0. This is useful for
1623 // displaying a tick mark once every 10 years, say, on long time scales.
1624 var months;
1625 var year_mod = 1; // e.g. to only print one point every 10 years.
1626
1627 if (granularity == Dygraph.MONTHLY) {
1628 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1629 } else if (granularity == Dygraph.QUARTERLY) {
1630 months = [ 0, 3, 6, 9 ];
1631 } else if (granularity == Dygraph.BIANNUAL) {
1632 months = [ 0, 6 ];
1633 } else if (granularity == Dygraph.ANNUAL) {
1634 months = [ 0 ];
1635 } else if (granularity == Dygraph.DECADAL) {
1636 months = [ 0 ];
1637 year_mod = 10;
1638 }
1639
1640 var start_year = new Date(start_time).getFullYear();
1641 var end_year = new Date(end_time).getFullYear();
1642 var zeropad = Dygraph.zeropad;
1643 for (var i = start_year; i <= end_year; i++) {
1644 if (i % year_mod != 0) continue;
1645 for (var j = 0; j < months.length; j++) {
1646 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1647 var t = Date.parse(date_str);
1648 if (t < start_time || t > end_time) continue;
1649 ticks.push({ v:t, label: formatter(new Date(t), granularity) });
1650 }
1651 }
1652 }
1653
1654 return ticks;
1655 };
1656
1657
1658 /**
1659 * Add ticks to the x-axis based on a date range.
1660 * @param {Number} startDate Start of the date window (millis since epoch)
1661 * @param {Number} endDate End of the date window (millis since epoch)
1662 * @return {Array.<Object>} Array of {label, value} tuples.
1663 * @public
1664 */
1665 Dygraph.dateTicker = function(startDate, endDate, self) {
1666 var chosen = -1;
1667 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1668 var num_ticks = self.NumXTicks(startDate, endDate, i);
1669 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
1670 chosen = i;
1671 break;
1672 }
1673 }
1674
1675 if (chosen >= 0) {
1676 return self.GetXAxis(startDate, endDate, chosen);
1677 } else {
1678 // TODO(danvk): signal error.
1679 }
1680 };
1681
1682 /**
1683 * Add ticks when the x axis has numbers on it (instead of dates)
1684 * @param {Number} startDate Start of the date window (millis since epoch)
1685 * @param {Number} endDate End of the date window (millis since epoch)
1686 * @param self
1687 * @param {function} attribute accessor function.
1688 * @return {Array.<Object>} Array of {label, value} tuples.
1689 * @public
1690 */
1691 Dygraph.numericTicks = function(minV, maxV, self, axis_props, vals) {
1692 var attr = function(k) {
1693 if (axis_props && axis_props.hasOwnProperty(k)) return axis_props[k];
1694 return self.attr_(k);
1695 };
1696
1697 var ticks = [];
1698 if (vals) {
1699 for (var i = 0; i < vals.length; i++) {
1700 ticks.push({v: vals[i]});
1701 }
1702 } else {
1703 // Basic idea:
1704 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1705 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
1706 // The first spacing greater than pixelsPerYLabel is what we use.
1707 // TODO(danvk): version that works on a log scale.
1708 if (attr("labelsKMG2")) {
1709 var mults = [1, 2, 4, 8];
1710 } else {
1711 var mults = [1, 2, 5];
1712 }
1713 var scale, low_val, high_val, nTicks;
1714 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1715 var pixelsPerTick = attr('pixelsPerYLabel');
1716 for (var i = -10; i < 50; i++) {
1717 if (attr("labelsKMG2")) {
1718 var base_scale = Math.pow(16, i);
1719 } else {
1720 var base_scale = Math.pow(10, i);
1721 }
1722 for (var j = 0; j < mults.length; j++) {
1723 scale = base_scale * mults[j];
1724 low_val = Math.floor(minV / scale) * scale;
1725 high_val = Math.ceil(maxV / scale) * scale;
1726 nTicks = Math.abs(high_val - low_val) / scale;
1727 var spacing = self.height_ / nTicks;
1728 // wish I could break out of both loops at once...
1729 if (spacing > pixelsPerTick) break;
1730 }
1731 if (spacing > pixelsPerTick) break;
1732 }
1733
1734 // Construct the set of ticks.
1735 // Allow reverse y-axis if it's explicitly requested.
1736 if (low_val > high_val) scale *= -1;
1737 for (var i = 0; i < nTicks; i++) {
1738 var tickV = low_val + i * scale;
1739 ticks.push( {v: tickV} );
1740 }
1741 }
1742
1743 // Add formatted labels to the ticks.
1744 var k;
1745 var k_labels = [];
1746 if (attr("labelsKMB")) {
1747 k = 1000;
1748 k_labels = [ "K", "M", "B", "T" ];
1749 }
1750 if (attr("labelsKMG2")) {
1751 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1752 k = 1024;
1753 k_labels = [ "k", "M", "G", "T" ];
1754 }
1755 var formatter = attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
1756
1757 for (var i = 0; i < ticks.length; i++) {
1758 var tickV = ticks[i].v;
1759 var absTickV = Math.abs(tickV);
1760 var label;
1761 if (formatter != undefined) {
1762 label = formatter(tickV);
1763 } else {
1764 label = Dygraph.round_(tickV, 2);
1765 }
1766 if (k_labels.length) {
1767 // Round up to an appropriate unit.
1768 var n = k*k*k*k;
1769 for (var j = 3; j >= 0; j--, n /= k) {
1770 if (absTickV >= n) {
1771 label = Dygraph.round_(tickV / n, 1) + k_labels[j];
1772 break;
1773 }
1774 }
1775 }
1776 ticks[i].label = label;
1777 }
1778 return ticks;
1779 };
1780
1781 // Computes the range of the data series (including confidence intervals).
1782 // series is either [ [x1, y1], [x2, y2], ... ] or
1783 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1784 // Returns [low, high]
1785 Dygraph.prototype.extremeValues_ = function(series) {
1786 var minY = null, maxY = null;
1787
1788 var bars = this.attr_("errorBars") || this.attr_("customBars");
1789 if (bars) {
1790 // With custom bars, maxY is the max of the high values.
1791 for (var j = 0; j < series.length; j++) {
1792 var y = series[j][1][0];
1793 if (!y) continue;
1794 var low = y - series[j][1][1];
1795 var high = y + series[j][1][2];
1796 if (low > y) low = y; // this can happen with custom bars,
1797 if (high < y) high = y; // e.g. in tests/custom-bars.html
1798 if (maxY == null || high > maxY) {
1799 maxY = high;
1800 }
1801 if (minY == null || low < minY) {
1802 minY = low;
1803 }
1804 }
1805 } else {
1806 for (var j = 0; j < series.length; j++) {
1807 var y = series[j][1];
1808 if (y === null || isNaN(y)) continue;
1809 if (maxY == null || y > maxY) {
1810 maxY = y;
1811 }
1812 if (minY == null || y < minY) {
1813 minY = y;
1814 }
1815 }
1816 }
1817
1818 return [minY, maxY];
1819 };
1820
1821 /**
1822 * This function is called once when the chart's data is changed or the options
1823 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1824 * idea is that values derived from the chart's data can be computed here,
1825 * rather than every time the chart is drawn. This includes things like the
1826 * number of axes, rolling averages, etc.
1827 */
1828 Dygraph.prototype.predraw_ = function() {
1829 // TODO(danvk): move more computations out of drawGraph_ and into here.
1830 this.computeYAxes_();
1831
1832 // Create a new plotter.
1833 if (this.plotter_) this.plotter_.clear();
1834 this.plotter_ = new DygraphCanvasRenderer(this,
1835 this.hidden_, this.layout_,
1836 this.renderOptions_);
1837
1838 // The roller sits in the bottom left corner of the chart. We don't know where
1839 // this will be until the options are available, so it's positioned here.
1840 this.createRollInterface_();
1841
1842 // Same thing applies for the labelsDiv. It's right edge should be flush with
1843 // the right edge of the charting area (which may not be the same as the right
1844 // edge of the div, if we have two y-axes.
1845 this.positionLabelsDiv_();
1846
1847 // If the data or options have changed, then we'd better redraw.
1848 this.drawGraph_();
1849 };
1850
1851 /**
1852 =======
1853 * Update the graph with new data. This method is called when the viewing area
1854 * has changed. If the underlying data or options have changed, predraw_ will
1855 * be called before drawGraph_ is called.
1856 * @private
1857 */
1858 Dygraph.prototype.drawGraph_ = function() {
1859 var data = this.rawData_;
1860
1861 // This is used to set the second parameter to drawCallback, below.
1862 var is_initial_draw = this.is_initial_draw_;
1863 this.is_initial_draw_ = false;
1864
1865 var minY = null, maxY = null;
1866 this.layout_.removeAllDatasets();
1867 this.setColors_();
1868 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1869
1870 // Loop over the fields (series). Go from the last to the first,
1871 // because if they're stacked that's how we accumulate the values.
1872
1873 var cumulative_y = []; // For stacked series.
1874 var datasets = [];
1875
1876 var extremes = {}; // series name -> [low, high]
1877
1878 // Loop over all fields and create datasets
1879 for (var i = data[0].length - 1; i >= 1; i--) {
1880 if (!this.visibility()[i - 1]) continue;
1881
1882 var seriesName = this.attr_("labels")[i];
1883 var connectSeparatedPoints = this.attr_('connectSeparatedPoints', i);
1884
1885 var series = [];
1886 for (var j = 0; j < data.length; j++) {
1887 if (data[j][i] != null || !connectSeparatedPoints) {
1888 var date = data[j][0];
1889 series.push([date, data[j][i]]);
1890 }
1891 }
1892
1893 // TODO(danvk): move this into predraw_. It's insane to do it here.
1894 series = this.rollingAverage(series, this.rollPeriod_);
1895
1896 // Prune down to the desired range, if necessary (for zooming)
1897 // Because there can be lines going to points outside of the visible area,
1898 // we actually prune to visible points, plus one on either side.
1899 var bars = this.attr_("errorBars") || this.attr_("customBars");
1900 if (this.dateWindow_) {
1901 var low = this.dateWindow_[0];
1902 var high= this.dateWindow_[1];
1903 var pruned = [];
1904 // TODO(danvk): do binary search instead of linear search.
1905 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1906 var firstIdx = null, lastIdx = null;
1907 for (var k = 0; k < series.length; k++) {
1908 if (series[k][0] >= low && firstIdx === null) {
1909 firstIdx = k;
1910 }
1911 if (series[k][0] <= high) {
1912 lastIdx = k;
1913 }
1914 }
1915 if (firstIdx === null) firstIdx = 0;
1916 if (firstIdx > 0) firstIdx--;
1917 if (lastIdx === null) lastIdx = series.length - 1;
1918 if (lastIdx < series.length - 1) lastIdx++;
1919 this.boundaryIds_[i-1] = [firstIdx, lastIdx];
1920 for (var k = firstIdx; k <= lastIdx; k++) {
1921 pruned.push(series[k]);
1922 }
1923 series = pruned;
1924 } else {
1925 this.boundaryIds_[i-1] = [0, series.length-1];
1926 }
1927
1928 var seriesExtremes = this.extremeValues_(series);
1929
1930 if (bars) {
1931 for (var j=0; j<series.length; j++) {
1932 val = [series[j][0], series[j][1][0], series[j][1][1], series[j][1][2]];
1933 series[j] = val;
1934 }
1935 } else if (this.attr_("stackedGraph")) {
1936 var l = series.length;
1937 var actual_y;
1938 for (var j = 0; j < l; j++) {
1939 // If one data set has a NaN, let all subsequent stacked
1940 // sets inherit the NaN -- only start at 0 for the first set.
1941 var x = series[j][0];
1942 if (cumulative_y[x] === undefined) {
1943 cumulative_y[x] = 0;
1944 }
1945
1946 actual_y = series[j][1];
1947 cumulative_y[x] += actual_y;
1948
1949 series[j] = [x, cumulative_y[x]]
1950
1951 if (cumulative_y[x] > seriesExtremes[1]) {
1952 seriesExtremes[1] = cumulative_y[x];
1953 }
1954 if (cumulative_y[x] < seriesExtremes[0]) {
1955 seriesExtremes[0] = cumulative_y[x];
1956 }
1957 }
1958 }
1959 extremes[seriesName] = seriesExtremes;
1960
1961 datasets[i] = series;
1962 }
1963
1964 for (var i = 1; i < datasets.length; i++) {
1965 if (!this.visibility()[i - 1]) continue;
1966 this.layout_.addDataset(this.attr_("labels")[i], datasets[i]);
1967 }
1968
1969 // TODO(danvk): this method doesn't need to return anything.
1970 var out = this.computeYAxisRanges_(extremes);
1971 var axes = out[0];
1972 var seriesToAxisMap = out[1];
1973 this.layout_.updateOptions( { yAxes: axes,
1974 seriesToAxisMap: seriesToAxisMap
1975 } );
1976
1977 this.addXTicks_();
1978
1979 // Tell PlotKit to use this new data and render itself
1980 this.layout_.updateOptions({dateWindow: this.dateWindow_});
1981 this.layout_.evaluateWithError();
1982 this.plotter_.clear();
1983 this.plotter_.render();
1984 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
1985 this.canvas_.height);
1986
1987 if (this.attr_("drawCallback") !== null) {
1988 this.attr_("drawCallback")(this, is_initial_draw);
1989 }
1990 };
1991
1992 /**
1993 * Determine properties of the y-axes which are independent of the data
1994 * currently being displayed. This includes things like the number of axes and
1995 * the style of the axes. It does not include the range of each axis and its
1996 * tick marks.
1997 * This fills in this.axes_ and this.seriesToAxisMap_.
1998 * axes_ = [ { options } ]
1999 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2000 * indices are into the axes_ array.
2001 */
2002 Dygraph.prototype.computeYAxes_ = function() {
2003 this.axes_ = [{}]; // always have at least one y-axis.
2004 this.seriesToAxisMap_ = {};
2005
2006 // Get a list of series names.
2007 var labels = this.attr_("labels");
2008 var series = {};
2009 for (var i = 1; i < labels.length; i++) series[labels[i]] = (i - 1);
2010
2011 // all options which could be applied per-axis:
2012 var axisOptions = [
2013 'includeZero',
2014 'valueRange',
2015 'labelsKMB',
2016 'labelsKMG2',
2017 'pixelsPerYLabel',
2018 'yAxisLabelWidth',
2019 'axisLabelFontSize',
2020 'axisTickSize'
2021 ];
2022
2023 // Copy global axis options over to the first axis.
2024 for (var i = 0; i < axisOptions.length; i++) {
2025 var k = axisOptions[i];
2026 var v = this.attr_(k);
2027 if (v) this.axes_[0][k] = v;
2028 }
2029
2030 // Go through once and add all the axes.
2031 for (var seriesName in series) {
2032 if (!series.hasOwnProperty(seriesName)) continue;
2033 var axis = this.attr_("axis", seriesName);
2034 if (axis == null) {
2035 this.seriesToAxisMap_[seriesName] = 0;
2036 continue;
2037 }
2038 if (typeof(axis) == 'object') {
2039 // Add a new axis, making a copy of its per-axis options.
2040 var opts = {};
2041 Dygraph.update(opts, this.axes_[0]);
2042 Dygraph.update(opts, { valueRange: null }); // shouldn't inherit this.
2043 Dygraph.update(opts, axis);
2044 this.axes_.push(opts);
2045 this.seriesToAxisMap_[seriesName] = this.axes_.length - 1;
2046 }
2047 }
2048
2049 // Go through one more time and assign series to an axis defined by another
2050 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2051 for (var seriesName in series) {
2052 if (!series.hasOwnProperty(seriesName)) continue;
2053 var axis = this.attr_("axis", seriesName);
2054 if (typeof(axis) == 'string') {
2055 if (!this.seriesToAxisMap_.hasOwnProperty(axis)) {
2056 this.error("Series " + seriesName + " wants to share a y-axis with " +
2057 "series " + axis + ", which does not define its own axis.");
2058 return null;
2059 }
2060 var idx = this.seriesToAxisMap_[axis];
2061 this.seriesToAxisMap_[seriesName] = idx;
2062 }
2063 }
2064
2065 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2066 // this last so that hiding the first series doesn't destroy the axis
2067 // properties of the primary axis.
2068 var seriesToAxisFiltered = {};
2069 var vis = this.visibility();
2070 for (var i = 1; i < labels.length; i++) {
2071 var s = labels[i];
2072 if (vis[i - 1]) seriesToAxisFiltered[s] = this.seriesToAxisMap_[s];
2073 }
2074 this.seriesToAxisMap_ = seriesToAxisFiltered;
2075 };
2076
2077 /**
2078 * Returns the number of y-axes on the chart.
2079 * @return {Number} the number of axes.
2080 */
2081 Dygraph.prototype.numAxes = function() {
2082 var last_axis = 0;
2083 for (var series in this.seriesToAxisMap_) {
2084 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2085 var idx = this.seriesToAxisMap_[series];
2086 if (idx > last_axis) last_axis = idx;
2087 }
2088 return 1 + last_axis;
2089 };
2090
2091 /**
2092 * Determine the value range and tick marks for each axis.
2093 * @param {Object} extremes A mapping from seriesName -> [low, high]
2094 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2095 */
2096 Dygraph.prototype.computeYAxisRanges_ = function(extremes) {
2097 // Build a map from axis number -> [list of series names]
2098 var seriesForAxis = [];
2099 for (var series in this.seriesToAxisMap_) {
2100 if (!this.seriesToAxisMap_.hasOwnProperty(series)) continue;
2101 var idx = this.seriesToAxisMap_[series];
2102 while (seriesForAxis.length <= idx) seriesForAxis.push([]);
2103 seriesForAxis[idx].push(series);
2104 }
2105
2106 // Compute extreme values, a span and tick marks for each axis.
2107 for (var i = 0; i < this.axes_.length; i++) {
2108 var axis = this.axes_[i];
2109 if (axis.valueWindow) {
2110 // This is only set if the user has zoomed on the y-axis. It is never set
2111 // by a user. It takes precedence over axis.valueRange because, if you set
2112 // valueRange, you'd still expect to be able to pan.
2113 axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]];
2114 } else if (axis.valueRange) {
2115 // This is a user-set value range for this axis.
2116 axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]];
2117 } else {
2118 // Calculate the extremes of extremes.
2119 var series = seriesForAxis[i];
2120 var minY = Infinity; // extremes[series[0]][0];
2121 var maxY = -Infinity; // extremes[series[0]][1];
2122 for (var j = 0; j < series.length; j++) {
2123 minY = Math.min(extremes[series[j]][0], minY);
2124 maxY = Math.max(extremes[series[j]][1], maxY);
2125 }
2126 if (axis.includeZero && minY > 0) minY = 0;
2127
2128 // Add some padding and round up to an integer to be human-friendly.
2129 var span = maxY - minY;
2130 // special case: if we have no sense of scale, use +/-10% of the sole value.
2131 if (span == 0) { span = maxY; }
2132 var maxAxisY = maxY + 0.1 * span;
2133 var minAxisY = minY - 0.1 * span;
2134
2135 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2136 if (!this.attr_("avoidMinZero")) {
2137 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
2138 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
2139 }
2140
2141 if (this.attr_("includeZero")) {
2142 if (maxY < 0) maxAxisY = 0;
2143 if (minY > 0) minAxisY = 0;
2144 }
2145
2146 axis.computedValueRange = [minAxisY, maxAxisY];
2147 }
2148
2149 // Add ticks. By default, all axes inherit the tick positions of the
2150 // primary axis. However, if an axis is specifically marked as having
2151 // independent ticks, then that is permissible as well.
2152 if (i == 0 || axis.independentTicks) {
2153 axis.ticks =
2154 Dygraph.numericTicks(axis.computedValueRange[0],
2155 axis.computedValueRange[1],
2156 this,
2157 axis);
2158 } else {
2159 var p_axis = this.axes_[0];
2160 var p_ticks = p_axis.ticks;
2161 var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0];
2162 var scale = axis.computedValueRange[1] - axis.computedValueRange[0];
2163 var tick_values = [];
2164 for (var i = 0; i < p_ticks.length; i++) {
2165 var y_frac = (p_ticks[i].v - p_axis.computedValueRange[0]) / p_scale;
2166 var y_val = axis.computedValueRange[0] + y_frac * scale;
2167 tick_values.push(y_val);
2168 }
2169
2170 axis.ticks =
2171 Dygraph.numericTicks(axis.computedValueRange[0],
2172 axis.computedValueRange[1],
2173 this, axis, tick_values);
2174 }
2175 }
2176
2177 return [this.axes_, this.seriesToAxisMap_];
2178 };
2179
2180 /**
2181 * Calculates the rolling average of a data set.
2182 * If originalData is [label, val], rolls the average of those.
2183 * If originalData is [label, [, it's interpreted as [value, stddev]
2184 * and the roll is returned in the same form, with appropriately reduced
2185 * stddev for each value.
2186 * Note that this is where fractional input (i.e. '5/10') is converted into
2187 * decimal values.
2188 * @param {Array} originalData The data in the appropriate format (see above)
2189 * @param {Number} rollPeriod The number of days over which to average the data
2190 */
2191 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
2192 if (originalData.length < 2)
2193 return originalData;
2194 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
2195 var rollingData = [];
2196 var sigma = this.attr_("sigma");
2197
2198 if (this.fractions_) {
2199 var num = 0;
2200 var den = 0; // numerator/denominator
2201 var mult = 100.0;
2202 for (var i = 0; i < originalData.length; i++) {
2203 num += originalData[i][1][0];
2204 den += originalData[i][1][1];
2205 if (i - rollPeriod >= 0) {
2206 num -= originalData[i - rollPeriod][1][0];
2207 den -= originalData[i - rollPeriod][1][1];
2208 }
2209
2210 var date = originalData[i][0];
2211 var value = den ? num / den : 0.0;
2212 if (this.attr_("errorBars")) {
2213 if (this.wilsonInterval_) {
2214 // For more details on this confidence interval, see:
2215 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
2216 if (den) {
2217 var p = value < 0 ? 0 : value, n = den;
2218 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
2219 var denom = 1 + sigma * sigma / den;
2220 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
2221 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
2222 rollingData[i] = [date,
2223 [p * mult, (p - low) * mult, (high - p) * mult]];
2224 } else {
2225 rollingData[i] = [date, [0, 0, 0]];
2226 }
2227 } else {
2228 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
2229 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
2230 }
2231 } else {
2232 rollingData[i] = [date, mult * value];
2233 }
2234 }
2235 } else if (this.attr_("customBars")) {
2236 var low = 0;
2237 var mid = 0;
2238 var high = 0;
2239 var count = 0;
2240 for (var i = 0; i < originalData.length; i++) {
2241 var data = originalData[i][1];
2242 var y = data[1];
2243 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
2244
2245 if (y != null && !isNaN(y)) {
2246 low += data[0];
2247 mid += y;
2248 high += data[2];
2249 count += 1;
2250 }
2251 if (i - rollPeriod >= 0) {
2252 var prev = originalData[i - rollPeriod];
2253 if (prev[1][1] != null && !isNaN(prev[1][1])) {
2254 low -= prev[1][0];
2255 mid -= prev[1][1];
2256 high -= prev[1][2];
2257 count -= 1;
2258 }
2259 }
2260 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
2261 1.0 * (mid - low) / count,
2262 1.0 * (high - mid) / count ]];
2263 }
2264 } else {
2265 // Calculate the rolling average for the first rollPeriod - 1 points where
2266 // there is not enough data to roll over the full number of days
2267 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
2268 if (!this.attr_("errorBars")){
2269 if (rollPeriod == 1) {
2270 return originalData;
2271 }
2272
2273 for (var i = 0; i < originalData.length; i++) {
2274 var sum = 0;
2275 var num_ok = 0;
2276 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
2277 var y = originalData[j][1];
2278 if (y == null || isNaN(y)) continue;
2279 num_ok++;
2280 sum += originalData[j][1];
2281 }
2282 if (num_ok) {
2283 rollingData[i] = [originalData[i][0], sum / num_ok];
2284 } else {
2285 rollingData[i] = [originalData[i][0], null];
2286 }
2287 }
2288
2289 } else {
2290 for (var i = 0; i < originalData.length; i++) {
2291 var sum = 0;
2292 var variance = 0;
2293 var num_ok = 0;
2294 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
2295 var y = originalData[j][1][0];
2296 if (y == null || isNaN(y)) continue;
2297 num_ok++;
2298 sum += originalData[j][1][0];
2299 variance += Math.pow(originalData[j][1][1], 2);
2300 }
2301 if (num_ok) {
2302 var stddev = Math.sqrt(variance) / num_ok;
2303 rollingData[i] = [originalData[i][0],
2304 [sum / num_ok, sigma * stddev, sigma * stddev]];
2305 } else {
2306 rollingData[i] = [originalData[i][0], [null, null, null]];
2307 }
2308 }
2309 }
2310 }
2311
2312 return rollingData;
2313 };
2314
2315 /**
2316 * Parses a date, returning the number of milliseconds since epoch. This can be
2317 * passed in as an xValueParser in the Dygraph constructor.
2318 * TODO(danvk): enumerate formats that this understands.
2319 * @param {String} A date in YYYYMMDD format.
2320 * @return {Number} Milliseconds since epoch.
2321 * @public
2322 */
2323 Dygraph.dateParser = function(dateStr, self) {
2324 var dateStrSlashed;
2325 var d;
2326 if (dateStr.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2327 dateStrSlashed = dateStr.replace("-", "/", "g");
2328 while (dateStrSlashed.search("-") != -1) {
2329 dateStrSlashed = dateStrSlashed.replace("-", "/");
2330 }
2331 d = Date.parse(dateStrSlashed);
2332 } else if (dateStr.length == 8) { // e.g. '20090712'
2333 // TODO(danvk): remove support for this format. It's confusing.
2334 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
2335 + "/" + dateStr.substr(6,2);
2336 d = Date.parse(dateStrSlashed);
2337 } else {
2338 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2339 // "2009/07/12 12:34:56"
2340 d = Date.parse(dateStr);
2341 }
2342
2343 if (!d || isNaN(d)) {
2344 self.error("Couldn't parse " + dateStr + " as a date");
2345 }
2346 return d;
2347 };
2348
2349 /**
2350 * Detects the type of the str (date or numeric) and sets the various
2351 * formatting attributes in this.attrs_ based on this type.
2352 * @param {String} str An x value.
2353 * @private
2354 */
2355 Dygraph.prototype.detectTypeFromString_ = function(str) {
2356 var isDate = false;
2357 if (str.indexOf('-') >= 0 ||
2358 str.indexOf('/') >= 0 ||
2359 isNaN(parseFloat(str))) {
2360 isDate = true;
2361 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
2362 // TODO(danvk): remove support for this format.
2363 isDate = true;
2364 }
2365
2366 if (isDate) {
2367 this.attrs_.xValueFormatter = Dygraph.dateString_;
2368 this.attrs_.xValueParser = Dygraph.dateParser;
2369 this.attrs_.xTicker = Dygraph.dateTicker;
2370 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2371 } else {
2372 this.attrs_.xValueFormatter = function(x) { return x; };
2373 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2374 this.attrs_.xTicker = Dygraph.numericTicks;
2375 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
2376 }
2377 };
2378
2379 /**
2380 * Parses a string in a special csv format. We expect a csv file where each
2381 * line is a date point, and the first field in each line is the date string.
2382 * We also expect that all remaining fields represent series.
2383 * if the errorBars attribute is set, then interpret the fields as:
2384 * date, series1, stddev1, series2, stddev2, ...
2385 * @param {Array.<Object>} data See above.
2386 * @private
2387 *
2388 * @return Array.<Object> An array with one entry for each row. These entries
2389 * are an array of cells in that row. The first entry is the parsed x-value for
2390 * the row. The second, third, etc. are the y-values. These can take on one of
2391 * three forms, depending on the CSV and constructor parameters:
2392 * 1. numeric value
2393 * 2. [ value, stddev ]
2394 * 3. [ low value, center value, high value ]
2395 */
2396 Dygraph.prototype.parseCSV_ = function(data) {
2397 var ret = [];
2398 var lines = data.split("\n");
2399
2400 // Use the default delimiter or fall back to a tab if that makes sense.
2401 var delim = this.attr_('delimiter');
2402 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
2403 delim = '\t';
2404 }
2405
2406 var start = 0;
2407 if (this.labelsFromCSV_) {
2408 start = 1;
2409 this.attrs_.labels = lines[0].split(delim);
2410 }
2411
2412 // Parse the x as a float or return null if it's not a number.
2413 var parseFloatOrNull = function(x) {
2414 var val = parseFloat(x);
2415 return isNaN(val) ? null : val;
2416 };
2417
2418 var xParser;
2419 var defaultParserSet = false; // attempt to auto-detect x value type
2420 var expectedCols = this.attr_("labels").length;
2421 var outOfOrder = false;
2422 for (var i = start; i < lines.length; i++) {
2423 var line = lines[i];
2424 if (line.length == 0) continue; // skip blank lines
2425 if (line[0] == '#') continue; // skip comment lines
2426 var inFields = line.split(delim);
2427 if (inFields.length < 2) continue;
2428
2429 var fields = [];
2430 if (!defaultParserSet) {
2431 this.detectTypeFromString_(inFields[0]);
2432 xParser = this.attr_("xValueParser");
2433 defaultParserSet = true;
2434 }
2435 fields[0] = xParser(inFields[0], this);
2436
2437 // If fractions are expected, parse the numbers as "A/B"
2438 if (this.fractions_) {
2439 for (var j = 1; j < inFields.length; j++) {
2440 // TODO(danvk): figure out an appropriate way to flag parse errors.
2441 var vals = inFields[j].split("/");
2442 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2443 }
2444 } else if (this.attr_("errorBars")) {
2445 // If there are error bars, values are (value, stddev) pairs
2446 for (var j = 1; j < inFields.length; j += 2)
2447 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2448 parseFloatOrNull(inFields[j + 1])];
2449 } else if (this.attr_("customBars")) {
2450 // Bars are a low;center;high tuple
2451 for (var j = 1; j < inFields.length; j++) {
2452 var vals = inFields[j].split(";");
2453 fields[j] = [ parseFloatOrNull(vals[0]),
2454 parseFloatOrNull(vals[1]),
2455 parseFloatOrNull(vals[2]) ];
2456 }
2457 } else {
2458 // Values are just numbers
2459 for (var j = 1; j < inFields.length; j++) {
2460 fields[j] = parseFloatOrNull(inFields[j]);
2461 }
2462 }
2463 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2464 outOfOrder = true;
2465 }
2466 ret.push(fields);
2467
2468 if (fields.length != expectedCols) {
2469 this.error("Number of columns in line " + i + " (" + fields.length +
2470 ") does not agree with number of labels (" + expectedCols +
2471 ") " + line);
2472 }
2473 }
2474
2475 if (outOfOrder) {
2476 this.warn("CSV is out of order; order it correctly to speed loading.");
2477 ret.sort(function(a,b) { return a[0] - b[0] });
2478 }
2479
2480 return ret;
2481 };
2482
2483 /**
2484 * The user has provided their data as a pre-packaged JS array. If the x values
2485 * are numeric, this is the same as dygraphs' internal format. If the x values
2486 * are dates, we need to convert them from Date objects to ms since epoch.
2487 * @param {Array.<Object>} data
2488 * @return {Array.<Object>} data with numeric x values.
2489 */
2490 Dygraph.prototype.parseArray_ = function(data) {
2491 // Peek at the first x value to see if it's numeric.
2492 if (data.length == 0) {
2493 this.error("Can't plot empty data set");
2494 return null;
2495 }
2496 if (data[0].length == 0) {
2497 this.error("Data set cannot contain an empty row");
2498 return null;
2499 }
2500
2501 if (this.attr_("labels") == null) {
2502 this.warn("Using default labels. Set labels explicitly via 'labels' " +
2503 "in the options parameter");
2504 this.attrs_.labels = [ "X" ];
2505 for (var i = 1; i < data[0].length; i++) {
2506 this.attrs_.labels.push("Y" + i);
2507 }
2508 }
2509
2510 if (Dygraph.isDateLike(data[0][0])) {
2511 // Some intelligent defaults for a date x-axis.
2512 this.attrs_.xValueFormatter = Dygraph.dateString_;
2513 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2514 this.attrs_.xTicker = Dygraph.dateTicker;
2515
2516 // Assume they're all dates.
2517 var parsedData = Dygraph.clone(data);
2518 for (var i = 0; i < data.length; i++) {
2519 if (parsedData[i].length == 0) {
2520 this.error("Row " + (1 + i) + " of data is empty");
2521 return null;
2522 }
2523 if (parsedData[i][0] == null
2524 || typeof(parsedData[i][0].getTime) != 'function'
2525 || isNaN(parsedData[i][0].getTime())) {
2526 this.error("x value in row " + (1 + i) + " is not a Date");
2527 return null;
2528 }
2529 parsedData[i][0] = parsedData[i][0].getTime();
2530 }
2531 return parsedData;
2532 } else {
2533 // Some intelligent defaults for a numeric x-axis.
2534 this.attrs_.xValueFormatter = function(x) { return x; };
2535 this.attrs_.xTicker = Dygraph.numericTicks;
2536 return data;
2537 }
2538 };
2539
2540 /**
2541 * Parses a DataTable object from gviz.
2542 * The data is expected to have a first column that is either a date or a
2543 * number. All subsequent columns must be numbers. If there is a clear mismatch
2544 * between this.xValueParser_ and the type of the first column, it will be
2545 * fixed. Fills out rawData_.
2546 * @param {Array.<Object>} data See above.
2547 * @private
2548 */
2549 Dygraph.prototype.parseDataTable_ = function(data) {
2550 var cols = data.getNumberOfColumns();
2551 var rows = data.getNumberOfRows();
2552
2553 var indepType = data.getColumnType(0);
2554 if (indepType == 'date' || indepType == 'datetime') {
2555 this.attrs_.xValueFormatter = Dygraph.dateString_;
2556 this.attrs_.xValueParser = Dygraph.dateParser;
2557 this.attrs_.xTicker = Dygraph.dateTicker;
2558 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2559 } else if (indepType == 'number') {
2560 this.attrs_.xValueFormatter = function(x) { return x; };
2561 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
2562 this.attrs_.xTicker = Dygraph.numericTicks;
2563 this.attrs_.xAxisLabelFormatter = this.attrs_.xValueFormatter;
2564 } else {
2565 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2566 "column 1 of DataTable input (Got '" + indepType + "')");
2567 return null;
2568 }
2569
2570 // Array of the column indices which contain data (and not annotations).
2571 var colIdx = [];
2572 var annotationCols = {}; // data index -> [annotation cols]
2573 var hasAnnotations = false;
2574 for (var i = 1; i < cols; i++) {
2575 var type = data.getColumnType(i);
2576 if (type == 'number') {
2577 colIdx.push(i);
2578 } else if (type == 'string' && this.attr_('displayAnnotations')) {
2579 // This is OK -- it's an annotation column.
2580 var dataIdx = colIdx[colIdx.length - 1];
2581 if (!annotationCols.hasOwnProperty(dataIdx)) {
2582 annotationCols[dataIdx] = [i];
2583 } else {
2584 annotationCols[dataIdx].push(i);
2585 }
2586 hasAnnotations = true;
2587 } else {
2588 this.error("Only 'number' is supported as a dependent type with Gviz." +
2589 " 'string' is only supported if displayAnnotations is true");
2590 }
2591 }
2592
2593 // Read column labels
2594 // TODO(danvk): add support back for errorBars
2595 var labels = [data.getColumnLabel(0)];
2596 for (var i = 0; i < colIdx.length; i++) {
2597 labels.push(data.getColumnLabel(colIdx[i]));
2598 if (this.attr_("errorBars")) i += 1;
2599 }
2600 this.attrs_.labels = labels;
2601 cols = labels.length;
2602
2603 var ret = [];
2604 var outOfOrder = false;
2605 var annotations = [];
2606 for (var i = 0; i < rows; i++) {
2607 var row = [];
2608 if (typeof(data.getValue(i, 0)) === 'undefined' ||
2609 data.getValue(i, 0) === null) {
2610 this.warn("Ignoring row " + i +
2611 " of DataTable because of undefined or null first column.");
2612 continue;
2613 }
2614
2615 if (indepType == 'date' || indepType == 'datetime') {
2616 row.push(data.getValue(i, 0).getTime());
2617 } else {
2618 row.push(data.getValue(i, 0));
2619 }
2620 if (!this.attr_("errorBars")) {
2621 for (var j = 0; j < colIdx.length; j++) {
2622 var col = colIdx[j];
2623 row.push(data.getValue(i, col));
2624 if (hasAnnotations &&
2625 annotationCols.hasOwnProperty(col) &&
2626 data.getValue(i, annotationCols[col][0]) != null) {
2627 var ann = {};
2628 ann.series = data.getColumnLabel(col);
2629 ann.xval = row[0];
2630 ann.shortText = String.fromCharCode(65 /* A */ + annotations.length)
2631 ann.text = '';
2632 for (var k = 0; k < annotationCols[col].length; k++) {
2633 if (k) ann.text += "\n";
2634 ann.text += data.getValue(i, annotationCols[col][k]);
2635 }
2636 annotations.push(ann);
2637 }
2638 }
2639 } else {
2640 for (var j = 0; j < cols - 1; j++) {
2641 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
2642 }
2643 }
2644 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
2645 outOfOrder = true;
2646 }
2647 ret.push(row);
2648 }
2649
2650 if (outOfOrder) {
2651 this.warn("DataTable is out of order; order it correctly to speed loading.");
2652 ret.sort(function(a,b) { return a[0] - b[0] });
2653 }
2654 this.rawData_ = ret;
2655
2656 if (annotations.length > 0) {
2657 this.setAnnotations(annotations, true);
2658 }
2659 }
2660
2661 // These functions are all based on MochiKit.
2662 Dygraph.update = function (self, o) {
2663 if (typeof(o) != 'undefined' && o !== null) {
2664 for (var k in o) {
2665 if (o.hasOwnProperty(k)) {
2666 self[k] = o[k];
2667 }
2668 }
2669 }
2670 return self;
2671 };
2672
2673 Dygraph.isArrayLike = function (o) {
2674 var typ = typeof(o);
2675 if (
2676 (typ != 'object' && !(typ == 'function' &&
2677 typeof(o.item) == 'function')) ||
2678 o === null ||
2679 typeof(o.length) != 'number' ||
2680 o.nodeType === 3
2681 ) {
2682 return false;
2683 }
2684 return true;
2685 };
2686
2687 Dygraph.isDateLike = function (o) {
2688 if (typeof(o) != "object" || o === null ||
2689 typeof(o.getTime) != 'function') {
2690 return false;
2691 }
2692 return true;
2693 };
2694
2695 Dygraph.clone = function(o) {
2696 // TODO(danvk): figure out how MochiKit's version works
2697 var r = [];
2698 for (var i = 0; i < o.length; i++) {
2699 if (Dygraph.isArrayLike(o[i])) {
2700 r.push(Dygraph.clone(o[i]));
2701 } else {
2702 r.push(o[i]);
2703 }
2704 }
2705 return r;
2706 };
2707
2708
2709 /**
2710 * Get the CSV data. If it's in a function, call that function. If it's in a
2711 * file, do an XMLHttpRequest to get it.
2712 * @private
2713 */
2714 Dygraph.prototype.start_ = function() {
2715 if (typeof this.file_ == 'function') {
2716 // CSV string. Pretend we got it via XHR.
2717 this.loadedEvent_(this.file_());
2718 } else if (Dygraph.isArrayLike(this.file_)) {
2719 this.rawData_ = this.parseArray_(this.file_);
2720 this.predraw_();
2721 } else if (typeof this.file_ == 'object' &&
2722 typeof this.file_.getColumnRange == 'function') {
2723 // must be a DataTable from gviz.
2724 this.parseDataTable_(this.file_);
2725 this.predraw_();
2726 } else if (typeof this.file_ == 'string') {
2727 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2728 if (this.file_.indexOf('\n') >= 0) {
2729 this.loadedEvent_(this.file_);
2730 } else {
2731 var req = new XMLHttpRequest();
2732 var caller = this;
2733 req.onreadystatechange = function () {
2734 if (req.readyState == 4) {
2735 if (req.status == 200) {
2736 caller.loadedEvent_(req.responseText);
2737 }
2738 }
2739 };
2740
2741 req.open("GET", this.file_, true);
2742 req.send(null);
2743 }
2744 } else {
2745 this.error("Unknown data format: " + (typeof this.file_));
2746 }
2747 };
2748
2749 /**
2750 * Changes various properties of the graph. These can include:
2751 * <ul>
2752 * <li>file: changes the source data for the graph</li>
2753 * <li>errorBars: changes whether the data contains stddev</li>
2754 * </ul>
2755 * @param {Object} attrs The new properties and values
2756 */
2757 Dygraph.prototype.updateOptions = function(attrs) {
2758 // TODO(danvk): this is a mess. Rethink this function.
2759 if ('rollPeriod' in attrs) {
2760 this.rollPeriod_ = attrs.rollPeriod;
2761 }
2762 if ('dateWindow' in attrs) {
2763 this.dateWindow_ = attrs.dateWindow;
2764 }
2765
2766 // TODO(danvk): validate per-series options.
2767 // Supported:
2768 // strokeWidth
2769 // pointSize
2770 // drawPoints
2771 // highlightCircleSize
2772
2773 Dygraph.update(this.user_attrs_, attrs);
2774 Dygraph.update(this.renderOptions_, attrs);
2775
2776 this.labelsFromCSV_ = (this.attr_("labels") == null);
2777
2778 // TODO(danvk): this doesn't match the constructor logic
2779 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
2780 if (attrs['file']) {
2781 this.file_ = attrs['file'];
2782 this.start_();
2783 } else {
2784 this.predraw_();
2785 }
2786 };
2787
2788 /**
2789 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2790 * containing div (which has presumably changed size since the dygraph was
2791 * instantiated. If the width/height are specified, the div will be resized.
2792 *
2793 * This is far more efficient than destroying and re-instantiating a
2794 * Dygraph, since it doesn't have to reparse the underlying data.
2795 *
2796 * @param {Number} width Width (in pixels)
2797 * @param {Number} height Height (in pixels)
2798 */
2799 Dygraph.prototype.resize = function(width, height) {
2800 if (this.resize_lock) {
2801 return;
2802 }
2803 this.resize_lock = true;
2804
2805 if ((width === null) != (height === null)) {
2806 this.warn("Dygraph.resize() should be called with zero parameters or " +
2807 "two non-NULL parameters. Pretending it was zero.");
2808 width = height = null;
2809 }
2810
2811 // TODO(danvk): there should be a clear() method.
2812 this.maindiv_.innerHTML = "";
2813 this.attrs_.labelsDiv = null;
2814
2815 if (width) {
2816 this.maindiv_.style.width = width + "px";
2817 this.maindiv_.style.height = height + "px";
2818 this.width_ = width;
2819 this.height_ = height;
2820 } else {
2821 this.width_ = this.maindiv_.offsetWidth;
2822 this.height_ = this.maindiv_.offsetHeight;
2823 }
2824
2825 this.createInterface_();
2826 this.predraw_();
2827
2828 this.resize_lock = false;
2829 };
2830
2831 /**
2832 * Adjusts the number of days in the rolling average. Updates the graph to
2833 * reflect the new averaging period.
2834 * @param {Number} length Number of days over which to average the data.
2835 */
2836 Dygraph.prototype.adjustRoll = function(length) {
2837 this.rollPeriod_ = length;
2838 this.predraw_();
2839 };
2840
2841 /**
2842 * Returns a boolean array of visibility statuses.
2843 */
2844 Dygraph.prototype.visibility = function() {
2845 // Do lazy-initialization, so that this happens after we know the number of
2846 // data series.
2847 if (!this.attr_("visibility")) {
2848 this.attrs_["visibility"] = [];
2849 }
2850 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
2851 this.attr_("visibility").push(true);
2852 }
2853 return this.attr_("visibility");
2854 };
2855
2856 /**
2857 * Changes the visiblity of a series.
2858 */
2859 Dygraph.prototype.setVisibility = function(num, value) {
2860 var x = this.visibility();
2861 if (num < 0 || num >= x.length) {
2862 this.warn("invalid series number in setVisibility: " + num);
2863 } else {
2864 x[num] = value;
2865 this.predraw_();
2866 }
2867 };
2868
2869 /**
2870 * Update the list of annotations and redraw the chart.
2871 */
2872 Dygraph.prototype.setAnnotations = function(ann, suppressDraw) {
2873 // Only add the annotation CSS rule once we know it will be used.
2874 Dygraph.addAnnotationRule();
2875 this.annotations_ = ann;
2876 this.layout_.setAnnotations(this.annotations_);
2877 if (!suppressDraw) {
2878 this.predraw_();
2879 }
2880 };
2881
2882 /**
2883 * Return the list of annotations.
2884 */
2885 Dygraph.prototype.annotations = function() {
2886 return this.annotations_;
2887 };
2888
2889 /**
2890 * Get the index of a series (column) given its name. The first column is the
2891 * x-axis, so the data series start with index 1.
2892 */
2893 Dygraph.prototype.indexFromSetName = function(name) {
2894 var labels = this.attr_("labels");
2895 for (var i = 0; i < labels.length; i++) {
2896 if (labels[i] == name) return i;
2897 }
2898 return null;
2899 };
2900
2901 Dygraph.addAnnotationRule = function() {
2902 if (Dygraph.addedAnnotationCSS) return;
2903
2904 var rule = "border: 1px solid black; " +
2905 "background-color: white; " +
2906 "text-align: center;";
2907
2908 var styleSheetElement = document.createElement("style");
2909 styleSheetElement.type = "text/css";
2910 document.getElementsByTagName("head")[0].appendChild(styleSheetElement);
2911
2912 // Find the first style sheet that we can access.
2913 // We may not add a rule to a style sheet from another domain for security
2914 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
2915 // adds its own style sheets from google.com.
2916 for (var i = 0; i < document.styleSheets.length; i++) {
2917 if (document.styleSheets[i].disabled) continue;
2918 var mysheet = document.styleSheets[i];
2919 try {
2920 if (mysheet.insertRule) { // Firefox
2921 var idx = mysheet.cssRules ? mysheet.cssRules.length : 0;
2922 mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx);
2923 } else if (mysheet.addRule) { // IE
2924 mysheet.addRule(".dygraphDefaultAnnotation", rule);
2925 }
2926 Dygraph.addedAnnotationCSS = true;
2927 return;
2928 } catch(err) {
2929 // Was likely a security exception.
2930 }
2931 }
2932
2933 this.warn("Unable to add default annotation CSS rule; display may be off.");
2934 }
2935
2936 /**
2937 * Create a new canvas element. This is more complex than a simple
2938 * document.createElement("canvas") because of IE and excanvas.
2939 */
2940 Dygraph.createCanvas = function() {
2941 var canvas = document.createElement("canvas");
2942
2943 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
2944 if (isIE && (typeof(G_vmlCanvasManager) != 'undefined')) {
2945 canvas = G_vmlCanvasManager.initElement(canvas);
2946 }
2947
2948 return canvas;
2949 };
2950
2951
2952 /**
2953 * A wrapper around Dygraph that implements the gviz API.
2954 * @param {Object} container The DOM object the visualization should live in.
2955 */
2956 Dygraph.GVizChart = function(container) {
2957 this.container = container;
2958 }
2959
2960 Dygraph.GVizChart.prototype.draw = function(data, options) {
2961 // Clear out any existing dygraph.
2962 // TODO(danvk): would it make more sense to simply redraw using the current
2963 // date_graph object?
2964 this.container.innerHTML = '';
2965 if (typeof(this.date_graph) != 'undefined') {
2966 this.date_graph.destroy();
2967 }
2968
2969 this.date_graph = new Dygraph(this.container, data, options);
2970 }
2971
2972 /**
2973 * Google charts compatible setSelection
2974 * Only row selection is supported, all points in the row will be highlighted
2975 * @param {Array} array of the selected cells
2976 * @public
2977 */
2978 Dygraph.GVizChart.prototype.setSelection = function(selection_array) {
2979 var row = false;
2980 if (selection_array.length) {
2981 row = selection_array[0].row;
2982 }
2983 this.date_graph.setSelection(row);
2984 }
2985
2986 /**
2987 * Google charts compatible getSelection implementation
2988 * @return {Array} array of the selected cells
2989 * @public
2990 */
2991 Dygraph.GVizChart.prototype.getSelection = function() {
2992 var selection = [];
2993
2994 var row = this.date_graph.getSelection();
2995
2996 if (row < 0) return selection;
2997
2998 col = 1;
2999 for (var i in this.date_graph.layout_.datasets) {
3000 selection.push({row: row, column: col});
3001 col++;
3002 }
3003
3004 return selection;
3005 }
3006
3007 // Older pages may still use this name.
3008 DateGraph = Dygraph;