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 // Directions for panning and zooming. Use bit operations when combined
139 // values are possible.
140 Dygraph
.HORIZONTAL
= 1;
141 Dygraph
.VERTICAL
= 2;
143 // Used for initializing annotation CSS rules only once.
144 Dygraph
.addedAnnotationCSS
= false;
146 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
147 // Labels is no longer a constructor parameter, since it's typically set
148 // directly from the data source. It also conains a name for the x-axis,
149 // which the previous constructor form did not.
150 if (labels
!= null) {
151 var new_labels
= ["Date"];
152 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
153 Dygraph
.update(attrs
, { 'labels': new_labels
});
155 this.__init__(div
, file
, attrs
);
159 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
160 * and interaction <canvas> inside of it. See the constructor for details
162 * @param {Element} div the Element to render the graph into.
163 * @param {String | Function} file Source data
164 * @param {Object} attrs Miscellaneous other options
167 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
168 // Support two-argument constructor
169 if (attrs
== null) { attrs
= {}; }
171 // Copy the important bits into the object
172 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
175 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
176 this.previousVerticalX_
= -1;
177 this.fractions_
= attrs
.fractions
|| false;
178 this.dateWindow_
= attrs
.dateWindow
|| null;
179 // valueRange and valueWindow are similar, but not the same. valueRange is a
180 // locally-stored copy of the attribute. valueWindow starts off the same as
181 // valueRange but is impacted by zoom or pan effects. valueRange is kept
182 // around to restore the original value back to valueRange.
183 this.valueRange_
= attrs
.valueRange
|| null;
184 this.valueWindow_
= this.valueRange_
;
186 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
187 this.is_initial_draw_
= true;
188 this.annotations_
= [];
190 // Clear the div. This ensure that, if multiple dygraphs are passed the same
191 // div, then only one will be drawn.
194 // If the div isn't already sized then inherit from our attrs or
195 // give it a default size.
196 if (div
.style
.width
== '') {
197 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
199 if (div
.style
.height
== '') {
200 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
202 this.width_
= parseInt(div
.style
.width
, 10);
203 this.height_
= parseInt(div
.style
.height
, 10);
204 // The div might have been specified as percent of the current window size,
205 // convert that to an appropriate number of pixels.
206 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
207 this.width_
= div
.offsetWidth
;
209 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
210 this.height_
= div
.offsetHeight
;
213 if (this.width_
== 0) {
214 this.error("dygraph has zero width. Please specify a width in pixels.");
216 if (this.height_
== 0) {
217 this.error("dygraph has zero height. Please specify a height in pixels.");
220 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
221 if (attrs
['stackedGraph']) {
222 attrs
['fillGraph'] = true;
223 // TODO(nikhilk): Add any other stackedGraph checks here.
226 // Dygraphs has many options, some of which interact with one another.
227 // To keep track of everything, we maintain two sets of options:
229 // this.user_attrs_ only options explicitly set by the user.
230 // this.attrs_ defaults, options derived from user_attrs_, data.
232 // Options are then accessed this.attr_('attr'), which first looks at
233 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
234 // defaults without overriding behavior that the user specifically asks for.
235 this.user_attrs_
= {};
236 Dygraph
.update(this.user_attrs_
, attrs
);
239 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
241 this.boundaryIds_
= [];
243 // Make a note of whether labels will be pulled from the CSV file.
244 this.labelsFromCSV_
= (this.attr_("labels") == null);
246 Dygraph
.addAnnotationRule();
248 // Create the containing DIV and other interactive elements
249 this.createInterface_();
254 Dygraph
.prototype.attr_
= function(name
) {
255 if (typeof(this.user_attrs_
[name
]) != 'undefined') {
256 return this.user_attrs_
[name
];
257 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
258 return this.attrs_
[name
];
264 // TODO(danvk): any way I can get the line numbers to be this.warn call?
265 Dygraph
.prototype.log
= function(severity
, message
) {
266 if (typeof(console
) != 'undefined') {
269 console
.debug('dygraphs: ' + message
);
272 console
.info('dygraphs: ' + message
);
274 case Dygraph
.WARNING
:
275 console
.warn('dygraphs: ' + message
);
278 console
.error('dygraphs: ' + message
);
283 Dygraph
.prototype.info
= function(message
) {
284 this.log(Dygraph
.INFO
, message
);
286 Dygraph
.prototype.warn
= function(message
) {
287 this.log(Dygraph
.WARNING
, message
);
289 Dygraph
.prototype.error
= function(message
) {
290 this.log(Dygraph
.ERROR
, message
);
294 * Returns the current rolling period, as set by the user or an option.
295 * @return {Number} The number of days in the rolling window
297 Dygraph
.prototype.rollPeriod
= function() {
298 return this.rollPeriod_
;
302 * Returns the currently-visible x-range. This can be affected by zooming,
303 * panning or a call to updateOptions.
304 * Returns a two-element array: [left, right].
305 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
307 Dygraph
.prototype.xAxisRange
= function() {
308 if (this.dateWindow_
) return this.dateWindow_
;
310 // The entire chart is visible.
311 var left
= this.rawData_
[0][0];
312 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
313 return [left
, right
];
317 * Returns the currently-visible y-range. This can be affected by zooming,
318 * panning or a call to updateOptions.
319 * Returns a two-element array: [bottom, top].
321 Dygraph
.prototype.yAxisRange
= function() {
322 return this.displayedYRange_
;
326 * Convert from data coordinates to canvas/div X/Y coordinates.
327 * Returns a two-element array: [X, Y]
329 Dygraph
.prototype.toDomCoords
= function(x
, y
) {
330 var ret
= [null, null];
331 var area
= this.plotter_
.area
;
333 var xRange
= this.xAxisRange();
334 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
338 var yRange
= this.yAxisRange();
339 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
345 // TODO(danvk): use these functions throughout dygraphs.
347 * Convert from canvas/div coords to data coordinates.
348 * Returns a two-element array: [X, Y]
350 Dygraph
.prototype.toDataCoords
= function(x
, y
) {
351 var ret
= [null, null];
352 var area
= this.plotter_
.area
;
354 var xRange
= this.xAxisRange();
355 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
359 var yRange
= this.yAxisRange();
360 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
367 * Returns the number of columns (including the independent variable).
369 Dygraph
.prototype.numColumns
= function() {
370 return this.rawData_
[0].length
;
374 * Returns the number of rows (excluding any header/label row).
376 Dygraph
.prototype.numRows
= function() {
377 return this.rawData_
.length
;
381 * Returns the value in the given row and column. If the row and column exceed
382 * the bounds on the data, returns null. Also returns null if the value is
385 Dygraph
.prototype.getValue
= function(row
, col
) {
386 if (row
< 0 || row
> this.rawData_
.length
) return null;
387 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
389 return this.rawData_
[row
][col
];
392 Dygraph
.addEvent
= function(el
, evt
, fn
) {
393 var normed_fn
= function(e
) {
394 if (!e
) var e
= window
.event
;
397 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
398 el
.addEventListener(evt
, normed_fn
, false);
400 el
.attachEvent('on' + evt
, normed_fn
);
404 Dygraph
.clipCanvas_
= function(cnv
, clip
) {
405 var ctx
= cnv
.getContext("2d");
407 ctx
.rect(clip
.left
, clip
.top
, clip
.width
, clip
.height
);
412 * Generates interface elements for the Dygraph: a containing div, a div to
413 * display the current point, and a textbox to adjust the rolling average
414 * period. Also creates the Renderer/Layout elements.
417 Dygraph
.prototype.createInterface_
= function() {
418 // Create the all-enclosing graph div
419 var enclosing
= this.maindiv_
;
421 this.graphDiv
= document
.createElement("div");
422 this.graphDiv
.style
.width
= this.width_
+ "px";
423 this.graphDiv
.style
.height
= this.height_
+ "px";
424 enclosing
.appendChild(this.graphDiv
);
428 left
: this.attr_("yAxisLabelWidth") + 2 * this.attr_("axisTickSize")
430 clip
.width
= this.width_
- clip
.left
- this.attr_("rightGap");
431 clip
.height
= this.height_
- this.attr_("axisLabelFontSize")
432 - 2 * this.attr_("axisTickSize");
433 this.clippingArea_
= clip
;
435 // Create the canvas for interactive parts of the chart.
436 this.canvas_
= Dygraph
.createCanvas();
437 this.canvas_
.style
.position
= "absolute";
438 this.canvas_
.width
= this.width_
;
439 this.canvas_
.height
= this.height_
;
440 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
441 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
443 // ... and for static parts of the chart.
444 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
446 // The interactive parts of the graph are drawn on top of the chart.
447 this.graphDiv
.appendChild(this.hidden_
);
448 this.graphDiv
.appendChild(this.canvas_
);
449 this.mouseEventElement_
= this.canvas_
;
451 // Make sure we don't overdraw.
452 Dygraph
.clipCanvas_(this.hidden_
, this.clippingArea_
);
453 Dygraph
.clipCanvas_(this.canvas_
, this.clippingArea_
);
456 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
457 dygraph
.mouseMove_(e
);
459 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
460 dygraph
.mouseOut_(e
);
463 // Create the grapher
464 // TODO(danvk): why does the Layout need its own set of options?
465 this.layoutOptions_
= { 'xOriginIsZero': false };
466 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
467 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
468 Dygraph
.update(this.layoutOptions_
, {
469 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
471 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
473 // TODO(danvk): why does the Renderer need its own set of options?
474 this.renderOptions_
= { colorScheme
: this.colors_
,
476 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
477 Dygraph
.update(this.renderOptions_
, this.attrs_
);
478 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
479 this.plotter_
= new DygraphCanvasRenderer(this,
480 this.hidden_
, this.layout_
,
481 this.renderOptions_
);
483 this.createStatusMessage_();
484 this.createRollInterface_();
485 this.createDragInterface_();
489 * Detach DOM elements in the dygraph and null out all data references.
490 * Calling this when you're done with a dygraph can dramatically reduce memory
491 * usage. See, e.g., the tests/perf.html example.
493 Dygraph
.prototype.destroy
= function() {
494 var removeRecursive
= function(node
) {
495 while (node
.hasChildNodes()) {
496 removeRecursive(node
.firstChild
);
497 node
.removeChild(node
.firstChild
);
500 removeRecursive(this.maindiv_
);
502 var nullOut
= function(obj
) {
504 if (typeof(obj
[n
]) === 'object') {
510 // These may not all be necessary, but it can't hurt...
511 nullOut(this.layout_
);
512 nullOut(this.plotter_
);
517 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
518 * this particular canvas. All Dygraph work is done on this.canvas_.
519 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
520 * @return {Object} The newly-created canvas
523 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
524 var h
= Dygraph
.createCanvas();
525 h
.style
.position
= "absolute";
526 // TODO(danvk): h should be offset from canvas. canvas needs to include
527 // some extra area to make it easier to zoom in on the far left and far
528 // right. h needs to be precisely the plot area, so that clipping occurs.
529 h
.style
.top
= canvas
.style
.top
;
530 h
.style
.left
= canvas
.style
.left
;
531 h
.width
= this.width_
;
532 h
.height
= this.height_
;
533 h
.style
.width
= this.width_
+ "px"; // for IE
534 h
.style
.height
= this.height_
+ "px"; // for IE
538 // Taken from MochiKit.Color
539 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
543 if (saturation
=== 0) {
548 var i
= Math
.floor(hue
* 6);
549 var f
= (hue
* 6) - i
;
550 var p
= value
* (1 - saturation
);
551 var q
= value
* (1 - (saturation
* f
));
552 var t
= value
* (1 - (saturation
* (1 - f
)));
554 case 1: red
= q
; green
= value
; blue
= p
; break;
555 case 2: red
= p
; green
= value
; blue
= t
; break;
556 case 3: red
= p
; green
= q
; blue
= value
; break;
557 case 4: red
= t
; green
= p
; blue
= value
; break;
558 case 5: red
= value
; green
= p
; blue
= q
; break;
559 case 6: // fall through
560 case 0: red
= value
; green
= t
; blue
= p
; break;
563 red
= Math
.floor(255 * red
+ 0.5);
564 green
= Math
.floor(255 * green
+ 0.5);
565 blue
= Math
.floor(255 * blue
+ 0.5);
566 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
571 * Generate a set of distinct colors for the data series. This is done with a
572 * color wheel. Saturation/Value are customizable, and the hue is
573 * equally-spaced around the color wheel. If a custom set of colors is
574 * specified, that is used instead.
577 Dygraph
.prototype.setColors_
= function() {
578 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
579 // away with this.renderOptions_.
580 var num
= this.attr_("labels").length
- 1;
582 var colors
= this.attr_('colors');
584 var sat
= this.attr_('colorSaturation') || 1.0;
585 var val
= this.attr_('colorValue') || 0.5;
586 var half
= Math
.ceil(num
/ 2);
587 for (var i
= 1; i
<= num
; i
++) {
588 if (!this.visibility()[i
-1]) continue;
589 // alternate colors for high contrast.
590 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
591 var hue
= (1.0 * idx
/ (1 + num
));
592 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
595 for (var i
= 0; i
< num
; i
++) {
596 if (!this.visibility()[i
]) continue;
597 var colorStr
= colors
[i
% colors
.length
];
598 this.colors_
.push(colorStr
);
602 // TODO(danvk): update this w/r
/t/ the
new options system
.
603 this.renderOptions_
.colorScheme
= this.colors_
;
604 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
605 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
606 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
610 * Return the list of colors. This is either the list of colors passed in the
611 * attributes, or the autogenerated list of rgb(r,g,b) strings.
612 * @return {Array<string>} The list of colors.
614 Dygraph
.prototype.getColors
= function() {
618 // The following functions are from quirksmode.org with a modification for Safari from
619 // http://blog.firetree.net/2005/07/04/javascript-find-position/
620 // http://www.quirksmode.org/js
/findpos
.html
621 Dygraph
.findPosX
= function(obj
) {
626 curleft
+= obj
.offsetLeft
;
627 if(!obj
.offsetParent
)
629 obj
= obj
.offsetParent
;
636 Dygraph
.findPosY
= function(obj
) {
641 curtop
+= obj
.offsetTop
;
642 if(!obj
.offsetParent
)
644 obj
= obj
.offsetParent
;
654 * Create the div that contains information on the selected point(s)
655 * This goes in the top right of the canvas, unless an external div has already
659 Dygraph
.prototype.createStatusMessage_
= function() {
660 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
661 if (userLabelsDiv
&& null != userLabelsDiv
662 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
663 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
665 if (!this.attr_("labelsDiv")) {
666 var divWidth
= this.attr_('labelsDivWidth');
668 "position": "absolute",
671 "width": divWidth
+ "px",
673 "left": (this.width_
- divWidth
- 2) + "px",
674 "background": "white",
676 "overflow": "hidden"};
677 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
678 var div
= document
.createElement("div");
679 for (var name
in messagestyle
) {
680 if (messagestyle
.hasOwnProperty(name
)) {
681 div
.style
[name
] = messagestyle
[name
];
684 this.graphDiv
.appendChild(div
);
685 this.attrs_
.labelsDiv
= div
;
690 * Create the text box to adjust the averaging period
691 * @return {Object} The newly-created text box
694 Dygraph
.prototype.createRollInterface_
= function() {
695 var display
= this.attr_('showRoller') ? "block" : "none";
696 var textAttr
= { "position": "absolute",
698 "top": (this.plotter_
.area
.h
- 25) + "px",
699 "left": (this.plotter_
.area
.x
+ 1) + "px",
702 var roller
= document
.createElement("input");
703 roller
.type
= "text";
705 roller
.value
= this.rollPeriod_
;
706 for (var name
in textAttr
) {
707 if (textAttr
.hasOwnProperty(name
)) {
708 roller
.style
[name
] = textAttr
[name
];
712 var pa
= this.graphDiv
;
713 pa
.appendChild(roller
);
715 roller
.onchange
= function() { dygraph
.adjustRoll(roller
.value
); };
719 // These functions are taken from MochiKit.Signal
720 Dygraph
.pageX
= function(e
) {
722 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
725 var b
= document
.body
;
727 (de
.scrollLeft
|| b
.scrollLeft
) -
728 (de
.clientLeft
|| 0);
732 Dygraph
.pageY
= function(e
) {
734 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
737 var b
= document
.body
;
739 (de
.scrollTop
|| b
.scrollTop
) -
745 * Set up all the mouse handlers needed to capture dragging behavior for zoom
749 Dygraph
.prototype.createDragInterface_
= function() {
752 // Tracks whether the mouse is down right now
753 var isZooming
= false;
754 var isPanning
= false;
755 var dragStartX
= null;
756 var dragStartY
= null;
761 var prevDragDirection
= null;
763 // draggingDate and draggingValue represent the [date,value] point on the
764 // graph at which the mouse was pressed. As the mouse moves while panning,
765 // the viewport must pan so that the mouse position points to
766 // [draggingDate, draggingValue]
767 var draggingDate
= null;
768 var draggingValue
= null;
770 // The range in second/value units that the viewport encompasses during a
771 // panning operation.
772 var dateRange
= null;
773 var valueRange
= null;
775 // Utility function to convert page-wide coordinates to canvas coords
778 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
779 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
781 // Draw zoom rectangles when the mouse is down and the user moves around
782 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
784 dragEndX
= getX(event
);
785 dragEndY
= getY(event
);
787 var xDelta
= Math
.abs(dragStartX
- dragEndX
);
788 var yDelta
= Math
.abs(dragStartY
- dragEndY
);
789 var dragDirection
= (xDelta
< yDelta
) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
791 self
.drawZoomRect_(dragDirection
, dragStartX
, dragEndX
, dragStartY
, dragEndY
,
792 prevDragDirection
, prevEndX
, prevEndY
);
796 prevDragDirection
= dragDirection
;
797 } else if (isPanning
) {
798 dragEndX
= getX(event
);
799 dragEndY
= getY(event
);
801 // Want to have it so that:
802 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
803 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
804 // 3. draggingValue appears at dragEndY.
805 // 4. valueRange is unaltered.
807 var minDate
= draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
808 var maxDate
= minDate
+ dateRange
;
809 self
.dateWindow_
= [minDate
, maxDate
];
811 var maxValue
= draggingValue
+ (dragEndY
/ self
.height_
) * valueRange
;
812 var minValue
= maxValue
- valueRange
;
813 self
.valueWindow_
= [ minValue
, maxValue
];
814 self
.drawGraph_(self
.rawData_
);
818 // Track the beginning of drag events
819 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
820 px
= Dygraph
.findPosX(self
.canvas_
);
821 py
= Dygraph
.findPosY(self
.canvas_
);
822 dragStartX
= getX(event
);
823 dragStartY
= getY(event
);
825 if (event
.altKey
|| event
.shiftKey
) {
826 // have to be zoomed in to pan.
827 if (!self
.dateWindow_
&& !self
.valueWindow_
) return;
830 var xRange
= self
.xAxisRange();
831 dateRange
= xRange
[1] - xRange
[0];
832 var yRange
= self
.yAxisRange();
833 valueRange
= yRange
[1] - yRange
[0];
835 // TODO(konigsberg): Switch from all this math to toDataCoords?
836 // Seems to work for the dragging value.
837 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+
839 var r
= self
.toDataCoords(null, dragStartY
);
840 draggingValue
= r
[1];
846 // If the user releases the mouse button during a drag, but not over the
847 // canvas, then it doesn't count as a zooming action.
848 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
849 if (isZooming
|| isPanning
) {
858 draggingValue
= null;
864 // Temporarily cancel the dragging event when the mouse leaves the graph
865 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
872 // If the mouse is released on the canvas during a drag event, then it's a
873 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
874 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
877 dragEndX
= getX(event
);
878 dragEndY
= getY(event
);
879 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
880 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
882 if (regionWidth
< 2 && regionHeight
< 2 &&
883 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
884 // TODO(danvk): pass along more info about the points, e.g. 'x'
885 if (self
.attr_('clickCallback') != null) {
886 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
888 if (self
.attr_('pointClickCallback')) {
889 // check if the click was on a particular point.
891 var closestDistance
= 0;
892 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
893 var p
= self
.selPoints_
[i
];
894 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
895 Math
.pow(p
.canvasy
- dragEndY
, 2);
896 if (closestIdx
== -1 || distance
< closestDistance
) {
897 closestDistance
= distance
;
902 // Allow any click within two pixels of the dot.
903 var radius
= self
.attr_('highlightCircleSize') + 2;
904 if (closestDistance
<= 5 * 5) {
905 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
910 if (regionWidth
>= 10 && regionWidth
> regionHeight
) {
911 self
.doZoomX_(Math
.min(dragStartX
, dragEndX
),
912 Math
.max(dragStartX
, dragEndX
));
913 } else if (regionHeight
>= 10 && regionHeight
> regionWidth
){
914 self
.doZoomY_(Math
.min(dragStartY
, dragEndY
),
915 Math
.max(dragStartY
, dragEndY
));
917 self
.canvas_
.getContext("2d").clearRect(0, 0,
919 self
.canvas_
.height
);
929 draggingValue
= null;
935 // Double-clicking zooms back out
936 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
937 // Disable zooming out if panning.
938 if (event
.altKey
|| event
.shiftKey
) return;
945 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
946 * up any previous zoom rectangles that were drawn. This could be optimized to
947 * avoid extra redrawing, but it's tricky to avoid interactions with the status
950 * @param {Number} direction the direction of the zoom rectangle. Acceptable
951 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
952 * @param {Number} startX The X position where the drag started, in canvas
954 * @param {Number} endX The current X position of the drag, in canvas coords.
955 * @param {Number} startY The Y position where the drag started, in canvas
957 * @param {Number} endY The current Y position of the drag, in canvas coords.
958 * @param {Number} prevDirection the value of direction on the previous call to
959 * this function. Used to avoid excess redrawing
960 * @param {Number} prevEndX The value of endX on the previous call to this
961 * function. Used to avoid excess redrawing
962 * @param {Number} prevEndY The value of endY on the previous call to this
963 * function. Used to avoid excess redrawing
966 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
967 prevDirection
, prevEndX
, prevEndY
) {
968 var ctx
= this.canvas_
.getContext("2d");
970 // Clean up from the previous rect if necessary
971 if (prevDirection
== Dygraph
.HORIZONTAL
) {
972 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
973 Math
.abs(startX
- prevEndX
), this.height_
);
974 } else if (prevDirection
== Dygraph
.VERTICAL
){
975 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
976 this.width_
, Math
.abs(startY
- prevEndY
));
979 // Draw a light-grey rectangle to show the new viewing area
980 if (direction
== Dygraph
.HORIZONTAL
) {
981 if (endX
&& startX
) {
982 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
983 ctx
.fillRect(Math
.min(startX
, endX
), 0,
984 Math
.abs(endX
- startX
), this.height_
);
987 if (direction
== Dygraph
.VERTICAL
) {
988 if (endY
&& startY
) {
989 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
990 ctx
.fillRect(0, Math
.min(startY
, endY
),
991 this.width_
, Math
.abs(endY
- startY
));
997 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
998 * the canvas. The exact zoom window may be slightly larger if there are no data
999 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1000 * which accepts dates that match the raw data. This function redraws the graph.
1002 * @param {Number} lowX The leftmost pixel value that should be visible.
1003 * @param {Number} highX The rightmost pixel value that should be visible.
1006 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1007 // Find the earliest and latest dates contained in this canvasx range.
1008 // Convert the call to date ranges of the raw data.
1009 var r
= this.toDataCoords(lowX
, null);
1011 r
= this.toDataCoords(highX
, null);
1013 this.doZoomXDates_(minDate
, maxDate
);
1017 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1018 * method with doZoomX which accepts pixel coordinates. This function redraws
1021 * @param {Number} minDate The minimum date that should be visible.
1022 * @param {Number} maxDate The maximum date that should be visible.
1025 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1026 this.dateWindow_
= [minDate
, maxDate
];
1027 this.drawGraph_(this.rawData_
);
1028 if (this.attr_("zoomCallback")) {
1029 var yRange
= this.yAxisRange();
1030 this.attr_("zoomCallback")(minDate
, maxDate
, yRange
[0], yRange
[1]);
1035 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1036 * the canvas. The exact zoom window may be slightly larger if there are no
1037 * data points near lowY or highY. Don't confuse this function with
1038 * doZoomYValues, which accepts parameters that match the raw data. This
1039 * function redraws the graph.
1041 * @param {Number} lowY The topmost pixel value that should be visible.
1042 * @param {Number} highY The lowest pixel value that should be visible.
1045 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1046 // Find the highest and lowest values in pixel range.
1047 var r
= this.toDataCoords(null, lowY
);
1048 var maxValue
= r
[1];
1049 r
= this.toDataCoords(null, highY
);
1050 var minValue
= r
[1];
1052 this.doZoomYValues_(minValue
, maxValue
);
1056 * Zoom to something containing [minValue, maxValue] values. Don't confuse this
1057 * method with doZoomY which accepts pixel coordinates. This function redraws
1060 * @param {Number} minValue The minimum Value that should be visible.
1061 * @param {Number} maxValue The maximum value that should be visible.
1064 Dygraph
.prototype.doZoomYValues_
= function(minValue
, maxValue
) {
1065 this.valueWindow_
= [minValue
, maxValue
];
1066 this.drawGraph_(this.rawData_
);
1067 if (this.attr_("zoomCallback")) {
1068 var xRange
= this.xAxisRange();
1069 this.attr_("zoomCallback")(xRange
[0], xRange
[1], minValue
, maxValue
);
1074 * Reset the zoom to the original view coordinates. This is the same as
1075 * double-clicking on the graph.
1079 Dygraph
.prototype.doUnzoom_
= function() {
1081 if (this.dateWindow_
!= null) {
1083 this.dateWindow_
= null;
1085 if (this.valueWindow_
!= null) {
1087 this.valueWindow_
= this.valueRange_
;
1091 // Putting the drawing operation before the callback because it resets
1093 this.drawGraph_(this.rawData_
);
1094 if (this.attr_("zoomCallback")) {
1095 var minDate
= this.rawData_
[0][0];
1096 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1097 var minValue
= this.yAxisRange()[0];
1098 var maxValue
= this.yAxisRange()[1];
1099 this.attr_("zoomCallback")(minDate
, maxDate
, minValue
, maxValue
);
1105 * When the mouse moves in the canvas, display information about a nearby data
1106 * point and draw dots over those points in the data series. This function
1107 * takes care of cleanup of previously-drawn dots.
1108 * @param {Object} event The mousemove event from the browser.
1111 Dygraph
.prototype.mouseMove_
= function(event
) {
1112 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1113 var points
= this.layout_
.points
;
1118 // Loop through all the points and find the date nearest to our current
1120 var minDist
= 1e+100;
1122 for (var i
= 0; i
< points
.length
; i
++) {
1123 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1124 if (dist
> minDist
) continue;
1128 if (idx
>= 0) lastx
= points
[idx
].xval
;
1129 // Check that you can really highlight the last day's data
1130 if (canvasx
> points
[points
.length
-1].canvasx
)
1131 lastx
= points
[points
.length
-1].xval
;
1133 // Extract the points we've selected
1134 this.selPoints_
= [];
1135 var l
= points
.length
;
1136 if (!this.attr_("stackedGraph")) {
1137 for (var i
= 0; i
< l
; i
++) {
1138 if (points
[i
].xval
== lastx
) {
1139 this.selPoints_
.push(points
[i
]);
1143 // Need to 'unstack' points starting from the bottom
1144 var cumulative_sum
= 0;
1145 for (var i
= l
- 1; i
>= 0; i
--) {
1146 if (points
[i
].xval
== lastx
) {
1147 var p
= {}; // Clone the point since we modify it
1148 for (var k
in points
[i
]) {
1149 p
[k
] = points
[i
][k
];
1151 p
.yval
-= cumulative_sum
;
1152 cumulative_sum
+= p
.yval
;
1153 this.selPoints_
.push(p
);
1156 this.selPoints_
.reverse();
1159 if (this.attr_("highlightCallback")) {
1160 var px
= this.lastx_
;
1161 if (px
!== null && lastx
!= px
) {
1162 // only fire if the selected point has changed.
1163 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
);
1167 // Save last x position for callbacks.
1168 this.lastx_
= lastx
;
1170 this.updateSelection_();
1174 * Draw dots over the selectied points in the data series. This function
1175 * takes care of cleanup of previously-drawn dots.
1178 Dygraph
.prototype.updateSelection_
= function() {
1179 // Clear the previously drawn vertical, if there is one
1180 var circleSize
= this.attr_('highlightCircleSize');
1181 var ctx
= this.canvas_
.getContext("2d");
1182 if (this.previousVerticalX_
>= 0) {
1183 var px
= this.previousVerticalX_
;
1184 ctx
.clearRect(px
- circleSize
- 1, 0, 2 * circleSize
+ 2, this.height_
);
1187 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1189 if (this.selPoints_
.length
> 0) {
1190 var canvasx
= this.selPoints_
[0].canvasx
;
1192 // Set the status message to indicate the selected point(s)
1193 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1194 var fmtFunc
= this.attr_('yValueFormatter');
1195 var clen
= this.colors_
.length
;
1197 if (this.attr_('showLabelsOnHighlight')) {
1198 // Set the status message to indicate the selected point(s)
1199 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1200 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1201 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1202 if (this.attr_("labelsSeparateLines")) {
1205 var point
= this.selPoints_
[i
];
1206 var c
= new RGBColor(this.colors_
[i
%clen
]);
1207 var yval
= fmtFunc(point
.yval
);
1208 replace
+= " <b><font color='" + c
.toHex() + "'>"
1209 + point
.name
+ "</font></b>:"
1213 this.attr_("labelsDiv").innerHTML
= replace
;
1216 // Draw colored circles over the center of each selected point
1218 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1219 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1221 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1222 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1223 0, 2 * Math
.PI
, false);
1228 this.previousVerticalX_
= canvasx
;
1233 * Set manually set selected dots, and display information about them
1234 * @param int row number that should by highlighted
1235 * false value clears the selection
1238 Dygraph
.prototype.setSelection
= function(row
) {
1239 // Extract the points we've selected
1240 this.selPoints_
= [];
1243 if (row
!== false) {
1244 row
= row
-this.boundaryIds_
[0][0];
1247 if (row
!== false && row
>= 0) {
1248 for (var i
in this.layout_
.datasets
) {
1249 if (row
< this.layout_
.datasets
[i
].length
) {
1250 this.selPoints_
.push(this.layout_
.points
[pos
+row
]);
1252 pos
+= this.layout_
.datasets
[i
].length
;
1256 if (this.selPoints_
.length
) {
1257 this.lastx_
= this.selPoints_
[0].xval
;
1258 this.updateSelection_();
1261 this.clearSelection();
1267 * The mouse has left the canvas. Clear out whatever artifacts remain
1268 * @param {Object} event the mouseout event from the browser.
1271 Dygraph
.prototype.mouseOut_
= function(event
) {
1272 if (this.attr_("unhighlightCallback")) {
1273 this.attr_("unhighlightCallback")(event
);
1276 if (this.attr_("hideOverlayOnMouseOut")) {
1277 this.clearSelection();
1282 * Remove all selection from the canvas
1285 Dygraph
.prototype.clearSelection
= function() {
1286 // Get rid of the overlay data
1287 var ctx
= this.canvas_
.getContext("2d");
1288 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1289 this.attr_("labelsDiv").innerHTML
= "";
1290 this.selPoints_
= [];
1295 * Returns the number of the currently selected row
1296 * @return int row number, of -1 if nothing is selected
1299 Dygraph
.prototype.getSelection
= function() {
1300 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1304 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1305 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1306 return row
+ this.boundaryIds_
[0][0];
1312 Dygraph
.zeropad
= function(x
) {
1313 if (x
< 10) return "0" + x
; else return "" + x
;
1317 * Return a string version of the hours, minutes and seconds portion of a date.
1318 * @param {Number} date The JavaScript date (ms since epoch)
1319 * @return {String} A time of the form "HH:MM:SS"
1322 Dygraph
.hmsString_
= function(date
) {
1323 var zeropad
= Dygraph
.zeropad
;
1324 var d
= new Date(date
);
1325 if (d
.getSeconds()) {
1326 return zeropad(d
.getHours()) + ":" +
1327 zeropad(d
.getMinutes()) + ":" +
1328 zeropad(d
.getSeconds());
1330 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1335 * Convert a JS date to a string appropriate to display on an axis that
1336 * is displaying values at the stated granularity.
1337 * @param {Date} date The date to format
1338 * @param {Number} granularity One of the Dygraph granularity constants
1339 * @return {String} The formatted date
1342 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1343 if (granularity
>= Dygraph
.MONTHLY
) {
1344 return date
.strftime('%b %y');
1346 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1347 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1348 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1350 return Dygraph
.hmsString_(date
.getTime());
1356 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1357 * @param {Number} date The JavaScript date (ms since epoch)
1358 * @return {String} A date of the form "YYYY/MM/DD"
1361 Dygraph
.dateString_
= function(date
, self
) {
1362 var zeropad
= Dygraph
.zeropad
;
1363 var d
= new Date(date
);
1366 var year
= "" + d
.getFullYear();
1367 // Get a 0 padded month string
1368 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1369 // Get a 0 padded day string
1370 var day
= zeropad(d
.getDate());
1373 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1374 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1376 return year
+ "/" + month + "/" + day
+ ret
;
1380 * Round a number to the specified number of digits past the decimal point.
1381 * @param {Number} num The number to round
1382 * @param {Number} places The number of decimals to which to round
1383 * @return {Number} The rounded number
1386 Dygraph
.round_
= function(num
, places
) {
1387 var shift
= Math
.pow(10, places
);
1388 return Math
.round(num
* shift
)/shift
;
1392 * Fires when there's data available to be graphed.
1393 * @param {String} data Raw CSV data to be plotted
1396 Dygraph
.prototype.loadedEvent_
= function(data
) {
1397 this.rawData_
= this.parseCSV_(data
);
1398 this.drawGraph_(this.rawData_
);
1401 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1402 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1403 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1406 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1409 Dygraph
.prototype.addXTicks_
= function() {
1410 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1411 var startDate
, endDate
;
1412 if (this.dateWindow_
) {
1413 startDate
= this.dateWindow_
[0];
1414 endDate
= this.dateWindow_
[1];
1416 startDate
= this.rawData_
[0][0];
1417 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1420 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1421 this.layout_
.updateOptions({xTicks
: xTicks
});
1424 // Time granularity enumeration
1425 Dygraph
.SECONDLY
= 0;
1426 Dygraph
.TWO_SECONDLY
= 1;
1427 Dygraph
.FIVE_SECONDLY
= 2;
1428 Dygraph
.TEN_SECONDLY
= 3;
1429 Dygraph
.THIRTY_SECONDLY
= 4;
1430 Dygraph
.MINUTELY
= 5;
1431 Dygraph
.TWO_MINUTELY
= 6;
1432 Dygraph
.FIVE_MINUTELY
= 7;
1433 Dygraph
.TEN_MINUTELY
= 8;
1434 Dygraph
.THIRTY_MINUTELY
= 9;
1435 Dygraph
.HOURLY
= 10;
1436 Dygraph
.TWO_HOURLY
= 11;
1437 Dygraph
.SIX_HOURLY
= 12;
1439 Dygraph
.WEEKLY
= 14;
1440 Dygraph
.MONTHLY
= 15;
1441 Dygraph
.QUARTERLY
= 16;
1442 Dygraph
.BIANNUAL
= 17;
1443 Dygraph
.ANNUAL
= 18;
1444 Dygraph
.DECADAL
= 19;
1445 Dygraph
.NUM_GRANULARITIES
= 20;
1447 Dygraph
.SHORT_SPACINGS
= [];
1448 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1449 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1450 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1451 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1452 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1453 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1454 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1455 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1456 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1457 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1458 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1459 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1460 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1461 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1462 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1466 // If we used this time granularity, how many ticks would there be?
1467 // This is only an approximation, but it's generally good enough.
1469 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1470 if (granularity
< Dygraph
.MONTHLY
) {
1471 // Generate one tick mark for every fixed interval of time.
1472 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1473 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1475 var year_mod
= 1; // e.g. to only print one point every 10 years.
1476 var num_months
= 12;
1477 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1478 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1479 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1480 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1482 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1483 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1484 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1490 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1491 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1493 // Returns an array containing {v: millis, label: label} dictionaries.
1495 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1496 var formatter
= this.attr_("xAxisLabelFormatter");
1498 if (granularity
< Dygraph
.MONTHLY
) {
1499 // Generate one tick mark for every fixed interval of time.
1500 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1501 var format
= '%d%b'; // e.g. "1Jan"
1503 // Find a time less than start_time which occurs on a "nice" time boundary
1504 // for this granularity.
1505 var g
= spacing
/ 1000;
1506 var d
= new Date(start_time
);
1507 if (g
<= 60) { // seconds
1508 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1512 if (g
<= 60) { // minutes
1513 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1518 if (g
<= 24) { // days
1519 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1524 if (g
== 7) { // one week
1525 d
.setDate(d
.getDate() - d
.getDay());
1530 start_time
= d
.getTime();
1532 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1533 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1536 // Display a tick mark on the first of a set of months of each year.
1537 // Years get a tick mark iff y % year_mod == 0. This is useful for
1538 // displaying a tick mark once every 10 years, say, on long time scales.
1540 var year_mod
= 1; // e.g. to only print one point every 10 years.
1542 if (granularity
== Dygraph
.MONTHLY
) {
1543 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1544 } else if (granularity
== Dygraph
.QUARTERLY
) {
1545 months
= [ 0, 3, 6, 9 ];
1546 } else if (granularity
== Dygraph
.BIANNUAL
) {
1548 } else if (granularity
== Dygraph
.ANNUAL
) {
1550 } else if (granularity
== Dygraph
.DECADAL
) {
1555 var start_year
= new Date(start_time
).getFullYear();
1556 var end_year
= new Date(end_time
).getFullYear();
1557 var zeropad
= Dygraph
.zeropad
;
1558 for (var i
= start_year
; i
<= end_year
; i
++) {
1559 if (i
% year_mod
!= 0) continue;
1560 for (var j
= 0; j
< months
.length
; j
++) {
1561 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1562 var t
= Date
.parse(date_str
);
1563 if (t
< start_time
|| t
> end_time
) continue;
1564 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1574 * Add ticks to the x-axis based on a date range.
1575 * @param {Number} startDate Start of the date window (millis since epoch)
1576 * @param {Number} endDate End of the date window (millis since epoch)
1577 * @return {Array.<Object>} Array of {label, value} tuples.
1580 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1582 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1583 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1584 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1591 return self
.GetXAxis(startDate
, endDate
, chosen
);
1593 // TODO(danvk): signal error.
1598 * Add ticks when the x axis has numbers on it (instead of dates)
1599 * @param {Number} startDate Start of the date window (millis since epoch)
1600 * @param {Number} endDate End of the date window (millis since epoch)
1601 * @return {Array.<Object>} Array of {label, value} tuples.
1604 Dygraph
.numericTicks
= function(minV
, maxV
, self
) {
1606 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1607 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1608 // The first spacing greater than pixelsPerYLabel is what we use.
1609 // TODO(danvk): version that works on a log scale.
1610 if (self
.attr_("labelsKMG2")) {
1611 var mults
= [1, 2, 4, 8];
1613 var mults
= [1, 2, 5];
1615 var scale
, low_val
, high_val
, nTicks
;
1616 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1617 var pixelsPerTick
= self
.attr_('pixelsPerYLabel');
1618 for (var i
= -10; i
< 50; i
++) {
1619 if (self
.attr_("labelsKMG2")) {
1620 var base_scale
= Math
.pow(16, i
);
1622 var base_scale
= Math
.pow(10, i
);
1624 for (var j
= 0; j
< mults
.length
; j
++) {
1625 scale
= base_scale
* mults
[j
];
1626 low_val
= Math
.floor(minV
/ scale
) * scale
;
1627 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1628 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1629 var spacing
= self
.height_
/ nTicks
;
1630 // wish I could break out of both loops at once...
1631 if (spacing
> pixelsPerTick
) break;
1633 if (spacing
> pixelsPerTick
) break;
1636 // Construct labels for the ticks
1640 if (self
.attr_("labelsKMB")) {
1642 k_labels
= [ "K", "M", "B", "T" ];
1644 if (self
.attr_("labelsKMG2")) {
1645 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1647 k_labels
= [ "k", "M", "G", "T" ];
1650 // Allow reverse y-axis if it's explicitly requested.
1651 if (low_val
> high_val
) scale
*= -1;
1653 for (var i
= 0; i
< nTicks
; i
++) {
1654 var tickV
= low_val
+ i
* scale
;
1655 var absTickV
= Math
.abs(tickV
);
1656 var label
= Dygraph
.round_(tickV
, 2);
1657 if (k_labels
.length
) {
1658 // Round up to an appropriate unit.
1660 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1661 if (absTickV
>= n
) {
1662 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1667 ticks
.push( {label
: label
, v
: tickV
} );
1673 * Adds appropriate ticks on the y-axis
1674 * @param {Number} minY The minimum Y value in the data set
1675 * @param {Number} maxY The maximum Y value in the data set
1678 Dygraph
.prototype.addYTicks_
= function(minY
, maxY
) {
1679 // Set the number of ticks so that the labels are human-friendly.
1680 // TODO(danvk): make this an attribute as well.
1681 var ticks
= Dygraph
.numericTicks(minY
, maxY
, this);
1682 this.layout_
.updateOptions( { yAxis
: [minY
, maxY
],
1686 // Computes the range of the data series (including confidence intervals).
1687 // series is either [ [x1, y1], [x2, y2], ... ] or
1688 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1689 // Returns [low, high]
1690 Dygraph
.prototype.extremeValues_
= function(series
) {
1691 var minY
= null, maxY
= null;
1693 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1695 // With custom bars, maxY is the max of the high values.
1696 for (var j
= 0; j
< series
.length
; j
++) {
1697 var y
= series
[j
][1][0];
1699 var low
= y
- series
[j
][1][1];
1700 var high
= y
+ series
[j
][1][2];
1701 if (low
> y
) low
= y
; // this can happen with custom bars,
1702 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1703 if (maxY
== null || high
> maxY
) {
1706 if (minY
== null || low
< minY
) {
1711 for (var j
= 0; j
< series
.length
; j
++) {
1712 var y
= series
[j
][1];
1713 if (y
=== null || isNaN(y
)) continue;
1714 if (maxY
== null || y
> maxY
) {
1717 if (minY
== null || y
< minY
) {
1723 return [minY
, maxY
];
1727 * Update the graph with new data. Data is in the format
1728 * [ [date1, val1, val2, ...], [date2, val1, val2, ...] if errorBars=false
1729 * or, if errorBars=true,
1730 * [ [date1, [val1,stddev1], [val2,stddev2], ...], [date2, ...], ...]
1731 * @param {Array.<Object>} data The data (see above)
1734 Dygraph
.prototype.drawGraph_
= function(data
) {
1735 // This is used to set the second parameter to drawCallback, below.
1736 var is_initial_draw
= this.is_initial_draw_
;
1737 this.is_initial_draw_
= false;
1739 var minY
= null, maxY
= null;
1740 this.layout_
.removeAllDatasets();
1742 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1744 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints');
1746 // Loop over the fields (series). Go from the last to the first,
1747 // because if they're stacked that's how we accumulate the values.
1749 var cumulative_y
= []; // For stacked series.
1752 // Loop over all fields and create datasets
1753 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1754 if (!this.visibility()[i
- 1]) continue;
1757 for (var j
= 0; j
< data
.length
; j
++) {
1758 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1759 var date
= data
[j
][0];
1760 series
.push([date
, data
[j
][i
]]);
1763 series
= this.rollingAverage(series
, this.rollPeriod_
);
1765 // Prune down to the desired range, if necessary (for zooming)
1766 // Because there can be lines going to points outside of the visible area,
1767 // we actually prune to visible points, plus one on either side.
1768 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1769 if (this.dateWindow_
) {
1770 var low
= this.dateWindow_
[0];
1771 var high
= this.dateWindow_
[1];
1773 // TODO(danvk): do binary search instead of linear search.
1774 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1775 var firstIdx
= null, lastIdx
= null;
1776 for (var k
= 0; k
< series
.length
; k
++) {
1777 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1780 if (series
[k
][0] <= high
) {
1784 if (firstIdx
=== null) firstIdx
= 0;
1785 if (firstIdx
> 0) firstIdx
--;
1786 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1787 if (lastIdx
< series
.length
- 1) lastIdx
++;
1788 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1789 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1790 pruned
.push(series
[k
]);
1794 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1797 var extremes
= this.extremeValues_(series
);
1798 var thisMinY
= extremes
[0];
1799 var thisMaxY
= extremes
[1];
1800 if (minY
=== null || thisMinY
< minY
) minY
= thisMinY
;
1801 if (maxY
=== null || thisMaxY
> maxY
) maxY
= thisMaxY
;
1804 for (var j
=0; j
<series
.length
; j
++) {
1805 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1808 } else if (this.attr_("stackedGraph")) {
1809 var l
= series
.length
;
1811 for (var j
= 0; j
< l
; j
++) {
1812 // If one data set has a NaN, let all subsequent stacked
1813 // sets inherit the NaN -- only start at 0 for the first set.
1814 var x
= series
[j
][0];
1815 if (cumulative_y
[x
] === undefined
)
1816 cumulative_y
[x
] = 0;
1818 actual_y
= series
[j
][1];
1819 cumulative_y
[x
] += actual_y
;
1821 series
[j
] = [x
, cumulative_y
[x
]]
1823 if (!maxY
|| cumulative_y
[x
] > maxY
)
1824 maxY
= cumulative_y
[x
];
1828 datasets
[i
] = series
;
1831 for (var i
= 1; i
< datasets
.length
; i
++) {
1832 if (!this.visibility()[i
- 1]) continue;
1833 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1836 // Use some heuristics to come up with a good maxY value, unless it's been
1837 // set explicitly by the developer or end-user (via drag)
1838 if (this.valueWindow_
!= null) {
1839 this.addYTicks_(this.valueWindow_
[0], this.valueWindow_
[1]);
1840 this.displayedYRange_
= this.valueWindow_
;
1842 // This affects the calculation of span, below.
1843 if (this.attr_("includeZero") && minY
> 0) {
1847 // Add some padding and round up to an integer to be human-friendly.
1848 var span
= maxY
- minY
;
1849 // special case: if we have no sense of scale, use +/-10% of the sole value
.
1850 if (span
== 0) { span
= maxY
; }
1851 var maxAxisY
= maxY
+ 0.1 * span
;
1852 var minAxisY
= minY
- 0.1 * span
;
1854 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
1855 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
1856 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
1858 if (this.attr_("includeZero")) {
1859 if (maxY
< 0) maxAxisY
= 0;
1860 if (minY
> 0) minAxisY
= 0;
1863 this.addYTicks_(minAxisY
, maxAxisY
);
1864 this.displayedYRange_
= [minAxisY
, maxAxisY
];
1869 // Tell PlotKit to use this new data and render itself
1870 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1871 this.layout_
.evaluateWithError();
1872 this.plotter_
.clear();
1873 this.plotter_
.render();
1874 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1875 this.canvas_
.height
);
1877 if (this.attr_("drawCallback") !== null) {
1878 this.attr_("drawCallback")(this, is_initial_draw
);
1883 * Calculates the rolling average of a data set.
1884 * If originalData is [label, val], rolls the average of those.
1885 * If originalData is [label, [, it's interpreted as [value, stddev]
1886 * and the roll is returned in the same form, with appropriately reduced
1887 * stddev for each value.
1888 * Note that this is where fractional input (i.e. '5/10') is converted into
1890 * @param {Array} originalData The data in the appropriate format (see above)
1891 * @param {Number} rollPeriod The number of days over which to average the data
1893 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
1894 if (originalData
.length
< 2)
1895 return originalData
;
1896 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
1897 var rollingData
= [];
1898 var sigma
= this.attr_("sigma");
1900 if (this.fractions_
) {
1902 var den
= 0; // numerator/denominator
1904 for (var i
= 0; i
< originalData
.length
; i
++) {
1905 num
+= originalData
[i
][1][0];
1906 den
+= originalData
[i
][1][1];
1907 if (i
- rollPeriod
>= 0) {
1908 num
-= originalData
[i
- rollPeriod
][1][0];
1909 den
-= originalData
[i
- rollPeriod
][1][1];
1912 var date
= originalData
[i
][0];
1913 var value
= den
? num
/ den
: 0.0;
1914 if (this.attr_("errorBars")) {
1915 if (this.wilsonInterval_
) {
1916 // For more details on this confidence interval, see:
1917 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
1919 var p
= value
< 0 ? 0 : value
, n
= den
;
1920 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
1921 var denom
= 1 + sigma
* sigma
/ den
;
1922 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
1923 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
1924 rollingData
[i
] = [date
,
1925 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
1927 rollingData
[i
] = [date
, [0, 0, 0]];
1930 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
1931 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
1934 rollingData
[i
] = [date
, mult
* value
];
1937 } else if (this.attr_("customBars")) {
1942 for (var i
= 0; i
< originalData
.length
; i
++) {
1943 var data
= originalData
[i
][1];
1945 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
1947 if (y
!= null && !isNaN(y
)) {
1953 if (i
- rollPeriod
>= 0) {
1954 var prev
= originalData
[i
- rollPeriod
];
1955 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
1962 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
1963 1.0 * (mid
- low
) / count
,
1964 1.0 * (high
- mid
) / count
]];
1967 // Calculate the rolling average for the first rollPeriod - 1 points where
1968 // there is not enough data to roll over the full number of days
1969 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
1970 if (!this.attr_("errorBars")){
1971 if (rollPeriod
== 1) {
1972 return originalData
;
1975 for (var i
= 0; i
< originalData
.length
; i
++) {
1978 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1979 var y
= originalData
[j
][1];
1980 if (y
== null || isNaN(y
)) continue;
1982 sum
+= originalData
[j
][1];
1985 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
1987 rollingData
[i
] = [originalData
[i
][0], null];
1992 for (var i
= 0; i
< originalData
.length
; i
++) {
1996 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
1997 var y
= originalData
[j
][1][0];
1998 if (y
== null || isNaN(y
)) continue;
2000 sum
+= originalData
[j
][1][0];
2001 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2004 var stddev
= Math
.sqrt(variance
) / num_ok
;
2005 rollingData
[i
] = [originalData
[i
][0],
2006 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2008 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2018 * Parses a date, returning the number of milliseconds since epoch. This can be
2019 * passed in as an xValueParser in the Dygraph constructor.
2020 * TODO(danvk): enumerate formats that this understands.
2021 * @param {String} A date in YYYYMMDD format.
2022 * @return {Number} Milliseconds since epoch.
2025 Dygraph
.dateParser
= function(dateStr
, self
) {
2028 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2029 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2030 while (dateStrSlashed
.search("-") != -1) {
2031 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2033 d
= Date
.parse(dateStrSlashed
);
2034 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2035 // TODO(danvk): remove support for this format. It's confusing.
2036 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2037 + "/" + dateStr
.substr(6,2);
2038 d
= Date
.parse(dateStrSlashed
);
2040 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2041 // "2009/07/12 12:34:56"
2042 d
= Date
.parse(dateStr
);
2045 if (!d
|| isNaN(d
)) {
2046 self
.error("Couldn't parse " + dateStr
+ " as a date");
2052 * Detects the type of the str (date or numeric) and sets the various
2053 * formatting attributes in this.attrs_ based on this type.
2054 * @param {String} str An x value.
2057 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2059 if (str
.indexOf('-') >= 0 ||
2060 str
.indexOf('/') >= 0 ||
2061 isNaN(parseFloat(str
))) {
2063 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2064 // TODO(danvk): remove support for this format.
2069 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2070 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2071 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2072 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2074 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2075 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2076 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2077 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2082 * Parses a string in a special csv format. We expect a csv file where each
2083 * line is a date point, and the first field in each line is the date string.
2084 * We also expect that all remaining fields represent series.
2085 * if the errorBars attribute is set, then interpret the fields as:
2086 * date, series1, stddev1, series2, stddev2, ...
2087 * @param {Array.<Object>} data See above.
2090 * @return Array.<Object> An array with one entry for each row. These entries
2091 * are an array of cells in that row. The first entry is the parsed x-value for
2092 * the row. The second, third, etc. are the y-values. These can take on one of
2093 * three forms, depending on the CSV and constructor parameters:
2095 * 2. [ value, stddev ]
2096 * 3. [ low value, center value, high value ]
2098 Dygraph
.prototype.parseCSV_
= function(data
) {
2100 var lines
= data
.split("\n");
2102 // Use the default delimiter or fall back to a tab if that makes sense.
2103 var delim
= this.attr_('delimiter');
2104 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2109 if (this.labelsFromCSV_
) {
2111 this.attrs_
.labels
= lines
[0].split(delim
);
2114 // Parse the x as a float or return null if it's not a number.
2115 var parseFloatOrNull
= function(x
) {
2116 var val
= parseFloat(x
);
2117 return isNaN(val
) ? null : val
;
2121 var defaultParserSet
= false; // attempt to auto-detect x value type
2122 var expectedCols
= this.attr_("labels").length
;
2123 var outOfOrder
= false;
2124 for (var i
= start
; i
< lines
.length
; i
++) {
2125 var line
= lines
[i
];
2126 if (line
.length
== 0) continue; // skip blank lines
2127 if (line
[0] == '#') continue; // skip comment lines
2128 var inFields
= line
.split(delim
);
2129 if (inFields
.length
< 2) continue;
2132 if (!defaultParserSet
) {
2133 this.detectTypeFromString_(inFields
[0]);
2134 xParser
= this.attr_("xValueParser");
2135 defaultParserSet
= true;
2137 fields
[0] = xParser(inFields
[0], this);
2139 // If fractions are expected, parse the numbers as "A/B
"
2140 if (this.fractions_) {
2141 for (var j = 1; j < inFields.length; j++) {
2142 // TODO(danvk): figure out an appropriate way to flag parse errors.
2143 var vals = inFields[j].split("/");
2144 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2146 } else if (this.attr_("errorBars
")) {
2147 // If there are error bars, values are (value, stddev) pairs
2148 for (var j = 1; j < inFields.length; j += 2)
2149 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2150 parseFloatOrNull(inFields[j + 1])];
2151 } else if (this.attr_("customBars
")) {
2152 // Bars are a low;center;high tuple
2153 for (var j = 1; j < inFields.length; j++) {
2154 var vals = inFields[j].split(";");
2155 fields[j] = [ parseFloatOrNull(vals[0]),
2156 parseFloatOrNull(vals[1]),
2157 parseFloatOrNull(vals[2]) ];
2160 // Values are just numbers
2161 for (var j = 1; j < inFields.length; j++) {
2162 fields[j] = parseFloatOrNull(inFields[j]);
2165 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2170 if (fields.length != expectedCols) {
2171 this.error("Number of columns
in line
" + i + " (" + fields.length +
2172 ") does not agree
with number of
labels (" + expectedCols +
2178 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2179 ret.sort(function(a,b) { return a[0] - b[0] });
2186 * The user has provided their data as a pre-packaged JS array. If the x values
2187 * are numeric, this is the same as dygraphs' internal format. If the x values
2188 * are dates, we need to convert them from Date objects to ms since epoch.
2189 * @param {Array.<Object>} data
2190 * @return {Array.<Object>} data with numeric x values.
2192 Dygraph.prototype.parseArray_ = function(data) {
2193 // Peek at the first x value to see if it's numeric.
2194 if (data.length == 0) {
2195 this.error("Can
't plot empty data set");
2198 if (data[0].length == 0) {
2199 this.error("Data set cannot contain an empty row");
2203 if (this.attr_("labels") == null) {
2204 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2205 "in the options parameter");
2206 this.attrs_.labels = [ "X" ];
2207 for (var i = 1; i < data[0].length; i++) {
2208 this.attrs_.labels.push("Y" + i);
2212 if (Dygraph.isDateLike(data[0][0])) {
2213 // Some intelligent defaults for a date x-axis.
2214 this.attrs_.xValueFormatter = Dygraph.dateString_;
2215 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2216 this.attrs_.xTicker = Dygraph.dateTicker;
2218 // Assume they're all dates
.
2219 var parsedData
= Dygraph
.clone(data
);
2220 for (var i
= 0; i
< data
.length
; i
++) {
2221 if (parsedData
[i
].length
== 0) {
2222 this.error("Row " + (1 + i
) + " of data is empty");
2225 if (parsedData
[i
][0] == null
2226 || typeof(parsedData
[i
][0].getTime
) != 'function'
2227 || isNaN(parsedData
[i
][0].getTime())) {
2228 this.error("x value in row " + (1 + i
) + " is not a Date");
2231 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2235 // Some intelligent defaults for a numeric x-axis.
2236 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2237 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2243 * Parses a DataTable object from gviz.
2244 * The data is expected to have a first column that is either a date or a
2245 * number. All subsequent columns must be numbers. If there is a clear mismatch
2246 * between this.xValueParser_ and the type of the first column, it will be
2247 * fixed. Fills out rawData_.
2248 * @param {Array.<Object>} data See above.
2251 Dygraph
.prototype.parseDataTable_
= function(data
) {
2252 var cols
= data
.getNumberOfColumns();
2253 var rows
= data
.getNumberOfRows();
2255 var indepType
= data
.getColumnType(0);
2256 if (indepType
== 'date' || indepType
== 'datetime') {
2257 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2258 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2259 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2260 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2261 } else if (indepType
== 'number') {
2262 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2263 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2264 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2265 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2267 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2268 "column 1 of DataTable input (Got '" + indepType
+ "')");
2272 // Array of the column indices which contain data (and not annotations).
2274 var annotationCols
= {}; // data index -> [annotation cols]
2275 var hasAnnotations
= false;
2276 for (var i
= 1; i
< cols
; i
++) {
2277 var type
= data
.getColumnType(i
);
2278 if (type
== 'number') {
2280 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2281 // This is OK -- it's an annotation column.
2282 var dataIdx
= colIdx
[colIdx
.length
- 1];
2283 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2284 annotationCols
[dataIdx
] = [i
];
2286 annotationCols
[dataIdx
].push(i
);
2288 hasAnnotations
= true;
2290 this.error("Only 'number' is supported as a dependent type with Gviz." +
2291 " 'string' is only supported if displayAnnotations is true");
2295 // Read column labels
2296 // TODO(danvk): add support back for errorBars
2297 var labels
= [data
.getColumnLabel(0)];
2298 for (var i
= 0; i
< colIdx
.length
; i
++) {
2299 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2301 this.attrs_
.labels
= labels
;
2302 cols
= labels
.length
;
2305 var outOfOrder
= false;
2306 var annotations
= [];
2307 for (var i
= 0; i
< rows
; i
++) {
2309 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2310 data
.getValue(i
, 0) === null) {
2311 this.warning("Ignoring row " + i
+
2312 " of DataTable because of undefined or null first column.");
2316 if (indepType
== 'date' || indepType
== 'datetime') {
2317 row
.push(data
.getValue(i
, 0).getTime());
2319 row
.push(data
.getValue(i
, 0));
2321 if (!this.attr_("errorBars")) {
2322 for (var j
= 0; j
< colIdx
.length
; j
++) {
2323 var col
= colIdx
[j
];
2324 row
.push(data
.getValue(i
, col
));
2325 if (hasAnnotations
&&
2326 annotationCols
.hasOwnProperty(col
) &&
2327 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2329 ann
.series
= data
.getColumnLabel(col
);
2331 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2333 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2334 if (k
) ann
.text
+= "\n";
2335 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2337 annotations
.push(ann
);
2341 for (var j
= 0; j
< cols
- 1; j
++) {
2342 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2345 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2352 this.warn("DataTable is out of order; order it correctly to speed loading.");
2353 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2355 this.rawData_
= ret
;
2357 if (annotations
.length
> 0) {
2358 this.setAnnotations(annotations
, true);
2362 // These functions are all based on MochiKit.
2363 Dygraph
.update
= function (self
, o
) {
2364 if (typeof(o
) != 'undefined' && o
!== null) {
2366 if (o
.hasOwnProperty(k
)) {
2374 Dygraph
.isArrayLike
= function (o
) {
2375 var typ
= typeof(o
);
2377 (typ
!= 'object' && !(typ
== 'function' &&
2378 typeof(o
.item
) == 'function')) ||
2380 typeof(o
.length
) != 'number' ||
2388 Dygraph
.isDateLike
= function (o
) {
2389 if (typeof(o
) != "object" || o
=== null ||
2390 typeof(o
.getTime
) != 'function') {
2396 Dygraph
.clone
= function(o
) {
2397 // TODO(danvk): figure out how MochiKit's version works
2399 for (var i
= 0; i
< o
.length
; i
++) {
2400 if (Dygraph
.isArrayLike(o
[i
])) {
2401 r
.push(Dygraph
.clone(o
[i
]));
2411 * Get the CSV data. If it's in a function, call that function. If it's in a
2412 * file, do an XMLHttpRequest to get it.
2415 Dygraph
.prototype.start_
= function() {
2416 if (typeof this.file_
== 'function') {
2417 // CSV string. Pretend we got it via XHR.
2418 this.loadedEvent_(this.file_());
2419 } else if (Dygraph
.isArrayLike(this.file_
)) {
2420 this.rawData_
= this.parseArray_(this.file_
);
2421 this.drawGraph_(this.rawData_
);
2422 } else if (typeof this.file_
== 'object' &&
2423 typeof this.file_
.getColumnRange
== 'function') {
2424 // must be a DataTable from gviz.
2425 this.parseDataTable_(this.file_
);
2426 this.drawGraph_(this.rawData_
);
2427 } else if (typeof this.file_
== 'string') {
2428 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2429 if (this.file_
.indexOf('\n') >= 0) {
2430 this.loadedEvent_(this.file_
);
2432 var req
= new XMLHttpRequest();
2434 req
.onreadystatechange
= function () {
2435 if (req
.readyState
== 4) {
2436 if (req
.status
== 200) {
2437 caller
.loadedEvent_(req
.responseText
);
2442 req
.open("GET", this.file_
, true);
2446 this.error("Unknown data format: " + (typeof this.file_
));
2451 * Changes various properties of the graph. These can include:
2453 * <li>file: changes the source data for the graph</li>
2454 * <li>errorBars: changes whether the data contains stddev</li>
2456 * @param {Object} attrs The new properties and values
2458 Dygraph
.prototype.updateOptions
= function(attrs
) {
2459 // TODO(danvk): this is a mess. Rethink this function.
2460 if (attrs
.rollPeriod
) {
2461 this.rollPeriod_
= attrs
.rollPeriod
;
2463 if (attrs
.dateWindow
) {
2464 this.dateWindow_
= attrs
.dateWindow
;
2466 if (attrs
.valueRange
) {
2467 this.valueRange_
= attrs
.valueRange
;
2469 Dygraph
.update(this.user_attrs_
, attrs
);
2470 Dygraph
.update(this.renderOptions_
, attrs
);
2472 this.labelsFromCSV_
= (this.attr_("labels") == null);
2474 // TODO(danvk): this doesn't match the constructor logic
2475 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2476 if (attrs
['file']) {
2477 this.file_
= attrs
['file'];
2480 this.drawGraph_(this.rawData_
);
2485 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2486 * containing div (which has presumably changed size since the dygraph was
2487 * instantiated. If the width/height are specified, the div will be resized.
2489 * This is far more efficient than destroying and re-instantiating a
2490 * Dygraph, since it doesn't have to reparse the underlying data.
2492 * @param {Number} width Width (in pixels)
2493 * @param {Number} height Height (in pixels)
2495 Dygraph
.prototype.resize
= function(width
, height
) {
2496 if (this.resize_lock
) {
2499 this.resize_lock
= true;
2501 if ((width
=== null) != (height
=== null)) {
2502 this.warn("Dygraph.resize() should be called with zero parameters or " +
2503 "two non-NULL parameters. Pretending it was zero.");
2504 width
= height
= null;
2507 // TODO(danvk): there should be a clear() method.
2508 this.maindiv_
.innerHTML
= "";
2509 this.attrs_
.labelsDiv
= null;
2512 this.maindiv_
.style
.width
= width
+ "px";
2513 this.maindiv_
.style
.height
= height
+ "px";
2514 this.width_
= width
;
2515 this.height_
= height
;
2517 this.width_
= this.maindiv_
.offsetWidth
;
2518 this.height_
= this.maindiv_
.offsetHeight
;
2521 this.createInterface_();
2522 this.drawGraph_(this.rawData_
);
2524 this.resize_lock
= false;
2528 * Adjusts the number of days in the rolling average. Updates the graph to
2529 * reflect the new averaging period.
2530 * @param {Number} length Number of days over which to average the data.
2532 Dygraph
.prototype.adjustRoll
= function(length
) {
2533 this.rollPeriod_
= length
;
2534 this.drawGraph_(this.rawData_
);
2538 * Returns a boolean array of visibility statuses.
2540 Dygraph
.prototype.visibility
= function() {
2541 // Do lazy-initialization, so that this happens after we know the number of
2543 if (!this.attr_("visibility")) {
2544 this.attrs_
["visibility"] = [];
2546 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2547 this.attr_("visibility").push(true);
2549 return this.attr_("visibility");
2553 * Changes the visiblity of a series.
2555 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2556 var x
= this.visibility();
2557 if (num
< 0 && num
>= x
.length
) {
2558 this.warn("invalid series number in setVisibility: " + num
);
2561 this.drawGraph_(this.rawData_
);
2566 * Update the list of annotations and redraw the chart.
2568 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2569 this.annotations_
= ann
;
2570 this.layout_
.setAnnotations(this.annotations_
);
2571 if (!suppressDraw
) {
2572 this.drawGraph_(this.rawData_
);
2577 * Return the list of annotations.
2579 Dygraph
.prototype.annotations
= function() {
2580 return this.annotations_
;
2583 Dygraph
.addAnnotationRule
= function() {
2584 if (Dygraph
.addedAnnotationCSS
) return;
2587 if (document
.styleSheets
.length
> 0) {
2588 mysheet
= document
.styleSheets
[0];
2590 var styleSheetElement
= document
.createElement("style");
2591 styleSheetElement
.type
= "text/css";
2592 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2593 for(i
= 0; i
< document
.styleSheets
.length
; i
++) {
2594 if (document
.styleSheets
[i
].disabled
) continue;
2595 mysheet
= document
.styleSheets
[i
];
2599 var rule
= "border: 1px solid black; " +
2600 "background-color: white; " +
2601 "text-align: center;";
2602 if (mysheet
.insertRule
) { // Firefox
2603 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", 0);
2604 } else if (mysheet
.addRule
) { // IE
2605 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2608 Dygraph
.addedAnnotationCSS
= true;
2612 * Create a new canvas element. This is more complex than a simple
2613 * document.createElement("canvas") because of IE and excanvas.
2615 Dygraph
.createCanvas
= function() {
2616 var canvas
= document
.createElement("canvas");
2618 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2620 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2628 * A wrapper around Dygraph that implements the gviz API.
2629 * @param {Object} container The DOM object the visualization should live in.
2631 Dygraph
.GVizChart
= function(container
) {
2632 this.container
= container
;
2635 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2636 this.container
.innerHTML
= '';
2637 this.date_graph
= new Dygraph(this.container
, data
, options
);
2641 * Google charts compatible setSelection
2642 * Only row selection is supported, all points in the row will be highlighted
2643 * @param {Array} array of the selected cells
2646 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2648 if (selection_array
.length
) {
2649 row
= selection_array
[0].row
;
2651 this.date_graph
.setSelection(row
);
2655 * Google charts compatible getSelection implementation
2656 * @return {Array} array of the selected cells
2659 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2662 var row
= this.date_graph
.getSelection();
2664 if (row
< 0) return selection
;
2667 for (var i
in this.date_graph
.layout_
.datasets
) {
2668 selection
.push({row
: row
, column
: col
});
2675 // Older pages may still use this name.
2676 DateGraph
= Dygraph
;