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