1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
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)
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
19 The CSV file is of the form
21 Date,SeriesA,SeriesB,SeriesC
25 If the 'errorBars' option is set in the constructor, the input should be of
28 Date,SeriesA,SeriesB,...
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
32 If the 'fractions' option is set, the input should be of the form:
34 Date,SeriesA,SeriesB,...
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
38 And error bars will be calculated automatically using a binomial distribution.
40 For further documentation and examples, see http://www.danvk.org/dygraphs
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.
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]);
62 this.__init__(div
, data
, opts
);
67 Dygraph
.NAME
= "Dygraph";
68 Dygraph
.VERSION
= "1.2";
69 Dygraph
.__repr__
= function() {
70 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
72 Dygraph
.toString
= function() {
73 return this.__repr__();
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;
82 // Default attribute values.
83 Dygraph
.DEFAULT_ATTRS
= {
84 highlightCircleSize
: 3,
90 // TODO(danvk): move defaults from createStatusMessage_ here.
92 labelsSeparateLines
: false,
99 axisLabelFontSize
: 14,
105 xValueFormatter
: Dygraph
.dateString_
,
106 xValueParser
: Dygraph
.dateParser
,
107 xTicker
: Dygraph
.dateTicker
,
115 wilsonInterval
: true, // only relevant if fractions is true
120 // Various logging levels.
126 Dygraph
.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
});
135 this.__init__(div
, file
, attrs
);
139 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
140 * and interaction <canvas> inside of it. See the constructor for details
142 * @param {String | Function} file Source data
143 * @param {Array.<String>} labels Names of the data series
144 * @param {Object} attrs Miscellaneous other options
147 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
148 // Support two-argument constructor
149 if (attrs
== null) { attrs
= {}; }
151 // Copy the important bits into the object
152 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
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;
162 // Clear the div. This ensure that, if multiple dygraphs are passed the same
163 // div, then only one will be drawn.
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";
170 if (div
.style
.height
== '') {
171 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
173 this.width_
= parseInt(div
.style
.width
, 10);
174 this.height_
= parseInt(div
.style
.height
, 10);
176 // Dygraphs has many options, some of which interact with one another.
177 // To keep track of everything, we maintain two sets of options:
179 // this.user_attrs_ only options explicitly set by the user.
180 // this.attrs_ defaults, options derived from user_attrs_, data.
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
);
189 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
191 // Make a note of whether labels will be pulled from the CSV file.
192 this.labelsFromCSV_
= (this.attr_("labels") == null);
194 // Create the containing DIV and other interactive elements
195 this.createInterface_();
200 Dygraph
.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
];
210 // TODO(danvk): any way I can get the line numbers to be this.warn call?
211 Dygraph
.prototype.log
= function(severity
, message
) {
212 if (typeof(console
) != 'undefined') {
215 console
.debug('dygraphs: ' + message
);
218 console
.info('dygraphs: ' + message
);
220 case Dygraph
.WARNING
:
221 console
.warn('dygraphs: ' + message
);
224 console
.error('dygraphs: ' + message
);
229 Dygraph
.prototype.info
= function(message
) {
230 this.log(Dygraph
.INFO
, message
);
232 Dygraph
.prototype.warn
= function(message
) {
233 this.log(Dygraph
.WARNING
, message
);
235 Dygraph
.prototype.error
= function(message
) {
236 this.log(Dygraph
.ERROR
, message
);
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
243 Dygraph
.prototype.rollPeriod
= function() {
244 return this.rollPeriod_
;
247 Dygraph
.addEvent
= function(el
, evt
, fn
) {
248 var normed_fn
= function(e
) {
249 if (!e
) var e
= window
.event
;
252 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
253 el
.addEventListener(evt
, normed_fn
, false);
255 el
.attachEvent('on' + evt
, normed_fn
);
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.
265 Dygraph
.prototype.createInterface_
= function() {
266 // Create the all-enclosing graph div
267 var enclosing
= this.maindiv_
;
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
);
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_
);
284 // ... and for static parts of the chart.
285 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
288 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
289 dygraph
.mouseMove_(e
);
291 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
292 dygraph
.mouseOut_(e
);
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")) });
303 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
305 // TODO(danvk): why does the Renderer need its own set of options?
306 this.renderOptions_
= { colorScheme
: this.colors_
,
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_
);
315 this.createStatusMessage_();
316 this.createRollInterface_();
317 this.createDragInterface_();
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
327 Dygraph
.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
);
341 // Taken from MochiKit.Color
342 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
346 if (saturation
=== 0) {
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
)));
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;
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
+ ')';
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.
380 Dygraph
.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;
385 var colors
= this.attr_('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
) );
394 for (var i
= 0; i
< num
; i
++) {
395 var colorStr
= colors
[i
% colors
.length
];
396 this.colors_
.push(colorStr
);
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_
);
407 // The following functions are from quirksmode.org
408 // http://www.quirksmode.org/js
/findpos
.html
409 Dygraph
.findPosX
= function(obj
) {
411 if (obj
.offsetParent
) {
412 while (obj
.offsetParent
) {
413 curleft
+= obj
.offsetLeft
;
414 obj
= obj
.offsetParent
;
422 Dygraph
.findPosY
= function(obj
) {
424 if (obj
.offsetParent
) {
425 while (obj
.offsetParent
) {
426 curtop
+= obj
.offsetTop
;
427 obj
= obj
.offsetParent
;
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
441 Dygraph
.prototype.createStatusMessage_
= function(){
442 if (!this.attr_("labelsDiv")) {
443 var divWidth
= this.attr_('labelsDivWidth');
445 "position": "absolute",
448 "width": divWidth
+ "px",
450 "left": (this.width_
- divWidth
- 2) + "px",
451 "background": "white",
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
];
461 this.graphDiv
.appendChild(div
);
462 this.attrs_
.labelsDiv
= div
;
467 * Create the text box to adjust the averaging period
468 * @return {Object} The newly-created text box
471 Dygraph
.prototype.createRollInterface_
= function() {
472 var display
= this.attr_('showRoller') ? "block" : "none";
473 var textAttr
= { "position": "absolute",
475 "top": (this.plotter_
.area
.h
- 25) + "px",
476 "left": (this.plotter_
.area
.x
+ 1) + "px",
479 var roller
= document
.createElement("input");
480 roller
.type
= "text";
482 roller
.value
= this.rollPeriod_
;
483 for (var name
in textAttr
) {
484 if (textAttr
.hasOwnProperty(name
)) {
485 roller
.style
[name
] = textAttr
[name
];
489 var pa
= this.graphDiv
;
490 pa
.appendChild(roller
);
492 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
496 // These functions are taken from MochiKit.Signal
497 Dygraph
.pageX
= function(e
) {
499 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
502 var b
= document
.body
;
504 (de
.scrollLeft
|| b
.scrollLeft
) -
505 (de
.clientLeft
|| 0);
509 Dygraph
.pageY
= function(e
) {
511 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
514 var b
= document
.body
;
516 (de
.scrollTop
|| b
.scrollTop
) -
522 * Set up all the mouse handlers needed to capture dragging behavior for zoom
526 Dygraph
.prototype.createDragInterface_
= function() {
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;
537 var draggingDate
= null;
538 var dateRange
= null;
540 // Utility function to convert page-wide coordinates to canvas coords
543 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
544 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
546 // Draw zoom rectangles when the mouse is down and the user moves around
547 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
549 dragEndX
= getX(event
);
550 dragEndY
= getY(event
);
552 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
554 } else if (isPanning
) {
555 dragEndX
= getX(event
);
556 dragEndY
= getY(event
);
558 // Want to have it so that:
559 // 1. draggingDate appears at dragEndX
560 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
562 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
563 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
564 self
.drawGraph_(self
.rawData_
);
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
);
575 if (event
.altKey
|| event
.shiftKey
) {
576 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
578 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
579 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
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
) {
602 // Temporarily cancel the dragging event when the mouse leaves the graph
603 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
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
) {
615 dragEndX
= getX(event
);
616 dragEndY
= getY(event
);
617 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
618 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
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_
);
627 if (regionWidth
>= 10) {
628 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
629 Math
.max(dragStartX
, dragEndX
));
631 self
.canvas_
.getContext("2d").clearRect(0, 0,
633 self
.canvas_
.height
);
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
);
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
665 * @param {Number} startX The X position where the drag started, in canvas
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
672 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
673 var ctx
= this.canvas_
.getContext("2d");
675 // Clean up from the previous rect if necessary
677 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
678 Math
.abs(startX
- prevEndX
), this.height_
);
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_
);
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.
697 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
698 // Find the earliest and latest dates contained in this canvasx range.
699 var points
= this.layout_
.points
;
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
;
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
;
713 this.dateWindow_
= [minDate
, maxDate
];
714 this.drawGraph_(this.rawData_
);
715 if (this.attr_("zoomCallback")) {
716 this.attr_("zoomCallback")(minDate
, maxDate
);
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.
727 Dygraph
.prototype.mouseMove_
= function(event
) {
728 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
729 var points
= this.layout_
.points
;
734 // Loop through all the points and find the date nearest to our current
736 var minDist
= 1e+100;
738 for (var i
= 0; i
< points
.length
; i
++) {
739 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
740 if (dist
> minDist
) break;
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
;
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
]);
757 if (this.attr_("highlightCallback")) {
758 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
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_
);
769 var isOK
= function(x
) { return x
&& !isNaN(x
); };
771 if (this.selPoints_
.length
> 0) {
772 var canvasx
= this.selPoints_
[0].canvasx
;
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")) {
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);
788 this.attr_("labelsDiv").innerHTML
= replace
;
790 // Save last x position for callbacks.
793 // Draw colored circles over the center of each selected point
795 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
796 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
798 ctx
.fillStyle
= this.colors_
[i
%clen
];
799 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
800 0, 2 * Math
.PI
, false);
805 this.previousVerticalX_
= canvasx
;
810 * The mouse has left the canvas. Clear out whatever artifacts remain
811 * @param {Object} event the mouseout event from the browser.
814 Dygraph
.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
= "";
821 Dygraph
.zeropad
= function(x
) {
822 if (x
< 10) return "0" + x
; else return "" + x
;
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"
831 Dygraph
.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());
841 return zeropad(d
.getHours());
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"
850 * TODO(danvk): why is this part of the prototype?
852 Dygraph
.dateString_
= function(date
, self
) {
853 var zeropad
= Dygraph
.zeropad
;
854 var d
= new Date(date
);
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());
864 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
865 if (frac
) ret
= " " + self
.hmsString_(date
);
867 return year
+ "/" + month + "/" + day
+ ret
;
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
877 Dygraph
.prototype.round_
= function(num
, places
) {
878 var shift
= Math
.pow(10, places
);
879 return Math
.round(num
* shift
)/shift
;
883 * Fires when there's data available to be graphed.
884 * @param {String} data Raw CSV data to be plotted
887 Dygraph
.prototype.loadedEvent_
= function(data
) {
888 this.rawData_
= this.parseCSV_(data
);
889 this.drawGraph_(this.rawData_
);
892 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
893 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
894 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
897 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
900 Dygraph
.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];
907 startDate
= this.rawData_
[0][0];
908 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
911 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
912 this.layout_
.updateOptions({xTicks
: xTicks
});
915 // Time granularity enumeration
916 Dygraph
.SECONDLY
= 0;
917 Dygraph
.TWO_SECONDLY
= 1;
918 Dygraph
.FIVE_SECONDLY
= 2;
919 Dygraph
.TEN_SECONDLY
= 3;
920 Dygraph
.THIRTY_SECONDLY
= 4;
921 Dygraph
.MINUTELY
= 5;
922 Dygraph
.TWO_MINUTELY
= 6;
923 Dygraph
.FIVE_MINUTELY
= 7;
924 Dygraph
.TEN_MINUTELY
= 8;
925 Dygraph
.THIRTY_MINUTELY
= 9;
927 Dygraph
.TWO_HOURLY
= 11;
928 Dygraph
.SIX_HOURLY
= 12;
931 Dygraph
.MONTHLY
= 15;
932 Dygraph
.QUARTERLY
= 16;
933 Dygraph
.BIANNUAL
= 17;
935 Dygraph
.DECADAL
= 19;
936 Dygraph
.NUM_GRANULARITIES
= 20;
938 Dygraph
.SHORT_SPACINGS
= [];
939 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
940 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
941 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
942 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
943 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
944 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
945 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
946 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
947 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
948 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
949 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
950 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
951 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
952 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
953 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
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.
960 Dygraph
.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
);
966 var year_mod
= 1; // e.g. to only print one point every 10 years.
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; }
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
);
981 // Construct an x-axis of nicely-formatted times on meaningful boundaries
982 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
984 // Returns an array containing {v: millis, label: label} dictionaries.
986 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
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"
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
);
1002 if (g
<= 60) { // minutes
1003 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1008 if (g
<= 24) { // days
1009 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1014 if (g
== 7) { // one week
1015 d
.setDate(d
.getDate() - d
.getDay());
1020 start_time
= d
.getTime();
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
) });
1029 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
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.
1037 var year_mod
= 1; // e.g. to only print one point every 10 years.
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
) {
1045 } else if (granularity
== Dygraph
.ANNUAL
) {
1047 } else if (granularity
== Dygraph
.DECADAL
) {
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') });
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.
1077 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
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')) {
1088 return self
.GetXAxis(startDate
, endDate
, chosen
);
1090 // TODO(danvk): signal error.
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.
1101 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
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];
1110 var mults
= [1, 2, 5];
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
);
1119 var base_scale
= Math
.pow(10, i
);
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;
1130 if (spacing
> pixelsPerTick
) break;
1133 // Construct labels for the ticks
1137 if (self
.attr_("labelsKMB")) {
1139 k_labels
= [ "K", "M", "B", "T" ];
1141 if (self
.attr_("labelsKMG2")) {
1142 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1144 k_labels
= [ "k", "M", "G", "T" ];
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.
1154 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1155 if (absTickV
>= n
) {
1156 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1161 ticks
.push( {label
: label
, v
: tickV
} );
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
1172 Dygraph
.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
],
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]
1184 Dygraph
.prototype.extremeValues_
= function(series
) {
1185 var minY
= null, maxY
= null;
1187 var bars
= this.attr_("errorBars") || this.attr_("customBars");
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];
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
) {
1200 if (minY
== null || low
< minY
) {
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
) {
1211 if (minY
== null || y
< minY
) {
1217 return [minY
, maxY
];
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)
1228 Dygraph
.prototype.drawGraph_
= function(data
) {
1229 var minY
= null, maxY
= null;
1230 this.layout_
.removeAllDatasets();
1232 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
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;
1239 for (var j
= 0; j
< data
.length
; j
++) {
1240 var date
= data
[j
][0];
1241 series
[j
] = [date
, data
[j
][i
]];
1243 series
= this.rollingAverage(series
, this.rollPeriod_
);
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];
1251 for (var k
= 0; k
< series
.length
; k
++) {
1252 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
1253 pruned
.push(series
[k
]);
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
;
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
);
1272 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
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]);
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
;
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;
1290 if (this.attr_("includeZero")) {
1291 if (maxY
< 0) maxAxisY
= 0;
1292 if (minY
> 0) minAxisY
= 0;
1295 this.addYTicks_(minAxisY
, maxAxisY
);
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
);
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
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
1320 Dygraph
.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");
1327 if (this.fractions_
) {
1329 var den
= 0; // numerator/denominator
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];
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
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
]];
1354 rollingData
[i
] = [date
, [0, 0, 0]];
1357 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1358 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1361 rollingData
[i
] = [date
, mult
* value
];
1364 } else if (this.attr_("customBars")) {
1369 for (var i
= 0; i
< originalData
.length
; i
++) {
1370 var data
= originalData
[i
][1];
1372 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1374 if (y
!= null && !isNaN(y
)) {
1380 if (i
- rollPeriod
>= 0) {
1381 var prev
= originalData
[i
- rollPeriod
];
1382 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1389 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1390 1.0 * (mid
- low
) / count
,
1391 1.0 * (high
- mid
) / count
]];
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
;
1402 for (var i
= 0; i
< originalData
.length
; i
++) {
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;
1409 sum
+= originalData
[j
][1];
1412 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1414 rollingData
[i
] = [originalData
[i
][0], null];
1419 for (var i
= 0; i
< originalData
.length
; i
++) {
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;
1427 sum
+= originalData
[j
][1][0];
1428 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1431 var stddev
= Math
.sqrt(variance
) / num_ok
;
1432 rollingData
[i
] = [originalData
[i
][0],
1433 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1435 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
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.
1452 Dygraph
.dateParser
= function(dateStr
, self
) {
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("-", "/");
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
);
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
);
1472 if (!d
|| isNaN(d
)) {
1473 self
.error("Couldn't parse " + dateStr
+ " as a date");
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.
1484 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1486 if (str
.indexOf('-') >= 0 ||
1487 str
.indexOf('/') >= 0 ||
1488 isNaN(parseFloat(str
))) {
1490 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1491 // TODO(danvk): remove support for this format.
1496 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1497 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1498 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1500 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1501 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1502 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
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.
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:
1520 * 2. [ value, stddev ]
1521 * 3. [ low value, center value, high value ]
1523 Dygraph
.prototype.parseCSV_
= function(data
) {
1525 var lines
= data
.split("\n");
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) {
1534 if (this.labelsFromCSV_
) {
1536 this.attrs_
.labels
= lines
[0].split(delim
);
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;
1550 if (!defaultParserSet
) {
1551 this.detectTypeFromString_(inFields
[0]);
1552 xParser
= this.attr_("xValueParser");
1553 defaultParserSet
= true;
1555 fields
[0] = xParser(inFields
[0], this);
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])];
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]) ];
1578 // Values are just numbers
1579 for (var j = 1; j < inFields.length; j++) {
1580 fields[j] = parseFloat(inFields[j]);
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 +
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.
1601 Dygraph.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");
1607 if (data[0].length == 0) {
1608 this.error("Data set cannot contain an empty row");
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);
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;
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");
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");
1638 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1642 // Some intelligent defaults for a numeric x-axis.
1643 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1644 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
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.
1658 Dygraph
.prototype.parseDataTable_
= function(data
) {
1659 var cols
= data
.getNumberOfColumns();
1660 var rows
= data
.getNumberOfRows();
1662 // Read column labels
1664 for (var i
= 0; i
< cols
; i
++) {
1665 labels
.push(data
.getColumnLabel(i
));
1666 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1668 this.attrs_
.labels
= labels
;
1669 cols
= labels
.length
;
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
;
1681 this.error("only 'date' and 'number' types are supported for column 1 " +
1682 "of DataTable input (Got '" + indepType
+ "')");
1687 for (var i
= 0; i
< rows
; i
++) {
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.");
1696 if (indepType
== 'date') {
1697 row
.push(data
.getValue(i
, 0).getTime());
1699 row
.push(data
.getValue(i
, 0));
1701 if (!this.attr_("errorBars")) {
1702 for (var j
= 1; j
< cols
; j
++) {
1703 row
.push(data
.getValue(i
, j
));
1706 for (var j
= 0; j
< cols
- 1; j
++) {
1707 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
1715 // These functions are all based on MochiKit.
1716 Dygraph
.update
= function (self
, o
) {
1717 if (typeof(o
) != 'undefined' && o
!== null) {
1719 if (o
.hasOwnProperty(k
)) {
1727 Dygraph
.isArrayLike
= function (o
) {
1728 var typ
= typeof(o
);
1730 (typ
!= 'object' && !(typ
== 'function' &&
1731 typeof(o
.item
) == 'function')) ||
1733 typeof(o
.length
) != 'number' ||
1741 Dygraph
.isDateLike
= function (o
) {
1742 if (typeof(o
) != "object" || o
=== null ||
1743 typeof(o
.getTime
) != 'function') {
1749 Dygraph
.clone
= function(o
) {
1750 // TODO(danvk): figure out how MochiKit's version works
1752 for (var i
= 0; i
< o
.length
; i
++) {
1753 if (Dygraph
.isArrayLike(o
[i
])) {
1754 r
.push(Dygraph
.clone(o
[i
]));
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.
1768 Dygraph
.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_
);
1785 var req
= new XMLHttpRequest();
1787 req
.onreadystatechange
= function () {
1788 if (req
.readyState
== 4) {
1789 if (req
.status
== 200) {
1790 caller
.loadedEvent_(req
.responseText
);
1795 req
.open("GET", this.file_
, true);
1799 this.error("Unknown data format: " + (typeof this.file_
));
1804 * Changes various properties of the graph. These can include:
1806 * <li>file: changes the source data for the graph</li>
1807 * <li>errorBars: changes whether the data contains stddev</li>
1809 * @param {Object} attrs The new properties and values
1811 Dygraph
.prototype.updateOptions
= function(attrs
) {
1812 // TODO(danvk): this is a mess. Rethink this function.
1813 if (attrs
.rollPeriod
) {
1814 this.rollPeriod_
= attrs
.rollPeriod
;
1816 if (attrs
.dateWindow
) {
1817 this.dateWindow_
= attrs
.dateWindow
;
1819 if (attrs
.valueRange
) {
1820 this.valueRange_
= attrs
.valueRange
;
1822 Dygraph
.update(this.user_attrs_
, attrs
);
1824 this.labelsFromCSV_
= (this.attr_("labels") == null);
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'];
1832 this.drawGraph_(this.rawData_
);
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.
1841 * This is far more efficient than destroying and re-instantiating a
1842 * Dygraph, since it doesn't have to reparse the underlying data.
1844 * @param {Number} width Width (in pixels)
1845 * @param {Number} height Height (in pixels)
1847 Dygraph
.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;
1854 // TODO(danvk): there should be a clear() method.
1855 this.maindiv_
.innerHTML
= "";
1856 this.attrs_
.labelsDiv
= null;
1859 this.maindiv_
.style
.width
= width
+ "px";
1860 this.maindiv_
.style
.height
= height
+ "px";
1861 this.width_
= width
;
1862 this.height_
= height
;
1864 this.width_
= this.maindiv_
.offsetWidth
;
1865 this.height_
= this.maindiv_
.offsetHeight
;
1868 this.createInterface_();
1869 this.drawGraph_(this.rawData_
);
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.
1877 Dygraph
.prototype.adjustRoll
= function(length
) {
1878 this.rollPeriod_
= length
;
1879 this.drawGraph_(this.rawData_
);
1883 * Returns a boolean array of visibility statuses.
1885 Dygraph
.prototype.visibility
= function() {
1886 // Do lazy-initialization, so that this happens after we know the number of
1888 if (!this.attr_("visibility")) {
1889 this.attrs_
["visibility"] = [];
1891 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
1892 this.attr_("visibility").push(true);
1894 return this.attr_("visibility");
1898 * Changes the visiblity of a series.
1900 Dygraph
.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
);
1906 this.drawGraph_(this.rawData_
);
1911 * Create a new canvas element. This is more complex than a simple
1912 * document.createElement("canvas") because of IE and excanvas.
1914 Dygraph
.createCanvas
= function() {
1915 var canvas
= document
.createElement("canvas");
1917 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
1919 canvas
= G_vmlCanvasManager
.initElement(canvas
);
1927 * A wrapper around Dygraph that implements the gviz API.
1928 * @param {Object} container The DOM object the visualization should live in.
1930 Dygraph
.GVizChart
= function(container
) {
1931 this.container
= container
;
1934 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
1935 this.container
.innerHTML
= '';
1936 this.date_graph
= new Dygraph(this.container
, data
, options
);
1939 // Older pages may still use this name.
1940 DateGraph
= Dygraph
;