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