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