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