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,
98 axisLabelFontSize
: 14,
104 xValueFormatter
: Dygraph
.dateString_
,
105 xValueParser
: Dygraph
.dateParser
,
106 xTicker
: Dygraph
.dateTicker
,
113 wilsonInterval
: true, // only relevant if fractions is true
117 // Various logging levels.
123 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
124 // Labels is no longer a constructor parameter, since it's typically set
125 // directly from the data source. It also conains a name for the x-axis,
126 // which the previous constructor form did not.
127 if (labels
!= null) {
128 var new_labels
= ["Date"];
129 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
130 Dygraph
.update(attrs
, { 'labels': new_labels
});
132 this.__init__(div
, file
, attrs
);
136 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
137 * and interaction <canvas> inside of it. See the constructor for details
139 * @param {String | Function} file Source data
140 * @param {Array.<String>} labels Names of the data series
141 * @param {Object} attrs Miscellaneous other options
144 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
145 // Support two-argument constructor
146 if (attrs
== null) { attrs
= {}; }
148 // Copy the important bits into the object
149 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
152 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
153 this.previousVerticalX_
= -1;
154 this.fractions_
= attrs
.fractions
|| false;
155 this.dateWindow_
= attrs
.dateWindow
|| null;
156 this.valueRange_
= attrs
.valueRange
|| null;
157 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
159 // Clear the div. This ensure that, if multiple dygraphs are passed the same
160 // div, then only one will be drawn.
163 // If the div isn't already sized then give it a default size.
164 if (div
.style
.width
== '') {
165 div
.style
.width
= Dygraph
.DEFAULT_WIDTH
+ "px";
167 if (div
.style
.height
== '') {
168 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
170 this.width_
= parseInt(div
.style
.width
, 10);
171 this.height_
= parseInt(div
.style
.height
, 10);
173 // Dygraphs has many options, some of which interact with one another.
174 // To keep track of everything, we maintain two sets of options:
176 // this.user_attrs_ only options explicitly set by the user.
177 // this.attrs_ defaults, options derived from user_attrs_, data.
179 // Options are then accessed this.attr_('attr'), which first looks at
180 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
181 // defaults without overriding behavior that the user specifically asks for.
182 this.user_attrs_
= {};
183 Dygraph
.update(this.user_attrs_
, attrs
);
186 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
188 // Make a note of whether labels will be pulled from the CSV file.
189 this.labelsFromCSV_
= (this.attr_("labels") == null);
191 // Create the containing DIV and other interactive elements
192 this.createInterface_();
194 // Create the PlotKit grapher
195 // TODO(danvk): why does the Layout need its own set of options?
196 this.layoutOptions_
= { 'errorBars': (this.attr_("errorBars") ||
197 this.attr_("customBars")),
198 'xOriginIsZero': false };
199 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
200 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
202 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
204 // TODO(danvk): why does the Renderer need its own set of options?
205 this.renderOptions_
= { colorScheme
: this.colors_
,
207 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
208 Dygraph
.update(this.renderOptions_
, this.attrs_
);
209 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
210 this.plotter_
= new DygraphCanvasRenderer(this,
211 this.hidden_
, this.layout_
,
212 this.renderOptions_
);
214 this.createStatusMessage_();
215 this.createRollInterface_();
216 this.createDragInterface_();
221 Dygraph
.prototype.attr_
= function(name
) {
222 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
223 return this.user_attrs_
[name
];
224 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
225 return this.attrs_
[name
];
231 // TODO(danvk): any way I can get the line numbers to be this.warn call?
232 Dygraph
.prototype.log
= function(severity
, message
) {
233 if (typeof(console
) != 'undefined') {
236 console
.debug('dygraphs: ' + message
);
239 console
.info('dygraphs: ' + message
);
241 case Dygraph
.WARNING
:
242 console
.warn('dygraphs: ' + message
);
245 console
.error('dygraphs: ' + message
);
250 Dygraph
.prototype.info
= function(message
) {
251 this.log(Dygraph
.INFO
, message
);
253 Dygraph
.prototype.warn
= function(message
) {
254 this.log(Dygraph
.WARNING
, message
);
256 Dygraph
.prototype.error
= function(message
) {
257 this.log(Dygraph
.ERROR
, message
);
261 * Returns the current rolling period, as set by the user or an option.
262 * @return {Number} The number of days in the rolling window
264 Dygraph
.prototype.rollPeriod
= function() {
265 return this.rollPeriod_
;
268 Dygraph
.addEvent
= function(el
, evt
, fn
) {
269 var normed_fn
= function(e
) {
270 if (!e
) var e
= window
.event
;
273 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
274 el
.addEventListener(evt
, normed_fn
, false);
276 el
.attachEvent('on' + evt
, normed_fn
);
281 * Generates interface elements for the Dygraph: a containing div, a div to
282 * display the current point, and a textbox to adjust the rolling average
286 Dygraph
.prototype.createInterface_
= function() {
287 // Create the all-enclosing graph div
288 var enclosing
= this.maindiv_
;
290 this.graphDiv
= document
.createElement("div");
291 this.graphDiv
.style
.width
= this.width_
+ "px";
292 this.graphDiv
.style
.height
= this.height_
+ "px";
293 enclosing
.appendChild(this.graphDiv
);
295 // Create the canvas for interactive parts of the chart.
296 this.canvas_
= document
.createElement("canvas");
297 this.canvas_
.style
.position
= "absolute";
298 this.canvas_
.width
= this.width_
;
299 this.canvas_
.height
= this.height_
;
300 this.graphDiv
.appendChild(this.canvas_
);
302 // ... and for static parts of the chart.
303 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
306 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
307 dygraph
.mouseMove_(e
);
309 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
310 dygraph
.mouseOut_(e
);
315 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
316 * this particular canvas. All Dygraph work is done on this.canvas_.
317 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
318 * @return {Object} The newly-created canvas
321 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
322 var h
= document
.createElement("canvas");
323 h
.style
.position
= "absolute";
324 h
.style
.top
= canvas
.style
.top
;
325 h
.style
.left
= canvas
.style
.left
;
326 h
.width
= this.width_
;
327 h
.height
= this.height_
;
328 this.graphDiv
.appendChild(h
);
332 // Taken from MochiKit.Color
333 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
337 if (saturation
=== 0) {
342 var i
= Math
.floor(hue
* 6);
343 var f
= (hue
* 6) - i
;
344 var p
= value
* (1 - saturation
);
345 var q
= value
* (1 - (saturation
* f
));
346 var t
= value
* (1 - (saturation
* (1 - f
)));
348 case 1: red
= q
; green
= value
; blue
= p
; break;
349 case 2: red
= p
; green
= value
; blue
= t
; break;
350 case 3: red
= p
; green
= q
; blue
= value
; break;
351 case 4: red
= t
; green
= p
; blue
= value
; break;
352 case 5: red
= value
; green
= p
; blue
= q
; break;
353 case 6: // fall through
354 case 0: red
= value
; green
= t
; blue
= p
; break;
357 red
= Math
.floor(255 * red
+ 0.5);
358 green
= Math
.floor(255 * green
+ 0.5);
359 blue
= Math
.floor(255 * blue
+ 0.5);
360 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
365 * Generate a set of distinct colors for the data series. This is done with a
366 * color wheel. Saturation/Value are customizable, and the hue is
367 * equally-spaced around the color wheel. If a custom set of colors is
368 * specified, that is used instead.
371 Dygraph
.prototype.setColors_
= function() {
372 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
373 // away with this.renderOptions_.
374 var num
= this.attr_("labels").length
- 1;
376 var colors
= this.attr_('colors');
378 var sat
= this.attr_('colorSaturation') || 1.0;
379 var val
= this.attr_('colorValue') || 0.5;
380 for (var i
= 1; i
<= num
; i
++) {
381 var hue
= (1.0*i
/(1+num
));
382 this.colors_
.push( Dygraph
.hsvToRGB(hue
, sat
, val
) );
385 for (var i
= 0; i
< num
; i
++) {
386 var colorStr
= colors
[i
% colors
.length
];
387 this.colors_
.push(colorStr
);
391 // TODO(danvk): update this w/r
/t/ the
new options system
.
392 this.renderOptions_
.colorScheme
= this.colors_
;
393 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
394 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
395 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
398 // The following functions are from quirksmode.org
399 // http://www.quirksmode.org/js
/findpos
.html
400 Dygraph
.findPosX
= function(obj
) {
402 if (obj
.offsetParent
) {
403 while (obj
.offsetParent
) {
404 curleft
+= obj
.offsetLeft
;
405 obj
= obj
.offsetParent
;
413 Dygraph
.findPosY
= function(obj
) {
415 if (obj
.offsetParent
) {
416 while (obj
.offsetParent
) {
417 curtop
+= obj
.offsetTop
;
418 obj
= obj
.offsetParent
;
427 * Create the div that contains information on the selected point(s)
428 * This goes in the top right of the canvas, unless an external div has already
432 Dygraph
.prototype.createStatusMessage_
= function(){
433 if (!this.attr_("labelsDiv")) {
434 var divWidth
= this.attr_('labelsDivWidth');
436 "position": "absolute",
439 "width": divWidth
+ "px",
441 "left": (this.width_
- divWidth
- 2) + "px",
442 "background": "white",
444 "overflow": "hidden"};
445 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
446 var div
= document
.createElement("div");
447 for (var name
in messagestyle
) {
448 div
.style
[name
] = messagestyle
[name
];
450 this.graphDiv
.appendChild(div
);
451 this.attrs_
.labelsDiv
= div
;
456 * Create the text box to adjust the averaging period
457 * @return {Object} The newly-created text box
460 Dygraph
.prototype.createRollInterface_
= function() {
461 var display
= this.attr_('showRoller') ? "block" : "none";
462 var textAttr
= { "position": "absolute",
464 "top": (this.plotter_
.area
.h
- 25) + "px",
465 "left": (this.plotter_
.area
.x
+ 1) + "px",
468 var roller
= document
.createElement("input");
469 roller
.type
= "text";
471 roller
.value
= this.rollPeriod_
;
472 for (var name
in textAttr
) {
473 roller
.style
[name
] = textAttr
[name
];
476 var pa
= this.graphDiv
;
477 pa
.appendChild(roller
);
479 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
483 // These functions are taken from MochiKit.Signal
484 Dygraph
.pageX
= function(e
) {
486 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
489 var b
= document
.body
;
491 (de
.scrollLeft
|| b
.scrollLeft
) -
492 (de
.clientLeft
|| 0);
496 Dygraph
.pageY
= function(e
) {
498 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
501 var b
= document
.body
;
503 (de
.scrollTop
|| b
.scrollTop
) -
509 * Set up all the mouse handlers needed to capture dragging behavior for zoom
513 Dygraph
.prototype.createDragInterface_
= function() {
516 // Tracks whether the mouse is down right now
517 var mouseDown
= false;
518 var dragStartX
= null;
519 var dragStartY
= null;
524 // Utility function to convert page-wide coordinates to canvas coords
527 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
528 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
530 // Draw zoom rectangles when the mouse is down and the user moves around
531 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
533 dragEndX
= getX(event
);
534 dragEndY
= getY(event
);
536 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
541 // Track the beginning of drag events
542 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
544 px
= Dygraph
.findPosX(self
.canvas_
);
545 py
= Dygraph
.findPosY(self
.canvas_
);
546 dragStartX
= getX(event
);
547 dragStartY
= getY(event
);
550 // If the user releases the mouse button during a drag, but not over the
551 // canvas, then it doesn't count as a zooming action.
552 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
560 // Temporarily cancel the dragging event when the mouse leaves the graph
561 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
568 // If the mouse is released on the canvas during a drag event, then it's a
569 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
570 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
573 dragEndX
= getX(event
);
574 dragEndY
= getY(event
);
575 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
576 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
578 if (regionWidth
< 2 && regionHeight
< 2 &&
579 self
.attr_('clickCallback') != null &&
580 self
.lastx_
!= undefined
) {
581 // TODO(danvk): pass along more info about the points.
582 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
585 if (regionWidth
>= 10) {
586 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
587 Math
.max(dragStartX
, dragEndX
));
589 self
.canvas_
.getContext("2d").clearRect(0, 0,
591 self
.canvas_
.height
);
599 // Double-clicking zooms back out
600 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
601 if (self
.dateWindow_
== null) return;
602 self
.dateWindow_
= null;
603 self
.drawGraph_(self
.rawData_
);
604 var minDate
= self
.rawData_
[0][0];
605 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
606 if (self
.attr_("zoomCallback")) {
607 self
.attr_("zoomCallback")(minDate
, maxDate
);
613 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
614 * up any previous zoom rectangles that were drawn. This could be optimized to
615 * avoid extra redrawing, but it's tricky to avoid interactions with the status
617 * @param {Number} startX The X position where the drag started, in canvas
619 * @param {Number} endX The current X position of the drag, in canvas coords.
620 * @param {Number} prevEndX The value of endX on the previous call to this
621 * function. Used to avoid excess redrawing
624 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
625 var ctx
= this.canvas_
.getContext("2d");
627 // Clean up from the previous rect if necessary
629 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
630 Math
.abs(startX
- prevEndX
), this.height_
);
633 // Draw a light-grey rectangle to show the new viewing area
634 if (endX
&& startX
) {
635 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
636 ctx
.fillRect(Math
.min(startX
, endX
), 0,
637 Math
.abs(endX
- startX
), this.height_
);
642 * Zoom to something containing [lowX, highX]. These are pixel coordinates
643 * in the canvas. The exact zoom window may be slightly larger if there are no
644 * data points near lowX or highX. This function redraws the graph.
645 * @param {Number} lowX The leftmost pixel value that should be visible.
646 * @param {Number} highX The rightmost pixel value that should be visible.
649 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
650 // Find the earliest and latest dates contained in this canvasx range.
651 var points
= this.layout_
.points
;
654 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
655 for (var i
= 0; i
< points
.length
; i
++) {
656 var cx
= points
[i
].canvasx
;
657 var x
= points
[i
].xval
;
658 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
659 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
661 // Use the extremes if either is missing
662 if (minDate
== null) minDate
= points
[0].xval
;
663 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
665 this.dateWindow_
= [minDate
, maxDate
];
666 this.drawGraph_(this.rawData_
);
667 if (this.attr_("zoomCallback")) {
668 this.attr_("zoomCallback")(minDate
, maxDate
);
673 * When the mouse moves in the canvas, display information about a nearby data
674 * point and draw dots over those points in the data series. This function
675 * takes care of cleanup of previously-drawn dots.
676 * @param {Object} event The mousemove event from the browser.
679 Dygraph
.prototype.mouseMove_
= function(event
) {
680 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
681 var points
= this.layout_
.points
;
686 // Loop through all the points and find the date nearest to our current
688 var minDist
= 1e+100;
690 for (var i
= 0; i
< points
.length
; i
++) {
691 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
692 if (dist
> minDist
) break;
696 if (idx
>= 0) lastx
= points
[idx
].xval
;
697 // Check that you can really highlight the last day's data
698 if (canvasx
> points
[points
.length
-1].canvasx
)
699 lastx
= points
[points
.length
-1].xval
;
701 // Extract the points we've selected
702 this.selPoints_
= [];
703 for (var i
= 0; i
< points
.length
; i
++) {
704 if (points
[i
].xval
== lastx
) {
705 this.selPoints_
.push(points
[i
]);
709 if (this.attr_("highlightCallback")) {
710 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
713 // Clear the previously drawn vertical, if there is one
714 var circleSize
= this.attr_('highlightCircleSize');
715 var ctx
= this.canvas_
.getContext("2d");
716 if (this.previousVerticalX_
>= 0) {
717 var px
= this.previousVerticalX_
;
718 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
721 var isOK
= function(x
) { return x
&& !isNaN(x
); };
723 if (this.selPoints_
.length
> 0) {
724 var canvasx
= this.selPoints_
[0].canvasx
;
726 // Set the status message to indicate the selected point(s)
727 var replace
= this.attr_('xValueFormatter')(lastx
, this) + ":";
728 var clen
= this.colors_
.length
;
729 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
730 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
731 if (this.attr_("labelsSeparateLines")) {
734 var point
= this.selPoints_
[i
];
735 var c
= new RGBColor(this.colors_
[i
%clen
]);
736 replace
+= " <b><font color='" + c
.toHex() + "'>"
737 + point
.name
+ "</font></b>:"
738 + this.round_(point
.yval
, 2);
740 this.attr_("labelsDiv").innerHTML
= replace
;
742 // Save last x position for callbacks.
745 // Draw colored circles over the center of each selected point
747 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
748 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
750 ctx
.fillStyle
= this.colors_
[i
%clen
];
751 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
757 this.previousVerticalX_
= canvasx
;
762 * The mouse has left the canvas. Clear out whatever artifacts remain
763 * @param {Object} event the mouseout event from the browser.
766 Dygraph
.prototype.mouseOut_
= function(event
) {
767 // Get rid of the overlay data
768 var ctx
= this.canvas_
.getContext("2d");
769 ctx
.clearRect(0, 0, this.width_
, this.height_
);
770 this.attr_("labelsDiv").innerHTML
= "";
773 Dygraph
.zeropad
= function(x
) {
774 if (x
< 10) return "0" + x
; else return "" + x
;
778 * Return a string version of the hours, minutes and seconds portion of a date.
779 * @param {Number} date The JavaScript date (ms since epoch)
780 * @return {String} A time of the form "HH:MM:SS"
783 Dygraph
.prototype.hmsString_
= function(date
) {
784 var zeropad
= Dygraph
.zeropad
;
785 var d
= new Date(date
);
786 if (d
.getSeconds()) {
787 return zeropad(d
.getHours()) + ":" +
788 zeropad(d
.getMinutes()) + ":" +
789 zeropad(d
.getSeconds());
790 } else if (d
.getMinutes()) {
791 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
793 return zeropad(d
.getHours());
798 * Convert a JS date (millis since epoch) to YYYY/MM/DD
799 * @param {Number} date The JavaScript date (ms since epoch)
800 * @return {String} A date of the form "YYYY/MM/DD"
802 * TODO(danvk): why is this part of the prototype?
804 Dygraph
.dateString_
= function(date
, self
) {
805 var zeropad
= Dygraph
.zeropad
;
806 var d
= new Date(date
);
809 var year
= "" + d
.getFullYear();
810 // Get a 0 padded month string
811 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
812 // Get a 0 padded day string
813 var day
= zeropad(d
.getDate());
816 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
817 if (frac
) ret
= " " + self
.hmsString_(date
);
819 return year
+ "/" + month + "/" + day
+ ret
;
823 * Round a number to the specified number of digits past the decimal point.
824 * @param {Number} num The number to round
825 * @param {Number} places The number of decimals to which to round
826 * @return {Number} The rounded number
829 Dygraph
.prototype.round_
= function(num
, places
) {
830 var shift
= Math
.pow(10, places
);
831 return Math
.round(num
* shift
)/shift
;
835 * Fires when there's data available to be graphed.
836 * @param {String} data Raw CSV data to be plotted
839 Dygraph
.prototype.loadedEvent_
= function(data
) {
840 this.rawData_
= this.parseCSV_(data
);
841 this.drawGraph_(this.rawData_
);
844 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
845 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
846 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
849 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
852 Dygraph
.prototype.addXTicks_
= function() {
853 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
854 var startDate
, endDate
;
855 if (this.dateWindow_
) {
856 startDate
= this.dateWindow_
[0];
857 endDate
= this.dateWindow_
[1];
859 startDate
= this.rawData_
[0][0];
860 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
863 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
864 this.layout_
.updateOptions({xTicks
: xTicks
});
867 // Time granularity enumeration
868 Dygraph
.SECONDLY
= 0;
869 Dygraph
.TEN_SECONDLY
= 1;
870 Dygraph
.THIRTY_SECONDLY
= 2;
871 Dygraph
.MINUTELY
= 3;
872 Dygraph
.TEN_MINUTELY
= 4;
873 Dygraph
.THIRTY_MINUTELY
= 5;
875 Dygraph
.SIX_HOURLY
= 7;
878 Dygraph
.MONTHLY
= 10;
879 Dygraph
.QUARTERLY
= 11;
880 Dygraph
.BIANNUAL
= 12;
882 Dygraph
.DECADAL
= 14;
883 Dygraph
.NUM_GRANULARITIES
= 15;
885 Dygraph
.SHORT_SPACINGS
= [];
886 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
887 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
888 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
889 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
890 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
891 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
892 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
893 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600 * 6;
894 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
895 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
899 // If we used this time granularity, how many ticks would there be?
900 // This is only an approximation, but it's generally good enough.
902 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
903 if (granularity
< Dygraph
.MONTHLY
) {
904 // Generate one tick mark for every fixed interval of time.
905 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
906 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
908 var year_mod
= 1; // e.g. to only print one point every 10 years.
910 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
911 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
912 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
913 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
915 var msInYear
= 365.2524 * 24 * 3600 * 1000;
916 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
917 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
923 // Construct an x-axis of nicely-formatted times on meaningful boundaries
924 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
926 // Returns an array containing {v: millis, label: label} dictionaries.
928 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
930 if (granularity
< Dygraph
.MONTHLY
) {
931 // Generate one tick mark for every fixed interval of time.
932 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
933 var format
= '%d%b'; // e.g. "1 Jan"
934 // TODO(danvk): be smarter about making sure this really hits a "nice" time.
935 if (granularity
< Dygraph
.HOURLY
) {
936 start_time
= spacing
* Math
.floor(0.5 + start_time
/ spacing
);
938 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
940 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
941 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
942 // the extra hour covers DST problems.
943 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
945 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
949 // Display a tick mark on the first of a set of months of each year.
950 // Years get a tick mark iff y % year_mod == 0. This is useful for
951 // displaying a tick mark once every 10 years, say, on long time scales.
953 var year_mod
= 1; // e.g. to only print one point every 10 years.
955 if (granularity
== Dygraph
.MONTHLY
) {
956 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
957 } else if (granularity
== Dygraph
.QUARTERLY
) {
958 months
= [ 0, 3, 6, 9 ];
959 } else if (granularity
== Dygraph
.BIANNUAL
) {
961 } else if (granularity
== Dygraph
.ANNUAL
) {
963 } else if (granularity
== Dygraph
.DECADAL
) {
968 var start_year
= new Date(start_time
).getFullYear();
969 var end_year
= new Date(end_time
).getFullYear();
970 var zeropad
= Dygraph
.zeropad
;
971 for (var i
= start_year
; i
<= end_year
; i
++) {
972 if (i
% year_mod
!= 0) continue;
973 for (var j
= 0; j
< months
.length
; j
++) {
974 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
975 var t
= Date
.parse(date_str
);
976 if (t
< start_time
|| t
> end_time
) continue;
977 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
987 * Add ticks to the x-axis based on a date range.
988 * @param {Number} startDate Start of the date window (millis since epoch)
989 * @param {Number} endDate End of the date window (millis since epoch)
990 * @return {Array.<Object>} Array of {label, value} tuples.
993 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
995 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
996 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
997 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1004 return self
.GetXAxis(startDate
, endDate
, chosen
);
1006 // TODO(danvk): signal error.
1011 * Add ticks when the x axis has numbers on it (instead of dates)
1012 * @param {Number} startDate Start of the date window (millis since epoch)
1013 * @param {Number} endDate End of the date window (millis since epoch)
1014 * @return {Array.<Object>} Array of {label, value} tuples.
1017 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1019 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1020 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1021 // The first spacing greater than pixelsPerYLabel is what we use.
1022 var mults
= [1, 2, 5];
1023 var scale
, low_val
, high_val
, nTicks
;
1024 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1025 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1026 for (var i
= -10; i
< 50; i
++) {
1027 var base_scale
= Math
.pow(10, i
);
1028 for (var j
= 0; j
< mults
.length
; j
++) {
1029 scale
= base_scale
* mults
[j
];
1030 low_val
= Math
.floor(minV
/ scale
) * scale
;
1031 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1032 nTicks
= (high_val
- low_val
) / scale
;
1033 var spacing
= self
.height_
/ nTicks
;
1034 // wish I could break out of both loops at once...
1035 if (spacing
> pixelsPerTick
) break;
1037 if (spacing
> pixelsPerTick
) break;
1040 // Construct labels for the ticks
1042 for (var i
= 0; i
< nTicks
; i
++) {
1043 var tickV
= low_val
+ i
* scale
;
1044 var label
= self
.round_(tickV
, 2);
1045 if (self
.attr_("labelsKMB")) {
1047 if (tickV
>= k
*k
*k
) {
1048 label
= self
.round_(tickV
/(k
*k
*k
), 1) + "B";
1049 } else if (tickV
>= k
*k
) {
1050 label
= self
.round_(tickV
/(k
*k
), 1) + "M";
1051 } else if (tickV
>= k
) {
1052 label
= self
.round_(tickV
/k
, 1) + "K";
1055 ticks
.push( {label
: label
, v
: tickV
} );
1061 * Adds appropriate ticks on the y-axis
1062 * @param {Number} minY The minimum Y value in the data set
1063 * @param {Number} maxY The maximum Y value in the data set
1066 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1067 // Set the number of ticks so that the labels are human-friendly.
1068 // TODO(danvk): make this an attribute as well.
1069 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1070 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1074 // Computes the range of the data series (including confidence intervals).
1075 // series is either [ [x1, y1], [x2, y2], ... ] or
1076 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1077 // Returns [low, high]
1078 Dygraph
.prototype.extremeValues_
= function(series
) {
1079 var minY
= null, maxY
= null;
1081 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1083 // With custom bars, maxY is the max of the high values.
1084 for (var j
= 0; j
< series
.length
; j
++) {
1085 var y
= series
[j
][1][0];
1087 var low
= y
- series
[j
][1][1];
1088 var high
= y
+ series
[j
][1][2];
1089 if (low
> y
) low
= y
; // this can happen with custom bars,
1090 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1091 if (maxY
== null || high
> maxY
) {
1094 if (minY
== null || low
< minY
) {
1099 for (var j
= 0; j
< series
.length
; j
++) {
1100 var y
= series
[j
][1];
1102 if (maxY
== null || y
> maxY
) {
1105 if (minY
== null || y
< minY
) {
1111 return [minY
, maxY
];
1115 * Update the graph with new data. Data is in the format
1116 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1117 * or, if errorBars=true,
1118 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1119 * @param {Array.<Object>} data The data (see above)
1122 Dygraph
.prototype.drawGraph_
= function(data
) {
1123 var minY
= null, maxY
= null;
1124 this.layout_
.removeAllDatasets();
1126 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1128 // Loop over all fields in the dataset
1129 for (var i
= 1; i
< data
[0].length
; i
++) {
1131 for (var j
= 0; j
< data
.length
; j
++) {
1132 var date
= data
[j
][0];
1133 series
[j
] = [date
, data
[j
][i
]];
1135 series
= this.rollingAverage(series
, this.rollPeriod_
);
1137 // Prune down to the desired range, if necessary (for zooming)
1138 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1139 if (this.dateWindow_
) {
1140 var low
= this.dateWindow_
[0];
1141 var high
= this.dateWindow_
[1];
1143 for (var k
= 0; k
< series
.length
; k
++) {
1144 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
1145 pruned
.push(series
[k
]);
1151 var extremes
= this.extremeValues_(series
);
1152 var thisMinY
= extremes
[0];
1153 var thisMaxY
= extremes
[1];
1154 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1155 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1159 for (var j
=0; j
<series
.length
; j
++)
1160 vals
[j
] = [series
[j
][0],
1161 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1162 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1164 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1168 // Use some heuristics to come up with a good maxY value, unless it's been
1169 // set explicitly by the user.
1170 if (this.valueRange_
!= null) {
1171 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1173 // Add some padding and round up to an integer to be human-friendly.
1174 var span
= maxY
- minY
;
1175 var maxAxisY
= maxY
+ 0.1 * span
;
1176 var minAxisY
= minY
- 0.1 * span
;
1178 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1179 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1180 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1182 if (this.attr_("includeZero")) {
1183 if (maxY
< 0) maxAxisY
= 0;
1184 if (minY
> 0) minAxisY
= 0;
1187 this.addYTicks_(minAxisY
, maxAxisY
);
1192 // Tell PlotKit to use this new data and render itself
1193 this.layout_
.evaluateWithError();
1194 this.plotter_
.clear();
1195 this.plotter_
.render();
1196 this.canvas_
.getContext('2d').clearRect(0, 0,
1197 this.canvas_
.width
, this.canvas_
.height
);
1201 * Calculates the rolling average of a data set.
1202 * If originalData is [label, val], rolls the average of those.
1203 * If originalData is [label, [, it's interpreted as [value, stddev]
1204 * and the roll is returned in the same form, with appropriately reduced
1205 * stddev for each value.
1206 * Note that this is where fractional input (i.e. '5/10') is converted into
1208 * @param {Array} originalData The data in the appropriate format (see above)
1209 * @param {Number} rollPeriod The number of days over which to average the data
1211 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1212 if (originalData
.length
< 2)
1213 return originalData
;
1214 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1215 var rollingData
= [];
1216 var sigma
= this.attr_("sigma");
1218 if (this.fractions_
) {
1220 var den
= 0; // numerator/denominator
1222 for (var i
= 0; i
< originalData
.length
; i
++) {
1223 num
+= originalData
[i
][1][0];
1224 den
+= originalData
[i
][1][1];
1225 if (i
- rollPeriod
>= 0) {
1226 num
-= originalData
[i
- rollPeriod
][1][0];
1227 den
-= originalData
[i
- rollPeriod
][1][1];
1230 var date
= originalData
[i
][0];
1231 var value
= den
? num
/ den
: 0.0;
1232 if (this.attr_("errorBars")) {
1233 if (this.wilsonInterval_
) {
1234 // For more details on this confidence interval, see:
1235 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1237 var p
= value
< 0 ? 0 : value
, n
= den
;
1238 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1239 var denom
= 1 + sigma
* sigma
/ den
;
1240 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1241 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1242 rollingData
[i
] = [date
,
1243 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1245 rollingData
[i
] = [date
, [0, 0, 0]];
1248 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1249 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1252 rollingData
[i
] = [date
, mult
* value
];
1255 } else if (this.attr_("customBars")) {
1260 for (var i
= 0; i
< originalData
.length
; i
++) {
1261 var data
= originalData
[i
][1];
1263 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1269 if (i
- rollPeriod
>= 0) {
1270 var prev
= originalData
[i
- rollPeriod
];
1276 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1277 1.0 * (mid
- low
) / count
,
1278 1.0 * (high
- mid
) / count
]];
1281 // Calculate the rolling average for the first rollPeriod - 1 points where
1282 // there is not enough data to roll over the full number of days
1283 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1284 if (!this.attr_("errorBars")){
1285 if (rollPeriod
== 1) {
1286 return originalData
;
1289 for (var i
= 0; i
< originalData
.length
; i
++) {
1292 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1293 var y
= originalData
[j
][1];
1294 if (!y
|| isNaN(y
)) continue;
1296 sum
+= originalData
[j
][1];
1299 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1301 rollingData
[i
] = [originalData
[i
][0], null];
1306 for (var i
= 0; i
< originalData
.length
; i
++) {
1310 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1311 var y
= originalData
[j
][1][0];
1312 if (!y
|| isNaN(y
)) continue;
1314 sum
+= originalData
[j
][1][0];
1315 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1318 var stddev
= Math
.sqrt(variance
) / num_ok
;
1319 rollingData
[i
] = [originalData
[i
][0],
1320 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1322 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1332 * Parses a date, returning the number of milliseconds since epoch. This can be
1333 * passed in as an xValueParser in the Dygraph constructor.
1334 * TODO(danvk): enumerate formats that this understands.
1335 * @param {String} A date in YYYYMMDD format.
1336 * @return {Number} Milliseconds since epoch.
1339 Dygraph
.dateParser
= function(dateStr
, self
) {
1342 if (dateStr
.length
== 10 && dateStr
.search("-") != -1) { // e.g. '2009-07-12'
1343 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1344 while (dateStrSlashed
.search("-") != -1) {
1345 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1347 d
= Date
.parse(dateStrSlashed
);
1348 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1349 // TODO(danvk): remove support for this format. It's confusing.
1350 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1351 + "/" + dateStr
.substr(6,2);
1352 d
= Date
.parse(dateStrSlashed
);
1354 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1355 // "2009/07/12 12:34:56"
1356 d
= Date
.parse(dateStr
);
1359 if (!d
|| isNaN(d
)) {
1360 self
.error("Couldn't parse " + dateStr
+ " as a date");
1366 * Detects the type of the str (date or numeric) and sets the various
1367 * formatting attributes in this.attrs_ based on this type.
1368 * @param {String} str An x value.
1371 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1373 if (str
.indexOf('-') >= 0 ||
1374 str
.indexOf('/') >= 0 ||
1375 isNaN(parseFloat(str
))) {
1377 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1378 // TODO(danvk): remove support for this format.
1383 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1384 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1385 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1387 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1388 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1389 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1394 * Parses a string in a special csv format. We expect a csv file where each
1395 * line is a date point, and the first field in each line is the date string.
1396 * We also expect that all remaining fields represent series.
1397 * if the errorBars attribute is set, then interpret the fields as:
1398 * date, series1, stddev1, series2, stddev2, ...
1399 * @param {Array.<Object>} data See above.
1402 * @return Array.<Object> An array with one entry for each row. These entries
1403 * are an array of cells in that row. The first entry is the parsed x-value for
1404 * the row. The second, third, etc. are the y-values. These can take on one of
1405 * three forms, depending on the CSV and constructor parameters:
1407 * 2. [ value, stddev ]
1408 * 3. [ low value, center value, high value ]
1410 Dygraph
.prototype.parseCSV_
= function(data
) {
1412 var lines
= data
.split("\n");
1414 // Use the default delimiter or fall back to a tab if that makes sense.
1415 var delim
= this.attr_('delimiter');
1416 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1421 if (this.labelsFromCSV_
) {
1423 this.attrs_
.labels
= lines
[0].split(delim
);
1427 var defaultParserSet
= false; // attempt to auto-detect x value type
1428 var expectedCols
= this.attr_("labels").length
;
1429 for (var i
= start
; i
< lines
.length
; i
++) {
1430 var line
= lines
[i
];
1431 if (line
.length
== 0) continue; // skip blank lines
1432 if (line
[0] == '#') continue; // skip comment lines
1433 var inFields
= line
.split(delim
);
1434 if (inFields
.length
< 2) continue;
1437 if (!defaultParserSet
) {
1438 this.detectTypeFromString_(inFields
[0]);
1439 xParser
= this.attr_("xValueParser");
1440 defaultParserSet
= true;
1442 fields
[0] = xParser(inFields
[0], this);
1444 // If fractions are expected, parse the numbers as "A/B
"
1445 if (this.fractions_) {
1446 for (var j = 1; j < inFields.length; j++) {
1447 // TODO(danvk): figure out an appropriate way to flag parse errors.
1448 var vals = inFields[j].split("/");
1449 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1451 } else if (this.attr_("errorBars
")) {
1452 // If there are error bars, values are (value, stddev) pairs
1453 for (var j = 1; j < inFields.length; j += 2)
1454 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1455 parseFloat(inFields[j + 1])];
1456 } else if (this.attr_("customBars
")) {
1457 // Bars are a low;center;high tuple
1458 for (var j = 1; j < inFields.length; j++) {
1459 var vals = inFields[j].split(";");
1460 fields[j] = [ parseFloat(vals[0]),
1461 parseFloat(vals[1]),
1462 parseFloat(vals[2]) ];
1465 // Values are just numbers
1466 for (var j = 1; j < inFields.length; j++) {
1467 fields[j] = parseFloat(inFields[j]);
1472 if (fields.length != expectedCols) {
1473 this.error("Number of columns
in line
" + i + " (" + fields.length +
1474 ") does not agree
with number of
labels (" + expectedCols +
1482 * The user has provided their data as a pre-packaged JS array. If the x values
1483 * are numeric, this is the same as dygraphs' internal format. If the x values
1484 * are dates, we need to convert them from Date objects to ms since epoch.
1485 * @param {Array.<Object>} data
1486 * @return {Array.<Object>} data with numeric x values.
1488 Dygraph.prototype.parseArray_ = function(data) {
1489 // Peek at the first x value to see if it's numeric.
1490 if (data.length == 0) {
1491 this.error("Can
't plot empty data set");
1494 if (data[0].length == 0) {
1495 this.error("Data set cannot contain an empty row");
1499 if (this.attr_("labels") == null) {
1500 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1501 "in the options parameter");
1502 this.attrs_.labels = [ "X" ];
1503 for (var i = 1; i < data[0].length; i++) {
1504 this.attrs_.labels.push("Y" + i);
1508 if (Dygraph.isDateLike(data[0][0])) {
1509 // Some intelligent defaults for a date x-axis.
1510 this.attrs_.xValueFormatter = Dygraph.dateString_;
1511 this.attrs_.xTicker = Dygraph.dateTicker;
1513 // Assume they're all dates
.
1514 var parsedData
= Dygraph
.clone(data
);
1515 for (var i
= 0; i
< data
.length
; i
++) {
1516 if (parsedData
[i
].length
== 0) {
1517 this.error("Row " << (1 + i
) << " of data is empty");
1520 if (parsedData
[i
][0] == null
1521 || typeof(parsedData
[i
][0].getTime
) != 'function') {
1522 this.error("x value in row " << (1 + i
) << " is not a Date");
1525 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1529 // Some intelligent defaults for a numeric x-axis.
1530 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1531 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1537 * Parses a DataTable object from gviz.
1538 * The data is expected to have a first column that is either a date or a
1539 * number. All subsequent columns must be numbers. If there is a clear mismatch
1540 * between this.xValueParser_ and the type of the first column, it will be
1541 * fixed. Returned value is in the same format as return value of parseCSV_.
1542 * @param {Array.<Object>} data See above.
1545 Dygraph
.prototype.parseDataTable_
= function(data
) {
1546 var cols
= data
.getNumberOfColumns();
1547 var rows
= data
.getNumberOfRows();
1549 // Read column labels
1551 for (var i
= 0; i
< cols
; i
++) {
1552 labels
.push(data
.getColumnLabel(i
));
1554 this.attrs_
.labels
= labels
;
1556 var indepType
= data
.getColumnType(0);
1557 if (indepType
== 'date') {
1558 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1559 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1560 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1561 } else if (indepType
== 'number') {
1562 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1563 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1564 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1566 this.error("only 'date' and 'number' types are supported for column 1 " +
1567 "of DataTable input (Got '" + indepType
+ "')");
1572 for (var i
= 0; i
< rows
; i
++) {
1574 if (!data
.getValue(i
, 0)) continue;
1575 if (indepType
== 'date') {
1576 row
.push(data
.getValue(i
, 0).getTime());
1578 row
.push(data
.getValue(i
, 0));
1580 for (var j
= 1; j
< cols
; j
++) {
1581 row
.push(data
.getValue(i
, j
));
1588 // These functions are all based on MochiKit.
1589 Dygraph
.update
= function (self
, o
) {
1590 if (typeof(o
) != 'undefined' && o
!== null) {
1598 Dygraph
.isArrayLike
= function (o
) {
1599 var typ
= typeof(o
);
1601 (typ
!= 'object' && !(typ
== 'function' &&
1602 typeof(o
.item
) == 'function')) ||
1604 typeof(o
.length
) != 'number' ||
1612 Dygraph
.isDateLike
= function (o
) {
1613 if (typeof(o
) != "object" || o
=== null ||
1614 typeof(o
.getTime
) != 'function') {
1620 Dygraph
.clone
= function(o
) {
1621 // TODO(danvk): figure out how MochiKit's version works
1623 for (var i
= 0; i
< o
.length
; i
++) {
1624 if (Dygraph
.isArrayLike(o
[i
])) {
1625 r
.push(Dygraph
.clone(o
[i
]));
1635 * Get the CSV data. If it's in a function, call that function. If it's in a
1636 * file, do an XMLHttpRequest to get it.
1639 Dygraph
.prototype.start_
= function() {
1640 if (typeof this.file_
== 'function') {
1641 // CSV string. Pretend we got it via XHR.
1642 this.loadedEvent_(this.file_());
1643 } else if (Dygraph
.isArrayLike(this.file_
)) {
1644 this.rawData_
= this.parseArray_(this.file_
);
1645 this.drawGraph_(this.rawData_
);
1646 } else if (typeof this.file_
== 'object' &&
1647 typeof this.file_
.getColumnRange
== 'function') {
1648 // must be a DataTable from gviz.
1649 this.rawData_
= this.parseDataTable_(this.file_
);
1650 this.drawGraph_(this.rawData_
);
1651 } else if (typeof this.file_
== 'string') {
1652 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
1653 if (this.file_
.indexOf('\n') >= 0) {
1654 this.loadedEvent_(this.file_
);
1656 var req
= new XMLHttpRequest();
1658 req
.onreadystatechange
= function () {
1659 if (req
.readyState
== 4) {
1660 if (req
.status
== 200) {
1661 caller
.loadedEvent_(req
.responseText
);
1666 req
.open("GET", this.file_
, true);
1670 this.error("Unknown data format: " + (typeof this.file_
));
1675 * Changes various properties of the graph. These can include:
1677 * <li>file: changes the source data for the graph</li>
1678 * <li>errorBars: changes whether the data contains stddev</li>
1680 * @param {Object} attrs The new properties and values
1682 Dygraph
.prototype.updateOptions
= function(attrs
) {
1683 // TODO(danvk): this is a mess. Rethink this function.
1684 if (attrs
.rollPeriod
) {
1685 this.rollPeriod_
= attrs
.rollPeriod
;
1687 if (attrs
.dateWindow
) {
1688 this.dateWindow_
= attrs
.dateWindow
;
1690 if (attrs
.valueRange
) {
1691 this.valueRange_
= attrs
.valueRange
;
1693 Dygraph
.update(this.user_attrs_
, attrs
);
1695 this.labelsFromCSV_
= (this.attr_("labels") == null);
1697 // TODO(danvk): this doesn't match the constructor logic
1698 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
1699 if (attrs
['file'] && attrs
['file'] != this.file_
) {
1700 this.file_
= attrs
['file'];
1703 this.drawGraph_(this.rawData_
);
1708 * Adjusts the number of days in the rolling average. Updates the graph to
1709 * reflect the new averaging period.
1710 * @param {Number} length Number of days over which to average the data.
1712 Dygraph
.prototype.adjustRoll
= function(length
) {
1713 this.rollPeriod_
= length
;
1714 this.drawGraph_(this.rawData_
);
1719 * A wrapper around Dygraph that implements the gviz API.
1720 * @param {Object} container The DOM object the visualization should live in.
1722 Dygraph
.GVizChart
= function(container
) {
1723 this.container
= container
;
1726 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
1727 this.container
.innerHTML
= '';
1728 this.date_graph
= new Dygraph(this.container
, data
, options
);
1731 // Older pages may still use this name.
1732 DateGraph
= Dygraph
;