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