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