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,
133 // Various logging levels.
139 // Directions for panning and zooming. Use bit operations when combined
140 // values are possible.
141 Dygraph
.HORIZONTAL
= 1;
142 Dygraph
.VERTICAL
= 2;
144 // Used for initializing annotation CSS rules only once.
145 Dygraph
.addedAnnotationCSS
= false;
147 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
148 // Labels is no longer a constructor parameter, since it's typically set
149 // directly from the data source. It also conains a name for the x-axis,
150 // which the previous constructor form did not.
151 if (labels
!= null) {
152 var new_labels
= ["Date"];
153 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
154 Dygraph
.update(attrs
, { 'labels': new_labels
});
156 this.__init__(div
, file
, attrs
);
160 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
161 * and interaction <canvas> inside of it. See the constructor for details
163 * @param {Element} div the Element to render the graph into.
164 * @param {String | Function} file Source data
165 * @param {Object} attrs Miscellaneous other options
168 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
169 // Support two-argument constructor
170 if (attrs
== null) { attrs
= {}; }
172 // Copy the important bits into the object
173 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
176 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
177 this.previousVerticalX_
= -1;
178 this.fractions_
= attrs
.fractions
|| false;
179 this.dateWindow_
= attrs
.dateWindow
|| null;
181 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
182 this.is_initial_draw_
= true;
183 this.annotations_
= [];
185 // Clear the div. This ensure that, if multiple dygraphs are passed the same
186 // div, then only one will be drawn.
189 // If the div isn't already sized then inherit from our attrs or
190 // give it a default size.
191 if (div
.style
.width
== '') {
192 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
194 if (div
.style
.height
== '') {
195 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
197 this.width_
= parseInt(div
.style
.width
, 10);
198 this.height_
= parseInt(div
.style
.height
, 10);
199 // The div might have been specified as percent of the current window size,
200 // convert that to an appropriate number of pixels.
201 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
202 this.width_
= div
.offsetWidth
;
204 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
205 this.height_
= div
.offsetHeight
;
208 if (this.width_
== 0) {
209 this.error("dygraph has zero width. Please specify a width in pixels.");
211 if (this.height_
== 0) {
212 this.error("dygraph has zero height. Please specify a height in pixels.");
215 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
216 if (attrs
['stackedGraph']) {
217 attrs
['fillGraph'] = true;
218 // TODO(nikhilk): Add any other stackedGraph checks here.
221 // Dygraphs has many options, some of which interact with one another.
222 // To keep track of everything, we maintain two sets of options:
224 // this.user_attrs_ only options explicitly set by the user.
225 // this.attrs_ defaults, options derived from user_attrs_, data.
227 // Options are then accessed this.attr_('attr'), which first looks at
228 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
229 // defaults without overriding behavior that the user specifically asks for.
230 this.user_attrs_
= {};
231 Dygraph
.update(this.user_attrs_
, attrs
);
234 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
236 this.boundaryIds_
= [];
238 // Make a note of whether labels will be pulled from the CSV file.
239 this.labelsFromCSV_
= (this.attr_("labels") == null);
241 // Create the containing DIV and other interactive elements
242 this.createInterface_();
247 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
249 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
250 this.user_attrs_
[seriesName
] != null &&
251 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
252 return this.user_attrs_
[seriesName
][name
];
253 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
254 return this.user_attrs_
[name
];
255 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
256 return this.attrs_
[name
];
262 // TODO(danvk): any way I can get the line numbers to be this.warn call?
263 Dygraph
.prototype.log
= function(severity
, message
) {
264 if (typeof(console
) != 'undefined') {
267 console
.debug('dygraphs: ' + message
);
270 console
.info('dygraphs: ' + message
);
272 case Dygraph
.WARNING
:
273 console
.warn('dygraphs: ' + message
);
276 console
.error('dygraphs: ' + message
);
281 Dygraph
.prototype.info
= function(message
) {
282 this.log(Dygraph
.INFO
, message
);
284 Dygraph
.prototype.warn
= function(message
) {
285 this.log(Dygraph
.WARNING
, message
);
287 Dygraph
.prototype.error
= function(message
) {
288 this.log(Dygraph
.ERROR
, message
);
292 * Returns the current rolling period, as set by the user or an option.
293 * @return {Number} The number of days in the rolling window
295 Dygraph
.prototype.rollPeriod
= function() {
296 return this.rollPeriod_
;
300 * Returns the currently-visible x-range. This can be affected by zooming,
301 * panning or a call to updateOptions.
302 * Returns a two-element array: [left, right].
303 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
305 Dygraph
.prototype.xAxisRange
= function() {
306 if (this.dateWindow_
) return this.dateWindow_
;
308 // The entire chart is visible.
309 var left
= this.rawData_
[0][0];
310 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
311 return [left
, right
];
315 * Returns the currently-visible y-range. This can be affected by zooming,
316 * panning or a call to updateOptions.
317 * Returns a two-element array: [bottom, top].
319 Dygraph
.prototype.yAxisRange
= function() {
320 return this.displayedYRange_
;
324 * Convert from data coordinates to canvas/div X/Y coordinates.
325 * Returns a two-element array: [X, Y]
327 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
328 var ret
= [null, null];
329 var area
= this.plotter_
.area
;
331 var xRange
= this.xAxisRange();
332 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
336 var yRange
= this.yAxisRange();
337 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
343 // TODO(danvk): use these functions throughout dygraphs.
345 * Convert from canvas/div coords to data coordinates.
346 * Returns a two-element array: [X, Y]
348 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
349 var ret
= [null, null];
350 var area
= this.plotter_
.area
;
352 var xRange
= this.xAxisRange();
353 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
357 var yRange
= this.yAxisRange();
358 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
365 * Returns the number of columns (including the independent variable).
367 Dygraph
.prototype.numColumns
= function() {
368 return this.rawData_
[0].length
;
372 * Returns the number of rows (excluding any header/label row).
374 Dygraph
.prototype.numRows
= function() {
375 return this.rawData_
.length
;
379 * Returns the value in the given row and column. If the row and column exceed
380 * the bounds on the data, returns null. Also returns null if the value is
383 Dygraph
.prototype.getValue
= function(row
, col
) {
384 if (row
< 0 || row
> this.rawData_
.length
) return null;
385 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
387 return this.rawData_
[row
][col
];
390 Dygraph
.addEvent
= function(el
, evt
, fn
) {
391 var normed_fn
= function(e
) {
392 if (!e
) var e
= window
.event
;
395 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
396 el
.addEventListener(evt
, normed_fn
, false);
398 el
.attachEvent('on' + evt
, normed_fn
);
403 * Generates interface elements for the Dygraph: a containing div, a div to
404 * display the current point, and a textbox to adjust the rolling average
405 * period. Also creates the Renderer/Layout elements.
408 Dygraph
.prototype.createInterface_
= function() {
409 // Create the all-enclosing graph div
410 var enclosing
= this.maindiv_
;
412 this.graphDiv
= document
.createElement("div");
413 this.graphDiv
.style
.width
= this.width_
+ "px";
414 this.graphDiv
.style
.height
= this.height_
+ "px";
415 enclosing
.appendChild(this.graphDiv
);
417 // Create the canvas for interactive parts of the chart.
418 this.canvas_
= Dygraph
.createCanvas();
419 this.canvas_
.style
.position
= "absolute";
420 this.canvas_
.width
= this.width_
;
421 this.canvas_
.height
= this.height_
;
422 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
423 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
425 // ... and for static parts of the chart.
426 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
428 // The interactive parts of the graph are drawn on top of the chart.
429 this.graphDiv
.appendChild(this.hidden_
);
430 this.graphDiv
.appendChild(this.canvas_
);
431 this.mouseEventElement_
= this.canvas_
;
434 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
435 dygraph
.mouseMove_(e
);
437 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
438 dygraph
.mouseOut_(e
);
441 // Create the grapher
442 // TODO(danvk): why does the Layout need its own set of options?
443 this.layoutOptions_
= { 'xOriginIsZero': false };
444 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
445 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
446 Dygraph
.update(this.layoutOptions_
, {
447 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
449 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
451 // TODO(danvk): why does the Renderer need its own set of options?
452 this.renderOptions_
= { colorScheme
: this.colors_
,
454 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
455 Dygraph
.update(this.renderOptions_
, this.attrs_
);
456 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
458 this.createStatusMessage_();
459 this.createDragInterface_();
463 * Detach DOM elements in the dygraph and null out all data references.
464 * Calling this when you're done with a dygraph can dramatically reduce memory
465 * usage. See, e.g., the tests/perf.html example.
467 Dygraph
.prototype.destroy
= function() {
468 var removeRecursive
= function(node
) {
469 while (node
.hasChildNodes()) {
470 removeRecursive(node
.firstChild
);
471 node
.removeChild(node
.firstChild
);
474 removeRecursive(this.maindiv_
);
476 var nullOut
= function(obj
) {
478 if (typeof(obj
[n
]) === 'object') {
484 // These may not all be necessary, but it can't hurt...
485 nullOut(this.layout_
);
486 nullOut(this.plotter_
);
491 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
492 * this particular canvas. All Dygraph work is done on this.canvas_.
493 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
494 * @return {Object} The newly-created canvas
497 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
498 var h
= Dygraph
.createCanvas();
499 h
.style
.position
= "absolute";
500 // TODO(danvk): h should be offset from canvas. canvas needs to include
501 // some extra area to make it easier to zoom in on the far left and far
502 // right. h needs to be precisely the plot area, so that clipping occurs.
503 h
.style
.top
= canvas
.style
.top
;
504 h
.style
.left
= canvas
.style
.left
;
505 h
.width
= this.width_
;
506 h
.height
= this.height_
;
507 h
.style
.width
= this.width_
+ "px"; // for IE
508 h
.style
.height
= this.height_
+ "px"; // for IE
512 // Taken from MochiKit.Color
513 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
517 if (saturation
=== 0) {
522 var i
= Math
.floor(hue
* 6);
523 var f
= (hue
* 6) - i
;
524 var p
= value
* (1 - saturation
);
525 var q
= value
* (1 - (saturation
* f
));
526 var t
= value
* (1 - (saturation
* (1 - f
)));
528 case 1: red
= q
; green
= value
; blue
= p
; break;
529 case 2: red
= p
; green
= value
; blue
= t
; break;
530 case 3: red
= p
; green
= q
; blue
= value
; break;
531 case 4: red
= t
; green
= p
; blue
= value
; break;
532 case 5: red
= value
; green
= p
; blue
= q
; break;
533 case 6: // fall through
534 case 0: red
= value
; green
= t
; blue
= p
; break;
537 red
= Math
.floor(255 * red
+ 0.5);
538 green
= Math
.floor(255 * green
+ 0.5);
539 blue
= Math
.floor(255 * blue
+ 0.5);
540 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
545 * Generate a set of distinct colors for the data series. This is done with a
546 * color wheel. Saturation/Value are customizable, and the hue is
547 * equally-spaced around the color wheel. If a custom set of colors is
548 * specified, that is used instead.
551 Dygraph
.prototype.setColors_
= function() {
552 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
553 // away with this.renderOptions_.
554 var num
= this.attr_("labels").length
- 1;
556 var colors
= this.attr_('colors');
558 var sat
= this.attr_('colorSaturation') || 1.0;
559 var val
= this.attr_('colorValue') || 0.5;
560 var half
= Math
.ceil(num
/ 2);
561 for (var i
= 1; i
<= num
; i
++) {
562 if (!this.visibility()[i
-1]) continue;
563 // alternate colors for high contrast.
564 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
565 var hue
= (1.0 * idx
/ (1 + num
));
566 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
569 for (var i
= 0; i
< num
; i
++) {
570 if (!this.visibility()[i
]) continue;
571 var colorStr
= colors
[i
% colors
.length
];
572 this.colors_
.push(colorStr
);
576 // TODO(danvk): update this w/r
/t/ the
new options system
.
577 this.renderOptions_
.colorScheme
= this.colors_
;
578 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
579 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
580 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
584 * Return the list of colors. This is either the list of colors passed in the
585 * attributes, or the autogenerated list of rgb(r,g,b) strings.
586 * @return {Array<string>} The list of colors.
588 Dygraph
.prototype.getColors
= function() {
592 // The following functions are from quirksmode.org with a modification for Safari from
593 // http://blog.firetree.net/2005/07/04/javascript-find-position/
594 // http://www.quirksmode.org/js
/findpos
.html
595 Dygraph
.findPosX
= function(obj
) {
600 curleft
+= obj
.offsetLeft
;
601 if(!obj
.offsetParent
)
603 obj
= obj
.offsetParent
;
610 Dygraph
.findPosY
= function(obj
) {
615 curtop
+= obj
.offsetTop
;
616 if(!obj
.offsetParent
)
618 obj
= obj
.offsetParent
;
628 * Create the div that contains information on the selected point(s)
629 * This goes in the top right of the canvas, unless an external div has already
633 Dygraph
.prototype.createStatusMessage_
= function() {
634 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
635 if (userLabelsDiv
&& null != userLabelsDiv
636 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
637 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
639 if (!this.attr_("labelsDiv")) {
640 var divWidth
= this.attr_('labelsDivWidth');
642 "position": "absolute",
645 "width": divWidth
+ "px",
647 "left": (this.width_
- divWidth
- 2) + "px",
648 "background": "white",
650 "overflow": "hidden"};
651 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
652 var div
= document
.createElement("div");
653 for (var name
in messagestyle
) {
654 if (messagestyle
.hasOwnProperty(name
)) {
655 div
.style
[name
] = messagestyle
[name
];
658 this.graphDiv
.appendChild(div
);
659 this.attrs_
.labelsDiv
= div
;
664 * Position the labels div so that its right edge is flush with the right edge
665 * of the charting area.
667 Dygraph
.prototype.positionLabelsDiv_
= function() {
668 // Don't touch a user-specified labelsDiv.
669 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
671 var area
= this.plotter_
.area
;
672 var div
= this.attr_("labelsDiv");
673 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") + "px";
677 * Create the text box to adjust the averaging period
678 * @return {Object} The newly-created text box
681 Dygraph
.prototype.createRollInterface_
= function() {
682 // Destroy any existing roller.
683 if (this.roller_
) this.graphDiv
.removeChild(this.roller_
);
685 var display
= this.attr_('showRoller') ? "block" : "none";
686 var textAttr
= { "position": "absolute",
688 "top": (this.plotter_
.area
.h
- 25) + "px",
689 "left": (this.plotter_
.area
.x
+ 1) + "px",
692 var roller
= document
.createElement("input");
693 roller
.type
= "text";
695 roller
.value
= this.rollPeriod_
;
696 for (var name
in textAttr
) {
697 if (textAttr
.hasOwnProperty(name
)) {
698 roller
.style
[name
] = textAttr
[name
];
702 var pa
= this.graphDiv
;
703 pa
.appendChild(roller
);
705 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
709 // These functions are taken from MochiKit.Signal
710 Dygraph
.pageX
= function(e
) {
712 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
715 var b
= document
.body
;
717 (de
.scrollLeft
|| b
.scrollLeft
) -
718 (de
.clientLeft
|| 0);
722 Dygraph
.pageY
= function(e
) {
724 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
727 var b
= document
.body
;
729 (de
.scrollTop
|| b
.scrollTop
) -
735 * Set up all the mouse handlers needed to capture dragging behavior for zoom
739 Dygraph
.prototype.createDragInterface_
= function() {
742 // Tracks whether the mouse is down right now
743 var isZooming
= false;
744 var isPanning
= false;
745 var dragStartX
= null;
746 var dragStartY
= null;
749 var dragDirection
= null;
752 var prevDragDirection
= null;
754 // draggingDate and draggingValue represent the [date,value] point on the
755 // graph at which the mouse was pressed. As the mouse moves while panning,
756 // the viewport must pan so that the mouse position points to
757 // [draggingDate, draggingValue]
758 var draggingDate
= null;
759 var draggingValue
= null;
761 // The range in second/value units that the viewport encompasses during a
762 // panning operation.
763 var dateRange
= null;
764 var valueRange
= null;
766 // Utility function to convert page-wide coordinates to canvas coords
769 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
770 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
772 // Draw zoom rectangles when the mouse is down and the user moves around
773 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
775 dragEndX
= getX(event
);
776 dragEndY
= getY(event
);
778 var xDelta
= Math
.abs(dragStartX
- dragEndX
);
779 var yDelta
= Math
.abs(dragStartY
- dragEndY
);
781 // drag direction threshold for y axis is twice as large as x axis
782 dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
784 self
.drawZoomRect_(dragDirection
, dragStartX
, dragEndX
, dragStartY
, dragEndY
,
785 prevDragDirection
, prevEndX
, prevEndY
);
789 prevDragDirection
= dragDirection
;
790 } else if (isPanning
) {
791 dragEndX
= getX(event
);
792 dragEndY
= getY(event
);
794 // Want to have it so that:
795 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
796 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
797 // 3. draggingValue appears at dragEndY.
798 // 4. valueRange is unaltered.
800 var minDate
= draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
801 var maxDate
= minDate
+ dateRange
;
802 self
.dateWindow_
= [minDate
, maxDate
];
806 // y-axis scaling is automatic unless a valueRange is defined or
807 // if the user zooms in on the y-axis. If neither is true, valueWindow_
809 if (self
.valueWindow_
) {
810 var maxValue
= draggingValue
+ (dragEndY
/ self
.height_
) * valueRange
;
811 var minValue
= maxValue
- valueRange
;
812 self
.valueWindow_
= [ minValue
, maxValue
];
819 // Track the beginning of drag events
820 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
821 px
= Dygraph
.findPosX(self
.canvas_
);
822 py
= Dygraph
.findPosY(self
.canvas_
);
823 dragStartX
= getX(event
);
824 dragStartY
= getY(event
);
826 if (event
.altKey
|| event
.shiftKey
) {
827 // have to be zoomed in to pan.
828 if (!self
.dateWindow_
&& !self
.valueWindow_
) return;
831 var xRange
= self
.xAxisRange();
832 dateRange
= xRange
[1] - xRange
[0];
833 var yRange
= self
.yAxisRange();
834 valueRange
= yRange
[1] - yRange
[0];
836 // TODO(konigsberg): Switch from all this math to toDataCoords?
837 // Seems to work for the dragging value.
838 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
840 var r
= self
.toDataCoords(null, dragStartY
);
841 draggingValue
= r
[1];
847 // If the user releases the mouse button during a drag, but not over the
848 // canvas, then it doesn't count as a zooming action.
849 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
850 if (isZooming
|| isPanning
) {
859 draggingValue
= null;
865 // Temporarily cancel the dragging event when the mouse leaves the graph
866 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
873 // If the mouse is released on the canvas during a drag event, then it's a
874 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
875 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
878 dragEndX
= getX(event
);
879 dragEndY
= getY(event
);
880 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
881 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
883 if (regionWidth
< 2 && regionHeight
< 2 &&
884 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
885 // TODO(danvk): pass along more info about the points, e.g. 'x'
886 if (self
.attr_('clickCallback') != null) {
887 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
889 if (self
.attr_('pointClickCallback')) {
890 // check if the click was on a particular point.
892 var closestDistance
= 0;
893 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
894 var p
= self
.selPoints_
[i
];
895 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
896 Math
.pow(p
.canvasy
- dragEndY
, 2);
897 if (closestIdx
== -1 || distance
< closestDistance
) {
898 closestDistance
= distance
;
903 // Allow any click within two pixels of the dot.
904 var radius
= self
.attr_('highlightCircleSize') + 2;
905 if (closestDistance
<= 5 * 5) {
906 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
911 if (regionWidth
>= 10 && dragDirection
== Dygraph
.HORIZONTAL
) {
912 self
.doZoomX_(Math
.min(dragStartX
, dragEndX
),
913 Math
.max(dragStartX
, dragEndX
));
914 } else if (regionHeight
>= 10 && dragDirection
== Dygraph
.VERTICAL
){
915 self
.doZoomY_(Math
.min(dragStartY
, dragEndY
),
916 Math
.max(dragStartY
, dragEndY
));
918 self
.canvas_
.getContext("2d").clearRect(0, 0,
920 self
.canvas_
.height
);
930 draggingValue
= null;
936 // Double-clicking zooms back out
937 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
938 // Disable zooming out if panning.
939 if (event
.altKey
|| event
.shiftKey
) return;
946 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
947 * up any previous zoom rectangles that were drawn. This could be optimized to
948 * avoid extra redrawing, but it's tricky to avoid interactions with the status
951 * @param {Number} direction the direction of the zoom rectangle. Acceptable
952 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
953 * @param {Number} startX The X position where the drag started, in canvas
955 * @param {Number} endX The current X position of the drag, in canvas coords.
956 * @param {Number} startY The Y position where the drag started, in canvas
958 * @param {Number} endY The current Y position of the drag, in canvas coords.
959 * @param {Number} prevDirection the value of direction on the previous call to
960 * this function. Used to avoid excess redrawing
961 * @param {Number} prevEndX The value of endX on the previous call to this
962 * function. Used to avoid excess redrawing
963 * @param {Number} prevEndY The value of endY on the previous call to this
964 * function. Used to avoid excess redrawing
967 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
968 prevDirection
, prevEndX
, prevEndY
) {
969 var ctx
= this.canvas_
.getContext("2d");
971 // Clean up from the previous rect if necessary
972 if (prevDirection
== Dygraph
.HORIZONTAL
) {
973 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
974 Math
.abs(startX
- prevEndX
), this.height_
);
975 } else if (prevDirection
== Dygraph
.VERTICAL
){
976 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
977 this.width_
, Math
.abs(startY
- prevEndY
));
980 // Draw a light-grey rectangle to show the new viewing area
981 if (direction
== Dygraph
.HORIZONTAL
) {
982 if (endX
&& startX
) {
983 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
984 ctx
.fillRect(Math
.min(startX
, endX
), 0,
985 Math
.abs(endX
- startX
), this.height_
);
988 if (direction
== Dygraph
.VERTICAL
) {
989 if (endY
&& startY
) {
990 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
991 ctx
.fillRect(0, Math
.min(startY
, endY
),
992 this.width_
, Math
.abs(endY
- startY
));
998 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
999 * the canvas. The exact zoom window may be slightly larger if there are no data
1000 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1001 * which accepts dates that match the raw data. This function redraws the graph.
1003 * @param {Number} lowX The leftmost pixel value that should be visible.
1004 * @param {Number} highX The rightmost pixel value that should be visible.
1007 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1008 // Find the earliest and latest dates contained in this canvasx range.
1009 // Convert the call to date ranges of the raw data.
1010 var r
= this.toDataCoords(lowX
, null);
1012 r
= this.toDataCoords(highX
, null);
1014 this.doZoomXDates_(minDate
, maxDate
);
1018 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1019 * method with doZoomX which accepts pixel coordinates. This function redraws
1022 * @param {Number} minDate The minimum date that should be visible.
1023 * @param {Number} maxDate The maximum date that should be visible.
1026 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1027 this.dateWindow_
= [minDate
, maxDate
];
1029 if (this.attr_("zoomCallback")) {
1030 var yRange
= this.yAxisRange();
1031 this.attr_("zoomCallback")(minDate
, maxDate
, yRange
[0], yRange
[1]);
1036 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1037 * the canvas. The exact zoom window may be slightly larger if there are no
1038 * data points near lowY or highY. Don't confuse this function with
1039 * doZoomYValues, which accepts parameters that match the raw data. This
1040 * function redraws the graph.
1042 * @param {Number} lowY The topmost pixel value that should be visible.
1043 * @param {Number} highY The lowest pixel value that should be visible.
1046 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1047 // Find the highest and lowest values in pixel range.
1048 var r
= this.toDataCoords(null, lowY
);
1049 var maxValue
= r
[1];
1050 r
= this.toDataCoords(null, highY
);
1051 var minValue
= r
[1];
1053 this.doZoomYValues_(minValue
, maxValue
);
1057 * Zoom to something containing [minValue, maxValue] values. Don't confuse this
1058 * method with doZoomY which accepts pixel coordinates. This function redraws
1061 * @param {Number} minValue The minimum Value that should be visible.
1062 * @param {Number} maxValue The maximum value that should be visible.
1065 // MERGE: this doesn't make sense anymore.
1066 Dygraph
.prototype.doZoomYValues_
= function(minValue
, maxValue
) {
1067 this.valueWindow_
= [minValue
, maxValue
];
1069 if (this.attr_("zoomCallback")) {
1070 var xRange
= this.xAxisRange();
1071 this.attr_("zoomCallback")(xRange
[0], xRange
[1], minValue
, maxValue
);
1076 * Reset the zoom to the original view coordinates. This is the same as
1077 * double-clicking on the graph.
1081 Dygraph
.prototype.doUnzoom_
= function() {
1083 if (this.dateWindow_
!= null) {
1085 this.dateWindow_
= null;
1087 if (this.valueWindow_
!= null) {
1089 this.valueWindow_
= this.valueRange_
;
1093 // Putting the drawing operation before the callback because it resets
1096 if (this.attr_("zoomCallback")) {
1097 var minDate
= this.rawData_
[0][0];
1098 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1099 var minValue
= this.yAxisRange()[0];
1100 var maxValue
= this.yAxisRange()[1];
1101 this.attr_("zoomCallback")(minDate
, maxDate
, minValue
, maxValue
);
1107 * When the mouse moves in the canvas, display information about a nearby data
1108 * point and draw dots over those points in the data series. This function
1109 * takes care of cleanup of previously-drawn dots.
1110 * @param {Object} event The mousemove event from the browser.
1113 Dygraph
.prototype.mouseMove_
= function(event
) {
1114 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1115 var points
= this.layout_
.points
;
1120 // Loop through all the points and find the date nearest to our current
1122 var minDist
= 1e+100;
1124 for (var i
= 0; i
< points
.length
; i
++) {
1125 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1126 if (dist
> minDist
) continue;
1130 if (idx
>= 0) lastx
= points
[idx
].xval
;
1131 // Check that you can really highlight the last day's data
1132 if (canvasx
> points
[points
.length
-1].canvasx
)
1133 lastx
= points
[points
.length
-1].xval
;
1135 // Extract the points we've selected
1136 this.selPoints_
= [];
1137 var l
= points
.length
;
1138 if (!this.attr_("stackedGraph")) {
1139 for (var i
= 0; i
< l
; i
++) {
1140 if (points
[i
].xval
== lastx
) {
1141 this.selPoints_
.push(points
[i
]);
1145 // Need to 'unstack' points starting from the bottom
1146 var cumulative_sum
= 0;
1147 for (var i
= l
- 1; i
>= 0; i
--) {
1148 if (points
[i
].xval
== lastx
) {
1149 var p
= {}; // Clone the point since we modify it
1150 for (var k
in points
[i
]) {
1151 p
[k
] = points
[i
][k
];
1153 p
.yval
-= cumulative_sum
;
1154 cumulative_sum
+= p
.yval
;
1155 this.selPoints_
.push(p
);
1158 this.selPoints_
.reverse();
1161 if (this.attr_("highlightCallback")) {
1162 var px
= this.lastx_
;
1163 if (px
!== null && lastx
!= px
) {
1164 // only fire if the selected point has changed.
1165 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
1169 // Save last x position for callbacks.
1170 this.lastx_
= lastx
;
1172 this.updateSelection_();
1176 * Draw dots over the selectied points in the data series. This function
1177 * takes care of cleanup of previously-drawn dots.
1180 Dygraph
.prototype.updateSelection_
= function() {
1181 // Clear the previously drawn vertical, if there is one
1182 var ctx
= this.canvas_
.getContext("2d");
1183 if (this.previousVerticalX_
>= 0) {
1184 // Determine the maximum highlight circle size.
1185 var maxCircleSize
= 0;
1186 var labels
= this.attr_('labels');
1187 for (var i
= 1; i
< labels
.length
; i
++) {
1188 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1189 if (r
> maxCircleSize
) maxCircleSize
= r
;
1191 var px
= this.previousVerticalX_
;
1192 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1193 2 * maxCircleSize
+ 2, this.height_
);
1196 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1198 if (this.selPoints_
.length
> 0) {
1199 var canvasx
= this.selPoints_
[0].canvasx
;
1201 // Set the status message to indicate the selected point(s)
1202 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1203 var fmtFunc
= this.attr_('yValueFormatter');
1204 var clen
= this.colors_
.length
;
1206 if (this.attr_('showLabelsOnHighlight')) {
1207 // Set the status message to indicate the selected point(s)
1208 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1209 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1210 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1211 if (this.attr_("labelsSeparateLines")) {
1214 var point
= this.selPoints_
[i
];
1215 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1216 var yval
= fmtFunc(point
.yval
);
1217 replace
+= " <b><font color='" + c
.toHex() + "'>"
1218 + point
.name
+ "</font></b>:"
1222 this.attr_("labelsDiv").innerHTML
= replace
;
1225 // Draw colored circles over the center of each selected point
1227 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1228 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1230 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1232 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1233 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1234 0, 2 * Math
.PI
, false);
1239 this.previousVerticalX_
= canvasx
;
1244 * Set manually set selected dots, and display information about them
1245 * @param int row number that should by highlighted
1246 * false value clears the selection
1249 Dygraph
.prototype.setSelection
= function(row
) {
1250 // Extract the points we've selected
1251 this.selPoints_
= [];
1254 if (row
!== false) {
1255 row
= row
-this.boundaryIds_
[0][0];
1258 if (row
!== false && row
>= 0) {
1259 for (var i
in this.layout_
.datasets
) {
1260 if (row
< this.layout_
.datasets
[i
].length
) {
1261 var point
= this.layout_
.points
[pos
+row
];
1263 if (this.attr_("stackedGraph")) {
1264 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1267 this.selPoints_
.push(point
);
1269 pos
+= this.layout_
.datasets
[i
].length
;
1273 if (this.selPoints_
.length
) {
1274 this.lastx_
= this.selPoints_
[0].xval
;
1275 this.updateSelection_();
1278 this.clearSelection();
1284 * The mouse has left the canvas. Clear out whatever artifacts remain
1285 * @param {Object} event the mouseout event from the browser.
1288 Dygraph
.prototype.mouseOut_
= function(event
) {
1289 if (this.attr_("unhighlightCallback")) {
1290 this.attr_("unhighlightCallback")(event
);
1293 if (this.attr_("hideOverlayOnMouseOut")) {
1294 this.clearSelection();
1299 * Remove all selection from the canvas
1302 Dygraph
.prototype.clearSelection
= function() {
1303 // Get rid of the overlay data
1304 var ctx
= this.canvas_
.getContext("2d");
1305 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1306 this.attr_("labelsDiv").innerHTML
= "";
1307 this.selPoints_
= [];
1312 * Returns the number of the currently selected row
1313 * @return int row number, of -1 if nothing is selected
1316 Dygraph
.prototype.getSelection
= function() {
1317 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1321 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1322 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1323 return row
+ this.boundaryIds_
[0][0];
1329 Dygraph
.zeropad
= function(x
) {
1330 if (x
< 10) return "0" + x
; else return "" + x
;
1334 * Return a string version of the hours, minutes and seconds portion of a date.
1335 * @param {Number} date The JavaScript date (ms since epoch)
1336 * @return {String} A time of the form "HH:MM:SS"
1339 Dygraph
.hmsString_
= function(date
) {
1340 var zeropad
= Dygraph
.zeropad
;
1341 var d
= new Date(date
);
1342 if (d
.getSeconds()) {
1343 return zeropad(d
.getHours()) + ":" +
1344 zeropad(d
.getMinutes()) + ":" +
1345 zeropad(d
.getSeconds());
1347 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1352 * Convert a JS date to a string appropriate to display on an axis that
1353 * is displaying values at the stated granularity.
1354 * @param {Date} date The date to format
1355 * @param {Number} granularity One of the Dygraph granularity constants
1356 * @return {String} The formatted date
1359 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1360 if (granularity
>= Dygraph
.MONTHLY
) {
1361 return date
.strftime('%b %y');
1363 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1364 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1365 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1367 return Dygraph
.hmsString_(date
.getTime());
1373 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1374 * @param {Number} date The JavaScript date (ms since epoch)
1375 * @return {String} A date of the form "YYYY/MM/DD"
1378 Dygraph
.dateString_
= function(date
, self
) {
1379 var zeropad
= Dygraph
.zeropad
;
1380 var d
= new Date(date
);
1383 var year
= "" + d
.getFullYear();
1384 // Get a 0 padded month string
1385 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1386 // Get a 0 padded day string
1387 var day
= zeropad(d
.getDate());
1390 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1391 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1393 return year
+ "/" + month + "/" + day
+ ret
;
1397 * Round a number to the specified number of digits past the decimal point.
1398 * @param {Number} num The number to round
1399 * @param {Number} places The number of decimals to which to round
1400 * @return {Number} The rounded number
1403 Dygraph
.round_
= function(num
, places
) {
1404 var shift
= Math
.pow(10, places
);
1405 return Math
.round(num
* shift
)/shift
;
1409 * Fires when there's data available to be graphed.
1410 * @param {String} data Raw CSV data to be plotted
1413 Dygraph
.prototype.loadedEvent_
= function(data
) {
1414 this.rawData_
= this.parseCSV_(data
);
1418 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1419 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1420 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1423 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1426 Dygraph
.prototype.addXTicks_
= function() {
1427 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1428 var startDate
, endDate
;
1429 if (this.dateWindow_
) {
1430 startDate
= this.dateWindow_
[0];
1431 endDate
= this.dateWindow_
[1];
1433 startDate
= this.rawData_
[0][0];
1434 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1437 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1438 this.layout_
.updateOptions({xTicks
: xTicks
});
1441 // Time granularity enumeration
1442 Dygraph
.SECONDLY
= 0;
1443 Dygraph
.TWO_SECONDLY
= 1;
1444 Dygraph
.FIVE_SECONDLY
= 2;
1445 Dygraph
.TEN_SECONDLY
= 3;
1446 Dygraph
.THIRTY_SECONDLY
= 4;
1447 Dygraph
.MINUTELY
= 5;
1448 Dygraph
.TWO_MINUTELY
= 6;
1449 Dygraph
.FIVE_MINUTELY
= 7;
1450 Dygraph
.TEN_MINUTELY
= 8;
1451 Dygraph
.THIRTY_MINUTELY
= 9;
1452 Dygraph
.HOURLY
= 10;
1453 Dygraph
.TWO_HOURLY
= 11;
1454 Dygraph
.SIX_HOURLY
= 12;
1456 Dygraph
.WEEKLY
= 14;
1457 Dygraph
.MONTHLY
= 15;
1458 Dygraph
.QUARTERLY
= 16;
1459 Dygraph
.BIANNUAL
= 17;
1460 Dygraph
.ANNUAL
= 18;
1461 Dygraph
.DECADAL
= 19;
1462 Dygraph
.NUM_GRANULARITIES
= 20;
1464 Dygraph
.SHORT_SPACINGS
= [];
1465 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1466 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1467 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1468 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1469 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1470 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1471 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1472 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1473 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1474 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1475 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1476 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1477 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1478 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1479 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1483 // If we used this time granularity, how many ticks would there be?
1484 // This is only an approximation, but it's generally good enough.
1486 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1487 if (granularity
< Dygraph
.MONTHLY
) {
1488 // Generate one tick mark for every fixed interval of time.
1489 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1490 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1492 var year_mod
= 1; // e.g. to only print one point every 10 years.
1493 var num_months
= 12;
1494 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1495 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1496 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1497 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1499 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1500 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1501 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1507 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1508 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1510 // Returns an array containing {v: millis, label: label} dictionaries.
1512 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1513 var formatter
= this.attr_("xAxisLabelFormatter");
1515 if (granularity
< Dygraph
.MONTHLY
) {
1516 // Generate one tick mark for every fixed interval of time.
1517 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1518 var format
= '%d%b'; // e.g. "1Jan"
1520 // Find a time less than start_time which occurs on a "nice" time boundary
1521 // for this granularity.
1522 var g
= spacing
/ 1000;
1523 var d
= new Date(start_time
);
1524 if (g
<= 60) { // seconds
1525 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1529 if (g
<= 60) { // minutes
1530 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1535 if (g
<= 24) { // days
1536 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1541 if (g
== 7) { // one week
1542 d
.setDate(d
.getDate() - d
.getDay());
1547 start_time
= d
.getTime();
1549 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1550 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1553 // Display a tick mark on the first of a set of months of each year.
1554 // Years get a tick mark iff y % year_mod == 0. This is useful for
1555 // displaying a tick mark once every 10 years, say, on long time scales.
1557 var year_mod
= 1; // e.g. to only print one point every 10 years.
1559 if (granularity
== Dygraph
.MONTHLY
) {
1560 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1561 } else if (granularity
== Dygraph
.QUARTERLY
) {
1562 months
= [ 0, 3, 6, 9 ];
1563 } else if (granularity
== Dygraph
.BIANNUAL
) {
1565 } else if (granularity
== Dygraph
.ANNUAL
) {
1567 } else if (granularity
== Dygraph
.DECADAL
) {
1572 var start_year
= new Date(start_time
).getFullYear();
1573 var end_year
= new Date(end_time
).getFullYear();
1574 var zeropad
= Dygraph
.zeropad
;
1575 for (var i
= start_year
; i
<= end_year
; i
++) {
1576 if (i
% year_mod
!= 0) continue;
1577 for (var j
= 0; j
< months
.length
; j
++) {
1578 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1579 var t
= Date
.parse(date_str
);
1580 if (t
< start_time
|| t
> end_time
) continue;
1581 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1591 * Add ticks to the x-axis based on a date range.
1592 * @param {Number} startDate Start of the date window (millis since epoch)
1593 * @param {Number} endDate End of the date window (millis since epoch)
1594 * @return {Array.<Object>} Array of {label, value} tuples.
1597 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1599 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1600 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1601 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1608 return self
.GetXAxis(startDate
, endDate
, chosen
);
1610 // TODO(danvk): signal error.
1615 * Add ticks when the x axis has numbers on it (instead of dates)
1616 * @param {Number} startDate Start of the date window (millis since epoch)
1617 * @param {Number} endDate End of the date window (millis since epoch)
1619 * @param {function} attribute accessor function.
1620 * @return {Array.<Object>} Array of {label, value} tuples.
1623 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1624 var attr
= function(k
) {
1625 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1626 return self
.attr_(k
);
1631 for (var i
= 0; i
< vals
.length
; i
++) {
1632 ticks
.push({v
: vals
[i
]});
1636 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1637 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1638 // The first spacing greater than pixelsPerYLabel is what we use.
1639 // TODO(danvk): version that works on a log scale.
1640 if (attr("labelsKMG2")) {
1641 var mults
= [1, 2, 4, 8];
1643 var mults
= [1, 2, 5];
1645 var scale
, low_val
, high_val
, nTicks
;
1646 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1647 var pixelsPerTick
= attr('pixelsPerYLabel');
1648 for (var i
= -10; i
< 50; i
++) {
1649 if (attr("labelsKMG2")) {
1650 var base_scale
= Math
.pow(16, i
);
1652 var base_scale
= Math
.pow(10, i
);
1654 for (var j
= 0; j
< mults
.length
; j
++) {
1655 scale
= base_scale
* mults
[j
];
1656 low_val
= Math
.floor(minV
/ scale
) * scale
;
1657 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1658 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1659 var spacing
= self
.height_
/ nTicks
;
1660 // wish I could break out of both loops at once...
1661 if (spacing
> pixelsPerTick
) break;
1663 if (spacing
> pixelsPerTick
) break;
1666 // Construct the set of ticks.
1667 // Allow reverse y-axis if it's explicitly requested.
1668 if (low_val
> high_val
) scale
*= -1;
1669 for (var i
= 0; i
< nTicks
; i
++) {
1670 var tickV
= low_val
+ i
* scale
;
1671 ticks
.push( {v
: tickV
} );
1675 // Add formatted labels to the ticks.
1678 if (attr("labelsKMB")) {
1680 k_labels
= [ "K", "M", "B", "T" ];
1682 if (attr("labelsKMG2")) {
1683 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1685 k_labels
= [ "k", "M", "G", "T" ];
1687 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
1689 for (var i
= 0; i
< ticks
.length
; i
++) {
1690 var tickV
= ticks
[i
].v
;
1691 var absTickV
= Math
.abs(tickV
);
1693 if (formatter
!= undefined
) {
1694 label
= formatter(tickV
);
1696 label
= Dygraph
.round_(tickV
, 2);
1698 if (k_labels
.length
) {
1699 // Round up to an appropriate unit.
1701 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1702 if (absTickV
>= n
) {
1703 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1708 ticks
[i
].label
= label
;
1713 // Computes the range of the data series (including confidence intervals).
1714 // series is either [ [x1, y1], [x2, y2], ... ] or
1715 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1716 // Returns [low, high]
1717 Dygraph
.prototype.extremeValues_
= function(series
) {
1718 var minY
= null, maxY
= null;
1720 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1722 // With custom bars, maxY is the max of the high values.
1723 for (var j
= 0; j
< series
.length
; j
++) {
1724 var y
= series
[j
][1][0];
1726 var low
= y
- series
[j
][1][1];
1727 var high
= y
+ series
[j
][1][2];
1728 if (low
> y
) low
= y
; // this can happen with custom bars,
1729 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1730 if (maxY
== null || high
> maxY
) {
1733 if (minY
== null || low
< minY
) {
1738 for (var j
= 0; j
< series
.length
; j
++) {
1739 var y
= series
[j
][1];
1740 if (y
=== null || isNaN(y
)) continue;
1741 if (maxY
== null || y
> maxY
) {
1744 if (minY
== null || y
< minY
) {
1750 return [minY
, maxY
];
1754 * This function is called once when the chart's data is changed or the options
1755 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1756 * idea is that values derived from the chart's data can be computed here,
1757 * rather than every time the chart is drawn. This includes things like the
1758 * number of axes, rolling averages, etc.
1760 Dygraph
.prototype.predraw_
= function() {
1761 // TODO(danvk): move more computations out of drawGraph_ and into here.
1762 this.computeYAxes_();
1764 // Create a new plotter.
1765 if (this.plotter_
) this.plotter_
.clear();
1766 this.plotter_
= new DygraphCanvasRenderer(this,
1767 this.hidden_
, this.layout_
,
1768 this.renderOptions_
);
1770 // The roller sits in the bottom left corner of the chart. We don't know where
1771 // this will be until the options are available, so it's positioned here.
1772 this.roller_
= this.createRollInterface_();
1774 // Same thing applies for the labelsDiv. It's right edge should be flush with
1775 // the right edge of the charting area (which may not be the same as the right
1776 // edge of the div, if we have two y-axes.
1777 this.positionLabelsDiv_();
1779 // If the data or options have changed, then we'd better redraw.
1785 * Update the graph with new data. This method is called when the viewing area
1786 * has changed. If the underlying data or options have changed, predraw_ will
1787 * be called before drawGraph_ is called.
1790 Dygraph
.prototype.drawGraph_
= function() {
1791 var data
= this.rawData_
;
1793 // This is used to set the second parameter to drawCallback, below.
1794 var is_initial_draw
= this.is_initial_draw_
;
1795 this.is_initial_draw_
= false;
1797 var minY
= null, maxY
= null;
1798 this.layout_
.removeAllDatasets();
1800 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1802 // Loop over the fields (series). Go from the last to the first,
1803 // because if they're stacked that's how we accumulate the values.
1805 var cumulative_y
= []; // For stacked series.
1808 var extremes
= {}; // series name -> [low, high]
1810 // Loop over all fields and create datasets
1811 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1812 if (!this.visibility()[i
- 1]) continue;
1814 var seriesName
= this.attr_("labels")[i
];
1815 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1818 for (var j
= 0; j
< data
.length
; j
++) {
1819 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1820 var date
= data
[j
][0];
1821 series
.push([date
, data
[j
][i
]]);
1825 // TODO(danvk): move this into predraw_. It's insane to do it here.
1826 series
= this.rollingAverage(series
, this.rollPeriod_
);
1828 // Prune down to the desired range, if necessary (for zooming)
1829 // Because there can be lines going to points outside of the visible area,
1830 // we actually prune to visible points, plus one on either side.
1831 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1832 if (this.dateWindow_
) {
1833 var low
= this.dateWindow_
[0];
1834 var high
= this.dateWindow_
[1];
1836 // TODO(danvk): do binary search instead of linear search.
1837 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1838 var firstIdx
= null, lastIdx
= null;
1839 for (var k
= 0; k
< series
.length
; k
++) {
1840 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1843 if (series
[k
][0] <= high
) {
1847 if (firstIdx
=== null) firstIdx
= 0;
1848 if (firstIdx
> 0) firstIdx
--;
1849 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1850 if (lastIdx
< series
.length
- 1) lastIdx
++;
1851 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1852 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1853 pruned
.push(series
[k
]);
1857 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1860 var seriesExtremes
= this.extremeValues_(series
);
1861 extremes
[seriesName
] = seriesExtremes
;
1862 var thisMinY
= seriesExtremes
[0];
1863 var thisMaxY
= seriesExtremes
[1];
1864 if (minY
=== null || (thisMinY
!= null && thisMinY
< minY
)) minY
= thisMinY
;
1865 if (maxY
=== null || (thisMaxY
!= null && thisMaxY
> maxY
)) maxY
= thisMaxY
;
1868 for (var j
=0; j
<series
.length
; j
++) {
1869 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1872 } else if (this.attr_("stackedGraph")) {
1873 var l
= series
.length
;
1875 for (var j
= 0; j
< l
; j
++) {
1876 // If one data set has a NaN, let all subsequent stacked
1877 // sets inherit the NaN -- only start at 0 for the first set.
1878 var x
= series
[j
][0];
1879 if (cumulative_y
[x
] === undefined
)
1880 cumulative_y
[x
] = 0;
1882 actual_y
= series
[j
][1];
1883 cumulative_y
[x
] += actual_y
;
1885 series
[j
] = [x
, cumulative_y
[x
]]
1887 if (!maxY
|| cumulative_y
[x
] > maxY
)
1888 maxY
= cumulative_y
[x
];
1892 datasets
[i
] = series
;
1895 for (var i
= 1; i
< datasets
.length
; i
++) {
1896 if (!this.visibility()[i
- 1]) continue;
1897 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1900 // TODO(danvk): this method doesn't need to return anything.
1901 var out
= this.computeYAxisRanges_(extremes
);
1903 var seriesToAxisMap
= out
[1];
1904 this.displayedYRange_
= axes
[0].valueRange
;
1905 this.layout_
.updateOptions( { yAxes
: axes
,
1906 seriesToAxisMap
: seriesToAxisMap
1911 // Tell PlotKit to use this new data and render itself
1912 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1913 this.layout_
.evaluateWithError();
1914 this.plotter_
.clear();
1915 this.plotter_
.render();
1916 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1917 this.canvas_
.height
);
1919 if (this.attr_("drawCallback") !== null) {
1920 this.attr_("drawCallback")(this, is_initial_draw
);
1925 * Determine properties of the y-axes which are independent of the data
1926 * currently being displayed. This includes things like the number of axes and
1927 * the style of the axes. It does not include the range of each axis and its
1929 * This fills in this.axes_ and this.seriesToAxisMap_.
1930 * axes_ = [ { options } ]
1931 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
1932 * indices are into the axes_ array.
1934 Dygraph
.prototype.computeYAxes_
= function() {
1935 this.axes_
= [{}]; // always have at least one y-axis.
1936 this.seriesToAxisMap_
= {};
1938 // Get a list of series names.
1939 var labels
= this.attr_("labels");
1941 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
1943 // all options which could be applied per-axis:
1951 'axisLabelFontSize',
1955 // Copy global axis options over to the first axis.
1956 for (var i
= 0; i
< axisOptions
.length
; i
++) {
1957 var k
= axisOptions
[i
];
1958 var v
= this.attr_(k
);
1959 if (v
) this.axes_
[0][k
] = v
;
1962 // Go through once and add all the axes.
1963 for (var seriesName
in series
) {
1964 if (!series
.hasOwnProperty(seriesName
)) continue;
1965 var axis
= this.attr_("axis", seriesName
);
1967 this.seriesToAxisMap_
[seriesName
] = 0;
1970 if (typeof(axis
) == 'object') {
1971 // Add a new axis, making a copy of its per-axis options.
1973 Dygraph
.update(opts
, this.axes_
[0]);
1974 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
1975 Dygraph
.update(opts
, axis
);
1976 this.axes_
.push(opts
);
1977 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
1981 // Go through one more time and assign series to an axis defined by another
1982 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
1983 for (var seriesName
in series
) {
1984 if (!series
.hasOwnProperty(seriesName
)) continue;
1985 var axis
= this.attr_("axis", seriesName
);
1986 if (typeof(axis
) == 'string') {
1987 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
1988 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
1989 "series " + axis
+ ", which does not define its own axis.");
1992 var idx
= this.seriesToAxisMap_
[axis
];
1993 this.seriesToAxisMap_
[seriesName
] = idx
;
1999 * Returns the number of y-axes on the chart.
2000 * @return {Number} the number of axes.
2002 Dygraph
.prototype.numAxes
= function() {
2004 for (var series
in this.seriesToAxisMap_
) {
2005 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2006 var idx
= this.seriesToAxisMap_
[series
];
2007 if (idx
> last_axis
) last_axis
= idx
;
2009 return 1 + last_axis
;
2013 * Determine the value range and tick marks for each axis.
2014 * @param {Object} extremes A mapping from seriesName -> [low, high]
2015 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2017 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2018 // Build a map from axis number -> [list of series names]
2019 var seriesForAxis
= [];
2020 for (var series
in this.seriesToAxisMap_
) {
2021 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2022 var idx
= this.seriesToAxisMap_
[series
];
2023 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2024 seriesForAxis
[idx
].push(series
);
2027 // Compute extreme values, a span and tick marks for each axis.
2028 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2029 var axis
= this.axes_
[i
];
2030 if (axis
.valueRange
) {
2031 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2033 // Calcuate the extremes of extremes.
2034 var series
= seriesForAxis
[i
];
2035 var minY
= Infinity
; // extremes[series[0]][0];
2036 var maxY
= -Infinity
; // extremes[series[0]][1];
2037 for (var j
= 0; j
< series
.length
; j
++) {
2038 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2039 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2041 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2043 // Add some padding and round up to an integer to be human-friendly.
2044 var span
= maxY
- minY
;
2045 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2046 if (span
== 0) { span
= maxY
; }
2047 var maxAxisY
= maxY
+ 0.1 * span
;
2048 var minAxisY
= minY
- 0.1 * span
;
2050 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2051 if (!this.attr_("avoidMinZero")) {
2052 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2053 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2056 if (this.attr_("includeZero")) {
2057 if (maxY
< 0) maxAxisY
= 0;
2058 if (minY
> 0) minAxisY
= 0;
2061 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2064 // Add ticks. By default, all axes inherit the tick positions of the
2065 // primary axis. However, if an axis is specifically marked as having
2066 // independent ticks, then that is permissible as well.
2067 if (i
== 0 || axis
.independentTicks
) {
2069 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2070 axis
.computedValueRange
[1],
2074 var p_axis
= this.axes_
[0];
2075 var p_ticks
= p_axis
.ticks
;
2076 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2077 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2078 var tick_values
= [];
2079 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2080 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2081 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2082 tick_values
.push(y_val
);
2086 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2087 axis
.computedValueRange
[1],
2088 this, axis
, tick_values
);
2092 return [this.axes_
, this.seriesToAxisMap_
];
2096 * Calculates the rolling average of a data set.
2097 * If originalData is [label, val], rolls the average of those.
2098 * If originalData is [label, [, it's interpreted as [value, stddev]
2099 * and the roll is returned in the same form, with appropriately reduced
2100 * stddev for each value.
2101 * Note that this is where fractional input (i.e. '5/10') is converted into
2103 * @param {Array} originalData The data in the appropriate format (see above)
2104 * @param {Number} rollPeriod The number of days over which to average the data
2106 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2107 if (originalData
.length
< 2)
2108 return originalData
;
2109 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2110 var rollingData
= [];
2111 var sigma
= this.attr_("sigma");
2113 if (this.fractions_
) {
2115 var den
= 0; // numerator/denominator
2117 for (var i
= 0; i
< originalData
.length
; i
++) {
2118 num
+= originalData
[i
][1][0];
2119 den
+= originalData
[i
][1][1];
2120 if (i
- rollPeriod
>= 0) {
2121 num
-= originalData
[i
- rollPeriod
][1][0];
2122 den
-= originalData
[i
- rollPeriod
][1][1];
2125 var date
= originalData
[i
][0];
2126 var value
= den
? num
/ den
: 0.0;
2127 if (this.attr_("errorBars")) {
2128 if (this.wilsonInterval_
) {
2129 // For more details on this confidence interval, see:
2130 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2132 var p
= value
< 0 ? 0 : value
, n
= den
;
2133 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2134 var denom
= 1 + sigma
* sigma
/ den
;
2135 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2136 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2137 rollingData
[i
] = [date
,
2138 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2140 rollingData
[i
] = [date
, [0, 0, 0]];
2143 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2144 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2147 rollingData
[i
] = [date
, mult
* value
];
2150 } else if (this.attr_("customBars")) {
2155 for (var i
= 0; i
< originalData
.length
; i
++) {
2156 var data
= originalData
[i
][1];
2158 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2160 if (y
!= null && !isNaN(y
)) {
2166 if (i
- rollPeriod
>= 0) {
2167 var prev
= originalData
[i
- rollPeriod
];
2168 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2175 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2176 1.0 * (mid
- low
) / count
,
2177 1.0 * (high
- mid
) / count
]];
2180 // Calculate the rolling average for the first rollPeriod - 1 points where
2181 // there is not enough data to roll over the full number of days
2182 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2183 if (!this.attr_("errorBars")){
2184 if (rollPeriod
== 1) {
2185 return originalData
;
2188 for (var i
= 0; i
< originalData
.length
; i
++) {
2191 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2192 var y
= originalData
[j
][1];
2193 if (y
== null || isNaN(y
)) continue;
2195 sum
+= originalData
[j
][1];
2198 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2200 rollingData
[i
] = [originalData
[i
][0], null];
2205 for (var i
= 0; i
< originalData
.length
; i
++) {
2209 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2210 var y
= originalData
[j
][1][0];
2211 if (y
== null || isNaN(y
)) continue;
2213 sum
+= originalData
[j
][1][0];
2214 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2217 var stddev
= Math
.sqrt(variance
) / num_ok
;
2218 rollingData
[i
] = [originalData
[i
][0],
2219 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2221 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2231 * Parses a date, returning the number of milliseconds since epoch. This can be
2232 * passed in as an xValueParser in the Dygraph constructor.
2233 * TODO(danvk): enumerate formats that this understands.
2234 * @param {String} A date in YYYYMMDD format.
2235 * @return {Number} Milliseconds since epoch.
2238 Dygraph
.dateParser
= function(dateStr
, self
) {
2241 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2242 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2243 while (dateStrSlashed
.search("-") != -1) {
2244 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2246 d
= Date
.parse(dateStrSlashed
);
2247 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2248 // TODO(danvk): remove support for this format. It's confusing.
2249 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2250 + "/" + dateStr
.substr(6,2);
2251 d
= Date
.parse(dateStrSlashed
);
2253 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2254 // "2009/07/12 12:34:56"
2255 d
= Date
.parse(dateStr
);
2258 if (!d
|| isNaN(d
)) {
2259 self
.error("Couldn't parse " + dateStr
+ " as a date");
2265 * Detects the type of the str (date or numeric) and sets the various
2266 * formatting attributes in this.attrs_ based on this type.
2267 * @param {String} str An x value.
2270 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2272 if (str
.indexOf('-') >= 0 ||
2273 str
.indexOf('/') >= 0 ||
2274 isNaN(parseFloat(str
))) {
2276 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2277 // TODO(danvk): remove support for this format.
2282 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2283 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2284 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2285 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2287 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2288 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2289 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2290 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2295 * Parses a string in a special csv format. We expect a csv file where each
2296 * line is a date point, and the first field in each line is the date string.
2297 * We also expect that all remaining fields represent series.
2298 * if the errorBars attribute is set, then interpret the fields as:
2299 * date, series1, stddev1, series2, stddev2, ...
2300 * @param {Array.<Object>} data See above.
2303 * @return Array.<Object> An array with one entry for each row. These entries
2304 * are an array of cells in that row. The first entry is the parsed x-value for
2305 * the row. The second, third, etc. are the y-values. These can take on one of
2306 * three forms, depending on the CSV and constructor parameters:
2308 * 2. [ value, stddev ]
2309 * 3. [ low value, center value, high value ]
2311 Dygraph
.prototype.parseCSV_
= function(data
) {
2313 var lines
= data
.split("\n");
2315 // Use the default delimiter or fall back to a tab if that makes sense.
2316 var delim
= this.attr_('delimiter');
2317 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2322 if (this.labelsFromCSV_
) {
2324 this.attrs_
.labels
= lines
[0].split(delim
);
2327 // Parse the x as a float or return null if it's not a number.
2328 var parseFloatOrNull
= function(x
) {
2329 var val
= parseFloat(x
);
2330 return isNaN(val
) ? null : val
;
2334 var defaultParserSet
= false; // attempt to auto-detect x value type
2335 var expectedCols
= this.attr_("labels").length
;
2336 var outOfOrder
= false;
2337 for (var i
= start
; i
< lines
.length
; i
++) {
2338 var line
= lines
[i
];
2339 if (line
.length
== 0) continue; // skip blank lines
2340 if (line
[0] == '#') continue; // skip comment lines
2341 var inFields
= line
.split(delim
);
2342 if (inFields
.length
< 2) continue;
2345 if (!defaultParserSet
) {
2346 this.detectTypeFromString_(inFields
[0]);
2347 xParser
= this.attr_("xValueParser");
2348 defaultParserSet
= true;
2350 fields
[0] = xParser(inFields
[0], this);
2352 // If fractions are expected, parse the numbers as "A/B
"
2353 if (this.fractions_) {
2354 for (var j = 1; j < inFields.length; j++) {
2355 // TODO(danvk): figure out an appropriate way to flag parse errors.
2356 var vals = inFields[j].split("/");
2357 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2359 } else if (this.attr_("errorBars
")) {
2360 // If there are error bars, values are (value, stddev) pairs
2361 for (var j = 1; j < inFields.length; j += 2)
2362 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2363 parseFloatOrNull(inFields[j + 1])];
2364 } else if (this.attr_("customBars
")) {
2365 // Bars are a low;center;high tuple
2366 for (var j = 1; j < inFields.length; j++) {
2367 var vals = inFields[j].split(";");
2368 fields[j] = [ parseFloatOrNull(vals[0]),
2369 parseFloatOrNull(vals[1]),
2370 parseFloatOrNull(vals[2]) ];
2373 // Values are just numbers
2374 for (var j = 1; j < inFields.length; j++) {
2375 fields[j] = parseFloatOrNull(inFields[j]);
2378 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2383 if (fields.length != expectedCols) {
2384 this.error("Number of columns
in line
" + i + " (" + fields.length +
2385 ") does not agree
with number of
labels (" + expectedCols +
2391 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2392 ret.sort(function(a,b) { return a[0] - b[0] });
2399 * The user has provided their data as a pre-packaged JS array. If the x values
2400 * are numeric, this is the same as dygraphs' internal format. If the x values
2401 * are dates, we need to convert them from Date objects to ms since epoch.
2402 * @param {Array.<Object>} data
2403 * @return {Array.<Object>} data with numeric x values.
2405 Dygraph.prototype.parseArray_ = function(data) {
2406 // Peek at the first x value to see if it's numeric.
2407 if (data.length == 0) {
2408 this.error("Can
't plot empty data set");
2411 if (data[0].length == 0) {
2412 this.error("Data set cannot contain an empty row");
2416 if (this.attr_("labels") == null) {
2417 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2418 "in the options parameter");
2419 this.attrs_.labels = [ "X" ];
2420 for (var i = 1; i < data[0].length; i++) {
2421 this.attrs_.labels.push("Y" + i);
2425 if (Dygraph.isDateLike(data[0][0])) {
2426 // Some intelligent defaults for a date x-axis.
2427 this.attrs_.xValueFormatter = Dygraph.dateString_;
2428 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2429 this.attrs_.xTicker = Dygraph.dateTicker;
2431 // Assume they're all dates
.
2432 var parsedData
= Dygraph
.clone(data
);
2433 for (var i
= 0; i
< data
.length
; i
++) {
2434 if (parsedData
[i
].length
== 0) {
2435 this.error("Row " + (1 + i
) + " of data is empty");
2438 if (parsedData
[i
][0] == null
2439 || typeof(parsedData
[i
][0].getTime
) != 'function'
2440 || isNaN(parsedData
[i
][0].getTime())) {
2441 this.error("x value in row " + (1 + i
) + " is not a Date");
2444 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2448 // Some intelligent defaults for a numeric x-axis.
2449 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2450 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2456 * Parses a DataTable object from gviz.
2457 * The data is expected to have a first column that is either a date or a
2458 * number. All subsequent columns must be numbers. If there is a clear mismatch
2459 * between this.xValueParser_ and the type of the first column, it will be
2460 * fixed. Fills out rawData_.
2461 * @param {Array.<Object>} data See above.
2464 Dygraph
.prototype.parseDataTable_
= function(data
) {
2465 var cols
= data
.getNumberOfColumns();
2466 var rows
= data
.getNumberOfRows();
2468 var indepType
= data
.getColumnType(0);
2469 if (indepType
== 'date' || indepType
== 'datetime') {
2470 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2471 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2472 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2473 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2474 } else if (indepType
== 'number') {
2475 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2476 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2477 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2478 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2480 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2481 "column 1 of DataTable input (Got '" + indepType
+ "')");
2485 // Array of the column indices which contain data (and not annotations).
2487 var annotationCols
= {}; // data index -> [annotation cols]
2488 var hasAnnotations
= false;
2489 for (var i
= 1; i
< cols
; i
++) {
2490 var type
= data
.getColumnType(i
);
2491 if (type
== 'number') {
2493 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2494 // This is OK -- it's an annotation column.
2495 var dataIdx
= colIdx
[colIdx
.length
- 1];
2496 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2497 annotationCols
[dataIdx
] = [i
];
2499 annotationCols
[dataIdx
].push(i
);
2501 hasAnnotations
= true;
2503 this.error("Only 'number' is supported as a dependent type with Gviz." +
2504 " 'string' is only supported if displayAnnotations is true");
2508 // Read column labels
2509 // TODO(danvk): add support back for errorBars
2510 var labels
= [data
.getColumnLabel(0)];
2511 for (var i
= 0; i
< colIdx
.length
; i
++) {
2512 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2513 if (this.attr_("errorBars")) i
+= 1;
2515 this.attrs_
.labels
= labels
;
2516 cols
= labels
.length
;
2519 var outOfOrder
= false;
2520 var annotations
= [];
2521 for (var i
= 0; i
< rows
; i
++) {
2523 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2524 data
.getValue(i
, 0) === null) {
2525 this.warn("Ignoring row " + i
+
2526 " of DataTable because of undefined or null first column.");
2530 if (indepType
== 'date' || indepType
== 'datetime') {
2531 row
.push(data
.getValue(i
, 0).getTime());
2533 row
.push(data
.getValue(i
, 0));
2535 if (!this.attr_("errorBars")) {
2536 for (var j
= 0; j
< colIdx
.length
; j
++) {
2537 var col
= colIdx
[j
];
2538 row
.push(data
.getValue(i
, col
));
2539 if (hasAnnotations
&&
2540 annotationCols
.hasOwnProperty(col
) &&
2541 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2543 ann
.series
= data
.getColumnLabel(col
);
2545 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2547 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2548 if (k
) ann
.text
+= "\n";
2549 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2551 annotations
.push(ann
);
2555 for (var j
= 0; j
< cols
- 1; j
++) {
2556 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2559 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2566 this.warn("DataTable is out of order; order it correctly to speed loading.");
2567 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2569 this.rawData_
= ret
;
2571 if (annotations
.length
> 0) {
2572 this.setAnnotations(annotations
, true);
2576 // These functions are all based on MochiKit.
2577 Dygraph
.update
= function (self
, o
) {
2578 if (typeof(o
) != 'undefined' && o
!== null) {
2580 if (o
.hasOwnProperty(k
)) {
2588 Dygraph
.isArrayLike
= function (o
) {
2589 var typ
= typeof(o
);
2591 (typ
!= 'object' && !(typ
== 'function' &&
2592 typeof(o
.item
) == 'function')) ||
2594 typeof(o
.length
) != 'number' ||
2602 Dygraph
.isDateLike
= function (o
) {
2603 if (typeof(o
) != "object" || o
=== null ||
2604 typeof(o
.getTime
) != 'function') {
2610 Dygraph
.clone
= function(o
) {
2611 // TODO(danvk): figure out how MochiKit's version works
2613 for (var i
= 0; i
< o
.length
; i
++) {
2614 if (Dygraph
.isArrayLike(o
[i
])) {
2615 r
.push(Dygraph
.clone(o
[i
]));
2625 * Get the CSV data. If it's in a function, call that function. If it's in a
2626 * file, do an XMLHttpRequest to get it.
2629 Dygraph
.prototype.start_
= function() {
2630 if (typeof this.file_
== 'function') {
2631 // CSV string. Pretend we got it via XHR.
2632 this.loadedEvent_(this.file_());
2633 } else if (Dygraph
.isArrayLike(this.file_
)) {
2634 this.rawData_
= this.parseArray_(this.file_
);
2636 } else if (typeof this.file_
== 'object' &&
2637 typeof this.file_
.getColumnRange
== 'function') {
2638 // must be a DataTable from gviz.
2639 this.parseDataTable_(this.file_
);
2641 } else if (typeof this.file_
== 'string') {
2642 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2643 if (this.file_
.indexOf('\n') >= 0) {
2644 this.loadedEvent_(this.file_
);
2646 var req
= new XMLHttpRequest();
2648 req
.onreadystatechange
= function () {
2649 if (req
.readyState
== 4) {
2650 if (req
.status
== 200) {
2651 caller
.loadedEvent_(req
.responseText
);
2656 req
.open("GET", this.file_
, true);
2660 this.error("Unknown data format: " + (typeof this.file_
));
2665 * Changes various properties of the graph. These can include:
2667 * <li>file: changes the source data for the graph</li>
2668 * <li>errorBars: changes whether the data contains stddev</li>
2670 * @param {Object} attrs The new properties and values
2672 Dygraph
.prototype.updateOptions
= function(attrs
) {
2673 // TODO(danvk): this is a mess. Rethink this function.
2674 if ('rollPeriod' in attrs
) {
2675 this.rollPeriod_
= attrs
.rollPeriod
;
2677 if ('dateWindow' in attrs
) {
2678 this.dateWindow_
= attrs
.dateWindow
;
2681 // TODO(danvk): validate per-series options.
2686 // highlightCircleSize
2688 Dygraph
.update(this.user_attrs_
, attrs
);
2689 Dygraph
.update(this.renderOptions_
, attrs
);
2691 this.labelsFromCSV_
= (this.attr_("labels") == null);
2693 // TODO(danvk): this doesn't match the constructor logic
2694 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2695 if (attrs
['file']) {
2696 this.file_
= attrs
['file'];
2704 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2705 * containing div (which has presumably changed size since the dygraph was
2706 * instantiated. If the width/height are specified, the div will be resized.
2708 * This is far more efficient than destroying and re-instantiating a
2709 * Dygraph, since it doesn't have to reparse the underlying data.
2711 * @param {Number} width Width (in pixels)
2712 * @param {Number} height Height (in pixels)
2714 Dygraph
.prototype.resize
= function(width
, height
) {
2715 if (this.resize_lock
) {
2718 this.resize_lock
= true;
2720 if ((width
=== null) != (height
=== null)) {
2721 this.warn("Dygraph.resize() should be called with zero parameters or " +
2722 "two non-NULL parameters. Pretending it was zero.");
2723 width
= height
= null;
2726 // TODO(danvk): there should be a clear() method.
2727 this.maindiv_
.innerHTML
= "";
2728 this.attrs_
.labelsDiv
= null;
2731 this.maindiv_
.style
.width
= width
+ "px";
2732 this.maindiv_
.style
.height
= height
+ "px";
2733 this.width_
= width
;
2734 this.height_
= height
;
2736 this.width_
= this.maindiv_
.offsetWidth
;
2737 this.height_
= this.maindiv_
.offsetHeight
;
2740 this.createInterface_();
2743 this.resize_lock
= false;
2747 * Adjusts the number of days in the rolling average. Updates the graph to
2748 * reflect the new averaging period.
2749 * @param {Number} length Number of days over which to average the data.
2751 Dygraph
.prototype.adjustRoll
= function(length
) {
2752 this.rollPeriod_
= length
;
2757 * Returns a boolean array of visibility statuses.
2759 Dygraph
.prototype.visibility
= function() {
2760 // Do lazy-initialization, so that this happens after we know the number of
2762 if (!this.attr_("visibility")) {
2763 this.attrs_
["visibility"] = [];
2765 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2766 this.attr_("visibility").push(true);
2768 return this.attr_("visibility");
2772 * Changes the visiblity of a series.
2774 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2775 var x
= this.visibility();
2776 if (num
< 0 && num
>= x
.length
) {
2777 this.warn("invalid series number in setVisibility: " + num
);
2785 * Update the list of annotations and redraw the chart.
2787 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2788 // Only add the annotation CSS rule once we know it will be used.
2789 Dygraph
.addAnnotationRule();
2790 this.annotations_
= ann
;
2791 this.layout_
.setAnnotations(this.annotations_
);
2792 if (!suppressDraw
) {
2798 * Return the list of annotations.
2800 Dygraph
.prototype.annotations
= function() {
2801 return this.annotations_
;
2805 * Get the index of a series (column) given its name. The first column is the
2806 * x-axis, so the data series start with index 1.
2808 Dygraph
.prototype.indexFromSetName
= function(name
) {
2809 var labels
= this.attr_("labels");
2810 for (var i
= 0; i
< labels
.length
; i
++) {
2811 if (labels
[i
] == name
) return i
;
2816 Dygraph
.addAnnotationRule
= function() {
2817 if (Dygraph
.addedAnnotationCSS
) return;
2820 if (document
.styleSheets
.length
> 0) {
2821 mysheet
= document
.styleSheets
[0];
2823 var styleSheetElement
= document
.createElement("style");
2824 styleSheetElement
.type
= "text/css";
2825 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2826 for(i
= 0; i
< document
.styleSheets
.length
; i
++) {
2827 if (document
.styleSheets
[i
].disabled
) continue;
2828 mysheet
= document
.styleSheets
[i
];
2832 var rule
= "border: 1px solid black; " +
2833 "background-color: white; " +
2834 "text-align: center;";
2835 if (mysheet
.insertRule
) { // Firefox
2836 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2837 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2838 } else if (mysheet
.addRule
) { // IE
2839 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2842 Dygraph
.addedAnnotationCSS
= true;
2846 * Create a new canvas element. This is more complex than a simple
2847 * document.createElement("canvas") because of IE and excanvas.
2849 Dygraph
.createCanvas
= function() {
2850 var canvas
= document
.createElement("canvas");
2852 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2853 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
2854 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2862 * A wrapper around Dygraph that implements the gviz API.
2863 * @param {Object} container The DOM object the visualization should live in.
2865 Dygraph
.GVizChart
= function(container
) {
2866 this.container
= container
;
2869 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2870 this.container
.innerHTML
= '';
2871 this.date_graph
= new Dygraph(this.container
, data
, options
);
2875 * Google charts compatible setSelection
2876 * Only row selection is supported, all points in the row will be highlighted
2877 * @param {Array} array of the selected cells
2880 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2882 if (selection_array
.length
) {
2883 row
= selection_array
[0].row
;
2885 this.date_graph
.setSelection(row
);
2889 * Google charts compatible getSelection implementation
2890 * @return {Array} array of the selected cells
2893 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2896 var row
= this.date_graph
.getSelection();
2898 if (row
< 0) return selection
;
2901 for (var i
in this.date_graph
.layout_
.datasets
) {
2902 selection
.push({row
: row
, column
: col
});
2909 // Older pages may still use this name.
2910 DateGraph
= Dygraph
;