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 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
191 if (attrs
['stackedGraph']) {
192 attrs
['fillGraph'] = true;
193 // TODO(nikhilk): Add any other stackedGraph checks here.
196 // Dygraphs has many options, some of which interact with one another.
197 // To keep track of everything, we maintain two sets of options:
199 // this.user_attrs_ only options explicitly set by the user.
200 // this.attrs_ defaults, options derived from user_attrs_, data.
202 // Options are then accessed this.attr_('attr'), which first looks at
203 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
204 // defaults without overriding behavior that the user specifically asks for.
205 this.user_attrs_
= {};
206 Dygraph
.update(this.user_attrs_
, attrs
);
209 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
211 // Make a note of whether labels will be pulled from the CSV file.
212 this.labelsFromCSV_
= (this.attr_("labels") == null);
214 // Create the containing DIV and other interactive elements
215 this.createInterface_();
220 Dygraph
.prototype.attr_
= function(name
) {
221 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
222 return this.user_attrs_
[name
];
223 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
224 return this.attrs_
[name
];
230 // TODO(danvk): any way I can get the line numbers to be this.warn call?
231 Dygraph
.prototype.log
= function(severity
, message
) {
232 if (typeof(console
) != 'undefined') {
235 console
.debug('dygraphs: ' + message
);
238 console
.info('dygraphs: ' + message
);
240 case Dygraph
.WARNING
:
241 console
.warn('dygraphs: ' + message
);
244 console
.error('dygraphs: ' + message
);
249 Dygraph
.prototype.info
= function(message
) {
250 this.log(Dygraph
.INFO
, message
);
252 Dygraph
.prototype.warn
= function(message
) {
253 this.log(Dygraph
.WARNING
, message
);
255 Dygraph
.prototype.error
= function(message
) {
256 this.log(Dygraph
.ERROR
, message
);
260 * Returns the current rolling period, as set by the user or an option.
261 * @return {Number} The number of days in the rolling window
263 Dygraph
.prototype.rollPeriod
= function() {
264 return this.rollPeriod_
;
268 * Returns the currently-visible x-range. This can be affected by zooming,
269 * panning or a call to updateOptions.
270 * Returns a two-element array: [left, right].
271 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
273 Dygraph
.prototype.xAxisRange
= function() {
274 if (this.dateWindow_
) return this.dateWindow_
;
276 // The entire chart is visible.
277 var left
= this.rawData_
[0][0];
278 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
279 return [left
, right
];
282 Dygraph
.addEvent
= function(el
, evt
, fn
) {
283 var normed_fn
= function(e
) {
284 if (!e
) var e
= window
.event
;
287 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
288 el
.addEventListener(evt
, normed_fn
, false);
290 el
.attachEvent('on' + evt
, normed_fn
);
295 * Generates interface elements for the Dygraph: a containing div, a div to
296 * display the current point, and a textbox to adjust the rolling average
297 * period. Also creates the Renderer/Layout elements.
300 Dygraph
.prototype.createInterface_
= function() {
301 // Create the all-enclosing graph div
302 var enclosing
= this.maindiv_
;
304 this.graphDiv
= document
.createElement("div");
305 this.graphDiv
.style
.width
= this.width_
+ "px";
306 this.graphDiv
.style
.height
= this.height_
+ "px";
307 enclosing
.appendChild(this.graphDiv
);
309 // Create the canvas for interactive parts of the chart.
310 // this.canvas_ = document.createElement("canvas");
311 this.canvas_
= Dygraph
.createCanvas();
312 this.canvas_
.style
.position
= "absolute";
313 this.canvas_
.width
= this.width_
;
314 this.canvas_
.height
= this.height_
;
315 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
316 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
317 this.graphDiv
.appendChild(this.canvas_
);
319 // ... and for static parts of the chart.
320 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
323 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
324 dygraph
.mouseMove_(e
);
326 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
327 dygraph
.mouseOut_(e
);
330 // Create the grapher
331 // TODO(danvk): why does the Layout need its own set of options?
332 this.layoutOptions_
= { 'xOriginIsZero': false };
333 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
334 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
335 Dygraph
.update(this.layoutOptions_
, {
336 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
338 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
340 // TODO(danvk): why does the Renderer need its own set of options?
341 this.renderOptions_
= { colorScheme
: this.colors_
,
343 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
344 Dygraph
.update(this.renderOptions_
, this.attrs_
);
345 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
346 this.plotter_
= new DygraphCanvasRenderer(this,
347 this.hidden_
, this.layout_
,
348 this.renderOptions_
);
350 this.createStatusMessage_();
351 this.createRollInterface_();
352 this.createDragInterface_();
356 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
357 * this particular canvas. All Dygraph work is done on this.canvas_.
358 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
359 * @return {Object} The newly-created canvas
362 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
363 // var h = document.createElement("canvas");
364 var h
= Dygraph
.createCanvas();
365 h
.style
.position
= "absolute";
366 h
.style
.top
= canvas
.style
.top
;
367 h
.style
.left
= canvas
.style
.left
;
368 h
.width
= this.width_
;
369 h
.height
= this.height_
;
370 h
.style
.width
= this.width_
+ "px"; // for IE
371 h
.style
.height
= this.height_
+ "px"; // for IE
372 this.graphDiv
.appendChild(h
);
376 // Taken from MochiKit.Color
377 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
381 if (saturation
=== 0) {
386 var i
= Math
.floor(hue
* 6);
387 var f
= (hue
* 6) - i
;
388 var p
= value
* (1 - saturation
);
389 var q
= value
* (1 - (saturation
* f
));
390 var t
= value
* (1 - (saturation
* (1 - f
)));
392 case 1: red
= q
; green
= value
; blue
= p
; break;
393 case 2: red
= p
; green
= value
; blue
= t
; break;
394 case 3: red
= p
; green
= q
; blue
= value
; break;
395 case 4: red
= t
; green
= p
; blue
= value
; break;
396 case 5: red
= value
; green
= p
; blue
= q
; break;
397 case 6: // fall through
398 case 0: red
= value
; green
= t
; blue
= p
; break;
401 red
= Math
.floor(255 * red
+ 0.5);
402 green
= Math
.floor(255 * green
+ 0.5);
403 blue
= Math
.floor(255 * blue
+ 0.5);
404 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
409 * Generate a set of distinct colors for the data series. This is done with a
410 * color wheel. Saturation/Value are customizable, and the hue is
411 * equally-spaced around the color wheel. If a custom set of colors is
412 * specified, that is used instead.
415 Dygraph
.prototype.setColors_
= function() {
416 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
417 // away with this.renderOptions_.
418 var num
= this.attr_("labels").length
- 1;
420 var colors
= this.attr_('colors');
422 var sat
= this.attr_('colorSaturation') || 1.0;
423 var val
= this.attr_('colorValue') || 0.5;
424 for (var i
= 1; i
<= num
; i
++) {
425 if (!this.visibility()[i
-1]) continue;
426 // alternate colors for high contrast.
427 var idx
= i
- parseInt(i
% 2 ? i
/ 2 : (i - num)/2, 10);
428 var hue
= (1.0 * idx
/ (1 + num
));
429 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
432 for (var i
= 0; i
< num
; i
++) {
433 if (!this.visibility()[i
]) continue;
434 var colorStr
= colors
[i
% colors
.length
];
435 this.colors_
.push(colorStr
);
439 // TODO(danvk): update this w/r
/t/ the
new options system
.
440 this.renderOptions_
.colorScheme
= this.colors_
;
441 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
442 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
443 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
447 * Return the list of colors. This is either the list of colors passed in the
448 * attributes, or the autogenerated list of rgb(r,g,b) strings.
449 * @return {Array<string>} The list of colors.
451 Dygraph
.prototype.getColors
= function() {
455 // The following functions are from quirksmode.org with a modification for Safari from
456 // http://blog.firetree.net/2005/07/04/javascript-find-position/
457 // http://www.quirksmode.org/js
/findpos
.html
458 Dygraph
.findPosX
= function(obj
) {
463 curleft
+= obj
.offsetLeft
;
464 if(!obj
.offsetParent
)
466 obj
= obj
.offsetParent
;
473 Dygraph
.findPosY
= function(obj
) {
478 curtop
+= obj
.offsetTop
;
479 if(!obj
.offsetParent
)
481 obj
= obj
.offsetParent
;
491 * Create the div that contains information on the selected point(s)
492 * This goes in the top right of the canvas, unless an external div has already
496 Dygraph
.prototype.createStatusMessage_
= function(){
497 if (!this.attr_("labelsDiv")) {
498 var divWidth
= this.attr_('labelsDivWidth');
500 "position": "absolute",
503 "width": divWidth
+ "px",
505 "left": (this.width_
- divWidth
- 2) + "px",
506 "background": "white",
508 "overflow": "hidden"};
509 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
510 var div
= document
.createElement("div");
511 for (var name
in messagestyle
) {
512 if (messagestyle
.hasOwnProperty(name
)) {
513 div
.style
[name
] = messagestyle
[name
];
516 this.graphDiv
.appendChild(div
);
517 this.attrs_
.labelsDiv
= div
;
522 * Create the text box to adjust the averaging period
523 * @return {Object} The newly-created text box
526 Dygraph
.prototype.createRollInterface_
= function() {
527 var display
= this.attr_('showRoller') ? "block" : "none";
528 var textAttr
= { "position": "absolute",
530 "top": (this.plotter_
.area
.h
- 25) + "px",
531 "left": (this.plotter_
.area
.x
+ 1) + "px",
534 var roller
= document
.createElement("input");
535 roller
.type
= "text";
537 roller
.value
= this.rollPeriod_
;
538 for (var name
in textAttr
) {
539 if (textAttr
.hasOwnProperty(name
)) {
540 roller
.style
[name
] = textAttr
[name
];
544 var pa
= this.graphDiv
;
545 pa
.appendChild(roller
);
547 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
551 // These functions are taken from MochiKit.Signal
552 Dygraph
.pageX
= function(e
) {
554 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
557 var b
= document
.body
;
559 (de
.scrollLeft
|| b
.scrollLeft
) -
560 (de
.clientLeft
|| 0);
564 Dygraph
.pageY
= function(e
) {
566 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
569 var b
= document
.body
;
571 (de
.scrollTop
|| b
.scrollTop
) -
577 * Set up all the mouse handlers needed to capture dragging behavior for zoom
581 Dygraph
.prototype.createDragInterface_
= function() {
584 // Tracks whether the mouse is down right now
585 var isZooming
= false;
586 var isPanning
= false;
587 var dragStartX
= null;
588 var dragStartY
= null;
592 var draggingDate
= null;
593 var dateRange
= null;
595 // Utility function to convert page-wide coordinates to canvas coords
598 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
599 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
601 // Draw zoom rectangles when the mouse is down and the user moves around
602 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
604 dragEndX
= getX(event
);
605 dragEndY
= getY(event
);
607 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
609 } else if (isPanning
) {
610 dragEndX
= getX(event
);
611 dragEndY
= getY(event
);
613 // Want to have it so that:
614 // 1. draggingDate appears at dragEndX
615 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
617 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
618 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
619 self
.drawGraph_(self
.rawData_
);
623 // Track the beginning of drag events
624 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
625 px
= Dygraph
.findPosX(self
.canvas_
);
626 py
= Dygraph
.findPosY(self
.canvas_
);
627 dragStartX
= getX(event
);
628 dragStartY
= getY(event
);
630 if (event
.altKey
|| event
.shiftKey
) {
631 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
633 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
634 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
641 // If the user releases the mouse button during a drag, but not over the
642 // canvas, then it doesn't count as a zooming action.
643 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
644 if (isZooming
|| isPanning
) {
657 // Temporarily cancel the dragging event when the mouse leaves the graph
658 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
665 // If the mouse is released on the canvas during a drag event, then it's a
666 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
667 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
670 dragEndX
= getX(event
);
671 dragEndY
= getY(event
);
672 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
673 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
675 if (regionWidth
< 2 && regionHeight
< 2 &&
676 self
.attr_('clickCallback') != null &&
677 self
.lastx_
!= undefined
) {
678 // TODO(danvk): pass along more info about the points.
679 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
682 if (regionWidth
>= 10) {
683 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
684 Math
.max(dragStartX
, dragEndX
));
686 self
.canvas_
.getContext("2d").clearRect(0, 0,
688 self
.canvas_
.height
);
702 // Double-clicking zooms back out
703 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
704 if (self
.dateWindow_
== null) return;
705 self
.dateWindow_
= null;
706 self
.drawGraph_(self
.rawData_
);
707 var minDate
= self
.rawData_
[0][0];
708 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
709 if (self
.attr_("zoomCallback")) {
710 self
.attr_("zoomCallback")(minDate
, maxDate
);
716 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
717 * up any previous zoom rectangles that were drawn. This could be optimized to
718 * avoid extra redrawing, but it's tricky to avoid interactions with the status
720 * @param {Number} startX The X position where the drag started, in canvas
722 * @param {Number} endX The current X position of the drag, in canvas coords.
723 * @param {Number} prevEndX The value of endX on the previous call to this
724 * function. Used to avoid excess redrawing
727 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
728 var ctx
= this.canvas_
.getContext("2d");
730 // Clean up from the previous rect if necessary
732 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
733 Math
.abs(startX
- prevEndX
), this.height_
);
736 // Draw a light-grey rectangle to show the new viewing area
737 if (endX
&& startX
) {
738 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
739 ctx
.fillRect(Math
.min(startX
, endX
), 0,
740 Math
.abs(endX
- startX
), this.height_
);
745 * Zoom to something containing [lowX, highX]. These are pixel coordinates
746 * in the canvas. The exact zoom window may be slightly larger if there are no
747 * data points near lowX or highX. This function redraws the graph.
748 * @param {Number} lowX The leftmost pixel value that should be visible.
749 * @param {Number} highX The rightmost pixel value that should be visible.
752 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
753 // Find the earliest and latest dates contained in this canvasx range.
754 var points
= this.layout_
.points
;
757 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
758 for (var i
= 0; i
< points
.length
; i
++) {
759 var cx
= points
[i
].canvasx
;
760 var x
= points
[i
].xval
;
761 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
762 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
764 // Use the extremes if either is missing
765 if (minDate
== null) minDate
= points
[0].xval
;
766 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
768 this.dateWindow_
= [minDate
, maxDate
];
769 this.drawGraph_(this.rawData_
);
770 if (this.attr_("zoomCallback")) {
771 this.attr_("zoomCallback")(minDate
, maxDate
);
776 * When the mouse moves in the canvas, display information about a nearby data
777 * point and draw dots over those points in the data series. This function
778 * takes care of cleanup of previously-drawn dots.
779 * @param {Object} event The mousemove event from the browser.
782 Dygraph
.prototype.mouseMove_
= function(event
) {
783 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
784 var points
= this.layout_
.points
;
789 // Loop through all the points and find the date nearest to our current
791 var minDist
= 1e+100;
793 for (var i
= 0; i
< points
.length
; i
++) {
794 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
795 if (dist
> minDist
) break;
799 if (idx
>= 0) lastx
= points
[idx
].xval
;
800 // Check that you can really highlight the last day's data
801 if (canvasx
> points
[points
.length
-1].canvasx
)
802 lastx
= points
[points
.length
-1].xval
;
804 // Extract the points we've selected
805 this.selPoints_
= [];
806 for (var i
= 0; i
< points
.length
; i
++) {
807 if (points
[i
].xval
== lastx
) {
808 this.selPoints_
.push(points
[i
]);
812 if (this.attr_("highlightCallback")) {
813 var px
= this.lastHighlightCallbackX
;
814 if (px
!== null && lastx
!= px
) {
815 // only fire if the selected point has changed.
816 this.lastHighlightCallbackX
= lastx
;
817 if (!this.attr_("stackedGraph")) {
818 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
820 // "unstack" the points.
821 var callbackPoints
= this.selPoints_
.map(
822 function(p
) { return {xval
: p
.xval
, yval
: p
.yval
, name
: p
.name
} });
823 var cumulative_sum
= 0;
824 for (var j
= callbackPoints
.length
- 1; j
>= 0; j
--) {
825 callbackPoints
[j
].yval
-= cumulative_sum
;
826 cumulative_sum
+= callbackPoints
[j
].yval
;
828 this.attr_("highlightCallback")(event
, lastx
, callbackPoints
);
833 // Clear the previously drawn vertical, if there is one
834 var circleSize
= this.attr_('highlightCircleSize');
835 var ctx
= this.canvas_
.getContext("2d");
836 if (this.previousVerticalX_
>= 0) {
837 var px
= this.previousVerticalX_
;
838 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
841 var isOK
= function(x
) { return x
&& !isNaN(x
); };
843 if (this.selPoints_
.length
> 0) {
844 var canvasx
= this.selPoints_
[0].canvasx
;
846 // Set the status message to indicate the selected point(s)
847 var replace
= this.attr_('xValueFormatter')(lastx
, this) + ":";
848 var clen
= this.colors_
.length
;
849 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
850 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
851 if (this.attr_("labelsSeparateLines")) {
854 var point
= this.selPoints_
[i
];
855 var c
= new RGBColor(this.colors_
[i
%clen
]);
856 replace
+= " <b><font color='" + c
.toHex() + "'>"
857 + point
.name
+ "</font></b>:"
858 + this.round_(point
.yval
, 2);
860 this.attr_("labelsDiv").innerHTML
= replace
;
862 // Save last x position for callbacks.
865 // Draw colored circles over the center of each selected point
867 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
868 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
870 ctx
.fillStyle
= this.colors_
[i
%clen
];
871 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
872 0, 2 * Math
.PI
, false);
877 this.previousVerticalX_
= canvasx
;
882 * The mouse has left the canvas. Clear out whatever artifacts remain
883 * @param {Object} event the mouseout event from the browser.
886 Dygraph
.prototype.mouseOut_
= function(event
) {
887 if (this.attr_("hideOverlayOnMouseOut")) {
888 // Get rid of the overlay data
889 var ctx
= this.canvas_
.getContext("2d");
890 ctx
.clearRect(0, 0, this.width_
, this.height_
);
891 this.attr_("labelsDiv").innerHTML
= "";
895 Dygraph
.zeropad
= function(x
) {
896 if (x
< 10) return "0" + x
; else return "" + x
;
900 * Return a string version of the hours, minutes and seconds portion of a date.
901 * @param {Number} date The JavaScript date (ms since epoch)
902 * @return {String} A time of the form "HH:MM:SS"
905 Dygraph
.prototype.hmsString_
= function(date
) {
906 var zeropad
= Dygraph
.zeropad
;
907 var d
= new Date(date
);
908 if (d
.getSeconds()) {
909 return zeropad(d
.getHours()) + ":" +
910 zeropad(d
.getMinutes()) + ":" +
911 zeropad(d
.getSeconds());
913 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
918 * Convert a JS date (millis since epoch) to YYYY/MM/DD
919 * @param {Number} date The JavaScript date (ms since epoch)
920 * @return {String} A date of the form "YYYY/MM/DD"
922 * TODO(danvk): why is this part of the prototype?
924 Dygraph
.dateString_
= function(date
, self
) {
925 var zeropad
= Dygraph
.zeropad
;
926 var d
= new Date(date
);
929 var year
= "" + d
.getFullYear();
930 // Get a 0 padded month string
931 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
932 // Get a 0 padded day string
933 var day
= zeropad(d
.getDate());
936 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
937 if (frac
) ret
= " " + self
.hmsString_(date
);
939 return year
+ "/" + month + "/" + day
+ ret
;
943 * Round a number to the specified number of digits past the decimal point.
944 * @param {Number} num The number to round
945 * @param {Number} places The number of decimals to which to round
946 * @return {Number} The rounded number
949 Dygraph
.prototype.round_
= function(num
, places
) {
950 var shift
= Math
.pow(10, places
);
951 return Math
.round(num
* shift
)/shift
;
955 * Fires when there's data available to be graphed.
956 * @param {String} data Raw CSV data to be plotted
959 Dygraph
.prototype.loadedEvent_
= function(data
) {
960 this.rawData_
= this.parseCSV_(data
);
961 this.drawGraph_(this.rawData_
);
964 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
965 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
966 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
969 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
972 Dygraph
.prototype.addXTicks_
= function() {
973 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
974 var startDate
, endDate
;
975 if (this.dateWindow_
) {
976 startDate
= this.dateWindow_
[0];
977 endDate
= this.dateWindow_
[1];
979 startDate
= this.rawData_
[0][0];
980 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
983 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
984 this.layout_
.updateOptions({xTicks
: xTicks
});
987 // Time granularity enumeration
988 Dygraph
.SECONDLY
= 0;
989 Dygraph
.TWO_SECONDLY
= 1;
990 Dygraph
.FIVE_SECONDLY
= 2;
991 Dygraph
.TEN_SECONDLY
= 3;
992 Dygraph
.THIRTY_SECONDLY
= 4;
993 Dygraph
.MINUTELY
= 5;
994 Dygraph
.TWO_MINUTELY
= 6;
995 Dygraph
.FIVE_MINUTELY
= 7;
996 Dygraph
.TEN_MINUTELY
= 8;
997 Dygraph
.THIRTY_MINUTELY
= 9;
999 Dygraph
.TWO_HOURLY
= 11;
1000 Dygraph
.SIX_HOURLY
= 12;
1002 Dygraph
.WEEKLY
= 14;
1003 Dygraph
.MONTHLY
= 15;
1004 Dygraph
.QUARTERLY
= 16;
1005 Dygraph
.BIANNUAL
= 17;
1006 Dygraph
.ANNUAL
= 18;
1007 Dygraph
.DECADAL
= 19;
1008 Dygraph
.NUM_GRANULARITIES
= 20;
1010 Dygraph
.SHORT_SPACINGS
= [];
1011 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1012 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1013 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1014 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1015 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1016 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1017 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1018 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1019 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1020 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1021 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1022 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1023 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1024 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1025 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1029 // If we used this time granularity, how many ticks would there be?
1030 // This is only an approximation, but it's generally good enough.
1032 Dygraph
.prototype.NumXTicks
= 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 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1038 var year_mod
= 1; // e.g. to only print one point every 10 years.
1039 var num_months
= 12;
1040 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1041 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1042 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1043 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1045 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1046 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1047 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1053 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1054 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1056 // Returns an array containing {v: millis, label: label} dictionaries.
1058 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1060 if (granularity
< Dygraph
.MONTHLY
) {
1061 // Generate one tick mark for every fixed interval of time.
1062 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1063 var format
= '%d%b'; // e.g. "1Jan"
1065 // Find a time less than start_time which occurs on a "nice" time boundary
1066 // for this granularity.
1067 var g
= spacing
/ 1000;
1068 var d
= new Date(start_time
);
1069 if (g
<= 60) { // seconds
1070 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1074 if (g
<= 60) { // minutes
1075 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1080 if (g
<= 24) { // days
1081 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1086 if (g
== 7) { // one week
1087 d
.setDate(d
.getDate() - d
.getDay());
1092 start_time
= d
.getTime();
1094 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1095 var d
= new Date(t
);
1096 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1097 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1098 // the extra hour covers DST problems.
1099 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
1101 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
1105 // Display a tick mark on the first of a set of months of each year.
1106 // Years get a tick mark iff y % year_mod == 0. This is useful for
1107 // displaying a tick mark once every 10 years, say, on long time scales.
1109 var year_mod
= 1; // e.g. to only print one point every 10 years.
1111 if (granularity
== Dygraph
.MONTHLY
) {
1112 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1113 } else if (granularity
== Dygraph
.QUARTERLY
) {
1114 months
= [ 0, 3, 6, 9 ];
1115 } else if (granularity
== Dygraph
.BIANNUAL
) {
1117 } else if (granularity
== Dygraph
.ANNUAL
) {
1119 } else if (granularity
== Dygraph
.DECADAL
) {
1124 var start_year
= new Date(start_time
).getFullYear();
1125 var end_year
= new Date(end_time
).getFullYear();
1126 var zeropad
= Dygraph
.zeropad
;
1127 for (var i
= start_year
; i
<= end_year
; i
++) {
1128 if (i
% year_mod
!= 0) continue;
1129 for (var j
= 0; j
< months
.length
; j
++) {
1130 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1131 var t
= Date
.parse(date_str
);
1132 if (t
< start_time
|| t
> end_time
) continue;
1133 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
1143 * Add ticks to the x-axis based on a date range.
1144 * @param {Number} startDate Start of the date window (millis since epoch)
1145 * @param {Number} endDate End of the date window (millis since epoch)
1146 * @return {Array.<Object>} Array of {label, value} tuples.
1149 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1151 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1152 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1153 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1160 return self
.GetXAxis(startDate
, endDate
, chosen
);
1162 // TODO(danvk): signal error.
1167 * Add ticks when the x axis has numbers on it (instead of dates)
1168 * @param {Number} startDate Start of the date window (millis since epoch)
1169 * @param {Number} endDate End of the date window (millis since epoch)
1170 * @return {Array.<Object>} Array of {label, value} tuples.
1173 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1175 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1176 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1177 // The first spacing greater than pixelsPerYLabel is what we use.
1178 // TODO(danvk): version that works on a log scale.
1179 if (self
.attr_("labelsKMG2")) {
1180 var mults
= [1, 2, 4, 8];
1182 var mults
= [1, 2, 5];
1184 var scale
, low_val
, high_val
, nTicks
;
1185 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1186 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1187 for (var i
= -10; i
< 50; i
++) {
1188 if (self
.attr_("labelsKMG2")) {
1189 var base_scale
= Math
.pow(16, i
);
1191 var base_scale
= Math
.pow(10, i
);
1193 for (var j
= 0; j
< mults
.length
; j
++) {
1194 scale
= base_scale
* mults
[j
];
1195 low_val
= Math
.floor(minV
/ scale
) * scale
;
1196 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1197 nTicks
= (high_val
- low_val
) / scale
;
1198 var spacing
= self
.height_
/ nTicks
;
1199 // wish I could break out of both loops at once...
1200 if (spacing
> pixelsPerTick
) break;
1202 if (spacing
> pixelsPerTick
) break;
1205 // Construct labels for the ticks
1209 if (self
.attr_("labelsKMB")) {
1211 k_labels
= [ "K", "M", "B", "T" ];
1213 if (self
.attr_("labelsKMG2")) {
1214 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1216 k_labels
= [ "k", "M", "G", "T" ];
1219 for (var i
= 0; i
< nTicks
; i
++) {
1220 var tickV
= low_val
+ i
* scale
;
1221 var absTickV
= Math
.abs(tickV
);
1222 var label
= self
.round_(tickV
, 2);
1223 if (k_labels
.length
) {
1224 // Round up to an appropriate unit.
1226 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1227 if (absTickV
>= n
) {
1228 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1233 ticks
.push( {label
: label
, v
: tickV
} );
1239 * Adds appropriate ticks on the y-axis
1240 * @param {Number} minY The minimum Y value in the data set
1241 * @param {Number} maxY The maximum Y value in the data set
1244 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1245 // Set the number of ticks so that the labels are human-friendly.
1246 // TODO(danvk): make this an attribute as well.
1247 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1248 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1252 // Computes the range of the data series (including confidence intervals).
1253 // series is either [ [x1, y1], [x2, y2], ... ] or
1254 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1255 // Returns [low, high]
1256 Dygraph
.prototype.extremeValues_
= function(series
) {
1257 var minY
= null, maxY
= null;
1259 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1261 // With custom bars, maxY is the max of the high values.
1262 for (var j
= 0; j
< series
.length
; j
++) {
1263 var y
= series
[j
][1][0];
1265 var low
= y
- series
[j
][1][1];
1266 var high
= y
+ series
[j
][1][2];
1267 if (low
> y
) low
= y
; // this can happen with custom bars,
1268 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1269 if (maxY
== null || high
> maxY
) {
1272 if (minY
== null || low
< minY
) {
1277 for (var j
= 0; j
< series
.length
; j
++) {
1278 var y
= series
[j
][1];
1279 if (y
=== null || isNaN(y
)) continue;
1280 if (maxY
== null || y
> maxY
) {
1283 if (minY
== null || y
< minY
) {
1289 return [minY
, maxY
];
1293 * Update the graph with new data. Data is in the format
1294 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1295 * or, if errorBars=true,
1296 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1297 * @param {Array.<Object>} data The data (see above)
1300 Dygraph
.prototype.drawGraph_
= function(data
) {
1301 var minY
= null, maxY
= null;
1302 this.layout_
.removeAllDatasets();
1304 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1306 // For stacked series.
1307 var cumulative_y
= [];
1308 var stacked_datasets
= [];
1310 // Loop over all fields in the dataset
1312 for (var i
= 1; i
< data
[0].length
; i
++) {
1313 if (!this.visibility()[i
- 1]) continue;
1316 for (var j
= 0; j
< data
.length
; j
++) {
1317 var date
= data
[j
][0];
1318 series
[j
] = [date
, data
[j
][i
]];
1320 series
= this.rollingAverage(series
, this.rollPeriod_
);
1322 // Prune down to the desired range, if necessary (for zooming)
1323 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1324 if (this.dateWindow_
) {
1325 var low
= this.dateWindow_
[0];
1326 var high
= this.dateWindow_
[1];
1328 for (var k
= 0; k
< series
.length
; k
++) {
1329 if (series
[k
][0] >= low
&& series
[k
][0] <= high
) {
1330 pruned
.push(series
[k
]);
1336 var extremes
= this.extremeValues_(series
);
1337 var thisMinY
= extremes
[0];
1338 var thisMaxY
= extremes
[1];
1339 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1340 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1344 for (var j
=0; j
<series
.length
; j
++)
1345 vals
[j
] = [series
[j
][0],
1346 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1347 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1348 } else if (this.attr_("stackedGraph")) {
1350 var l
= series
.length
;
1352 for (var j
= 0; j
< l
; j
++) {
1353 if (cumulative_y
[series
[j
][0]] === undefined
)
1354 cumulative_y
[series
[j
][0]] = 0;
1356 actual_y
= series
[j
][1];
1357 cumulative_y
[series
[j
][0]] += actual_y
;
1359 vals
[j
] = [series
[j
][0], cumulative_y
[series
[j
][0]]]
1361 if (!maxY
|| cumulative_y
[series
[j
][0]] > maxY
)
1362 maxY
= cumulative_y
[series
[j
][0]];
1364 stacked_datasets
.push([this.attr_("labels")[i
], vals
]);
1365 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1367 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1371 if (stacked_datasets
.length
> 0) {
1372 for (var i
= (stacked_datasets
.length
- 1); i
>= 0; i
--) {
1373 this.layout_
.addDataset(stacked_datasets
[i
][0], stacked_datasets
[i
][1]);
1377 // Use some heuristics to come up with a good maxY value, unless it's been
1378 // set explicitly by the user.
1379 if (this.valueRange_
!= null) {
1380 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1382 // This affects the calculation of span, below.
1383 if (this.attr_("includeZero") && minY
> 0) {
1387 // Add some padding and round up to an integer to be human-friendly.
1388 var span
= maxY
- minY
;
1389 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1390 if (span
== 0) { span
= maxY
; }
1391 var maxAxisY
= maxY
+ 0.1 * span
;
1392 var minAxisY
= minY
- 0.1 * span
;
1394 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1395 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1396 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1398 if (this.attr_("includeZero")) {
1399 if (maxY
< 0) maxAxisY
= 0;
1400 if (minY
> 0) minAxisY
= 0;
1403 this.addYTicks_(minAxisY
, maxAxisY
);
1408 // Tell PlotKit to use this new data and render itself
1409 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1410 this.layout_
.evaluateWithError();
1411 this.plotter_
.clear();
1412 this.plotter_
.render();
1413 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1414 this.canvas_
.height
);
1416 if (this.attr_("drawCallback") !== null) {
1417 this.attr_("drawCallback")(this);
1422 * Calculates the rolling average of a data set.
1423 * If originalData is [label, val], rolls the average of those.
1424 * If originalData is [label, [, it's interpreted as [value, stddev]
1425 * and the roll is returned in the same form, with appropriately reduced
1426 * stddev for each value.
1427 * Note that this is where fractional input (i.e. '5/10') is converted into
1429 * @param {Array} originalData The data in the appropriate format (see above)
1430 * @param {Number} rollPeriod The number of days over which to average the data
1432 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1433 if (originalData
.length
< 2)
1434 return originalData
;
1435 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1436 var rollingData
= [];
1437 var sigma
= this.attr_("sigma");
1439 if (this.fractions_
) {
1441 var den
= 0; // numerator/denominator
1443 for (var i
= 0; i
< originalData
.length
; i
++) {
1444 num
+= originalData
[i
][1][0];
1445 den
+= originalData
[i
][1][1];
1446 if (i
- rollPeriod
>= 0) {
1447 num
-= originalData
[i
- rollPeriod
][1][0];
1448 den
-= originalData
[i
- rollPeriod
][1][1];
1451 var date
= originalData
[i
][0];
1452 var value
= den
? num
/ den
: 0.0;
1453 if (this.attr_("errorBars")) {
1454 if (this.wilsonInterval_
) {
1455 // For more details on this confidence interval, see:
1456 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1458 var p
= value
< 0 ? 0 : value
, n
= den
;
1459 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1460 var denom
= 1 + sigma
* sigma
/ den
;
1461 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1462 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1463 rollingData
[i
] = [date
,
1464 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1466 rollingData
[i
] = [date
, [0, 0, 0]];
1469 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1470 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1473 rollingData
[i
] = [date
, mult
* value
];
1476 } else if (this.attr_("customBars")) {
1481 for (var i
= 0; i
< originalData
.length
; i
++) {
1482 var data
= originalData
[i
][1];
1484 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1486 if (y
!= null && !isNaN(y
)) {
1492 if (i
- rollPeriod
>= 0) {
1493 var prev
= originalData
[i
- rollPeriod
];
1494 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1501 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1502 1.0 * (mid
- low
) / count
,
1503 1.0 * (high
- mid
) / count
]];
1506 // Calculate the rolling average for the first rollPeriod - 1 points where
1507 // there is not enough data to roll over the full number of days
1508 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1509 if (!this.attr_("errorBars")){
1510 if (rollPeriod
== 1) {
1511 return originalData
;
1514 for (var i
= 0; i
< originalData
.length
; i
++) {
1517 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1518 var y
= originalData
[j
][1];
1519 if (y
== null || isNaN(y
)) continue;
1521 sum
+= originalData
[j
][1];
1524 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1526 rollingData
[i
] = [originalData
[i
][0], null];
1531 for (var i
= 0; i
< originalData
.length
; i
++) {
1535 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1536 var y
= originalData
[j
][1][0];
1537 if (y
== null || isNaN(y
)) continue;
1539 sum
+= originalData
[j
][1][0];
1540 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1543 var stddev
= Math
.sqrt(variance
) / num_ok
;
1544 rollingData
[i
] = [originalData
[i
][0],
1545 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1547 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1557 * Parses a date, returning the number of milliseconds since epoch. This can be
1558 * passed in as an xValueParser in the Dygraph constructor.
1559 * TODO(danvk): enumerate formats that this understands.
1560 * @param {String} A date in YYYYMMDD format.
1561 * @return {Number} Milliseconds since epoch.
1564 Dygraph
.dateParser
= function(dateStr
, self
) {
1567 if (dateStr
.length
== 10 && dateStr
.search("-") != -1) { // e.g. '2009-07-12'
1568 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1569 while (dateStrSlashed
.search("-") != -1) {
1570 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1572 d
= Date
.parse(dateStrSlashed
);
1573 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1574 // TODO(danvk): remove support for this format. It's confusing.
1575 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1576 + "/" + dateStr
.substr(6,2);
1577 d
= Date
.parse(dateStrSlashed
);
1579 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1580 // "2009/07/12 12:34:56"
1581 d
= Date
.parse(dateStr
);
1584 if (!d
|| isNaN(d
)) {
1585 self
.error("Couldn't parse " + dateStr
+ " as a date");
1591 * Detects the type of the str (date or numeric) and sets the various
1592 * formatting attributes in this.attrs_ based on this type.
1593 * @param {String} str An x value.
1596 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1598 if (str
.indexOf('-') >= 0 ||
1599 str
.indexOf('/') >= 0 ||
1600 isNaN(parseFloat(str
))) {
1602 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1603 // TODO(danvk): remove support for this format.
1608 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1609 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1610 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1612 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1613 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1614 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1619 * Parses a string in a special csv format. We expect a csv file where each
1620 * line is a date point, and the first field in each line is the date string.
1621 * We also expect that all remaining fields represent series.
1622 * if the errorBars attribute is set, then interpret the fields as:
1623 * date, series1, stddev1, series2, stddev2, ...
1624 * @param {Array.<Object>} data See above.
1627 * @return Array.<Object> An array with one entry for each row. These entries
1628 * are an array of cells in that row. The first entry is the parsed x-value for
1629 * the row. The second, third, etc. are the y-values. These can take on one of
1630 * three forms, depending on the CSV and constructor parameters:
1632 * 2. [ value, stddev ]
1633 * 3. [ low value, center value, high value ]
1635 Dygraph
.prototype.parseCSV_
= function(data
) {
1637 var lines
= data
.split("\n");
1639 // Use the default delimiter or fall back to a tab if that makes sense.
1640 var delim
= this.attr_('delimiter');
1641 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1646 if (this.labelsFromCSV_
) {
1648 this.attrs_
.labels
= lines
[0].split(delim
);
1652 var defaultParserSet
= false; // attempt to auto-detect x value type
1653 var expectedCols
= this.attr_("labels").length
;
1654 var outOfOrder
= false;
1655 for (var i
= start
; i
< lines
.length
; i
++) {
1656 var line
= lines
[i
];
1657 if (line
.length
== 0) continue; // skip blank lines
1658 if (line
[0] == '#') continue; // skip comment lines
1659 var inFields
= line
.split(delim
);
1660 if (inFields
.length
< 2) continue;
1663 if (!defaultParserSet
) {
1664 this.detectTypeFromString_(inFields
[0]);
1665 xParser
= this.attr_("xValueParser");
1666 defaultParserSet
= true;
1668 fields
[0] = xParser(inFields
[0], this);
1670 // If fractions are expected, parse the numbers as "A/B
"
1671 if (this.fractions_) {
1672 for (var j = 1; j < inFields.length; j++) {
1673 // TODO(danvk): figure out an appropriate way to flag parse errors.
1674 var vals = inFields[j].split("/");
1675 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1677 } else if (this.attr_("errorBars
")) {
1678 // If there are error bars, values are (value, stddev) pairs
1679 for (var j = 1; j < inFields.length; j += 2)
1680 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1681 parseFloat(inFields[j + 1])];
1682 } else if (this.attr_("customBars
")) {
1683 // Bars are a low;center;high tuple
1684 for (var j = 1; j < inFields.length; j++) {
1685 var vals = inFields[j].split(";");
1686 fields[j] = [ parseFloat(vals[0]),
1687 parseFloat(vals[1]),
1688 parseFloat(vals[2]) ];
1691 // Values are just numbers
1692 for (var j = 1; j < inFields.length; j++) {
1693 fields[j] = parseFloat(inFields[j]);
1696 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1701 if (fields.length != expectedCols) {
1702 this.error("Number of columns
in line
" + i + " (" + fields.length +
1703 ") does not agree
with number of
labels (" + expectedCols +
1709 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1710 ret.sort(function(a,b) { return a[0] - b[0] });
1717 * The user has provided their data as a pre-packaged JS array. If the x values
1718 * are numeric, this is the same as dygraphs' internal format. If the x values
1719 * are dates, we need to convert them from Date objects to ms since epoch.
1720 * @param {Array.<Object>} data
1721 * @return {Array.<Object>} data with numeric x values.
1723 Dygraph.prototype.parseArray_ = function(data) {
1724 // Peek at the first x value to see if it's numeric.
1725 if (data.length == 0) {
1726 this.error("Can
't plot empty data set");
1729 if (data[0].length == 0) {
1730 this.error("Data set cannot contain an empty row");
1734 if (this.attr_("labels") == null) {
1735 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1736 "in the options parameter");
1737 this.attrs_.labels = [ "X" ];
1738 for (var i = 1; i < data[0].length; i++) {
1739 this.attrs_.labels.push("Y" + i);
1743 if (Dygraph.isDateLike(data[0][0])) {
1744 // Some intelligent defaults for a date x-axis.
1745 this.attrs_.xValueFormatter = Dygraph.dateString_;
1746 this.attrs_.xTicker = Dygraph.dateTicker;
1748 // Assume they're all dates
.
1749 var parsedData
= Dygraph
.clone(data
);
1750 for (var i
= 0; i
< data
.length
; i
++) {
1751 if (parsedData
[i
].length
== 0) {
1752 this.error("Row " << (1 + i
) << " of data is empty");
1755 if (parsedData
[i
][0] == null
1756 || typeof(parsedData
[i
][0].getTime
) != 'function') {
1757 this.error("x value in row " << (1 + i
) << " is not a Date");
1760 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1764 // Some intelligent defaults for a numeric x-axis.
1765 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1766 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1772 * Parses a DataTable object from gviz.
1773 * The data is expected to have a first column that is either a date or a
1774 * number. All subsequent columns must be numbers. If there is a clear mismatch
1775 * between this.xValueParser_ and the type of the first column, it will be
1776 * fixed. Returned value is in the same format as return value of parseCSV_.
1777 * @param {Array.<Object>} data See above.
1780 Dygraph
.prototype.parseDataTable_
= function(data
) {
1781 var cols
= data
.getNumberOfColumns();
1782 var rows
= data
.getNumberOfRows();
1784 // Read column labels
1786 for (var i
= 0; i
< cols
; i
++) {
1787 labels
.push(data
.getColumnLabel(i
));
1788 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1790 this.attrs_
.labels
= labels
;
1791 cols
= labels
.length
;
1793 var indepType
= data
.getColumnType(0);
1794 if (indepType
== 'date' || indepType
== 'datetime') {
1795 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1796 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1797 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1798 } else if (indepType
== 'number') {
1799 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1800 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1801 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1803 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1804 "column 1 of DataTable input (Got '" + indepType
+ "')");
1809 var outOfOrder
= false;
1810 for (var i
= 0; i
< rows
; i
++) {
1812 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
1813 data
.getValue(i
, 0) === null) {
1814 this.warning("Ignoring row " + i
+
1815 " of DataTable because of undefined or null first column.");
1819 if (indepType
== 'date' || indepType
== 'datetime') {
1820 row
.push(data
.getValue(i
, 0).getTime());
1822 row
.push(data
.getValue(i
, 0));
1824 if (!this.attr_("errorBars")) {
1825 for (var j
= 1; j
< cols
; j
++) {
1826 row
.push(data
.getValue(i
, j
));
1829 for (var j
= 0; j
< cols
- 1; j
++) {
1830 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
1833 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
1840 this.warn("DataTable is out of order; order it correctly to speed loading.");
1841 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
1846 // These functions are all based on MochiKit.
1847 Dygraph
.update
= function (self
, o
) {
1848 if (typeof(o
) != 'undefined' && o
!== null) {
1850 if (o
.hasOwnProperty(k
)) {
1858 Dygraph
.isArrayLike
= function (o
) {
1859 var typ
= typeof(o
);
1861 (typ
!= 'object' && !(typ
== 'function' &&
1862 typeof(o
.item
) == 'function')) ||
1864 typeof(o
.length
) != 'number' ||
1872 Dygraph
.isDateLike
= function (o
) {
1873 if (typeof(o
) != "object" || o
=== null ||
1874 typeof(o
.getTime
) != 'function') {
1880 Dygraph
.clone
= function(o
) {
1881 // TODO(danvk): figure out how MochiKit's version works
1883 for (var i
= 0; i
< o
.length
; i
++) {
1884 if (Dygraph
.isArrayLike(o
[i
])) {
1885 r
.push(Dygraph
.clone(o
[i
]));
1895 * Get the CSV data. If it's in a function, call that function. If it's in a
1896 * file, do an XMLHttpRequest to get it.
1899 Dygraph
.prototype.start_
= function() {
1900 if (typeof this.file_
== 'function') {
1901 // CSV string. Pretend we got it via XHR.
1902 this.loadedEvent_(this.file_());
1903 } else if (Dygraph
.isArrayLike(this.file_
)) {
1904 this.rawData_
= this.parseArray_(this.file_
);
1905 this.drawGraph_(this.rawData_
);
1906 } else if (typeof this.file_
== 'object' &&
1907 typeof this.file_
.getColumnRange
== 'function') {
1908 // must be a DataTable from gviz.
1909 this.rawData_
= this.parseDataTable_(this.file_
);
1910 this.drawGraph_(this.rawData_
);
1911 } else if (typeof this.file_
== 'string') {
1912 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
1913 if (this.file_
.indexOf('\n') >= 0) {
1914 this.loadedEvent_(this.file_
);
1916 var req
= new XMLHttpRequest();
1918 req
.onreadystatechange
= function () {
1919 if (req
.readyState
== 4) {
1920 if (req
.status
== 200) {
1921 caller
.loadedEvent_(req
.responseText
);
1926 req
.open("GET", this.file_
, true);
1930 this.error("Unknown data format: " + (typeof this.file_
));
1935 * Changes various properties of the graph. These can include:
1937 * <li>file: changes the source data for the graph</li>
1938 * <li>errorBars: changes whether the data contains stddev</li>
1940 * @param {Object} attrs The new properties and values
1942 Dygraph
.prototype.updateOptions
= function(attrs
) {
1943 // TODO(danvk): this is a mess. Rethink this function.
1944 if (attrs
.rollPeriod
) {
1945 this.rollPeriod_
= attrs
.rollPeriod
;
1947 if (attrs
.dateWindow
) {
1948 this.dateWindow_
= attrs
.dateWindow
;
1950 if (attrs
.valueRange
) {
1951 this.valueRange_
= attrs
.valueRange
;
1953 Dygraph
.update(this.user_attrs_
, attrs
);
1955 this.labelsFromCSV_
= (this.attr_("labels") == null);
1957 // TODO(danvk): this doesn't match the constructor logic
1958 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
1959 if (attrs
['file'] && attrs
['file'] != this.file_
) {
1960 this.file_
= attrs
['file'];
1963 this.drawGraph_(this.rawData_
);
1968 * Resizes the dygraph. If no parameters are specified, resizes to fill the
1969 * containing div (which has presumably changed size since the dygraph was
1970 * instantiated. If the width/height are specified, the div will be resized.
1972 * This is far more efficient than destroying and re-instantiating a
1973 * Dygraph, since it doesn't have to reparse the underlying data.
1975 * @param {Number} width Width (in pixels)
1976 * @param {Number} height Height (in pixels)
1978 Dygraph
.prototype.resize
= function(width
, height
) {
1979 if ((width
=== null) != (height
=== null)) {
1980 this.warn("Dygraph.resize() should be called with zero parameters or " +
1981 "two non-NULL parameters. Pretending it was zero.");
1982 width
= height
= null;
1985 // TODO(danvk): there should be a clear() method.
1986 this.maindiv_
.innerHTML
= "";
1987 this.attrs_
.labelsDiv
= null;
1990 this.maindiv_
.style
.width
= width
+ "px";
1991 this.maindiv_
.style
.height
= height
+ "px";
1992 this.width_
= width
;
1993 this.height_
= height
;
1995 this.width_
= this.maindiv_
.offsetWidth
;
1996 this.height_
= this.maindiv_
.offsetHeight
;
1999 this.createInterface_();
2000 this.drawGraph_(this.rawData_
);
2004 * Adjusts the number of days in the rolling average. Updates the graph to
2005 * reflect the new averaging period.
2006 * @param {Number} length Number of days over which to average the data.
2008 Dygraph
.prototype.adjustRoll
= function(length
) {
2009 this.rollPeriod_
= length
;
2010 this.drawGraph_(this.rawData_
);
2014 * Returns a boolean array of visibility statuses.
2016 Dygraph
.prototype.visibility
= function() {
2017 // Do lazy-initialization, so that this happens after we know the number of
2019 if (!this.attr_("visibility")) {
2020 this.attrs_
["visibility"] = [];
2022 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2023 this.attr_("visibility").push(true);
2025 return this.attr_("visibility");
2029 * Changes the visiblity of a series.
2031 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2032 var x
= this.visibility();
2033 if (num
< 0 && num
>= x
.length
) {
2034 this.warn("invalid series number in setVisibility: " + num
);
2037 this.drawGraph_(this.rawData_
);
2042 * Create a new canvas element. This is more complex than a simple
2043 * document.createElement("canvas") because of IE and excanvas.
2045 Dygraph
.createCanvas
= function() {
2046 var canvas
= document
.createElement("canvas");
2048 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2050 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2058 * A wrapper around Dygraph that implements the gviz API.
2059 * @param {Object} container The DOM object the visualization should live in.
2061 Dygraph
.GVizChart
= function(container
) {
2062 this.container
= container
;
2065 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2066 this.container
.innerHTML
= '';
2067 this.date_graph
= new Dygraph(this.container
, data
, options
);
2070 // Older pages may still use this name.
2071 DateGraph
= Dygraph
;