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,
93 labelsShowZeroValues
: true,
96 showLabelsOnHighlight
: true,
98 yValueFormatter
: function(x
) { return Dygraph
.round_(x
, 2); },
103 axisLabelFontSize
: 14,
106 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
110 xValueFormatter
: Dygraph
.dateString_
,
111 xValueParser
: Dygraph
.dateParser
,
112 xTicker
: Dygraph
.dateTicker
,
120 wilsonInterval
: true, // only relevant if fractions is true
124 connectSeparatedPoints
: false,
127 hideOverlayOnMouseOut
: true,
132 // Various logging levels.
138 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
139 // Labels is no longer a constructor parameter, since it's typically set
140 // directly from the data source. It also conains a name for the x-axis,
141 // which the previous constructor form did not.
142 if (labels
!= null) {
143 var new_labels
= ["Date"];
144 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
145 Dygraph
.update(attrs
, { 'labels': new_labels
});
147 this.__init__(div
, file
, attrs
);
151 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
152 * and interaction <canvas> inside of it. See the constructor for details
154 * @param {Element} div the Element to render the graph into.
155 * @param {String | Function} file Source data
156 * @param {Object} attrs Miscellaneous other options
159 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
160 // Support two-argument constructor
161 if (attrs
== null) { attrs
= {}; }
163 // Copy the important bits into the object
164 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
167 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
168 this.previousVerticalX_
= -1;
169 this.fractions_
= attrs
.fractions
|| false;
170 this.dateWindow_
= attrs
.dateWindow
|| null;
171 this.valueRange_
= attrs
.valueRange
|| null;
172 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
173 this.is_initial_draw_
= true;
175 // Clear the div. This ensure that, if multiple dygraphs are passed the same
176 // div, then only one will be drawn.
179 // If the div isn't already sized then inherit from our attrs or
180 // give it a default size.
181 if (div
.style
.width
== '') {
182 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
184 if (div
.style
.height
== '') {
185 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
187 this.width_
= parseInt(div
.style
.width
, 10);
188 this.height_
= parseInt(div
.style
.height
, 10);
189 // The div might have been specified as percent of the current window size,
190 // convert that to an appropriate number of pixels.
191 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
192 this.width_
= div
.offsetWidth
;
194 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
195 this.height_
= div
.offsetHeight
;
198 if (this.width_
== 0) {
199 this.error("dygraph has zero width. Please specify a width in pixels.");
201 if (this.height_
== 0) {
202 this.error("dygraph has zero height. Please specify a height in pixels.");
205 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
206 if (attrs
['stackedGraph']) {
207 attrs
['fillGraph'] = true;
208 // TODO(nikhilk): Add any other stackedGraph checks here.
211 // Dygraphs has many options, some of which interact with one another.
212 // To keep track of everything, we maintain two sets of options:
214 // this.user_attrs_ only options explicitly set by the user.
215 // this.attrs_ defaults, options derived from user_attrs_, data.
217 // Options are then accessed this.attr_('attr'), which first looks at
218 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
219 // defaults without overriding behavior that the user specifically asks for.
220 this.user_attrs_
= {};
221 Dygraph
.update(this.user_attrs_
, attrs
);
224 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
226 this.boundaryIds_
= [];
228 // Make a note of whether labels will be pulled from the CSV file.
229 this.labelsFromCSV_
= (this.attr_("labels") == null);
231 // Create the containing DIV and other interactive elements
232 this.createInterface_();
237 Dygraph
.prototype.attr_
= function(name
) {
238 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
239 return this.user_attrs_
[name
];
240 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
241 return this.attrs_
[name
];
247 // TODO(danvk): any way I can get the line numbers to be this.warn call?
248 Dygraph
.prototype.log
= function(severity
, message
) {
249 if (typeof(console
) != 'undefined') {
252 console
.debug('dygraphs: ' + message
);
255 console
.info('dygraphs: ' + message
);
257 case Dygraph
.WARNING
:
258 console
.warn('dygraphs: ' + message
);
261 console
.error('dygraphs: ' + message
);
266 Dygraph
.prototype.info
= function(message
) {
267 this.log(Dygraph
.INFO
, message
);
269 Dygraph
.prototype.warn
= function(message
) {
270 this.log(Dygraph
.WARNING
, message
);
272 Dygraph
.prototype.error
= function(message
) {
273 this.log(Dygraph
.ERROR
, message
);
277 * Returns the current rolling period, as set by the user or an option.
278 * @return {Number} The number of days in the rolling window
280 Dygraph
.prototype.rollPeriod
= function() {
281 return this.rollPeriod_
;
285 * Returns the currently-visible x-range. This can be affected by zooming,
286 * panning or a call to updateOptions.
287 * Returns a two-element array: [left, right].
288 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
290 Dygraph
.prototype.xAxisRange
= function() {
291 if (this.dateWindow_
) return this.dateWindow_
;
293 // The entire chart is visible.
294 var left
= this.rawData_
[0][0];
295 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
296 return [left
, right
];
300 * Returns the currently-visible y-range. This can be affected by zooming,
301 * panning or a call to updateOptions.
302 * Returns a two-element array: [bottom, top].
304 Dygraph
.prototype.yAxisRange
= function() {
305 return this.displayedYRange_
;
309 * Convert from data coordinates to canvas/div X/Y coordinates.
310 * Returns a two-element array: [X, Y]
312 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
313 var ret
= [null, null];
314 var area
= this.plotter_
.area
;
316 var xRange
= this.xAxisRange();
317 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
321 var yRange
= this.yAxisRange();
322 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
328 // TODO(danvk): use these functions throughout dygraphs.
330 * Convert from canvas/div coords to data coordinates.
331 * Returns a two-element array: [X, Y]
333 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
334 var ret
= [null, null];
335 var area
= this.plotter_
.area
;
337 var xRange
= this.xAxisRange();
338 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
342 var yRange
= this.yAxisRange();
343 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
349 Dygraph
.addEvent
= function(el
, evt
, fn
) {
350 var normed_fn
= function(e
) {
351 if (!e
) var e
= window
.event
;
354 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
355 el
.addEventListener(evt
, normed_fn
, false);
357 el
.attachEvent('on' + evt
, normed_fn
);
361 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
362 var ctx
= cnv
.getContext("2d");
364 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
369 * Generates interface elements for the Dygraph: a containing div, a div to
370 * display the current point, and a textbox to adjust the rolling average
371 * period. Also creates the Renderer/Layout elements.
374 Dygraph
.prototype.createInterface_
= function() {
375 // Create the all-enclosing graph div
376 var enclosing
= this.maindiv_
;
378 this.graphDiv
= document
.createElement("div");
379 this.graphDiv
.style
.width
= this.width_
+ "px";
380 this.graphDiv
.style
.height
= this.height_
+ "px";
381 enclosing
.appendChild(this.graphDiv
);
385 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
387 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
388 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
389 - 2 * this.attr_("axisTickSize");
390 this.clippingArea_
= clip
;
392 // Create the canvas for interactive parts of the chart.
393 this.canvas_
= Dygraph
.createCanvas();
394 this.canvas_
.style
.position
= "absolute";
395 this.canvas_
.width
= this.width_
;
396 this.canvas_
.height
= this.height_
;
397 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
398 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
400 // ... and for static parts of the chart.
401 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
403 // The interactive parts of the graph are drawn on top of the chart.
404 this.graphDiv
.appendChild(this.hidden_
);
405 this.graphDiv
.appendChild(this.canvas_
);
406 this.mouseEventElement_
= this.canvas_
;
408 // Make sure we don't overdraw.
409 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
410 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
413 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
414 dygraph
.mouseMove_(e
);
416 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
417 dygraph
.mouseOut_(e
);
420 // Create the grapher
421 // TODO(danvk): why does the Layout need its own set of options?
422 this.layoutOptions_
= { 'xOriginIsZero': false };
423 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
424 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
425 Dygraph
.update(this.layoutOptions_
, {
426 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
428 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
430 // TODO(danvk): why does the Renderer need its own set of options?
431 this.renderOptions_
= { colorScheme
: this.colors_
,
433 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
434 Dygraph
.update(this.renderOptions_
, this.attrs_
);
435 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
436 this.plotter_
= new DygraphCanvasRenderer(this,
437 this.hidden_
, this.layout_
,
438 this.renderOptions_
);
440 this.createStatusMessage_();
441 this.createRollInterface_();
442 this.createDragInterface_();
446 * Detach DOM elements in the dygraph and null out all data references.
447 * Calling this when you're done with a dygraph can dramatically reduce memory
448 * usage. See, e.g., the tests/perf.html example.
450 Dygraph
.prototype.destroy
= function() {
451 var removeRecursive
= function(node
) {
452 while (node
.hasChildNodes()) {
453 removeRecursive(node
.firstChild
);
454 node
.removeChild(node
.firstChild
);
457 removeRecursive(this.maindiv_
);
459 var nullOut
= function(obj
) {
461 if (typeof(obj
[n
]) === 'object') {
467 // These may not all be necessary, but it can't hurt...
468 nullOut(this.layout_
);
469 nullOut(this.plotter_
);
474 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
475 * this particular canvas. All Dygraph work is done on this.canvas_.
476 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
477 * @return {Object} The newly-created canvas
480 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
481 var h
= Dygraph
.createCanvas();
482 h
.style
.position
= "absolute";
483 // TODO(danvk): h should be offset from canvas. canvas needs to include
484 // some extra area to make it easier to zoom in on the far left and far
485 // right. h needs to be precisely the plot area, so that clipping occurs.
486 h
.style
.top
= canvas
.style
.top
;
487 h
.style
.left
= canvas
.style
.left
;
488 h
.width
= this.width_
;
489 h
.height
= this.height_
;
490 h
.style
.width
= this.width_
+ "px"; // for IE
491 h
.style
.height
= this.height_
+ "px"; // for IE
495 // Taken from MochiKit.Color
496 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
500 if (saturation
=== 0) {
505 var i
= Math
.floor(hue
* 6);
506 var f
= (hue
* 6) - i
;
507 var p
= value
* (1 - saturation
);
508 var q
= value
* (1 - (saturation
* f
));
509 var t
= value
* (1 - (saturation
* (1 - f
)));
511 case 1: red
= q
; green
= value
; blue
= p
; break;
512 case 2: red
= p
; green
= value
; blue
= t
; break;
513 case 3: red
= p
; green
= q
; blue
= value
; break;
514 case 4: red
= t
; green
= p
; blue
= value
; break;
515 case 5: red
= value
; green
= p
; blue
= q
; break;
516 case 6: // fall through
517 case 0: red
= value
; green
= t
; blue
= p
; break;
520 red
= Math
.floor(255 * red
+ 0.5);
521 green
= Math
.floor(255 * green
+ 0.5);
522 blue
= Math
.floor(255 * blue
+ 0.5);
523 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
528 * Generate a set of distinct colors for the data series. This is done with a
529 * color wheel. Saturation/Value are customizable, and the hue is
530 * equally-spaced around the color wheel. If a custom set of colors is
531 * specified, that is used instead.
534 Dygraph
.prototype.setColors_
= function() {
535 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
536 // away with this.renderOptions_.
537 var num
= this.attr_("labels").length
- 1;
539 var colors
= this.attr_('colors');
541 var sat
= this.attr_('colorSaturation') || 1.0;
542 var val
= this.attr_('colorValue') || 0.5;
543 var half
= Math
.ceil(num
/ 2);
544 for (var i
= 1; i
<= num
; i
++) {
545 if (!this.visibility()[i
-1]) continue;
546 // alternate colors for high contrast.
547 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
548 var hue
= (1.0 * idx
/ (1 + num
));
549 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
552 for (var i
= 0; i
< num
; i
++) {
553 if (!this.visibility()[i
]) continue;
554 var colorStr
= colors
[i
% colors
.length
];
555 this.colors_
.push(colorStr
);
559 // TODO(danvk): update this w/r
/t/ the
new options system
.
560 this.renderOptions_
.colorScheme
= this.colors_
;
561 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
562 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
563 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
567 * Return the list of colors. This is either the list of colors passed in the
568 * attributes, or the autogenerated list of rgb(r,g,b) strings.
569 * @return {Array<string>} The list of colors.
571 Dygraph
.prototype.getColors
= function() {
575 // The following functions are from quirksmode.org with a modification for Safari from
576 // http://blog.firetree.net/2005/07/04/javascript-find-position/
577 // http://www.quirksmode.org/js
/findpos
.html
578 Dygraph
.findPosX
= function(obj
) {
583 curleft
+= obj
.offsetLeft
;
584 if(!obj
.offsetParent
)
586 obj
= obj
.offsetParent
;
593 Dygraph
.findPosY
= function(obj
) {
598 curtop
+= obj
.offsetTop
;
599 if(!obj
.offsetParent
)
601 obj
= obj
.offsetParent
;
611 * Create the div that contains information on the selected point(s)
612 * This goes in the top right of the canvas, unless an external div has already
616 Dygraph
.prototype.createStatusMessage_
= function() {
617 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
618 if (userLabelsDiv
&& null != userLabelsDiv
619 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
620 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
622 if (!this.attr_("labelsDiv")) {
623 var divWidth
= this.attr_('labelsDivWidth');
625 "position": "absolute",
628 "width": divWidth
+ "px",
630 "left": (this.width_
- divWidth
- 2) + "px",
631 "background": "white",
633 "overflow": "hidden"};
634 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
635 var div
= document
.createElement("div");
636 for (var name
in messagestyle
) {
637 if (messagestyle
.hasOwnProperty(name
)) {
638 div
.style
[name
] = messagestyle
[name
];
641 this.graphDiv
.appendChild(div
);
642 this.attrs_
.labelsDiv
= div
;
647 * Create the text box to adjust the averaging period
648 * @return {Object} The newly-created text box
651 Dygraph
.prototype.createRollInterface_
= function() {
652 var display
= this.attr_('showRoller') ? "block" : "none";
653 var textAttr
= { "position": "absolute",
655 "top": (this.plotter_
.area
.h
- 25) + "px",
656 "left": (this.plotter_
.area
.x
+ 1) + "px",
659 var roller
= document
.createElement("input");
660 roller
.type
= "text";
662 roller
.value
= this.rollPeriod_
;
663 for (var name
in textAttr
) {
664 if (textAttr
.hasOwnProperty(name
)) {
665 roller
.style
[name
] = textAttr
[name
];
669 var pa
= this.graphDiv
;
670 pa
.appendChild(roller
);
672 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
676 // These functions are taken from MochiKit.Signal
677 Dygraph
.pageX
= function(e
) {
679 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
682 var b
= document
.body
;
684 (de
.scrollLeft
|| b
.scrollLeft
) -
685 (de
.clientLeft
|| 0);
689 Dygraph
.pageY
= function(e
) {
691 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
694 var b
= document
.body
;
696 (de
.scrollTop
|| b
.scrollTop
) -
702 * Set up all the mouse handlers needed to capture dragging behavior for zoom
706 Dygraph
.prototype.createDragInterface_
= function() {
709 // Tracks whether the mouse is down right now
710 var isZooming
= false;
711 var isPanning
= false;
712 var dragStartX
= null;
713 var dragStartY
= null;
717 var draggingDate
= null;
718 var dateRange
= null;
720 // Utility function to convert page-wide coordinates to canvas coords
723 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
724 var getY
= function(e
) { return Dygraph
.pageX(e
) - py
};
726 // Draw zoom rectangles when the mouse is down and the user moves around
727 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
729 dragEndX
= getX(event
);
730 dragEndY
= getY(event
);
732 self
.drawZoomRect_(dragStartX
, dragEndX
, prevEndX
);
734 } else if (isPanning
) {
735 dragEndX
= getX(event
);
736 dragEndY
= getY(event
);
738 // Want to have it so that:
739 // 1. draggingDate appears at dragEndX
740 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
742 self
.dateWindow_
[0] = draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
743 self
.dateWindow_
[1] = self
.dateWindow_
[0] + dateRange
;
744 self
.drawGraph_(self
.rawData_
);
748 // Track the beginning of drag events
749 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
750 px
= Dygraph
.findPosX(self
.canvas_
);
751 py
= Dygraph
.findPosY(self
.canvas_
);
752 dragStartX
= getX(event
);
753 dragStartY
= getY(event
);
755 if (event
.altKey
|| event
.shiftKey
) {
756 if (!self
.dateWindow_
) return; // have to be zoomed in to pan.
758 dateRange
= self
.dateWindow_
[1] - self
.dateWindow_
[0];
759 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
766 // If the user releases the mouse button during a drag, but not over the
767 // canvas, then it doesn't count as a zooming action.
768 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
769 if (isZooming
|| isPanning
) {
782 // Temporarily cancel the dragging event when the mouse leaves the graph
783 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
790 // If the mouse is released on the canvas during a drag event, then it's a
791 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
792 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
795 dragEndX
= getX(event
);
796 dragEndY
= getY(event
);
797 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
798 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
800 if (regionWidth
< 2 && regionHeight
< 2 &&
801 self
.attr_('clickCallback') != null &&
802 self
.lastx_
!= undefined
) {
803 // TODO(danvk): pass along more info about the points.
804 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
807 if (regionWidth
>= 10) {
808 self
.doZoom_(Math
.min(dragStartX
, dragEndX
),
809 Math
.max(dragStartX
, dragEndX
));
811 self
.canvas_
.getContext("2d").clearRect(0, 0,
813 self
.canvas_
.height
);
827 // Double-clicking zooms back out
828 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
829 if (self
.dateWindow_
== null) return;
830 self
.dateWindow_
= null;
831 self
.drawGraph_(self
.rawData_
);
832 var minDate
= self
.rawData_
[0][0];
833 var maxDate
= self
.rawData_
[self
.rawData_
.length
- 1][0];
834 if (self
.attr_("zoomCallback")) {
835 self
.attr_("zoomCallback")(minDate
, maxDate
);
841 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
842 * up any previous zoom rectangles that were drawn. This could be optimized to
843 * avoid extra redrawing, but it's tricky to avoid interactions with the status
845 * @param {Number} startX The X position where the drag started, in canvas
847 * @param {Number} endX The current X position of the drag, in canvas coords.
848 * @param {Number} prevEndX The value of endX on the previous call to this
849 * function. Used to avoid excess redrawing
852 Dygraph
.prototype.drawZoomRect_
= function(startX
, endX
, prevEndX
) {
853 var ctx
= this.canvas_
.getContext("2d");
855 // Clean up from the previous rect if necessary
857 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
858 Math
.abs(startX
- prevEndX
), this.height_
);
861 // Draw a light-grey rectangle to show the new viewing area
862 if (endX
&& startX
) {
863 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
864 ctx
.fillRect(Math
.min(startX
, endX
), 0,
865 Math
.abs(endX
- startX
), this.height_
);
870 * Zoom to something containing [lowX, highX]. These are pixel coordinates
871 * in the canvas. The exact zoom window may be slightly larger if there are no
872 * data points near lowX or highX. This function redraws the graph.
873 * @param {Number} lowX The leftmost pixel value that should be visible.
874 * @param {Number} highX The rightmost pixel value that should be visible.
877 Dygraph
.prototype.doZoom_
= function(lowX
, highX
) {
878 // Find the earliest and latest dates contained in this canvasx range.
879 var r
= this.toDataCoords(lowX
, null);
881 r
= this.toDataCoords(highX
, null);
884 this.dateWindow_
= [minDate
, maxDate
];
885 this.drawGraph_(this.rawData_
);
886 if (this.attr_("zoomCallback")) {
887 this.attr_("zoomCallback")(minDate
, maxDate
);
892 * When the mouse moves in the canvas, display information about a nearby data
893 * point and draw dots over those points in the data series. This function
894 * takes care of cleanup of previously-drawn dots.
895 * @param {Object} event The mousemove event from the browser.
898 Dygraph
.prototype.mouseMove_
= function(event
) {
899 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
900 var points
= this.layout_
.points
;
905 // Loop through all the points and find the date nearest to our current
907 var minDist
= 1e+100;
909 for (var i
= 0; i
< points
.length
; i
++) {
910 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
911 if (dist
> minDist
) continue;
915 if (idx
>= 0) lastx
= points
[idx
].xval
;
916 // Check that you can really highlight the last day's data
917 if (canvasx
> points
[points
.length
-1].canvasx
)
918 lastx
= points
[points
.length
-1].xval
;
920 // Extract the points we've selected
921 this.selPoints_
= [];
922 var l
= points
.length
;
923 if (!this.attr_("stackedGraph")) {
924 for (var i
= 0; i
< l
; i
++) {
925 if (points
[i
].xval
== lastx
) {
926 this.selPoints_
.push(points
[i
]);
930 // Need to 'unstack' points starting from the bottom
931 var cumulative_sum
= 0;
932 for (var i
= l
- 1; i
>= 0; i
--) {
933 if (points
[i
].xval
== lastx
) {
934 var p
= {}; // Clone the point since we modify it
935 for (var k
in points
[i
]) {
938 p
.yval
-= cumulative_sum
;
939 cumulative_sum
+= p
.yval
;
940 this.selPoints_
.push(p
);
943 this.selPoints_
.reverse();
946 if (this.attr_("highlightCallback")) {
947 var px
= this.lastx_
;
948 if (px
!== null && lastx
!= px
) {
949 // only fire if the selected point has changed.
950 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
954 // Save last x position for callbacks.
957 this.updateSelection_();
961 * Draw dots over the selectied points in the data series. This function
962 * takes care of cleanup of previously-drawn dots.
965 Dygraph
.prototype.updateSelection_
= function() {
966 // Clear the previously drawn vertical, if there is one
967 var circleSize
= this.attr_('highlightCircleSize');
968 var ctx
= this.canvas_
.getContext("2d");
969 if (this.previousVerticalX_
>= 0) {
970 var px
= this.previousVerticalX_
;
971 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
974 var isOK
= function(x
) { return x
&& !isNaN(x
); };
976 if (this.selPoints_
.length
> 0) {
977 var canvasx
= this.selPoints_
[0].canvasx
;
979 // Set the status message to indicate the selected point(s)
980 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
981 var fmtFunc
= this.attr_('yValueFormatter');
982 var clen
= this.colors_
.length
;
984 if (this.attr_('showLabelsOnHighlight')) {
985 // Set the status message to indicate the selected point(s)
986 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
987 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
988 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
989 if (this.attr_("labelsSeparateLines")) {
992 var point
= this.selPoints_
[i
];
993 var c
= new RGBColor(this.colors_
[i
%clen
]);
994 var yval
= fmtFunc(point
.yval
);
995 replace
+= " <b><font color='" + c
.toHex() + "'>"
996 + point
.name
+ "</font></b>:"
1000 this.attr_("labelsDiv").innerHTML
= replace
;
1003 // Draw colored circles over the center of each selected point
1005 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1006 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1008 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1009 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1010 0, 2 * Math
.PI
, false);
1015 this.previousVerticalX_
= canvasx
;
1020 * Set manually set selected dots, and display information about them
1021 * @param int row number that should by highlighted
1022 * false value clears the selection
1025 Dygraph
.prototype.setSelection
= function(row
) {
1026 // Extract the points we've selected
1027 this.selPoints_
= [];
1030 if (row
!== false) {
1031 row
= row
-this.boundaryIds_
[0][0];
1034 if (row
!== false && row
>= 0) {
1035 for (var i
in this.layout_
.datasets
) {
1036 if (row
< this.layout_
.datasets
[i
].length
) {
1037 this.selPoints_
.push(this.layout_
.points
[pos
+row
]);
1039 pos
+= this.layout_
.datasets
[i
].length
;
1043 if (this.selPoints_
.length
) {
1044 this.lastx_
= this.selPoints_
[0].xval
;
1045 this.updateSelection_();
1048 this.clearSelection();
1054 * The mouse has left the canvas. Clear out whatever artifacts remain
1055 * @param {Object} event the mouseout event from the browser.
1058 Dygraph
.prototype.mouseOut_
= function(event
) {
1059 if (this.attr_("unhighlightCallback")) {
1060 this.attr_("unhighlightCallback")(event
);
1063 if (this.attr_("hideOverlayOnMouseOut")) {
1064 this.clearSelection();
1069 * Remove all selection from the canvas
1072 Dygraph
.prototype.clearSelection
= function() {
1073 // Get rid of the overlay data
1074 var ctx
= this.canvas_
.getContext("2d");
1075 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1076 this.attr_("labelsDiv").innerHTML
= "";
1077 this.selPoints_
= [];
1082 * Returns the number of the currently selected row
1083 * @return int row number, of -1 if nothing is selected
1086 Dygraph
.prototype.getSelection
= function() {
1087 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1091 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1092 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1093 return row
+ this.boundaryIds_
[0][0];
1099 Dygraph
.zeropad
= function(x
) {
1100 if (x
< 10) return "0" + x
; else return "" + x
;
1104 * Return a string version of the hours, minutes and seconds portion of a date.
1105 * @param {Number} date The JavaScript date (ms since epoch)
1106 * @return {String} A time of the form "HH:MM:SS"
1109 Dygraph
.hmsString_
= function(date
) {
1110 var zeropad
= Dygraph
.zeropad
;
1111 var d
= new Date(date
);
1112 if (d
.getSeconds()) {
1113 return zeropad(d
.getHours()) + ":" +
1114 zeropad(d
.getMinutes()) + ":" +
1115 zeropad(d
.getSeconds());
1117 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1122 * Convert a JS date to a string appropriate to display on an axis that
1123 * is displaying values at the stated granularity.
1124 * @param {Date} date The date to format
1125 * @param {Number} granularity One of the Dygraph granularity constants
1126 * @return {String} The formatted date
1129 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1130 if (granularity
>= Dygraph
.MONTHLY
) {
1131 return date
.strftime('%b %y');
1133 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1134 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1135 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1137 return Dygraph
.hmsString_(date
.getTime());
1143 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1144 * @param {Number} date The JavaScript date (ms since epoch)
1145 * @return {String} A date of the form "YYYY/MM/DD"
1148 Dygraph
.dateString_
= function(date
, self
) {
1149 var zeropad
= Dygraph
.zeropad
;
1150 var d
= new Date(date
);
1153 var year
= "" + d
.getFullYear();
1154 // Get a 0 padded month string
1155 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1156 // Get a 0 padded day string
1157 var day
= zeropad(d
.getDate());
1160 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1161 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1163 return year
+ "/" + month + "/" + day
+ ret
;
1167 * Round a number to the specified number of digits past the decimal point.
1168 * @param {Number} num The number to round
1169 * @param {Number} places The number of decimals to which to round
1170 * @return {Number} The rounded number
1173 Dygraph
.round_
= function(num
, places
) {
1174 var shift
= Math
.pow(10, places
);
1175 return Math
.round(num
* shift
)/shift
;
1179 * Fires when there's data available to be graphed.
1180 * @param {String} data Raw CSV data to be plotted
1183 Dygraph
.prototype.loadedEvent_
= function(data
) {
1184 this.rawData_
= this.parseCSV_(data
);
1185 this.drawGraph_(this.rawData_
);
1188 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1189 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1190 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1193 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1196 Dygraph
.prototype.addXTicks_
= function() {
1197 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1198 var startDate
, endDate
;
1199 if (this.dateWindow_
) {
1200 startDate
= this.dateWindow_
[0];
1201 endDate
= this.dateWindow_
[1];
1203 startDate
= this.rawData_
[0][0];
1204 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1207 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1208 this.layout_
.updateOptions({xTicks
: xTicks
});
1211 // Time granularity enumeration
1212 Dygraph
.SECONDLY
= 0;
1213 Dygraph
.TWO_SECONDLY
= 1;
1214 Dygraph
.FIVE_SECONDLY
= 2;
1215 Dygraph
.TEN_SECONDLY
= 3;
1216 Dygraph
.THIRTY_SECONDLY
= 4;
1217 Dygraph
.MINUTELY
= 5;
1218 Dygraph
.TWO_MINUTELY
= 6;
1219 Dygraph
.FIVE_MINUTELY
= 7;
1220 Dygraph
.TEN_MINUTELY
= 8;
1221 Dygraph
.THIRTY_MINUTELY
= 9;
1222 Dygraph
.HOURLY
= 10;
1223 Dygraph
.TWO_HOURLY
= 11;
1224 Dygraph
.SIX_HOURLY
= 12;
1226 Dygraph
.WEEKLY
= 14;
1227 Dygraph
.MONTHLY
= 15;
1228 Dygraph
.QUARTERLY
= 16;
1229 Dygraph
.BIANNUAL
= 17;
1230 Dygraph
.ANNUAL
= 18;
1231 Dygraph
.DECADAL
= 19;
1232 Dygraph
.NUM_GRANULARITIES
= 20;
1234 Dygraph
.SHORT_SPACINGS
= [];
1235 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1236 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1237 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1238 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1239 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1240 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1241 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1242 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1243 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1244 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1245 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1246 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1247 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1248 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1249 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1253 // If we used this time granularity, how many ticks would there be?
1254 // This is only an approximation, but it's generally good enough.
1256 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1257 if (granularity
< Dygraph
.MONTHLY
) {
1258 // Generate one tick mark for every fixed interval of time.
1259 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1260 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1262 var year_mod
= 1; // e.g. to only print one point every 10 years.
1263 var num_months
= 12;
1264 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1265 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1266 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1267 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1269 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1270 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1271 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1277 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1278 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1280 // Returns an array containing {v: millis, label: label} dictionaries.
1282 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1283 var formatter
= this.attr_("xAxisLabelFormatter");
1285 if (granularity
< Dygraph
.MONTHLY
) {
1286 // Generate one tick mark for every fixed interval of time.
1287 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1288 var format
= '%d%b'; // e.g. "1Jan"
1290 // Find a time less than start_time which occurs on a "nice" time boundary
1291 // for this granularity.
1292 var g
= spacing
/ 1000;
1293 var d
= new Date(start_time
);
1294 if (g
<= 60) { // seconds
1295 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1299 if (g
<= 60) { // minutes
1300 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1305 if (g
<= 24) { // days
1306 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1311 if (g
== 7) { // one week
1312 d
.setDate(d
.getDate() - d
.getDay());
1317 start_time
= d
.getTime();
1319 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1320 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1323 // Display a tick mark on the first of a set of months of each year.
1324 // Years get a tick mark iff y % year_mod == 0. This is useful for
1325 // displaying a tick mark once every 10 years, say, on long time scales.
1327 var year_mod
= 1; // e.g. to only print one point every 10 years.
1329 if (granularity
== Dygraph
.MONTHLY
) {
1330 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1331 } else if (granularity
== Dygraph
.QUARTERLY
) {
1332 months
= [ 0, 3, 6, 9 ];
1333 } else if (granularity
== Dygraph
.BIANNUAL
) {
1335 } else if (granularity
== Dygraph
.ANNUAL
) {
1337 } else if (granularity
== Dygraph
.DECADAL
) {
1342 var start_year
= new Date(start_time
).getFullYear();
1343 var end_year
= new Date(end_time
).getFullYear();
1344 var zeropad
= Dygraph
.zeropad
;
1345 for (var i
= start_year
; i
<= end_year
; i
++) {
1346 if (i
% year_mod
!= 0) continue;
1347 for (var j
= 0; j
< months
.length
; j
++) {
1348 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1349 var t
= Date
.parse(date_str
);
1350 if (t
< start_time
|| t
> end_time
) continue;
1351 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1361 * Add ticks to the x-axis based on a date range.
1362 * @param {Number} startDate Start of the date window (millis since epoch)
1363 * @param {Number} endDate End of the date window (millis since epoch)
1364 * @return {Array.<Object>} Array of {label, value} tuples.
1367 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1369 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1370 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1371 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1378 return self
.GetXAxis(startDate
, endDate
, chosen
);
1380 // TODO(danvk): signal error.
1385 * Add ticks when the x axis has numbers on it (instead of dates)
1386 * @param {Number} startDate Start of the date window (millis since epoch)
1387 * @param {Number} endDate End of the date window (millis since epoch)
1388 * @return {Array.<Object>} Array of {label, value} tuples.
1391 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1393 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1394 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1395 // The first spacing greater than pixelsPerYLabel is what we use.
1396 // TODO(danvk): version that works on a log scale.
1397 if (self
.attr_("labelsKMG2")) {
1398 var mults
= [1, 2, 4, 8];
1400 var mults
= [1, 2, 5];
1402 var scale
, low_val
, high_val
, nTicks
;
1403 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1404 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1405 for (var i
= -10; i
< 50; i
++) {
1406 if (self
.attr_("labelsKMG2")) {
1407 var base_scale
= Math
.pow(16, i
);
1409 var base_scale
= Math
.pow(10, i
);
1411 for (var j
= 0; j
< mults
.length
; j
++) {
1412 scale
= base_scale
* mults
[j
];
1413 low_val
= Math
.floor(minV
/ scale
) * scale
;
1414 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1415 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1416 var spacing
= self
.height_
/ nTicks
;
1417 // wish I could break out of both loops at once...
1418 if (spacing
> pixelsPerTick
) break;
1420 if (spacing
> pixelsPerTick
) break;
1423 // Construct labels for the ticks
1427 if (self
.attr_("labelsKMB")) {
1429 k_labels
= [ "K", "M", "B", "T" ];
1431 if (self
.attr_("labelsKMG2")) {
1432 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1434 k_labels
= [ "k", "M", "G", "T" ];
1437 // Allow reverse y-axis if it's explicitly requested.
1438 if (low_val
> high_val
) scale
*= -1;
1440 for (var i
= 0; i
< nTicks
; i
++) {
1441 var tickV
= low_val
+ i
* scale
;
1442 var absTickV
= Math
.abs(tickV
);
1443 var label
= Dygraph
.round_(tickV
, 2);
1444 if (k_labels
.length
) {
1445 // Round up to an appropriate unit.
1447 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1448 if (absTickV
>= n
) {
1449 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1454 ticks
.push( {label
: label
, v
: tickV
} );
1460 * Adds appropriate ticks on the y-axis
1461 * @param {Number} minY The minimum Y value in the data set
1462 * @param {Number} maxY The maximum Y value in the data set
1465 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1466 // Set the number of ticks so that the labels are human-friendly.
1467 // TODO(danvk): make this an attribute as well.
1468 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1469 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1473 // Computes the range of the data series (including confidence intervals).
1474 // series is either [ [x1, y1], [x2, y2], ... ] or
1475 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1476 // Returns [low, high]
1477 Dygraph
.prototype.extremeValues_
= function(series
) {
1478 var minY
= null, maxY
= null;
1480 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1482 // With custom bars, maxY is the max of the high values.
1483 for (var j
= 0; j
< series
.length
; j
++) {
1484 var y
= series
[j
][1][0];
1486 var low
= y
- series
[j
][1][1];
1487 var high
= y
+ series
[j
][1][2];
1488 if (low
> y
) low
= y
; // this can happen with custom bars,
1489 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1490 if (maxY
== null || high
> maxY
) {
1493 if (minY
== null || low
< minY
) {
1498 for (var j
= 0; j
< series
.length
; j
++) {
1499 var y
= series
[j
][1];
1500 if (y
=== null || isNaN(y
)) continue;
1501 if (maxY
== null || y
> maxY
) {
1504 if (minY
== null || y
< minY
) {
1510 return [minY
, maxY
];
1514 * Update the graph with new data. Data is in the format
1515 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1516 * or, if errorBars=true,
1517 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1518 * @param {Array.<Object>} data The data (see above)
1521 Dygraph
.prototype.drawGraph_
= function(data
) {
1522 // This is used to set the second parameter to drawCallback, below.
1523 var is_initial_draw
= this.is_initial_draw_
;
1524 this.is_initial_draw_
= false;
1526 var minY
= null, maxY
= null;
1527 this.layout_
.removeAllDatasets();
1529 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1531 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints');
1533 // Loop over the fields (series). Go from the last to the first,
1534 // because if they're stacked that's how we accumulate the values.
1536 var cumulative_y
= []; // For stacked series.
1539 // Loop over all fields and create datasets
1540 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1541 if (!this.visibility()[i
- 1]) continue;
1544 for (var j
= 0; j
< data
.length
; j
++) {
1545 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1546 var date
= data
[j
][0];
1547 series
.push([date
, data
[j
][i
]]);
1550 series
= this.rollingAverage(series
, this.rollPeriod_
);
1552 // Prune down to the desired range, if necessary (for zooming)
1553 // Because there can be lines going to points outside of the visible area,
1554 // we actually prune to visible points, plus one on either side.
1555 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1556 if (this.dateWindow_
) {
1557 var low
= this.dateWindow_
[0];
1558 var high
= this.dateWindow_
[1];
1560 // TODO(danvk): do binary search instead of linear search.
1561 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1562 var firstIdx
= null, lastIdx
= null;
1563 for (var k
= 0; k
< series
.length
; k
++) {
1564 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1567 if (series
[k
][0] <= high
) {
1571 if (firstIdx
=== null) firstIdx
= 0;
1572 if (firstIdx
> 0) firstIdx
--;
1573 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1574 if (lastIdx
< series
.length
- 1) lastIdx
++;
1575 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1576 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1577 pruned
.push(series
[k
]);
1581 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1584 var extremes
= this.extremeValues_(series
);
1585 var thisMinY
= extremes
[0];
1586 var thisMaxY
= extremes
[1];
1587 if (minY
=== null || thisMinY
< minY
) minY
= thisMinY
;
1588 if (maxY
=== null || thisMaxY
> maxY
) maxY
= thisMaxY
;
1591 for (var j
=0; j
<series
.length
; j
++) {
1592 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1595 } else if (this.attr_("stackedGraph")) {
1596 var l
= series
.length
;
1598 for (var j
= 0; j
< l
; j
++) {
1599 // If one data set has a NaN, let all subsequent stacked
1600 // sets inherit the NaN -- only start at 0 for the first set.
1601 var x
= series
[j
][0];
1602 if (cumulative_y
[x
] === undefined
)
1603 cumulative_y
[x
] = 0;
1605 actual_y
= series
[j
][1];
1606 cumulative_y
[x
] += actual_y
;
1608 series
[j
] = [x
, cumulative_y
[x
]]
1610 if (!maxY
|| cumulative_y
[x
] > maxY
)
1611 maxY
= cumulative_y
[x
];
1615 datasets
[i
] = series
;
1618 for (var i
= 1; i
< datasets
.length
; i
++) {
1619 if (!this.visibility()[i
- 1]) continue;
1620 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1623 // Use some heuristics to come up with a good maxY value, unless it's been
1624 // set explicitly by the user.
1625 if (this.valueRange_
!= null) {
1626 this.addYTicks_(this.valueRange_
[0], this.valueRange_
[1]);
1627 this.displayedYRange_
= this.valueRange_
;
1629 // This affects the calculation of span, below.
1630 if (this.attr_("includeZero") && minY
> 0) {
1634 // Add some padding and round up to an integer to be human-friendly.
1635 var span
= maxY
- minY
;
1636 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1637 if (span
== 0) { span
= maxY
; }
1638 var maxAxisY
= maxY
+ 0.1 * span
;
1639 var minAxisY
= minY
- 0.1 * span
;
1641 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1642 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1643 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1645 if (this.attr_("includeZero")) {
1646 if (maxY
< 0) maxAxisY
= 0;
1647 if (minY
> 0) minAxisY
= 0;
1650 this.addYTicks_(minAxisY
, maxAxisY
);
1651 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1656 // Tell PlotKit to use this new data and render itself
1657 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1658 this.layout_
.evaluateWithError();
1659 this.plotter_
.clear();
1660 this.plotter_
.render();
1661 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1662 this.canvas_
.height
);
1664 if (this.attr_("drawCallback") !== null) {
1665 this.attr_("drawCallback")(this, is_initial_draw
);
1670 * Calculates the rolling average of a data set.
1671 * If originalData is [label, val], rolls the average of those.
1672 * If originalData is [label, [, it's interpreted as [value, stddev]
1673 * and the roll is returned in the same form, with appropriately reduced
1674 * stddev for each value.
1675 * Note that this is where fractional input (i.e. '5/10') is converted into
1677 * @param {Array} originalData The data in the appropriate format (see above)
1678 * @param {Number} rollPeriod The number of days over which to average the data
1680 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1681 if (originalData
.length
< 2)
1682 return originalData
;
1683 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1684 var rollingData
= [];
1685 var sigma
= this.attr_("sigma");
1687 if (this.fractions_
) {
1689 var den
= 0; // numerator/denominator
1691 for (var i
= 0; i
< originalData
.length
; i
++) {
1692 num
+= originalData
[i
][1][0];
1693 den
+= originalData
[i
][1][1];
1694 if (i
- rollPeriod
>= 0) {
1695 num
-= originalData
[i
- rollPeriod
][1][0];
1696 den
-= originalData
[i
- rollPeriod
][1][1];
1699 var date
= originalData
[i
][0];
1700 var value
= den
? num
/ den
: 0.0;
1701 if (this.attr_("errorBars")) {
1702 if (this.wilsonInterval_
) {
1703 // For more details on this confidence interval, see:
1704 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1706 var p
= value
< 0 ? 0 : value
, n
= den
;
1707 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1708 var denom
= 1 + sigma
* sigma
/ den
;
1709 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1710 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1711 rollingData
[i
] = [date
,
1712 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1714 rollingData
[i
] = [date
, [0, 0, 0]];
1717 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1718 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1721 rollingData
[i
] = [date
, mult
* value
];
1724 } else if (this.attr_("customBars")) {
1729 for (var i
= 0; i
< originalData
.length
; i
++) {
1730 var data
= originalData
[i
][1];
1732 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1734 if (y
!= null && !isNaN(y
)) {
1740 if (i
- rollPeriod
>= 0) {
1741 var prev
= originalData
[i
- rollPeriod
];
1742 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1749 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1750 1.0 * (mid
- low
) / count
,
1751 1.0 * (high
- mid
) / count
]];
1754 // Calculate the rolling average for the first rollPeriod - 1 points where
1755 // there is not enough data to roll over the full number of days
1756 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1757 if (!this.attr_("errorBars")){
1758 if (rollPeriod
== 1) {
1759 return originalData
;
1762 for (var i
= 0; i
< originalData
.length
; i
++) {
1765 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1766 var y
= originalData
[j
][1];
1767 if (y
== null || isNaN(y
)) continue;
1769 sum
+= originalData
[j
][1];
1772 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1774 rollingData
[i
] = [originalData
[i
][0], null];
1779 for (var i
= 0; i
< originalData
.length
; i
++) {
1783 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1784 var y
= originalData
[j
][1][0];
1785 if (y
== null || isNaN(y
)) continue;
1787 sum
+= originalData
[j
][1][0];
1788 variance
+= Math
.pow(originalData
[j
][1][1], 2);
1791 var stddev
= Math
.sqrt(variance
) / num_ok
;
1792 rollingData
[i
] = [originalData
[i
][0],
1793 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
1795 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
1805 * Parses a date, returning the number of milliseconds since epoch. This can be
1806 * passed in as an xValueParser in the Dygraph constructor.
1807 * TODO(danvk): enumerate formats that this understands.
1808 * @param {String} A date in YYYYMMDD format.
1809 * @return {Number} Milliseconds since epoch.
1812 Dygraph
.dateParser
= function(dateStr
, self
) {
1815 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
1816 dateStrSlashed
= dateStr
.replace("-", "/", "g");
1817 while (dateStrSlashed
.search("-") != -1) {
1818 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
1820 d
= Date
.parse(dateStrSlashed
);
1821 } else if (dateStr
.length
== 8) { // e.g. '20090712'
1822 // TODO(danvk): remove support for this format. It's confusing.
1823 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
1824 + "/" + dateStr
.substr(6,2);
1825 d
= Date
.parse(dateStrSlashed
);
1827 // Any format that Date.parse will accept, e.g. "2009/07/12" or
1828 // "2009/07/12 12:34:56"
1829 d
= Date
.parse(dateStr
);
1832 if (!d
|| isNaN(d
)) {
1833 self
.error("Couldn't parse " + dateStr
+ " as a date");
1839 * Detects the type of the str (date or numeric) and sets the various
1840 * formatting attributes in this.attrs_ based on this type.
1841 * @param {String} str An x value.
1844 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
1846 if (str
.indexOf('-') >= 0 ||
1847 str
.indexOf('/') >= 0 ||
1848 isNaN(parseFloat(str
))) {
1850 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
1851 // TODO(danvk): remove support for this format.
1856 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
1857 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
1858 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
1859 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
1861 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
1862 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
1863 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
1864 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
1869 * Parses a string in a special csv format. We expect a csv file where each
1870 * line is a date point, and the first field in each line is the date string.
1871 * We also expect that all remaining fields represent series.
1872 * if the errorBars attribute is set, then interpret the fields as:
1873 * date, series1, stddev1, series2, stddev2, ...
1874 * @param {Array.<Object>} data See above.
1877 * @return Array.<Object> An array with one entry for each row. These entries
1878 * are an array of cells in that row. The first entry is the parsed x-value for
1879 * the row. The second, third, etc. are the y-values. These can take on one of
1880 * three forms, depending on the CSV and constructor parameters:
1882 * 2. [ value, stddev ]
1883 * 3. [ low value, center value, high value ]
1885 Dygraph
.prototype.parseCSV_
= function(data
) {
1887 var lines
= data
.split("\n");
1889 // Use the default delimiter or fall back to a tab if that makes sense.
1890 var delim
= this.attr_('delimiter');
1891 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
1896 if (this.labelsFromCSV_
) {
1898 this.attrs_
.labels
= lines
[0].split(delim
);
1901 // Parse the x as a float or return null if it's not a number.
1902 var parseFloatOrNull
= function(x
) {
1903 if (x
.length
== 0) return null;
1904 return parseFloat(x
);
1908 var defaultParserSet
= false; // attempt to auto-detect x value type
1909 var expectedCols
= this.attr_("labels").length
;
1910 var outOfOrder
= false;
1911 for (var i
= start
; i
< lines
.length
; i
++) {
1912 var line
= lines
[i
];
1913 if (line
.length
== 0) continue; // skip blank lines
1914 if (line
[0] == '#') continue; // skip comment lines
1915 var inFields
= line
.split(delim
);
1916 if (inFields
.length
< 2) continue;
1919 if (!defaultParserSet
) {
1920 this.detectTypeFromString_(inFields
[0]);
1921 xParser
= this.attr_("xValueParser");
1922 defaultParserSet
= true;
1924 fields
[0] = xParser(inFields
[0], this);
1926 // If fractions are expected, parse the numbers as "A/B
"
1927 if (this.fractions_) {
1928 for (var j = 1; j < inFields.length; j++) {
1929 // TODO(danvk): figure out an appropriate way to flag parse errors.
1930 var vals = inFields[j].split("/");
1931 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
1933 } else if (this.attr_("errorBars
")) {
1934 // If there are error bars, values are (value, stddev) pairs
1935 for (var j = 1; j < inFields.length; j += 2)
1936 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
1937 parseFloatOrNull(inFields[j + 1])];
1938 } else if (this.attr_("customBars
")) {
1939 // Bars are a low;center;high tuple
1940 for (var j = 1; j < inFields.length; j++) {
1941 var vals = inFields[j].split(";");
1942 fields[j] = [ parseFloatOrNull(vals[0]),
1943 parseFloatOrNull(vals[1]),
1944 parseFloatOrNull(vals[2]) ];
1947 // Values are just numbers
1948 for (var j = 1; j < inFields.length; j++) {
1949 fields[j] = parseFloatOrNull(inFields[j]);
1952 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
1957 if (fields.length != expectedCols) {
1958 this.error("Number of columns
in line
" + i + " (" + fields.length +
1959 ") does not agree
with number of
labels (" + expectedCols +
1965 this.warn("CSV is out of order
; order it correctly to speed loading
.");
1966 ret.sort(function(a,b) { return a[0] - b[0] });
1973 * The user has provided their data as a pre-packaged JS array. If the x values
1974 * are numeric, this is the same as dygraphs' internal format. If the x values
1975 * are dates, we need to convert them from Date objects to ms since epoch.
1976 * @param {Array.<Object>} data
1977 * @return {Array.<Object>} data with numeric x values.
1979 Dygraph.prototype.parseArray_ = function(data) {
1980 // Peek at the first x value to see if it's numeric.
1981 if (data.length == 0) {
1982 this.error("Can
't plot empty data set");
1985 if (data[0].length == 0) {
1986 this.error("Data set cannot contain an empty row");
1990 if (this.attr_("labels") == null) {
1991 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
1992 "in the options parameter");
1993 this.attrs_.labels = [ "X" ];
1994 for (var i = 1; i < data[0].length; i++) {
1995 this.attrs_.labels.push("Y" + i);
1999 if (Dygraph.isDateLike(data[0][0])) {
2000 // Some intelligent defaults for a date x-axis.
2001 this.attrs_.xValueFormatter = Dygraph.dateString_;
2002 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2003 this.attrs_.xTicker = Dygraph.dateTicker;
2005 // Assume they're all dates
.
2006 var parsedData
= Dygraph
.clone(data
);
2007 for (var i
= 0; i
< data
.length
; i
++) {
2008 if (parsedData
[i
].length
== 0) {
2009 this.error("Row " << (1 + i
) << " of data is empty");
2012 if (parsedData
[i
][0] == null
2013 || typeof(parsedData
[i
][0].getTime
) != 'function'
2014 || isNaN(parsedData
[i
][0].getTime())) {
2015 this.error("x value in row " + (1 + i
) + " is not a Date");
2018 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2022 // Some intelligent defaults for a numeric x-axis.
2023 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2024 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2030 * Parses a DataTable object from gviz.
2031 * The data is expected to have a first column that is either a date or a
2032 * number. All subsequent columns must be numbers. If there is a clear mismatch
2033 * between this.xValueParser_ and the type of the first column, it will be
2034 * fixed. Returned value is in the same format as return value of parseCSV_.
2035 * @param {Array.<Object>} data See above.
2038 Dygraph
.prototype.parseDataTable_
= function(data
) {
2039 var cols
= data
.getNumberOfColumns();
2040 var rows
= data
.getNumberOfRows();
2042 // Read column labels
2044 for (var i
= 0; i
< cols
; i
++) {
2045 labels
.push(data
.getColumnLabel(i
));
2046 if (i
!= 0 && this.attr_("errorBars")) i
+= 1;
2048 this.attrs_
.labels
= labels
;
2049 cols
= labels
.length
;
2051 var indepType
= data
.getColumnType(0);
2052 if (indepType
== 'date' || indepType
== 'datetime') {
2053 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2054 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2055 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2056 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2057 } else if (indepType
== 'number') {
2058 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2059 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2060 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2061 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2063 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2064 "column 1 of DataTable input (Got '" + indepType
+ "')");
2069 var outOfOrder
= false;
2070 for (var i
= 0; i
< rows
; i
++) {
2072 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2073 data
.getValue(i
, 0) === null) {
2074 this.warning("Ignoring row " + i
+
2075 " of DataTable because of undefined or null first column.");
2079 if (indepType
== 'date' || indepType
== 'datetime') {
2080 row
.push(data
.getValue(i
, 0).getTime());
2082 row
.push(data
.getValue(i
, 0));
2084 if (!this.attr_("errorBars")) {
2085 for (var j
= 1; j
< cols
; j
++) {
2086 row
.push(data
.getValue(i
, j
));
2089 for (var j
= 0; j
< cols
- 1; j
++) {
2090 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2093 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2100 this.warn("DataTable is out of order; order it correctly to speed loading.");
2101 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2106 // These functions are all based on MochiKit.
2107 Dygraph
.update
= function (self
, o
) {
2108 if (typeof(o
) != 'undefined' && o
!== null) {
2110 if (o
.hasOwnProperty(k
)) {
2118 Dygraph
.isArrayLike
= function (o
) {
2119 var typ
= typeof(o
);
2121 (typ
!= 'object' && !(typ
== 'function' &&
2122 typeof(o
.item
) == 'function')) ||
2124 typeof(o
.length
) != 'number' ||
2132 Dygraph
.isDateLike
= function (o
) {
2133 if (typeof(o
) != "object" || o
=== null ||
2134 typeof(o
.getTime
) != 'function') {
2140 Dygraph
.clone
= function(o
) {
2141 // TODO(danvk): figure out how MochiKit's version works
2143 for (var i
= 0; i
< o
.length
; i
++) {
2144 if (Dygraph
.isArrayLike(o
[i
])) {
2145 r
.push(Dygraph
.clone(o
[i
]));
2155 * Get the CSV data. If it's in a function, call that function. If it's in a
2156 * file, do an XMLHttpRequest to get it.
2159 Dygraph
.prototype.start_
= function() {
2160 if (typeof this.file_
== 'function') {
2161 // CSV string. Pretend we got it via XHR.
2162 this.loadedEvent_(this.file_());
2163 } else if (Dygraph
.isArrayLike(this.file_
)) {
2164 this.rawData_
= this.parseArray_(this.file_
);
2165 this.drawGraph_(this.rawData_
);
2166 } else if (typeof this.file_
== 'object' &&
2167 typeof this.file_
.getColumnRange
== 'function') {
2168 // must be a DataTable from gviz.
2169 this.rawData_
= this.parseDataTable_(this.file_
);
2170 this.drawGraph_(this.rawData_
);
2171 } else if (typeof this.file_
== 'string') {
2172 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2173 if (this.file_
.indexOf('\n') >= 0) {
2174 this.loadedEvent_(this.file_
);
2176 var req
= new XMLHttpRequest();
2178 req
.onreadystatechange
= function () {
2179 if (req
.readyState
== 4) {
2180 if (req
.status
== 200) {
2181 caller
.loadedEvent_(req
.responseText
);
2186 req
.open("GET", this.file_
, true);
2190 this.error("Unknown data format: " + (typeof this.file_
));
2195 * Changes various properties of the graph. These can include:
2197 * <li>file: changes the source data for the graph</li>
2198 * <li>errorBars: changes whether the data contains stddev</li>
2200 * @param {Object} attrs The new properties and values
2202 Dygraph
.prototype.updateOptions
= function(attrs
) {
2203 // TODO(danvk): this is a mess. Rethink this function.
2204 if (attrs
.rollPeriod
) {
2205 this.rollPeriod_
= attrs
.rollPeriod
;
2207 if (attrs
.dateWindow
) {
2208 this.dateWindow_
= attrs
.dateWindow
;
2210 if (attrs
.valueRange
) {
2211 this.valueRange_
= attrs
.valueRange
;
2213 Dygraph
.update(this.user_attrs_
, attrs
);
2214 Dygraph
.update(this.renderOptions_
, attrs
);
2216 this.labelsFromCSV_
= (this.attr_("labels") == null);
2218 // TODO(danvk): this doesn't match the constructor logic
2219 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2220 if (attrs
['file']) {
2221 this.file_
= attrs
['file'];
2224 this.drawGraph_(this.rawData_
);
2229 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2230 * containing div (which has presumably changed size since the dygraph was
2231 * instantiated. If the width/height are specified, the div will be resized.
2233 * This is far more efficient than destroying and re-instantiating a
2234 * Dygraph, since it doesn't have to reparse the underlying data.
2236 * @param {Number} width Width (in pixels)
2237 * @param {Number} height Height (in pixels)
2239 Dygraph
.prototype.resize
= function(width
, height
) {
2240 if (this.resize_lock
) {
2243 this.resize_lock
= true;
2245 if ((width
=== null) != (height
=== null)) {
2246 this.warn("Dygraph.resize() should be called with zero parameters or " +
2247 "two non-NULL parameters. Pretending it was zero.");
2248 width
= height
= null;
2251 // TODO(danvk): there should be a clear() method.
2252 this.maindiv_
.innerHTML
= "";
2253 this.attrs_
.labelsDiv
= null;
2256 this.maindiv_
.style
.width
= width
+ "px";
2257 this.maindiv_
.style
.height
= height
+ "px";
2258 this.width_
= width
;
2259 this.height_
= height
;
2261 this.width_
= this.maindiv_
.offsetWidth
;
2262 this.height_
= this.maindiv_
.offsetHeight
;
2265 this.createInterface_();
2266 this.drawGraph_(this.rawData_
);
2268 this.resize_lock
= false;
2272 * Adjusts the number of days in the rolling average. Updates the graph to
2273 * reflect the new averaging period.
2274 * @param {Number} length Number of days over which to average the data.
2276 Dygraph
.prototype.adjustRoll
= function(length
) {
2277 this.rollPeriod_
= length
;
2278 this.drawGraph_(this.rawData_
);
2282 * Returns a boolean array of visibility statuses.
2284 Dygraph
.prototype.visibility
= function() {
2285 // Do lazy-initialization, so that this happens after we know the number of
2287 if (!this.attr_("visibility")) {
2288 this.attrs_
["visibility"] = [];
2290 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2291 this.attr_("visibility").push(true);
2293 return this.attr_("visibility");
2297 * Changes the visiblity of a series.
2299 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2300 var x
= this.visibility();
2301 if (num
< 0 && num
>= x
.length
) {
2302 this.warn("invalid series number in setVisibility: " + num
);
2305 this.drawGraph_(this.rawData_
);
2310 * Create a new canvas element. This is more complex than a simple
2311 * document.createElement("canvas") because of IE and excanvas.
2313 Dygraph
.createCanvas
= function() {
2314 var canvas
= document
.createElement("canvas");
2316 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2318 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2326 * A wrapper around Dygraph that implements the gviz API.
2327 * @param {Object} container The DOM object the visualization should live in.
2329 Dygraph
.GVizChart
= function(container
) {
2330 this.container
= container
;
2333 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2334 this.container
.innerHTML
= '';
2335 this.date_graph
= new Dygraph(this.container
, data
, options
);
2339 * Google charts compatible setSelection
2340 * Only row selection is supported, all points in the row will be highlighted
2341 * @param {Array} array of the selected cells
2344 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2346 if (selection_array
.length
) {
2347 row
= selection_array
[0].row
;
2349 this.date_graph
.setSelection(row
);
2353 * Google charts compatible getSelection implementation
2354 * @return {Array} array of the selected cells
2357 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2360 var row
= this.date_graph
.getSelection();
2362 if (row
< 0) return selection
;
2365 for (var i
in this.date_graph
.layout_
.datasets
) {
2366 selection
.push({row
: row
, column
: col
});
2373 // Older pages may still use this name.
2374 DateGraph
= Dygraph
;