1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
6 * string. Dygraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. Dygraph uses the
8 * <canvas> tag, so it only works in FF1.5+.
9 * @author danvdk@gmail.com (Dan Vanderkam)
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
14 new Dygraph(document.getElementById("graphdiv"),
15 "datafile.csv", // CSV file with headers
19 The CSV file is of the form
21 Date,SeriesA,SeriesB,SeriesC
25 If the 'errorBars' option is set in the constructor, the input should be of
28 Date,SeriesA,SeriesB,...
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
32 If the 'fractions' option is set, the input should be of the form:
34 Date,SeriesA,SeriesB,...
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
38 And error bars will be calculated automatically using a binomial distribution.
40 For further documentation and examples, see http://www.danvk.org/dygraphs
45 * An interactive, zoomable graph
46 * @param {String | Function} file A file containing CSV data or a function that
47 * returns this data. The expected format for each line is
48 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
49 * YYYYMMDD,val1,stddev1,val2,stddev2,...
50 * @param {Object} attrs Various other attributes, e.g. errorBars determines
51 * whether the input data contains error ranges.
53 Dygraph
= function(div
, data
, opts
) {
54 if (arguments
.length
> 0) {
55 if (arguments
.length
== 4) {
56 // Old versions of dygraphs took in the series labels as a constructor
57 // parameter. This doesn't make sense anymore, but it's easy to continue
58 // to support this usage.
59 this.warn("Using deprecated four-argument dygraph constructor");
60 this.__old_init__(div
, data
, arguments
[2], arguments
[3]);
62 this.__init__(div
, data
, opts
);
67 Dygraph
.NAME
= "Dygraph";
68 Dygraph
.VERSION
= "1.2";
69 Dygraph
.__repr__
= function() {
70 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
72 Dygraph
.toString
= function() {
73 return this.__repr__();
76 // Various default values
77 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
78 Dygraph
.DEFAULT_WIDTH
= 480;
79 Dygraph
.DEFAULT_HEIGHT
= 320;
80 Dygraph
.AXIS_LINE_WIDTH
= 0.3;
82 // Default attribute values.
83 Dygraph
.DEFAULT_ATTRS
= {
84 highlightCircleSize
: 3,
90 // TODO(danvk): move defaults from createStatusMessage_ here.
92 labelsSeparateLines
: false,
99 axisLabelFontSize
: 14,
105 xValueFormatter
: Dygraph
.dateString_
,
106 xValueParser
: Dygraph
.dateParser
,
107 xTicker
: Dygraph
.dateTicker
,
115 wilsonInterval
: true, // only relevant if fractions is true
121 hideOverlayOnMouseOut
: true
124 // Various logging levels.
130 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
131 // Labels is no longer a constructor parameter, since it's typically set
132 // directly from the data source. It also conains a name for the x-axis,
133 // which the previous constructor form did not.
134 if (labels
!= null) {
135 var new_labels
= ["Date"];
136 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
137 Dygraph
.update(attrs
, { 'labels': new_labels
});
139 this.__init__(div
, file
, attrs
);
143 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
144 * and interaction <canvas> inside of it. See the constructor for details
146 * @param {String | Function} file Source data
147 * @param {Array.<String>} labels Names of the data series
148 * @param {Object} attrs Miscellaneous other options
151 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
152 // Support two-argument constructor
153 if (attrs
== null) { attrs
= {}; }
155 // Copy the important bits into the object
156 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
159 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
160 this.previousVerticalX_
= -1;
161 this.fractions_
= attrs
.fractions
|| false;
162 this.dateWindow_
= attrs
.dateWindow
|| null;
163 this.valueRange_
= attrs
.valueRange
|| null;
164 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
166 // Clear the div. This ensure that, if multiple dygraphs are passed the same
167 // div, then only one will be drawn.
170 // If the div isn't already sized then inherit from our attrs or
171 // give it a default size.
172 if (div
.style
.width
== '') {
173 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
175 if (div
.style
.height
== '') {
176 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
178 this.width_
= parseInt(div
.style
.width
, 10);
179 this.height_
= parseInt(div
.style
.height
, 10);
180 // The div might have been specified as percent of the current window size,
181 // convert that to an appropriate number of pixels.
182 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
183 // Minus ten pixels keeps scrollbars from showing up for a 100% width div.
184 this.width_
= (this.width_
* self
.innerWidth
/ 100) - 10;
186 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
187 this.height_
= (this.height_
* self
.innerHeight
/ 100) - 10;
190 if (attrs
['stackedGraph']) {
191 attrs
['fillGraph'] = true;
192 // TODO(nikhilk): Add any other stackedGraph checks here.
195 // Dygraphs has many options, some of which interact with one another.
196 // To keep track of everything, we maintain two sets of options:
198 // this.user_attrs_ only options explicitly set by the user.
199 // this.attrs_ defaults, options derived from user_attrs_, data.
201 // Options are then accessed this.attr_('attr'), which first looks at
202 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
203 // defaults without overriding behavior that the user specifically asks for.
204 this.user_attrs_
= {};
205 Dygraph
.update(this.user_attrs_
, attrs
);
208 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
210 // Make a note of whether labels will be pulled from the CSV file.
211 this.labelsFromCSV_
= (this.attr_("labels") == null);
213 // Create the containing DIV and other interactive elements
214 this.createInterface_();
219 Dygraph
.prototype.attr_
= function(name
) {
220 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
221 return this.user_attrs_
[name
];
222 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
223 return this.attrs_
[name
];
229 // TODO(danvk): any way I can get the line numbers to be this.warn call?
230 Dygraph
.prototype.log
= function(severity
, message
) {
231 if (typeof(console
) != 'undefined') {
234 console
.debug('dygraphs: ' + message
);
237 console
.info('dygraphs: ' + message
);
239 case Dygraph
.WARNING
:
240 console
.warn('dygraphs: ' + message
);
243 console
.error('dygraphs: ' + message
);
248 Dygraph
.prototype.info
= function(message
) {
249 this.log(Dygraph
.INFO
, message
);
251 Dygraph
.prototype.warn
= function(message
) {
252 this.log(Dygraph
.WARNING
, message
);
254 Dygraph
.prototype.error
= function(message
) {
255 this.log(Dygraph
.ERROR
, message
);
259 * Returns the current rolling period, as set by the user or an option.
260 * @return {Number} The number of days in the rolling window
262 Dygraph
.prototype.rollPeriod
= function() {
263 return this.rollPeriod_
;
266 Dygraph
.addEvent
= function(el
, evt
, fn
) {
267 var normed_fn
= function(e
) {
268 if (!e
) var e
= window
.event
;
271 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
272 el
.addEventListener(evt
, normed_fn
, false);
274 el
.attachEvent('on' + evt
, normed_fn
);
279 * Generates interface elements for the Dygraph: a containing div, a div to
280 * display the current point, and a textbox to adjust the rolling average
281 * period. Also creates the Renderer/Layout elements.
284 Dygraph
.prototype.createInterface_
= function() {
285 // Create the all-enclosing graph div
286 var enclosing
= this.maindiv_
;
288 this.graphDiv
= document
.createElement("div");
289 this.graphDiv
.style
.width
= this.width_
+ "px";
290 this.graphDiv
.style
.height
= this.height_
+ "px";
291 enclosing
.appendChild(this.graphDiv
);
293 // Create the canvas for interactive parts of the chart.
294 // this.canvas_ = document.createElement("canvas");
295 this.canvas_
= Dygraph
.createCanvas();
296 this.canvas_
.style
.position
= "absolute";
297 this.canvas_
.width
= this.width_
;
298 this.canvas_
.height
= this.height_
;
299 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
300 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
301 this.graphDiv
.appendChild(this.canvas_
);
303 // ... and for static parts of the chart.
304 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
307 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
308 dygraph
.mouseMove_(e
);
310 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
311 dygraph
.mouseOut_(e
);
314 // Create the grapher
315 // TODO(danvk): why does the Layout need its own set of options?
316 this.layoutOptions_
= { 'xOriginIsZero': false };
317 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
318 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
319 Dygraph
.update(this.layoutOptions_
, {
320 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
322 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
324 // TODO(danvk): why does the Renderer need its own set of options?
325 this.renderOptions_
= { colorScheme
: this.colors_
,
327 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
328 Dygraph
.update(this.renderOptions_
, this.attrs_
);
329 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
330 this.plotter_
= new DygraphCanvasRenderer(this,
331 this.hidden_
, this.layout_
,
332 this.renderOptions_
);
334 this.createStatusMessage_();
335 this.createRollInterface_();
336 this.createDragInterface_();
340 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
341 * this particular canvas. All Dygraph work is done on this.canvas_.
342 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
343 * @return {Object} The newly-created canvas
346 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
347 // var h = document.createElement("canvas");
348 var h
= Dygraph
.createCanvas();
349 h
.style
.position
= "absolute";
350 h
.style
.top
= canvas
.style
.top
;
351 h
.style
.left
= canvas
.style
.left
;
352 h
.width
= this.width_
;
353 h
.height
= this.height_
;
354 h
.style
.width
= this.width_
+ "px"; // for IE
355 h
.style
.height
= this.height_
+ "px"; // for IE
356 this.graphDiv
.appendChild(h
);
360 // Taken from MochiKit.Color
361 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
365 if (saturation
=== 0) {
370 var i
= Math
.floor(hue
* 6);
371 var f
= (hue
* 6) - i
;
372 var p
= value
* (1 - saturation
);
373 var q
= value
* (1 - (saturation
* f
));
374 var t
= value
* (1 - (saturation
* (1 - f
)));
376 case 1: red
= q
; green
= value
; blue
= p
; break;
377 case 2: red
= p
; green
= value
; blue
= t
; break;
378 case 3: red
= p
; green
= q
; blue
= value
; break;
379 case 4: red
= t
; green
= p
; blue
= value
; break;
380 case 5: red
= value
; green
= p
; blue
= q
; break;
381 case 6: // fall through
382 case 0: red
= value
; green
= t
; blue
= p
; break;
385 red
= Math
.floor(255 * red
+ 0.5);
386 green
= Math
.floor(255 * green
+ 0.5);
387 blue
= Math
.floor(255 * blue
+ 0.5);
388 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
393 * Generate a set of distinct colors for the data series. This is done with a
394 * color wheel. Saturation/Value are customizable, and the hue is
395 * equally-spaced around the color wheel. If a custom set of colors is
396 * specified, that is used instead.
399 Dygraph
.prototype.setColors_
= function() {
400 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
401 // away with this.renderOptions_.
402 var num
= this.attr_("labels").length
- 1;
404 var colors
= this.attr_('colors');
406 var sat
= this.attr_('colorSaturation') || 1.0;
407 var val
= this.attr_('colorValue') || 0.5;
408 for (var i
= 1; i
<= num
; i
++) {
409 if (!this.visibility()[i
-1]) continue;
410 // alternate colors for high contrast.
411 var idx
= i
- parseInt(i
% 2 ? i
/ 2 : (i - num)/2, 10);
412 var hue
= (1.0 * idx
/ (1 + num
));
413 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
416 for (var i
= 0; i
< num
; i
++) {
417 if (!this.visibility()[i
]) continue;
418 var colorStr
= colors
[i
% colors
.length
];
419 this.colors_
.push(colorStr
);
423 // TODO(danvk): update this w/r
/t/ the
new options system
.
424 this.renderOptions_
.colorScheme
= this.colors_
;
425 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
426 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
427 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
431 * Return the list of colors. This is either the list of colors passed in the
432 * attributes, or the autogenerated list of rgb(r,g,b) strings.
433 * @return {Array<string>} The list of colors.
435 Dygraph
.prototype.getColors
= function() {
439 // The following functions are from quirksmode.org
440 // http://www.quirksmode.org/js
/findpos
.html
441 Dygraph
.findPosX
= function(obj
) {
443 if (obj
.offsetParent
) {
444 while (obj
.offsetParent
) {
445 curleft
+= obj
.offsetLeft
;
446 obj
= obj
.offsetParent
;
454 Dygraph
.findPosY
= function(obj
) {
456 if (obj
.offsetParent
) {
457 while (obj
.offsetParent
) {
458 curtop
+= obj
.offsetTop
;
459 obj
= obj
.offsetParent
;
468 * Create the div that contains information on the selected point(s)
469 * This goes in the top right of the canvas, unless an external div has already
473 Dygraph
.prototype.createStatusMessage_
= function(){
474 if (!this.attr_("labelsDiv")) {
475 var divWidth
= this.attr_('labelsDivWidth');
477 "position": "absolute",
480 "width": divWidth
+ "px",
482 "left": (this.width_
- divWidth
- 2) + "px",
483 "background": "white",
485 "overflow": "hidden"};
486 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
487 var div
= document
.createElement("div");
488 for (var name
in messagestyle
) {
489 if (messagestyle
.hasOwnProperty(name
)) {
490 div
.style
[name
] = messagestyle
[name
];
493 this.graphDiv
.appendChild(div
);
494 this.attrs_
.labelsDiv
= div
;
499 * Create the text box to adjust the averaging period
500 * @return {Object} The newly-created text box
503 Dygraph
.prototype.createRollInterface_
= function() {
504 var display
= this.attr_('showRoller') ? "block" : "none";
505 var textAttr
= { "position": "absolute",
507 "top": (this.plotter_
.area
.h
- 25) + "px",
508 "left": (this.plotter_
.area
.x
+ 1) + "px",
511 var roller
= document
.createElement("input");
512 roller
.type
= "text";
514 roller
.value
= this.rollPeriod_
;
515 for (var name
in textAttr
) {
516 if (textAttr
.hasOwnProperty(name
)) {
517 roller
.style
[name
] = textAttr
[name
];
521 var pa
= this.graphDiv
;
522 pa
.appendChild(roller
);
524 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
528 // These functions are taken from MochiKit.Signal
529 Dygraph
.pageX
= function(e
) {
531 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
534 var b
= document
.body
;
536 (de
.scrollLeft
|| b
.scrollLeft
) -
537 (de
.clientLeft
|| 0);
541 Dygraph
.pageY
= function(e
) {
543 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
546 var b
= document
.body
;
548 (de
.scrollTop
|| b
.scrollTop
) -
554 * Set up all the mouse handlers needed to capture dragging behavior for zoom
558 Dygraph
.prototype.createDragInterface_
= function() {
561 // Tracks whether the mouse is down right now
562 var isZooming
= false;
563 var isPanning
= false;
564 var dragStartX
= null;
565 var dragStartY
= null;
569 var draggingDate
= null;
570 var dateRange
= null;
572 // Utility function to convert page-wide coordinates to canvas coords
575 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
576 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
578 // Draw zoom rectangles when the mouse is down and the user moves around
579 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
581 dragEndX
= getX(event
);
582 dragEndY
= getY(event
);
584 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
586 } else if (isPanning
) {
587 dragEndX
= getX(event
);
588 dragEndY
= getY(event
);
590 // Want to have it so that:
591 // 1. draggingDate appears at dragEndX
592 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
594 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
595 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
596 self
.drawGraph_(self
.rawData_
);
600 // Track the beginning of drag events
601 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
602 px
= Dygraph
.findPosX(self
.canvas_
);
603 py
= Dygraph
.findPosY(self
.canvas_
);
604 dragStartX
= getX(event
);
605 dragStartY
= getY(event
);
607 if (event
.altKey
|| event
.shiftKey
) {
608 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
610 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
611 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
618 // If the user releases the mouse button during a drag, but not over the
619 // canvas, then it doesn't count as a zooming action.
620 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
621 if (isZooming
|| isPanning
) {
634 // Temporarily cancel the dragging event when the mouse leaves the graph
635 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
642 // If the mouse is released on the canvas during a drag event, then it's a
643 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
644 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
647 dragEndX
= getX(event
);
648 dragEndY
= getY(event
);
649 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
650 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
652 if (regionWidth
< 2 && regionHeight
< 2 &&
653 self
.attr_('clickCallback') != null &&
654 self
.lastx_
!= undefined
) {
655 // TODO(danvk): pass along more info about the points.
656 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
659 if (regionWidth
>= 10) {
660 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
661 Math
.max(dragStartX
, dragEndX
));
663 self
.canvas_
.getContext("2d").clearRect(0, 0,
665 self
.canvas_
.height
);
679 // Double-clicking zooms back out
680 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
681 if (self
.dateWindow_
== null) return;
682 self
.dateWindow_
= null;
683 self
.drawGraph_(self
.rawData_
);
684 var minDate
= self
.rawData_
[0][0];
685 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
686 if (self
.attr_("zoomCallback")) {
687 self
.attr_("zoomCallback")(minDate
, maxDate
);
693 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
694 * up any previous zoom rectangles that were drawn. This could be optimized to
695 * avoid extra redrawing, but it's tricky to avoid interactions with the status
697 * @param {Number} startX The X position where the drag started, in canvas
699 * @param {Number} endX The current X position of the drag, in canvas coords.
700 * @param {Number} prevEndX The value of endX on the previous call to this
701 * function. Used to avoid excess redrawing
704 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
705 var ctx
= this.canvas_
.getContext("2d");
707 // Clean up from the previous rect if necessary
709 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
710 Math
.abs(startX
- prevEndX
), this.height_
);
713 // Draw a light-grey rectangle to show the new viewing area
714 if (endX
&& startX
) {
715 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
716 ctx
.fillRect(Math
.min(startX
, endX
), 0,
717 Math
.abs(endX
- startX
), this.height_
);
722 * Zoom to something containing [lowX, highX]. These are pixel coordinates
723 * in the canvas. The exact zoom window may be slightly larger if there are no
724 * data points near lowX or highX. This function redraws the graph.
725 * @param {Number} lowX The leftmost pixel value that should be visible.
726 * @param {Number} highX The rightmost pixel value that should be visible.
729 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
730 // Find the earliest and latest dates contained in this canvasx range.
731 var points
= this.layout_
.points
;
734 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
735 for (var i
= 0; i
< points
.length
; i
++) {
736 var cx
= points
[i
].canvasx
;
737 var x
= points
[i
].xval
;
738 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
739 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
741 // Use the extremes if either is missing
742 if (minDate
== null) minDate
= points
[0].xval
;
743 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
745 this.dateWindow_
= [minDate
, maxDate
];
746 this.drawGraph_(this.rawData_
);
747 if (this.attr_("zoomCallback")) {
748 this.attr_("zoomCallback")(minDate
, maxDate
);
753 * When the mouse moves in the canvas, display information about a nearby data
754 * point and draw dots over those points in the data series. This function
755 * takes care of cleanup of previously-drawn dots.
756 * @param {Object} event The mousemove event from the browser.
759 Dygraph
.prototype.mouseMove_
= function(event
) {
760 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
761 var points
= this.layout_
.points
;
766 // Loop through all the points and find the date nearest to our current
768 var minDist
= 1e+100;
770 for (var i
= 0; i
< points
.length
; i
++) {
771 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
772 if (dist
> minDist
) break;
776 if (idx
>= 0) lastx
= points
[idx
].xval
;
777 // Check that you can really highlight the last day's data
778 if (canvasx
> points
[points
.length
-1].canvasx
)
779 lastx
= points
[points
.length
-1].xval
;
781 // Extract the points we've selected
782 this.selPoints_
= [];
783 for (var i
= 0; i
< points
.length
; i
++) {
784 if (points
[i
].xval
== lastx
) {
785 this.selPoints_
.push(points
[i
]);
789 if (this.attr_("highlightCallback")) {
790 var callbackPoints
= this.selPoints_
.map(
791 function(p
) { return {xval
: p
.xval
, yval
: p
.yval
, name
: p
.name
} });
792 if (this.attr_("stackedGraph")) {
793 // "unstack" the points.
794 var cumulative_sum
= 0;
795 for (var j
= callbackPoints
.length
- 1; j
>= 0; j
--) {
796 callbackPoints
[j
].yval
-= cumulative_sum
;
797 cumulative_sum
+= callbackPoints
[j
].yval
;
801 this.attr_("highlightCallback")(event
, lastx
, callbackPoints
);
804 // Clear the previously drawn vertical, if there is one
805 var circleSize
= this.attr_('highlightCircleSize');
806 var ctx
= this.canvas_
.getContext("2d");
807 if (this.previousVerticalX_
>= 0) {
808 var px
= this.previousVerticalX_
;
809 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
812 var isOK
= function(x
) { return x
&& !isNaN(x
); };
814 if (this.selPoints_
.length
> 0) {
815 var canvasx
= this.selPoints_
[0].canvasx
;
817 // Set the status message to indicate the selected point(s)
818 var replace
= this.attr_('xValueFormatter')(lastx
, this) + ":";
819 var clen
= this.colors_
.length
;
820 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
821 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
822 if (this.attr_("labelsSeparateLines")) {
825 var point
= this.selPoints_
[i
];
826 var c
= new RGBColor(this.colors_
[i
%clen
]);
827 replace
+= " <b><font color='" + c
.toHex() + "'>"
828 + point
.name
+ "</font></b>:"
829 + this.round_(point
.yval
, 2);
831 this.attr_("labelsDiv").innerHTML
= replace
;
833 // Save last x position for callbacks.
836 // Draw colored circles over the center of each selected point
838 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
839 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
841 ctx
.fillStyle
= this.colors_
[i
%clen
];
842 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
843 0, 2 * Math
.PI
, false);
848 this.previousVerticalX_
= canvasx
;
853 * The mouse has left the canvas. Clear out whatever artifacts remain
854 * @param {Object} event the mouseout event from the browser.
857 Dygraph
.prototype.mouseOut_
= function(event
) {
858 if (this.attr_("hideOverlayOnMouseOut")) {
859 // Get rid of the overlay data
860 var ctx
= this.canvas_
.getContext("2d");
861 ctx
.clearRect(0, 0, this.width_
, this.height_
);
862 this.attr_("labelsDiv").innerHTML
= "";
866 Dygraph
.zeropad
= function(x
) {
867 if (x
< 10) return "0" + x
; else return "" + x
;
871 * Return a string version of the hours, minutes and seconds portion of a date.
872 * @param {Number} date The JavaScript date (ms since epoch)
873 * @return {String} A time of the form "HH:MM:SS"
876 Dygraph
.prototype.hmsString_
= function(date
) {
877 var zeropad
= Dygraph
.zeropad
;
878 var d
= new Date(date
);
879 if (d
.getSeconds()) {
880 return zeropad(d
.getHours()) + ":" +
881 zeropad(d
.getMinutes()) + ":" +
882 zeropad(d
.getSeconds());
883 } else if (d
.getMinutes()) {
884 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
886 return zeropad(d
.getHours());
891 * Convert a JS date (millis since epoch) to YYYY/MM/DD
892 * @param {Number} date The JavaScript date (ms since epoch)
893 * @return {String} A date of the form "YYYY/MM/DD"
895 * TODO(danvk): why is this part of the prototype?
897 Dygraph
.dateString_
= function(date
, self
) {
898 var zeropad
= Dygraph
.zeropad
;
899 var d
= new Date(date
);
902 var year
= "" + d
.getFullYear();
903 // Get a 0 padded month string
904 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
905 // Get a 0 padded day string
906 var day
= zeropad(d
.getDate());
909 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
910 if (frac
) ret
= " " + self
.hmsString_(date
);
912 return year
+ "/" + month + "/" + day
+ ret
;
916 * Round a number to the specified number of digits past the decimal point.
917 * @param {Number} num The number to round
918 * @param {Number} places The number of decimals to which to round
919 * @return {Number} The rounded number
922 Dygraph
.prototype.round_
= function(num
, places
) {
923 var shift
= Math
.pow(10, places
);
924 return Math
.round(num
* shift
)/shift
;
928 * Fires when there's data available to be graphed.
929 * @param {String} data Raw CSV data to be plotted
932 Dygraph
.prototype.loadedEvent_
= function(data
) {
933 this.rawData_
= this.parseCSV_(data
);
934 this.drawGraph_(this.rawData_
);
937 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
938 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
939 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
942 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
945 Dygraph
.prototype.addXTicks_
= function() {
946 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
947 var startDate
, endDate
;
948 if (this.dateWindow_
) {
949 startDate
= this.dateWindow_
[0];
950 endDate
= this.dateWindow_
[1];
952 startDate
= this.rawData_
[0][0];
953 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
956 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
957 this.layout_
.updateOptions({xTicks
: xTicks
});
960 // Time granularity enumeration
961 Dygraph
.SECONDLY
= 0;
962 Dygraph
.TWO_SECONDLY
= 1;
963 Dygraph
.FIVE_SECONDLY
= 2;
964 Dygraph
.TEN_SECONDLY
= 3;
965 Dygraph
.THIRTY_SECONDLY
= 4;
966 Dygraph
.MINUTELY
= 5;
967 Dygraph
.TWO_MINUTELY
= 6;
968 Dygraph
.FIVE_MINUTELY
= 7;
969 Dygraph
.TEN_MINUTELY
= 8;
970 Dygraph
.THIRTY_MINUTELY
= 9;
972 Dygraph
.TWO_HOURLY
= 11;
973 Dygraph
.SIX_HOURLY
= 12;
976 Dygraph
.MONTHLY
= 15;
977 Dygraph
.QUARTERLY
= 16;
978 Dygraph
.BIANNUAL
= 17;
980 Dygraph
.DECADAL
= 19;
981 Dygraph
.NUM_GRANULARITIES
= 20;
983 Dygraph
.SHORT_SPACINGS
= [];
984 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
985 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
986 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
987 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
988 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
989 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
990 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
991 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
992 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
993 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
994 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
995 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
996 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
997 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
998 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1002 // If we used this time granularity, how many ticks would there be?
1003 // This is only an approximation, but it's generally good enough.
1005 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1006 if (granularity
< Dygraph
.MONTHLY
) {
1007 // Generate one tick mark for every fixed interval of time.
1008 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1009 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1011 var year_mod
= 1; // e.g. to only print one point every 10 years.
1012 var num_months
= 12;
1013 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1014 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1015 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1016 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1018 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1019 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1020 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1026 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1027 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1029 // Returns an array containing {v: millis, label: label} dictionaries.
1031 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1033 if (granularity
< Dygraph
.MONTHLY
) {
1034 // Generate one tick mark for every fixed interval of time.
1035 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1036 var format
= '%d%b'; // e.g. "1Jan"
1038 // Find a time less than start_time which occurs on a "nice" time boundary
1039 // for this granularity.
1040 var g
= spacing
/ 1000;
1041 var d
= new Date(start_time
);
1042 if (g
<= 60) { // seconds
1043 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1047 if (g
<= 60) { // minutes
1048 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1053 if (g
<= 24) { // days
1054 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1059 if (g
== 7) { // one week
1060 d
.setDate(d
.getDate() - d
.getDay());
1065 start_time
= d
.getTime();
1067 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1068 var d
= new Date(t
);
1069 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1070 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1071 // the extra hour covers DST problems.
1072 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
1074 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
1078 // Display a tick mark on the first of a set of months of each year.
1079 // Years get a tick mark iff y % year_mod == 0. This is useful for
1080 // displaying a tick mark once every 10 years, say, on long time scales.
1082 var year_mod
= 1; // e.g. to only print one point every 10 years.
1084 if (granularity
== Dygraph
.MONTHLY
) {
1085 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1086 } else if (granularity
== Dygraph
.QUARTERLY
) {
1087 months
= [ 0, 3, 6, 9 ];
1088 } else if (granularity
== Dygraph
.BIANNUAL
) {
1090 } else if (granularity
== Dygraph
.ANNUAL
) {
1092 } else if (granularity
== Dygraph
.DECADAL
) {
1097 var start_year
= new Date(start_time
).getFullYear();
1098 var end_year
= new Date(end_time
).getFullYear();
1099 var zeropad
= Dygraph
.zeropad
;
1100 for (var i
= start_year
; i
<= end_year
; i
++) {
1101 if (i
% year_mod
!= 0) continue;
1102 for (var j
= 0; j
< months
.length
; j
++) {
1103 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1104 var t
= Date
.parse(date_str
);
1105 if (t
< start_time
|| t
> end_time
) continue;
1106 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
1116 * Add ticks to the x-axis based on a date range.
1117 * @param {Number} startDate Start of the date window (millis since epoch)
1118 * @param {Number} endDate End of the date window (millis since epoch)
1119 * @return {Array.<Object>} Array of {label, value} tuples.
1122 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1124 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1125 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1126 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1133 return self
.GetXAxis(startDate
, endDate
, chosen
);
1135 // TODO(danvk): signal error.
1140 * Add ticks when the x axis has numbers on it (instead of dates)
1141 * @param {Number} startDate Start of the date window (millis since epoch)
1142 * @param {Number} endDate End of the date window (millis since epoch)
1143 * @return {Array.<Object>} Array of {label, value} tuples.
1146 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1148 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1149 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1150 // The first spacing greater than pixelsPerYLabel is what we use.
1151 // TODO(danvk): version that works on a log scale.
1152 if (self
.attr_("labelsKMG2")) {
1153 var mults
= [1, 2, 4, 8];
1155 var mults
= [1, 2, 5];
1157 var scale
, low_val
, high_val
, nTicks
;
1158 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1159 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1160 for (var i
= -10; i
< 50; i
++) {
1161 if (self
.attr_("labelsKMG2")) {
1162 var base_scale
= Math
.pow(16, i
);
1164 var base_scale
= Math
.pow(10, i
);
1166 for (var j
= 0; j
< mults
.length
; j
++) {
1167 scale
= base_scale
* mults
[j
];
1168 low_val
= Math
.floor(minV
/ scale
) * scale
;
1169 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1170 nTicks
= (high_val
- low_val
) / scale
;
1171 var spacing
= self
.height_
/ nTicks
;
1172 // wish I could break out of both loops at once...
1173 if (spacing
> pixelsPerTick
) break;
1175 if (spacing
> pixelsPerTick
) break;
1178 // Construct labels for the ticks
1182 if (self
.attr_("labelsKMB")) {
1184 k_labels
= [ "K", "M", "B", "T" ];
1186 if (self
.attr_("labelsKMG2")) {
1187 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1189 k_labels
= [ "k", "M", "G", "T" ];
1192 for (var i
= 0; i
< nTicks
; i
++) {
1193 var tickV
= low_val
+ i
* scale
;
1194 var absTickV
= Math
.abs(tickV
);
1195 var label
= self
.round_(tickV
, 2);
1196 if (k_labels
.length
) {
1197 // Round up to an appropriate unit.
1199 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1200 if (absTickV
>= n
) {
1201 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1206 ticks
.push( {label
: label
, v
: tickV
} );
1212 * Adds appropriate ticks on the y-axis
1213 * @param {Number} minY The minimum Y value in the data set
1214 * @param {Number} maxY The maximum Y value in the data set
1217 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1218 // Set the number of ticks so that the labels are human-friendly.
1219 // TODO(danvk): make this an attribute as well.
1220 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1221 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1225 // Computes the range of the data series (including confidence intervals).
1226 // series is either [ [x1, y1], [x2, y2], ... ] or
1227 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1228 // Returns [low, high]
1229 Dygraph
.prototype.extremeValues_
= function(series
) {
1230 var minY
= null, maxY
= null;
1232 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1234 // With custom bars, maxY is the max of the high values.
1235 for (var j
= 0; j
< series
.length
; j
++) {
1236 var y
= series
[j
][1][0];
1238 var low
= y
- series
[j
][1][1];
1239 var high
= y
+ series
[j
][1][2];
1240 if (low
> y
) low
= y
; // this can happen with custom bars,
1241 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1242 if (maxY
== null || high
> maxY
) {
1245 if (minY
== null || low
< minY
) {
1250 for (var j
= 0; j
< series
.length
; j
++) {
1251 var y
= series
[j
][1];
1252 if (y
=== null || isNaN(y
)) continue;
1253 if (maxY
== null || y
> maxY
) {
1256 if (minY
== null || y
< minY
) {
1262 return [minY
, maxY
];
1266 * Update the graph with new data. Data is in the format
1267 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1268 * or, if errorBars=true,
1269 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1270 * @param {Array.<Object>} data The data (see above)
1273 Dygraph
.prototype.drawGraph_
= function(data
) {
1274 var minY
= null, maxY
= null;
1275 this.layout_
.removeAllDatasets();
1277 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1279 // For stacked series.
1280 var cumulative_y
= [];
1283 // Loop over all fields in the dataset
1285 for (var i
= 1; i
< data
[0].length
; i
++) {
1286 if (!this.visibility()[i
- 1]) continue;
1289 for (var j
= 0; j
< data
.length
; j
++) {
1290 var date
= data
[j
][0];
1291 series
[j
] = [date
, data
[j
][i
]];
1293 series
= this.rollingAverage(series
, this.rollPeriod_
);
1295 // Prune down to the desired range, if necessary (for zooming)
1296 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1297 if (this.dateWindow_
) {
1298 var low
= this.dateWindow_
[0];
1299 var high
= this.dateWindow_
[1];
1301 for (var k
= 0; k
< series
.length
; k
++) {
1302 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
1303 pruned
.push(series
[k
]);
1309 var extremes
= this.extremeValues_(series
);
1310 var thisMinY
= extremes
[0];
1311 var thisMaxY
= extremes
[1];
1312 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1313 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1317 for (var j
=0; j
<series
.length
; j
++)
1318 vals
[j
] = [series
[j
][0],
1319 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1320 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1321 } else if (this.attr_("stackedGraph")) {
1323 var l
= series
.length
;
1325 for (var j
= 0; j
< l
; j
++) {
1326 if (cumulative_y
[series
[j
][0]] === undefined
)
1327 cumulative_y
[series
[j
][0]] = 0;
1329 actual_y
= series
[j
][1];
1330 cumulative_y
[series
[j
][0]] += actual_y
;
1332 vals
[j
] = [series
[j
][0], cumulative_y
[series
[j
][0]]]
1334 if (!maxY
|| cumulative_y
[series
[j
][0]] > maxY
)
1335 maxY
= cumulative_y
[series
[j
][0]];
1337 datasets
.push([this.attr_("labels")[i
], vals
]);
1338 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1340 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1344 if (datasets
.length
> 0) {
1345 for (var i
= (datasets
.length
- 1); i
>= 0; i
--) {
1346 this.layout_
.addDataset(datasets
[i
][0], datasets
[i
][1]);
1350 // Use some heuristics to come up with a good maxY value, unless it's been
1351 // set explicitly by the user.
1352 if (this.valueRange_
!= null) {
1353 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1355 // This affects the calculation of span, below.
1356 if (this.attr_("includeZero") && minY
> 0) {
1360 // Add some padding and round up to an integer to be human-friendly.
1361 var span
= maxY
- minY
;
1362 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1363 if (span
== 0) { span
= maxY
; }
1364 var maxAxisY
= maxY
+ 0.1 * span
;
1365 var minAxisY
= minY
- 0.1 * span
;
1367 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1368 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1369 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1371 if (this.attr_("includeZero")) {
1372 if (maxY
< 0) maxAxisY
= 0;
1373 if (minY
> 0) minAxisY
= 0;
1376 this.addYTicks_(minAxisY
, maxAxisY
);
1381 // Tell PlotKit to use this new data and render itself
1382 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1383 this.layout_
.evaluateWithError();
1384 this.plotter_
.clear();
1385 this.plotter_
.render();
1386 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1387 this.canvas_
.height
);
1391 * Calculates the rolling average of a data set.
1392 * If originalData is [label, val], rolls the average of those.
1393 * If originalData is [label, [, it's interpreted as [value, stddev]
1394 * and the roll is returned in the same form, with appropriately reduced
1395 * stddev for each value.
1396 * Note that this is where fractional input (i.e. '5/10') is converted into
1398 * @param {Array} originalData The data in the appropriate format (see above)
1399 * @param {Number} rollPeriod The number of days over which to average the data
1401 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1402 if (originalData
.length
< 2)
1403 return originalData
;
1404 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1405 var rollingData
= [];
1406 var sigma
= this.attr_("sigma");
1408 if (this.fractions_
) {
1410 var den
= 0; // numerator/denominator
1412 for (var i
= 0; i
< originalData
.length
; i
++) {
1413 num
+= originalData
[i
][1][0];
1414 den
+= originalData
[i
][1][1];
1415 if (i
- rollPeriod
>= 0) {
1416 num
-= originalData
[i
- rollPeriod
][1][0];
1417 den
-= originalData
[i
- rollPeriod
][1][1];
1420 var date
= originalData
[i
][0];
1421 var value
= den
? num
/ den
: 0.0;
1422 if (this.attr_("errorBars")) {
1423 if (this.wilsonInterval_
) {
1424 // For more details on this confidence interval, see:
1425 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1427 var p
= value
< 0 ? 0 : value
, n
= den
;
1428 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1429 var denom
= 1 + sigma
* sigma
/ den
;
1430 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1431 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1432 rollingData
[i
] = [date
,
1433 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1435 rollingData
[i
] = [date
, [0, 0, 0]];
1438 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1439 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1442 rollingData
[i
] = [date
, mult
* value
];
1445 } else if (this.attr_("customBars")) {
1450 for (var i
= 0; i
< originalData
.length
; i
++) {
1451 var data
= originalData
[i
][1];
1453 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1455 if (y
!= null && !isNaN(y
)) {
1461 if (i
- rollPeriod
>= 0) {
1462 var prev
= originalData
[i
- rollPeriod
];
1463 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1470 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1471 1.0 * (mid
- low
) / count
,
1472 1.0 * (high
- mid
) / count
]];
1475 // Calculate the rolling average for the first rollPeriod - 1 points where
1476 // there is not enough data to roll over the full number of days
1477 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1478 if (!this.attr_("errorBars")){
1479 if (rollPeriod
== 1) {
1480 return originalData
;
1483 for (var i
= 0; i
< originalData
.length
; i
++) {
1486 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1487 var y
= originalData
[j
][1];
1488 if (y
== null || isNaN(y
)) continue;
1490 sum
+= originalData
[j
][1];
1493 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1495 rollingData
[i
] = [originalData
[i
][0], null];
1500 for (var i
= 0; i
< originalData
.length
; i
++) {
1504 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1505 var y
= originalData
[j
][1][0];
1506 if (y
== null || isNaN(y
)) continue;
1508 sum
+= originalData
[j
][1][0];
1509 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1512 var stddev
= Math
.sqrt(variance
) / num_ok
;
1513 rollingData
[i
] = [originalData
[i
][0],
1514 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1516 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1526 * Parses a date, returning the number of milliseconds since epoch. This can be
1527 * passed in as an xValueParser in the Dygraph constructor.
1528 * TODO(danvk): enumerate formats that this understands.
1529 * @param {String} A date in YYYYMMDD format.
1530 * @return {Number} Milliseconds since epoch.
1533 Dygraph
.dateParser
= function(dateStr
, self
) {
1536 if (dateStr
.length
== 10 && dateStr
.search("-") != -1) { // e.g. '2009-07-12'
1537 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1538 while (dateStrSlashed
.search("-") != -1) {
1539 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1541 d
= Date
.parse(dateStrSlashed
);
1542 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1543 // TODO(danvk): remove support for this format. It's confusing.
1544 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1545 + "/" + dateStr
.substr(6,2);
1546 d
= Date
.parse(dateStrSlashed
);
1548 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1549 // "2009/07/12 12:34:56"
1550 d
= Date
.parse(dateStr
);
1553 if (!d
|| isNaN(d
)) {
1554 self
.error("Couldn't parse " + dateStr
+ " as a date");
1560 * Detects the type of the str (date or numeric) and sets the various
1561 * formatting attributes in this.attrs_ based on this type.
1562 * @param {String} str An x value.
1565 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1567 if (str
.indexOf('-') >= 0 ||
1568 str
.indexOf('/') >= 0 ||
1569 isNaN(parseFloat(str
))) {
1571 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1572 // TODO(danvk): remove support for this format.
1577 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1578 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1579 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1581 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1582 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1583 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1588 * Parses a string in a special csv format. We expect a csv file where each
1589 * line is a date point, and the first field in each line is the date string.
1590 * We also expect that all remaining fields represent series.
1591 * if the errorBars attribute is set, then interpret the fields as:
1592 * date, series1, stddev1, series2, stddev2, ...
1593 * @param {Array.<Object>} data See above.
1596 * @return Array.<Object> An array with one entry for each row. These entries
1597 * are an array of cells in that row. The first entry is the parsed x-value for
1598 * the row. The second, third, etc. are the y-values. These can take on one of
1599 * three forms, depending on the CSV and constructor parameters:
1601 * 2. [ value, stddev ]
1602 * 3. [ low value, center value, high value ]
1604 Dygraph
.prototype.parseCSV_
= function(data
) {
1606 var lines
= data
.split("\n");
1608 // Use the default delimiter or fall back to a tab if that makes sense.
1609 var delim
= this.attr_('delimiter');
1610 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1615 if (this.labelsFromCSV_
) {
1617 this.attrs_
.labels
= lines
[0].split(delim
);
1621 var defaultParserSet
= false; // attempt to auto-detect x value type
1622 var expectedCols
= this.attr_("labels").length
;
1623 var outOfOrder
= false;
1624 for (var i
= start
; i
< lines
.length
; i
++) {
1625 var line
= lines
[i
];
1626 if (line
.length
== 0) continue; // skip blank lines
1627 if (line
[0] == '#') continue; // skip comment lines
1628 var inFields
= line
.split(delim
);
1629 if (inFields
.length
< 2) continue;
1632 if (!defaultParserSet
) {
1633 this.detectTypeFromString_(inFields
[0]);
1634 xParser
= this.attr_("xValueParser");
1635 defaultParserSet
= true;
1637 fields
[0] = xParser(inFields
[0], this);
1639 // If fractions are expected, parse the numbers as "A/B
"
1640 if (this.fractions_) {
1641 for (var j = 1; j < inFields.length; j++) {
1642 // TODO(danvk): figure out an appropriate way to flag parse errors.
1643 var vals = inFields[j].split("/");
1644 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1646 } else if (this.attr_("errorBars
")) {
1647 // If there are error bars, values are (value, stddev) pairs
1648 for (var j = 1; j < inFields.length; j += 2)
1649 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1650 parseFloat(inFields[j + 1])];
1651 } else if (this.attr_("customBars
")) {
1652 // Bars are a low;center;high tuple
1653 for (var j = 1; j < inFields.length; j++) {
1654 var vals = inFields[j].split(";");
1655 fields[j] = [ parseFloat(vals[0]),
1656 parseFloat(vals[1]),
1657 parseFloat(vals[2]) ];
1660 // Values are just numbers
1661 for (var j = 1; j < inFields.length; j++) {
1662 fields[j] = parseFloat(inFields[j]);
1665 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1670 if (fields.length != expectedCols) {
1671 this.error("Number of columns
in line
" + i + " (" + fields.length +
1672 ") does not agree
with number of
labels (" + expectedCols +
1678 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1679 ret.sort(function(a,b) { return a[0] - b[0] });
1686 * The user has provided their data as a pre-packaged JS array. If the x values
1687 * are numeric, this is the same as dygraphs' internal format. If the x values
1688 * are dates, we need to convert them from Date objects to ms since epoch.
1689 * @param {Array.<Object>} data
1690 * @return {Array.<Object>} data with numeric x values.
1692 Dygraph.prototype.parseArray_ = function(data) {
1693 // Peek at the first x value to see if it's numeric.
1694 if (data.length == 0) {
1695 this.error("Can
't plot empty data set");
1698 if (data[0].length == 0) {
1699 this.error("Data set cannot contain an empty row");
1703 if (this.attr_("labels") == null) {
1704 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1705 "in the options parameter");
1706 this.attrs_.labels = [ "X" ];
1707 for (var i = 1; i < data[0].length; i++) {
1708 this.attrs_.labels.push("Y" + i);
1712 if (Dygraph.isDateLike(data[0][0])) {
1713 // Some intelligent defaults for a date x-axis.
1714 this.attrs_.xValueFormatter = Dygraph.dateString_;
1715 this.attrs_.xTicker = Dygraph.dateTicker;
1717 // Assume they're all dates
.
1718 var parsedData
= Dygraph
.clone(data
);
1719 for (var i
= 0; i
< data
.length
; i
++) {
1720 if (parsedData
[i
].length
== 0) {
1721 this.error("Row " << (1 + i
) << " of data is empty");
1724 if (parsedData
[i
][0] == null
1725 || typeof(parsedData
[i
][0].getTime
) != 'function') {
1726 this.error("x value in row " << (1 + i
) << " is not a Date");
1729 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1733 // Some intelligent defaults for a numeric x-axis.
1734 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1735 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1741 * Parses a DataTable object from gviz.
1742 * The data is expected to have a first column that is either a date or a
1743 * number. All subsequent columns must be numbers. If there is a clear mismatch
1744 * between this.xValueParser_ and the type of the first column, it will be
1745 * fixed. Returned value is in the same format as return value of parseCSV_.
1746 * @param {Array.<Object>} data See above.
1749 Dygraph
.prototype.parseDataTable_
= function(data
) {
1750 var cols
= data
.getNumberOfColumns();
1751 var rows
= data
.getNumberOfRows();
1753 // Read column labels
1755 for (var i
= 0; i
< cols
; i
++) {
1756 labels
.push(data
.getColumnLabel(i
));
1757 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1759 this.attrs_
.labels
= labels
;
1760 cols
= labels
.length
;
1762 var indepType
= data
.getColumnType(0);
1763 if (indepType
== 'date' || indepType
== 'datetime') {
1764 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1765 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1766 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1767 } else if (indepType
== 'number') {
1768 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1769 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1770 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1772 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1773 "column 1 of DataTable input (Got '" + indepType
+ "')");
1778 var outOfOrder
= false;
1779 for (var i
= 0; i
< rows
; i
++) {
1781 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
1782 data
.getValue(i
, 0) === null) {
1783 this.warning("Ignoring row " + i
+
1784 " of DataTable because of undefined or null first column.");
1788 if (indepType
== 'date' || indepType
== 'datetime') {
1789 row
.push(data
.getValue(i
, 0).getTime());
1791 row
.push(data
.getValue(i
, 0));
1793 if (!this.attr_("errorBars")) {
1794 for (var j
= 1; j
< cols
; j
++) {
1795 row
.push(data
.getValue(i
, j
));
1798 for (var j
= 0; j
< cols
- 1; j
++) {
1799 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
1802 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
1809 this.warn("DataTable is out of order; order it correctly to speed loading.");
1810 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
1815 // These functions are all based on MochiKit.
1816 Dygraph
.update
= function (self
, o
) {
1817 if (typeof(o
) != 'undefined' && o
!== null) {
1819 if (o
.hasOwnProperty(k
)) {
1827 Dygraph
.isArrayLike
= function (o
) {
1828 var typ
= typeof(o
);
1830 (typ
!= 'object' && !(typ
== 'function' &&
1831 typeof(o
.item
) == 'function')) ||
1833 typeof(o
.length
) != 'number' ||
1841 Dygraph
.isDateLike
= function (o
) {
1842 if (typeof(o
) != "object" || o
=== null ||
1843 typeof(o
.getTime
) != 'function') {
1849 Dygraph
.clone
= function(o
) {
1850 // TODO(danvk): figure out how MochiKit's version works
1852 for (var i
= 0; i
< o
.length
; i
++) {
1853 if (Dygraph
.isArrayLike(o
[i
])) {
1854 r
.push(Dygraph
.clone(o
[i
]));
1864 * Get the CSV data. If it's in a function, call that function. If it's in a
1865 * file, do an XMLHttpRequest to get it.
1868 Dygraph
.prototype.start_
= function() {
1869 if (typeof this.file_
== 'function') {
1870 // CSV string. Pretend we got it via XHR.
1871 this.loadedEvent_(this.file_());
1872 } else if (Dygraph
.isArrayLike(this.file_
)) {
1873 this.rawData_
= this.parseArray_(this.file_
);
1874 this.drawGraph_(this.rawData_
);
1875 } else if (typeof this.file_
== 'object' &&
1876 typeof this.file_
.getColumnRange
== 'function') {
1877 // must be a DataTable from gviz.
1878 this.rawData_
= this.parseDataTable_(this.file_
);
1879 this.drawGraph_(this.rawData_
);
1880 } else if (typeof this.file_
== 'string') {
1881 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
1882 if (this.file_
.indexOf('\n') >= 0) {
1883 this.loadedEvent_(this.file_
);
1885 var req
= new XMLHttpRequest();
1887 req
.onreadystatechange
= function () {
1888 if (req
.readyState
== 4) {
1889 if (req
.status
== 200) {
1890 caller
.loadedEvent_(req
.responseText
);
1895 req
.open("GET", this.file_
, true);
1899 this.error("Unknown data format: " + (typeof this.file_
));
1904 * Changes various properties of the graph. These can include:
1906 * <li>file: changes the source data for the graph</li>
1907 * <li>errorBars: changes whether the data contains stddev</li>
1909 * @param {Object} attrs The new properties and values
1911 Dygraph
.prototype.updateOptions
= function(attrs
) {
1912 // TODO(danvk): this is a mess. Rethink this function.
1913 if (attrs
.rollPeriod
) {
1914 this.rollPeriod_
= attrs
.rollPeriod
;
1916 if (attrs
.dateWindow
) {
1917 this.dateWindow_
= attrs
.dateWindow
;
1919 if (attrs
.valueRange
) {
1920 this.valueRange_
= attrs
.valueRange
;
1922 Dygraph
.update(this.user_attrs_
, attrs
);
1924 this.labelsFromCSV_
= (this.attr_("labels") == null);
1926 // TODO(danvk): this doesn't match the constructor logic
1927 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
1928 if (attrs
['file'] && attrs
['file'] != this.file_
) {
1929 this.file_
= attrs
['file'];
1932 this.drawGraph_(this.rawData_
);
1937 * Resizes the dygraph. If no parameters are specified, resizes to fill the
1938 * containing div (which has presumably changed size since the dygraph was
1939 * instantiated. If the width/height are specified, the div will be resized.
1941 * This is far more efficient than destroying and re-instantiating a
1942 * Dygraph, since it doesn't have to reparse the underlying data.
1944 * @param {Number} width Width (in pixels)
1945 * @param {Number} height Height (in pixels)
1947 Dygraph
.prototype.resize
= function(width
, height
) {
1948 if ((width
=== null) != (height
=== null)) {
1949 this.warn("Dygraph.resize() should be called with zero parameters or " +
1950 "two non-NULL parameters. Pretending it was zero.");
1951 width
= height
= null;
1954 // TODO(danvk): there should be a clear() method.
1955 this.maindiv_
.innerHTML
= "";
1956 this.attrs_
.labelsDiv
= null;
1959 this.maindiv_
.style
.width
= width
+ "px";
1960 this.maindiv_
.style
.height
= height
+ "px";
1961 this.width_
= width
;
1962 this.height_
= height
;
1964 this.width_
= this.maindiv_
.offsetWidth
;
1965 this.height_
= this.maindiv_
.offsetHeight
;
1968 this.createInterface_();
1969 this.drawGraph_(this.rawData_
);
1973 * Adjusts the number of days in the rolling average. Updates the graph to
1974 * reflect the new averaging period.
1975 * @param {Number} length Number of days over which to average the data.
1977 Dygraph
.prototype.adjustRoll
= function(length
) {
1978 this.rollPeriod_
= length
;
1979 this.drawGraph_(this.rawData_
);
1983 * Returns a boolean array of visibility statuses.
1985 Dygraph
.prototype.visibility
= function() {
1986 // Do lazy-initialization, so that this happens after we know the number of
1988 if (!this.attr_("visibility")) {
1989 this.attrs_
["visibility"] = [];
1991 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
1992 this.attr_("visibility").push(true);
1994 return this.attr_("visibility");
1998 * Changes the visiblity of a series.
2000 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2001 var x
= this.visibility();
2002 if (num
< 0 && num
>= x
.length
) {
2003 this.warn("invalid series number in setVisibility: " + num
);
2006 this.drawGraph_(this.rawData_
);
2011 * Create a new canvas element. This is more complex than a simple
2012 * document.createElement("canvas") because of IE and excanvas.
2014 Dygraph
.createCanvas
= function() {
2015 var canvas
= document
.createElement("canvas");
2017 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2019 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2027 * A wrapper around Dygraph that implements the gviz API.
2028 * @param {Object} container The DOM object the visualization should live in.
2030 Dygraph
.GVizChart
= function(container
) {
2031 this.container
= container
;
2034 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2035 this.container
.innerHTML
= '';
2036 this.date_graph
= new Dygraph(this.container
, data
, options
);
2039 // Older pages may still use this name.
2040 DateGraph
= Dygraph
;