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 // Make a note of whether labels will be pulled from the CSV file.
213 this.labelsFromCSV_
= (this.attr_("labels") == null);
215 // Create the containing DIV and other interactive elements
216 this.createInterface_();
221 Dygraph
.prototype.attr_
= function(name
) {
222 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
223 return this.user_attrs_
[name
];
224 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
225 return this.attrs_
[name
];
231 // TODO(danvk): any way I can get the line numbers to be this.warn call?
232 Dygraph
.prototype.log
= function(severity
, message
) {
233 if (typeof(console
) != 'undefined') {
236 console
.debug('dygraphs: ' + message
);
239 console
.info('dygraphs: ' + message
);
241 case Dygraph
.WARNING
:
242 console
.warn('dygraphs: ' + message
);
245 console
.error('dygraphs: ' + message
);
250 Dygraph
.prototype.info
= function(message
) {
251 this.log(Dygraph
.INFO
, message
);
253 Dygraph
.prototype.warn
= function(message
) {
254 this.log(Dygraph
.WARNING
, message
);
256 Dygraph
.prototype.error
= function(message
) {
257 this.log(Dygraph
.ERROR
, message
);
261 * Returns the current rolling period, as set by the user or an option.
262 * @return {Number} The number of days in the rolling window
264 Dygraph
.prototype.rollPeriod
= function() {
265 return this.rollPeriod_
;
269 * Returns the currently-visible x-range. This can be affected by zooming,
270 * panning or a call to updateOptions.
271 * Returns a two-element array: [left, right].
272 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
274 Dygraph
.prototype.xAxisRange
= function() {
275 if (this.dateWindow_
) return this.dateWindow_
;
277 // The entire chart is visible.
278 var left
= this.rawData_
[0][0];
279 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
280 return [left
, right
];
284 * Returns the currently-visible y-range. This can be affected by zooming,
285 * panning or a call to updateOptions.
286 * Returns a two-element array: [bottom, top].
288 Dygraph
.prototype.yAxisRange
= function() {
289 return this.displayedYRange_
;
293 * Convert from data coordinates to canvas/div X/Y coordinates.
294 * Returns a two-element array: [X, Y]
296 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
297 var ret
= [null, null];
298 var area
= this.plotter_
.area
;
300 var xRange
= this.xAxisRange();
301 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
305 var yRange
= this.yAxisRange();
306 ret
[1] = area
.y
+ (yRange
[0] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
313 * Convert from canvas/div coords to data coordinates.
314 * Returns a two-element array: [X, Y]
316 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
317 var ret
= [null, null];
318 var area
= this.plotter_
.area
;
320 var xRange
= this.xAxisRange();
321 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
325 var yRange
= this.yAxisRange();
326 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
332 Dygraph
.addEvent
= function(el
, evt
, fn
) {
333 var normed_fn
= function(e
) {
334 if (!e
) var e
= window
.event
;
337 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
338 el
.addEventListener(evt
, normed_fn
, false);
340 el
.attachEvent('on' + evt
, normed_fn
);
344 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
345 var ctx
= cnv
.getContext("2d");
347 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
352 * Generates interface elements for the Dygraph: a containing div, a div to
353 * display the current point, and a textbox to adjust the rolling average
354 * period. Also creates the Renderer/Layout elements.
357 Dygraph
.prototype.createInterface_
= function() {
358 // Create the all-enclosing graph div
359 var enclosing
= this.maindiv_
;
361 this.graphDiv
= document
.createElement("div");
362 this.graphDiv
.style
.width
= this.width_
+ "px";
363 this.graphDiv
.style
.height
= this.height_
+ "px";
364 enclosing
.appendChild(this.graphDiv
);
368 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
370 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
371 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
372 - 2 * this.attr_("axisTickSize");
373 this.clippingArea_
= clip
;
375 // Create the canvas for interactive parts of the chart.
376 this.canvas_
= Dygraph
.createCanvas();
377 this.canvas_
.style
.position
= "absolute";
378 this.canvas_
.width
= this.width_
;
379 this.canvas_
.height
= this.height_
;
380 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
381 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
382 this.graphDiv
.appendChild(this.canvas_
);
384 // ... and for static parts of the chart.
385 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
387 // Make sure we don't overdraw.
388 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
389 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
392 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
393 dygraph
.mouseMove_(e
);
395 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
396 dygraph
.mouseOut_(e
);
399 // Create the grapher
400 // TODO(danvk): why does the Layout need its own set of options?
401 this.layoutOptions_
= { 'xOriginIsZero': false };
402 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
403 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
404 Dygraph
.update(this.layoutOptions_
, {
405 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
407 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
409 // TODO(danvk): why does the Renderer need its own set of options?
410 this.renderOptions_
= { colorScheme
: this.colors_
,
412 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
413 Dygraph
.update(this.renderOptions_
, this.attrs_
);
414 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
415 this.plotter_
= new DygraphCanvasRenderer(this,
416 this.hidden_
, this.layout_
,
417 this.renderOptions_
);
419 this.createStatusMessage_();
420 this.createRollInterface_();
421 this.createDragInterface_();
425 * Detach DOM elements in the dygraph and null out all data references.
426 * Calling this when you're done with a dygraph can dramatically reduce memory
427 * usage. See, e.g., the tests/perf.html example.
429 Dygraph
.prototype.destroy
= function() {
430 var removeRecursive
= function(node
) {
431 while (node
.hasChildNodes()) {
432 removeRecursive(node
.firstChild
);
433 node
.removeChild(node
.firstChild
);
436 removeRecursive(this.maindiv_
);
438 var nullOut
= function(obj
) {
440 if (typeof(obj
[n
]) === 'object') {
446 // These may not all be necessary, but it can't hurt...
447 nullOut(this.layout_
);
448 nullOut(this.plotter_
);
453 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
454 * this particular canvas. All Dygraph work is done on this.canvas_.
455 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
456 * @return {Object} The newly-created canvas
459 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
460 var h
= Dygraph
.createCanvas();
461 h
.style
.position
= "absolute";
462 // TODO(danvk): h should be offset from canvas. canvas needs to include
463 // some extra area to make it easier to zoom in on the far left and far
464 // right. h needs to be precisely the plot area, so that clipping occurs.
465 h
.style
.top
= canvas
.style
.top
;
466 h
.style
.left
= canvas
.style
.left
;
467 h
.width
= this.width_
;
468 h
.height
= this.height_
;
469 h
.style
.width
= this.width_
+ "px"; // for IE
470 h
.style
.height
= this.height_
+ "px"; // for IE
471 this.graphDiv
.appendChild(h
);
475 // Taken from MochiKit.Color
476 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
480 if (saturation
=== 0) {
485 var i
= Math
.floor(hue
* 6);
486 var f
= (hue
* 6) - i
;
487 var p
= value
* (1 - saturation
);
488 var q
= value
* (1 - (saturation
* f
));
489 var t
= value
* (1 - (saturation
* (1 - f
)));
491 case 1: red
= q
; green
= value
; blue
= p
; break;
492 case 2: red
= p
; green
= value
; blue
= t
; break;
493 case 3: red
= p
; green
= q
; blue
= value
; break;
494 case 4: red
= t
; green
= p
; blue
= value
; break;
495 case 5: red
= value
; green
= p
; blue
= q
; break;
496 case 6: // fall through
497 case 0: red
= value
; green
= t
; blue
= p
; break;
500 red
= Math
.floor(255 * red
+ 0.5);
501 green
= Math
.floor(255 * green
+ 0.5);
502 blue
= Math
.floor(255 * blue
+ 0.5);
503 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
508 * Generate a set of distinct colors for the data series. This is done with a
509 * color wheel. Saturation/Value are customizable, and the hue is
510 * equally-spaced around the color wheel. If a custom set of colors is
511 * specified, that is used instead.
514 Dygraph
.prototype.setColors_
= function() {
515 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
516 // away with this.renderOptions_.
517 var num
= this.attr_("labels").length
- 1;
519 var colors
= this.attr_('colors');
521 var sat
= this.attr_('colorSaturation') || 1.0;
522 var val
= this.attr_('colorValue') || 0.5;
523 for (var i
= 1; i
<= num
; i
++) {
524 if (!this.visibility()[i
-1]) continue;
525 // alternate colors for high contrast.
526 var idx
= i
- parseInt(i
% 2 ? i
/ 2 : (i - num)/2, 10);
527 var hue
= (1.0 * idx
/ (1 + num
));
528 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
531 for (var i
= 0; i
< num
; i
++) {
532 if (!this.visibility()[i
]) continue;
533 var colorStr
= colors
[i
% colors
.length
];
534 this.colors_
.push(colorStr
);
538 // TODO(danvk): update this w/r
/t/ the
new options system
.
539 this.renderOptions_
.colorScheme
= this.colors_
;
540 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
541 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
542 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
546 * Return the list of colors. This is either the list of colors passed in the
547 * attributes, or the autogenerated list of rgb(r,g,b) strings.
548 * @return {Array<string>} The list of colors.
550 Dygraph
.prototype.getColors
= function() {
554 // The following functions are from quirksmode.org with a modification for Safari from
555 // http://blog.firetree.net/2005/07/04/javascript-find-position/
556 // http://www.quirksmode.org/js
/findpos
.html
557 Dygraph
.findPosX
= function(obj
) {
562 curleft
+= obj
.offsetLeft
;
563 if(!obj
.offsetParent
)
565 obj
= obj
.offsetParent
;
572 Dygraph
.findPosY
= function(obj
) {
577 curtop
+= obj
.offsetTop
;
578 if(!obj
.offsetParent
)
580 obj
= obj
.offsetParent
;
590 * Create the div that contains information on the selected point(s)
591 * This goes in the top right of the canvas, unless an external div has already
595 Dygraph
.prototype.createStatusMessage_
= function(){
596 if (!this.attr_("labelsDiv")) {
597 var divWidth
= this.attr_('labelsDivWidth');
599 "position": "absolute",
602 "width": divWidth
+ "px",
604 "left": (this.width_
- divWidth
- 2) + "px",
605 "background": "white",
607 "overflow": "hidden"};
608 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
609 var div
= document
.createElement("div");
610 for (var name
in messagestyle
) {
611 if (messagestyle
.hasOwnProperty(name
)) {
612 div
.style
[name
] = messagestyle
[name
];
615 this.graphDiv
.appendChild(div
);
616 this.attrs_
.labelsDiv
= div
;
621 * Create the text box to adjust the averaging period
622 * @return {Object} The newly-created text box
625 Dygraph
.prototype.createRollInterface_
= function() {
626 var display
= this.attr_('showRoller') ? "block" : "none";
627 var textAttr
= { "position": "absolute",
629 "top": (this.plotter_
.area
.h
- 25) + "px",
630 "left": (this.plotter_
.area
.x
+ 1) + "px",
633 var roller
= document
.createElement("input");
634 roller
.type
= "text";
636 roller
.value
= this.rollPeriod_
;
637 for (var name
in textAttr
) {
638 if (textAttr
.hasOwnProperty(name
)) {
639 roller
.style
[name
] = textAttr
[name
];
643 var pa
= this.graphDiv
;
644 pa
.appendChild(roller
);
646 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
650 // These functions are taken from MochiKit.Signal
651 Dygraph
.pageX
= function(e
) {
653 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
656 var b
= document
.body
;
658 (de
.scrollLeft
|| b
.scrollLeft
) -
659 (de
.clientLeft
|| 0);
663 Dygraph
.pageY
= function(e
) {
665 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
668 var b
= document
.body
;
670 (de
.scrollTop
|| b
.scrollTop
) -
676 * Set up all the mouse handlers needed to capture dragging behavior for zoom
680 Dygraph
.prototype.createDragInterface_
= function() {
683 // Tracks whether the mouse is down right now
684 var isZooming
= false;
685 var isPanning
= false;
686 var dragStartX
= null;
687 var dragStartY
= null;
691 var draggingDate
= null;
692 var dateRange
= null;
694 // Utility function to convert page-wide coordinates to canvas coords
697 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
698 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
700 // Draw zoom rectangles when the mouse is down and the user moves around
701 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
703 dragEndX
= getX(event
);
704 dragEndY
= getY(event
);
706 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
708 } else if (isPanning
) {
709 dragEndX
= getX(event
);
710 dragEndY
= getY(event
);
712 // Want to have it so that:
713 // 1. draggingDate appears at dragEndX
714 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
716 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
717 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
718 self
.drawGraph_(self
.rawData_
);
722 // Track the beginning of drag events
723 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
724 px
= Dygraph
.findPosX(self
.canvas_
);
725 py
= Dygraph
.findPosY(self
.canvas_
);
726 dragStartX
= getX(event
);
727 dragStartY
= getY(event
);
729 if (event
.altKey
|| event
.shiftKey
) {
730 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
732 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
733 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
740 // If the user releases the mouse button during a drag, but not over the
741 // canvas, then it doesn't count as a zooming action.
742 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
743 if (isZooming
|| isPanning
) {
756 // Temporarily cancel the dragging event when the mouse leaves the graph
757 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
764 // If the mouse is released on the canvas during a drag event, then it's a
765 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
766 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
769 dragEndX
= getX(event
);
770 dragEndY
= getY(event
);
771 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
772 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
774 if (regionWidth
< 2 && regionHeight
< 2 &&
775 self
.attr_('clickCallback') != null &&
776 self
.lastx_
!= undefined
) {
777 // TODO(danvk): pass along more info about the points.
778 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
781 if (regionWidth
>= 10) {
782 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
783 Math
.max(dragStartX
, dragEndX
));
785 self
.canvas_
.getContext("2d").clearRect(0, 0,
787 self
.canvas_
.height
);
801 // Double-clicking zooms back out
802 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
803 if (self
.dateWindow_
== null) return;
804 self
.dateWindow_
= null;
805 self
.drawGraph_(self
.rawData_
);
806 var minDate
= self
.rawData_
[0][0];
807 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
808 if (self
.attr_("zoomCallback")) {
809 self
.attr_("zoomCallback")(minDate
, maxDate
);
815 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
816 * up any previous zoom rectangles that were drawn. This could be optimized to
817 * avoid extra redrawing, but it's tricky to avoid interactions with the status
819 * @param {Number} startX The X position where the drag started, in canvas
821 * @param {Number} endX The current X position of the drag, in canvas coords.
822 * @param {Number} prevEndX The value of endX on the previous call to this
823 * function. Used to avoid excess redrawing
826 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
827 var ctx
= this.canvas_
.getContext("2d");
829 // Clean up from the previous rect if necessary
831 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
832 Math
.abs(startX
- prevEndX
), this.height_
);
835 // Draw a light-grey rectangle to show the new viewing area
836 if (endX
&& startX
) {
837 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
838 ctx
.fillRect(Math
.min(startX
, endX
), 0,
839 Math
.abs(endX
- startX
), this.height_
);
844 * Zoom to something containing [lowX, highX]. These are pixel coordinates
845 * in the canvas. The exact zoom window may be slightly larger if there are no
846 * data points near lowX or highX. This function redraws the graph.
847 * @param {Number} lowX The leftmost pixel value that should be visible.
848 * @param {Number} highX The rightmost pixel value that should be visible.
851 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
852 // Find the earliest and latest dates contained in this canvasx range.
853 var points
= this.layout_
.points
;
856 // Find the nearest [minDate, maxDate] that contains [lowX, highX]
857 for (var i
= 0; i
< points
.length
; i
++) {
858 var cx
= points
[i
].canvasx
;
859 var x
= points
[i
].xval
;
860 if (cx
< lowX
&& (minDate
== null || x
> minDate
)) minDate
= x
;
861 if (cx
> highX
&& (maxDate
== null || x
< maxDate
)) maxDate
= x
;
863 // Use the extremes if either is missing
864 if (minDate
== null) minDate
= points
[0].xval
;
865 if (maxDate
== null) maxDate
= points
[points
.length
-1].xval
;
867 this.dateWindow_
= [minDate
, maxDate
];
868 this.drawGraph_(this.rawData_
);
869 if (this.attr_("zoomCallback")) {
870 this.attr_("zoomCallback")(minDate
, maxDate
);
875 * When the mouse moves in the canvas, display information about a nearby data
876 * point and draw dots over those points in the data series. This function
877 * takes care of cleanup of previously-drawn dots.
878 * @param {Object} event The mousemove event from the browser.
881 Dygraph
.prototype.mouseMove_
= function(event
) {
882 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
883 var points
= this.layout_
.points
;
888 // Loop through all the points and find the date nearest to our current
890 var minDist
= 1e+100;
892 for (var i
= 0; i
< points
.length
; i
++) {
893 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
894 if (dist
> minDist
) break;
898 if (idx
>= 0) lastx
= points
[idx
].xval
;
899 // Check that you can really highlight the last day's data
900 if (canvasx
> points
[points
.length
-1].canvasx
)
901 lastx
= points
[points
.length
-1].xval
;
903 // Extract the points we've selected
904 this.selPoints_
= [];
905 for (var i
= 0; i
< points
.length
; i
++) {
906 if (points
[i
].xval
== lastx
) {
907 this.selPoints_
.push(points
[i
]);
911 if (this.attr_("highlightCallback")) {
912 var px
= this.lastHighlightCallbackX
;
913 if (px
!== null && lastx
!= px
) {
914 // only fire if the selected point has changed.
915 this.lastHighlightCallbackX
= lastx
;
916 if (!this.attr_("stackedGraph")) {
917 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
919 // "unstack" the points.
920 var callbackPoints
= this.selPoints_
.map(
921 function(p
) { return {xval
: p
.xval
, yval
: p
.yval
, name
: p
.name
} });
922 var cumulative_sum
= 0;
923 for (var j
= callbackPoints
.length
- 1; j
>= 0; j
--) {
924 callbackPoints
[j
].yval
-= cumulative_sum
;
925 cumulative_sum
+= callbackPoints
[j
].yval
;
927 this.attr_("highlightCallback")(event
, lastx
, callbackPoints
);
932 // Clear the previously drawn vertical, if there is one
933 var circleSize
= this.attr_('highlightCircleSize');
934 var ctx
= this.canvas_
.getContext("2d");
935 if (this.previousVerticalX_
>= 0) {
936 var px
= this.previousVerticalX_
;
937 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
940 var isOK
= function(x
) { return x
&& !isNaN(x
); };
942 if (this.selPoints_
.length
> 0) {
943 var canvasx
= this.selPoints_
[0].canvasx
;
945 // Set the status message to indicate the selected point(s)
946 var replace
= this.attr_('xValueFormatter')(lastx
, this) + ":";
947 var clen
= this.colors_
.length
;
948 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
949 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
950 if (this.attr_("labelsSeparateLines")) {
953 var point
= this.selPoints_
[i
];
954 var c
= new RGBColor(this.colors_
[i
%clen
]);
955 replace
+= " <b><font color='" + c
.toHex() + "'>"
956 + point
.name
+ "</font></b>:"
957 + this.round_(point
.yval
, 2);
959 this.attr_("labelsDiv").innerHTML
= replace
;
961 // Save last x position for callbacks.
964 // Draw colored circles over the center of each selected point
966 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
967 if (!isOK(this.selPoints_
[i
%clen
].canvasy
)) continue;
969 ctx
.fillStyle
= this.colors_
[i
%clen
];
970 ctx
.arc(canvasx
, this.selPoints_
[i
%clen
].canvasy
, circleSize
,
971 0, 2 * Math
.PI
, false);
976 this.previousVerticalX_
= canvasx
;
981 * The mouse has left the canvas. Clear out whatever artifacts remain
982 * @param {Object} event the mouseout event from the browser.
985 Dygraph
.prototype.mouseOut_
= function(event
) {
986 if (this.attr_("hideOverlayOnMouseOut")) {
987 // Get rid of the overlay data
988 var ctx
= this.canvas_
.getContext("2d");
989 ctx
.clearRect(0, 0, this.width_
, this.height_
);
990 this.attr_("labelsDiv").innerHTML
= "";
994 Dygraph
.zeropad
= function(x
) {
995 if (x
< 10) return "0" + x
; else return "" + x
;
999 * Return a string version of the hours, minutes and seconds portion of a date.
1000 * @param {Number} date The JavaScript date (ms since epoch)
1001 * @return {String} A time of the form "HH:MM:SS"
1004 Dygraph
.prototype.hmsString_
= function(date
) {
1005 var zeropad
= Dygraph
.zeropad
;
1006 var d
= new Date(date
);
1007 if (d
.getSeconds()) {
1008 return zeropad(d
.getHours()) + ":" +
1009 zeropad(d
.getMinutes()) + ":" +
1010 zeropad(d
.getSeconds());
1012 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1017 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1018 * @param {Number} date The JavaScript date (ms since epoch)
1019 * @return {String} A date of the form "YYYY/MM/DD"
1021 * TODO(danvk): why is this part of the prototype?
1023 Dygraph
.dateString_
= function(date
, self
) {
1024 var zeropad
= Dygraph
.zeropad
;
1025 var d
= new Date(date
);
1028 var year
= "" + d
.getFullYear();
1029 // Get a 0 padded month string
1030 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1031 // Get a 0 padded day string
1032 var day
= zeropad(d
.getDate());
1035 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1036 if (frac
) ret
= " " + self
.hmsString_(date
);
1038 return year
+ "/" + month + "/" + day
+ ret
;
1042 * Round a number to the specified number of digits past the decimal point.
1043 * @param {Number} num The number to round
1044 * @param {Number} places The number of decimals to which to round
1045 * @return {Number} The rounded number
1048 Dygraph
.prototype.round_
= function(num
, places
) {
1049 var shift
= Math
.pow(10, places
);
1050 return Math
.round(num
* shift
)/shift
;
1054 * Fires when there's data available to be graphed.
1055 * @param {String} data Raw CSV data to be plotted
1058 Dygraph
.prototype.loadedEvent_
= function(data
) {
1059 this.rawData_
= this.parseCSV_(data
);
1060 this.drawGraph_(this.rawData_
);
1063 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1064 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1065 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1068 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1071 Dygraph
.prototype.addXTicks_
= function() {
1072 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1073 var startDate
, endDate
;
1074 if (this.dateWindow_
) {
1075 startDate
= this.dateWindow_
[0];
1076 endDate
= this.dateWindow_
[1];
1078 startDate
= this.rawData_
[0][0];
1079 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1082 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1083 this.layout_
.updateOptions({xTicks
: xTicks
});
1086 // Time granularity enumeration
1087 Dygraph
.SECONDLY
= 0;
1088 Dygraph
.TWO_SECONDLY
= 1;
1089 Dygraph
.FIVE_SECONDLY
= 2;
1090 Dygraph
.TEN_SECONDLY
= 3;
1091 Dygraph
.THIRTY_SECONDLY
= 4;
1092 Dygraph
.MINUTELY
= 5;
1093 Dygraph
.TWO_MINUTELY
= 6;
1094 Dygraph
.FIVE_MINUTELY
= 7;
1095 Dygraph
.TEN_MINUTELY
= 8;
1096 Dygraph
.THIRTY_MINUTELY
= 9;
1097 Dygraph
.HOURLY
= 10;
1098 Dygraph
.TWO_HOURLY
= 11;
1099 Dygraph
.SIX_HOURLY
= 12;
1101 Dygraph
.WEEKLY
= 14;
1102 Dygraph
.MONTHLY
= 15;
1103 Dygraph
.QUARTERLY
= 16;
1104 Dygraph
.BIANNUAL
= 17;
1105 Dygraph
.ANNUAL
= 18;
1106 Dygraph
.DECADAL
= 19;
1107 Dygraph
.NUM_GRANULARITIES
= 20;
1109 Dygraph
.SHORT_SPACINGS
= [];
1110 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1111 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1112 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1113 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1114 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1115 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1116 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1117 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1118 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1119 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1120 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1121 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1122 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1123 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1124 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1128 // If we used this time granularity, how many ticks would there be?
1129 // This is only an approximation, but it's generally good enough.
1131 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1132 if (granularity
< Dygraph
.MONTHLY
) {
1133 // Generate one tick mark for every fixed interval of time.
1134 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1135 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1137 var year_mod
= 1; // e.g. to only print one point every 10 years.
1138 var num_months
= 12;
1139 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1140 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1141 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1142 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1144 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1145 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1146 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1152 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1153 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1155 // Returns an array containing {v: millis, label: label} dictionaries.
1157 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1159 if (granularity
< Dygraph
.MONTHLY
) {
1160 // Generate one tick mark for every fixed interval of time.
1161 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1162 var format
= '%d%b'; // e.g. "1Jan"
1164 // Find a time less than start_time which occurs on a "nice" time boundary
1165 // for this granularity.
1166 var g
= spacing
/ 1000;
1167 var d
= new Date(start_time
);
1168 if (g
<= 60) { // seconds
1169 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1173 if (g
<= 60) { // minutes
1174 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1179 if (g
<= 24) { // days
1180 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1185 if (g
== 7) { // one week
1186 d
.setDate(d
.getDate() - d
.getDay());
1191 start_time
= d
.getTime();
1193 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1194 var d
= new Date(t
);
1195 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1196 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1197 // the extra hour covers DST problems.
1198 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
1200 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
1204 // Display a tick mark on the first of a set of months of each year.
1205 // Years get a tick mark iff y % year_mod == 0. This is useful for
1206 // displaying a tick mark once every 10 years, say, on long time scales.
1208 var year_mod
= 1; // e.g. to only print one point every 10 years.
1210 if (granularity
== Dygraph
.MONTHLY
) {
1211 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1212 } else if (granularity
== Dygraph
.QUARTERLY
) {
1213 months
= [ 0, 3, 6, 9 ];
1214 } else if (granularity
== Dygraph
.BIANNUAL
) {
1216 } else if (granularity
== Dygraph
.ANNUAL
) {
1218 } else if (granularity
== Dygraph
.DECADAL
) {
1223 var start_year
= new Date(start_time
).getFullYear();
1224 var end_year
= new Date(end_time
).getFullYear();
1225 var zeropad
= Dygraph
.zeropad
;
1226 for (var i
= start_year
; i
<= end_year
; i
++) {
1227 if (i
% year_mod
!= 0) continue;
1228 for (var j
= 0; j
< months
.length
; j
++) {
1229 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1230 var t
= Date
.parse(date_str
);
1231 if (t
< start_time
|| t
> end_time
) continue;
1232 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
1242 * Add ticks to the x-axis based on a date range.
1243 * @param {Number} startDate Start of the date window (millis since epoch)
1244 * @param {Number} endDate End of the date window (millis since epoch)
1245 * @return {Array.<Object>} Array of {label, value} tuples.
1248 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1250 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1251 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1252 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1259 return self
.GetXAxis(startDate
, endDate
, chosen
);
1261 // TODO(danvk): signal error.
1266 * Add ticks when the x axis has numbers on it (instead of dates)
1267 * @param {Number} startDate Start of the date window (millis since epoch)
1268 * @param {Number} endDate End of the date window (millis since epoch)
1269 * @return {Array.<Object>} Array of {label, value} tuples.
1272 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1274 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1275 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1276 // The first spacing greater than pixelsPerYLabel is what we use.
1277 // TODO(danvk): version that works on a log scale.
1278 if (self
.attr_("labelsKMG2")) {
1279 var mults
= [1, 2, 4, 8];
1281 var mults
= [1, 2, 5];
1283 var scale
, low_val
, high_val
, nTicks
;
1284 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1285 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1286 for (var i
= -10; i
< 50; i
++) {
1287 if (self
.attr_("labelsKMG2")) {
1288 var base_scale
= Math
.pow(16, i
);
1290 var base_scale
= Math
.pow(10, i
);
1292 for (var j
= 0; j
< mults
.length
; j
++) {
1293 scale
= base_scale
* mults
[j
];
1294 low_val
= Math
.floor(minV
/ scale
) * scale
;
1295 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1296 nTicks
= (high_val
- low_val
) / scale
;
1297 var spacing
= self
.height_
/ nTicks
;
1298 // wish I could break out of both loops at once...
1299 if (spacing
> pixelsPerTick
) break;
1301 if (spacing
> pixelsPerTick
) break;
1304 // Construct labels for the ticks
1308 if (self
.attr_("labelsKMB")) {
1310 k_labels
= [ "K", "M", "B", "T" ];
1312 if (self
.attr_("labelsKMG2")) {
1313 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1315 k_labels
= [ "k", "M", "G", "T" ];
1318 for (var i
= 0; i
< nTicks
; i
++) {
1319 var tickV
= low_val
+ i
* scale
;
1320 var absTickV
= Math
.abs(tickV
);
1321 var label
= self
.round_(tickV
, 2);
1322 if (k_labels
.length
) {
1323 // Round up to an appropriate unit.
1325 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1326 if (absTickV
>= n
) {
1327 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1332 ticks
.push( {label
: label
, v
: tickV
} );
1338 * Adds appropriate ticks on the y-axis
1339 * @param {Number} minY The minimum Y value in the data set
1340 * @param {Number} maxY The maximum Y value in the data set
1343 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1344 // Set the number of ticks so that the labels are human-friendly.
1345 // TODO(danvk): make this an attribute as well.
1346 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1347 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1351 // Computes the range of the data series (including confidence intervals).
1352 // series is either [ [x1, y1], [x2, y2], ... ] or
1353 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1354 // Returns [low, high]
1355 Dygraph
.prototype.extremeValues_
= function(series
) {
1356 var minY
= null, maxY
= null;
1358 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1360 // With custom bars, maxY is the max of the high values.
1361 for (var j
= 0; j
< series
.length
; j
++) {
1362 var y
= series
[j
][1][0];
1364 var low
= y
- series
[j
][1][1];
1365 var high
= y
+ series
[j
][1][2];
1366 if (low
> y
) low
= y
; // this can happen with custom bars,
1367 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1368 if (maxY
== null || high
> maxY
) {
1371 if (minY
== null || low
< minY
) {
1376 for (var j
= 0; j
< series
.length
; j
++) {
1377 var y
= series
[j
][1];
1378 if (y
=== null || isNaN(y
)) continue;
1379 if (maxY
== null || y
> maxY
) {
1382 if (minY
== null || y
< minY
) {
1388 return [minY
, maxY
];
1392 * Update the graph with new data. Data is in the format
1393 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1394 * or, if errorBars=true,
1395 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1396 * @param {Array.<Object>} data The data (see above)
1399 Dygraph
.prototype.drawGraph_
= function(data
) {
1400 // This is used to set the second parameter to drawCallback, below.
1401 var is_initial_draw
= this.is_initial_draw_
;
1402 this.is_initial_draw_
= false;
1404 var minY
= null, maxY
= null;
1405 this.layout_
.removeAllDatasets();
1407 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1409 // For stacked series.
1410 var cumulative_y
= [];
1411 var stacked_datasets
= [];
1413 // Loop over all fields in the dataset
1414 for (var i
= 1; i
< data
[0].length
; i
++) {
1415 if (!this.visibility()[i
- 1]) continue;
1418 for (var j
= 0; j
< data
.length
; j
++) {
1419 var date
= data
[j
][0];
1420 series
[j
] = [date
, data
[j
][i
]];
1422 series
= this.rollingAverage(series
, this.rollPeriod_
);
1424 // Prune down to the desired range, if necessary (for zooming)
1425 // Because there can be lines going to points outside of the visible area,
1426 // we actually prune to visible points, plus one on either side.
1427 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1428 if (this.dateWindow_
) {
1429 var low
= this.dateWindow_
[0];
1430 var high
= this.dateWindow_
[1];
1432 // TODO(danvk): do binary search instead of linear search.
1433 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1434 var firstIdx
= null, lastIdx
= null;
1435 for (var k
= 0; k
< series
.length
; k
++) {
1436 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1439 if (series
[k
][0] <= high
) {
1443 if (firstIdx
=== null) firstIdx
= 0;
1444 if (firstIdx
> 0) firstIdx
--;
1445 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1446 if (lastIdx
< series
.length
- 1) lastIdx
++;
1447 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1448 pruned
.push(series
[k
]);
1453 var extremes
= this.extremeValues_(series
);
1454 var thisMinY
= extremes
[0];
1455 var thisMaxY
= extremes
[1];
1456 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1457 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1461 for (var j
=0; j
<series
.length
; j
++)
1462 vals
[j
] = [series
[j
][0],
1463 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1464 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1465 } else if (this.attr_("stackedGraph")) {
1467 var l
= series
.length
;
1469 for (var j
= 0; j
< l
; j
++) {
1470 if (cumulative_y
[series
[j
][0]] === undefined
)
1471 cumulative_y
[series
[j
][0]] = 0;
1473 actual_y
= series
[j
][1];
1474 cumulative_y
[series
[j
][0]] += actual_y
;
1476 vals
[j
] = [series
[j
][0], cumulative_y
[series
[j
][0]]]
1478 if (!maxY
|| cumulative_y
[series
[j
][0]] > maxY
)
1479 maxY
= cumulative_y
[series
[j
][0]];
1481 stacked_datasets
.push([this.attr_("labels")[i
], vals
]);
1482 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1484 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1488 if (stacked_datasets
.length
> 0) {
1489 for (var i
= (stacked_datasets
.length
- 1); i
>= 0; i
--) {
1490 this.layout_
.addDataset(stacked_datasets
[i
][0], stacked_datasets
[i
][1]);
1494 // Use some heuristics to come up with a good maxY value, unless it's been
1495 // set explicitly by the user.
1496 if (this.valueRange_
!= null) {
1497 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1498 this.displayedYRange_
= this.valueRange_
;
1500 // This affects the calculation of span, below.
1501 if (this.attr_("includeZero") && minY
> 0) {
1505 // Add some padding and round up to an integer to be human-friendly.
1506 var span
= maxY
- minY
;
1507 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1508 if (span
== 0) { span
= maxY
; }
1509 var maxAxisY
= maxY
+ 0.1 * span
;
1510 var minAxisY
= minY
- 0.1 * span
;
1512 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1513 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1514 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1516 if (this.attr_("includeZero")) {
1517 if (maxY
< 0) maxAxisY
= 0;
1518 if (minY
> 0) minAxisY
= 0;
1521 this.addYTicks_(minAxisY
, maxAxisY
);
1522 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1527 // Tell PlotKit to use this new data and render itself
1528 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1529 this.layout_
.evaluateWithError();
1530 this.plotter_
.clear();
1531 this.plotter_
.render();
1532 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1533 this.canvas_
.height
);
1535 if (this.attr_("drawCallback") !== null) {
1536 this.attr_("drawCallback")(this, is_initial_draw
);
1541 * Calculates the rolling average of a data set.
1542 * If originalData is [label, val], rolls the average of those.
1543 * If originalData is [label, [, it's interpreted as [value, stddev]
1544 * and the roll is returned in the same form, with appropriately reduced
1545 * stddev for each value.
1546 * Note that this is where fractional input (i.e. '5/10') is converted into
1548 * @param {Array} originalData The data in the appropriate format (see above)
1549 * @param {Number} rollPeriod The number of days over which to average the data
1551 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1552 if (originalData
.length
< 2)
1553 return originalData
;
1554 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1555 var rollingData
= [];
1556 var sigma
= this.attr_("sigma");
1558 if (this.fractions_
) {
1560 var den
= 0; // numerator/denominator
1562 for (var i
= 0; i
< originalData
.length
; i
++) {
1563 num
+= originalData
[i
][1][0];
1564 den
+= originalData
[i
][1][1];
1565 if (i
- rollPeriod
>= 0) {
1566 num
-= originalData
[i
- rollPeriod
][1][0];
1567 den
-= originalData
[i
- rollPeriod
][1][1];
1570 var date
= originalData
[i
][0];
1571 var value
= den
? num
/ den
: 0.0;
1572 if (this.attr_("errorBars")) {
1573 if (this.wilsonInterval_
) {
1574 // For more details on this confidence interval, see:
1575 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1577 var p
= value
< 0 ? 0 : value
, n
= den
;
1578 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1579 var denom
= 1 + sigma
* sigma
/ den
;
1580 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1581 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1582 rollingData
[i
] = [date
,
1583 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1585 rollingData
[i
] = [date
, [0, 0, 0]];
1588 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1589 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1592 rollingData
[i
] = [date
, mult
* value
];
1595 } else if (this.attr_("customBars")) {
1600 for (var i
= 0; i
< originalData
.length
; i
++) {
1601 var data
= originalData
[i
][1];
1603 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1605 if (y
!= null && !isNaN(y
)) {
1611 if (i
- rollPeriod
>= 0) {
1612 var prev
= originalData
[i
- rollPeriod
];
1613 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1620 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1621 1.0 * (mid
- low
) / count
,
1622 1.0 * (high
- mid
) / count
]];
1625 // Calculate the rolling average for the first rollPeriod - 1 points where
1626 // there is not enough data to roll over the full number of days
1627 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1628 if (!this.attr_("errorBars")){
1629 if (rollPeriod
== 1) {
1630 return originalData
;
1633 for (var i
= 0; i
< originalData
.length
; i
++) {
1636 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1637 var y
= originalData
[j
][1];
1638 if (y
== null || isNaN(y
)) continue;
1640 sum
+= originalData
[j
][1];
1643 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1645 rollingData
[i
] = [originalData
[i
][0], null];
1650 for (var i
= 0; i
< originalData
.length
; i
++) {
1654 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1655 var y
= originalData
[j
][1][0];
1656 if (y
== null || isNaN(y
)) continue;
1658 sum
+= originalData
[j
][1][0];
1659 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1662 var stddev
= Math
.sqrt(variance
) / num_ok
;
1663 rollingData
[i
] = [originalData
[i
][0],
1664 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1666 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1676 * Parses a date, returning the number of milliseconds since epoch. This can be
1677 * passed in as an xValueParser in the Dygraph constructor.
1678 * TODO(danvk): enumerate formats that this understands.
1679 * @param {String} A date in YYYYMMDD format.
1680 * @return {Number} Milliseconds since epoch.
1683 Dygraph
.dateParser
= function(dateStr
, self
) {
1686 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1687 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1688 while (dateStrSlashed
.search("-") != -1) {
1689 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1691 d
= Date
.parse(dateStrSlashed
);
1692 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1693 // TODO(danvk): remove support for this format. It's confusing.
1694 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1695 + "/" + dateStr
.substr(6,2);
1696 d
= Date
.parse(dateStrSlashed
);
1698 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1699 // "2009/07/12 12:34:56"
1700 d
= Date
.parse(dateStr
);
1703 if (!d
|| isNaN(d
)) {
1704 self
.error("Couldn't parse " + dateStr
+ " as a date");
1710 * Detects the type of the str (date or numeric) and sets the various
1711 * formatting attributes in this.attrs_ based on this type.
1712 * @param {String} str An x value.
1715 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1717 if (str
.indexOf('-') >= 0 ||
1718 str
.indexOf('/') >= 0 ||
1719 isNaN(parseFloat(str
))) {
1721 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1722 // TODO(danvk): remove support for this format.
1727 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1728 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1729 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1731 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1732 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1733 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1738 * Parses a string in a special csv format. We expect a csv file where each
1739 * line is a date point, and the first field in each line is the date string.
1740 * We also expect that all remaining fields represent series.
1741 * if the errorBars attribute is set, then interpret the fields as:
1742 * date, series1, stddev1, series2, stddev2, ...
1743 * @param {Array.<Object>} data See above.
1746 * @return Array.<Object> An array with one entry for each row. These entries
1747 * are an array of cells in that row. The first entry is the parsed x-value for
1748 * the row. The second, third, etc. are the y-values. These can take on one of
1749 * three forms, depending on the CSV and constructor parameters:
1751 * 2. [ value, stddev ]
1752 * 3. [ low value, center value, high value ]
1754 Dygraph
.prototype.parseCSV_
= function(data
) {
1756 var lines
= data
.split("\n");
1758 // Use the default delimiter or fall back to a tab if that makes sense.
1759 var delim
= this.attr_('delimiter');
1760 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1765 if (this.labelsFromCSV_
) {
1767 this.attrs_
.labels
= lines
[0].split(delim
);
1771 var defaultParserSet
= false; // attempt to auto-detect x value type
1772 var expectedCols
= this.attr_("labels").length
;
1773 var outOfOrder
= false;
1774 for (var i
= start
; i
< lines
.length
; i
++) {
1775 var line
= lines
[i
];
1776 if (line
.length
== 0) continue; // skip blank lines
1777 if (line
[0] == '#') continue; // skip comment lines
1778 var inFields
= line
.split(delim
);
1779 if (inFields
.length
< 2) continue;
1782 if (!defaultParserSet
) {
1783 this.detectTypeFromString_(inFields
[0]);
1784 xParser
= this.attr_("xValueParser");
1785 defaultParserSet
= true;
1787 fields
[0] = xParser(inFields
[0], this);
1789 // If fractions are expected, parse the numbers as "A/B
"
1790 if (this.fractions_) {
1791 for (var j = 1; j < inFields.length; j++) {
1792 // TODO(danvk): figure out an appropriate way to flag parse errors.
1793 var vals = inFields[j].split("/");
1794 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1796 } else if (this.attr_("errorBars
")) {
1797 // If there are error bars, values are (value, stddev) pairs
1798 for (var j = 1; j < inFields.length; j += 2)
1799 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1800 parseFloat(inFields[j + 1])];
1801 } else if (this.attr_("customBars
")) {
1802 // Bars are a low;center;high tuple
1803 for (var j = 1; j < inFields.length; j++) {
1804 var vals = inFields[j].split(";");
1805 fields[j] = [ parseFloat(vals[0]),
1806 parseFloat(vals[1]),
1807 parseFloat(vals[2]) ];
1810 // Values are just numbers
1811 for (var j = 1; j < inFields.length; j++) {
1812 fields[j] = parseFloat(inFields[j]);
1815 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1820 if (fields.length != expectedCols) {
1821 this.error("Number of columns
in line
" + i + " (" + fields.length +
1822 ") does not agree
with number of
labels (" + expectedCols +
1828 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1829 ret.sort(function(a,b) { return a[0] - b[0] });
1836 * The user has provided their data as a pre-packaged JS array. If the x values
1837 * are numeric, this is the same as dygraphs' internal format. If the x values
1838 * are dates, we need to convert them from Date objects to ms since epoch.
1839 * @param {Array.<Object>} data
1840 * @return {Array.<Object>} data with numeric x values.
1842 Dygraph.prototype.parseArray_ = function(data) {
1843 // Peek at the first x value to see if it's numeric.
1844 if (data.length == 0) {
1845 this.error("Can
't plot empty data set");
1848 if (data[0].length == 0) {
1849 this.error("Data set cannot contain an empty row");
1853 if (this.attr_("labels") == null) {
1854 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1855 "in the options parameter");
1856 this.attrs_.labels = [ "X" ];
1857 for (var i = 1; i < data[0].length; i++) {
1858 this.attrs_.labels.push("Y" + i);
1862 if (Dygraph.isDateLike(data[0][0])) {
1863 // Some intelligent defaults for a date x-axis.
1864 this.attrs_.xValueFormatter = Dygraph.dateString_;
1865 this.attrs_.xTicker = Dygraph.dateTicker;
1867 // Assume they're all dates
.
1868 var parsedData
= Dygraph
.clone(data
);
1869 for (var i
= 0; i
< data
.length
; i
++) {
1870 if (parsedData
[i
].length
== 0) {
1871 this.error("Row " << (1 + i
) << " of data is empty");
1874 if (parsedData
[i
][0] == null
1875 || typeof(parsedData
[i
][0].getTime
) != 'function'
1876 || isNaN(parsedData
[i
][0].getTime())) {
1877 this.error("x value in row " + (1 + i
) + " is not a Date");
1880 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1884 // Some intelligent defaults for a numeric x-axis.
1885 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1886 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1892 * Parses a DataTable object from gviz.
1893 * The data is expected to have a first column that is either a date or a
1894 * number. All subsequent columns must be numbers. If there is a clear mismatch
1895 * between this.xValueParser_ and the type of the first column, it will be
1896 * fixed. Returned value is in the same format as return value of parseCSV_.
1897 * @param {Array.<Object>} data See above.
1900 Dygraph
.prototype.parseDataTable_
= function(data
) {
1901 var cols
= data
.getNumberOfColumns();
1902 var rows
= data
.getNumberOfRows();
1904 // Read column labels
1906 for (var i
= 0; i
< cols
; i
++) {
1907 labels
.push(data
.getColumnLabel(i
));
1908 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1910 this.attrs_
.labels
= labels
;
1911 cols
= labels
.length
;
1913 var indepType
= data
.getColumnType(0);
1914 if (indepType
== 'date' || indepType
== 'datetime') {
1915 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1916 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1917 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1918 } else if (indepType
== 'number') {
1919 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1920 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1921 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1923 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1924 "column 1 of DataTable input (Got '" + indepType
+ "')");
1929 var outOfOrder
= false;
1930 for (var i
= 0; i
< rows
; i
++) {
1932 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
1933 data
.getValue(i
, 0) === null) {
1934 this.warning("Ignoring row " + i
+
1935 " of DataTable because of undefined or null first column.");
1939 if (indepType
== 'date' || indepType
== 'datetime') {
1940 row
.push(data
.getValue(i
, 0).getTime());
1942 row
.push(data
.getValue(i
, 0));
1944 if (!this.attr_("errorBars")) {
1945 for (var j
= 1; j
< cols
; j
++) {
1946 row
.push(data
.getValue(i
, j
));
1949 for (var j
= 0; j
< cols
- 1; j
++) {
1950 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
1953 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
1960 this.warn("DataTable is out of order; order it correctly to speed loading.");
1961 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
1966 // These functions are all based on MochiKit.
1967 Dygraph
.update
= function (self
, o
) {
1968 if (typeof(o
) != 'undefined' && o
!== null) {
1970 if (o
.hasOwnProperty(k
)) {
1978 Dygraph
.isArrayLike
= function (o
) {
1979 var typ
= typeof(o
);
1981 (typ
!= 'object' && !(typ
== 'function' &&
1982 typeof(o
.item
) == 'function')) ||
1984 typeof(o
.length
) != 'number' ||
1992 Dygraph
.isDateLike
= function (o
) {
1993 if (typeof(o
) != "object" || o
=== null ||
1994 typeof(o
.getTime
) != 'function') {
2000 Dygraph
.clone
= function(o
) {
2001 // TODO(danvk): figure out how MochiKit's version works
2003 for (var i
= 0; i
< o
.length
; i
++) {
2004 if (Dygraph
.isArrayLike(o
[i
])) {
2005 r
.push(Dygraph
.clone(o
[i
]));
2015 * Get the CSV data. If it's in a function, call that function. If it's in a
2016 * file, do an XMLHttpRequest to get it.
2019 Dygraph
.prototype.start_
= function() {
2020 if (typeof this.file_
== 'function') {
2021 // CSV string. Pretend we got it via XHR.
2022 this.loadedEvent_(this.file_());
2023 } else if (Dygraph
.isArrayLike(this.file_
)) {
2024 this.rawData_
= this.parseArray_(this.file_
);
2025 this.drawGraph_(this.rawData_
);
2026 } else if (typeof this.file_
== 'object' &&
2027 typeof this.file_
.getColumnRange
== 'function') {
2028 // must be a DataTable from gviz.
2029 this.rawData_
= this.parseDataTable_(this.file_
);
2030 this.drawGraph_(this.rawData_
);
2031 } else if (typeof this.file_
== 'string') {
2032 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2033 if (this.file_
.indexOf('\n') >= 0) {
2034 this.loadedEvent_(this.file_
);
2036 var req
= new XMLHttpRequest();
2038 req
.onreadystatechange
= function () {
2039 if (req
.readyState
== 4) {
2040 if (req
.status
== 200) {
2041 caller
.loadedEvent_(req
.responseText
);
2046 req
.open("GET", this.file_
, true);
2050 this.error("Unknown data format: " + (typeof this.file_
));
2055 * Changes various properties of the graph. These can include:
2057 * <li>file: changes the source data for the graph</li>
2058 * <li>errorBars: changes whether the data contains stddev</li>
2060 * @param {Object} attrs The new properties and values
2062 Dygraph
.prototype.updateOptions
= function(attrs
) {
2063 // TODO(danvk): this is a mess. Rethink this function.
2064 if (attrs
.rollPeriod
) {
2065 this.rollPeriod_
= attrs
.rollPeriod
;
2067 if (attrs
.dateWindow
) {
2068 this.dateWindow_
= attrs
.dateWindow
;
2070 if (attrs
.valueRange
) {
2071 this.valueRange_
= attrs
.valueRange
;
2073 Dygraph
.update(this.user_attrs_
, attrs
);
2075 this.labelsFromCSV_
= (this.attr_("labels") == null);
2077 // TODO(danvk): this doesn't match the constructor logic
2078 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2079 if (attrs
['file'] && attrs
['file'] != this.file_
) {
2080 this.file_
= attrs
['file'];
2083 this.drawGraph_(this.rawData_
);
2088 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2089 * containing div (which has presumably changed size since the dygraph was
2090 * instantiated. If the width/height are specified, the div will be resized.
2092 * This is far more efficient than destroying and re-instantiating a
2093 * Dygraph, since it doesn't have to reparse the underlying data.
2095 * @param {Number} width Width (in pixels)
2096 * @param {Number} height Height (in pixels)
2098 Dygraph
.prototype.resize
= function(width
, height
) {
2099 if ((width
=== null) != (height
=== null)) {
2100 this.warn("Dygraph.resize() should be called with zero parameters or " +
2101 "two non-NULL parameters. Pretending it was zero.");
2102 width
= height
= null;
2105 // TODO(danvk): there should be a clear() method.
2106 this.maindiv_
.innerHTML
= "";
2107 this.attrs_
.labelsDiv
= null;
2110 this.maindiv_
.style
.width
= width
+ "px";
2111 this.maindiv_
.style
.height
= height
+ "px";
2112 this.width_
= width
;
2113 this.height_
= height
;
2115 this.width_
= this.maindiv_
.offsetWidth
;
2116 this.height_
= this.maindiv_
.offsetHeight
;
2119 this.createInterface_();
2120 this.drawGraph_(this.rawData_
);
2124 * Adjusts the number of days in the rolling average. Updates the graph to
2125 * reflect the new averaging period.
2126 * @param {Number} length Number of days over which to average the data.
2128 Dygraph
.prototype.adjustRoll
= function(length
) {
2129 this.rollPeriod_
= length
;
2130 this.drawGraph_(this.rawData_
);
2134 * Returns a boolean array of visibility statuses.
2136 Dygraph
.prototype.visibility
= function() {
2137 // Do lazy-initialization, so that this happens after we know the number of
2139 if (!this.attr_("visibility")) {
2140 this.attrs_
["visibility"] = [];
2142 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2143 this.attr_("visibility").push(true);
2145 return this.attr_("visibility");
2149 * Changes the visiblity of a series.
2151 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2152 var x
= this.visibility();
2153 if (num
< 0 && num
>= x
.length
) {
2154 this.warn("invalid series number in setVisibility: " + num
);
2157 this.drawGraph_(this.rawData_
);
2162 * Create a new canvas element. This is more complex than a simple
2163 * document.createElement("canvas") because of IE and excanvas.
2165 Dygraph
.createCanvas
= function() {
2166 var canvas
= document
.createElement("canvas");
2168 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2170 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2178 * A wrapper around Dygraph that implements the gviz API.
2179 * @param {Object} container The DOM object the visualization should live in.
2181 Dygraph
.GVizChart
= function(container
) {
2182 this.container
= container
;
2185 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2186 this.container
.innerHTML
= '';
2187 this.date_graph
= new Dygraph(this.container
, data
, options
);
2190 // Older pages may still use this name.
2191 DateGraph
= Dygraph
;