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;
165 this.is_initial_draw_
= true;
167 // Clear the div. This ensure that, if multiple dygraphs are passed the same
168 // div, then only one will be drawn.
171 // If the div isn't already sized then inherit from our attrs or
172 // give it a default size.
173 if (div
.style
.width
== '') {
174 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
176 if (div
.style
.height
== '') {
177 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
179 this.width_
= parseInt(div
.style
.width
, 10);
180 this.height_
= parseInt(div
.style
.height
, 10);
181 // The div might have been specified as percent of the current window size,
182 // convert that to an appropriate number of pixels.
183 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
184 // Minus ten pixels keeps scrollbars from showing up for a 100% width div.
185 this.width_
= (this.width_
* self
.innerWidth
/ 100) - 10;
187 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
188 this.height_
= (this.height_
* self
.innerHeight
/ 100) - 10;
191 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
192 if (attrs
['stackedGraph']) {
193 attrs
['fillGraph'] = true;
194 // TODO(nikhilk): Add any other stackedGraph checks here.
197 // Dygraphs has many options, some of which interact with one another.
198 // To keep track of everything, we maintain two sets of options:
200 // this.user_attrs_ only options explicitly set by the user.
201 // this.attrs_ defaults, options derived from user_attrs_, data.
203 // Options are then accessed this.attr_('attr'), which first looks at
204 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
205 // defaults without overriding behavior that the user specifically asks for.
206 this.user_attrs_
= {};
207 Dygraph
.update(this.user_attrs_
, attrs
);
210 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
212 this.boundaryIds_
= [];
214 // Make a note of whether labels will be pulled from the CSV file.
215 this.labelsFromCSV_
= (this.attr_("labels") == null);
217 // Create the containing DIV and other interactive elements
218 this.createInterface_();
223 Dygraph
.prototype.attr_
= function(name
) {
224 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
225 return this.user_attrs_
[name
];
226 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
227 return this.attrs_
[name
];
233 // TODO(danvk): any way I can get the line numbers to be this.warn call?
234 Dygraph
.prototype.log
= function(severity
, message
) {
235 if (typeof(console
) != 'undefined') {
238 console
.debug('dygraphs: ' + message
);
241 console
.info('dygraphs: ' + message
);
243 case Dygraph
.WARNING
:
244 console
.warn('dygraphs: ' + message
);
247 console
.error('dygraphs: ' + message
);
252 Dygraph
.prototype.info
= function(message
) {
253 this.log(Dygraph
.INFO
, message
);
255 Dygraph
.prototype.warn
= function(message
) {
256 this.log(Dygraph
.WARNING
, message
);
258 Dygraph
.prototype.error
= function(message
) {
259 this.log(Dygraph
.ERROR
, message
);
263 * Returns the current rolling period, as set by the user or an option.
264 * @return {Number} The number of days in the rolling window
266 Dygraph
.prototype.rollPeriod
= function() {
267 return this.rollPeriod_
;
271 * Returns the currently-visible x-range. This can be affected by zooming,
272 * panning or a call to updateOptions.
273 * Returns a two-element array: [left, right].
274 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
276 Dygraph
.prototype.xAxisRange
= function() {
277 if (this.dateWindow_
) return this.dateWindow_
;
279 // The entire chart is visible.
280 var left
= this.rawData_
[0][0];
281 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
282 return [left
, right
];
286 * Returns the currently-visible y-range. This can be affected by zooming,
287 * panning or a call to updateOptions.
288 * Returns a two-element array: [bottom, top].
290 Dygraph
.prototype.yAxisRange
= function() {
291 return this.displayedYRange_
;
295 * Convert from data coordinates to canvas/div X/Y coordinates.
296 * Returns a two-element array: [X, Y]
298 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
299 var ret
= [null, null];
300 var area
= this.plotter_
.area
;
302 var xRange
= this.xAxisRange();
303 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
307 var yRange
= this.yAxisRange();
308 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
314 // TODO(danvk): use these functions throughout dygraphs.
316 * Convert from canvas/div coords to data coordinates.
317 * Returns a two-element array: [X, Y]
319 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
320 var ret
= [null, null];
321 var area
= this.plotter_
.area
;
323 var xRange
= this.xAxisRange();
324 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
328 var yRange
= this.yAxisRange();
329 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
335 Dygraph
.addEvent
= function(el
, evt
, fn
) {
336 var normed_fn
= function(e
) {
337 if (!e
) var e
= window
.event
;
340 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
341 el
.addEventListener(evt
, normed_fn
, false);
343 el
.attachEvent('on' + evt
, normed_fn
);
347 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
348 var ctx
= cnv
.getContext("2d");
350 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
355 * Generates interface elements for the Dygraph: a containing div, a div to
356 * display the current point, and a textbox to adjust the rolling average
357 * period. Also creates the Renderer/Layout elements.
360 Dygraph
.prototype.createInterface_
= function() {
361 // Create the all-enclosing graph div
362 var enclosing
= this.maindiv_
;
364 this.graphDiv
= document
.createElement("div");
365 this.graphDiv
.style
.width
= this.width_
+ "px";
366 this.graphDiv
.style
.height
= this.height_
+ "px";
367 enclosing
.appendChild(this.graphDiv
);
371 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
373 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
374 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
375 - 2 * this.attr_("axisTickSize");
376 this.clippingArea_
= clip
;
378 // Create the canvas for interactive parts of the chart.
379 this.canvas_
= Dygraph
.createCanvas();
380 this.canvas_
.style
.position
= "absolute";
381 this.canvas_
.width
= this.width_
;
382 this.canvas_
.height
= this.height_
;
383 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
384 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
385 this.graphDiv
.appendChild(this.canvas_
);
387 // ... and for static parts of the chart.
388 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
390 // Make sure we don't overdraw.
391 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
392 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
395 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
396 dygraph
.mouseMove_(e
);
398 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
399 dygraph
.mouseOut_(e
);
402 // Create the grapher
403 // TODO(danvk): why does the Layout need its own set of options?
404 this.layoutOptions_
= { 'xOriginIsZero': false };
405 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
406 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
407 Dygraph
.update(this.layoutOptions_
, {
408 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
410 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
412 // TODO(danvk): why does the Renderer need its own set of options?
413 this.renderOptions_
= { colorScheme
: this.colors_
,
415 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
416 Dygraph
.update(this.renderOptions_
, this.attrs_
);
417 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
418 this.plotter_
= new DygraphCanvasRenderer(this,
419 this.hidden_
, this.layout_
,
420 this.renderOptions_
);
422 this.createStatusMessage_();
423 this.createRollInterface_();
424 this.createDragInterface_();
428 * Detach DOM elements in the dygraph and null out all data references.
429 * Calling this when you're done with a dygraph can dramatically reduce memory
430 * usage. See, e.g., the tests/perf.html example.
432 Dygraph
.prototype.destroy
= function() {
433 var removeRecursive
= function(node
) {
434 while (node
.hasChildNodes()) {
435 removeRecursive(node
.firstChild
);
436 node
.removeChild(node
.firstChild
);
439 removeRecursive(this.maindiv_
);
441 var nullOut
= function(obj
) {
443 if (typeof(obj
[n
]) === 'object') {
449 // These may not all be necessary, but it can't hurt...
450 nullOut(this.layout_
);
451 nullOut(this.plotter_
);
456 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
457 * this particular canvas. All Dygraph work is done on this.canvas_.
458 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
459 * @return {Object} The newly-created canvas
462 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
463 var h
= Dygraph
.createCanvas();
464 h
.style
.position
= "absolute";
465 // TODO(danvk): h should be offset from canvas. canvas needs to include
466 // some extra area to make it easier to zoom in on the far left and far
467 // right. h needs to be precisely the plot area, so that clipping occurs.
468 h
.style
.top
= canvas
.style
.top
;
469 h
.style
.left
= canvas
.style
.left
;
470 h
.width
= this.width_
;
471 h
.height
= this.height_
;
472 h
.style
.width
= this.width_
+ "px"; // for IE
473 h
.style
.height
= this.height_
+ "px"; // for IE
474 this.graphDiv
.appendChild(h
);
478 // Taken from MochiKit.Color
479 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
483 if (saturation
=== 0) {
488 var i
= Math
.floor(hue
* 6);
489 var f
= (hue
* 6) - i
;
490 var p
= value
* (1 - saturation
);
491 var q
= value
* (1 - (saturation
* f
));
492 var t
= value
* (1 - (saturation
* (1 - f
)));
494 case 1: red
= q
; green
= value
; blue
= p
; break;
495 case 2: red
= p
; green
= value
; blue
= t
; break;
496 case 3: red
= p
; green
= q
; blue
= value
; break;
497 case 4: red
= t
; green
= p
; blue
= value
; break;
498 case 5: red
= value
; green
= p
; blue
= q
; break;
499 case 6: // fall through
500 case 0: red
= value
; green
= t
; blue
= p
; break;
503 red
= Math
.floor(255 * red
+ 0.5);
504 green
= Math
.floor(255 * green
+ 0.5);
505 blue
= Math
.floor(255 * blue
+ 0.5);
506 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
511 * Generate a set of distinct colors for the data series. This is done with a
512 * color wheel. Saturation/Value are customizable, and the hue is
513 * equally-spaced around the color wheel. If a custom set of colors is
514 * specified, that is used instead.
517 Dygraph
.prototype.setColors_
= function() {
518 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
519 // away with this.renderOptions_.
520 var num
= this.attr_("labels").length
- 1;
522 var colors
= this.attr_('colors');
524 var sat
= this.attr_('colorSaturation') || 1.0;
525 var val
= this.attr_('colorValue') || 0.5;
526 for (var i
= 1; i
<= num
; i
++) {
527 if (!this.visibility()[i
-1]) continue;
528 // alternate colors for high contrast.
529 var idx
= i
- parseInt(i
% 2 ? i
/ 2 : (i - num)/2, 10);
530 var hue
= (1.0 * idx
/ (1 + num
));
531 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
534 for (var i
= 0; i
< num
; i
++) {
535 if (!this.visibility()[i
]) continue;
536 var colorStr
= colors
[i
% colors
.length
];
537 this.colors_
.push(colorStr
);
541 // TODO(danvk): update this w/r
/t/ the
new options system
.
542 this.renderOptions_
.colorScheme
= this.colors_
;
543 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
544 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
545 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
549 * Return the list of colors. This is either the list of colors passed in the
550 * attributes, or the autogenerated list of rgb(r,g,b) strings.
551 * @return {Array<string>} The list of colors.
553 Dygraph
.prototype.getColors
= function() {
557 // The following functions are from quirksmode.org with a modification for Safari from
558 // http://blog.firetree.net/2005/07/04/javascript-find-position/
559 // http://www.quirksmode.org/js
/findpos
.html
560 Dygraph
.findPosX
= function(obj
) {
565 curleft
+= obj
.offsetLeft
;
566 if(!obj
.offsetParent
)
568 obj
= obj
.offsetParent
;
575 Dygraph
.findPosY
= function(obj
) {
580 curtop
+= obj
.offsetTop
;
581 if(!obj
.offsetParent
)
583 obj
= obj
.offsetParent
;
593 * Create the div that contains information on the selected point(s)
594 * This goes in the top right of the canvas, unless an external div has already
598 Dygraph
.prototype.createStatusMessage_
= function(){
599 if (!this.attr_("labelsDiv")) {
600 var divWidth
= this.attr_('labelsDivWidth');
602 "position": "absolute",
605 "width": divWidth
+ "px",
607 "left": (this.width_
- divWidth
- 2) + "px",
608 "background": "white",
610 "overflow": "hidden"};
611 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
612 var div
= document
.createElement("div");
613 for (var name
in messagestyle
) {
614 if (messagestyle
.hasOwnProperty(name
)) {
615 div
.style
[name
] = messagestyle
[name
];
618 this.graphDiv
.appendChild(div
);
619 this.attrs_
.labelsDiv
= div
;
624 * Create the text box to adjust the averaging period
625 * @return {Object} The newly-created text box
628 Dygraph
.prototype.createRollInterface_
= function() {
629 var display
= this.attr_('showRoller') ? "block" : "none";
630 var textAttr
= { "position": "absolute",
632 "top": (this.plotter_
.area
.h
- 25) + "px",
633 "left": (this.plotter_
.area
.x
+ 1) + "px",
636 var roller
= document
.createElement("input");
637 roller
.type
= "text";
639 roller
.value
= this.rollPeriod_
;
640 for (var name
in textAttr
) {
641 if (textAttr
.hasOwnProperty(name
)) {
642 roller
.style
[name
] = textAttr
[name
];
646 var pa
= this.graphDiv
;
647 pa
.appendChild(roller
);
649 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
653 // These functions are taken from MochiKit.Signal
654 Dygraph
.pageX
= function(e
) {
656 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
659 var b
= document
.body
;
661 (de
.scrollLeft
|| b
.scrollLeft
) -
662 (de
.clientLeft
|| 0);
666 Dygraph
.pageY
= function(e
) {
668 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
671 var b
= document
.body
;
673 (de
.scrollTop
|| b
.scrollTop
) -
679 * Set up all the mouse handlers needed to capture dragging behavior for zoom
683 Dygraph
.prototype.createDragInterface_
= function() {
686 // Tracks whether the mouse is down right now
687 var isZooming
= false;
688 var isPanning
= false;
689 var dragStartX
= null;
690 var dragStartY
= null;
694 var draggingDate
= null;
695 var dateRange
= null;
697 // Utility function to convert page-wide coordinates to canvas coords
700 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
701 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
703 // Draw zoom rectangles when the mouse is down and the user moves around
704 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
706 dragEndX
= getX(event
);
707 dragEndY
= getY(event
);
709 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
711 } else if (isPanning
) {
712 dragEndX
= getX(event
);
713 dragEndY
= getY(event
);
715 // Want to have it so that:
716 // 1. draggingDate appears at dragEndX
717 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
719 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
720 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
721 self
.drawGraph_(self
.rawData_
);
725 // Track the beginning of drag events
726 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
727 px
= Dygraph
.findPosX(self
.canvas_
);
728 py
= Dygraph
.findPosY(self
.canvas_
);
729 dragStartX
= getX(event
);
730 dragStartY
= getY(event
);
732 if (event
.altKey
|| event
.shiftKey
) {
733 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
735 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
736 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
743 // If the user releases the mouse button during a drag, but not over the
744 // canvas, then it doesn't count as a zooming action.
745 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
746 if (isZooming
|| isPanning
) {
759 // Temporarily cancel the dragging event when the mouse leaves the graph
760 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
767 // If the mouse is released on the canvas during a drag event, then it's a
768 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
769 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
772 dragEndX
= getX(event
);
773 dragEndY
= getY(event
);
774 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
775 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
777 if (regionWidth
< 2 && regionHeight
< 2 &&
778 self
.attr_('clickCallback') != null &&
779 self
.lastx_
!= undefined
) {
780 // TODO(danvk): pass along more info about the points.
781 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
784 if (regionWidth
>= 10) {
785 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
786 Math
.max(dragStartX
, dragEndX
));
788 self
.canvas_
.getContext("2d").clearRect(0, 0,
790 self
.canvas_
.height
);
804 // Double-clicking zooms back out
805 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
806 if (self
.dateWindow_
== null) return;
807 self
.dateWindow_
= null;
808 self
.drawGraph_(self
.rawData_
);
809 var minDate
= self
.rawData_
[0][0];
810 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
811 if (self
.attr_("zoomCallback")) {
812 self
.attr_("zoomCallback")(minDate
, maxDate
);
818 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
819 * up any previous zoom rectangles that were drawn. This could be optimized to
820 * avoid extra redrawing, but it's tricky to avoid interactions with the status
822 * @param {Number} startX The X position where the drag started, in canvas
824 * @param {Number} endX The current X position of the drag, in canvas coords.
825 * @param {Number} prevEndX The value of endX on the previous call to this
826 * function. Used to avoid excess redrawing
829 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
830 var ctx
= this.canvas_
.getContext("2d");
832 // Clean up from the previous rect if necessary
834 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
835 Math
.abs(startX
- prevEndX
), this.height_
);
838 // Draw a light-grey rectangle to show the new viewing area
839 if (endX
&& startX
) {
840 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
841 ctx
.fillRect(Math
.min(startX
, endX
), 0,
842 Math
.abs(endX
- startX
), this.height_
);
847 * Zoom to something containing [lowX, highX]. These are pixel coordinates
848 * in the canvas. The exact zoom window may be slightly larger if there are no
849 * data points near lowX or highX. This function redraws the graph.
850 * @param {Number} lowX The leftmost pixel value that should be visible.
851 * @param {Number} highX The rightmost pixel value that should be visible.
854 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
855 // Find the earliest and latest dates contained in this canvasx range.
856 var r
= this.toDataCoords(lowX
, null);
858 r
= this.toDataCoords(highX
, null);
861 this.dateWindow_
= [minDate
, maxDate
];
862 this.drawGraph_(this.rawData_
);
863 if (this.attr_("zoomCallback")) {
864 this.attr_("zoomCallback")(minDate
, maxDate
);
869 * When the mouse moves in the canvas, display information about a nearby data
870 * point and draw dots over those points in the data series. This function
871 * takes care of cleanup of previously-drawn dots.
872 * @param {Object} event The mousemove event from the browser.
875 Dygraph
.prototype.mouseMove_
= function(event
) {
876 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
877 var points
= this.layout_
.points
;
882 // Loop through all the points and find the date nearest to our current
884 var minDist
= 1e+100;
886 for (var i
= 0; i
< points
.length
; i
++) {
887 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
888 if (dist
> minDist
) break;
892 if (idx
>= 0) lastx
= points
[idx
].xval
;
893 // Check that you can really highlight the last day's data
894 if (canvasx
> points
[points
.length
-1].canvasx
)
895 lastx
= points
[points
.length
-1].xval
;
897 // Extract the points we've selected
898 this.selPoints_
= [];
899 for (var i
= 0; i
< points
.length
; i
++) {
900 if (points
[i
].xval
== lastx
) {
901 this.selPoints_
.push(points
[i
]);
905 if (this.attr_("highlightCallback")) {
906 var px
= this.lastHighlightCallbackX
;
907 if (px
!== null && lastx
!= px
) {
908 // only fire if the selected point has changed.
909 this.lastHighlightCallbackX
= lastx
;
910 if (!this.attr_("stackedGraph")) {
911 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
913 // "unstack" the points.
914 var callbackPoints
= this.selPoints_
.map(
915 function(p
) { return {xval
: p
.xval
, yval
: p
.yval
, name
: p
.name
} });
916 var cumulative_sum
= 0;
917 for (var j
= callbackPoints
.length
- 1; j
>= 0; j
--) {
918 callbackPoints
[j
].yval
-= cumulative_sum
;
919 cumulative_sum
+= callbackPoints
[j
].yval
;
921 this.attr_("highlightCallback")(event
, lastx
, callbackPoints
);
926 // Save last x position for callbacks.
929 this.updateSelection_();
933 * Draw dots over the selectied points in the data series. This function
934 * takes care of cleanup of previously-drawn dots.
937 Dygraph
.prototype.updateSelection_
= function() {
938 // Clear the previously drawn vertical, if there is one
939 var circleSize
= this.attr_('highlightCircleSize');
940 var ctx
= this.canvas_
.getContext("2d");
941 if (this.previousVerticalX_
>= 0) {
942 var px
= this.previousVerticalX_
;
943 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
946 var isOK
= function(x
) { return x
&& !isNaN(x
); };
948 if (this.selPoints_
.length
> 0) {
949 var canvasx
= this.selPoints_
[0].canvasx
;
951 // Set the status message to indicate the selected point(s)
952 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
953 var clen
= this.colors_
.length
;
954 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
955 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
956 if (this.attr_("labelsSeparateLines")) {
959 var point
= this.selPoints_
[i
];
960 var c
= new RGBColor(this.colors_
[i
%clen
]);
961 replace
+= " <b><font color='" + c
.toHex() + "'>"
962 + point
.name
+ "</font></b>:"
963 + this.round_(point
.yval
, 2);
965 this.attr_("labelsDiv").innerHTML
= replace
;
967 // Draw colored circles over the center of each selected point
969 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
970 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
972 ctx
.fillStyle
= this.colors_
[i
%clen
];
973 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
974 0, 2 * Math
.PI
, false);
979 this.previousVerticalX_
= canvasx
;
984 * Set manually set selected dots, and display information about them
985 * @param int row number that should by highlighted
986 * false value clears the selection
989 Dygraph
.prototype.setSelection
= function(row
) {
990 // Extract the points we've selected
991 this.selPoints_
= [];
995 row
= row
-this.boundaryIds_
[0][0];
998 if (row
!== false && row
>= 0) {
999 for (var i
in this.layout_
.datasets
) {
1000 if (row
< this.layout_
.datasets
[i
].length
) {
1001 this.selPoints_
.push(this.layout_
.points
[pos
+row
]);
1003 pos
+= this.layout_
.datasets
[i
].length
;
1007 if (this.selPoints_
.length
) {
1008 this.lastx_
= this.selPoints_
[0].xval
;
1009 this.updateSelection_();
1012 this.clearSelection();
1018 * The mouse has left the canvas. Clear out whatever artifacts remain
1019 * @param {Object} event the mouseout event from the browser.
1022 Dygraph
.prototype.mouseOut_
= function(event
) {
1023 if (this.attr_("hideOverlayOnMouseOut")) {
1024 this.clearSelection();
1029 * Remove all selection from the canvas
1032 Dygraph
.prototype.clearSelection
= function() {
1033 // Get rid of the overlay data
1034 var ctx
= this.canvas_
.getContext("2d");
1035 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1036 this.attr_("labelsDiv").innerHTML
= "";
1037 this.selPoints_
= [];
1042 * Returns the number of the currently selected row
1043 * @return int row number, of -1 if nothing is selected
1046 Dygraph
.prototype.getSelection
= function() {
1047 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1051 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1052 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1053 return row
+ this.boundaryIds_
[0][0];
1059 Dygraph
.zeropad
= function(x
) {
1060 if (x
< 10) return "0" + x
; else return "" + x
;
1064 * Return a string version of the hours, minutes and seconds portion of a date.
1065 * @param {Number} date The JavaScript date (ms since epoch)
1066 * @return {String} A time of the form "HH:MM:SS"
1069 Dygraph
.prototype.hmsString_
= function(date
) {
1070 var zeropad
= Dygraph
.zeropad
;
1071 var d
= new Date(date
);
1072 if (d
.getSeconds()) {
1073 return zeropad(d
.getHours()) + ":" +
1074 zeropad(d
.getMinutes()) + ":" +
1075 zeropad(d
.getSeconds());
1077 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1082 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1083 * @param {Number} date The JavaScript date (ms since epoch)
1084 * @return {String} A date of the form "YYYY/MM/DD"
1086 * TODO(danvk): why is this part of the prototype?
1088 Dygraph
.dateString_
= function(date
, self
) {
1089 var zeropad
= Dygraph
.zeropad
;
1090 var d
= new Date(date
);
1093 var year
= "" + d
.getFullYear();
1094 // Get a 0 padded month string
1095 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1096 // Get a 0 padded day string
1097 var day
= zeropad(d
.getDate());
1100 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1101 if (frac
) ret
= " " + self
.hmsString_(date
);
1103 return year
+ "/" + month + "/" + day
+ ret
;
1107 * Round a number to the specified number of digits past the decimal point.
1108 * @param {Number} num The number to round
1109 * @param {Number} places The number of decimals to which to round
1110 * @return {Number} The rounded number
1113 Dygraph
.prototype.round_
= function(num
, places
) {
1114 var shift
= Math
.pow(10, places
);
1115 return Math
.round(num
* shift
)/shift
;
1119 * Fires when there's data available to be graphed.
1120 * @param {String} data Raw CSV data to be plotted
1123 Dygraph
.prototype.loadedEvent_
= function(data
) {
1124 this.rawData_
= this.parseCSV_(data
);
1125 this.drawGraph_(this.rawData_
);
1128 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1129 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1130 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1133 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1136 Dygraph
.prototype.addXTicks_
= function() {
1137 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1138 var startDate
, endDate
;
1139 if (this.dateWindow_
) {
1140 startDate
= this.dateWindow_
[0];
1141 endDate
= this.dateWindow_
[1];
1143 startDate
= this.rawData_
[0][0];
1144 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1147 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1148 this.layout_
.updateOptions({xTicks
: xTicks
});
1151 // Time granularity enumeration
1152 Dygraph
.SECONDLY
= 0;
1153 Dygraph
.TWO_SECONDLY
= 1;
1154 Dygraph
.FIVE_SECONDLY
= 2;
1155 Dygraph
.TEN_SECONDLY
= 3;
1156 Dygraph
.THIRTY_SECONDLY
= 4;
1157 Dygraph
.MINUTELY
= 5;
1158 Dygraph
.TWO_MINUTELY
= 6;
1159 Dygraph
.FIVE_MINUTELY
= 7;
1160 Dygraph
.TEN_MINUTELY
= 8;
1161 Dygraph
.THIRTY_MINUTELY
= 9;
1162 Dygraph
.HOURLY
= 10;
1163 Dygraph
.TWO_HOURLY
= 11;
1164 Dygraph
.SIX_HOURLY
= 12;
1166 Dygraph
.WEEKLY
= 14;
1167 Dygraph
.MONTHLY
= 15;
1168 Dygraph
.QUARTERLY
= 16;
1169 Dygraph
.BIANNUAL
= 17;
1170 Dygraph
.ANNUAL
= 18;
1171 Dygraph
.DECADAL
= 19;
1172 Dygraph
.NUM_GRANULARITIES
= 20;
1174 Dygraph
.SHORT_SPACINGS
= [];
1175 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1176 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1177 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1178 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1179 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1180 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1181 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1182 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1183 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1184 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1185 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1186 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1187 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1188 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1189 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1193 // If we used this time granularity, how many ticks would there be?
1194 // This is only an approximation, but it's generally good enough.
1196 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1197 if (granularity
< Dygraph
.MONTHLY
) {
1198 // Generate one tick mark for every fixed interval of time.
1199 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1200 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1202 var year_mod
= 1; // e.g. to only print one point every 10 years.
1203 var num_months
= 12;
1204 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1205 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1206 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1207 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1209 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1210 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1211 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1217 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1218 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1220 // Returns an array containing {v: millis, label: label} dictionaries.
1222 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1224 if (granularity
< Dygraph
.MONTHLY
) {
1225 // Generate one tick mark for every fixed interval of time.
1226 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1227 var format
= '%d%b'; // e.g. "1Jan"
1229 // Find a time less than start_time which occurs on a "nice" time boundary
1230 // for this granularity.
1231 var g
= spacing
/ 1000;
1232 var d
= new Date(start_time
);
1233 if (g
<= 60) { // seconds
1234 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1238 if (g
<= 60) { // minutes
1239 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1244 if (g
<= 24) { // days
1245 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1250 if (g
== 7) { // one week
1251 d
.setDate(d
.getDate() - d
.getDay());
1256 start_time
= d
.getTime();
1258 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1259 var d
= new Date(t
);
1260 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1261 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1262 // the extra hour covers DST problems.
1263 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
1265 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
1269 // Display a tick mark on the first of a set of months of each year.
1270 // Years get a tick mark iff y % year_mod == 0. This is useful for
1271 // displaying a tick mark once every 10 years, say, on long time scales.
1273 var year_mod
= 1; // e.g. to only print one point every 10 years.
1275 if (granularity
== Dygraph
.MONTHLY
) {
1276 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1277 } else if (granularity
== Dygraph
.QUARTERLY
) {
1278 months
= [ 0, 3, 6, 9 ];
1279 } else if (granularity
== Dygraph
.BIANNUAL
) {
1281 } else if (granularity
== Dygraph
.ANNUAL
) {
1283 } else if (granularity
== Dygraph
.DECADAL
) {
1288 var start_year
= new Date(start_time
).getFullYear();
1289 var end_year
= new Date(end_time
).getFullYear();
1290 var zeropad
= Dygraph
.zeropad
;
1291 for (var i
= start_year
; i
<= end_year
; i
++) {
1292 if (i
% year_mod
!= 0) continue;
1293 for (var j
= 0; j
< months
.length
; j
++) {
1294 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1295 var t
= Date
.parse(date_str
);
1296 if (t
< start_time
|| t
> end_time
) continue;
1297 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
1307 * Add ticks to the x-axis based on a date range.
1308 * @param {Number} startDate Start of the date window (millis since epoch)
1309 * @param {Number} endDate End of the date window (millis since epoch)
1310 * @return {Array.<Object>} Array of {label, value} tuples.
1313 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1315 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1316 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1317 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1324 return self
.GetXAxis(startDate
, endDate
, chosen
);
1326 // TODO(danvk): signal error.
1331 * Add ticks when the x axis has numbers on it (instead of dates)
1332 * @param {Number} startDate Start of the date window (millis since epoch)
1333 * @param {Number} endDate End of the date window (millis since epoch)
1334 * @return {Array.<Object>} Array of {label, value} tuples.
1337 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1339 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1340 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1341 // The first spacing greater than pixelsPerYLabel is what we use.
1342 // TODO(danvk): version that works on a log scale.
1343 if (self
.attr_("labelsKMG2")) {
1344 var mults
= [1, 2, 4, 8];
1346 var mults
= [1, 2, 5];
1348 var scale
, low_val
, high_val
, nTicks
;
1349 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1350 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1351 for (var i
= -10; i
< 50; i
++) {
1352 if (self
.attr_("labelsKMG2")) {
1353 var base_scale
= Math
.pow(16, i
);
1355 var base_scale
= Math
.pow(10, i
);
1357 for (var j
= 0; j
< mults
.length
; j
++) {
1358 scale
= base_scale
* mults
[j
];
1359 low_val
= Math
.floor(minV
/ scale
) * scale
;
1360 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1361 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1362 var spacing
= self
.height_
/ nTicks
;
1363 // wish I could break out of both loops at once...
1364 if (spacing
> pixelsPerTick
) break;
1366 if (spacing
> pixelsPerTick
) break;
1369 // Construct labels for the ticks
1373 if (self
.attr_("labelsKMB")) {
1375 k_labels
= [ "K", "M", "B", "T" ];
1377 if (self
.attr_("labelsKMG2")) {
1378 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1380 k_labels
= [ "k", "M", "G", "T" ];
1383 // Allow reverse y-axis if it's explicitly requested.
1384 if (low_val
> high_val
) scale
*= -1;
1386 for (var i
= 0; i
< nTicks
; i
++) {
1387 var tickV
= low_val
+ i
* scale
;
1388 var absTickV
= Math
.abs(tickV
);
1389 var label
= self
.round_(tickV
, 2);
1390 if (k_labels
.length
) {
1391 // Round up to an appropriate unit.
1393 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1394 if (absTickV
>= n
) {
1395 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1400 ticks
.push( {label
: label
, v
: tickV
} );
1406 * Adds appropriate ticks on the y-axis
1407 * @param {Number} minY The minimum Y value in the data set
1408 * @param {Number} maxY The maximum Y value in the data set
1411 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1412 // Set the number of ticks so that the labels are human-friendly.
1413 // TODO(danvk): make this an attribute as well.
1414 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1415 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1419 // Computes the range of the data series (including confidence intervals).
1420 // series is either [ [x1, y1], [x2, y2], ... ] or
1421 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1422 // Returns [low, high]
1423 Dygraph
.prototype.extremeValues_
= function(series
) {
1424 var minY
= null, maxY
= null;
1426 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1428 // With custom bars, maxY is the max of the high values.
1429 for (var j
= 0; j
< series
.length
; j
++) {
1430 var y
= series
[j
][1][0];
1432 var low
= y
- series
[j
][1][1];
1433 var high
= y
+ series
[j
][1][2];
1434 if (low
> y
) low
= y
; // this can happen with custom bars,
1435 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1436 if (maxY
== null || high
> maxY
) {
1439 if (minY
== null || low
< minY
) {
1444 for (var j
= 0; j
< series
.length
; j
++) {
1445 var y
= series
[j
][1];
1446 if (y
=== null || isNaN(y
)) continue;
1447 if (maxY
== null || y
> maxY
) {
1450 if (minY
== null || y
< minY
) {
1456 return [minY
, maxY
];
1460 * Update the graph with new data. Data is in the format
1461 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1462 * or, if errorBars=true,
1463 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1464 * @param {Array.<Object>} data The data (see above)
1467 Dygraph
.prototype.drawGraph_
= function(data
) {
1468 // This is used to set the second parameter to drawCallback, below.
1469 var is_initial_draw
= this.is_initial_draw_
;
1470 this.is_initial_draw_
= false;
1472 var minY
= null, maxY
= null;
1473 this.layout_
.removeAllDatasets();
1475 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1477 // For stacked series.
1478 var cumulative_y
= [];
1479 var stacked_datasets
= [];
1481 // Loop over all fields in the dataset
1482 for (var i
= 1; i
< data
[0].length
; i
++) {
1483 if (!this.visibility()[i
- 1]) continue;
1486 for (var j
= 0; j
< data
.length
; j
++) {
1487 var date
= data
[j
][0];
1488 series
[j
] = [date
, data
[j
][i
]];
1490 series
= this.rollingAverage(series
, this.rollPeriod_
);
1492 // Prune down to the desired range, if necessary (for zooming)
1493 // Because there can be lines going to points outside of the visible area,
1494 // we actually prune to visible points, plus one on either side.
1495 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1496 if (this.dateWindow_
) {
1497 var low
= this.dateWindow_
[0];
1498 var high
= this.dateWindow_
[1];
1500 // TODO(danvk): do binary search instead of linear search.
1501 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1502 var firstIdx
= null, lastIdx
= null;
1503 for (var k
= 0; k
< series
.length
; k
++) {
1504 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1507 if (series
[k
][0] <= high
) {
1511 if (firstIdx
=== null) firstIdx
= 0;
1512 if (firstIdx
> 0) firstIdx
--;
1513 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1514 if (lastIdx
< series
.length
- 1) lastIdx
++;
1515 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1516 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1517 pruned
.push(series
[k
]);
1521 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1524 var extremes
= this.extremeValues_(series
);
1525 var thisMinY
= extremes
[0];
1526 var thisMaxY
= extremes
[1];
1527 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1528 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1532 for (var j
=0; j
<series
.length
; j
++)
1533 vals
[j
] = [series
[j
][0],
1534 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1535 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1536 } else if (this.attr_("stackedGraph")) {
1538 var l
= series
.length
;
1540 for (var j
= 0; j
< l
; j
++) {
1541 if (cumulative_y
[series
[j
][0]] === undefined
)
1542 cumulative_y
[series
[j
][0]] = 0;
1544 actual_y
= series
[j
][1];
1545 cumulative_y
[series
[j
][0]] += actual_y
;
1547 vals
[j
] = [series
[j
][0], cumulative_y
[series
[j
][0]]]
1549 if (!maxY
|| cumulative_y
[series
[j
][0]] > maxY
)
1550 maxY
= cumulative_y
[series
[j
][0]];
1552 stacked_datasets
.push([this.attr_("labels")[i
], vals
]);
1553 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1555 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1559 if (stacked_datasets
.length
> 0) {
1560 for (var i
= (stacked_datasets
.length
- 1); i
>= 0; i
--) {
1561 this.layout_
.addDataset(stacked_datasets
[i
][0], stacked_datasets
[i
][1]);
1565 // Use some heuristics to come up with a good maxY value, unless it's been
1566 // set explicitly by the user.
1567 if (this.valueRange_
!= null) {
1568 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1569 this.displayedYRange_
= this.valueRange_
;
1571 // This affects the calculation of span, below.
1572 if (this.attr_("includeZero") && minY
> 0) {
1576 // Add some padding and round up to an integer to be human-friendly.
1577 var span
= maxY
- minY
;
1578 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1579 if (span
== 0) { span
= maxY
; }
1580 var maxAxisY
= maxY
+ 0.1 * span
;
1581 var minAxisY
= minY
- 0.1 * span
;
1583 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1584 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1585 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1587 if (this.attr_("includeZero")) {
1588 if (maxY
< 0) maxAxisY
= 0;
1589 if (minY
> 0) minAxisY
= 0;
1592 this.addYTicks_(minAxisY
, maxAxisY
);
1593 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1598 // Tell PlotKit to use this new data and render itself
1599 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1600 this.layout_
.evaluateWithError();
1601 this.plotter_
.clear();
1602 this.plotter_
.render();
1603 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1604 this.canvas_
.height
);
1606 if (this.attr_("drawCallback") !== null) {
1607 this.attr_("drawCallback")(this, is_initial_draw
);
1612 * Calculates the rolling average of a data set.
1613 * If originalData is [label, val], rolls the average of those.
1614 * If originalData is [label, [, it's interpreted as [value, stddev]
1615 * and the roll is returned in the same form, with appropriately reduced
1616 * stddev for each value.
1617 * Note that this is where fractional input (i.e. '5/10') is converted into
1619 * @param {Array} originalData The data in the appropriate format (see above)
1620 * @param {Number} rollPeriod The number of days over which to average the data
1622 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1623 if (originalData
.length
< 2)
1624 return originalData
;
1625 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1626 var rollingData
= [];
1627 var sigma
= this.attr_("sigma");
1629 if (this.fractions_
) {
1631 var den
= 0; // numerator/denominator
1633 for (var i
= 0; i
< originalData
.length
; i
++) {
1634 num
+= originalData
[i
][1][0];
1635 den
+= originalData
[i
][1][1];
1636 if (i
- rollPeriod
>= 0) {
1637 num
-= originalData
[i
- rollPeriod
][1][0];
1638 den
-= originalData
[i
- rollPeriod
][1][1];
1641 var date
= originalData
[i
][0];
1642 var value
= den
? num
/ den
: 0.0;
1643 if (this.attr_("errorBars")) {
1644 if (this.wilsonInterval_
) {
1645 // For more details on this confidence interval, see:
1646 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1648 var p
= value
< 0 ? 0 : value
, n
= den
;
1649 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1650 var denom
= 1 + sigma
* sigma
/ den
;
1651 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1652 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1653 rollingData
[i
] = [date
,
1654 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1656 rollingData
[i
] = [date
, [0, 0, 0]];
1659 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1660 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1663 rollingData
[i
] = [date
, mult
* value
];
1666 } else if (this.attr_("customBars")) {
1671 for (var i
= 0; i
< originalData
.length
; i
++) {
1672 var data
= originalData
[i
][1];
1674 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1676 if (y
!= null && !isNaN(y
)) {
1682 if (i
- rollPeriod
>= 0) {
1683 var prev
= originalData
[i
- rollPeriod
];
1684 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1691 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1692 1.0 * (mid
- low
) / count
,
1693 1.0 * (high
- mid
) / count
]];
1696 // Calculate the rolling average for the first rollPeriod - 1 points where
1697 // there is not enough data to roll over the full number of days
1698 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1699 if (!this.attr_("errorBars")){
1700 if (rollPeriod
== 1) {
1701 return originalData
;
1704 for (var i
= 0; i
< originalData
.length
; i
++) {
1707 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1708 var y
= originalData
[j
][1];
1709 if (y
== null || isNaN(y
)) continue;
1711 sum
+= originalData
[j
][1];
1714 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1716 rollingData
[i
] = [originalData
[i
][0], null];
1721 for (var i
= 0; i
< originalData
.length
; i
++) {
1725 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1726 var y
= originalData
[j
][1][0];
1727 if (y
== null || isNaN(y
)) continue;
1729 sum
+= originalData
[j
][1][0];
1730 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1733 var stddev
= Math
.sqrt(variance
) / num_ok
;
1734 rollingData
[i
] = [originalData
[i
][0],
1735 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1737 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1747 * Parses a date, returning the number of milliseconds since epoch. This can be
1748 * passed in as an xValueParser in the Dygraph constructor.
1749 * TODO(danvk): enumerate formats that this understands.
1750 * @param {String} A date in YYYYMMDD format.
1751 * @return {Number} Milliseconds since epoch.
1754 Dygraph
.dateParser
= function(dateStr
, self
) {
1757 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1758 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1759 while (dateStrSlashed
.search("-") != -1) {
1760 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1762 d
= Date
.parse(dateStrSlashed
);
1763 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1764 // TODO(danvk): remove support for this format. It's confusing.
1765 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1766 + "/" + dateStr
.substr(6,2);
1767 d
= Date
.parse(dateStrSlashed
);
1769 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1770 // "2009/07/12 12:34:56"
1771 d
= Date
.parse(dateStr
);
1774 if (!d
|| isNaN(d
)) {
1775 self
.error("Couldn't parse " + dateStr
+ " as a date");
1781 * Detects the type of the str (date or numeric) and sets the various
1782 * formatting attributes in this.attrs_ based on this type.
1783 * @param {String} str An x value.
1786 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1788 if (str
.indexOf('-') >= 0 ||
1789 str
.indexOf('/') >= 0 ||
1790 isNaN(parseFloat(str
))) {
1792 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1793 // TODO(danvk): remove support for this format.
1798 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1799 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1800 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1802 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1803 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1804 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1809 * Parses a string in a special csv format. We expect a csv file where each
1810 * line is a date point, and the first field in each line is the date string.
1811 * We also expect that all remaining fields represent series.
1812 * if the errorBars attribute is set, then interpret the fields as:
1813 * date, series1, stddev1, series2, stddev2, ...
1814 * @param {Array.<Object>} data See above.
1817 * @return Array.<Object> An array with one entry for each row. These entries
1818 * are an array of cells in that row. The first entry is the parsed x-value for
1819 * the row. The second, third, etc. are the y-values. These can take on one of
1820 * three forms, depending on the CSV and constructor parameters:
1822 * 2. [ value, stddev ]
1823 * 3. [ low value, center value, high value ]
1825 Dygraph
.prototype.parseCSV_
= function(data
) {
1827 var lines
= data
.split("\n");
1829 // Use the default delimiter or fall back to a tab if that makes sense.
1830 var delim
= this.attr_('delimiter');
1831 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1836 if (this.labelsFromCSV_
) {
1838 this.attrs_
.labels
= lines
[0].split(delim
);
1842 var defaultParserSet
= false; // attempt to auto-detect x value type
1843 var expectedCols
= this.attr_("labels").length
;
1844 var outOfOrder
= false;
1845 for (var i
= start
; i
< lines
.length
; i
++) {
1846 var line
= lines
[i
];
1847 if (line
.length
== 0) continue; // skip blank lines
1848 if (line
[0] == '#') continue; // skip comment lines
1849 var inFields
= line
.split(delim
);
1850 if (inFields
.length
< 2) continue;
1853 if (!defaultParserSet
) {
1854 this.detectTypeFromString_(inFields
[0]);
1855 xParser
= this.attr_("xValueParser");
1856 defaultParserSet
= true;
1858 fields
[0] = xParser(inFields
[0], this);
1860 // If fractions are expected, parse the numbers as "A/B
"
1861 if (this.fractions_) {
1862 for (var j = 1; j < inFields.length; j++) {
1863 // TODO(danvk): figure out an appropriate way to flag parse errors.
1864 var vals = inFields[j].split("/");
1865 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1867 } else if (this.attr_("errorBars
")) {
1868 // If there are error bars, values are (value, stddev) pairs
1869 for (var j = 1; j < inFields.length; j += 2)
1870 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1871 parseFloat(inFields[j + 1])];
1872 } else if (this.attr_("customBars
")) {
1873 // Bars are a low;center;high tuple
1874 for (var j = 1; j < inFields.length; j++) {
1875 var vals = inFields[j].split(";");
1876 fields[j] = [ parseFloat(vals[0]),
1877 parseFloat(vals[1]),
1878 parseFloat(vals[2]) ];
1881 // Values are just numbers
1882 for (var j = 1; j < inFields.length; j++) {
1883 fields[j] = parseFloat(inFields[j]);
1886 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1891 if (fields.length != expectedCols) {
1892 this.error("Number of columns
in line
" + i + " (" + fields.length +
1893 ") does not agree
with number of
labels (" + expectedCols +
1899 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1900 ret.sort(function(a,b) { return a[0] - b[0] });
1907 * The user has provided their data as a pre-packaged JS array. If the x values
1908 * are numeric, this is the same as dygraphs' internal format. If the x values
1909 * are dates, we need to convert them from Date objects to ms since epoch.
1910 * @param {Array.<Object>} data
1911 * @return {Array.<Object>} data with numeric x values.
1913 Dygraph.prototype.parseArray_ = function(data) {
1914 // Peek at the first x value to see if it's numeric.
1915 if (data.length == 0) {
1916 this.error("Can
't plot empty data set");
1919 if (data[0].length == 0) {
1920 this.error("Data set cannot contain an empty row");
1924 if (this.attr_("labels") == null) {
1925 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1926 "in the options parameter");
1927 this.attrs_.labels = [ "X" ];
1928 for (var i = 1; i < data[0].length; i++) {
1929 this.attrs_.labels.push("Y" + i);
1933 if (Dygraph.isDateLike(data[0][0])) {
1934 // Some intelligent defaults for a date x-axis.
1935 this.attrs_.xValueFormatter = Dygraph.dateString_;
1936 this.attrs_.xTicker = Dygraph.dateTicker;
1938 // Assume they're all dates
.
1939 var parsedData
= Dygraph
.clone(data
);
1940 for (var i
= 0; i
< data
.length
; i
++) {
1941 if (parsedData
[i
].length
== 0) {
1942 this.error("Row " << (1 + i
) << " of data is empty");
1945 if (parsedData
[i
][0] == null
1946 || typeof(parsedData
[i
][0].getTime
) != 'function'
1947 || isNaN(parsedData
[i
][0].getTime())) {
1948 this.error("x value in row " + (1 + i
) + " is not a Date");
1951 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1955 // Some intelligent defaults for a numeric x-axis.
1956 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1957 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1963 * Parses a DataTable object from gviz.
1964 * The data is expected to have a first column that is either a date or a
1965 * number. All subsequent columns must be numbers. If there is a clear mismatch
1966 * between this.xValueParser_ and the type of the first column, it will be
1967 * fixed. Returned value is in the same format as return value of parseCSV_.
1968 * @param {Array.<Object>} data See above.
1971 Dygraph
.prototype.parseDataTable_
= function(data
) {
1972 var cols
= data
.getNumberOfColumns();
1973 var rows
= data
.getNumberOfRows();
1975 // Read column labels
1977 for (var i
= 0; i
< cols
; i
++) {
1978 labels
.push(data
.getColumnLabel(i
));
1979 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1981 this.attrs_
.labels
= labels
;
1982 cols
= labels
.length
;
1984 var indepType
= data
.getColumnType(0);
1985 if (indepType
== 'date' || indepType
== 'datetime') {
1986 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1987 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1988 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1989 } else if (indepType
== 'number') {
1990 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1991 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1992 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1994 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1995 "column 1 of DataTable input (Got '" + indepType
+ "')");
2000 var outOfOrder
= false;
2001 for (var i
= 0; i
< rows
; i
++) {
2003 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2004 data
.getValue(i
, 0) === null) {
2005 this.warning("Ignoring row " + i
+
2006 " of DataTable because of undefined or null first column.");
2010 if (indepType
== 'date' || indepType
== 'datetime') {
2011 row
.push(data
.getValue(i
, 0).getTime());
2013 row
.push(data
.getValue(i
, 0));
2015 if (!this.attr_("errorBars")) {
2016 for (var j
= 1; j
< cols
; j
++) {
2017 row
.push(data
.getValue(i
, j
));
2020 for (var j
= 0; j
< cols
- 1; j
++) {
2021 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2024 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2031 this.warn("DataTable is out of order; order it correctly to speed loading.");
2032 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2037 // These functions are all based on MochiKit.
2038 Dygraph
.update
= function (self
, o
) {
2039 if (typeof(o
) != 'undefined' && o
!== null) {
2041 if (o
.hasOwnProperty(k
)) {
2049 Dygraph
.isArrayLike
= function (o
) {
2050 var typ
= typeof(o
);
2052 (typ
!= 'object' && !(typ
== 'function' &&
2053 typeof(o
.item
) == 'function')) ||
2055 typeof(o
.length
) != 'number' ||
2063 Dygraph
.isDateLike
= function (o
) {
2064 if (typeof(o
) != "object" || o
=== null ||
2065 typeof(o
.getTime
) != 'function') {
2071 Dygraph
.clone
= function(o
) {
2072 // TODO(danvk): figure out how MochiKit's version works
2074 for (var i
= 0; i
< o
.length
; i
++) {
2075 if (Dygraph
.isArrayLike(o
[i
])) {
2076 r
.push(Dygraph
.clone(o
[i
]));
2086 * Get the CSV data. If it's in a function, call that function. If it's in a
2087 * file, do an XMLHttpRequest to get it.
2090 Dygraph
.prototype.start_
= function() {
2091 if (typeof this.file_
== 'function') {
2092 // CSV string. Pretend we got it via XHR.
2093 this.loadedEvent_(this.file_());
2094 } else if (Dygraph
.isArrayLike(this.file_
)) {
2095 this.rawData_
= this.parseArray_(this.file_
);
2096 this.drawGraph_(this.rawData_
);
2097 } else if (typeof this.file_
== 'object' &&
2098 typeof this.file_
.getColumnRange
== 'function') {
2099 // must be a DataTable from gviz.
2100 this.rawData_
= this.parseDataTable_(this.file_
);
2101 this.drawGraph_(this.rawData_
);
2102 } else if (typeof this.file_
== 'string') {
2103 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2104 if (this.file_
.indexOf('\n') >= 0) {
2105 this.loadedEvent_(this.file_
);
2107 var req
= new XMLHttpRequest();
2109 req
.onreadystatechange
= function () {
2110 if (req
.readyState
== 4) {
2111 if (req
.status
== 200) {
2112 caller
.loadedEvent_(req
.responseText
);
2117 req
.open("GET", this.file_
, true);
2121 this.error("Unknown data format: " + (typeof this.file_
));
2126 * Changes various properties of the graph. These can include:
2128 * <li>file: changes the source data for the graph</li>
2129 * <li>errorBars: changes whether the data contains stddev</li>
2131 * @param {Object} attrs The new properties and values
2133 Dygraph
.prototype.updateOptions
= function(attrs
) {
2134 // TODO(danvk): this is a mess. Rethink this function.
2135 if (attrs
.rollPeriod
) {
2136 this.rollPeriod_
= attrs
.rollPeriod
;
2138 if (attrs
.dateWindow
) {
2139 this.dateWindow_
= attrs
.dateWindow
;
2141 if (attrs
.valueRange
) {
2142 this.valueRange_
= attrs
.valueRange
;
2144 Dygraph
.update(this.user_attrs_
, attrs
);
2146 this.labelsFromCSV_
= (this.attr_("labels") == null);
2148 // TODO(danvk): this doesn't match the constructor logic
2149 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2150 if (attrs
['file'] && attrs
['file'] != this.file_
) {
2151 this.file_
= attrs
['file'];
2154 this.drawGraph_(this.rawData_
);
2159 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2160 * containing div (which has presumably changed size since the dygraph was
2161 * instantiated. If the width/height are specified, the div will be resized.
2163 * This is far more efficient than destroying and re-instantiating a
2164 * Dygraph, since it doesn't have to reparse the underlying data.
2166 * @param {Number} width Width (in pixels)
2167 * @param {Number} height Height (in pixels)
2169 Dygraph
.prototype.resize
= function(width
, height
) {
2170 if ((width
=== null) != (height
=== null)) {
2171 this.warn("Dygraph.resize() should be called with zero parameters or " +
2172 "two non-NULL parameters. Pretending it was zero.");
2173 width
= height
= null;
2176 // TODO(danvk): there should be a clear() method.
2177 this.maindiv_
.innerHTML
= "";
2178 this.attrs_
.labelsDiv
= null;
2181 this.maindiv_
.style
.width
= width
+ "px";
2182 this.maindiv_
.style
.height
= height
+ "px";
2183 this.width_
= width
;
2184 this.height_
= height
;
2186 this.width_
= this.maindiv_
.offsetWidth
;
2187 this.height_
= this.maindiv_
.offsetHeight
;
2190 this.createInterface_();
2191 this.drawGraph_(this.rawData_
);
2195 * Adjusts the number of days in the rolling average. Updates the graph to
2196 * reflect the new averaging period.
2197 * @param {Number} length Number of days over which to average the data.
2199 Dygraph
.prototype.adjustRoll
= function(length
) {
2200 this.rollPeriod_
= length
;
2201 this.drawGraph_(this.rawData_
);
2205 * Returns a boolean array of visibility statuses.
2207 Dygraph
.prototype.visibility
= function() {
2208 // Do lazy-initialization, so that this happens after we know the number of
2210 if (!this.attr_("visibility")) {
2211 this.attrs_
["visibility"] = [];
2213 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2214 this.attr_("visibility").push(true);
2216 return this.attr_("visibility");
2220 * Changes the visiblity of a series.
2222 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2223 var x
= this.visibility();
2224 if (num
< 0 && num
>= x
.length
) {
2225 this.warn("invalid series number in setVisibility: " + num
);
2228 this.drawGraph_(this.rawData_
);
2233 * Create a new canvas element. This is more complex than a simple
2234 * document.createElement("canvas") because of IE and excanvas.
2236 Dygraph
.createCanvas
= function() {
2237 var canvas
= document
.createElement("canvas");
2239 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2241 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2249 * A wrapper around Dygraph that implements the gviz API.
2250 * @param {Object} container The DOM object the visualization should live in.
2252 Dygraph
.GVizChart
= function(container
) {
2253 this.container
= container
;
2256 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2257 this.container
.innerHTML
= '';
2258 this.date_graph
= new Dygraph(this.container
, data
, options
);
2262 * Google charts compatible setSelection
2263 * Only row selection is supported, all points in the
2264 * row will be highlighted
2265 * @param {Array} array of the selected cells
2268 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2270 if (selection_array
.length
) {
2271 row
= selection_array
[0].row
;
2273 this.date_graph
.setSelection(row
);
2277 * Google charts compatible getSelection implementation
2278 * @return {Array} array of the selected cells
2281 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2284 var row
= this.date_graph
.getSelection();
2286 if (row
< 0) return selection
;
2289 for (var i
in this.date_graph
.layout_
.datasets
) {
2290 selection
.push({row
: row
, column
: col
});
2297 // Older pages may still use this name.
2298 DateGraph
= Dygraph
;