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
119 connectSeparatedPoints
: false,
122 hideOverlayOnMouseOut
: true
125 // Various logging levels.
131 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
132 // Labels is no longer a constructor parameter, since it's typically set
133 // directly from the data source. It also conains a name for the x-axis,
134 // which the previous constructor form did not.
135 if (labels
!= null) {
136 var new_labels
= ["Date"];
137 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
138 Dygraph
.update(attrs
, { 'labels': new_labels
});
140 this.__init__(div
, file
, attrs
);
144 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
145 * and interaction <canvas> inside of it. See the constructor for details
147 * @param {String | Function} file Source data
148 * @param {Array.<String>} labels Names of the data series
149 * @param {Object} attrs Miscellaneous other options
152 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
153 // Support two-argument constructor
154 if (attrs
== null) { attrs
= {}; }
156 // Copy the important bits into the object
157 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
160 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
161 this.previousVerticalX_
= -1;
162 this.fractions_
= attrs
.fractions
|| false;
163 this.dateWindow_
= attrs
.dateWindow
|| null;
164 this.valueRange_
= attrs
.valueRange
|| null;
165 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
166 this.is_initial_draw_
= true;
168 // Clear the div. This ensure that, if multiple dygraphs are passed the same
169 // div, then only one will be drawn.
172 // If the div isn't already sized then inherit from our attrs or
173 // give it a default size.
174 if (div
.style
.width
== '') {
175 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
177 if (div
.style
.height
== '') {
178 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
180 this.width_
= parseInt(div
.style
.width
, 10);
181 this.height_
= parseInt(div
.style
.height
, 10);
182 // The div might have been specified as percent of the current window size,
183 // convert that to an appropriate number of pixels.
184 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
185 // Minus ten pixels keeps scrollbars from showing up for a 100% width div.
186 this.width_
= (this.width_
* self
.innerWidth
/ 100) - 10;
188 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
189 this.height_
= (this.height_
* self
.innerHeight
/ 100) - 10;
192 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
193 if (attrs
['stackedGraph']) {
194 attrs
['fillGraph'] = true;
195 // TODO(nikhilk): Add any other stackedGraph checks here.
198 // Dygraphs has many options, some of which interact with one another.
199 // To keep track of everything, we maintain two sets of options:
201 // this.user_attrs_ only options explicitly set by the user.
202 // this.attrs_ defaults, options derived from user_attrs_, data.
204 // Options are then accessed this.attr_('attr'), which first looks at
205 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
206 // defaults without overriding behavior that the user specifically asks for.
207 this.user_attrs_
= {};
208 Dygraph
.update(this.user_attrs_
, attrs
);
211 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
213 this.boundaryIds_
= [];
215 // Make a note of whether labels will be pulled from the CSV file.
216 this.labelsFromCSV_
= (this.attr_("labels") == null);
218 // Create the containing DIV and other interactive elements
219 this.createInterface_();
224 Dygraph
.prototype.attr_
= function(name
) {
225 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
226 return this.user_attrs_
[name
];
227 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
228 return this.attrs_
[name
];
234 // TODO(danvk): any way I can get the line numbers to be this.warn call?
235 Dygraph
.prototype.log
= function(severity
, message
) {
236 if (typeof(console
) != 'undefined') {
239 console
.debug('dygraphs: ' + message
);
242 console
.info('dygraphs: ' + message
);
244 case Dygraph
.WARNING
:
245 console
.warn('dygraphs: ' + message
);
248 console
.error('dygraphs: ' + message
);
253 Dygraph
.prototype.info
= function(message
) {
254 this.log(Dygraph
.INFO
, message
);
256 Dygraph
.prototype.warn
= function(message
) {
257 this.log(Dygraph
.WARNING
, message
);
259 Dygraph
.prototype.error
= function(message
) {
260 this.log(Dygraph
.ERROR
, message
);
264 * Returns the current rolling period, as set by the user or an option.
265 * @return {Number} The number of days in the rolling window
267 Dygraph
.prototype.rollPeriod
= function() {
268 return this.rollPeriod_
;
272 * Returns the currently-visible x-range. This can be affected by zooming,
273 * panning or a call to updateOptions.
274 * Returns a two-element array: [left, right].
275 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
277 Dygraph
.prototype.xAxisRange
= function() {
278 if (this.dateWindow_
) return this.dateWindow_
;
280 // The entire chart is visible.
281 var left
= this.rawData_
[0][0];
282 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
283 return [left
, right
];
287 * Returns the currently-visible y-range. This can be affected by zooming,
288 * panning or a call to updateOptions.
289 * Returns a two-element array: [bottom, top].
291 Dygraph
.prototype.yAxisRange
= function() {
292 return this.displayedYRange_
;
296 * Convert from data coordinates to canvas/div X/Y coordinates.
297 * Returns a two-element array: [X, Y]
299 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
300 var ret
= [null, null];
301 var area
= this.plotter_
.area
;
303 var xRange
= this.xAxisRange();
304 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
308 var yRange
= this.yAxisRange();
309 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
315 // TODO(danvk): use these functions throughout dygraphs.
317 * Convert from canvas/div coords to data coordinates.
318 * Returns a two-element array: [X, Y]
320 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
321 var ret
= [null, null];
322 var area
= this.plotter_
.area
;
324 var xRange
= this.xAxisRange();
325 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
329 var yRange
= this.yAxisRange();
330 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
336 Dygraph
.addEvent
= function(el
, evt
, fn
) {
337 var normed_fn
= function(e
) {
338 if (!e
) var e
= window
.event
;
341 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
342 el
.addEventListener(evt
, normed_fn
, false);
344 el
.attachEvent('on' + evt
, normed_fn
);
348 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
349 var ctx
= cnv
.getContext("2d");
351 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
356 * Generates interface elements for the Dygraph: a containing div, a div to
357 * display the current point, and a textbox to adjust the rolling average
358 * period. Also creates the Renderer/Layout elements.
361 Dygraph
.prototype.createInterface_
= function() {
362 // Create the all-enclosing graph div
363 var enclosing
= this.maindiv_
;
365 this.graphDiv
= document
.createElement("div");
366 this.graphDiv
.style
.width
= this.width_
+ "px";
367 this.graphDiv
.style
.height
= this.height_
+ "px";
368 enclosing
.appendChild(this.graphDiv
);
372 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
374 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
375 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
376 - 2 * this.attr_("axisTickSize");
377 this.clippingArea_
= clip
;
379 // Create the canvas for interactive parts of the chart.
380 this.canvas_
= Dygraph
.createCanvas();
381 this.canvas_
.style
.position
= "absolute";
382 this.canvas_
.width
= this.width_
;
383 this.canvas_
.height
= this.height_
;
384 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
385 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
386 this.graphDiv
.appendChild(this.canvas_
);
388 // ... and for static parts of the chart.
389 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
391 // Make sure we don't overdraw.
392 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
393 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
396 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(e
) {
397 dygraph
.mouseMove_(e
);
399 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(e
) {
400 dygraph
.mouseOut_(e
);
403 // Create the grapher
404 // TODO(danvk): why does the Layout need its own set of options?
405 this.layoutOptions_
= { 'xOriginIsZero': false };
406 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
407 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
408 Dygraph
.update(this.layoutOptions_
, {
409 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
411 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
413 // TODO(danvk): why does the Renderer need its own set of options?
414 this.renderOptions_
= { colorScheme
: this.colors_
,
416 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
417 Dygraph
.update(this.renderOptions_
, this.attrs_
);
418 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
419 this.plotter_
= new DygraphCanvasRenderer(this,
420 this.hidden_
, this.layout_
,
421 this.renderOptions_
);
423 this.createStatusMessage_();
424 this.createRollInterface_();
425 this.createDragInterface_();
429 * Detach DOM elements in the dygraph and null out all data references.
430 * Calling this when you're done with a dygraph can dramatically reduce memory
431 * usage. See, e.g., the tests/perf.html example.
433 Dygraph
.prototype.destroy
= function() {
434 var removeRecursive
= function(node
) {
435 while (node
.hasChildNodes()) {
436 removeRecursive(node
.firstChild
);
437 node
.removeChild(node
.firstChild
);
440 removeRecursive(this.maindiv_
);
442 var nullOut
= function(obj
) {
444 if (typeof(obj
[n
]) === 'object') {
450 // These may not all be necessary, but it can't hurt...
451 nullOut(this.layout_
);
452 nullOut(this.plotter_
);
457 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
458 * this particular canvas. All Dygraph work is done on this.canvas_.
459 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
460 * @return {Object} The newly-created canvas
463 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
464 var h
= Dygraph
.createCanvas();
465 h
.style
.position
= "absolute";
466 // TODO(danvk): h should be offset from canvas. canvas needs to include
467 // some extra area to make it easier to zoom in on the far left and far
468 // right. h needs to be precisely the plot area, so that clipping occurs.
469 h
.style
.top
= canvas
.style
.top
;
470 h
.style
.left
= canvas
.style
.left
;
471 h
.width
= this.width_
;
472 h
.height
= this.height_
;
473 h
.style
.width
= this.width_
+ "px"; // for IE
474 h
.style
.height
= this.height_
+ "px"; // for IE
475 this.graphDiv
.appendChild(h
);
479 // Taken from MochiKit.Color
480 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
484 if (saturation
=== 0) {
489 var i
= Math
.floor(hue
* 6);
490 var f
= (hue
* 6) - i
;
491 var p
= value
* (1 - saturation
);
492 var q
= value
* (1 - (saturation
* f
));
493 var t
= value
* (1 - (saturation
* (1 - f
)));
495 case 1: red
= q
; green
= value
; blue
= p
; break;
496 case 2: red
= p
; green
= value
; blue
= t
; break;
497 case 3: red
= p
; green
= q
; blue
= value
; break;
498 case 4: red
= t
; green
= p
; blue
= value
; break;
499 case 5: red
= value
; green
= p
; blue
= q
; break;
500 case 6: // fall through
501 case 0: red
= value
; green
= t
; blue
= p
; break;
504 red
= Math
.floor(255 * red
+ 0.5);
505 green
= Math
.floor(255 * green
+ 0.5);
506 blue
= Math
.floor(255 * blue
+ 0.5);
507 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
512 * Generate a set of distinct colors for the data series. This is done with a
513 * color wheel. Saturation/Value are customizable, and the hue is
514 * equally-spaced around the color wheel. If a custom set of colors is
515 * specified, that is used instead.
518 Dygraph
.prototype.setColors_
= function() {
519 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
520 // away with this.renderOptions_.
521 var num
= this.attr_("labels").length
- 1;
523 var colors
= this.attr_('colors');
525 var sat
= this.attr_('colorSaturation') || 1.0;
526 var val
= this.attr_('colorValue') || 0.5;
527 for (var i
= 1; i
<= num
; i
++) {
528 if (!this.visibility()[i
-1]) continue;
529 // alternate colors for high contrast.
530 var idx
= i
- parseInt(i
% 2 ? i
/ 2 : (i - num)/2, 10);
531 var hue
= (1.0 * idx
/ (1 + num
));
532 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
535 for (var i
= 0; i
< num
; i
++) {
536 if (!this.visibility()[i
]) continue;
537 var colorStr
= colors
[i
% colors
.length
];
538 this.colors_
.push(colorStr
);
542 // TODO(danvk): update this w/r
/t/ the
new options system
.
543 this.renderOptions_
.colorScheme
= this.colors_
;
544 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
545 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
546 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
550 * Return the list of colors. This is either the list of colors passed in the
551 * attributes, or the autogenerated list of rgb(r,g,b) strings.
552 * @return {Array<string>} The list of colors.
554 Dygraph
.prototype.getColors
= function() {
558 // The following functions are from quirksmode.org with a modification for Safari from
559 // http://blog.firetree.net/2005/07/04/javascript-find-position/
560 // http://www.quirksmode.org/js
/findpos
.html
561 Dygraph
.findPosX
= function(obj
) {
566 curleft
+= obj
.offsetLeft
;
567 if(!obj
.offsetParent
)
569 obj
= obj
.offsetParent
;
576 Dygraph
.findPosY
= function(obj
) {
581 curtop
+= obj
.offsetTop
;
582 if(!obj
.offsetParent
)
584 obj
= obj
.offsetParent
;
594 * Create the div that contains information on the selected point(s)
595 * This goes in the top right of the canvas, unless an external div has already
599 Dygraph
.prototype.createStatusMessage_
= function(){
600 if (!this.attr_("labelsDiv")) {
601 var divWidth
= this.attr_('labelsDivWidth');
603 "position": "absolute",
606 "width": divWidth
+ "px",
608 "left": (this.width_
- divWidth
- 2) + "px",
609 "background": "white",
611 "overflow": "hidden"};
612 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
613 var div
= document
.createElement("div");
614 for (var name
in messagestyle
) {
615 if (messagestyle
.hasOwnProperty(name
)) {
616 div
.style
[name
] = messagestyle
[name
];
619 this.graphDiv
.appendChild(div
);
620 this.attrs_
.labelsDiv
= div
;
625 * Create the text box to adjust the averaging period
626 * @return {Object} The newly-created text box
629 Dygraph
.prototype.createRollInterface_
= function() {
630 var display
= this.attr_('showRoller') ? "block" : "none";
631 var textAttr
= { "position": "absolute",
633 "top": (this.plotter_
.area
.h
- 25) + "px",
634 "left": (this.plotter_
.area
.x
+ 1) + "px",
637 var roller
= document
.createElement("input");
638 roller
.type
= "text";
640 roller
.value
= this.rollPeriod_
;
641 for (var name
in textAttr
) {
642 if (textAttr
.hasOwnProperty(name
)) {
643 roller
.style
[name
] = textAttr
[name
];
647 var pa
= this.graphDiv
;
648 pa
.appendChild(roller
);
650 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
654 // These functions are taken from MochiKit.Signal
655 Dygraph
.pageX
= function(e
) {
657 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
660 var b
= document
.body
;
662 (de
.scrollLeft
|| b
.scrollLeft
) -
663 (de
.clientLeft
|| 0);
667 Dygraph
.pageY
= function(e
) {
669 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
672 var b
= document
.body
;
674 (de
.scrollTop
|| b
.scrollTop
) -
680 * Set up all the mouse handlers needed to capture dragging behavior for zoom
684 Dygraph
.prototype.createDragInterface_
= function() {
687 // Tracks whether the mouse is down right now
688 var isZooming
= false;
689 var isPanning
= false;
690 var dragStartX
= null;
691 var dragStartY
= null;
695 var draggingDate
= null;
696 var dateRange
= null;
698 // Utility function to convert page-wide coordinates to canvas coords
701 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
702 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
704 // Draw zoom rectangles when the mouse is down and the user moves around
705 Dygraph
.addEvent(this.hidden_
, 'mousemove', function(event
) {
707 dragEndX
= getX(event
);
708 dragEndY
= getY(event
);
710 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
712 } else if (isPanning
) {
713 dragEndX
= getX(event
);
714 dragEndY
= getY(event
);
716 // Want to have it so that:
717 // 1. draggingDate appears at dragEndX
718 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
720 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
721 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
722 self
.drawGraph_(self
.rawData_
);
726 // Track the beginning of drag events
727 Dygraph
.addEvent(this.hidden_
, 'mousedown', function(event
) {
728 px
= Dygraph
.findPosX(self
.canvas_
);
729 py
= Dygraph
.findPosY(self
.canvas_
);
730 dragStartX
= getX(event
);
731 dragStartY
= getY(event
);
733 if (event
.altKey
|| event
.shiftKey
) {
734 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
736 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
737 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
744 // If the user releases the mouse button during a drag, but not over the
745 // canvas, then it doesn't count as a zooming action.
746 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
747 if (isZooming
|| isPanning
) {
760 // Temporarily cancel the dragging event when the mouse leaves the graph
761 Dygraph
.addEvent(this.hidden_
, 'mouseout', function(event
) {
768 // If the mouse is released on the canvas during a drag event, then it's a
769 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
770 Dygraph
.addEvent(this.hidden_
, 'mouseup', function(event
) {
773 dragEndX
= getX(event
);
774 dragEndY
= getY(event
);
775 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
776 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
778 if (regionWidth
< 2 && regionHeight
< 2 &&
779 self
.attr_('clickCallback') != null &&
780 self
.lastx_
!= undefined
) {
781 // TODO(danvk): pass along more info about the points.
782 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
785 if (regionWidth
>= 10) {
786 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
787 Math
.max(dragStartX
, dragEndX
));
789 self
.canvas_
.getContext("2d").clearRect(0, 0,
791 self
.canvas_
.height
);
805 // Double-clicking zooms back out
806 Dygraph
.addEvent(this.hidden_
, 'dblclick', function(event
) {
807 if (self
.dateWindow_
== null) return;
808 self
.dateWindow_
= null;
809 self
.drawGraph_(self
.rawData_
);
810 var minDate
= self
.rawData_
[0][0];
811 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
812 if (self
.attr_("zoomCallback")) {
813 self
.attr_("zoomCallback")(minDate
, maxDate
);
819 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
820 * up any previous zoom rectangles that were drawn. This could be optimized to
821 * avoid extra redrawing, but it's tricky to avoid interactions with the status
823 * @param {Number} startX The X position where the drag started, in canvas
825 * @param {Number} endX The current X position of the drag, in canvas coords.
826 * @param {Number} prevEndX The value of endX on the previous call to this
827 * function. Used to avoid excess redrawing
830 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
831 var ctx
= this.canvas_
.getContext("2d");
833 // Clean up from the previous rect if necessary
835 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
836 Math
.abs(startX
- prevEndX
), this.height_
);
839 // Draw a light-grey rectangle to show the new viewing area
840 if (endX
&& startX
) {
841 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
842 ctx
.fillRect(Math
.min(startX
, endX
), 0,
843 Math
.abs(endX
- startX
), this.height_
);
848 * Zoom to something containing [lowX, highX]. These are pixel coordinates
849 * in the canvas. The exact zoom window may be slightly larger if there are no
850 * data points near lowX or highX. This function redraws the graph.
851 * @param {Number} lowX The leftmost pixel value that should be visible.
852 * @param {Number} highX The rightmost pixel value that should be visible.
855 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
856 // Find the earliest and latest dates contained in this canvasx range.
857 var r
= this.toDataCoords(lowX
, null);
859 r
= this.toDataCoords(highX
, null);
862 this.dateWindow_
= [minDate
, maxDate
];
863 this.drawGraph_(this.rawData_
);
864 if (this.attr_("zoomCallback")) {
865 this.attr_("zoomCallback")(minDate
, maxDate
);
870 * When the mouse moves in the canvas, display information about a nearby data
871 * point and draw dots over those points in the data series. This function
872 * takes care of cleanup of previously-drawn dots.
873 * @param {Object} event The mousemove event from the browser.
876 Dygraph
.prototype.mouseMove_
= function(event
) {
877 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.hidden_
);
878 var points
= this.layout_
.points
;
883 // Loop through all the points and find the date nearest to our current
885 var minDist
= 1e+100;
887 for (var i
= 0; i
< points
.length
; i
++) {
888 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
889 if (dist
> minDist
) continue;
893 if (idx
>= 0) lastx
= points
[idx
].xval
;
894 // Check that you can really highlight the last day's data
895 if (canvasx
> points
[points
.length
-1].canvasx
)
896 lastx
= points
[points
.length
-1].xval
;
898 // Extract the points we've selected
899 this.selPoints_
= [];
900 for (var i
= 0; i
< points
.length
; i
++) {
901 if (points
[i
].xval
== lastx
) {
902 this.selPoints_
.push(points
[i
]);
906 if (this.attr_("highlightCallback")) {
907 var px
= this.lastHighlightCallbackX
;
908 if (px
!== null && lastx
!= px
) {
909 // only fire if the selected point has changed.
910 this.lastHighlightCallbackX
= lastx
;
911 if (!this.attr_("stackedGraph")) {
912 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
914 // "unstack" the points.
915 var callbackPoints
= this.selPoints_
.map(
916 function(p
) { return {xval
: p
.xval
, yval
: p
.yval
, name
: p
.name
} });
917 var cumulative_sum
= 0;
918 for (var j
= callbackPoints
.length
- 1; j
>= 0; j
--) {
919 callbackPoints
[j
].yval
-= cumulative_sum
;
920 cumulative_sum
+= callbackPoints
[j
].yval
;
922 this.attr_("highlightCallback")(event
, lastx
, callbackPoints
);
927 // Save last x position for callbacks.
930 this.updateSelection_();
934 * Draw dots over the selectied points in the data series. This function
935 * takes care of cleanup of previously-drawn dots.
938 Dygraph
.prototype.updateSelection_
= function() {
939 // Clear the previously drawn vertical, if there is one
940 var circleSize
= this.attr_('highlightCircleSize');
941 var ctx
= this.canvas_
.getContext("2d");
942 if (this.previousVerticalX_
>= 0) {
943 var px
= this.previousVerticalX_
;
944 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
947 var isOK
= function(x
) { return x
&& !isNaN(x
); };
949 if (this.selPoints_
.length
> 0) {
950 var canvasx
= this.selPoints_
[0].canvasx
;
952 // Set the status message to indicate the selected point(s)
953 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
954 var clen
= this.colors_
.length
;
955 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
956 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
957 if (this.attr_("labelsSeparateLines")) {
960 var point
= this.selPoints_
[i
];
961 var c
= new RGBColor(this.plotter_
.colors_
[point
.name
]);
962 replace
+= " <b><font color='" + c
.toHex() + "'>"
963 + point
.name
+ "</font></b>:"
964 + this.round_(point
.yval
, 2);
966 this.attr_("labelsDiv").innerHTML
= replace
;
968 // Draw colored circles over the center of each selected point
970 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
971 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
973 ctx
.fillStyle
= this.plotter_
.colors_
[this.selPoints_
[i
].name
];
974 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
975 0, 2 * Math
.PI
, false);
980 this.previousVerticalX_
= canvasx
;
985 * Set manually set selected dots, and display information about them
986 * @param int row number that should by highlighted
987 * false value clears the selection
990 Dygraph
.prototype.setSelection
= function(row
) {
991 // Extract the points we've selected
992 this.selPoints_
= [];
996 row
= row
-this.boundaryIds_
[0][0];
999 if (row
!== false && row
>= 0) {
1000 for (var i
in this.layout_
.datasets
) {
1001 if (row
< this.layout_
.datasets
[i
].length
) {
1002 this.selPoints_
.push(this.layout_
.points
[pos
+row
]);
1004 pos
+= this.layout_
.datasets
[i
].length
;
1008 if (this.selPoints_
.length
) {
1009 this.lastx_
= this.selPoints_
[0].xval
;
1010 this.updateSelection_();
1013 this.clearSelection();
1019 * The mouse has left the canvas. Clear out whatever artifacts remain
1020 * @param {Object} event the mouseout event from the browser.
1023 Dygraph
.prototype.mouseOut_
= function(event
) {
1024 if (this.attr_("hideOverlayOnMouseOut")) {
1025 this.clearSelection();
1030 * Remove all selection from the canvas
1033 Dygraph
.prototype.clearSelection
= function() {
1034 // Get rid of the overlay data
1035 var ctx
= this.canvas_
.getContext("2d");
1036 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1037 this.attr_("labelsDiv").innerHTML
= "";
1038 this.selPoints_
= [];
1043 * Returns the number of the currently selected row
1044 * @return int row number, of -1 if nothing is selected
1047 Dygraph
.prototype.getSelection
= function() {
1048 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1052 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1053 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1054 return row
+ this.boundaryIds_
[0][0];
1060 Dygraph
.zeropad
= function(x
) {
1061 if (x
< 10) return "0" + x
; else return "" + x
;
1065 * Return a string version of the hours, minutes and seconds portion of a date.
1066 * @param {Number} date The JavaScript date (ms since epoch)
1067 * @return {String} A time of the form "HH:MM:SS"
1070 Dygraph
.prototype.hmsString_
= function(date
) {
1071 var zeropad
= Dygraph
.zeropad
;
1072 var d
= new Date(date
);
1073 if (d
.getSeconds()) {
1074 return zeropad(d
.getHours()) + ":" +
1075 zeropad(d
.getMinutes()) + ":" +
1076 zeropad(d
.getSeconds());
1078 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1083 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1084 * @param {Number} date The JavaScript date (ms since epoch)
1085 * @return {String} A date of the form "YYYY/MM/DD"
1087 * TODO(danvk): why is this part of the prototype?
1089 Dygraph
.dateString_
= function(date
, self
) {
1090 var zeropad
= Dygraph
.zeropad
;
1091 var d
= new Date(date
);
1094 var year
= "" + d
.getFullYear();
1095 // Get a 0 padded month string
1096 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1097 // Get a 0 padded day string
1098 var day
= zeropad(d
.getDate());
1101 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1102 if (frac
) ret
= " " + self
.hmsString_(date
);
1104 return year
+ "/" + month + "/" + day
+ ret
;
1108 * Round a number to the specified number of digits past the decimal point.
1109 * @param {Number} num The number to round
1110 * @param {Number} places The number of decimals to which to round
1111 * @return {Number} The rounded number
1114 Dygraph
.prototype.round_
= function(num
, places
) {
1115 var shift
= Math
.pow(10, places
);
1116 return Math
.round(num
* shift
)/shift
;
1120 * Fires when there's data available to be graphed.
1121 * @param {String} data Raw CSV data to be plotted
1124 Dygraph
.prototype.loadedEvent_
= function(data
) {
1125 this.rawData_
= this.parseCSV_(data
);
1126 this.drawGraph_(this.rawData_
);
1129 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1130 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1131 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1134 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1137 Dygraph
.prototype.addXTicks_
= function() {
1138 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1139 var startDate
, endDate
;
1140 if (this.dateWindow_
) {
1141 startDate
= this.dateWindow_
[0];
1142 endDate
= this.dateWindow_
[1];
1144 startDate
= this.rawData_
[0][0];
1145 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1148 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1149 this.layout_
.updateOptions({xTicks
: xTicks
});
1152 // Time granularity enumeration
1153 Dygraph
.SECONDLY
= 0;
1154 Dygraph
.TWO_SECONDLY
= 1;
1155 Dygraph
.FIVE_SECONDLY
= 2;
1156 Dygraph
.TEN_SECONDLY
= 3;
1157 Dygraph
.THIRTY_SECONDLY
= 4;
1158 Dygraph
.MINUTELY
= 5;
1159 Dygraph
.TWO_MINUTELY
= 6;
1160 Dygraph
.FIVE_MINUTELY
= 7;
1161 Dygraph
.TEN_MINUTELY
= 8;
1162 Dygraph
.THIRTY_MINUTELY
= 9;
1163 Dygraph
.HOURLY
= 10;
1164 Dygraph
.TWO_HOURLY
= 11;
1165 Dygraph
.SIX_HOURLY
= 12;
1167 Dygraph
.WEEKLY
= 14;
1168 Dygraph
.MONTHLY
= 15;
1169 Dygraph
.QUARTERLY
= 16;
1170 Dygraph
.BIANNUAL
= 17;
1171 Dygraph
.ANNUAL
= 18;
1172 Dygraph
.DECADAL
= 19;
1173 Dygraph
.NUM_GRANULARITIES
= 20;
1175 Dygraph
.SHORT_SPACINGS
= [];
1176 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1177 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1178 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1179 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1180 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1181 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1182 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1183 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1184 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1185 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1186 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1187 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1188 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1189 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1190 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1194 // If we used this time granularity, how many ticks would there be?
1195 // This is only an approximation, but it's generally good enough.
1197 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1198 if (granularity
< Dygraph
.MONTHLY
) {
1199 // Generate one tick mark for every fixed interval of time.
1200 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1201 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1203 var year_mod
= 1; // e.g. to only print one point every 10 years.
1204 var num_months
= 12;
1205 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1206 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1207 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1208 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1210 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1211 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1212 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1218 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1219 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1221 // Returns an array containing {v: millis, label: label} dictionaries.
1223 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1225 if (granularity
< Dygraph
.MONTHLY
) {
1226 // Generate one tick mark for every fixed interval of time.
1227 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1228 var format
= '%d%b'; // e.g. "1Jan"
1230 // Find a time less than start_time which occurs on a "nice" time boundary
1231 // for this granularity.
1232 var g
= spacing
/ 1000;
1233 var d
= new Date(start_time
);
1234 if (g
<= 60) { // seconds
1235 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1239 if (g
<= 60) { // minutes
1240 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1245 if (g
<= 24) { // days
1246 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1251 if (g
== 7) { // one week
1252 d
.setDate(d
.getDate() - d
.getDay());
1257 start_time
= d
.getTime();
1259 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1260 var d
= new Date(t
);
1261 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1262 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1263 // the extra hour covers DST problems.
1264 ticks
.push({ v
:t
, label
: new Date(t
+ 3600*1000).strftime(format
) });
1266 ticks
.push({ v
:t
, label
: this.hmsString_(t
) });
1270 // Display a tick mark on the first of a set of months of each year.
1271 // Years get a tick mark iff y % year_mod == 0. This is useful for
1272 // displaying a tick mark once every 10 years, say, on long time scales.
1274 var year_mod
= 1; // e.g. to only print one point every 10 years.
1276 if (granularity
== Dygraph
.MONTHLY
) {
1277 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1278 } else if (granularity
== Dygraph
.QUARTERLY
) {
1279 months
= [ 0, 3, 6, 9 ];
1280 } else if (granularity
== Dygraph
.BIANNUAL
) {
1282 } else if (granularity
== Dygraph
.ANNUAL
) {
1284 } else if (granularity
== Dygraph
.DECADAL
) {
1289 var start_year
= new Date(start_time
).getFullYear();
1290 var end_year
= new Date(end_time
).getFullYear();
1291 var zeropad
= Dygraph
.zeropad
;
1292 for (var i
= start_year
; i
<= end_year
; i
++) {
1293 if (i
% year_mod
!= 0) continue;
1294 for (var j
= 0; j
< months
.length
; j
++) {
1295 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1296 var t
= Date
.parse(date_str
);
1297 if (t
< start_time
|| t
> end_time
) continue;
1298 ticks
.push({ v
:t
, label
: new Date(t
).strftime('%b %y') });
1308 * Add ticks to the x-axis based on a date range.
1309 * @param {Number} startDate Start of the date window (millis since epoch)
1310 * @param {Number} endDate End of the date window (millis since epoch)
1311 * @return {Array.<Object>} Array of {label, value} tuples.
1314 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1316 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1317 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1318 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1325 return self
.GetXAxis(startDate
, endDate
, chosen
);
1327 // TODO(danvk): signal error.
1332 * Add ticks when the x axis has numbers on it (instead of dates)
1333 * @param {Number} startDate Start of the date window (millis since epoch)
1334 * @param {Number} endDate End of the date window (millis since epoch)
1335 * @return {Array.<Object>} Array of {label, value} tuples.
1338 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1340 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1341 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1342 // The first spacing greater than pixelsPerYLabel is what we use.
1343 // TODO(danvk): version that works on a log scale.
1344 if (self
.attr_("labelsKMG2")) {
1345 var mults
= [1, 2, 4, 8];
1347 var mults
= [1, 2, 5];
1349 var scale
, low_val
, high_val
, nTicks
;
1350 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1351 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1352 for (var i
= -10; i
< 50; i
++) {
1353 if (self
.attr_("labelsKMG2")) {
1354 var base_scale
= Math
.pow(16, i
);
1356 var base_scale
= Math
.pow(10, i
);
1358 for (var j
= 0; j
< mults
.length
; j
++) {
1359 scale
= base_scale
* mults
[j
];
1360 low_val
= Math
.floor(minV
/ scale
) * scale
;
1361 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1362 nTicks
= (high_val
- low_val
) / scale
;
1363 var spacing
= self
.height_
/ nTicks
;
1364 // wish I could break out of both loops at once...
1365 if (spacing
> pixelsPerTick
) break;
1367 if (spacing
> pixelsPerTick
) break;
1370 // Construct labels for the ticks
1374 if (self
.attr_("labelsKMB")) {
1376 k_labels
= [ "K", "M", "B", "T" ];
1378 if (self
.attr_("labelsKMG2")) {
1379 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1381 k_labels
= [ "k", "M", "G", "T" ];
1384 for (var i
= 0; i
< nTicks
; i
++) {
1385 var tickV
= low_val
+ i
* scale
;
1386 var absTickV
= Math
.abs(tickV
);
1387 var label
= self
.round_(tickV
, 2);
1388 if (k_labels
.length
) {
1389 // Round up to an appropriate unit.
1391 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1392 if (absTickV
>= n
) {
1393 label
= self
.round_(tickV
/ n
, 1) + k_labels
[j
];
1398 ticks
.push( {label
: label
, v
: tickV
} );
1404 * Adds appropriate ticks on the y-axis
1405 * @param {Number} minY The minimum Y value in the data set
1406 * @param {Number} maxY The maximum Y value in the data set
1409 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1410 // Set the number of ticks so that the labels are human-friendly.
1411 // TODO(danvk): make this an attribute as well.
1412 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1413 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1417 // Computes the range of the data series (including confidence intervals).
1418 // series is either [ [x1, y1], [x2, y2], ... ] or
1419 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1420 // Returns [low, high]
1421 Dygraph
.prototype.extremeValues_
= function(series
) {
1422 var minY
= null, maxY
= null;
1424 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1426 // With custom bars, maxY is the max of the high values.
1427 for (var j
= 0; j
< series
.length
; j
++) {
1428 var y
= series
[j
][1][0];
1430 var low
= y
- series
[j
][1][1];
1431 var high
= y
+ series
[j
][1][2];
1432 if (low
> y
) low
= y
; // this can happen with custom bars,
1433 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1434 if (maxY
== null || high
> maxY
) {
1437 if (minY
== null || low
< minY
) {
1442 for (var j
= 0; j
< series
.length
; j
++) {
1443 var y
= series
[j
][1];
1444 if (y
=== null || isNaN(y
)) continue;
1445 if (maxY
== null || y
> maxY
) {
1448 if (minY
== null || y
< minY
) {
1454 return [minY
, maxY
];
1458 * Update the graph with new data. Data is in the format
1459 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1460 * or, if errorBars=true,
1461 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1462 * @param {Array.<Object>} data The data (see above)
1465 Dygraph
.prototype.drawGraph_
= function(data
) {
1466 // This is used to set the second parameter to drawCallback, below.
1467 var is_initial_draw
= this.is_initial_draw_
;
1468 this.is_initial_draw_
= false;
1470 var minY
= null, maxY
= null;
1471 this.layout_
.removeAllDatasets();
1473 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1475 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints');
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 if (data
[j
][i
] || !connectSeparatedPoints
) {
1488 var date
= data
[j
][0];
1489 series
[j
] = [date
, data
[j
][i
]];
1492 series
= this.rollingAverage(series
, this.rollPeriod_
);
1494 // Prune down to the desired range, if necessary (for zooming)
1495 // Because there can be lines going to points outside of the visible area,
1496 // we actually prune to visible points, plus one on either side.
1497 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1498 if (this.dateWindow_
) {
1499 var low
= this.dateWindow_
[0];
1500 var high
= this.dateWindow_
[1];
1502 // TODO(danvk): do binary search instead of linear search.
1503 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1504 var firstIdx
= null, lastIdx
= null;
1505 for (var k
= 0; k
< series
.length
; k
++) {
1506 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1509 if (series
[k
][0] <= high
) {
1513 if (firstIdx
=== null) firstIdx
= 0;
1514 if (firstIdx
> 0) firstIdx
--;
1515 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1516 if (lastIdx
< series
.length
- 1) lastIdx
++;
1517 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1518 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1519 pruned
.push(series
[k
]);
1523 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1526 var extremes
= this.extremeValues_(series
);
1527 var thisMinY
= extremes
[0];
1528 var thisMaxY
= extremes
[1];
1529 if (!minY
|| thisMinY
< minY
) minY
= thisMinY
;
1530 if (!maxY
|| thisMaxY
> maxY
) maxY
= thisMaxY
;
1534 for (var j
=0; j
<series
.length
; j
++)
1535 vals
[j
] = [series
[j
][0],
1536 series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1537 this.layout_
.addDataset(this.attr_("labels")[i
], vals
);
1538 } else if (this.attr_("stackedGraph")) {
1540 var l
= series
.length
;
1542 for (var j
= 0; j
< l
; j
++) {
1543 if (cumulative_y
[series
[j
][0]] === undefined
)
1544 cumulative_y
[series
[j
][0]] = 0;
1546 actual_y
= series
[j
][1];
1547 cumulative_y
[series
[j
][0]] += actual_y
;
1549 vals
[j
] = [series
[j
][0], cumulative_y
[series
[j
][0]]]
1551 if (!maxY
|| cumulative_y
[series
[j
][0]] > maxY
)
1552 maxY
= cumulative_y
[series
[j
][0]];
1554 stacked_datasets
.push([this.attr_("labels")[i
], vals
]);
1555 //this.layout_.addDataset(this.attr_("labels")[i], vals);
1557 this.layout_
.addDataset(this.attr_("labels")[i
], series
);
1561 if (stacked_datasets
.length
> 0) {
1562 for (var i
= (stacked_datasets
.length
- 1); i
>= 0; i
--) {
1563 this.layout_
.addDataset(stacked_datasets
[i
][0], stacked_datasets
[i
][1]);
1567 // Use some heuristics to come up with a good maxY value, unless it's been
1568 // set explicitly by the user.
1569 if (this.valueRange_
!= null) {
1570 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1571 this.displayedYRange_
= this.valueRange_
;
1573 // This affects the calculation of span, below.
1574 if (this.attr_("includeZero") && minY
> 0) {
1578 // Add some padding and round up to an integer to be human-friendly.
1579 var span
= maxY
- minY
;
1580 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1581 if (span
== 0) { span
= maxY
; }
1582 var maxAxisY
= maxY
+ 0.1 * span
;
1583 var minAxisY
= minY
- 0.1 * span
;
1585 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1586 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1587 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1589 if (this.attr_("includeZero")) {
1590 if (maxY
< 0) maxAxisY
= 0;
1591 if (minY
> 0) minAxisY
= 0;
1594 this.addYTicks_(minAxisY
, maxAxisY
);
1595 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1600 // Tell PlotKit to use this new data and render itself
1601 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1602 this.layout_
.evaluateWithError();
1603 this.plotter_
.clear();
1604 this.plotter_
.render();
1605 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1606 this.canvas_
.height
);
1608 if (this.attr_("drawCallback") !== null) {
1609 this.attr_("drawCallback")(this, is_initial_draw
);
1614 * Calculates the rolling average of a data set.
1615 * If originalData is [label, val], rolls the average of those.
1616 * If originalData is [label, [, it's interpreted as [value, stddev]
1617 * and the roll is returned in the same form, with appropriately reduced
1618 * stddev for each value.
1619 * Note that this is where fractional input (i.e. '5/10') is converted into
1621 * @param {Array} originalData The data in the appropriate format (see above)
1622 * @param {Number} rollPeriod The number of days over which to average the data
1624 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1625 if (originalData
.length
< 2)
1626 return originalData
;
1627 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1628 var rollingData
= [];
1629 var sigma
= this.attr_("sigma");
1631 if (this.fractions_
) {
1633 var den
= 0; // numerator/denominator
1635 for (var i
= 0; i
< originalData
.length
; i
++) {
1636 num
+= originalData
[i
][1][0];
1637 den
+= originalData
[i
][1][1];
1638 if (i
- rollPeriod
>= 0) {
1639 num
-= originalData
[i
- rollPeriod
][1][0];
1640 den
-= originalData
[i
- rollPeriod
][1][1];
1643 var date
= originalData
[i
][0];
1644 var value
= den
? num
/ den
: 0.0;
1645 if (this.attr_("errorBars")) {
1646 if (this.wilsonInterval_
) {
1647 // For more details on this confidence interval, see:
1648 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1650 var p
= value
< 0 ? 0 : value
, n
= den
;
1651 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1652 var denom
= 1 + sigma
* sigma
/ den
;
1653 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1654 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1655 rollingData
[i
] = [date
,
1656 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1658 rollingData
[i
] = [date
, [0, 0, 0]];
1661 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1662 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1665 rollingData
[i
] = [date
, mult
* value
];
1668 } else if (this.attr_("customBars")) {
1673 for (var i
= 0; i
< originalData
.length
; i
++) {
1674 var data
= originalData
[i
][1];
1676 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1678 if (y
!= null && !isNaN(y
)) {
1684 if (i
- rollPeriod
>= 0) {
1685 var prev
= originalData
[i
- rollPeriod
];
1686 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1693 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1694 1.0 * (mid
- low
) / count
,
1695 1.0 * (high
- mid
) / count
]];
1698 // Calculate the rolling average for the first rollPeriod - 1 points where
1699 // there is not enough data to roll over the full number of days
1700 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1701 if (!this.attr_("errorBars")){
1702 if (rollPeriod
== 1) {
1703 return originalData
;
1706 for (var i
= 0; i
< originalData
.length
; i
++) {
1709 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1710 var y
= originalData
[j
][1];
1711 if (y
== null || isNaN(y
)) continue;
1713 sum
+= originalData
[j
][1];
1716 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1718 rollingData
[i
] = [originalData
[i
][0], null];
1723 for (var i
= 0; i
< originalData
.length
; i
++) {
1727 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1728 var y
= originalData
[j
][1][0];
1729 if (y
== null || isNaN(y
)) continue;
1731 sum
+= originalData
[j
][1][0];
1732 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1735 var stddev
= Math
.sqrt(variance
) / num_ok
;
1736 rollingData
[i
] = [originalData
[i
][0],
1737 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1739 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1749 * Parses a date, returning the number of milliseconds since epoch. This can be
1750 * passed in as an xValueParser in the Dygraph constructor.
1751 * TODO(danvk): enumerate formats that this understands.
1752 * @param {String} A date in YYYYMMDD format.
1753 * @return {Number} Milliseconds since epoch.
1756 Dygraph
.dateParser
= function(dateStr
, self
) {
1759 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1760 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1761 while (dateStrSlashed
.search("-") != -1) {
1762 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1764 d
= Date
.parse(dateStrSlashed
);
1765 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1766 // TODO(danvk): remove support for this format. It's confusing.
1767 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1768 + "/" + dateStr
.substr(6,2);
1769 d
= Date
.parse(dateStrSlashed
);
1771 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1772 // "2009/07/12 12:34:56"
1773 d
= Date
.parse(dateStr
);
1776 if (!d
|| isNaN(d
)) {
1777 self
.error("Couldn't parse " + dateStr
+ " as a date");
1783 * Detects the type of the str (date or numeric) and sets the various
1784 * formatting attributes in this.attrs_ based on this type.
1785 * @param {String} str An x value.
1788 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1790 if (str
.indexOf('-') >= 0 ||
1791 str
.indexOf('/') >= 0 ||
1792 isNaN(parseFloat(str
))) {
1794 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1795 // TODO(danvk): remove support for this format.
1800 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1801 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1802 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1804 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1805 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1806 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1811 * Parses a string in a special csv format. We expect a csv file where each
1812 * line is a date point, and the first field in each line is the date string.
1813 * We also expect that all remaining fields represent series.
1814 * if the errorBars attribute is set, then interpret the fields as:
1815 * date, series1, stddev1, series2, stddev2, ...
1816 * @param {Array.<Object>} data See above.
1819 * @return Array.<Object> An array with one entry for each row. These entries
1820 * are an array of cells in that row. The first entry is the parsed x-value for
1821 * the row. The second, third, etc. are the y-values. These can take on one of
1822 * three forms, depending on the CSV and constructor parameters:
1824 * 2. [ value, stddev ]
1825 * 3. [ low value, center value, high value ]
1827 Dygraph
.prototype.parseCSV_
= function(data
) {
1829 var lines
= data
.split("\n");
1831 // Use the default delimiter or fall back to a tab if that makes sense.
1832 var delim
= this.attr_('delimiter');
1833 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1838 if (this.labelsFromCSV_
) {
1840 this.attrs_
.labels
= lines
[0].split(delim
);
1844 var defaultParserSet
= false; // attempt to auto-detect x value type
1845 var expectedCols
= this.attr_("labels").length
;
1846 var outOfOrder
= false;
1847 for (var i
= start
; i
< lines
.length
; i
++) {
1848 var line
= lines
[i
];
1849 if (line
.length
== 0) continue; // skip blank lines
1850 if (line
[0] == '#') continue; // skip comment lines
1851 var inFields
= line
.split(delim
);
1852 if (inFields
.length
< 2) continue;
1855 if (!defaultParserSet
) {
1856 this.detectTypeFromString_(inFields
[0]);
1857 xParser
= this.attr_("xValueParser");
1858 defaultParserSet
= true;
1860 fields
[0] = xParser(inFields
[0], this);
1862 // If fractions are expected, parse the numbers as "A/B
"
1863 if (this.fractions_) {
1864 for (var j = 1; j < inFields.length; j++) {
1865 // TODO(danvk): figure out an appropriate way to flag parse errors.
1866 var vals = inFields[j].split("/");
1867 fields[j] = [parseFloat(vals[0]), parseFloat(vals[1])];
1869 } else if (this.attr_("errorBars
")) {
1870 // If there are error bars, values are (value, stddev) pairs
1871 for (var j = 1; j < inFields.length; j += 2)
1872 fields[(j + 1) / 2] = [parseFloat(inFields[j]),
1873 parseFloat(inFields[j + 1])];
1874 } else if (this.attr_("customBars
")) {
1875 // Bars are a low;center;high tuple
1876 for (var j = 1; j < inFields.length; j++) {
1877 var vals = inFields[j].split(";");
1878 fields[j] = [ parseFloat(vals[0]),
1879 parseFloat(vals[1]),
1880 parseFloat(vals[2]) ];
1883 // Values are just numbers
1884 for (var j = 1; j < inFields.length; j++) {
1885 fields[j] = parseFloat(inFields[j]);
1888 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1893 if (fields.length != expectedCols) {
1894 this.error("Number of columns
in line
" + i + " (" + fields.length +
1895 ") does not agree
with number of
labels (" + expectedCols +
1901 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1902 ret.sort(function(a,b) { return a[0] - b[0] });
1909 * The user has provided their data as a pre-packaged JS array. If the x values
1910 * are numeric, this is the same as dygraphs' internal format. If the x values
1911 * are dates, we need to convert them from Date objects to ms since epoch.
1912 * @param {Array.<Object>} data
1913 * @return {Array.<Object>} data with numeric x values.
1915 Dygraph.prototype.parseArray_ = function(data) {
1916 // Peek at the first x value to see if it's numeric.
1917 if (data.length == 0) {
1918 this.error("Can
't plot empty data set");
1921 if (data[0].length == 0) {
1922 this.error("Data set cannot contain an empty row");
1926 if (this.attr_("labels") == null) {
1927 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1928 "in the options parameter");
1929 this.attrs_.labels = [ "X" ];
1930 for (var i = 1; i < data[0].length; i++) {
1931 this.attrs_.labels.push("Y" + i);
1935 if (Dygraph.isDateLike(data[0][0])) {
1936 // Some intelligent defaults for a date x-axis.
1937 this.attrs_.xValueFormatter = Dygraph.dateString_;
1938 this.attrs_.xTicker = Dygraph.dateTicker;
1940 // Assume they're all dates
.
1941 var parsedData
= Dygraph
.clone(data
);
1942 for (var i
= 0; i
< data
.length
; i
++) {
1943 if (parsedData
[i
].length
== 0) {
1944 this.error("Row " << (1 + i
) << " of data is empty");
1947 if (parsedData
[i
][0] == null
1948 || typeof(parsedData
[i
][0].getTime
) != 'function'
1949 || isNaN(parsedData
[i
][0].getTime())) {
1950 this.error("x value in row " + (1 + i
) + " is not a Date");
1953 parsedData
[i
][0] = parsedData
[i
][0].getTime();
1957 // Some intelligent defaults for a numeric x-axis.
1958 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1959 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1965 * Parses a DataTable object from gviz.
1966 * The data is expected to have a first column that is either a date or a
1967 * number. All subsequent columns must be numbers. If there is a clear mismatch
1968 * between this.xValueParser_ and the type of the first column, it will be
1969 * fixed. Returned value is in the same format as return value of parseCSV_.
1970 * @param {Array.<Object>} data See above.
1973 Dygraph
.prototype.parseDataTable_
= function(data
) {
1974 var cols
= data
.getNumberOfColumns();
1975 var rows
= data
.getNumberOfRows();
1977 // Read column labels
1979 for (var i
= 0; i
< cols
; i
++) {
1980 labels
.push(data
.getColumnLabel(i
));
1981 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
1983 this.attrs_
.labels
= labels
;
1984 cols
= labels
.length
;
1986 var indepType
= data
.getColumnType(0);
1987 if (indepType
== 'date' || indepType
== 'datetime') {
1988 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1989 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1990 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1991 } else if (indepType
== 'number') {
1992 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1993 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1994 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1996 this.error("only 'date', 'datetime' and 'number' types are supported for " +
1997 "column 1 of DataTable input (Got '" + indepType
+ "')");
2002 var outOfOrder
= false;
2003 for (var i
= 0; i
< rows
; i
++) {
2005 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2006 data
.getValue(i
, 0) === null) {
2007 this.warning("Ignoring row " + i
+
2008 " of DataTable because of undefined or null first column.");
2012 if (indepType
== 'date' || indepType
== 'datetime') {
2013 row
.push(data
.getValue(i
, 0).getTime());
2015 row
.push(data
.getValue(i
, 0));
2017 if (!this.attr_("errorBars")) {
2018 for (var j
= 1; j
< cols
; j
++) {
2019 row
.push(data
.getValue(i
, j
));
2022 for (var j
= 0; j
< cols
- 1; j
++) {
2023 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2026 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2033 this.warn("DataTable is out of order; order it correctly to speed loading.");
2034 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2039 // These functions are all based on MochiKit.
2040 Dygraph
.update
= function (self
, o
) {
2041 if (typeof(o
) != 'undefined' && o
!== null) {
2043 if (o
.hasOwnProperty(k
)) {
2051 Dygraph
.isArrayLike
= function (o
) {
2052 var typ
= typeof(o
);
2054 (typ
!= 'object' && !(typ
== 'function' &&
2055 typeof(o
.item
) == 'function')) ||
2057 typeof(o
.length
) != 'number' ||
2065 Dygraph
.isDateLike
= function (o
) {
2066 if (typeof(o
) != "object" || o
=== null ||
2067 typeof(o
.getTime
) != 'function') {
2073 Dygraph
.clone
= function(o
) {
2074 // TODO(danvk): figure out how MochiKit's version works
2076 for (var i
= 0; i
< o
.length
; i
++) {
2077 if (Dygraph
.isArrayLike(o
[i
])) {
2078 r
.push(Dygraph
.clone(o
[i
]));
2088 * Get the CSV data. If it's in a function, call that function. If it's in a
2089 * file, do an XMLHttpRequest to get it.
2092 Dygraph
.prototype.start_
= function() {
2093 if (typeof this.file_
== 'function') {
2094 // CSV string. Pretend we got it via XHR.
2095 this.loadedEvent_(this.file_());
2096 } else if (Dygraph
.isArrayLike(this.file_
)) {
2097 this.rawData_
= this.parseArray_(this.file_
);
2098 this.drawGraph_(this.rawData_
);
2099 } else if (typeof this.file_
== 'object' &&
2100 typeof this.file_
.getColumnRange
== 'function') {
2101 // must be a DataTable from gviz.
2102 this.rawData_
= this.parseDataTable_(this.file_
);
2103 this.drawGraph_(this.rawData_
);
2104 } else if (typeof this.file_
== 'string') {
2105 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2106 if (this.file_
.indexOf('\n') >= 0) {
2107 this.loadedEvent_(this.file_
);
2109 var req
= new XMLHttpRequest();
2111 req
.onreadystatechange
= function () {
2112 if (req
.readyState
== 4) {
2113 if (req
.status
== 200) {
2114 caller
.loadedEvent_(req
.responseText
);
2119 req
.open("GET", this.file_
, true);
2123 this.error("Unknown data format: " + (typeof this.file_
));
2128 * Changes various properties of the graph. These can include:
2130 * <li>file: changes the source data for the graph</li>
2131 * <li>errorBars: changes whether the data contains stddev</li>
2133 * @param {Object} attrs The new properties and values
2135 Dygraph
.prototype.updateOptions
= function(attrs
) {
2136 // TODO(danvk): this is a mess. Rethink this function.
2137 if (attrs
.rollPeriod
) {
2138 this.rollPeriod_
= attrs
.rollPeriod
;
2140 if (attrs
.dateWindow
) {
2141 this.dateWindow_
= attrs
.dateWindow
;
2143 if (attrs
.valueRange
) {
2144 this.valueRange_
= attrs
.valueRange
;
2146 Dygraph
.update(this.user_attrs_
, attrs
);
2148 this.labelsFromCSV_
= (this.attr_("labels") == null);
2150 // TODO(danvk): this doesn't match the constructor logic
2151 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2152 if (attrs
['file'] && attrs
['file'] != this.file_
) {
2153 this.file_
= attrs
['file'];
2156 this.drawGraph_(this.rawData_
);
2161 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2162 * containing div (which has presumably changed size since the dygraph was
2163 * instantiated. If the width/height are specified, the div will be resized.
2165 * This is far more efficient than destroying and re-instantiating a
2166 * Dygraph, since it doesn't have to reparse the underlying data.
2168 * @param {Number} width Width (in pixels)
2169 * @param {Number} height Height (in pixels)
2171 Dygraph
.prototype.resize
= function(width
, height
) {
2172 if ((width
=== null) != (height
=== null)) {
2173 this.warn("Dygraph.resize() should be called with zero parameters or " +
2174 "two non-NULL parameters. Pretending it was zero.");
2175 width
= height
= null;
2178 // TODO(danvk): there should be a clear() method.
2179 this.maindiv_
.innerHTML
= "";
2180 this.attrs_
.labelsDiv
= null;
2183 this.maindiv_
.style
.width
= width
+ "px";
2184 this.maindiv_
.style
.height
= height
+ "px";
2185 this.width_
= width
;
2186 this.height_
= height
;
2188 this.width_
= this.maindiv_
.offsetWidth
;
2189 this.height_
= this.maindiv_
.offsetHeight
;
2192 this.createInterface_();
2193 this.drawGraph_(this.rawData_
);
2197 * Adjusts the number of days in the rolling average. Updates the graph to
2198 * reflect the new averaging period.
2199 * @param {Number} length Number of days over which to average the data.
2201 Dygraph
.prototype.adjustRoll
= function(length
) {
2202 this.rollPeriod_
= length
;
2203 this.drawGraph_(this.rawData_
);
2207 * Returns a boolean array of visibility statuses.
2209 Dygraph
.prototype.visibility
= function() {
2210 // Do lazy-initialization, so that this happens after we know the number of
2212 if (!this.attr_("visibility")) {
2213 this.attrs_
["visibility"] = [];
2215 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2216 this.attr_("visibility").push(true);
2218 return this.attr_("visibility");
2222 * Changes the visiblity of a series.
2224 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2225 var x
= this.visibility();
2226 if (num
< 0 && num
>= x
.length
) {
2227 this.warn("invalid series number in setVisibility: " + num
);
2230 this.drawGraph_(this.rawData_
);
2235 * Create a new canvas element. This is more complex than a simple
2236 * document.createElement("canvas") because of IE and excanvas.
2238 Dygraph
.createCanvas
= function() {
2239 var canvas
= document
.createElement("canvas");
2241 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2243 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2251 * A wrapper around Dygraph that implements the gviz API.
2252 * @param {Object} container The DOM object the visualization should live in.
2254 Dygraph
.GVizChart
= function(container
) {
2255 this.container
= container
;
2258 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2259 this.container
.innerHTML
= '';
2260 this.date_graph
= new Dygraph(this.container
, data
, options
);
2264 * Google charts compatible setSelection
2265 * Only row selection is supported, all points in the
2266 * row will be highlighted
2267 * @param {Array} array of the selected cells
2270 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2272 if (selection_array
.length
) {
2273 row
= selection_array
[0].row
;
2275 this.date_graph
.setSelection(row
);
2279 * Google charts compatible getSelection implementation
2280 * @return {Array} array of the selected cells
2283 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2286 var row
= this.date_graph
.getSelection();
2288 if (row
< 0) return selection
;
2291 for (var i
in this.date_graph
.layout_
.datasets
) {
2292 selection
.push({row
: row
, column
: col
});
2299 // Older pages may still use this name.
2300 DateGraph
= Dygraph
;