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