Merged stacked graph support from iownbey's version and added assorted
[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 labelsKMB: false,
94 labelsKMG2: false,
95
96 strokeWidth: 1.0,
97
98 axisTickSize: 3,
99 axisLabelFontSize: 14,
100 xAxisLabelWidth: 50,
101 yAxisLabelWidth: 50,
102 rightGap: 5,
103
104 showRoller: false,
105 xValueFormatter: Dygraph.dateString_,
106 xValueParser: Dygraph.dateParser,
107 xTicker: Dygraph.dateTicker,
108
109 delimiter: ',',
110
111 logScale: false,
112 sigma: 2.0,
113 errorBars: false,
114 fractions: false,
115 wilsonInterval: true, // only relevant if fractions is true
116 customBars: false,
117 fillGraph: false,
118 fillAlpha: 0.15,
119
120 stackedGraph: false,
121 hideOverlayOnMouseOut: true
122 };
123
124 // Various logging levels.
125 Dygraph.DEBUG = 1;
126 Dygraph.INFO = 2;
127 Dygraph.WARNING = 3;
128 Dygraph.ERROR = 3;
129
130 Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) {
131 // Labels is no longer a constructor parameter, since it's typically set
132 // directly from the data source. It also conains a name for the x-axis,
133 // which the previous constructor form did not.
134 if (labels != null) {
135 var new_labels = ["Date"];
136 for (var i = 0; i < labels.length; i++) new_labels.push(labels[i]);
137 Dygraph.update(attrs, { 'labels': new_labels });
138 }
139 this.__init__(div, file, attrs);
140 };
141
142 /**
143 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
144 * and interaction &lt;canvas&gt; inside of it. See the constructor for details
145 * on the parameters.
146 * @param {String | Function} file Source data
147 * @param {Array.<String>} labels Names of the data series
148 * @param {Object} attrs Miscellaneous other options
149 * @private
150 */
151 Dygraph.prototype.__init__ = function(div, file, attrs) {
152 // Support two-argument constructor
153 if (attrs == null) { attrs = {}; }
154
155 // Copy the important bits into the object
156 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
157 this.maindiv_ = div;
158 this.file_ = file;
159 this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD;
160 this.previousVerticalX_ = -1;
161 this.fractions_ = attrs.fractions || false;
162 this.dateWindow_ = attrs.dateWindow || null;
163 this.valueRange_ = attrs.valueRange || null;
164 this.wilsonInterval_ = attrs.wilsonInterval || true;
165
166 // Clear the div. This ensure that, if multiple dygraphs are passed the same
167 // div, then only one will be drawn.
168 div.innerHTML = "";
169
170 // If the div isn't already sized then inherit from our attrs or
171 // give it a default size.
172 if (div.style.width == '') {
173 div.style.width = attrs.width || Dygraph.DEFAULT_WIDTH + "px";
174 }
175 if (div.style.height == '') {
176 div.style.height = attrs.height || Dygraph.DEFAULT_HEIGHT + "px";
177 }
178 this.width_ = parseInt(div.style.width, 10);
179 this.height_ = parseInt(div.style.height, 10);
180 // The div might have been specified as percent of the current window size,
181 // convert that to an appropriate number of pixels.
182 if (div.style.width.indexOf("%") == div.style.width.length - 1) {
183 // Minus ten pixels keeps scrollbars from showing up for a 100% width div.
184 this.width_ = (this.width_ * self.innerWidth / 100) - 10;
185 }
186 if (div.style.height.indexOf("%") == div.style.height.length - 1) {
187 this.height_ = (this.height_ * self.innerHeight / 100) - 10;
188 }
189
190 if (attrs['stackedGraph']) {
191 attrs['fillGraph'] = true;
192 // TODO(nikhilk): Add any other stackedGraph checks here.
193 }
194
195 // Dygraphs has many options, some of which interact with one another.
196 // To keep track of everything, we maintain two sets of options:
197 //
198 // this.user_attrs_ only options explicitly set by the user.
199 // this.attrs_ defaults, options derived from user_attrs_, data.
200 //
201 // Options are then accessed this.attr_('attr'), which first looks at
202 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
203 // defaults without overriding behavior that the user specifically asks for.
204 this.user_attrs_ = {};
205 Dygraph.update(this.user_attrs_, attrs);
206
207 this.attrs_ = {};
208 Dygraph.update(this.attrs_, Dygraph.DEFAULT_ATTRS);
209
210 // Make a note of whether labels will be pulled from the CSV file.
211 this.labelsFromCSV_ = (this.attr_("labels") == null);
212
213 // Create the containing DIV and other interactive elements
214 this.createInterface_();
215
216 this.start_();
217 };
218
219 Dygraph.prototype.attr_ = function(name) {
220 if (typeof(this.user_attrs_[name]) != 'undefined') {
221 return this.user_attrs_[name];
222 } else if (typeof(this.attrs_[name]) != 'undefined') {
223 return this.attrs_[name];
224 } else {
225 return null;
226 }
227 };
228
229 // TODO(danvk): any way I can get the line numbers to be this.warn call?
230 Dygraph.prototype.log = function(severity, message) {
231 if (typeof(console) != 'undefined') {
232 switch (severity) {
233 case Dygraph.DEBUG:
234 console.debug('dygraphs: ' + message);
235 break;
236 case Dygraph.INFO:
237 console.info('dygraphs: ' + message);
238 break;
239 case Dygraph.WARNING:
240 console.warn('dygraphs: ' + message);
241 break;
242 case Dygraph.ERROR:
243 console.error('dygraphs: ' + message);
244 break;
245 }
246 }
247 }
248 Dygraph.prototype.info = function(message) {
249 this.log(Dygraph.INFO, message);
250 }
251 Dygraph.prototype.warn = function(message) {
252 this.log(Dygraph.WARNING, message);
253 }
254 Dygraph.prototype.error = function(message) {
255 this.log(Dygraph.ERROR, message);
256 }
257
258 /**
259 * Returns the current rolling period, as set by the user or an option.
260 * @return {Number} The number of days in the rolling window
261 */
262 Dygraph.prototype.rollPeriod = function() {
263 return this.rollPeriod_;
264 };
265
266 Dygraph.addEvent = function(el, evt, fn) {
267 var normed_fn = function(e) {
268 if (!e) var e = window.event;
269 fn(e);
270 };
271 if (window.addEventListener) { // Mozilla, Netscape, Firefox
272 el.addEventListener(evt, normed_fn, false);
273 } else { // IE
274 el.attachEvent('on' + evt, normed_fn);
275 }
276 };
277
278 /**
279 * Generates interface elements for the Dygraph: a containing div, a div to
280 * display the current point, and a textbox to adjust the rolling average
281 * period. Also creates the Renderer/Layout elements.
282 * @private
283 */
284 Dygraph.prototype.createInterface_ = function() {
285 // Create the all-enclosing graph div
286 var enclosing = this.maindiv_;
287
288 this.graphDiv = document.createElement("div");
289 this.graphDiv.style.width = this.width_ + "px";
290 this.graphDiv.style.height = this.height_ + "px";
291 enclosing.appendChild(this.graphDiv);
292
293 // Create the canvas for interactive parts of the chart.
294 // this.canvas_ = document.createElement("canvas");
295 this.canvas_ = Dygraph.createCanvas();
296 this.canvas_.style.position = "absolute";
297 this.canvas_.width = this.width_;
298 this.canvas_.height = this.height_;
299 this.canvas_.style.width = this.width_ + "px"; // for IE
300 this.canvas_.style.height = this.height_ + "px"; // for IE
301 this.graphDiv.appendChild(this.canvas_);
302
303 // ... and for static parts of the chart.
304 this.hidden_ = this.createPlotKitCanvas_(this.canvas_);
305
306 var dygraph = this;
307 Dygraph.addEvent(this.hidden_, 'mousemove', function(e) {
308 dygraph.mouseMove_(e);
309 });
310 Dygraph.addEvent(this.hidden_, 'mouseout', function(e) {
311 dygraph.mouseOut_(e);
312 });
313
314 // Create the grapher
315 // TODO(danvk): why does the Layout need its own set of options?
316 this.layoutOptions_ = { 'xOriginIsZero': false };
317 Dygraph.update(this.layoutOptions_, this.attrs_);
318 Dygraph.update(this.layoutOptions_, this.user_attrs_);
319 Dygraph.update(this.layoutOptions_, {
320 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
321
322 this.layout_ = new DygraphLayout(this, this.layoutOptions_);
323
324 // TODO(danvk): why does the Renderer need its own set of options?
325 this.renderOptions_ = { colorScheme: this.colors_,
326 strokeColor: null,
327 axisLineWidth: Dygraph.AXIS_LINE_WIDTH };
328 Dygraph.update(this.renderOptions_, this.attrs_);
329 Dygraph.update(this.renderOptions_, this.user_attrs_);
330 this.plotter_ = new DygraphCanvasRenderer(this,
331 this.hidden_, this.layout_,
332 this.renderOptions_);
333
334 this.createStatusMessage_();
335 this.createRollInterface_();
336 this.createDragInterface_();
337 }
338
339 /**
340 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
341 * this particular canvas. All Dygraph work is done on this.canvas_.
342 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
343 * @return {Object} The newly-created canvas
344 * @private
345 */
346 Dygraph.prototype.createPlotKitCanvas_ = function(canvas) {
347 // var h = document.createElement("canvas");
348 var h = Dygraph.createCanvas();
349 h.style.position = "absolute";
350 h.style.top = canvas.style.top;
351 h.style.left = canvas.style.left;
352 h.width = this.width_;
353 h.height = this.height_;
354 h.style.width = this.width_ + "px"; // for IE
355 h.style.height = this.height_ + "px"; // for IE
356 this.graphDiv.appendChild(h);
357 return h;
358 };
359
360 // Taken from MochiKit.Color
361 Dygraph.hsvToRGB = function (hue, saturation, value) {
362 var red;
363 var green;
364 var blue;
365 if (saturation === 0) {
366 red = value;
367 green = value;
368 blue = value;
369 } else {
370 var i = Math.floor(hue * 6);
371 var f = (hue * 6) - i;
372 var p = value * (1 - saturation);
373 var q = value * (1 - (saturation * f));
374 var t = value * (1 - (saturation * (1 - f)));
375 switch (i) {
376 case 1: red = q; green = value; blue = p; break;
377 case 2: red = p; green = value; blue = t; break;
378 case 3: red = p; green = q; blue = value; break;
379 case 4: red = t; green = p; blue = value; break;
380 case 5: red = value; green = p; blue = q; break;
381 case 6: // fall through
382 case 0: red = value; green = t; blue = p; break;
383 }
384 }
385 red = Math.floor(255 * red + 0.5);
386 green = Math.floor(255 * green + 0.5);
387 blue = Math.floor(255 * blue + 0.5);
388 return 'rgb(' + red + ',' + green + ',' + blue + ')';
389 };
390
391
392 /**
393 * Generate a set of distinct colors for the data series. This is done with a
394 * color wheel. Saturation/Value are customizable, and the hue is
395 * equally-spaced around the color wheel. If a custom set of colors is
396 * specified, that is used instead.
397 * @private
398 */
399 Dygraph.prototype.setColors_ = function() {
400 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
401 // away with this.renderOptions_.
402 var num = this.attr_("labels").length - 1;
403 this.colors_ = [];
404 var colors = this.attr_('colors');
405 if (!colors) {
406 var sat = this.attr_('colorSaturation') || 1.0;
407 var val = this.attr_('colorValue') || 0.5;
408 for (var i = 1; i <= num; i++) {
409 if (!this.visibility()[i-1]) continue;
410 // alternate colors for high contrast.
411 var idx = i - parseInt(i % 2 ? i / 2 : (i - num)/2, 10);
412 var hue = (1.0 * idx/ (1 + num));
413 this.colors_.push(Dygraph.hsvToRGB(hue, sat, val));
414 }
415 } else {
416 for (var i = 0; i < num; i++) {
417 if (!this.visibility()[i]) continue;
418 var colorStr = colors[i % colors.length];
419 this.colors_.push(colorStr);
420 }
421 }
422
423 // TODO(danvk): update this w/r/t/ the new options system.
424 this.renderOptions_.colorScheme = this.colors_;
425 Dygraph.update(this.plotter_.options, this.renderOptions_);
426 Dygraph.update(this.layoutOptions_, this.user_attrs_);
427 Dygraph.update(this.layoutOptions_, this.attrs_);
428 }
429
430 /**
431 * Return the list of colors. This is either the list of colors passed in the
432 * attributes, or the autogenerated list of rgb(r,g,b) strings.
433 * @return {Array<string>} The list of colors.
434 */
435 Dygraph.prototype.getColors = function() {
436 return this.colors_;
437 };
438
439 // The following functions are from quirksmode.org
440 // http://www.quirksmode.org/js/findpos.html
441 Dygraph.findPosX = function(obj) {
442 var curleft = 0;
443 if (obj.offsetParent) {
444 while (obj.offsetParent) {
445 curleft += obj.offsetLeft;
446 obj = obj.offsetParent;
447 }
448 }
449 else if (obj.x)
450 curleft += obj.x;
451 return curleft;
452 };
453
454 Dygraph.findPosY = function(obj) {
455 var curtop = 0;
456 if (obj.offsetParent) {
457 while (obj.offsetParent) {
458 curtop += obj.offsetTop;
459 obj = obj.offsetParent;
460 }
461 }
462 else if (obj.y)
463 curtop += obj.y;
464 return curtop;
465 };
466
467 /**
468 * Create the div that contains information on the selected point(s)
469 * This goes in the top right of the canvas, unless an external div has already
470 * been specified.
471 * @private
472 */
473 Dygraph.prototype.createStatusMessage_ = function(){
474 if (!this.attr_("labelsDiv")) {
475 var divWidth = this.attr_('labelsDivWidth');
476 var messagestyle = {
477 "position": "absolute",
478 "fontSize": "14px",
479 "zIndex": 10,
480 "width": divWidth + "px",
481 "top": "0px",
482 "left": (this.width_ - divWidth - 2) + "px",
483 "background": "white",
484 "textAlign": "left",
485 "overflow": "hidden"};
486 Dygraph.update(messagestyle, this.attr_('labelsDivStyles'));
487 var div = document.createElement("div");
488 for (var name in messagestyle) {
489 if (messagestyle.hasOwnProperty(name)) {
490 div.style[name] = messagestyle[name];
491 }
492 }
493 this.graphDiv.appendChild(div);
494 this.attrs_.labelsDiv = div;
495 }
496 };
497
498 /**
499 * Create the text box to adjust the averaging period
500 * @return {Object} The newly-created text box
501 * @private
502 */
503 Dygraph.prototype.createRollInterface_ = function() {
504 var display = this.attr_('showRoller') ? "block" : "none";
505 var textAttr = { "position": "absolute",
506 "zIndex": 10,
507 "top": (this.plotter_.area.h - 25) + "px",
508 "left": (this.plotter_.area.x + 1) + "px",
509 "display": display
510 };
511 var roller = document.createElement("input");
512 roller.type = "text";
513 roller.size = "2";
514 roller.value = this.rollPeriod_;
515 for (var name in textAttr) {
516 if (textAttr.hasOwnProperty(name)) {
517 roller.style[name] = textAttr[name];
518 }
519 }
520
521 var pa = this.graphDiv;
522 pa.appendChild(roller);
523 var dygraph = this;
524 roller.onchange = function() { dygraph.adjustRoll(roller.value); };
525 return roller;
526 };
527
528 // These functions are taken from MochiKit.Signal
529 Dygraph.pageX = function(e) {
530 if (e.pageX) {
531 return (!e.pageX || e.pageX < 0) ? 0 : e.pageX;
532 } else {
533 var de = document;
534 var b = document.body;
535 return e.clientX +
536 (de.scrollLeft || b.scrollLeft) -
537 (de.clientLeft || 0);
538 }
539 };
540
541 Dygraph.pageY = function(e) {
542 if (e.pageY) {
543 return (!e.pageY || e.pageY < 0) ? 0 : e.pageY;
544 } else {
545 var de = document;
546 var b = document.body;
547 return e.clientY +
548 (de.scrollTop || b.scrollTop) -
549 (de.clientTop || 0);
550 }
551 };
552
553 /**
554 * Set up all the mouse handlers needed to capture dragging behavior for zoom
555 * events.
556 * @private
557 */
558 Dygraph.prototype.createDragInterface_ = function() {
559 var self = this;
560
561 // Tracks whether the mouse is down right now
562 var isZooming = false;
563 var isPanning = false;
564 var dragStartX = null;
565 var dragStartY = null;
566 var dragEndX = null;
567 var dragEndY = null;
568 var prevEndX = null;
569 var draggingDate = null;
570 var dateRange = null;
571
572 // Utility function to convert page-wide coordinates to canvas coords
573 var px = 0;
574 var py = 0;
575 var getX = function(e) { return Dygraph.pageX(e) - px };
576 var getY = function(e) { return Dygraph.pageX(e) - py };
577
578 // Draw zoom rectangles when the mouse is down and the user moves around
579 Dygraph.addEvent(this.hidden_, 'mousemove', function(event) {
580 if (isZooming) {
581 dragEndX = getX(event);
582 dragEndY = getY(event);
583
584 self.drawZoomRect_(dragStartX, dragEndX, prevEndX);
585 prevEndX = dragEndX;
586 } else if (isPanning) {
587 dragEndX = getX(event);
588 dragEndY = getY(event);
589
590 // Want to have it so that:
591 // 1. draggingDate appears at dragEndX
592 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
593
594 self.dateWindow_[0] = draggingDate - (dragEndX / self.width_) * dateRange;
595 self.dateWindow_[1] = self.dateWindow_[0] + dateRange;
596 self.drawGraph_(self.rawData_);
597 }
598 });
599
600 // Track the beginning of drag events
601 Dygraph.addEvent(this.hidden_, 'mousedown', function(event) {
602 px = Dygraph.findPosX(self.canvas_);
603 py = Dygraph.findPosY(self.canvas_);
604 dragStartX = getX(event);
605 dragStartY = getY(event);
606
607 if (event.altKey || event.shiftKey) {
608 if (!self.dateWindow_) return; // have to be zoomed in to pan.
609 isPanning = true;
610 dateRange = self.dateWindow_[1] - self.dateWindow_[0];
611 draggingDate = (dragStartX / self.width_) * dateRange +
612 self.dateWindow_[0];
613 } else {
614 isZooming = true;
615 }
616 });
617
618 // If the user releases the mouse button during a drag, but not over the
619 // canvas, then it doesn't count as a zooming action.
620 Dygraph.addEvent(document, 'mouseup', function(event) {
621 if (isZooming || isPanning) {
622 isZooming = false;
623 dragStartX = null;
624 dragStartY = null;
625 }
626
627 if (isPanning) {
628 isPanning = false;
629 draggingDate = null;
630 dateRange = null;
631 }
632 });
633
634 // Temporarily cancel the dragging event when the mouse leaves the graph
635 Dygraph.addEvent(this.hidden_, 'mouseout', function(event) {
636 if (isZooming) {
637 dragEndX = null;
638 dragEndY = null;
639 }
640 });
641
642 // If the mouse is released on the canvas during a drag event, then it's a
643 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
644 Dygraph.addEvent(this.hidden_, 'mouseup', function(event) {
645 if (isZooming) {
646 isZooming = false;
647 dragEndX = getX(event);
648 dragEndY = getY(event);
649 var regionWidth = Math.abs(dragEndX - dragStartX);
650 var regionHeight = Math.abs(dragEndY - dragStartY);
651
652 if (regionWidth < 2 && regionHeight < 2 &&
653 self.attr_('clickCallback') != null &&
654 self.lastx_ != undefined) {
655 // TODO(danvk): pass along more info about the points.
656 self.attr_('clickCallback')(event, self.lastx_, self.selPoints_);
657 }
658
659 if (regionWidth >= 10) {
660 self.doZoom_(Math.min(dragStartX, dragEndX),
661 Math.max(dragStartX, dragEndX));
662 } else {
663 self.canvas_.getContext("2d").clearRect(0, 0,
664 self.canvas_.width,
665 self.canvas_.height);
666 }
667
668 dragStartX = null;
669 dragStartY = null;
670 }
671
672 if (isPanning) {
673 isPanning = false;
674 draggingDate = null;
675 dateRange = null;
676 }
677 });
678
679 // Double-clicking zooms back out
680 Dygraph.addEvent(this.hidden_, 'dblclick', function(event) {
681 if (self.dateWindow_ == null) return;
682 self.dateWindow_ = null;
683 self.drawGraph_(self.rawData_);
684 var minDate = self.rawData_[0][0];
685 var maxDate = self.rawData_[self.rawData_.length - 1][0];
686 if (self.attr_("zoomCallback")) {
687 self.attr_("zoomCallback")(minDate, maxDate);
688 }
689 });
690 };
691
692 /**
693 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
694 * up any previous zoom rectangles that were drawn. This could be optimized to
695 * avoid extra redrawing, but it's tricky to avoid interactions with the status
696 * dots.
697 * @param {Number} startX The X position where the drag started, in canvas
698 * coordinates.
699 * @param {Number} endX The current X position of the drag, in canvas coords.
700 * @param {Number} prevEndX The value of endX on the previous call to this
701 * function. Used to avoid excess redrawing
702 * @private
703 */
704 Dygraph.prototype.drawZoomRect_ = function(startX, endX, prevEndX) {
705 var ctx = this.canvas_.getContext("2d");
706
707 // Clean up from the previous rect if necessary
708 if (prevEndX) {
709 ctx.clearRect(Math.min(startX, prevEndX), 0,
710 Math.abs(startX - prevEndX), this.height_);
711 }
712
713 // Draw a light-grey rectangle to show the new viewing area
714 if (endX && startX) {
715 ctx.fillStyle = "rgba(128,128,128,0.33)";
716 ctx.fillRect(Math.min(startX, endX), 0,
717 Math.abs(endX - startX), this.height_);
718 }
719 };
720
721 /**
722 * Zoom to something containing [lowX, highX]. These are pixel coordinates
723 * in the canvas. The exact zoom window may be slightly larger if there are no
724 * data points near lowX or highX. This function redraws the graph.
725 * @param {Number} lowX The leftmost pixel value that should be visible.
726 * @param {Number} highX The rightmost pixel value that should be visible.
727 * @private
728 */
729 Dygraph.prototype.doZoom_ = function(lowX, highX) {
730 // Find the earliest and latest dates contained in this canvasx range.
731 var points = this.layout_.points;
732 var minDate = null;
733 var maxDate = null;
734 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
735 for (var i = 0; i < points.length; i++) {
736 var cx = points[i].canvasx;
737 var x = points[i].xval;
738 if (cx < lowX && (minDate == null || x > minDate)) minDate = x;
739 if (cx > highX && (maxDate == null || x < maxDate)) maxDate = x;
740 }
741 // Use the extremes if either is missing
742 if (minDate == null) minDate = points[0].xval;
743 if (maxDate == null) maxDate = points[points.length-1].xval;
744
745 this.dateWindow_ = [minDate, maxDate];
746 this.drawGraph_(this.rawData_);
747 if (this.attr_("zoomCallback")) {
748 this.attr_("zoomCallback")(minDate, maxDate);
749 }
750 };
751
752 /**
753 * When the mouse moves in the canvas, display information about a nearby data
754 * point and draw dots over those points in the data series. This function
755 * takes care of cleanup of previously-drawn dots.
756 * @param {Object} event The mousemove event from the browser.
757 * @private
758 */
759 Dygraph.prototype.mouseMove_ = function(event) {
760 var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.hidden_);
761 var points = this.layout_.points;
762
763 var lastx = -1;
764 var lasty = -1;
765
766 // Loop through all the points and find the date nearest to our current
767 // location.
768 var minDist = 1e+100;
769 var idx = -1;
770 for (var i = 0; i < points.length; i++) {
771 var dist = Math.abs(points[i].canvasx - canvasx);
772 if (dist > minDist) break;
773 minDist = dist;
774 idx = i;
775 }
776 if (idx >= 0) lastx = points[idx].xval;
777 // Check that you can really highlight the last day's data
778 if (canvasx > points[points.length-1].canvasx)
779 lastx = points[points.length-1].xval;
780
781 // Extract the points we've selected
782 this.selPoints_ = [];
783 for (var i = 0; i < points.length; i++) {
784 if (points[i].xval == lastx) {
785 this.selPoints_.push(points[i]);
786 }
787 }
788
789 if (this.attr_("highlightCallback")) {
790 var callbackPoints = this.selPoints_.map(
791 function(p) { return {xval: p.xval, yval: p.yval, name: p.name} });
792 if (this.attr_("stackedGraph")) {
793 // "unstack" the points.
794 var cumulative_sum = 0;
795 for (var j = callbackPoints.length - 1; j >= 0; j--) {
796 callbackPoints[j].yval -= cumulative_sum;
797 cumulative_sum += callbackPoints[j].yval;
798 }
799 }
800
801 this.attr_("highlightCallback")(event, lastx, callbackPoints);
802 }
803
804 // Clear the previously drawn vertical, if there is one
805 var circleSize = this.attr_('highlightCircleSize');
806 var ctx = this.canvas_.getContext("2d");
807 if (this.previousVerticalX_ >= 0) {
808 var px = this.previousVerticalX_;
809 ctx.clearRect(px - circleSize - 1, 0, 2 * circleSize + 2, this.height_);
810 }
811
812 var isOK = function(x) { return x && !isNaN(x); };
813
814 if (this.selPoints_.length > 0) {
815 var canvasx = this.selPoints_[0].canvasx;
816
817 // Set the status message to indicate the selected point(s)
818 var replace = this.attr_('xValueFormatter')(lastx, this) + ":";
819 var clen = this.colors_.length;
820 for (var i = 0; i < this.selPoints_.length; i++) {
821 if (!isOK(this.selPoints_[i].canvasy)) continue;
822 if (this.attr_("labelsSeparateLines")) {
823 replace += "<br/>";
824 }
825 var point = this.selPoints_[i];
826 var c = new RGBColor(this.colors_[i%clen]);
827 replace += " <b><font color='" + c.toHex() + "'>"
828 + point.name + "</font></b>:"
829 + this.round_(point.yval, 2);
830 }
831 this.attr_("labelsDiv").innerHTML = replace;
832
833 // Save last x position for callbacks.
834 this.lastx_ = lastx;
835
836 // Draw colored circles over the center of each selected point
837 ctx.save();
838 for (var i = 0; i < this.selPoints_.length; i++) {
839 if (!isOK(this.selPoints_[i%clen].canvasy)) continue;
840 ctx.beginPath();
841 ctx.fillStyle = this.colors_[i%clen];
842 ctx.arc(canvasx, this.selPoints_[i%clen].canvasy, circleSize,
843 0, 2 * Math.PI, false);
844 ctx.fill();
845 }
846 ctx.restore();
847
848 this.previousVerticalX_ = canvasx;
849 }
850 };
851
852 /**
853 * The mouse has left the canvas. Clear out whatever artifacts remain
854 * @param {Object} event the mouseout event from the browser.
855 * @private
856 */
857 Dygraph.prototype.mouseOut_ = function(event) {
858 if (this.attr_("hideOverlayOnMouseOut")) {
859 // Get rid of the overlay data
860 var ctx = this.canvas_.getContext("2d");
861 ctx.clearRect(0, 0, this.width_, this.height_);
862 this.attr_("labelsDiv").innerHTML = "";
863 }
864 };
865
866 Dygraph.zeropad = function(x) {
867 if (x < 10) return "0" + x; else return "" + x;
868 }
869
870 /**
871 * Return a string version of the hours, minutes and seconds portion of a date.
872 * @param {Number} date The JavaScript date (ms since epoch)
873 * @return {String} A time of the form "HH:MM:SS"
874 * @private
875 */
876 Dygraph.prototype.hmsString_ = function(date) {
877 var zeropad = Dygraph.zeropad;
878 var d = new Date(date);
879 if (d.getSeconds()) {
880 return zeropad(d.getHours()) + ":" +
881 zeropad(d.getMinutes()) + ":" +
882 zeropad(d.getSeconds());
883 } else if (d.getMinutes()) {
884 return zeropad(d.getHours()) + ":" + zeropad(d.getMinutes());
885 } else {
886 return zeropad(d.getHours());
887 }
888 }
889
890 /**
891 * Convert a JS date (millis since epoch) to YYYY/MM/DD
892 * @param {Number} date The JavaScript date (ms since epoch)
893 * @return {String} A date of the form "YYYY/MM/DD"
894 * @private
895 * TODO(danvk): why is this part of the prototype?
896 */
897 Dygraph.dateString_ = function(date, self) {
898 var zeropad = Dygraph.zeropad;
899 var d = new Date(date);
900
901 // Get the year:
902 var year = "" + d.getFullYear();
903 // Get a 0 padded month string
904 var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh
905 // Get a 0 padded day string
906 var day = zeropad(d.getDate());
907
908 var ret = "";
909 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
910 if (frac) ret = " " + self.hmsString_(date);
911
912 return year + "/" + month + "/" + day + ret;
913 };
914
915 /**
916 * Round a number to the specified number of digits past the decimal point.
917 * @param {Number} num The number to round
918 * @param {Number} places The number of decimals to which to round
919 * @return {Number} The rounded number
920 * @private
921 */
922 Dygraph.prototype.round_ = function(num, places) {
923 var shift = Math.pow(10, places);
924 return Math.round(num * shift)/shift;
925 };
926
927 /**
928 * Fires when there's data available to be graphed.
929 * @param {String} data Raw CSV data to be plotted
930 * @private
931 */
932 Dygraph.prototype.loadedEvent_ = function(data) {
933 this.rawData_ = this.parseCSV_(data);
934 this.drawGraph_(this.rawData_);
935 };
936
937 Dygraph.prototype.months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
938 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
939 Dygraph.prototype.quarters = ["Jan", "Apr", "Jul", "Oct"];
940
941 /**
942 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
943 * @private
944 */
945 Dygraph.prototype.addXTicks_ = function() {
946 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
947 var startDate, endDate;
948 if (this.dateWindow_) {
949 startDate = this.dateWindow_[0];
950 endDate = this.dateWindow_[1];
951 } else {
952 startDate = this.rawData_[0][0];
953 endDate = this.rawData_[this.rawData_.length - 1][0];
954 }
955
956 var xTicks = this.attr_('xTicker')(startDate, endDate, this);
957 this.layout_.updateOptions({xTicks: xTicks});
958 };
959
960 // Time granularity enumeration
961 Dygraph.SECONDLY = 0;
962 Dygraph.TWO_SECONDLY = 1;
963 Dygraph.FIVE_SECONDLY = 2;
964 Dygraph.TEN_SECONDLY = 3;
965 Dygraph.THIRTY_SECONDLY = 4;
966 Dygraph.MINUTELY = 5;
967 Dygraph.TWO_MINUTELY = 6;
968 Dygraph.FIVE_MINUTELY = 7;
969 Dygraph.TEN_MINUTELY = 8;
970 Dygraph.THIRTY_MINUTELY = 9;
971 Dygraph.HOURLY = 10;
972 Dygraph.TWO_HOURLY = 11;
973 Dygraph.SIX_HOURLY = 12;
974 Dygraph.DAILY = 13;
975 Dygraph.WEEKLY = 14;
976 Dygraph.MONTHLY = 15;
977 Dygraph.QUARTERLY = 16;
978 Dygraph.BIANNUAL = 17;
979 Dygraph.ANNUAL = 18;
980 Dygraph.DECADAL = 19;
981 Dygraph.NUM_GRANULARITIES = 20;
982
983 Dygraph.SHORT_SPACINGS = [];
984 Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY] = 1000 * 1;
985 Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY] = 1000 * 2;
986 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY] = 1000 * 5;
987 Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY] = 1000 * 10;
988 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY] = 1000 * 30;
989 Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY] = 1000 * 60;
990 Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY] = 1000 * 60 * 2;
991 Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY] = 1000 * 60 * 5;
992 Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY] = 1000 * 60 * 10;
993 Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY] = 1000 * 60 * 30;
994 Dygraph.SHORT_SPACINGS[Dygraph.HOURLY] = 1000 * 3600;
995 Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY] = 1000 * 3600 * 2;
996 Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY] = 1000 * 3600 * 6;
997 Dygraph.SHORT_SPACINGS[Dygraph.DAILY] = 1000 * 86400;
998 Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY] = 1000 * 604800;
999
1000 // NumXTicks()
1001 //
1002 // If we used this time granularity, how many ticks would there be?
1003 // This is only an approximation, but it's generally good enough.
1004 //
1005 Dygraph.prototype.NumXTicks = function(start_time, end_time, granularity) {
1006 if (granularity < Dygraph.MONTHLY) {
1007 // Generate one tick mark for every fixed interval of time.
1008 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1009 return Math.floor(0.5 + 1.0 * (end_time - start_time) / spacing);
1010 } else {
1011 var year_mod = 1; // e.g. to only print one point every 10 years.
1012 var num_months = 12;
1013 if (granularity == Dygraph.QUARTERLY) num_months = 3;
1014 if (granularity == Dygraph.BIANNUAL) num_months = 2;
1015 if (granularity == Dygraph.ANNUAL) num_months = 1;
1016 if (granularity == Dygraph.DECADAL) { num_months = 1; year_mod = 10; }
1017
1018 var msInYear = 365.2524 * 24 * 3600 * 1000;
1019 var num_years = 1.0 * (end_time - start_time) / msInYear;
1020 return Math.floor(0.5 + 1.0 * num_years * num_months / year_mod);
1021 }
1022 };
1023
1024 // GetXAxis()
1025 //
1026 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1027 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1028 //
1029 // Returns an array containing {v: millis, label: label} dictionaries.
1030 //
1031 Dygraph.prototype.GetXAxis = function(start_time, end_time, granularity) {
1032 var ticks = [];
1033 if (granularity < Dygraph.MONTHLY) {
1034 // Generate one tick mark for every fixed interval of time.
1035 var spacing = Dygraph.SHORT_SPACINGS[granularity];
1036 var format = '%d%b'; // e.g. "1Jan"
1037
1038 // Find a time less than start_time which occurs on a "nice" time boundary
1039 // for this granularity.
1040 var g = spacing / 1000;
1041 var d = new Date(start_time);
1042 if (g <= 60) { // seconds
1043 var x = d.getSeconds(); d.setSeconds(x - x % g);
1044 } else {
1045 d.setSeconds(0);
1046 g /= 60;
1047 if (g <= 60) { // minutes
1048 var x = d.getMinutes(); d.setMinutes(x - x % g);
1049 } else {
1050 d.setMinutes(0);
1051 g /= 60;
1052
1053 if (g <= 24) { // days
1054 var x = d.getHours(); d.setHours(x - x % g);
1055 } else {
1056 d.setHours(0);
1057 g /= 24;
1058
1059 if (g == 7) { // one week
1060 d.setDate(d.getDate() - d.getDay());
1061 }
1062 }
1063 }
1064 }
1065 start_time = d.getTime();
1066
1067 for (var t = start_time; t <= end_time; t += spacing) {
1068 var d = new Date(t);
1069 var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
1070 if (frac == 0 || granularity >= Dygraph.DAILY) {
1071 // the extra hour covers DST problems.
1072 ticks.push({ v:t, label: new Date(t + 3600*1000).strftime(format) });
1073 } else {
1074 ticks.push({ v:t, label: this.hmsString_(t) });
1075 }
1076 }
1077 } else {
1078 // Display a tick mark on the first of a set of months of each year.
1079 // Years get a tick mark iff y % year_mod == 0. This is useful for
1080 // displaying a tick mark once every 10 years, say, on long time scales.
1081 var months;
1082 var year_mod = 1; // e.g. to only print one point every 10 years.
1083
1084 if (granularity == Dygraph.MONTHLY) {
1085 months = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1086 } else if (granularity == Dygraph.QUARTERLY) {
1087 months = [ 0, 3, 6, 9 ];
1088 } else if (granularity == Dygraph.BIANNUAL) {
1089 months = [ 0, 6 ];
1090 } else if (granularity == Dygraph.ANNUAL) {
1091 months = [ 0 ];
1092 } else if (granularity == Dygraph.DECADAL) {
1093 months = [ 0 ];
1094 year_mod = 10;
1095 }
1096
1097 var start_year = new Date(start_time).getFullYear();
1098 var end_year = new Date(end_time).getFullYear();
1099 var zeropad = Dygraph.zeropad;
1100 for (var i = start_year; i <= end_year; i++) {
1101 if (i % year_mod != 0) continue;
1102 for (var j = 0; j < months.length; j++) {
1103 var date_str = i + "/" + zeropad(1 + months[j]) + "/01";
1104 var t = Date.parse(date_str);
1105 if (t < start_time || t > end_time) continue;
1106 ticks.push({ v:t, label: new Date(t).strftime('%b %y') });
1107 }
1108 }
1109 }
1110
1111 return ticks;
1112 };
1113
1114
1115 /**
1116 * Add ticks to the x-axis based on a date range.
1117 * @param {Number} startDate Start of the date window (millis since epoch)
1118 * @param {Number} endDate End of the date window (millis since epoch)
1119 * @return {Array.<Object>} Array of {label, value} tuples.
1120 * @public
1121 */
1122 Dygraph.dateTicker = function(startDate, endDate, self) {
1123 var chosen = -1;
1124 for (var i = 0; i < Dygraph.NUM_GRANULARITIES; i++) {
1125 var num_ticks = self.NumXTicks(startDate, endDate, i);
1126 if (self.width_ / num_ticks >= self.attr_('pixelsPerXLabel')) {
1127 chosen = i;
1128 break;
1129 }
1130 }
1131
1132 if (chosen >= 0) {
1133 return self.GetXAxis(startDate, endDate, chosen);
1134 } else {
1135 // TODO(danvk): signal error.
1136 }
1137 };
1138
1139 /**
1140 * Add ticks when the x axis has numbers on it (instead of dates)
1141 * @param {Number} startDate Start of the date window (millis since epoch)
1142 * @param {Number} endDate End of the date window (millis since epoch)
1143 * @return {Array.<Object>} Array of {label, value} tuples.
1144 * @public
1145 */
1146 Dygraph.numericTicks = function(minV, maxV, self) {
1147 // Basic idea:
1148 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1149 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks).
1150 // The first spacing greater than pixelsPerYLabel is what we use.
1151 // TODO(danvk): version that works on a log scale.
1152 if (self.attr_("labelsKMG2")) {
1153 var mults = [1, 2, 4, 8];
1154 } else {
1155 var mults = [1, 2, 5];
1156 }
1157 var scale, low_val, high_val, nTicks;
1158 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1159 var pixelsPerTick = self.attr_('pixelsPerYLabel');
1160 for (var i = -10; i < 50; i++) {
1161 if (self.attr_("labelsKMG2")) {
1162 var base_scale = Math.pow(16, i);
1163 } else {
1164 var base_scale = Math.pow(10, i);
1165 }
1166 for (var j = 0; j < mults.length; j++) {
1167 scale = base_scale * mults[j];
1168 low_val = Math.floor(minV / scale) * scale;
1169 high_val = Math.ceil(maxV / scale) * scale;
1170 nTicks = (high_val - low_val) / scale;
1171 var spacing = self.height_ / nTicks;
1172 // wish I could break out of both loops at once...
1173 if (spacing > pixelsPerTick) break;
1174 }
1175 if (spacing > pixelsPerTick) break;
1176 }
1177
1178 // Construct labels for the ticks
1179 var ticks = [];
1180 var k;
1181 var k_labels = [];
1182 if (self.attr_("labelsKMB")) {
1183 k = 1000;
1184 k_labels = [ "K", "M", "B", "T" ];
1185 }
1186 if (self.attr_("labelsKMG2")) {
1187 if (k) self.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1188 k = 1024;
1189 k_labels = [ "k", "M", "G", "T" ];
1190 }
1191
1192 for (var i = 0; i < nTicks; i++) {
1193 var tickV = low_val + i * scale;
1194 var absTickV = Math.abs(tickV);
1195 var label = self.round_(tickV, 2);
1196 if (k_labels.length) {
1197 // Round up to an appropriate unit.
1198 var n = k*k*k*k;
1199 for (var j = 3; j >= 0; j--, n /= k) {
1200 if (absTickV >= n) {
1201 label = self.round_(tickV / n, 1) + k_labels[j];
1202 break;
1203 }
1204 }
1205 }
1206 ticks.push( {label: label, v: tickV} );
1207 }
1208 return ticks;
1209 };
1210
1211 /**
1212 * Adds appropriate ticks on the y-axis
1213 * @param {Number} minY The minimum Y value in the data set
1214 * @param {Number} maxY The maximum Y value in the data set
1215 * @private
1216 */
1217 Dygraph.prototype.addYTicks_ = function(minY, maxY) {
1218 // Set the number of ticks so that the labels are human-friendly.
1219 // TODO(danvk): make this an attribute as well.
1220 var ticks = Dygraph.numericTicks(minY, maxY, this);
1221 this.layout_.updateOptions( { yAxis: [minY, maxY],
1222 yTicks: ticks } );
1223 };
1224
1225 // Computes the range of the data series (including confidence intervals).
1226 // series is either [ [x1, y1], [x2, y2], ... ] or
1227 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1228 // Returns [low, high]
1229 Dygraph.prototype.extremeValues_ = function(series) {
1230 var minY = null, maxY = null;
1231
1232 var bars = this.attr_("errorBars") || this.attr_("customBars");
1233 if (bars) {
1234 // With custom bars, maxY is the max of the high values.
1235 for (var j = 0; j < series.length; j++) {
1236 var y = series[j][1][0];
1237 if (!y) continue;
1238 var low = y - series[j][1][1];
1239 var high = y + series[j][1][2];
1240 if (low > y) low = y; // this can happen with custom bars,
1241 if (high < y) high = y; // e.g. in tests/custom-bars.html
1242 if (maxY == null || high > maxY) {
1243 maxY = high;
1244 }
1245 if (minY == null || low < minY) {
1246 minY = low;
1247 }
1248 }
1249 } else {
1250 for (var j = 0; j < series.length; j++) {
1251 var y = series[j][1];
1252 if (y === null || isNaN(y)) continue;
1253 if (maxY == null || y > maxY) {
1254 maxY = y;
1255 }
1256 if (minY == null || y < minY) {
1257 minY = y;
1258 }
1259 }
1260 }
1261
1262 return [minY, maxY];
1263 };
1264
1265 /**
1266 * Update the graph with new data. Data is in the format
1267 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1268 * or, if errorBars=true,
1269 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1270 * @param {Array.<Object>} data The data (see above)
1271 * @private
1272 */
1273 Dygraph.prototype.drawGraph_ = function(data) {
1274 var minY = null, maxY = null;
1275 this.layout_.removeAllDatasets();
1276 this.setColors_();
1277 this.attrs_['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1278
1279 // For stacked series.
1280 var cumulative_y = [];
1281 var datasets = [];
1282
1283 // Loop over all fields in the dataset
1284
1285 for (var i = 1; i < data[0].length; i++) {
1286 if (!this.visibility()[i - 1]) continue;
1287
1288 var series = [];
1289 for (var j = 0; j < data.length; j++) {
1290 var date = data[j][0];
1291 series[j] = [date, data[j][i]];
1292 }
1293 series = this.rollingAverage(series, this.rollPeriod_);
1294
1295 // Prune down to the desired range, if necessary (for zooming)
1296 var bars = this.attr_("errorBars") || this.attr_("customBars");
1297 if (this.dateWindow_) {
1298 var low = this.dateWindow_[0];
1299 var high= this.dateWindow_[1];
1300 var pruned = [];
1301 for (var k = 0; k < series.length; k++) {
1302 if (series[k][0] >= low && series[k][0] <= high) {
1303 pruned.push(series[k]);
1304 }
1305 }
1306 series = pruned;
1307 }
1308
1309 var extremes = this.extremeValues_(series);
1310 var thisMinY = extremes[0];
1311 var thisMaxY = extremes[1];
1312 if (!minY || thisMinY < minY) minY = thisMinY;
1313 if (!maxY || thisMaxY > maxY) maxY = thisMaxY;
1314
1315 if (bars) {
1316 var vals = [];
1317 for (var j=0; j<series.length; j++)
1318 vals[j] = [series[j][0],
1319 series[j][1][0], series[j][1][1], series[j][1][2]];
1320 this.layout_.addDataset(this.attr_("labels")[i], vals);
1321 } else if (this.attr_("stackedGraph")) {
1322 var vals = [];
1323 var l = series.length;
1324 var actual_y;
1325 for (var j = 0; j < l; j++) {
1326 if (cumulative_y[series[j][0]] === undefined)
1327 cumulative_y[series[j][0]] = 0;
1328
1329 actual_y = series[j][1];
1330 cumulative_y[series[j][0]] += actual_y;
1331
1332 vals[j] = [series[j][0], cumulative_y[series[j][0]]]
1333
1334 if (!maxY || cumulative_y[series[j][0]] > maxY)
1335 maxY = cumulative_y[series[j][0]];
1336 }
1337 datasets.push([this.attr_("labels")[i], vals]);
1338 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1339 } else {
1340 this.layout_.addDataset(this.attr_("labels")[i], series);
1341 }
1342 }
1343
1344 if (datasets.length > 0) {
1345 for (var i = (datasets.length - 1); i >= 0; i--) {
1346 this.layout_.addDataset(datasets[i][0], datasets[i][1]);
1347 }
1348 }
1349
1350 // Use some heuristics to come up with a good maxY value, unless it's been
1351 // set explicitly by the user.
1352 if (this.valueRange_ != null) {
1353 this.addYTicks_(this.valueRange_[0], this.valueRange_[1]);
1354 } else {
1355 // This affects the calculation of span, below.
1356 if (this.attr_("includeZero") && minY > 0) {
1357 minY = 0;
1358 }
1359
1360 // Add some padding and round up to an integer to be human-friendly.
1361 var span = maxY - minY;
1362 // special case: if we have no sense of scale, use +/-10% of the sole value.
1363 if (span == 0) { span = maxY; }
1364 var maxAxisY = maxY + 0.1 * span;
1365 var minAxisY = minY - 0.1 * span;
1366
1367 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1368 if (minAxisY < 0 && minY >= 0) minAxisY = 0;
1369 if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0;
1370
1371 if (this.attr_("includeZero")) {
1372 if (maxY < 0) maxAxisY = 0;
1373 if (minY > 0) minAxisY = 0;
1374 }
1375
1376 this.addYTicks_(minAxisY, maxAxisY);
1377 }
1378
1379 this.addXTicks_();
1380
1381 // Tell PlotKit to use this new data and render itself
1382 this.layout_.updateOptions({dateWindow: this.dateWindow_});
1383 this.layout_.evaluateWithError();
1384 this.plotter_.clear();
1385 this.plotter_.render();
1386 this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width,
1387 this.canvas_.height);
1388 };
1389
1390 /**
1391 * Calculates the rolling average of a data set.
1392 * If originalData is [label, val], rolls the average of those.
1393 * If originalData is [label, [, it's interpreted as [value, stddev]
1394 * and the roll is returned in the same form, with appropriately reduced
1395 * stddev for each value.
1396 * Note that this is where fractional input (i.e. '5/10') is converted into
1397 * decimal values.
1398 * @param {Array} originalData The data in the appropriate format (see above)
1399 * @param {Number} rollPeriod The number of days over which to average the data
1400 */
1401 Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) {
1402 if (originalData.length < 2)
1403 return originalData;
1404 var rollPeriod = Math.min(rollPeriod, originalData.length - 1);
1405 var rollingData = [];
1406 var sigma = this.attr_("sigma");
1407
1408 if (this.fractions_) {
1409 var num = 0;
1410 var den = 0; // numerator/denominator
1411 var mult = 100.0;
1412 for (var i = 0; i < originalData.length; i++) {
1413 num += originalData[i][1][0];
1414 den += originalData[i][1][1];
1415 if (i - rollPeriod >= 0) {
1416 num -= originalData[i - rollPeriod][1][0];
1417 den -= originalData[i - rollPeriod][1][1];
1418 }
1419
1420 var date = originalData[i][0];
1421 var value = den ? num / den : 0.0;
1422 if (this.attr_("errorBars")) {
1423 if (this.wilsonInterval_) {
1424 // For more details on this confidence interval, see:
1425 // http://en.wikipedia.org/wiki/Binomial_confidence_interval
1426 if (den) {
1427 var p = value < 0 ? 0 : value, n = den;
1428 var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n));
1429 var denom = 1 + sigma * sigma / den;
1430 var low = (p + sigma * sigma / (2 * den) - pm) / denom;
1431 var high = (p + sigma * sigma / (2 * den) + pm) / denom;
1432 rollingData[i] = [date,
1433 [p * mult, (p - low) * mult, (high - p) * mult]];
1434 } else {
1435 rollingData[i] = [date, [0, 0, 0]];
1436 }
1437 } else {
1438 var stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0;
1439 rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]];
1440 }
1441 } else {
1442 rollingData[i] = [date, mult * value];
1443 }
1444 }
1445 } else if (this.attr_("customBars")) {
1446 var low = 0;
1447 var mid = 0;
1448 var high = 0;
1449 var count = 0;
1450 for (var i = 0; i < originalData.length; i++) {
1451 var data = originalData[i][1];
1452 var y = data[1];
1453 rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]];
1454
1455 if (y != null && !isNaN(y)) {
1456 low += data[0];
1457 mid += y;
1458 high += data[2];
1459 count += 1;
1460 }
1461 if (i - rollPeriod >= 0) {
1462 var prev = originalData[i - rollPeriod];
1463 if (prev[1][1] != null && !isNaN(prev[1][1])) {
1464 low -= prev[1][0];
1465 mid -= prev[1][1];
1466 high -= prev[1][2];
1467 count -= 1;
1468 }
1469 }
1470 rollingData[i] = [originalData[i][0], [ 1.0 * mid / count,
1471 1.0 * (mid - low) / count,
1472 1.0 * (high - mid) / count ]];
1473 }
1474 } else {
1475 // Calculate the rolling average for the first rollPeriod - 1 points where
1476 // there is not enough data to roll over the full number of days
1477 var num_init_points = Math.min(rollPeriod - 1, originalData.length - 2);
1478 if (!this.attr_("errorBars")){
1479 if (rollPeriod == 1) {
1480 return originalData;
1481 }
1482
1483 for (var i = 0; i < originalData.length; i++) {
1484 var sum = 0;
1485 var num_ok = 0;
1486 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1487 var y = originalData[j][1];
1488 if (y == null || isNaN(y)) continue;
1489 num_ok++;
1490 sum += originalData[j][1];
1491 }
1492 if (num_ok) {
1493 rollingData[i] = [originalData[i][0], sum / num_ok];
1494 } else {
1495 rollingData[i] = [originalData[i][0], null];
1496 }
1497 }
1498
1499 } else {
1500 for (var i = 0; i < originalData.length; i++) {
1501 var sum = 0;
1502 var variance = 0;
1503 var num_ok = 0;
1504 for (var j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) {
1505 var y = originalData[j][1][0];
1506 if (y == null || isNaN(y)) continue;
1507 num_ok++;
1508 sum += originalData[j][1][0];
1509 variance += Math.pow(originalData[j][1][1], 2);
1510 }
1511 if (num_ok) {
1512 var stddev = Math.sqrt(variance) / num_ok;
1513 rollingData[i] = [originalData[i][0],
1514 [sum / num_ok, sigma * stddev, sigma * stddev]];
1515 } else {
1516 rollingData[i] = [originalData[i][0], [null, null, null]];
1517 }
1518 }
1519 }
1520 }
1521
1522 return rollingData;
1523 };
1524
1525 /**
1526 * Parses a date, returning the number of milliseconds since epoch. This can be
1527 * passed in as an xValueParser in the Dygraph constructor.
1528 * TODO(danvk): enumerate formats that this understands.
1529 * @param {String} A date in YYYYMMDD format.
1530 * @return {Number} Milliseconds since epoch.
1531 * @public
1532 */
1533 Dygraph.dateParser = function(dateStr, self) {
1534 var dateStrSlashed;
1535 var d;
1536 if (dateStr.length == 10 && dateStr.search("-") != -1) { // e.g. '2009-07-12'
1537 dateStrSlashed = dateStr.replace("-", "/", "g");
1538 while (dateStrSlashed.search("-") != -1) {
1539 dateStrSlashed = dateStrSlashed.replace("-", "/");
1540 }
1541 d = Date.parse(dateStrSlashed);
1542 } else if (dateStr.length == 8) { // e.g. '20090712'
1543 // TODO(danvk): remove support for this format. It's confusing.
1544 dateStrSlashed = dateStr.substr(0,4) + "/" + dateStr.substr(4,2)
1545 + "/" + dateStr.substr(6,2);
1546 d = Date.parse(dateStrSlashed);
1547 } else {
1548 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1549 // "2009/07/12 12:34:56"
1550 d = Date.parse(dateStr);
1551 }
1552
1553 if (!d || isNaN(d)) {
1554 self.error("Couldn't parse " + dateStr + " as a date");
1555 }
1556 return d;
1557 };
1558
1559 /**
1560 * Detects the type of the str (date or numeric) and sets the various
1561 * formatting attributes in this.attrs_ based on this type.
1562 * @param {String} str An x value.
1563 * @private
1564 */
1565 Dygraph.prototype.detectTypeFromString_ = function(str) {
1566 var isDate = false;
1567 if (str.indexOf('-') >= 0 ||
1568 str.indexOf('/') >= 0 ||
1569 isNaN(parseFloat(str))) {
1570 isDate = true;
1571 } else if (str.length == 8 && str > '19700101' && str < '20371231') {
1572 // TODO(danvk): remove support for this format.
1573 isDate = true;
1574 }
1575
1576 if (isDate) {
1577 this.attrs_.xValueFormatter = Dygraph.dateString_;
1578 this.attrs_.xValueParser = Dygraph.dateParser;
1579 this.attrs_.xTicker = Dygraph.dateTicker;
1580 } else {
1581 this.attrs_.xValueFormatter = function(x) { return x; };
1582 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
1583 this.attrs_.xTicker = Dygraph.numericTicks;
1584 }
1585 };
1586
1587 /**
1588 * Parses a string in a special csv format. We expect a csv file where each
1589 * line is a date point, and the first field in each line is the date string.
1590 * We also expect that all remaining fields represent series.
1591 * if the errorBars attribute is set, then interpret the fields as:
1592 * date, series1, stddev1, series2, stddev2, ...
1593 * @param {Array.<Object>} data See above.
1594 * @private
1595 *
1596 * @return Array.<Object> An array with one entry for each row. These entries
1597 * are an array of cells in that row. The first entry is the parsed x-value for
1598 * the row. The second, third, etc. are the y-values. These can take on one of
1599 * three forms, depending on the CSV and constructor parameters:
1600 * 1. numeric value
1601 * 2. [ value, stddev ]
1602 * 3. [ low value, center value, high value ]
1603 */
1604 Dygraph.prototype.parseCSV_ = function(data) {
1605 var ret = [];
1606 var lines = data.split("\n");
1607
1608 // Use the default delimiter or fall back to a tab if that makes sense.
1609 var delim = this.attr_('delimiter');
1610 if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) {
1611 delim = '\t';
1612 }
1613
1614 var start = 0;
1615 if (this.labelsFromCSV_) {
1616 start = 1;
1617 this.attrs_.labels = lines[0].split(delim);
1618 }
1619
1620 var xParser;
1621 var defaultParserSet = false; // attempt to auto-detect x value type
1622 var expectedCols = this.attr_("labels").length;
1623 var outOfOrder = false;
1624 for (var i = start; i < lines.length; i++) {
1625 var line = lines[i];
1626 if (line.length == 0) continue; // skip blank lines
1627 if (line[0] == '#') continue; // skip comment lines
1628 var inFields = line.split(delim);
1629 if (inFields.length < 2) continue;
1630
1631 var fields = [];
1632 if (!defaultParserSet) {
1633 this.detectTypeFromString_(inFields[0]);
1634 xParser = this.attr_("xValueParser");
1635 defaultParserSet = true;
1636 }
1637 fields[0] = xParser(inFields[0], this);
1638
1639 // If fractions are expected, parse the numbers as "A/B"
1640 if (this.fractions_) {
1641 for (var j = 1; j < inFields.length; j++) {
1642 // TODO(danvk): figure out an appropriate way to flag parse errors.
1643 var vals = inFields[j].split("/");
1644 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1645 }
1646 } else if (this.attr_("errorBars")) {
1647 // If there are error bars, values are (value, stddev) pairs
1648 for (var j = 1; j < inFields.length; j += 2)
1649 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1650 parseFloat(inFields[j + 1])];
1651 } else if (this.attr_("customBars")) {
1652 // Bars are a low;center;high tuple
1653 for (var j = 1; j < inFields.length; j++) {
1654 var vals = inFields[j].split(";");
1655 fields[j] = [ parseFloat(vals[0]),
1656 parseFloat(vals[1]),
1657 parseFloat(vals[2]) ];
1658 }
1659 } else {
1660 // Values are just numbers
1661 for (var j = 1; j < inFields.length; j++) {
1662 fields[j] = parseFloat(inFields[j]);
1663 }
1664 }
1665 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1666 outOfOrder = true;
1667 }
1668 ret.push(fields);
1669
1670 if (fields.length != expectedCols) {
1671 this.error("Number of columns in line " + i + " (" + fields.length +
1672 ") does not agree with number of labels (" + expectedCols +
1673 ") " + line);
1674 }
1675 }
1676
1677 if (outOfOrder) {
1678 this.warn("CSV is out of order; order it correctly to speed loading.");
1679 ret.sort(function(a,b) { return a[0] - b[0] });
1680 }
1681
1682 return ret;
1683 };
1684
1685 /**
1686 * The user has provided their data as a pre-packaged JS array. If the x values
1687 * are numeric, this is the same as dygraphs' internal format. If the x values
1688 * are dates, we need to convert them from Date objects to ms since epoch.
1689 * @param {Array.<Object>} data
1690 * @return {Array.<Object>} data with numeric x values.
1691 */
1692 Dygraph.prototype.parseArray_ = function(data) {
1693 // Peek at the first x value to see if it's numeric.
1694 if (data.length == 0) {
1695 this.error("Can't plot empty data set");
1696 return null;
1697 }
1698 if (data[0].length == 0) {
1699 this.error("Data set cannot contain an empty row");
1700 return null;
1701 }
1702
1703 if (this.attr_("labels") == null) {
1704 this.warn("Using default labels. Set labels explicitly via 'labels' " +
1705 "in the options parameter");
1706 this.attrs_.labels = [ "X" ];
1707 for (var i = 1; i < data[0].length; i++) {
1708 this.attrs_.labels.push("Y" + i);
1709 }
1710 }
1711
1712 if (Dygraph.isDateLike(data[0][0])) {
1713 // Some intelligent defaults for a date x-axis.
1714 this.attrs_.xValueFormatter = Dygraph.dateString_;
1715 this.attrs_.xTicker = Dygraph.dateTicker;
1716
1717 // Assume they're all dates.
1718 var parsedData = Dygraph.clone(data);
1719 for (var i = 0; i < data.length; i++) {
1720 if (parsedData[i].length == 0) {
1721 this.error("Row " << (1 + i) << " of data is empty");
1722 return null;
1723 }
1724 if (parsedData[i][0] == null
1725 || typeof(parsedData[i][0].getTime) != 'function') {
1726 this.error("x value in row " << (1 + i) << " is not a Date");
1727 return null;
1728 }
1729 parsedData[i][0] = parsedData[i][0].getTime();
1730 }
1731 return parsedData;
1732 } else {
1733 // Some intelligent defaults for a numeric x-axis.
1734 this.attrs_.xValueFormatter = function(x) { return x; };
1735 this.attrs_.xTicker = Dygraph.numericTicks;
1736 return data;
1737 }
1738 };
1739
1740 /**
1741 * Parses a DataTable object from gviz.
1742 * The data is expected to have a first column that is either a date or a
1743 * number. All subsequent columns must be numbers. If there is a clear mismatch
1744 * between this.xValueParser_ and the type of the first column, it will be
1745 * fixed. Returned value is in the same format as return value of parseCSV_.
1746 * @param {Array.<Object>} data See above.
1747 * @private
1748 */
1749 Dygraph.prototype.parseDataTable_ = function(data) {
1750 var cols = data.getNumberOfColumns();
1751 var rows = data.getNumberOfRows();
1752
1753 // Read column labels
1754 var labels = [];
1755 for (var i = 0; i < cols; i++) {
1756 labels.push(data.getColumnLabel(i));
1757 if (i != 0 && this.attr_("errorBars")) i += 1;
1758 }
1759 this.attrs_.labels = labels;
1760 cols = labels.length;
1761
1762 var indepType = data.getColumnType(0);
1763 if (indepType == 'date' || indepType == 'datetime') {
1764 this.attrs_.xValueFormatter = Dygraph.dateString_;
1765 this.attrs_.xValueParser = Dygraph.dateParser;
1766 this.attrs_.xTicker = Dygraph.dateTicker;
1767 } else if (indepType == 'number') {
1768 this.attrs_.xValueFormatter = function(x) { return x; };
1769 this.attrs_.xValueParser = function(x) { return parseFloat(x); };
1770 this.attrs_.xTicker = Dygraph.numericTicks;
1771 } else {
1772 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1773 "column 1 of DataTable input (Got '" + indepType + "')");
1774 return null;
1775 }
1776
1777 var ret = [];
1778 var outOfOrder = false;
1779 for (var i = 0; i < rows; i++) {
1780 var row = [];
1781 if (typeof(data.getValue(i, 0)) === 'undefined' ||
1782 data.getValue(i, 0) === null) {
1783 this.warning("Ignoring row " + i +
1784 " of DataTable because of undefined or null first column.");
1785 continue;
1786 }
1787
1788 if (indepType == 'date' || indepType == 'datetime') {
1789 row.push(data.getValue(i, 0).getTime());
1790 } else {
1791 row.push(data.getValue(i, 0));
1792 }
1793 if (!this.attr_("errorBars")) {
1794 for (var j = 1; j < cols; j++) {
1795 row.push(data.getValue(i, j));
1796 }
1797 } else {
1798 for (var j = 0; j < cols - 1; j++) {
1799 row.push([ data.getValue(i, 1 + 2 * j), data.getValue(i, 2 + 2 * j) ]);
1800 }
1801 }
1802 if (ret.length > 0 && row[0] < ret[ret.length - 1][0]) {
1803 outOfOrder = true;
1804 }
1805 ret.push(row);
1806 }
1807
1808 if (outOfOrder) {
1809 this.warn("DataTable is out of order; order it correctly to speed loading.");
1810 ret.sort(function(a,b) { return a[0] - b[0] });
1811 }
1812 return ret;
1813 }
1814
1815 // These functions are all based on MochiKit.
1816 Dygraph.update = function (self, o) {
1817 if (typeof(o) != 'undefined' && o !== null) {
1818 for (var k in o) {
1819 if (o.hasOwnProperty(k)) {
1820 self[k] = o[k];
1821 }
1822 }
1823 }
1824 return self;
1825 };
1826
1827 Dygraph.isArrayLike = function (o) {
1828 var typ = typeof(o);
1829 if (
1830 (typ != 'object' && !(typ == 'function' &&
1831 typeof(o.item) == 'function')) ||
1832 o === null ||
1833 typeof(o.length) != 'number' ||
1834 o.nodeType === 3
1835 ) {
1836 return false;
1837 }
1838 return true;
1839 };
1840
1841 Dygraph.isDateLike = function (o) {
1842 if (typeof(o) != "object" || o === null ||
1843 typeof(o.getTime) != 'function') {
1844 return false;
1845 }
1846 return true;
1847 };
1848
1849 Dygraph.clone = function(o) {
1850 // TODO(danvk): figure out how MochiKit's version works
1851 var r = [];
1852 for (var i = 0; i < o.length; i++) {
1853 if (Dygraph.isArrayLike(o[i])) {
1854 r.push(Dygraph.clone(o[i]));
1855 } else {
1856 r.push(o[i]);
1857 }
1858 }
1859 return r;
1860 };
1861
1862
1863 /**
1864 * Get the CSV data. If it's in a function, call that function. If it's in a
1865 * file, do an XMLHttpRequest to get it.
1866 * @private
1867 */
1868 Dygraph.prototype.start_ = function() {
1869 if (typeof this.file_ == 'function') {
1870 // CSV string. Pretend we got it via XHR.
1871 this.loadedEvent_(this.file_());
1872 } else if (Dygraph.isArrayLike(this.file_)) {
1873 this.rawData_ = this.parseArray_(this.file_);
1874 this.drawGraph_(this.rawData_);
1875 } else if (typeof this.file_ == 'object' &&
1876 typeof this.file_.getColumnRange == 'function') {
1877 // must be a DataTable from gviz.
1878 this.rawData_ = this.parseDataTable_(this.file_);
1879 this.drawGraph_(this.rawData_);
1880 } else if (typeof this.file_ == 'string') {
1881 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
1882 if (this.file_.indexOf('\n') >= 0) {
1883 this.loadedEvent_(this.file_);
1884 } else {
1885 var req = new XMLHttpRequest();
1886 var caller = this;
1887 req.onreadystatechange = function () {
1888 if (req.readyState == 4) {
1889 if (req.status == 200) {
1890 caller.loadedEvent_(req.responseText);
1891 }
1892 }
1893 };
1894
1895 req.open("GET", this.file_, true);
1896 req.send(null);
1897 }
1898 } else {
1899 this.error("Unknown data format: " + (typeof this.file_));
1900 }
1901 };
1902
1903 /**
1904 * Changes various properties of the graph. These can include:
1905 * <ul>
1906 * <li>file: changes the source data for the graph</li>
1907 * <li>errorBars: changes whether the data contains stddev</li>
1908 * </ul>
1909 * @param {Object} attrs The new properties and values
1910 */
1911 Dygraph.prototype.updateOptions = function(attrs) {
1912 // TODO(danvk): this is a mess. Rethink this function.
1913 if (attrs.rollPeriod) {
1914 this.rollPeriod_ = attrs.rollPeriod;
1915 }
1916 if (attrs.dateWindow) {
1917 this.dateWindow_ = attrs.dateWindow;
1918 }
1919 if (attrs.valueRange) {
1920 this.valueRange_ = attrs.valueRange;
1921 }
1922 Dygraph.update(this.user_attrs_, attrs);
1923
1924 this.labelsFromCSV_ = (this.attr_("labels") == null);
1925
1926 // TODO(danvk): this doesn't match the constructor logic
1927 this.layout_.updateOptions({ 'errorBars': this.attr_("errorBars") });
1928 if (attrs['file'] && attrs['file'] != this.file_) {
1929 this.file_ = attrs['file'];
1930 this.start_();
1931 } else {
1932 this.drawGraph_(this.rawData_);
1933 }
1934 };
1935
1936 /**
1937 * Resizes the dygraph. If no parameters are specified, resizes to fill the
1938 * containing div (which has presumably changed size since the dygraph was
1939 * instantiated. If the width/height are specified, the div will be resized.
1940 *
1941 * This is far more efficient than destroying and re-instantiating a
1942 * Dygraph, since it doesn't have to reparse the underlying data.
1943 *
1944 * @param {Number} width Width (in pixels)
1945 * @param {Number} height Height (in pixels)
1946 */
1947 Dygraph.prototype.resize = function(width, height) {
1948 if ((width === null) != (height === null)) {
1949 this.warn("Dygraph.resize() should be called with zero parameters or " +
1950 "two non-NULL parameters. Pretending it was zero.");
1951 width = height = null;
1952 }
1953
1954 // TODO(danvk): there should be a clear() method.
1955 this.maindiv_.innerHTML = "";
1956 this.attrs_.labelsDiv = null;
1957
1958 if (width) {
1959 this.maindiv_.style.width = width + "px";
1960 this.maindiv_.style.height = height + "px";
1961 this.width_ = width;
1962 this.height_ = height;
1963 } else {
1964 this.width_ = this.maindiv_.offsetWidth;
1965 this.height_ = this.maindiv_.offsetHeight;
1966 }
1967
1968 this.createInterface_();
1969 this.drawGraph_(this.rawData_);
1970 };
1971
1972 /**
1973 * Adjusts the number of days in the rolling average. Updates the graph to
1974 * reflect the new averaging period.
1975 * @param {Number} length Number of days over which to average the data.
1976 */
1977 Dygraph.prototype.adjustRoll = function(length) {
1978 this.rollPeriod_ = length;
1979 this.drawGraph_(this.rawData_);
1980 };
1981
1982 /**
1983 * Returns a boolean array of visibility statuses.
1984 */
1985 Dygraph.prototype.visibility = function() {
1986 // Do lazy-initialization, so that this happens after we know the number of
1987 // data series.
1988 if (!this.attr_("visibility")) {
1989 this.attrs_["visibility"] = [];
1990 }
1991 while (this.attr_("visibility").length < this.rawData_[0].length - 1) {
1992 this.attr_("visibility").push(true);
1993 }
1994 return this.attr_("visibility");
1995 };
1996
1997 /**
1998 * Changes the visiblity of a series.
1999 */
2000 Dygraph.prototype.setVisibility = function(num, value) {
2001 var x = this.visibility();
2002 if (num < 0 && num >= x.length) {
2003 this.warn("invalid series number in setVisibility: " + num);
2004 } else {
2005 x[num] = value;
2006 this.drawGraph_(this.rawData_);
2007 }
2008 };
2009
2010 /**
2011 * Create a new canvas element. This is more complex than a simple
2012 * document.createElement("canvas") because of IE and excanvas.
2013 */
2014 Dygraph.createCanvas = function() {
2015 var canvas = document.createElement("canvas");
2016
2017 isIE = (/MSIE/.test(navigator.userAgent) && !window.opera);
2018 if (isIE) {
2019 canvas = G_vmlCanvasManager.initElement(canvas);
2020 }
2021
2022 return canvas;
2023 };
2024
2025
2026 /**
2027 * A wrapper around Dygraph that implements the gviz API.
2028 * @param {Object} container The DOM object the visualization should live in.
2029 */
2030 Dygraph.GVizChart = function(container) {
2031 this.container = container;
2032 }
2033
2034 Dygraph.GVizChart.prototype.draw = function(data, options) {
2035 this.container.innerHTML = '';
2036 this.date_graph = new Dygraph(this.container, data, options);
2037 }
2038
2039 // Older pages may still use this name.
2040 DateGraph = Dygraph;