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