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