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
= Dygraph
.createCanvas();
364 h
.style
.position
= "absolute";
365 // TODO(danvk): h should be offset from canvas. canvas needs to include
366 // some extra area to make it easier to zoom in on the far left and far
367 // right. h needs to be precisely the plot area, so that clipping occurs.
368 h
.style
.top
= canvas
.style
.top
;
369 h
.style
.left
= canvas
.style
.left
;
370 h
.width
= this.width_
;
371 h
.height
= this.height_
;
372 h
.style
.width
= this.width_
+ "px"; // for IE
373 h
.style
.height
= this.height_
+ "px"; // for IE
374 this.graphDiv
.appendChild(h
);
378 // Taken from MochiKit.Color
379 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
383 if (saturation
=== 0) {
388 var i
= Math
.floor(hue
* 6);
389 var f
= (hue
* 6) - i
;
390 var p
= value
* (1 - saturation
);
391 var q
= value
* (1 - (saturation
* f
));
392 var t
= value
* (1 - (saturation
* (1 - f
)));
394 case 1: red
= q
; green
= value
; blue
= p
; break;
395 case 2: red
= p
; green
= value
; blue
= t
; break;
396 case 3: red
= p
; green
= q
; blue
= value
; break;
397 case 4: red
= t
; green
= p
; blue
= value
; break;
398 case 5: red
= value
; green
= p
; blue
= q
; break;
399 case 6: // fall through
400 case 0: red
= value
; green
= t
; blue
= p
; break;
403 red
= Math
.floor(255 * red
+ 0.5);
404 green
= Math
.floor(255 * green
+ 0.5);
405 blue
= Math
.floor(255 * blue
+ 0.5);
406 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
411 * Generate a set of distinct colors for the data series. This is done with a
412 * color wheel. Saturation/Value are customizable, and the hue is
413 * equally-spaced around the color wheel. If a custom set of colors is
414 * specified, that is used instead.
417 Dygraph
.prototype.setColors_
= function() {
418 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
419 // away with this.renderOptions_.
420 var num
= this.attr_("labels").length
- 1;
422 var colors
= this.attr_('colors');
424 var sat
= this.attr_('colorSaturation') || 1.0;
425 var val
= this.attr_('colorValue') || 0.5;
426 for (var i
= 1; i
<= num
; i
++) {
427 if (!this.visibility()[i
-1]) continue;
428 // alternate colors for high contrast.
429 var idx
= i
- parseInt(i
% 2 ? i
/ 2 : (i - num)/2, 10);
430 var hue
= (1.0 * idx
/ (1 + num
));
431 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
434 for (var i
= 0; i
< num
; i
++) {
435 if (!this.visibility()[i
]) continue;
436 var colorStr
= colors
[i
% colors
.length
];
437 this.colors_
.push(colorStr
);
441 // TODO(danvk): update this w/r
/t/ the
new options system
.
442 this.renderOptions_
.colorScheme
= this.colors_
;
443 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
444 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
445 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
449 * Return the list of colors. This is either the list of colors passed in the
450 * attributes, or the autogenerated list of rgb(r,g,b) strings.
451 * @return {Array<string>} The list of colors.
453 Dygraph
.prototype.getColors
= function() {
457 // The following functions are from quirksmode.org with a modification for Safari from
458 // http://blog.firetree.net/2005/07/04/javascript-find-position/
459 // http://www.quirksmode.org/js
/findpos
.html
460 Dygraph
.findPosX
= function(obj
) {
465 curleft
+= obj
.offsetLeft
;
466 if(!obj
.offsetParent
)
468 obj
= obj
.offsetParent
;
475 Dygraph
.findPosY
= function(obj
) {
480 curtop
+= obj
.offsetTop
;
481 if(!obj
.offsetParent
)
483 obj
= obj
.offsetParent
;
493 * Create the div that contains information on the selected point(s)
494 * This goes in the top right of the canvas, unless an external div has already
498 Dygraph
.prototype.createStatusMessage_
= function(){
499 if (!this.attr_("labelsDiv")) {
500 var divWidth
= this.attr_('labelsDivWidth');
502 "position": "absolute",
505 "width": divWidth
+ "px",
507 "left": (this.width_
- divWidth
- 2) + "px",
508 "background": "white",
510 "overflow": "hidden"};
511 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
512 var div
= document
.createElement("div");
513 for (var name
in messagestyle
) {
514 if (messagestyle
.hasOwnProperty(name
)) {
515 div
.style
[name
] = messagestyle
[name
];
518 this.graphDiv
.appendChild(div
);
519 this.attrs_
.labelsDiv
= div
;
524 * Create the text box to adjust the averaging period
525 * @return {Object} The newly-created text box
528 Dygraph
.prototype.createRollInterface_
= function() {
529 var display
= this.attr_('showRoller') ? "block" : "none";
530 var textAttr
= { "position": "absolute",
532 "top": (this.plotter_
.area
.h
- 25) + "px",
533 "left": (this.plotter_
.area
.x
+ 1) + "px",
536 var roller
= document
.createElement("input");
537 roller
.type
= "text";
539 roller
.value
= this.rollPeriod_
;
540 for (var name
in textAttr
) {
541 if (textAttr
.hasOwnProperty(name
)) {
542 roller
.style
[name
] = textAttr
[name
];
546 var pa
= this.graphDiv
;
547 pa
.appendChild(roller
);
549 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
553 // These functions are taken from MochiKit.Signal
554 Dygraph
.pageX
= function(e
) {
556 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
559 var b
= document
.body
;
561 (de
.scrollLeft
|| b
.scrollLeft
) -
562 (de
.clientLeft
|| 0);
566 Dygraph
.pageY
= function(e
) {
568 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
571 var b
= document
.body
;
573 (de
.scrollTop
|| b
.scrollTop
) -
579 * Set up all the mouse handlers needed to capture dragging behavior for zoom
583 Dygraph
.prototype.createDragInterface_
= function() {
586 // Tracks whether the mouse is down right now
587 var isZooming
= false;
588 var isPanning
= false;
589 var dragStartX
= null;
590 var dragStartY
= null;
594 var draggingDate
= null;
595 var dateRange
= null;
597 // Utility function to convert page-wide coordinates to canvas coords
600 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
601 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
603 // Draw zoom rectangles when the mouse is down and the user moves around
604 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
606 dragEndX
= getX(event
);
607 dragEndY
= getY(event
);
609 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
611 } else if (isPanning
) {
612 dragEndX
= getX(event
);
613 dragEndY
= getY(event
);
615 // Want to have it so that:
616 // 1. draggingDate appears at dragEndX
617 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
619 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
620 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
621 self
.drawGraph_(self
.rawData_
);
625 // Track the beginning of drag events
626 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
627 px
= Dygraph
.findPosX(self
.canvas_
);
628 py
= Dygraph
.findPosY(self
.canvas_
);
629 dragStartX
= getX(event
);
630 dragStartY
= getY(event
);
632 if (event
.altKey
|| event
.shiftKey
) {
633 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
635 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
636 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
643 // If the user releases the mouse button during a drag, but not over the
644 // canvas, then it doesn't count as a zooming action.
645 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
646 if (isZooming
|| isPanning
) {
659 // Temporarily cancel the dragging event when the mouse leaves the graph
660 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
667 // If the mouse is released on the canvas during a drag event, then it's a
668 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
669 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
672 dragEndX
= getX(event
);
673 dragEndY
= getY(event
);
674 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
675 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
677 if (regionWidth
< 2 && regionHeight
< 2 &&
678 self
.attr_('clickCallback') != null &&
679 self
.lastx_
!= undefined
) {
680 // TODO(danvk): pass along more info about the points.
681 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
684 if (regionWidth
>= 10) {
685 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
686 Math
.max(dragStartX
, dragEndX
));
688 self
.canvas_
.getContext("2d").clearRect(0, 0,
690 self
.canvas_
.height
);
704 // Double-clicking zooms back out
705 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
706 if (self
.dateWindow_
== null) return;
707 self
.dateWindow_
= null;
708 self
.drawGraph_(self
.rawData_
);
709 var minDate
= self
.rawData_
[0][0];
710 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
711 if (self
.attr_("zoomCallback")) {
712 self
.attr_("zoomCallback")(minDate
, maxDate
);
718 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
719 * up any previous zoom rectangles that were drawn. This could be optimized to
720 * avoid extra redrawing, but it's tricky to avoid interactions with the status
722 * @param {Number} startX The X position where the drag started, in canvas
724 * @param {Number} endX The current X position of the drag, in canvas coords.
725 * @param {Number} prevEndX The value of endX on the previous call to this
726 * function. Used to avoid excess redrawing
729 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
730 var ctx
= this.canvas_
.getContext("2d");
732 // Clean up from the previous rect if necessary
734 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
735 Math
.abs(startX
- prevEndX
), this.height_
);
738 // Draw a light-grey rectangle to show the new viewing area
739 if (endX
&& startX
) {
740 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
741 ctx
.fillRect(Math
.min(startX
, endX
), 0,
742 Math
.abs(endX
- startX
), this.height_
);
747 * Zoom to something containing [lowX, highX]. These are pixel coordinates
748 * in the canvas. The exact zoom window may be slightly larger if there are no
749 * data points near lowX or highX. This function redraws the graph.
750 * @param {Number} lowX The leftmost pixel value that should be visible.
751 * @param {Number} highX The rightmost pixel value that should be visible.
754 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
755 // Find the earliest and latest dates contained in this canvasx range.
756 var points
= this.layout_
.points
;
759 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
760 for (var i
= 0; i
< points
.length
; i
++) {
761 var cx
= points
[i
].canvasx
;
762 var x
= points
[i
].xval
;
763 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
764 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
766 // Use the extremes if either is missing
767 if (minDate
== null) minDate
= points
[0].xval
;
768 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
770 this.dateWindow_
= [minDate
, maxDate
];
771 this.drawGraph_(this.rawData_
);
772 if (this.attr_("zoomCallback")) {
773 this.attr_("zoomCallback")(minDate
, maxDate
);
778 * When the mouse moves in the canvas, display information about a nearby data
779 * point and draw dots over those points in the data series. This function
780 * takes care of cleanup of previously-drawn dots.
781 * @param {Object} event The mousemove event from the browser.
784 Dygraph
.prototype.mouseMove_
= function(event
) {
785 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
786 var points
= this.layout_
.points
;
791 // Loop through all the points and find the date nearest to our current
793 var minDist
= 1e+100;
795 for (var i
= 0; i
< points
.length
; i
++) {
796 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
797 if (dist
> minDist
) break;
801 if (idx
>= 0) lastx
= points
[idx
].xval
;
802 // Check that you can really highlight the last day's data
803 if (canvasx
> points
[points
.length
-1].canvasx
)
804 lastx
= points
[points
.length
-1].xval
;
806 // Extract the points we've selected
807 this.selPoints_
= [];
808 for (var i
= 0; i
< points
.length
; i
++) {
809 if (points
[i
].xval
== lastx
) {
810 this.selPoints_
.push(points
[i
]);
814 if (this.attr_("highlightCallback")) {
815 var px
= this.lastHighlightCallbackX
;
816 if (px
!== null && lastx
!= px
) {
817 // only fire if the selected point has changed.
818 this.lastHighlightCallbackX
= lastx
;
819 if (!this.attr_("stackedGraph")) {
820 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
822 // "unstack" the points.
823 var callbackPoints
= this.selPoints_
.map(
824 function(p
) { return {xval
: p
.xval
, yval
: p
.yval
, name
: p
.name
} });
825 var cumulative_sum
= 0;
826 for (var j
= callbackPoints
.length
- 1; j
>= 0; j
--) {
827 callbackPoints
[j
].yval
-= cumulative_sum
;
828 cumulative_sum
+= callbackPoints
[j
].yval
;
830 this.attr_("highlightCallback")(event
, lastx
, callbackPoints
);
835 // Clear the previously drawn vertical, if there is one
836 var circleSize
= this.attr_('highlightCircleSize');
837 var ctx
= this.canvas_
.getContext("2d");
838 if (this.previousVerticalX_
>= 0) {
839 var px
= this.previousVerticalX_
;
840 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
843 var isOK
= function(x
) { return x
&& !isNaN(x
); };
845 if (this.selPoints_
.length
> 0) {
846 var canvasx
= this.selPoints_
[0].canvasx
;
848 // Set the status message to indicate the selected point(s)
849 var replace
= this.attr_('xValueFormatter')(lastx
, this) + ":";
850 var clen
= this.colors_
.length
;
851 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
852 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
853 if (this.attr_("labelsSeparateLines")) {
856 var point
= this.selPoints_
[i
];
857 var c
= new RGBColor(this.colors_
[i
%clen
]);
858 replace
+= " <b><font color='" + c
.toHex() + "'>"
859 + point
.name
+ "</font></b>:"
860 + this.round_(point
.yval
, 2);
862 this.attr_("labelsDiv").innerHTML
= replace
;
864 // Save last x position for callbacks.
867 // Draw colored circles over the center of each selected point
869 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
870 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
872 ctx
.fillStyle
= this.colors_
[i
%clen
];
873 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
874 0, 2 * Math
.PI
, false);
879 this.previousVerticalX_
= canvasx
;
884 * The mouse has left the canvas. Clear out whatever artifacts remain
885 * @param {Object} event the mouseout event from the browser.
888 Dygraph
.prototype.mouseOut_
= function(event
) {
889 if (this.attr_("hideOverlayOnMouseOut")) {
890 // Get rid of the overlay data
891 var ctx
= this.canvas_
.getContext("2d");
892 ctx
.clearRect(0, 0, this.width_
, this.height_
);
893 this.attr_("labelsDiv").innerHTML
= "";
897 Dygraph
.zeropad
= function(x
) {
898 if (x
< 10) return "0" + x
; else return "" + x
;
902 * Return a string version of the hours, minutes and seconds portion of a date.
903 * @param {Number} date The JavaScript date (ms since epoch)
904 * @return {String} A time of the form "HH:MM:SS"
907 Dygraph
.prototype.hmsString_
= function(date
) {
908 var zeropad
= Dygraph
.zeropad
;
909 var d
= new Date(date
);
910 if (d
.getSeconds()) {
911 return zeropad(d
.getHours()) + ":" +
912 zeropad(d
.getMinutes()) + ":" +
913 zeropad(d
.getSeconds());
915 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
920 * Convert a JS date (millis since epoch) to YYYY/MM/DD
921 * @param {Number} date The JavaScript date (ms since epoch)
922 * @return {String} A date of the form "YYYY/MM/DD"
924 * TODO(danvk): why is this part of the prototype?
926 Dygraph
.dateString_
= function(date
, self
) {
927 var zeropad
= Dygraph
.zeropad
;
928 var d
= new Date(date
);
931 var year
= "" + d
.getFullYear();
932 // Get a 0 padded month string
933 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
934 // Get a 0 padded day string
935 var day
= zeropad(d
.getDate());
938 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
939 if (frac
) ret
= " " + self
.hmsString_(date
);
941 return year
+ "/" + month + "/" + day
+ ret
;
945 * Round a number to the specified number of digits past the decimal point.
946 * @param {Number} num The number to round
947 * @param {Number} places The number of decimals to which to round
948 * @return {Number} The rounded number
951 Dygraph
.prototype.round_
= function(num
, places
) {
952 var shift
= Math
.pow(10, places
);
953 return Math
.round(num
* shift
)/shift
;
957 * Fires when there's data available to be graphed.
958 * @param {String} data Raw CSV data to be plotted
961 Dygraph
.prototype.loadedEvent_
= function(data
) {
962 this.rawData_
= this.parseCSV_(data
);
963 this.drawGraph_(this.rawData_
);
966 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
967 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
968 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
971 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
974 Dygraph
.prototype.addXTicks_
= function() {
975 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
976 var startDate
, endDate
;
977 if (this.dateWindow_
) {
978 startDate
= this.dateWindow_
[0];
979 endDate
= this.dateWindow_
[1];
981 startDate
= this.rawData_
[0][0];
982 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
985 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
986 this.layout_
.updateOptions({xTicks
: xTicks
});
989 // Time granularity enumeration
990 Dygraph
.SECONDLY
= 0;
991 Dygraph
.TWO_SECONDLY
= 1;
992 Dygraph
.FIVE_SECONDLY
= 2;
993 Dygraph
.TEN_SECONDLY
= 3;
994 Dygraph
.THIRTY_SECONDLY
= 4;
995 Dygraph
.MINUTELY
= 5;
996 Dygraph
.TWO_MINUTELY
= 6;
997 Dygraph
.FIVE_MINUTELY
= 7;
998 Dygraph
.TEN_MINUTELY
= 8;
999 Dygraph
.THIRTY_MINUTELY
= 9;
1000 Dygraph
.HOURLY
= 10;
1001 Dygraph
.TWO_HOURLY
= 11;
1002 Dygraph
.SIX_HOURLY
= 12;
1004 Dygraph
.WEEKLY
= 14;
1005 Dygraph
.MONTHLY
= 15;
1006 Dygraph
.QUARTERLY
= 16;
1007 Dygraph
.BIANNUAL
= 17;
1008 Dygraph
.ANNUAL
= 18;
1009 Dygraph
.DECADAL
= 19;
1010 Dygraph
.NUM_GRANULARITIES
= 20;
1012 Dygraph
.SHORT_SPACINGS
= [];
1013 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1014 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1015 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1016 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1017 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1018 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1019 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1020 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1021 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1022 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1023 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1024 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1025 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1026 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1027 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1031 // If we used this time granularity, how many ticks would there be?
1032 // This is only an approximation, but it's generally good enough.
1034 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1035 if (granularity
< Dygraph
.MONTHLY
) {
1036 // Generate one tick mark for every fixed interval of time.
1037 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1038 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1040 var year_mod
= 1; // e.g. to only print one point every 10 years.
1041 var num_months
= 12;
1042 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1043 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1044 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1045 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1047 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1048 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1049 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1055 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1056 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1058 // Returns an array containing {v: millis, label: label} dictionaries.
1060 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1062 if (granularity
< Dygraph
.MONTHLY
) {
1063 // Generate one tick mark for every fixed interval of time.
1064 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1065 var format
= '%d%b'; // e.g. "1Jan"
1067 // Find a time less than start_time which occurs on a "nice" time boundary
1068 // for this granularity.
1069 var g
= spacing
/ 1000;
1070 var d
= new Date(start_time
);
1071 if (g
<= 60) { // seconds
1072 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1076 if (g
<= 60) { // minutes
1077 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1082 if (g
<= 24) { // days
1083 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1088 if (g
== 7) { // one week
1089 d
.setDate(d
.getDate() - d
.getDay());
1094 start_time
= d
.getTime();
1096 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1097 var d
= new Date(t
);
1098 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1099 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1100 // the extra hour covers DST problems.
1101 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
1103 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
1107 // Display a tick mark on the first of a set of months of each year.
1108 // Years get a tick mark iff y % year_mod == 0. This is useful for
1109 // displaying a tick mark once every 10 years, say, on long time scales.
1111 var year_mod
= 1; // e.g. to only print one point every 10 years.
1113 if (granularity
== Dygraph
.MONTHLY
) {
1114 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1115 } else if (granularity
== Dygraph
.QUARTERLY
) {
1116 months
= [ 0, 3, 6, 9 ];
1117 } else if (granularity
== Dygraph
.BIANNUAL
) {
1119 } else if (granularity
== Dygraph
.ANNUAL
) {
1121 } else if (granularity
== Dygraph
.DECADAL
) {
1126 var start_year
= new Date(start_time
).getFullYear();
1127 var end_year
= new Date(end_time
).getFullYear();
1128 var zeropad
= Dygraph
.zeropad
;
1129 for (var i
= start_year
; i
<= end_year
; i
++) {
1130 if (i
% year_mod
!= 0) continue;
1131 for (var j
= 0; j
< months
.length
; j
++) {
1132 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1133 var t
= Date
.parse(date_str
);
1134 if (t
< start_time
|| t
> end_time
) continue;
1135 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
1145 * Add ticks to the x-axis based on a date range.
1146 * @param {Number} startDate Start of the date window (millis since epoch)
1147 * @param {Number} endDate End of the date window (millis since epoch)
1148 * @return {Array.<Object>} Array of {label, value} tuples.
1151 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1153 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1154 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1155 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1162 return self
.GetXAxis(startDate
, endDate
, chosen
);
1164 // TODO(danvk): signal error.
1169 * Add ticks when the x axis has numbers on it (instead of dates)
1170 * @param {Number} startDate Start of the date window (millis since epoch)
1171 * @param {Number} endDate End of the date window (millis since epoch)
1172 * @return {Array.<Object>} Array of {label, value} tuples.
1175 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1177 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1178 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1179 // The first spacing greater than pixelsPerYLabel is what we use.
1180 // TODO(danvk): version that works on a log scale.
1181 if (self
.attr_("labelsKMG2")) {
1182 var mults
= [1, 2, 4, 8];
1184 var mults
= [1, 2, 5];
1186 var scale
, low_val
, high_val
, nTicks
;
1187 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1188 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1189 for (var i
= -10; i
< 50; i
++) {
1190 if (self
.attr_("labelsKMG2")) {
1191 var base_scale
= Math
.pow(16, i
);
1193 var base_scale
= Math
.pow(10, i
);
1195 for (var j
= 0; j
< mults
.length
; j
++) {
1196 scale
= base_scale
* mults
[j
];
1197 low_val
= Math
.floor(minV
/ scale
) * scale
;
1198 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1199 nTicks
= (high_val
- low_val
) / scale
;
1200 var spacing
= self
.height_
/ nTicks
;
1201 // wish I could break out of both loops at once...
1202 if (spacing
> pixelsPerTick
) break;
1204 if (spacing
> pixelsPerTick
) break;
1207 // Construct labels for the ticks
1211 if (self
.attr_("labelsKMB")) {
1213 k_labels
= [ "K", "M", "B", "T" ];
1215 if (self
.attr_("labelsKMG2")) {
1216 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1218 k_labels
= [ "k", "M", "G", "T" ];
1221 for (var i
= 0; i
< nTicks
; i
++) {
1222 var tickV
= low_val
+ i
* scale
;
1223 var absTickV
= Math
.abs(tickV
);
1224 var label
= self
.round_(tickV
, 2);
1225 if (k_labels
.length
) {
1226 // Round up to an appropriate unit.
1228 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1229 if (absTickV
>= n
) {
1230 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1235 ticks
.push( {label
: label
, v
: tickV
} );
1241 * Adds appropriate ticks on the y-axis
1242 * @param {Number} minY The minimum Y value in the data set
1243 * @param {Number} maxY The maximum Y value in the data set
1246 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1247 // Set the number of ticks so that the labels are human-friendly.
1248 // TODO(danvk): make this an attribute as well.
1249 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1250 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1254 // Computes the range of the data series (including confidence intervals).
1255 // series is either [ [x1, y1], [x2, y2], ... ] or
1256 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1257 // Returns [low, high]
1258 Dygraph
.prototype.extremeValues_
= function(series
) {
1259 var minY
= null, maxY
= null;
1261 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1263 // With custom bars, maxY is the max of the high values.
1264 for (var j
= 0; j
< series
.length
; j
++) {
1265 var y
= series
[j
][1][0];
1267 var low
= y
- series
[j
][1][1];
1268 var high
= y
+ series
[j
][1][2];
1269 if (low
> y
) low
= y
; // this can happen with custom bars,
1270 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1271 if (maxY
== null || high
> maxY
) {
1274 if (minY
== null || low
< minY
) {
1279 for (var j
= 0; j
< series
.length
; j
++) {
1280 var y
= series
[j
][1];
1281 if (y
=== null || isNaN(y
)) continue;
1282 if (maxY
== null || y
> maxY
) {
1285 if (minY
== null || y
< minY
) {
1291 return [minY
, maxY
];
1295 * Update the graph with new data. Data is in the format
1296 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1297 * or, if errorBars=true,
1298 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1299 * @param {Array.<Object>} data The data (see above)
1302 Dygraph
.prototype.drawGraph_
= function(data
) {
1303 var minY
= null, maxY
= null;
1304 this.layout_
.removeAllDatasets();
1306 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1308 // For stacked series.
1309 var cumulative_y
= [];
1310 var stacked_datasets
= [];
1312 // Loop over all fields in the dataset
1314 for (var i
= 1; i
< data
[0].length
; i
++) {
1315 if (!this.visibility()[i
- 1]) continue;
1318 for (var j
= 0; j
< data
.length
; j
++) {
1319 var date
= data
[j
][0];
1320 series
[j
] = [date
, data
[j
][i
]];
1322 series
= this.rollingAverage(series
, this.rollPeriod_
);
1324 // Prune down to the desired range, if necessary (for zooming)
1325 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1326 if (this.dateWindow_
) {
1327 var low
= this.dateWindow_
[0];
1328 var high
= this.dateWindow_
[1];
1330 for (var k
= 0; k
< series
.length
; k
++) {
1331 // if (series[k][0] >= low && series[k][0] <= high) {
1332 pruned
.push(series
[k
]);
1338 var extremes
= this.extremeValues_(series
);
1339 var thisMinY
= extremes
[0];
1340 var thisMaxY
= extremes
[1];
1341 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1342 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1346 for (var j
=0; j
<series
.length
; j
++)
1347 vals
[j
] = [series
[j
][0],
1348 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1349 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1350 } else if (this.attr_("stackedGraph")) {
1352 var l
= series
.length
;
1354 for (var j
= 0; j
< l
; j
++) {
1355 if (cumulative_y
[series
[j
][0]] === undefined
)
1356 cumulative_y
[series
[j
][0]] = 0;
1358 actual_y
= series
[j
][1];
1359 cumulative_y
[series
[j
][0]] += actual_y
;
1361 vals
[j
] = [series
[j
][0], cumulative_y
[series
[j
][0]]]
1363 if (!maxY
|| cumulative_y
[series
[j
][0]] > maxY
)
1364 maxY
= cumulative_y
[series
[j
][0]];
1366 stacked_datasets
.push([this.attr_("labels")[i
], vals
]);
1367 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1369 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1373 if (stacked_datasets
.length
> 0) {
1374 for (var i
= (stacked_datasets
.length
- 1); i
>= 0; i
--) {
1375 this.layout_
.addDataset(stacked_datasets
[i
][0], stacked_datasets
[i
][1]);
1379 // Use some heuristics to come up with a good maxY value, unless it's been
1380 // set explicitly by the user.
1381 if (this.valueRange_
!= null) {
1382 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1384 // This affects the calculation of span, below.
1385 if (this.attr_("includeZero") && minY
> 0) {
1389 // Add some padding and round up to an integer to be human-friendly.
1390 var span
= maxY
- minY
;
1391 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1392 if (span
== 0) { span
= maxY
; }
1393 var maxAxisY
= maxY
+ 0.1 * span
;
1394 var minAxisY
= minY
- 0.1 * span
;
1396 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1397 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1398 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1400 if (this.attr_("includeZero")) {
1401 if (maxY
< 0) maxAxisY
= 0;
1402 if (minY
> 0) minAxisY
= 0;
1405 this.addYTicks_(minAxisY
, maxAxisY
);
1410 // Tell PlotKit to use this new data and render itself
1411 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1412 this.layout_
.evaluateWithError();
1413 this.plotter_
.clear();
1414 this.plotter_
.render();
1415 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1416 this.canvas_
.height
);
1418 if (this.attr_("drawCallback") !== null) {
1419 this.attr_("drawCallback")(this);
1424 * Calculates the rolling average of a data set.
1425 * If originalData is [label, val], rolls the average of those.
1426 * If originalData is [label, [, it's interpreted as [value, stddev]
1427 * and the roll is returned in the same form, with appropriately reduced
1428 * stddev for each value.
1429 * Note that this is where fractional input (i.e. '5/10') is converted into
1431 * @param {Array} originalData The data in the appropriate format (see above)
1432 * @param {Number} rollPeriod The number of days over which to average the data
1434 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1435 if (originalData
.length
< 2)
1436 return originalData
;
1437 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1438 var rollingData
= [];
1439 var sigma
= this.attr_("sigma");
1441 if (this.fractions_
) {
1443 var den
= 0; // numerator/denominator
1445 for (var i
= 0; i
< originalData
.length
; i
++) {
1446 num
+= originalData
[i
][1][0];
1447 den
+= originalData
[i
][1][1];
1448 if (i
- rollPeriod
>= 0) {
1449 num
-= originalData
[i
- rollPeriod
][1][0];
1450 den
-= originalData
[i
- rollPeriod
][1][1];
1453 var date
= originalData
[i
][0];
1454 var value
= den
? num
/ den
: 0.0;
1455 if (this.attr_("errorBars")) {
1456 if (this.wilsonInterval_
) {
1457 // For more details on this confidence interval, see:
1458 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1460 var p
= value
< 0 ? 0 : value
, n
= den
;
1461 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1462 var denom
= 1 + sigma
* sigma
/ den
;
1463 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1464 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1465 rollingData
[i
] = [date
,
1466 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1468 rollingData
[i
] = [date
, [0, 0, 0]];
1471 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1472 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1475 rollingData
[i
] = [date
, mult
* value
];
1478 } else if (this.attr_("customBars")) {
1483 for (var i
= 0; i
< originalData
.length
; i
++) {
1484 var data
= originalData
[i
][1];
1486 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1488 if (y
!= null && !isNaN(y
)) {
1494 if (i
- rollPeriod
>= 0) {
1495 var prev
= originalData
[i
- rollPeriod
];
1496 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1503 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1504 1.0 * (mid
- low
) / count
,
1505 1.0 * (high
- mid
) / count
]];
1508 // Calculate the rolling average for the first rollPeriod - 1 points where
1509 // there is not enough data to roll over the full number of days
1510 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1511 if (!this.attr_("errorBars")){
1512 if (rollPeriod
== 1) {
1513 return originalData
;
1516 for (var i
= 0; i
< originalData
.length
; i
++) {
1519 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1520 var y
= originalData
[j
][1];
1521 if (y
== null || isNaN(y
)) continue;
1523 sum
+= originalData
[j
][1];
1526 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1528 rollingData
[i
] = [originalData
[i
][0], null];
1533 for (var i
= 0; i
< originalData
.length
; i
++) {
1537 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1538 var y
= originalData
[j
][1][0];
1539 if (y
== null || isNaN(y
)) continue;
1541 sum
+= originalData
[j
][1][0];
1542 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1545 var stddev
= Math
.sqrt(variance
) / num_ok
;
1546 rollingData
[i
] = [originalData
[i
][0],
1547 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1549 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1559 * Parses a date, returning the number of milliseconds since epoch. This can be
1560 * passed in as an xValueParser in the Dygraph constructor.
1561 * TODO(danvk): enumerate formats that this understands.
1562 * @param {String} A date in YYYYMMDD format.
1563 * @return {Number} Milliseconds since epoch.
1566 Dygraph
.dateParser
= function(dateStr
, self
) {
1569 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1570 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1571 while (dateStrSlashed
.search("-") != -1) {
1572 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1574 d
= Date
.parse(dateStrSlashed
);
1575 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1576 // TODO(danvk): remove support for this format. It's confusing.
1577 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1578 + "/" + dateStr
.substr(6,2);
1579 d
= Date
.parse(dateStrSlashed
);
1581 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1582 // "2009/07/12 12:34:56"
1583 d
= Date
.parse(dateStr
);
1586 if (!d
|| isNaN(d
)) {
1587 self
.error("Couldn't parse " + dateStr
+ " as a date");
1593 * Detects the type of the str (date or numeric) and sets the various
1594 * formatting attributes in this.attrs_ based on this type.
1595 * @param {String} str An x value.
1598 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1600 if (str
.indexOf('-') >= 0 ||
1601 str
.indexOf('/') >= 0 ||
1602 isNaN(parseFloat(str
))) {
1604 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1605 // TODO(danvk): remove support for this format.
1610 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1611 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1612 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1614 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1615 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1616 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1621 * Parses a string in a special csv format. We expect a csv file where each
1622 * line is a date point, and the first field in each line is the date string.
1623 * We also expect that all remaining fields represent series.
1624 * if the errorBars attribute is set, then interpret the fields as:
1625 * date, series1, stddev1, series2, stddev2, ...
1626 * @param {Array.<Object>} data See above.
1629 * @return Array.<Object> An array with one entry for each row. These entries
1630 * are an array of cells in that row. The first entry is the parsed x-value for
1631 * the row. The second, third, etc. are the y-values. These can take on one of
1632 * three forms, depending on the CSV and constructor parameters:
1634 * 2. [ value, stddev ]
1635 * 3. [ low value, center value, high value ]
1637 Dygraph
.prototype.parseCSV_
= function(data
) {
1639 var lines
= data
.split("\n");
1641 // Use the default delimiter or fall back to a tab if that makes sense.
1642 var delim
= this.attr_('delimiter');
1643 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1648 if (this.labelsFromCSV_
) {
1650 this.attrs_
.labels
= lines
[0].split(delim
);
1654 var defaultParserSet
= false; // attempt to auto-detect x value type
1655 var expectedCols
= this.attr_("labels").length
;
1656 var outOfOrder
= false;
1657 for (var i
= start
; i
< lines
.length
; i
++) {
1658 var line
= lines
[i
];
1659 if (line
.length
== 0) continue; // skip blank lines
1660 if (line
[0] == '#') continue; // skip comment lines
1661 var inFields
= line
.split(delim
);
1662 if (inFields
.length
< 2) continue;
1665 if (!defaultParserSet
) {
1666 this.detectTypeFromString_(inFields
[0]);
1667 xParser
= this.attr_("xValueParser");
1668 defaultParserSet
= true;
1670 fields
[0] = xParser(inFields
[0], this);
1672 // If fractions are expected, parse the numbers as "A/B
"
1673 if (this.fractions_) {
1674 for (var j = 1; j < inFields.length; j++) {
1675 // TODO(danvk): figure out an appropriate way to flag parse errors.
1676 var vals = inFields[j].split("/");
1677 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1679 } else if (this.attr_("errorBars
")) {
1680 // If there are error bars, values are (value, stddev) pairs
1681 for (var j = 1; j < inFields.length; j += 2)
1682 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1683 parseFloat(inFields[j + 1])];
1684 } else if (this.attr_("customBars
")) {
1685 // Bars are a low;center;high tuple
1686 for (var j = 1; j < inFields.length; j++) {
1687 var vals = inFields[j].split(";");
1688 fields[j] = [ parseFloat(vals[0]),
1689 parseFloat(vals[1]),
1690 parseFloat(vals[2]) ];
1693 // Values are just numbers
1694 for (var j = 1; j < inFields.length; j++) {
1695 fields[j] = parseFloat(inFields[j]);
1698 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1703 if (fields.length != expectedCols) {
1704 this.error("Number of columns
in line
" + i + " (" + fields.length +
1705 ") does not agree
with number of
labels (" + expectedCols +
1711 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1712 ret.sort(function(a,b) { return a[0] - b[0] });
1719 * The user has provided their data as a pre-packaged JS array. If the x values
1720 * are numeric, this is the same as dygraphs' internal format. If the x values
1721 * are dates, we need to convert them from Date objects to ms since epoch.
1722 * @param {Array.<Object>} data
1723 * @return {Array.<Object>} data with numeric x values.
1725 Dygraph.prototype.parseArray_ = function(data) {
1726 // Peek at the first x value to see if it's numeric.
1727 if (data.length == 0) {
1728 this.error("Can
't plot empty data set");
1731 if (data[0].length == 0) {
1732 this.error("Data set cannot contain an empty row");
1736 if (this.attr_("labels") == null) {
1737 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1738 "in the options parameter");
1739 this.attrs_.labels = [ "X" ];
1740 for (var i = 1; i < data[0].length; i++) {
1741 this.attrs_.labels.push("Y" + i);
1745 if (Dygraph.isDateLike(data[0][0])) {
1746 // Some intelligent defaults for a date x-axis.
1747 this.attrs_.xValueFormatter = Dygraph.dateString_;
1748 this.attrs_.xTicker = Dygraph.dateTicker;
1750 // Assume they're all dates
.
1751 var parsedData
= Dygraph
.clone(data
);
1752 for (var i
= 0; i
< data
.length
; i
++) {
1753 if (parsedData
[i
].length
== 0) {
1754 this.error("Row " << (1 + i
) << " of data is empty");
1757 if (parsedData
[i
][0] == null
1758 || typeof(parsedData
[i
][0].getTime
) != 'function') {
1759 this.error("x value in row " << (1 + i
) << " is not a Date");
1762 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1766 // Some intelligent defaults for a numeric x-axis.
1767 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1768 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1774 * Parses a DataTable object from gviz.
1775 * The data is expected to have a first column that is either a date or a
1776 * number. All subsequent columns must be numbers. If there is a clear mismatch
1777 * between this.xValueParser_ and the type of the first column, it will be
1778 * fixed. Returned value is in the same format as return value of parseCSV_.
1779 * @param {Array.<Object>} data See above.
1782 Dygraph
.prototype.parseDataTable_
= function(data
) {
1783 var cols
= data
.getNumberOfColumns();
1784 var rows
= data
.getNumberOfRows();
1786 // Read column labels
1788 for (var i
= 0; i
< cols
; i
++) {
1789 labels
.push(data
.getColumnLabel(i
));
1790 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1792 this.attrs_
.labels
= labels
;
1793 cols
= labels
.length
;
1795 var indepType
= data
.getColumnType(0);
1796 if (indepType
== 'date' || indepType
== 'datetime') {
1797 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1798 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1799 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1800 } else if (indepType
== 'number') {
1801 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1802 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1803 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1805 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1806 "column 1 of DataTable input (Got '" + indepType
+ "')");
1811 var outOfOrder
= false;
1812 for (var i
= 0; i
< rows
; i
++) {
1814 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
1815 data
.getValue(i
, 0) === null) {
1816 this.warning("Ignoring row " + i
+
1817 " of DataTable because of undefined or null first column.");
1821 if (indepType
== 'date' || indepType
== 'datetime') {
1822 row
.push(data
.getValue(i
, 0).getTime());
1824 row
.push(data
.getValue(i
, 0));
1826 if (!this.attr_("errorBars")) {
1827 for (var j
= 1; j
< cols
; j
++) {
1828 row
.push(data
.getValue(i
, j
));
1831 for (var j
= 0; j
< cols
- 1; j
++) {
1832 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
1835 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
1842 this.warn("DataTable is out of order; order it correctly to speed loading.");
1843 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
1848 // These functions are all based on MochiKit.
1849 Dygraph
.update
= function (self
, o
) {
1850 if (typeof(o
) != 'undefined' && o
!== null) {
1852 if (o
.hasOwnProperty(k
)) {
1860 Dygraph
.isArrayLike
= function (o
) {
1861 var typ
= typeof(o
);
1863 (typ
!= 'object' && !(typ
== 'function' &&
1864 typeof(o
.item
) == 'function')) ||
1866 typeof(o
.length
) != 'number' ||
1874 Dygraph
.isDateLike
= function (o
) {
1875 if (typeof(o
) != "object" || o
=== null ||
1876 typeof(o
.getTime
) != 'function') {
1882 Dygraph
.clone
= function(o
) {
1883 // TODO(danvk): figure out how MochiKit's version works
1885 for (var i
= 0; i
< o
.length
; i
++) {
1886 if (Dygraph
.isArrayLike(o
[i
])) {
1887 r
.push(Dygraph
.clone(o
[i
]));
1897 * Get the CSV data. If it's in a function, call that function. If it's in a
1898 * file, do an XMLHttpRequest to get it.
1901 Dygraph
.prototype.start_
= function() {
1902 if (typeof this.file_
== 'function') {
1903 // CSV string. Pretend we got it via XHR.
1904 this.loadedEvent_(this.file_());
1905 } else if (Dygraph
.isArrayLike(this.file_
)) {
1906 this.rawData_
= this.parseArray_(this.file_
);
1907 this.drawGraph_(this.rawData_
);
1908 } else if (typeof this.file_
== 'object' &&
1909 typeof this.file_
.getColumnRange
== 'function') {
1910 // must be a DataTable from gviz.
1911 this.rawData_
= this.parseDataTable_(this.file_
);
1912 this.drawGraph_(this.rawData_
);
1913 } else if (typeof this.file_
== 'string') {
1914 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
1915 if (this.file_
.indexOf('\n') >= 0) {
1916 this.loadedEvent_(this.file_
);
1918 var req
= new XMLHttpRequest();
1920 req
.onreadystatechange
= function () {
1921 if (req
.readyState
== 4) {
1922 if (req
.status
== 200) {
1923 caller
.loadedEvent_(req
.responseText
);
1928 req
.open("GET", this.file_
, true);
1932 this.error("Unknown data format: " + (typeof this.file_
));
1937 * Changes various properties of the graph. These can include:
1939 * <li>file: changes the source data for the graph</li>
1940 * <li>errorBars: changes whether the data contains stddev</li>
1942 * @param {Object} attrs The new properties and values
1944 Dygraph
.prototype.updateOptions
= function(attrs
) {
1945 // TODO(danvk): this is a mess. Rethink this function.
1946 if (attrs
.rollPeriod
) {
1947 this.rollPeriod_
= attrs
.rollPeriod
;
1949 if (attrs
.dateWindow
) {
1950 this.dateWindow_
= attrs
.dateWindow
;
1952 if (attrs
.valueRange
) {
1953 this.valueRange_
= attrs
.valueRange
;
1955 Dygraph
.update(this.user_attrs_
, attrs
);
1957 this.labelsFromCSV_
= (this.attr_("labels") == null);
1959 // TODO(danvk): this doesn't match the constructor logic
1960 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
1961 if (attrs
['file'] && attrs
['file'] != this.file_
) {
1962 this.file_
= attrs
['file'];
1965 this.drawGraph_(this.rawData_
);
1970 * Resizes the dygraph. If no parameters are specified, resizes to fill the
1971 * containing div (which has presumably changed size since the dygraph was
1972 * instantiated. If the width/height are specified, the div will be resized.
1974 * This is far more efficient than destroying and re-instantiating a
1975 * Dygraph, since it doesn't have to reparse the underlying data.
1977 * @param {Number} width Width (in pixels)
1978 * @param {Number} height Height (in pixels)
1980 Dygraph
.prototype.resize
= function(width
, height
) {
1981 if ((width
=== null) != (height
=== null)) {
1982 this.warn("Dygraph.resize() should be called with zero parameters or " +
1983 "two non-NULL parameters. Pretending it was zero.");
1984 width
= height
= null;
1987 // TODO(danvk): there should be a clear() method.
1988 this.maindiv_
.innerHTML
= "";
1989 this.attrs_
.labelsDiv
= null;
1992 this.maindiv_
.style
.width
= width
+ "px";
1993 this.maindiv_
.style
.height
= height
+ "px";
1994 this.width_
= width
;
1995 this.height_
= height
;
1997 this.width_
= this.maindiv_
.offsetWidth
;
1998 this.height_
= this.maindiv_
.offsetHeight
;
2001 this.createInterface_();
2002 this.drawGraph_(this.rawData_
);
2006 * Adjusts the number of days in the rolling average. Updates the graph to
2007 * reflect the new averaging period.
2008 * @param {Number} length Number of days over which to average the data.
2010 Dygraph
.prototype.adjustRoll
= function(length
) {
2011 this.rollPeriod_
= length
;
2012 this.drawGraph_(this.rawData_
);
2016 * Returns a boolean array of visibility statuses.
2018 Dygraph
.prototype.visibility
= function() {
2019 // Do lazy-initialization, so that this happens after we know the number of
2021 if (!this.attr_("visibility")) {
2022 this.attrs_
["visibility"] = [];
2024 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2025 this.attr_("visibility").push(true);
2027 return this.attr_("visibility");
2031 * Changes the visiblity of a series.
2033 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2034 var x
= this.visibility();
2035 if (num
< 0 && num
>= x
.length
) {
2036 this.warn("invalid series number in setVisibility: " + num
);
2039 this.drawGraph_(this.rawData_
);
2044 * Create a new canvas element. This is more complex than a simple
2045 * document.createElement("canvas") because of IE and excanvas.
2047 Dygraph
.createCanvas
= function() {
2048 var canvas
= document
.createElement("canvas");
2050 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2052 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2060 * A wrapper around Dygraph that implements the gviz API.
2061 * @param {Object} container The DOM object the visualization should live in.
2063 Dygraph
.GVizChart
= function(container
) {
2064 this.container
= container
;
2067 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2068 this.container
.innerHTML
= '';
2069 this.date_graph
= new Dygraph(this.container
, data
, options
);
2072 // Older pages may still use this name.
2073 DateGraph
= Dygraph
;