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_
= { 'xOriginIsZero': false };
197 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
198 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
199 Dygraph
.update(this.layoutOptions_
, {
200 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
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_
= Dygraph
.createCanvas();
298 this.canvas_
.style
.position
= "absolute";
299 this.canvas_
.width
= this.width_
;
300 this.canvas_
.height
= this.height_
;
301 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
302 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
303 this.graphDiv
.appendChild(this.canvas_
);
305 // ... and for static parts of the chart.
306 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
309 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
310 dygraph
.mouseMove_(e
);
312 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
313 dygraph
.mouseOut_(e
);
318 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
319 * this particular canvas. All Dygraph work is done on this.canvas_.
320 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
321 * @return {Object} The newly-created canvas
324 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
325 // var h = document.createElement("canvas");
326 var h
= Dygraph
.createCanvas();
327 h
.style
.position
= "absolute";
328 h
.style
.top
= canvas
.style
.top
;
329 h
.style
.left
= canvas
.style
.left
;
330 h
.width
= this.width_
;
331 h
.height
= this.height_
;
332 h
.style
.width
= this.width_
+ "px"; // for IE
333 h
.style
.height
= this.height_
+ "px"; // for IE
334 this.graphDiv
.appendChild(h
);
338 // Taken from MochiKit.Color
339 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
343 if (saturation
=== 0) {
348 var i
= Math
.floor(hue
* 6);
349 var f
= (hue
* 6) - i
;
350 var p
= value
* (1 - saturation
);
351 var q
= value
* (1 - (saturation
* f
));
352 var t
= value
* (1 - (saturation
* (1 - f
)));
354 case 1: red
= q
; green
= value
; blue
= p
; break;
355 case 2: red
= p
; green
= value
; blue
= t
; break;
356 case 3: red
= p
; green
= q
; blue
= value
; break;
357 case 4: red
= t
; green
= p
; blue
= value
; break;
358 case 5: red
= value
; green
= p
; blue
= q
; break;
359 case 6: // fall through
360 case 0: red
= value
; green
= t
; blue
= p
; break;
363 red
= Math
.floor(255 * red
+ 0.5);
364 green
= Math
.floor(255 * green
+ 0.5);
365 blue
= Math
.floor(255 * blue
+ 0.5);
366 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
371 * Generate a set of distinct colors for the data series. This is done with a
372 * color wheel. Saturation/Value are customizable, and the hue is
373 * equally-spaced around the color wheel. If a custom set of colors is
374 * specified, that is used instead.
377 Dygraph
.prototype.setColors_
= function() {
378 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
379 // away with this.renderOptions_.
380 var num
= this.attr_("labels").length
- 1;
382 var colors
= this.attr_('colors');
384 var sat
= this.attr_('colorSaturation') || 1.0;
385 var val
= this.attr_('colorValue') || 0.5;
386 for (var i
= 1; i
<= num
; i
++) {
387 var hue
= (1.0*i
/(1+num
));
388 this.colors_
.push( Dygraph
.hsvToRGB(hue
, sat
, val
) );
391 for (var i
= 0; i
< num
; i
++) {
392 var colorStr
= colors
[i
% colors
.length
];
393 this.colors_
.push(colorStr
);
397 // TODO(danvk): update this w/r
/t/ the
new options system
.
398 this.renderOptions_
.colorScheme
= this.colors_
;
399 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
400 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
401 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
404 // The following functions are from quirksmode.org
405 // http://www.quirksmode.org/js
/findpos
.html
406 Dygraph
.findPosX
= function(obj
) {
408 if (obj
.offsetParent
) {
409 while (obj
.offsetParent
) {
410 curleft
+= obj
.offsetLeft
;
411 obj
= obj
.offsetParent
;
419 Dygraph
.findPosY
= function(obj
) {
421 if (obj
.offsetParent
) {
422 while (obj
.offsetParent
) {
423 curtop
+= obj
.offsetTop
;
424 obj
= obj
.offsetParent
;
433 * Create the div that contains information on the selected point(s)
434 * This goes in the top right of the canvas, unless an external div has already
438 Dygraph
.prototype.createStatusMessage_
= function(){
439 if (!this.attr_("labelsDiv")) {
440 var divWidth
= this.attr_('labelsDivWidth');
442 "position": "absolute",
445 "width": divWidth
+ "px",
447 "left": (this.width_
- divWidth
- 2) + "px",
448 "background": "white",
450 "overflow": "hidden"};
451 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
452 var div
= document
.createElement("div");
453 for (var name
in messagestyle
) {
454 if (messagestyle
.hasOwnProperty(name
)) {
455 div
.style
[name
] = messagestyle
[name
];
458 this.graphDiv
.appendChild(div
);
459 this.attrs_
.labelsDiv
= div
;
464 * Create the text box to adjust the averaging period
465 * @return {Object} The newly-created text box
468 Dygraph
.prototype.createRollInterface_
= function() {
469 var display
= this.attr_('showRoller') ? "block" : "none";
470 var textAttr
= { "position": "absolute",
472 "top": (this.plotter_
.area
.h
- 25) + "px",
473 "left": (this.plotter_
.area
.x
+ 1) + "px",
476 var roller
= document
.createElement("input");
477 roller
.type
= "text";
479 roller
.value
= this.rollPeriod_
;
480 for (var name
in textAttr
) {
481 if (textAttr
.hasOwnProperty(name
)) {
482 roller
.style
[name
] = textAttr
[name
];
486 var pa
= this.graphDiv
;
487 pa
.appendChild(roller
);
489 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
493 // These functions are taken from MochiKit.Signal
494 Dygraph
.pageX
= function(e
) {
496 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
499 var b
= document
.body
;
501 (de
.scrollLeft
|| b
.scrollLeft
) -
502 (de
.clientLeft
|| 0);
506 Dygraph
.pageY
= function(e
) {
508 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
511 var b
= document
.body
;
513 (de
.scrollTop
|| b
.scrollTop
) -
519 * Set up all the mouse handlers needed to capture dragging behavior for zoom
523 Dygraph
.prototype.createDragInterface_
= function() {
526 // Tracks whether the mouse is down right now
527 var mouseDown
= false;
528 var dragStartX
= null;
529 var dragStartY
= null;
534 // Utility function to convert page-wide coordinates to canvas coords
537 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
538 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
540 // Draw zoom rectangles when the mouse is down and the user moves around
541 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
543 dragEndX
= getX(event
);
544 dragEndY
= getY(event
);
546 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
551 // Track the beginning of drag events
552 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
554 px
= Dygraph
.findPosX(self
.canvas_
);
555 py
= Dygraph
.findPosY(self
.canvas_
);
556 dragStartX
= getX(event
);
557 dragStartY
= getY(event
);
560 // If the user releases the mouse button during a drag, but not over the
561 // canvas, then it doesn't count as a zooming action.
562 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
570 // Temporarily cancel the dragging event when the mouse leaves the graph
571 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
578 // If the mouse is released on the canvas during a drag event, then it's a
579 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
580 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
583 dragEndX
= getX(event
);
584 dragEndY
= getY(event
);
585 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
586 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
588 if (regionWidth
< 2 && regionHeight
< 2 &&
589 self
.attr_('clickCallback') != null &&
590 self
.lastx_
!= undefined
) {
591 // TODO(danvk): pass along more info about the points.
592 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
595 if (regionWidth
>= 10) {
596 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
597 Math
.max(dragStartX
, dragEndX
));
599 self
.canvas_
.getContext("2d").clearRect(0, 0,
601 self
.canvas_
.height
);
609 // Double-clicking zooms back out
610 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
611 if (self
.dateWindow_
== null) return;
612 self
.dateWindow_
= null;
613 self
.drawGraph_(self
.rawData_
);
614 var minDate
= self
.rawData_
[0][0];
615 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
616 if (self
.attr_("zoomCallback")) {
617 self
.attr_("zoomCallback")(minDate
, maxDate
);
623 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
624 * up any previous zoom rectangles that were drawn. This could be optimized to
625 * avoid extra redrawing, but it's tricky to avoid interactions with the status
627 * @param {Number} startX The X position where the drag started, in canvas
629 * @param {Number} endX The current X position of the drag, in canvas coords.
630 * @param {Number} prevEndX The value of endX on the previous call to this
631 * function. Used to avoid excess redrawing
634 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
635 var ctx
= this.canvas_
.getContext("2d");
637 // Clean up from the previous rect if necessary
639 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
640 Math
.abs(startX
- prevEndX
), this.height_
);
643 // Draw a light-grey rectangle to show the new viewing area
644 if (endX
&& startX
) {
645 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
646 ctx
.fillRect(Math
.min(startX
, endX
), 0,
647 Math
.abs(endX
- startX
), this.height_
);
652 * Zoom to something containing [lowX, highX]. These are pixel coordinates
653 * in the canvas. The exact zoom window may be slightly larger if there are no
654 * data points near lowX or highX. This function redraws the graph.
655 * @param {Number} lowX The leftmost pixel value that should be visible.
656 * @param {Number} highX The rightmost pixel value that should be visible.
659 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
660 // Find the earliest and latest dates contained in this canvasx range.
661 var points
= this.layout_
.points
;
664 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
665 for (var i
= 0; i
< points
.length
; i
++) {
666 var cx
= points
[i
].canvasx
;
667 var x
= points
[i
].xval
;
668 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
669 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
671 // Use the extremes if either is missing
672 if (minDate
== null) minDate
= points
[0].xval
;
673 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
675 this.dateWindow_
= [minDate
, maxDate
];
676 this.drawGraph_(this.rawData_
);
677 if (this.attr_("zoomCallback")) {
678 this.attr_("zoomCallback")(minDate
, maxDate
);
683 * When the mouse moves in the canvas, display information about a nearby data
684 * point and draw dots over those points in the data series. This function
685 * takes care of cleanup of previously-drawn dots.
686 * @param {Object} event The mousemove event from the browser.
689 Dygraph
.prototype.mouseMove_
= function(event
) {
690 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
691 var points
= this.layout_
.points
;
696 // Loop through all the points and find the date nearest to our current
698 var minDist
= 1e+100;
700 for (var i
= 0; i
< points
.length
; i
++) {
701 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
702 if (dist
> minDist
) break;
706 if (idx
>= 0) lastx
= points
[idx
].xval
;
707 // Check that you can really highlight the last day's data
708 if (canvasx
> points
[points
.length
-1].canvasx
)
709 lastx
= points
[points
.length
-1].xval
;
711 // Extract the points we've selected
712 this.selPoints_
= [];
713 for (var i
= 0; i
< points
.length
; i
++) {
714 if (points
[i
].xval
== lastx
) {
715 this.selPoints_
.push(points
[i
]);
719 if (this.attr_("highlightCallback")) {
720 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
723 // Clear the previously drawn vertical, if there is one
724 var circleSize
= this.attr_('highlightCircleSize');
725 var ctx
= this.canvas_
.getContext("2d");
726 if (this.previousVerticalX_
>= 0) {
727 var px
= this.previousVerticalX_
;
728 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
731 var isOK
= function(x
) { return x
&& !isNaN(x
); };
733 if (this.selPoints_
.length
> 0) {
734 var canvasx
= this.selPoints_
[0].canvasx
;
736 // Set the status message to indicate the selected point(s)
737 var replace
= this.attr_('xValueFormatter')(lastx
, this) + ":";
738 var clen
= this.colors_
.length
;
739 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
740 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
741 if (this.attr_("labelsSeparateLines")) {
744 var point
= this.selPoints_
[i
];
745 var c
= new RGBColor(this.colors_
[i
%clen
]);
746 replace
+= " <b><font color='" + c
.toHex() + "'>"
747 + point
.name
+ "</font></b>:"
748 + this.round_(point
.yval
, 2);
750 this.attr_("labelsDiv").innerHTML
= replace
;
752 // Save last x position for callbacks.
755 // Draw colored circles over the center of each selected point
757 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
758 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
760 ctx
.fillStyle
= this.colors_
[i
%clen
];
761 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
762 0, 2 * Math
.PI
, false);
767 this.previousVerticalX_
= canvasx
;
772 * The mouse has left the canvas. Clear out whatever artifacts remain
773 * @param {Object} event the mouseout event from the browser.
776 Dygraph
.prototype.mouseOut_
= function(event
) {
777 // Get rid of the overlay data
778 var ctx
= this.canvas_
.getContext("2d");
779 ctx
.clearRect(0, 0, this.width_
, this.height_
);
780 this.attr_("labelsDiv").innerHTML
= "";
783 Dygraph
.zeropad
= function(x
) {
784 if (x
< 10) return "0" + x
; else return "" + x
;
788 * Return a string version of the hours, minutes and seconds portion of a date.
789 * @param {Number} date The JavaScript date (ms since epoch)
790 * @return {String} A time of the form "HH:MM:SS"
793 Dygraph
.prototype.hmsString_
= function(date
) {
794 var zeropad
= Dygraph
.zeropad
;
795 var d
= new Date(date
);
796 if (d
.getSeconds()) {
797 return zeropad(d
.getHours()) + ":" +
798 zeropad(d
.getMinutes()) + ":" +
799 zeropad(d
.getSeconds());
800 } else if (d
.getMinutes()) {
801 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
803 return zeropad(d
.getHours());
808 * Convert a JS date (millis since epoch) to YYYY/MM/DD
809 * @param {Number} date The JavaScript date (ms since epoch)
810 * @return {String} A date of the form "YYYY/MM/DD"
812 * TODO(danvk): why is this part of the prototype?
814 Dygraph
.dateString_
= function(date
, self
) {
815 var zeropad
= Dygraph
.zeropad
;
816 var d
= new Date(date
);
819 var year
= "" + d
.getFullYear();
820 // Get a 0 padded month string
821 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
822 // Get a 0 padded day string
823 var day
= zeropad(d
.getDate());
826 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
827 if (frac
) ret
= " " + self
.hmsString_(date
);
829 return year
+ "/" + month + "/" + day
+ ret
;
833 * Round a number to the specified number of digits past the decimal point.
834 * @param {Number} num The number to round
835 * @param {Number} places The number of decimals to which to round
836 * @return {Number} The rounded number
839 Dygraph
.prototype.round_
= function(num
, places
) {
840 var shift
= Math
.pow(10, places
);
841 return Math
.round(num
* shift
)/shift
;
845 * Fires when there's data available to be graphed.
846 * @param {String} data Raw CSV data to be plotted
849 Dygraph
.prototype.loadedEvent_
= function(data
) {
850 this.rawData_
= this.parseCSV_(data
);
851 this.drawGraph_(this.rawData_
);
854 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
855 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
856 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
859 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
862 Dygraph
.prototype.addXTicks_
= function() {
863 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
864 var startDate
, endDate
;
865 if (this.dateWindow_
) {
866 startDate
= this.dateWindow_
[0];
867 endDate
= this.dateWindow_
[1];
869 startDate
= this.rawData_
[0][0];
870 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
873 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
874 this.layout_
.updateOptions({xTicks
: xTicks
});
877 // Time granularity enumeration
878 Dygraph
.SECONDLY
= 0;
879 Dygraph
.TEN_SECONDLY
= 1;
880 Dygraph
.THIRTY_SECONDLY
= 2;
881 Dygraph
.MINUTELY
= 3;
882 Dygraph
.TEN_MINUTELY
= 4;
883 Dygraph
.THIRTY_MINUTELY
= 5;
885 Dygraph
.SIX_HOURLY
= 7;
888 Dygraph
.MONTHLY
= 10;
889 Dygraph
.QUARTERLY
= 11;
890 Dygraph
.BIANNUAL
= 12;
892 Dygraph
.DECADAL
= 14;
893 Dygraph
.NUM_GRANULARITIES
= 15;
895 Dygraph
.SHORT_SPACINGS
= [];
896 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
897 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
898 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
899 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
900 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
901 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
902 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
903 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600 * 6;
904 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
905 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
909 // If we used this time granularity, how many ticks would there be?
910 // This is only an approximation, but it's generally good enough.
912 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
913 if (granularity
< Dygraph
.MONTHLY
) {
914 // Generate one tick mark for every fixed interval of time.
915 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
916 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
918 var year_mod
= 1; // e.g. to only print one point every 10 years.
920 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
921 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
922 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
923 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
925 var msInYear
= 365.2524 * 24 * 3600 * 1000;
926 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
927 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
933 // Construct an x-axis of nicely-formatted times on meaningful boundaries
934 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
936 // Returns an array containing {v: millis, label: label} dictionaries.
938 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
940 if (granularity
< Dygraph
.MONTHLY
) {
941 // Generate one tick mark for every fixed interval of time.
942 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
943 var format
= '%d%b'; // e.g. "1 Jan"
944 // TODO(danvk): be smarter about making sure this really hits a "nice" time.
945 if (granularity
< Dygraph
.HOURLY
) {
946 start_time
= spacing
* Math
.floor(0.5 + start_time
/ spacing
);
948 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
950 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
951 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
952 // the extra hour covers DST problems.
953 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
955 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
959 // Display a tick mark on the first of a set of months of each year.
960 // Years get a tick mark iff y % year_mod == 0. This is useful for
961 // displaying a tick mark once every 10 years, say, on long time scales.
963 var year_mod
= 1; // e.g. to only print one point every 10 years.
965 if (granularity
== Dygraph
.MONTHLY
) {
966 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
967 } else if (granularity
== Dygraph
.QUARTERLY
) {
968 months
= [ 0, 3, 6, 9 ];
969 } else if (granularity
== Dygraph
.BIANNUAL
) {
971 } else if (granularity
== Dygraph
.ANNUAL
) {
973 } else if (granularity
== Dygraph
.DECADAL
) {
978 var start_year
= new Date(start_time
).getFullYear();
979 var end_year
= new Date(end_time
).getFullYear();
980 var zeropad
= Dygraph
.zeropad
;
981 for (var i
= start_year
; i
<= end_year
; i
++) {
982 if (i
% year_mod
!= 0) continue;
983 for (var j
= 0; j
< months
.length
; j
++) {
984 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
985 var t
= Date
.parse(date_str
);
986 if (t
< start_time
|| t
> end_time
) continue;
987 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
997 * Add ticks to the x-axis based on a date range.
998 * @param {Number} startDate Start of the date window (millis since epoch)
999 * @param {Number} endDate End of the date window (millis since epoch)
1000 * @return {Array.<Object>} Array of {label, value} tuples.
1003 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1005 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1006 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1007 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1014 return self
.GetXAxis(startDate
, endDate
, chosen
);
1016 // TODO(danvk): signal error.
1021 * Add ticks when the x axis has numbers on it (instead of dates)
1022 * @param {Number} startDate Start of the date window (millis since epoch)
1023 * @param {Number} endDate End of the date window (millis since epoch)
1024 * @return {Array.<Object>} Array of {label, value} tuples.
1027 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1029 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1030 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1031 // The first spacing greater than pixelsPerYLabel is what we use.
1032 var mults
= [1, 2, 5];
1033 var scale
, low_val
, high_val
, nTicks
;
1034 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1035 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1036 for (var i
= -10; i
< 50; i
++) {
1037 var base_scale
= Math
.pow(10, i
);
1038 for (var j
= 0; j
< mults
.length
; j
++) {
1039 scale
= base_scale
* mults
[j
];
1040 low_val
= Math
.floor(minV
/ scale
) * scale
;
1041 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1042 nTicks
= (high_val
- low_val
) / scale
;
1043 var spacing
= self
.height_
/ nTicks
;
1044 // wish I could break out of both loops at once...
1045 if (spacing
> pixelsPerTick
) break;
1047 if (spacing
> pixelsPerTick
) break;
1050 // Construct labels for the ticks
1052 for (var i
= 0; i
< nTicks
; i
++) {
1053 var tickV
= low_val
+ i
* scale
;
1054 var label
= self
.round_(tickV
, 2);
1055 if (self
.attr_("labelsKMB")) {
1057 if (tickV
>= k
*k
*k
) {
1058 label
= self
.round_(tickV
/(k
*k
*k
), 1) + "B";
1059 } else if (tickV
>= k
*k
) {
1060 label
= self
.round_(tickV
/(k
*k
), 1) + "M";
1061 } else if (tickV
>= k
) {
1062 label
= self
.round_(tickV
/k
, 1) + "K";
1065 ticks
.push( {label
: label
, v
: tickV
} );
1071 * Adds appropriate ticks on the y-axis
1072 * @param {Number} minY The minimum Y value in the data set
1073 * @param {Number} maxY The maximum Y value in the data set
1076 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1077 // Set the number of ticks so that the labels are human-friendly.
1078 // TODO(danvk): make this an attribute as well.
1079 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1080 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1084 // Computes the range of the data series (including confidence intervals).
1085 // series is either [ [x1, y1], [x2, y2], ... ] or
1086 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1087 // Returns [low, high]
1088 Dygraph
.prototype.extremeValues_
= function(series
) {
1089 var minY
= null, maxY
= null;
1091 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1093 // With custom bars, maxY is the max of the high values.
1094 for (var j
= 0; j
< series
.length
; j
++) {
1095 var y
= series
[j
][1][0];
1097 var low
= y
- series
[j
][1][1];
1098 var high
= y
+ series
[j
][1][2];
1099 if (low
> y
) low
= y
; // this can happen with custom bars,
1100 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1101 if (maxY
== null || high
> maxY
) {
1104 if (minY
== null || low
< minY
) {
1109 for (var j
= 0; j
< series
.length
; j
++) {
1110 var y
= series
[j
][1];
1112 if (maxY
== null || y
> maxY
) {
1115 if (minY
== null || y
< minY
) {
1121 return [minY
, maxY
];
1125 * Update the graph with new data. Data is in the format
1126 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1127 * or, if errorBars=true,
1128 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1129 * @param {Array.<Object>} data The data (see above)
1132 Dygraph
.prototype.drawGraph_
= function(data
) {
1133 var minY
= null, maxY
= null;
1134 this.layout_
.removeAllDatasets();
1136 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1138 // Loop over all fields in the dataset
1139 for (var i
= 1; i
< data
[0].length
; i
++) {
1141 for (var j
= 0; j
< data
.length
; j
++) {
1142 var date
= data
[j
][0];
1143 series
[j
] = [date
, data
[j
][i
]];
1145 series
= this.rollingAverage(series
, this.rollPeriod_
);
1147 // Prune down to the desired range, if necessary (for zooming)
1148 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1149 if (this.dateWindow_
) {
1150 var low
= this.dateWindow_
[0];
1151 var high
= this.dateWindow_
[1];
1153 for (var k
= 0; k
< series
.length
; k
++) {
1154 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
1155 pruned
.push(series
[k
]);
1161 var extremes
= this.extremeValues_(series
);
1162 var thisMinY
= extremes
[0];
1163 var thisMaxY
= extremes
[1];
1164 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1165 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1169 for (var j
=0; j
<series
.length
; j
++)
1170 vals
[j
] = [series
[j
][0],
1171 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1172 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1174 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1178 // Use some heuristics to come up with a good maxY value, unless it's been
1179 // set explicitly by the user.
1180 if (this.valueRange_
!= null) {
1181 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1183 // Add some padding and round up to an integer to be human-friendly.
1184 var span
= maxY
- minY
;
1185 var maxAxisY
= maxY
+ 0.1 * span
;
1186 var minAxisY
= minY
- 0.1 * span
;
1188 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1189 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1190 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1192 if (this.attr_("includeZero")) {
1193 if (maxY
< 0) maxAxisY
= 0;
1194 if (minY
> 0) minAxisY
= 0;
1197 this.addYTicks_(minAxisY
, maxAxisY
);
1202 // Tell PlotKit to use this new data and render itself
1203 this.layout_
.evaluateWithError();
1204 this.plotter_
.clear();
1205 this.plotter_
.render();
1206 this.canvas_
.getContext('2d').clearRect(0, 0,
1207 this.canvas_
.width
, this.canvas_
.height
);
1211 * Calculates the rolling average of a data set.
1212 * If originalData is [label, val], rolls the average of those.
1213 * If originalData is [label, [, it's interpreted as [value, stddev]
1214 * and the roll is returned in the same form, with appropriately reduced
1215 * stddev for each value.
1216 * Note that this is where fractional input (i.e. '5/10') is converted into
1218 * @param {Array} originalData The data in the appropriate format (see above)
1219 * @param {Number} rollPeriod The number of days over which to average the data
1221 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1222 if (originalData
.length
< 2)
1223 return originalData
;
1224 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1225 var rollingData
= [];
1226 var sigma
= this.attr_("sigma");
1228 if (this.fractions_
) {
1230 var den
= 0; // numerator/denominator
1232 for (var i
= 0; i
< originalData
.length
; i
++) {
1233 num
+= originalData
[i
][1][0];
1234 den
+= originalData
[i
][1][1];
1235 if (i
- rollPeriod
>= 0) {
1236 num
-= originalData
[i
- rollPeriod
][1][0];
1237 den
-= originalData
[i
- rollPeriod
][1][1];
1240 var date
= originalData
[i
][0];
1241 var value
= den
? num
/ den
: 0.0;
1242 if (this.attr_("errorBars")) {
1243 if (this.wilsonInterval_
) {
1244 // For more details on this confidence interval, see:
1245 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1247 var p
= value
< 0 ? 0 : value
, n
= den
;
1248 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1249 var denom
= 1 + sigma
* sigma
/ den
;
1250 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1251 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1252 rollingData
[i
] = [date
,
1253 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1255 rollingData
[i
] = [date
, [0, 0, 0]];
1258 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1259 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1262 rollingData
[i
] = [date
, mult
* value
];
1265 } else if (this.attr_("customBars")) {
1270 for (var i
= 0; i
< originalData
.length
; i
++) {
1271 var data
= originalData
[i
][1];
1273 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1275 if (y
!= null && !isNaN(y
)) {
1281 if (i
- rollPeriod
>= 0) {
1282 var prev
= originalData
[i
- rollPeriod
];
1283 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1290 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1291 1.0 * (mid
- low
) / count
,
1292 1.0 * (high
- mid
) / count
]];
1295 // Calculate the rolling average for the first rollPeriod - 1 points where
1296 // there is not enough data to roll over the full number of days
1297 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1298 if (!this.attr_("errorBars")){
1299 if (rollPeriod
== 1) {
1300 return originalData
;
1303 for (var i
= 0; i
< originalData
.length
; i
++) {
1306 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1307 var y
= originalData
[j
][1];
1308 if (y
== null || isNaN(y
)) continue;
1310 sum
+= originalData
[j
][1];
1313 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1315 rollingData
[i
] = [originalData
[i
][0], null];
1320 for (var i
= 0; i
< originalData
.length
; i
++) {
1324 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1325 var y
= originalData
[j
][1][0];
1326 if (y
== null || isNaN(y
)) continue;
1328 sum
+= originalData
[j
][1][0];
1329 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1332 var stddev
= Math
.sqrt(variance
) / num_ok
;
1333 rollingData
[i
] = [originalData
[i
][0],
1334 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1336 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1346 * Parses a date, returning the number of milliseconds since epoch. This can be
1347 * passed in as an xValueParser in the Dygraph constructor.
1348 * TODO(danvk): enumerate formats that this understands.
1349 * @param {String} A date in YYYYMMDD format.
1350 * @return {Number} Milliseconds since epoch.
1353 Dygraph
.dateParser
= function(dateStr
, self
) {
1356 if (dateStr
.length
== 10 && dateStr
.search("-") != -1) { // e.g. '2009-07-12'
1357 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1358 while (dateStrSlashed
.search("-") != -1) {
1359 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1361 d
= Date
.parse(dateStrSlashed
);
1362 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1363 // TODO(danvk): remove support for this format. It's confusing.
1364 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1365 + "/" + dateStr
.substr(6,2);
1366 d
= Date
.parse(dateStrSlashed
);
1368 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1369 // "2009/07/12 12:34:56"
1370 d
= Date
.parse(dateStr
);
1373 if (!d
|| isNaN(d
)) {
1374 self
.error("Couldn't parse " + dateStr
+ " as a date");
1380 * Detects the type of the str (date or numeric) and sets the various
1381 * formatting attributes in this.attrs_ based on this type.
1382 * @param {String} str An x value.
1385 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1387 if (str
.indexOf('-') >= 0 ||
1388 str
.indexOf('/') >= 0 ||
1389 isNaN(parseFloat(str
))) {
1391 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1392 // TODO(danvk): remove support for this format.
1397 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1398 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1399 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1401 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1402 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1403 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1408 * Parses a string in a special csv format. We expect a csv file where each
1409 * line is a date point, and the first field in each line is the date string.
1410 * We also expect that all remaining fields represent series.
1411 * if the errorBars attribute is set, then interpret the fields as:
1412 * date, series1, stddev1, series2, stddev2, ...
1413 * @param {Array.<Object>} data See above.
1416 * @return Array.<Object> An array with one entry for each row. These entries
1417 * are an array of cells in that row. The first entry is the parsed x-value for
1418 * the row. The second, third, etc. are the y-values. These can take on one of
1419 * three forms, depending on the CSV and constructor parameters:
1421 * 2. [ value, stddev ]
1422 * 3. [ low value, center value, high value ]
1424 Dygraph
.prototype.parseCSV_
= function(data
) {
1426 var lines
= data
.split("\n");
1428 // Use the default delimiter or fall back to a tab if that makes sense.
1429 var delim
= this.attr_('delimiter');
1430 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1435 if (this.labelsFromCSV_
) {
1437 this.attrs_
.labels
= lines
[0].split(delim
);
1441 var defaultParserSet
= false; // attempt to auto-detect x value type
1442 var expectedCols
= this.attr_("labels").length
;
1443 for (var i
= start
; i
< lines
.length
; i
++) {
1444 var line
= lines
[i
];
1445 if (line
.length
== 0) continue; // skip blank lines
1446 if (line
[0] == '#') continue; // skip comment lines
1447 var inFields
= line
.split(delim
);
1448 if (inFields
.length
< 2) continue;
1451 if (!defaultParserSet
) {
1452 this.detectTypeFromString_(inFields
[0]);
1453 xParser
= this.attr_("xValueParser");
1454 defaultParserSet
= true;
1456 fields
[0] = xParser(inFields
[0], this);
1458 // If fractions are expected, parse the numbers as "A/B
"
1459 if (this.fractions_) {
1460 for (var j = 1; j < inFields.length; j++) {
1461 // TODO(danvk): figure out an appropriate way to flag parse errors.
1462 var vals = inFields[j].split("/");
1463 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1465 } else if (this.attr_("errorBars
")) {
1466 // If there are error bars, values are (value, stddev) pairs
1467 for (var j = 1; j < inFields.length; j += 2)
1468 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1469 parseFloat(inFields[j + 1])];
1470 } else if (this.attr_("customBars
")) {
1471 // Bars are a low;center;high tuple
1472 for (var j = 1; j < inFields.length; j++) {
1473 var vals = inFields[j].split(";");
1474 fields[j] = [ parseFloat(vals[0]),
1475 parseFloat(vals[1]),
1476 parseFloat(vals[2]) ];
1479 // Values are just numbers
1480 for (var j = 1; j < inFields.length; j++) {
1481 fields[j] = parseFloat(inFields[j]);
1486 if (fields.length != expectedCols) {
1487 this.error("Number of columns
in line
" + i + " (" + fields.length +
1488 ") does not agree
with number of
labels (" + expectedCols +
1496 * The user has provided their data as a pre-packaged JS array. If the x values
1497 * are numeric, this is the same as dygraphs' internal format. If the x values
1498 * are dates, we need to convert them from Date objects to ms since epoch.
1499 * @param {Array.<Object>} data
1500 * @return {Array.<Object>} data with numeric x values.
1502 Dygraph.prototype.parseArray_ = function(data) {
1503 // Peek at the first x value to see if it's numeric.
1504 if (data.length == 0) {
1505 this.error("Can
't plot empty data set");
1508 if (data[0].length == 0) {
1509 this.error("Data set cannot contain an empty row");
1513 if (this.attr_("labels") == null) {
1514 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1515 "in the options parameter");
1516 this.attrs_.labels = [ "X" ];
1517 for (var i = 1; i < data[0].length; i++) {
1518 this.attrs_.labels.push("Y" + i);
1522 if (Dygraph.isDateLike(data[0][0])) {
1523 // Some intelligent defaults for a date x-axis.
1524 this.attrs_.xValueFormatter = Dygraph.dateString_;
1525 this.attrs_.xTicker = Dygraph.dateTicker;
1527 // Assume they're all dates
.
1528 var parsedData
= Dygraph
.clone(data
);
1529 for (var i
= 0; i
< data
.length
; i
++) {
1530 if (parsedData
[i
].length
== 0) {
1531 this.error("Row " << (1 + i
) << " of data is empty");
1534 if (parsedData
[i
][0] == null
1535 || typeof(parsedData
[i
][0].getTime
) != 'function') {
1536 this.error("x value in row " << (1 + i
) << " is not a Date");
1539 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1543 // Some intelligent defaults for a numeric x-axis.
1544 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1545 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1551 * Parses a DataTable object from gviz.
1552 * The data is expected to have a first column that is either a date or a
1553 * number. All subsequent columns must be numbers. If there is a clear mismatch
1554 * between this.xValueParser_ and the type of the first column, it will be
1555 * fixed. Returned value is in the same format as return value of parseCSV_.
1556 * @param {Array.<Object>} data See above.
1559 Dygraph
.prototype.parseDataTable_
= function(data
) {
1560 var cols
= data
.getNumberOfColumns();
1561 var rows
= data
.getNumberOfRows();
1563 // Read column labels
1565 for (var i
= 0; i
< cols
; i
++) {
1566 labels
.push(data
.getColumnLabel(i
));
1567 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1569 this.attrs_
.labels
= labels
;
1570 cols
= labels
.length
;
1572 var indepType
= data
.getColumnType(0);
1573 if (indepType
== 'date') {
1574 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1575 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1576 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1577 } else if (indepType
== 'number') {
1578 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1579 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1580 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1582 this.error("only 'date' and 'number' types are supported for column 1 " +
1583 "of DataTable input (Got '" + indepType
+ "')");
1588 for (var i
= 0; i
< rows
; i
++) {
1590 if (!data
.getValue(i
, 0)) continue;
1591 if (indepType
== 'date') {
1592 row
.push(data
.getValue(i
, 0).getTime());
1594 row
.push(data
.getValue(i
, 0));
1596 if (!this.attr_("errorBars")) {
1597 for (var j
= 1; j
< cols
; j
++) {
1598 row
.push(data
.getValue(i
, j
));
1601 for (var j
= 0; j
< cols
- 1; j
++) {
1602 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
1610 // These functions are all based on MochiKit.
1611 Dygraph
.update
= function (self
, o
) {
1612 if (typeof(o
) != 'undefined' && o
!== null) {
1614 if (o
.hasOwnProperty(k
)) {
1622 Dygraph
.isArrayLike
= function (o
) {
1623 var typ
= typeof(o
);
1625 (typ
!= 'object' && !(typ
== 'function' &&
1626 typeof(o
.item
) == 'function')) ||
1628 typeof(o
.length
) != 'number' ||
1636 Dygraph
.isDateLike
= function (o
) {
1637 if (typeof(o
) != "object" || o
=== null ||
1638 typeof(o
.getTime
) != 'function') {
1644 Dygraph
.clone
= function(o
) {
1645 // TODO(danvk): figure out how MochiKit's version works
1647 for (var i
= 0; i
< o
.length
; i
++) {
1648 if (Dygraph
.isArrayLike(o
[i
])) {
1649 r
.push(Dygraph
.clone(o
[i
]));
1659 * Get the CSV data. If it's in a function, call that function. If it's in a
1660 * file, do an XMLHttpRequest to get it.
1663 Dygraph
.prototype.start_
= function() {
1664 if (typeof this.file_
== 'function') {
1665 // CSV string. Pretend we got it via XHR.
1666 this.loadedEvent_(this.file_());
1667 } else if (Dygraph
.isArrayLike(this.file_
)) {
1668 this.rawData_
= this.parseArray_(this.file_
);
1669 this.drawGraph_(this.rawData_
);
1670 } else if (typeof this.file_
== 'object' &&
1671 typeof this.file_
.getColumnRange
== 'function') {
1672 // must be a DataTable from gviz.
1673 this.rawData_
= this.parseDataTable_(this.file_
);
1674 this.drawGraph_(this.rawData_
);
1675 } else if (typeof this.file_
== 'string') {
1676 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
1677 if (this.file_
.indexOf('\n') >= 0) {
1678 this.loadedEvent_(this.file_
);
1680 var req
= new XMLHttpRequest();
1682 req
.onreadystatechange
= function () {
1683 if (req
.readyState
== 4) {
1684 if (req
.status
== 200) {
1685 caller
.loadedEvent_(req
.responseText
);
1690 req
.open("GET", this.file_
, true);
1694 this.error("Unknown data format: " + (typeof this.file_
));
1699 * Changes various properties of the graph. These can include:
1701 * <li>file: changes the source data for the graph</li>
1702 * <li>errorBars: changes whether the data contains stddev</li>
1704 * @param {Object} attrs The new properties and values
1706 Dygraph
.prototype.updateOptions
= function(attrs
) {
1707 // TODO(danvk): this is a mess. Rethink this function.
1708 if (attrs
.rollPeriod
) {
1709 this.rollPeriod_
= attrs
.rollPeriod
;
1711 if (attrs
.dateWindow
) {
1712 this.dateWindow_
= attrs
.dateWindow
;
1714 if (attrs
.valueRange
) {
1715 this.valueRange_
= attrs
.valueRange
;
1717 Dygraph
.update(this.user_attrs_
, attrs
);
1719 this.labelsFromCSV_
= (this.attr_("labels") == null);
1721 // TODO(danvk): this doesn't match the constructor logic
1722 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
1723 if (attrs
['file'] && attrs
['file'] != this.file_
) {
1724 this.file_
= attrs
['file'];
1727 this.drawGraph_(this.rawData_
);
1732 * Adjusts the number of days in the rolling average. Updates the graph to
1733 * reflect the new averaging period.
1734 * @param {Number} length Number of days over which to average the data.
1736 Dygraph
.prototype.adjustRoll
= function(length
) {
1737 this.rollPeriod_
= length
;
1738 this.drawGraph_(this.rawData_
);
1742 * Create a new canvas element. This is more complex than a simple
1743 * document.createElement("canvas") because of IE and excanvas.
1745 Dygraph
.createCanvas
= function() {
1746 var canvas
= document
.createElement("canvas");
1748 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
1750 canvas
= G_vmlCanvasManager
.initElement(canvas
);
1758 * A wrapper around Dygraph that implements the gviz API.
1759 * @param {Object} container The DOM object the visualization should live in.
1761 Dygraph
.GVizChart
= function(container
) {
1762 this.container
= container
;
1765 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
1766 this.container
.innerHTML
= '';
1767 this.date_graph
= new Dygraph(this.container
, data
, options
);
1770 // Older pages may still use this name.
1771 DateGraph
= Dygraph
;