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 // Hack for IE: if we're using excanvas and the document hasn't finished
170 // loading yet (and hence may not have initialized whatever it needs to
171 // initialize), then keep calling this routine periodically until it has.
172 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
173 typeof(G_vmlCanvasManager
) != 'undefined' &&
174 document
.readyState
!= 'complete') {
176 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
179 // Support two-argument constructor
180 if (attrs
== null) { attrs
= {}; }
182 // Copy the important bits into the object
183 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
186 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
187 this.previousVerticalX_
= -1;
188 this.fractions_
= attrs
.fractions
|| false;
189 this.dateWindow_
= attrs
.dateWindow
|| null;
191 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
192 this.is_initial_draw_
= true;
193 this.annotations_
= [];
195 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
197 this.zoomedX
= false;
198 this.zoomedY
= false;
200 // Clear the div. This ensure that, if multiple dygraphs are passed the same
201 // div, then only one will be drawn.
204 // If the div isn't already sized then inherit from our attrs or
205 // give it a default size.
206 if (div
.style
.width
== '') {
207 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
209 if (div
.style
.height
== '') {
210 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
212 this.width_
= parseInt(div
.style
.width
, 10);
213 this.height_
= parseInt(div
.style
.height
, 10);
214 // The div might have been specified as percent of the current window size,
215 // convert that to an appropriate number of pixels.
216 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
217 this.width_
= div
.offsetWidth
;
219 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
220 this.height_
= div
.offsetHeight
;
223 if (this.width_
== 0) {
224 this.error("dygraph has zero width. Please specify a width in pixels.");
226 if (this.height_
== 0) {
227 this.error("dygraph has zero height. Please specify a height in pixels.");
230 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
231 if (attrs
['stackedGraph']) {
232 attrs
['fillGraph'] = true;
233 // TODO(nikhilk): Add any other stackedGraph checks here.
236 // Dygraphs has many options, some of which interact with one another.
237 // To keep track of everything, we maintain two sets of options:
239 // this.user_attrs_ only options explicitly set by the user.
240 // this.attrs_ defaults, options derived from user_attrs_, data.
242 // Options are then accessed this.attr_('attr'), which first looks at
243 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
244 // defaults without overriding behavior that the user specifically asks for.
245 this.user_attrs_
= {};
246 Dygraph
.update(this.user_attrs_
, attrs
);
249 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
251 this.boundaryIds_
= [];
253 // Make a note of whether labels will be pulled from the CSV file.
254 this.labelsFromCSV_
= (this.attr_("labels") == null);
256 // Create the containing DIV and other interactive elements
257 this.createInterface_();
262 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
264 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
265 this.user_attrs_
[seriesName
] != null &&
266 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
267 return this.user_attrs_
[seriesName
][name
];
268 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
269 return this.user_attrs_
[name
];
270 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
271 return this.attrs_
[name
];
277 // TODO(danvk): any way I can get the line numbers to be this.warn call?
278 Dygraph
.prototype.log
= function(severity
, message
) {
279 if (typeof(console
) != 'undefined') {
282 console
.debug('dygraphs: ' + message
);
285 console
.info('dygraphs: ' + message
);
287 case Dygraph
.WARNING
:
288 console
.warn('dygraphs: ' + message
);
291 console
.error('dygraphs: ' + message
);
296 Dygraph
.prototype.info
= function(message
) {
297 this.log(Dygraph
.INFO
, message
);
299 Dygraph
.prototype.warn
= function(message
) {
300 this.log(Dygraph
.WARNING
, message
);
302 Dygraph
.prototype.error
= function(message
) {
303 this.log(Dygraph
.ERROR
, message
);
307 * Returns the current rolling period, as set by the user or an option.
308 * @return {Number} The number of days in the rolling window
310 Dygraph
.prototype.rollPeriod
= function() {
311 return this.rollPeriod_
;
315 * Returns the currently-visible x-range. This can be affected by zooming,
316 * panning or a call to updateOptions.
317 * Returns a two-element array: [left, right].
318 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
320 Dygraph
.prototype.xAxisRange
= function() {
321 if (this.dateWindow_
) return this.dateWindow_
;
323 // The entire chart is visible.
324 var left
= this.rawData_
[0][0];
325 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
326 return [left
, right
];
330 * Returns the currently-visible y-range for an axis. This can be affected by
331 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
332 * called with no arguments, returns the range of the first axis.
333 * Returns a two-element array: [bottom, top].
335 Dygraph
.prototype.yAxisRange
= function(idx
) {
336 if (typeof(idx
) == "undefined") idx
= 0;
337 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
338 return [ this.axes_
[idx
].computedValueRange
[0],
339 this.axes_
[idx
].computedValueRange
[1] ];
343 * Returns the currently-visible y-ranges for each axis. This can be affected by
344 * zooming, panning, calls to updateOptions, etc.
345 * Returns an array of [bottom, top] pairs, one for each y-axis.
347 Dygraph
.prototype.yAxisRanges
= function() {
349 for (var i
= 0; i
< this.axes_
.length
; i
++) {
350 ret
.push(this.yAxisRange(i
));
355 // TODO(danvk): use these functions throughout dygraphs.
357 * Convert from data coordinates to canvas/div X/Y coordinates.
358 * If specified, do this conversion for the coordinate system of a particular
359 * axis. Uses the first axis by default.
360 * Returns a two-element array: [X, Y]
362 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
363 var ret
= [null, null];
364 var area
= this.plotter_
.area
;
366 var xRange
= this.xAxisRange();
367 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
371 var yRange
= this.yAxisRange(axis
);
372 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
379 * Convert from canvas/div coords to data coordinates.
380 * If specified, do this conversion for the coordinate system of a particular
381 * axis. Uses the first axis by default.
382 * Returns a two-element array: [X, Y]
384 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
385 var ret
= [null, null];
386 var area
= this.plotter_
.area
;
388 var xRange
= this.xAxisRange();
389 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
393 var yRange
= this.yAxisRange(axis
);
394 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
401 * Returns the number of columns (including the independent variable).
403 Dygraph
.prototype.numColumns
= function() {
404 return this.rawData_
[0].length
;
408 * Returns the number of rows (excluding any header/label row).
410 Dygraph
.prototype.numRows
= function() {
411 return this.rawData_
.length
;
415 * Returns the value in the given row and column. If the row and column exceed
416 * the bounds on the data, returns null. Also returns null if the value is
419 Dygraph
.prototype.getValue
= function(row
, col
) {
420 if (row
< 0 || row
> this.rawData_
.length
) return null;
421 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
423 return this.rawData_
[row
][col
];
426 Dygraph
.addEvent
= function(el
, evt
, fn
) {
427 var normed_fn
= function(e
) {
428 if (!e
) var e
= window
.event
;
431 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
432 el
.addEventListener(evt
, normed_fn
, false);
434 el
.attachEvent('on' + evt
, normed_fn
);
439 * Generates interface elements for the Dygraph: a containing div, a div to
440 * display the current point, and a textbox to adjust the rolling average
441 * period. Also creates the Renderer/Layout elements.
444 Dygraph
.prototype.createInterface_
= function() {
445 // Create the all-enclosing graph div
446 var enclosing
= this.maindiv_
;
448 this.graphDiv
= document
.createElement("div");
449 this.graphDiv
.style
.width
= this.width_
+ "px";
450 this.graphDiv
.style
.height
= this.height_
+ "px";
451 enclosing
.appendChild(this.graphDiv
);
453 // Create the canvas for interactive parts of the chart.
454 this.canvas_
= Dygraph
.createCanvas();
455 this.canvas_
.style
.position
= "absolute";
456 this.canvas_
.width
= this.width_
;
457 this.canvas_
.height
= this.height_
;
458 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
459 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
461 // ... and for static parts of the chart.
462 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
464 // The interactive parts of the graph are drawn on top of the chart.
465 this.graphDiv
.appendChild(this.hidden_
);
466 this.graphDiv
.appendChild(this.canvas_
);
467 this.mouseEventElement_
= this.canvas_
;
470 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
471 dygraph
.mouseMove_(e
);
473 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
474 dygraph
.mouseOut_(e
);
477 // Create the grapher
478 // TODO(danvk): why does the Layout need its own set of options?
479 this.layoutOptions_
= { 'xOriginIsZero': false };
480 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
481 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
482 Dygraph
.update(this.layoutOptions_
, {
483 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
485 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
487 // TODO(danvk): why does the Renderer need its own set of options?
488 this.renderOptions_
= { colorScheme
: this.colors_
,
490 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
491 Dygraph
.update(this.renderOptions_
, this.attrs_
);
492 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
494 this.createStatusMessage_();
495 this.createDragInterface_();
499 * Detach DOM elements in the dygraph and null out all data references.
500 * Calling this when you're done with a dygraph can dramatically reduce memory
501 * usage. See, e.g., the tests/perf.html example.
503 Dygraph
.prototype.destroy
= function() {
504 var removeRecursive
= function(node
) {
505 while (node
.hasChildNodes()) {
506 removeRecursive(node
.firstChild
);
507 node
.removeChild(node
.firstChild
);
510 removeRecursive(this.maindiv_
);
512 var nullOut
= function(obj
) {
514 if (typeof(obj
[n
]) === 'object') {
520 // These may not all be necessary, but it can't hurt...
521 nullOut(this.layout_
);
522 nullOut(this.plotter_
);
527 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
528 * this particular canvas. All Dygraph work is done on this.canvas_.
529 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
530 * @return {Object} The newly-created canvas
533 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
534 var h
= Dygraph
.createCanvas();
535 h
.style
.position
= "absolute";
536 // TODO(danvk): h should be offset from canvas. canvas needs to include
537 // some extra area to make it easier to zoom in on the far left and far
538 // right. h needs to be precisely the plot area, so that clipping occurs.
539 h
.style
.top
= canvas
.style
.top
;
540 h
.style
.left
= canvas
.style
.left
;
541 h
.width
= this.width_
;
542 h
.height
= this.height_
;
543 h
.style
.width
= this.width_
+ "px"; // for IE
544 h
.style
.height
= this.height_
+ "px"; // for IE
548 // Taken from MochiKit.Color
549 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
553 if (saturation
=== 0) {
558 var i
= Math
.floor(hue
* 6);
559 var f
= (hue
* 6) - i
;
560 var p
= value
* (1 - saturation
);
561 var q
= value
* (1 - (saturation
* f
));
562 var t
= value
* (1 - (saturation
* (1 - f
)));
564 case 1: red
= q
; green
= value
; blue
= p
; break;
565 case 2: red
= p
; green
= value
; blue
= t
; break;
566 case 3: red
= p
; green
= q
; blue
= value
; break;
567 case 4: red
= t
; green
= p
; blue
= value
; break;
568 case 5: red
= value
; green
= p
; blue
= q
; break;
569 case 6: // fall through
570 case 0: red
= value
; green
= t
; blue
= p
; break;
573 red
= Math
.floor(255 * red
+ 0.5);
574 green
= Math
.floor(255 * green
+ 0.5);
575 blue
= Math
.floor(255 * blue
+ 0.5);
576 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
581 * Generate a set of distinct colors for the data series. This is done with a
582 * color wheel. Saturation/Value are customizable, and the hue is
583 * equally-spaced around the color wheel. If a custom set of colors is
584 * specified, that is used instead.
587 Dygraph
.prototype.setColors_
= function() {
588 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
589 // away with this.renderOptions_.
590 var num
= this.attr_("labels").length
- 1;
592 var colors
= this.attr_('colors');
594 var sat
= this.attr_('colorSaturation') || 1.0;
595 var val
= this.attr_('colorValue') || 0.5;
596 var half
= Math
.ceil(num
/ 2);
597 for (var i
= 1; i
<= num
; i
++) {
598 if (!this.visibility()[i
-1]) continue;
599 // alternate colors for high contrast.
600 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
601 var hue
= (1.0 * idx
/ (1 + num
));
602 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
605 for (var i
= 0; i
< num
; i
++) {
606 if (!this.visibility()[i
]) continue;
607 var colorStr
= colors
[i
% colors
.length
];
608 this.colors_
.push(colorStr
);
612 // TODO(danvk): update this w/r
/t/ the
new options system
.
613 this.renderOptions_
.colorScheme
= this.colors_
;
614 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
615 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
616 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
620 * Return the list of colors. This is either the list of colors passed in the
621 * attributes, or the autogenerated list of rgb(r,g,b) strings.
622 * @return {Array<string>} The list of colors.
624 Dygraph
.prototype.getColors
= function() {
628 // The following functions are from quirksmode.org with a modification for Safari from
629 // http://blog.firetree.net/2005/07/04/javascript-find-position/
630 // http://www.quirksmode.org/js
/findpos
.html
631 Dygraph
.findPosX
= function(obj
) {
636 curleft
+= obj
.offsetLeft
;
637 if(!obj
.offsetParent
)
639 obj
= obj
.offsetParent
;
646 Dygraph
.findPosY
= function(obj
) {
651 curtop
+= obj
.offsetTop
;
652 if(!obj
.offsetParent
)
654 obj
= obj
.offsetParent
;
664 * Create the div that contains information on the selected point(s)
665 * This goes in the top right of the canvas, unless an external div has already
669 Dygraph
.prototype.createStatusMessage_
= function() {
670 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
671 if (userLabelsDiv
&& null != userLabelsDiv
672 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
673 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
675 if (!this.attr_("labelsDiv")) {
676 var divWidth
= this.attr_('labelsDivWidth');
678 "position": "absolute",
681 "width": divWidth
+ "px",
683 "left": (this.width_
- divWidth
- 2) + "px",
684 "background": "white",
686 "overflow": "hidden"};
687 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
688 var div
= document
.createElement("div");
689 for (var name
in messagestyle
) {
690 if (messagestyle
.hasOwnProperty(name
)) {
691 div
.style
[name
] = messagestyle
[name
];
694 this.graphDiv
.appendChild(div
);
695 this.attrs_
.labelsDiv
= div
;
700 * Position the labels div so that its right edge is flush with the right edge
701 * of the charting area.
703 Dygraph
.prototype.positionLabelsDiv_
= function() {
704 // Don't touch a user-specified labelsDiv.
705 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
707 var area
= this.plotter_
.area
;
708 var div
= this.attr_("labelsDiv");
709 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
713 * Create the text box to adjust the averaging period
716 Dygraph
.prototype.createRollInterface_
= function() {
717 // Create a roller if one doesn't exist already.
719 this.roller_
= document
.createElement("input");
720 this.roller_
.type
= "text";
721 this.roller_
.style
.display
= "none";
722 this.graphDiv
.appendChild(this.roller_
);
725 var display
= this.attr_('showRoller') ? 'block' : 'none';
727 var textAttr
= { "position": "absolute",
729 "top": (this.plotter_
.area
.h
- 25) + "px",
730 "left": (this.plotter_
.area
.x
+ 1) + "px",
733 this.roller_
.size
= "2";
734 this.roller_
.value
= this.rollPeriod_
;
735 for (var name
in textAttr
) {
736 if (textAttr
.hasOwnProperty(name
)) {
737 this.roller_
.style
[name
] = textAttr
[name
];
742 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
745 // These functions are taken from MochiKit.Signal
746 Dygraph
.pageX
= function(e
) {
748 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
751 var b
= document
.body
;
753 (de
.scrollLeft
|| b
.scrollLeft
) -
754 (de
.clientLeft
|| 0);
758 Dygraph
.pageY
= function(e
) {
760 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
763 var b
= document
.body
;
765 (de
.scrollTop
|| b
.scrollTop
) -
771 * Set up all the mouse handlers needed to capture dragging behavior for zoom
775 Dygraph
.prototype.createDragInterface_
= function() {
778 // Tracks whether the mouse is down right now
779 var isZooming
= false;
780 var isPanning
= false; // is this drag part of a pan?
781 var is2DPan
= false; // if so, is that pan 1- or 2-dimensional?
782 var dragStartX
= null;
783 var dragStartY
= null;
786 var dragDirection
= null;
789 var prevDragDirection
= null;
791 // TODO(danvk): update this comment
792 // draggingDate and draggingValue represent the [date,value] point on the
793 // graph at which the mouse was pressed. As the mouse moves while panning,
794 // the viewport must pan so that the mouse position points to
795 // [draggingDate, draggingValue]
796 var draggingDate
= null;
798 // TODO(danvk): update this comment
799 // The range in second/value units that the viewport encompasses during a
800 // panning operation.
801 var dateRange
= null;
803 // Utility function to convert page-wide coordinates to canvas coords
806 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
807 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
809 // Draw zoom rectangles when the mouse is down and the user moves around
810 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
812 dragEndX
= getX(event
);
813 dragEndY
= getY(event
);
815 var xDelta
= Math
.abs(dragStartX
- dragEndX
);
816 var yDelta
= Math
.abs(dragStartY
- dragEndY
);
818 // drag direction threshold for y axis is twice as large as x axis
819 dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
821 self
.drawZoomRect_(dragDirection
, dragStartX
, dragEndX
, dragStartY
, dragEndY
,
822 prevDragDirection
, prevEndX
, prevEndY
);
826 prevDragDirection
= dragDirection
;
827 } else if (isPanning
) {
828 dragEndX
= getX(event
);
829 dragEndY
= getY(event
);
831 // TODO(danvk): update this comment
832 // Want to have it so that:
833 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
834 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
835 // 3. draggingValue appears at dragEndY.
836 // 4. valueRange is unaltered.
838 var minDate
= draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
839 var maxDate
= minDate
+ dateRange
;
840 self
.dateWindow_
= [minDate
, maxDate
];
843 // y-axis scaling is automatic unless this is a full 2D pan.
845 // Adjust each axis appropriately.
846 var y_frac
= dragEndY
/ self
.height_
;
847 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
848 var axis
= self
.axes_
[i
];
849 var maxValue
= axis
.draggingValue
+ y_frac
* axis
.dragValueRange
;
850 var minValue
= maxValue
- axis
.dragValueRange
;
851 axis
.valueWindow
= [ minValue
, maxValue
];
859 // Track the beginning of drag events
860 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
861 // prevents mouse drags from selecting page text.
862 if (event
.preventDefault
) {
863 event
.preventDefault(); // Firefox, Chrome, etc.
865 event
.returnValue
= false; // IE
866 event
.cancelBubble
= true;
869 px
= Dygraph
.findPosX(self
.canvas_
);
870 py
= Dygraph
.findPosY(self
.canvas_
);
871 dragStartX
= getX(event
);
872 dragStartY
= getY(event
);
874 if (event
.altKey
|| event
.shiftKey
) {
875 // have to be zoomed in to pan.
877 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
878 if (self
.axes_
[i
].valueWindow
|| self
.axes_
[i
].valueRange
) {
883 if (!self
.dateWindow_
&& !zoomedY
) return;
886 var xRange
= self
.xAxisRange();
887 dateRange
= xRange
[1] - xRange
[0];
889 // Record the range of each y-axis at the start of the drag.
890 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
892 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
893 var axis
= self
.axes_
[i
];
894 var yRange
= self
.yAxisRange(i
);
895 axis
.dragValueRange
= yRange
[1] - yRange
[0];
896 var r
= self
.toDataCoords(null, dragStartY
, i
);
897 axis
.draggingValue
= r
[1];
898 if (axis
.valueWindow
|| axis
.valueRange
) is2DPan
= true;
901 // TODO(konigsberg): Switch from all this math to toDataCoords?
902 // Seems to work for the dragging value.
903 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+ xRange
[0];
909 // If the user releases the mouse button during a drag, but not over the
910 // canvas, then it doesn't count as a zooming action.
911 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
912 if (isZooming
|| isPanning
) {
922 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
923 delete self
.axes_
[i
].draggingValue
;
924 delete self
.axes_
[i
].dragValueRange
;
929 // Temporarily cancel the dragging event when the mouse leaves the graph
930 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
937 // If the mouse is released on the canvas during a drag event, then it's a
938 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
939 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
942 dragEndX
= getX(event
);
943 dragEndY
= getY(event
);
944 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
945 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
947 if (regionWidth
< 2 && regionHeight
< 2 &&
948 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
949 // TODO(danvk): pass along more info about the points, e.g. 'x'
950 if (self
.attr_('clickCallback') != null) {
951 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
953 if (self
.attr_('pointClickCallback')) {
954 // check if the click was on a particular point.
956 var closestDistance
= 0;
957 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
958 var p
= self
.selPoints_
[i
];
959 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
960 Math
.pow(p
.canvasy
- dragEndY
, 2);
961 if (closestIdx
== -1 || distance
< closestDistance
) {
962 closestDistance
= distance
;
967 // Allow any click within two pixels of the dot.
968 var radius
= self
.attr_('highlightCircleSize') + 2;
969 if (closestDistance
<= 5 * 5) {
970 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
975 if (regionWidth
>= 10 && dragDirection
== Dygraph
.HORIZONTAL
) {
976 self
.doZoomX_(Math
.min(dragStartX
, dragEndX
),
977 Math
.max(dragStartX
, dragEndX
));
978 } else if (regionHeight
>= 10 && dragDirection
== Dygraph
.VERTICAL
){
979 self
.doZoomY_(Math
.min(dragStartY
, dragEndY
),
980 Math
.max(dragStartY
, dragEndY
));
982 self
.canvas_
.getContext("2d").clearRect(0, 0,
984 self
.canvas_
.height
);
1000 // Double-clicking zooms back out
1001 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
1002 // Disable zooming out if panning.
1003 if (event
.altKey
|| event
.shiftKey
) return;
1010 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1011 * up any previous zoom rectangles that were drawn. This could be optimized to
1012 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1015 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1016 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1017 * @param {Number} startX The X position where the drag started, in canvas
1019 * @param {Number} endX The current X position of the drag, in canvas coords.
1020 * @param {Number} startY The Y position where the drag started, in canvas
1022 * @param {Number} endY The current Y position of the drag, in canvas coords.
1023 * @param {Number} prevDirection the value of direction on the previous call to
1024 * this function. Used to avoid excess redrawing
1025 * @param {Number} prevEndX The value of endX on the previous call to this
1026 * function. Used to avoid excess redrawing
1027 * @param {Number} prevEndY The value of endY on the previous call to this
1028 * function. Used to avoid excess redrawing
1031 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1032 prevDirection
, prevEndX
, prevEndY
) {
1033 var ctx
= this.canvas_
.getContext("2d");
1035 // Clean up from the previous rect if necessary
1036 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1037 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1038 Math
.abs(startX
- prevEndX
), this.height_
);
1039 } else if (prevDirection
== Dygraph
.VERTICAL
){
1040 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1041 this.width_
, Math
.abs(startY
- prevEndY
));
1044 // Draw a light-grey rectangle to show the new viewing area
1045 if (direction
== Dygraph
.HORIZONTAL
) {
1046 if (endX
&& startX
) {
1047 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1048 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1049 Math
.abs(endX
- startX
), this.height_
);
1052 if (direction
== Dygraph
.VERTICAL
) {
1053 if (endY
&& startY
) {
1054 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1055 ctx
.fillRect(0, Math
.min(startY
, endY
),
1056 this.width_
, Math
.abs(endY
- startY
));
1062 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1063 * the canvas. The exact zoom window may be slightly larger if there are no data
1064 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1065 * which accepts dates that match the raw data. This function redraws the graph.
1067 * @param {Number} lowX The leftmost pixel value that should be visible.
1068 * @param {Number} highX The rightmost pixel value that should be visible.
1071 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1072 // Find the earliest and latest dates contained in this canvasx range.
1073 // Convert the call to date ranges of the raw data.
1074 var r
= this.toDataCoords(lowX
, null);
1076 r
= this.toDataCoords(highX
, null);
1078 this.doZoomXDates_(minDate
, maxDate
);
1082 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1083 * method with doZoomX which accepts pixel coordinates. This function redraws
1086 * @param {Number} minDate The minimum date that should be visible.
1087 * @param {Number} maxDate The maximum date that should be visible.
1090 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1091 this.dateWindow_
= [minDate
, maxDate
];
1093 this.zoomedX
= true;
1095 if (this.attr_("zoomCallback")) {
1096 var yRange
= this.yAxisRange();
1097 this.attr_("zoomCallback")(minDate
, maxDate
, yRange
[0], yRange
[1]);
1102 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1103 * the canvas. This function redraws the graph.
1105 * @param {Number} lowY The topmost pixel value that should be visible.
1106 * @param {Number} highY The lowest pixel value that should be visible.
1109 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1110 // Find the highest and lowest values in pixel range for each axis.
1111 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1112 // This is because pixels increase as you go down on the screen, whereas data
1113 // coordinates increase as you go up the screen.
1114 var valueRanges
= [];
1115 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1116 var hi
= this.toDataCoords(null, lowY
, i
);
1117 var low
= this.toDataCoords(null, highY
, i
);
1118 this.axes_
[i
].valueWindow
= [low
[1], hi
[1]];
1119 valueRanges
.push([low
[1], hi
[1]]);
1123 this.zoomedY
= true;
1125 if (this.attr_("zoomCallback")) {
1126 var xRange
= this.xAxisRange();
1127 var yRange
= this.yAxisRange();
1128 this.attr_("zoomCallback")(xRange
[0], xRange
[1], yRange
[0], yRange
[1]);
1133 * Reset the zoom to the original view coordinates. This is the same as
1134 * double-clicking on the graph.
1138 Dygraph
.prototype.doUnzoom_
= function() {
1140 if (this.dateWindow_
!= null) {
1142 this.dateWindow_
= null;
1145 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1146 if (this.axes_
[i
].valueWindow
!= null) {
1148 delete this.axes_
[i
].valueWindow
;
1153 // Putting the drawing operation before the callback because it resets
1155 this.zoomed
= false;
1156 this.zoomedX
= false;
1157 this.zoomedY
= false;
1159 if (this.attr_("zoomCallback")) {
1160 var minDate
= this.rawData_
[0][0];
1161 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1162 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1168 * When the mouse moves in the canvas, display information about a nearby data
1169 * point and draw dots over those points in the data series. This function
1170 * takes care of cleanup of previously-drawn dots.
1171 * @param {Object} event The mousemove event from the browser.
1174 Dygraph
.prototype.mouseMove_
= function(event
) {
1175 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1176 var points
= this.layout_
.points
;
1181 // Loop through all the points and find the date nearest to our current
1183 var minDist
= 1e+100;
1185 for (var i
= 0; i
< points
.length
; i
++) {
1186 var point
= points
[i
];
1187 if (point
== null) continue;
1188 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1189 if (dist
> minDist
) continue;
1193 if (idx
>= 0) lastx
= points
[idx
].xval
;
1194 // Check that you can really highlight the last day's data
1195 var last
= points
[points
.length
-1];
1196 if (last
!= null && canvasx
> last
.canvasx
)
1197 lastx
= points
[points
.length
-1].xval
;
1199 // Extract the points we've selected
1200 this.selPoints_
= [];
1201 var l
= points
.length
;
1202 if (!this.attr_("stackedGraph")) {
1203 for (var i
= 0; i
< l
; i
++) {
1204 if (points
[i
].xval
== lastx
) {
1205 this.selPoints_
.push(points
[i
]);
1209 // Need to 'unstack' points starting from the bottom
1210 var cumulative_sum
= 0;
1211 for (var i
= l
- 1; i
>= 0; i
--) {
1212 if (points
[i
].xval
== lastx
) {
1213 var p
= {}; // Clone the point since we modify it
1214 for (var k
in points
[i
]) {
1215 p
[k
] = points
[i
][k
];
1217 p
.yval
-= cumulative_sum
;
1218 cumulative_sum
+= p
.yval
;
1219 this.selPoints_
.push(p
);
1222 this.selPoints_
.reverse();
1225 if (this.attr_("highlightCallback")) {
1226 var px
= this.lastx_
;
1227 if (px
!== null && lastx
!= px
) {
1228 // only fire if the selected point has changed.
1229 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1233 // Save last x position for callbacks.
1234 this.lastx_
= lastx
;
1236 this.updateSelection_();
1240 * Transforms layout_.points index into data row number.
1241 * @param int layout_.points index
1242 * @return int row number, or -1 if none could be found.
1245 Dygraph
.prototype.idxToRow_
= function(idx
) {
1246 if (idx
< 0) return -1;
1248 for (var i
in this.layout_
.datasets
) {
1249 if (idx
< this.layout_
.datasets
[i
].length
) {
1250 return this.boundaryIds_
[0][0]+idx
;
1252 idx
-= this.layout_
.datasets
[i
].length
;
1258 * Draw dots over the selectied points in the data series. This function
1259 * takes care of cleanup of previously-drawn dots.
1262 Dygraph
.prototype.updateSelection_
= function() {
1263 // Clear the previously drawn vertical, if there is one
1264 var ctx
= this.canvas_
.getContext("2d");
1265 if (this.previousVerticalX_
>= 0) {
1266 // Determine the maximum highlight circle size.
1267 var maxCircleSize
= 0;
1268 var labels
= this.attr_('labels');
1269 for (var i
= 1; i
< labels
.length
; i
++) {
1270 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1271 if (r
> maxCircleSize
) maxCircleSize
= r
;
1273 var px
= this.previousVerticalX_
;
1274 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1275 2 * maxCircleSize
+ 2, this.height_
);
1278 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1280 if (this.selPoints_
.length
> 0) {
1281 var canvasx
= this.selPoints_
[0].canvasx
;
1283 // Set the status message to indicate the selected point(s)
1284 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1285 var fmtFunc
= this.attr_('yValueFormatter');
1286 var clen
= this.colors_
.length
;
1288 if (this.attr_('showLabelsOnHighlight')) {
1289 // Set the status message to indicate the selected point(s)
1290 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1291 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1292 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1293 if (this.attr_("labelsSeparateLines")) {
1296 var point
= this.selPoints_
[i
];
1297 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1298 var yval
= fmtFunc(point
.yval
);
1299 replace
+= " <b><font color='" + c
.toHex() + "'>"
1300 + point
.name
+ "</font></b>:"
1304 this.attr_("labelsDiv").innerHTML
= replace
;
1307 // Draw colored circles over the center of each selected point
1309 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1310 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1312 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1314 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1315 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1316 0, 2 * Math
.PI
, false);
1321 this.previousVerticalX_
= canvasx
;
1326 * Set manually set selected dots, and display information about them
1327 * @param int row number that should by highlighted
1328 * false value clears the selection
1331 Dygraph
.prototype.setSelection
= function(row
) {
1332 // Extract the points we've selected
1333 this.selPoints_
= [];
1336 if (row
!== false) {
1337 row
= row
-this.boundaryIds_
[0][0];
1340 if (row
!== false && row
>= 0) {
1341 for (var i
in this.layout_
.datasets
) {
1342 if (row
< this.layout_
.datasets
[i
].length
) {
1343 var point
= this.layout_
.points
[pos
+row
];
1345 if (this.attr_("stackedGraph")) {
1346 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1349 this.selPoints_
.push(point
);
1351 pos
+= this.layout_
.datasets
[i
].length
;
1355 if (this.selPoints_
.length
) {
1356 this.lastx_
= this.selPoints_
[0].xval
;
1357 this.updateSelection_();
1360 this.clearSelection();
1366 * The mouse has left the canvas. Clear out whatever artifacts remain
1367 * @param {Object} event the mouseout event from the browser.
1370 Dygraph
.prototype.mouseOut_
= function(event
) {
1371 if (this.attr_("unhighlightCallback")) {
1372 this.attr_("unhighlightCallback")(event
);
1375 if (this.attr_("hideOverlayOnMouseOut")) {
1376 this.clearSelection();
1381 * Remove all selection from the canvas
1384 Dygraph
.prototype.clearSelection
= function() {
1385 // Get rid of the overlay data
1386 var ctx
= this.canvas_
.getContext("2d");
1387 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1388 this.attr_("labelsDiv").innerHTML
= "";
1389 this.selPoints_
= [];
1394 * Returns the number of the currently selected row
1395 * @return int row number, of -1 if nothing is selected
1398 Dygraph
.prototype.getSelection
= function() {
1399 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1403 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1404 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1405 return row
+ this.boundaryIds_
[0][0];
1411 Dygraph
.zeropad
= function(x
) {
1412 if (x
< 10) return "0" + x
; else return "" + x
;
1416 * Return a string version of the hours, minutes and seconds portion of a date.
1417 * @param {Number} date The JavaScript date (ms since epoch)
1418 * @return {String} A time of the form "HH:MM:SS"
1421 Dygraph
.hmsString_
= function(date
) {
1422 var zeropad
= Dygraph
.zeropad
;
1423 var d
= new Date(date
);
1424 if (d
.getSeconds()) {
1425 return zeropad(d
.getHours()) + ":" +
1426 zeropad(d
.getMinutes()) + ":" +
1427 zeropad(d
.getSeconds());
1429 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1434 * Convert a JS date to a string appropriate to display on an axis that
1435 * is displaying values at the stated granularity.
1436 * @param {Date} date The date to format
1437 * @param {Number} granularity One of the Dygraph granularity constants
1438 * @return {String} The formatted date
1441 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1442 if (granularity
>= Dygraph
.MONTHLY
) {
1443 return date
.strftime('%b %y');
1445 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1446 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1447 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1449 return Dygraph
.hmsString_(date
.getTime());
1455 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1456 * @param {Number} date The JavaScript date (ms since epoch)
1457 * @return {String} A date of the form "YYYY/MM/DD"
1460 Dygraph
.dateString_
= function(date
, self
) {
1461 var zeropad
= Dygraph
.zeropad
;
1462 var d
= new Date(date
);
1465 var year
= "" + d
.getFullYear();
1466 // Get a 0 padded month string
1467 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1468 // Get a 0 padded day string
1469 var day
= zeropad(d
.getDate());
1472 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1473 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1475 return year
+ "/" + month + "/" + day
+ ret
;
1479 * Round a number to the specified number of digits past the decimal point.
1480 * @param {Number} num The number to round
1481 * @param {Number} places The number of decimals to which to round
1482 * @return {Number} The rounded number
1485 Dygraph
.round_
= function(num
, places
) {
1486 var shift
= Math
.pow(10, places
);
1487 return Math
.round(num
* shift
)/shift
;
1491 * Fires when there's data available to be graphed.
1492 * @param {String} data Raw CSV data to be plotted
1495 Dygraph
.prototype.loadedEvent_
= function(data
) {
1496 this.rawData_
= this.parseCSV_(data
);
1500 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1501 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1502 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1505 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1508 Dygraph
.prototype.addXTicks_
= function() {
1509 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1510 var startDate
, endDate
;
1511 if (this.dateWindow_
) {
1512 startDate
= this.dateWindow_
[0];
1513 endDate
= this.dateWindow_
[1];
1515 startDate
= this.rawData_
[0][0];
1516 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1519 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1520 this.layout_
.updateOptions({xTicks
: xTicks
});
1523 // Time granularity enumeration
1524 Dygraph
.SECONDLY
= 0;
1525 Dygraph
.TWO_SECONDLY
= 1;
1526 Dygraph
.FIVE_SECONDLY
= 2;
1527 Dygraph
.TEN_SECONDLY
= 3;
1528 Dygraph
.THIRTY_SECONDLY
= 4;
1529 Dygraph
.MINUTELY
= 5;
1530 Dygraph
.TWO_MINUTELY
= 6;
1531 Dygraph
.FIVE_MINUTELY
= 7;
1532 Dygraph
.TEN_MINUTELY
= 8;
1533 Dygraph
.THIRTY_MINUTELY
= 9;
1534 Dygraph
.HOURLY
= 10;
1535 Dygraph
.TWO_HOURLY
= 11;
1536 Dygraph
.SIX_HOURLY
= 12;
1538 Dygraph
.WEEKLY
= 14;
1539 Dygraph
.MONTHLY
= 15;
1540 Dygraph
.QUARTERLY
= 16;
1541 Dygraph
.BIANNUAL
= 17;
1542 Dygraph
.ANNUAL
= 18;
1543 Dygraph
.DECADAL
= 19;
1544 Dygraph
.NUM_GRANULARITIES
= 20;
1546 Dygraph
.SHORT_SPACINGS
= [];
1547 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1548 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1549 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1550 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1551 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1552 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1553 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1554 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1555 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1556 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1557 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1558 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1559 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1560 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1561 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1565 // If we used this time granularity, how many ticks would there be?
1566 // This is only an approximation, but it's generally good enough.
1568 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1569 if (granularity
< Dygraph
.MONTHLY
) {
1570 // Generate one tick mark for every fixed interval of time.
1571 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1572 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1574 var year_mod
= 1; // e.g. to only print one point every 10 years.
1575 var num_months
= 12;
1576 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1577 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1578 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1579 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1581 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1582 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1583 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1589 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1590 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1592 // Returns an array containing {v: millis, label: label} dictionaries.
1594 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1595 var formatter
= this.attr_("xAxisLabelFormatter");
1597 if (granularity
< Dygraph
.MONTHLY
) {
1598 // Generate one tick mark for every fixed interval of time.
1599 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1600 var format
= '%d%b'; // e.g. "1Jan"
1602 // Find a time less than start_time which occurs on a "nice" time boundary
1603 // for this granularity.
1604 var g
= spacing
/ 1000;
1605 var d
= new Date(start_time
);
1606 if (g
<= 60) { // seconds
1607 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1611 if (g
<= 60) { // minutes
1612 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1617 if (g
<= 24) { // days
1618 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1623 if (g
== 7) { // one week
1624 d
.setDate(d
.getDate() - d
.getDay());
1629 start_time
= d
.getTime();
1631 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1632 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1635 // Display a tick mark on the first of a set of months of each year.
1636 // Years get a tick mark iff y % year_mod == 0. This is useful for
1637 // displaying a tick mark once every 10 years, say, on long time scales.
1639 var year_mod
= 1; // e.g. to only print one point every 10 years.
1641 if (granularity
== Dygraph
.MONTHLY
) {
1642 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1643 } else if (granularity
== Dygraph
.QUARTERLY
) {
1644 months
= [ 0, 3, 6, 9 ];
1645 } else if (granularity
== Dygraph
.BIANNUAL
) {
1647 } else if (granularity
== Dygraph
.ANNUAL
) {
1649 } else if (granularity
== Dygraph
.DECADAL
) {
1654 var start_year
= new Date(start_time
).getFullYear();
1655 var end_year
= new Date(end_time
).getFullYear();
1656 var zeropad
= Dygraph
.zeropad
;
1657 for (var i
= start_year
; i
<= end_year
; i
++) {
1658 if (i
% year_mod
!= 0) continue;
1659 for (var j
= 0; j
< months
.length
; j
++) {
1660 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1661 var t
= Date
.parse(date_str
);
1662 if (t
< start_time
|| t
> end_time
) continue;
1663 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1673 * Add ticks to the x-axis based on a date range.
1674 * @param {Number} startDate Start of the date window (millis since epoch)
1675 * @param {Number} endDate End of the date window (millis since epoch)
1676 * @return {Array.<Object>} Array of {label, value} tuples.
1679 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1681 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1682 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1683 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1690 return self
.GetXAxis(startDate
, endDate
, chosen
);
1692 // TODO(danvk): signal error.
1697 * Add ticks when the x axis has numbers on it (instead of dates)
1698 * @param {Number} startDate Start of the date window (millis since epoch)
1699 * @param {Number} endDate End of the date window (millis since epoch)
1701 * @param {function} attribute accessor function.
1702 * @return {Array.<Object>} Array of {label, value} tuples.
1705 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1706 var attr
= function(k
) {
1707 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1708 return self
.attr_(k
);
1713 for (var i
= 0; i
< vals
.length
; i
++) {
1714 ticks
.push({v
: vals
[i
]});
1718 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1719 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1720 // The first spacing greater than pixelsPerYLabel is what we use.
1721 // TODO(danvk): version that works on a log scale.
1722 if (attr("labelsKMG2")) {
1723 var mults
= [1, 2, 4, 8];
1725 var mults
= [1, 2, 5];
1727 var scale
, low_val
, high_val
, nTicks
;
1728 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1729 var pixelsPerTick
= attr('pixelsPerYLabel');
1730 for (var i
= -10; i
< 50; i
++) {
1731 if (attr("labelsKMG2")) {
1732 var base_scale
= Math
.pow(16, i
);
1734 var base_scale
= Math
.pow(10, i
);
1736 for (var j
= 0; j
< mults
.length
; j
++) {
1737 scale
= base_scale
* mults
[j
];
1738 low_val
= Math
.floor(minV
/ scale
) * scale
;
1739 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1740 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1741 var spacing
= self
.height_
/ nTicks
;
1742 // wish I could break out of both loops at once...
1743 if (spacing
> pixelsPerTick
) break;
1745 if (spacing
> pixelsPerTick
) break;
1748 // Construct the set of ticks.
1749 // Allow reverse y-axis if it's explicitly requested.
1750 if (low_val
> high_val
) scale
*= -1;
1751 for (var i
= 0; i
< nTicks
; i
++) {
1752 var tickV
= low_val
+ i
* scale
;
1753 ticks
.push( {v
: tickV
} );
1757 // Add formatted labels to the ticks.
1760 if (attr("labelsKMB")) {
1762 k_labels
= [ "K", "M", "B", "T" ];
1764 if (attr("labelsKMG2")) {
1765 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1767 k_labels
= [ "k", "M", "G", "T" ];
1769 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
1771 for (var i
= 0; i
< ticks
.length
; i
++) {
1772 var tickV
= ticks
[i
].v
;
1773 var absTickV
= Math
.abs(tickV
);
1775 if (formatter
!= undefined
) {
1776 label
= formatter(tickV
);
1778 label
= Dygraph
.round_(tickV
, 2);
1780 if (k_labels
.length
) {
1781 // Round up to an appropriate unit.
1783 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1784 if (absTickV
>= n
) {
1785 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1790 ticks
[i
].label
= label
;
1795 // Computes the range of the data series (including confidence intervals).
1796 // series is either [ [x1, y1], [x2, y2], ... ] or
1797 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1798 // Returns [low, high]
1799 Dygraph
.prototype.extremeValues_
= function(series
) {
1800 var minY
= null, maxY
= null;
1802 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1804 // With custom bars, maxY is the max of the high values.
1805 for (var j
= 0; j
< series
.length
; j
++) {
1806 var y
= series
[j
][1][0];
1808 var low
= y
- series
[j
][1][1];
1809 var high
= y
+ series
[j
][1][2];
1810 if (low
> y
) low
= y
; // this can happen with custom bars,
1811 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1812 if (maxY
== null || high
> maxY
) {
1815 if (minY
== null || low
< minY
) {
1820 for (var j
= 0; j
< series
.length
; j
++) {
1821 var y
= series
[j
][1];
1822 if (y
=== null || isNaN(y
)) continue;
1823 if (maxY
== null || y
> maxY
) {
1826 if (minY
== null || y
< minY
) {
1832 return [minY
, maxY
];
1836 * This function is called once when the chart's data is changed or the options
1837 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1838 * idea is that values derived from the chart's data can be computed here,
1839 * rather than every time the chart is drawn. This includes things like the
1840 * number of axes, rolling averages, etc.
1842 Dygraph
.prototype.predraw_
= function() {
1843 // TODO(danvk): move more computations out of drawGraph_ and into here.
1844 this.computeYAxes_();
1846 // Create a new plotter.
1847 if (this.plotter_
) this.plotter_
.clear();
1848 this.plotter_
= new DygraphCanvasRenderer(this,
1849 this.hidden_
, this.layout_
,
1850 this.renderOptions_
);
1852 // The roller sits in the bottom left corner of the chart. We don't know where
1853 // this will be until the options are available, so it's positioned here.
1854 this.createRollInterface_();
1856 // Same thing applies for the labelsDiv. It's right edge should be flush with
1857 // the right edge of the charting area (which may not be the same as the right
1858 // edge of the div, if we have two y-axes.
1859 this.positionLabelsDiv_();
1861 // If the data or options have changed, then we'd better redraw.
1867 * Update the graph with new data. This method is called when the viewing area
1868 * has changed. If the underlying data or options have changed, predraw_ will
1869 * be called before drawGraph_ is called.
1872 Dygraph
.prototype.drawGraph_
= function() {
1873 var data
= this.rawData_
;
1875 // This is used to set the second parameter to drawCallback, below.
1876 var is_initial_draw
= this.is_initial_draw_
;
1877 this.is_initial_draw_
= false;
1879 var minY
= null, maxY
= null;
1880 this.layout_
.removeAllDatasets();
1882 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1884 // Loop over the fields (series). Go from the last to the first,
1885 // because if they're stacked that's how we accumulate the values.
1887 var cumulative_y
= []; // For stacked series.
1890 var extremes
= {}; // series name -> [low, high]
1892 // Loop over all fields and create datasets
1893 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1894 if (!this.visibility()[i
- 1]) continue;
1896 var seriesName
= this.attr_("labels")[i
];
1897 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1900 for (var j
= 0; j
< data
.length
; j
++) {
1901 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1902 var date
= data
[j
][0];
1903 series
.push([date
, data
[j
][i
]]);
1907 // TODO(danvk): move this into predraw_. It's insane to do it here.
1908 series
= this.rollingAverage(series
, this.rollPeriod_
);
1910 // Prune down to the desired range, if necessary (for zooming)
1911 // Because there can be lines going to points outside of the visible area,
1912 // we actually prune to visible points, plus one on either side.
1913 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1914 if (this.dateWindow_
) {
1915 var low
= this.dateWindow_
[0];
1916 var high
= this.dateWindow_
[1];
1918 // TODO(danvk): do binary search instead of linear search.
1919 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1920 var firstIdx
= null, lastIdx
= null;
1921 for (var k
= 0; k
< series
.length
; k
++) {
1922 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1925 if (series
[k
][0] <= high
) {
1929 if (firstIdx
=== null) firstIdx
= 0;
1930 if (firstIdx
> 0) firstIdx
--;
1931 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1932 if (lastIdx
< series
.length
- 1) lastIdx
++;
1933 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1934 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1935 pruned
.push(series
[k
]);
1939 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1942 var seriesExtremes
= this.extremeValues_(series
);
1945 for (var j
=0; j
<series
.length
; j
++) {
1946 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1949 } else if (this.attr_("stackedGraph")) {
1950 var l
= series
.length
;
1952 for (var j
= 0; j
< l
; j
++) {
1953 // If one data set has a NaN, let all subsequent stacked
1954 // sets inherit the NaN -- only start at 0 for the first set.
1955 var x
= series
[j
][0];
1956 if (cumulative_y
[x
] === undefined
) {
1957 cumulative_y
[x
] = 0;
1960 actual_y
= series
[j
][1];
1961 cumulative_y
[x
] += actual_y
;
1963 series
[j
] = [x
, cumulative_y
[x
]]
1965 if (cumulative_y
[x
] > seriesExtremes
[1]) {
1966 seriesExtremes
[1] = cumulative_y
[x
];
1968 if (cumulative_y
[x
] < seriesExtremes
[0]) {
1969 seriesExtremes
[0] = cumulative_y
[x
];
1973 extremes
[seriesName
] = seriesExtremes
;
1975 datasets
[i
] = series
;
1978 for (var i
= 1; i
< datasets
.length
; i
++) {
1979 if (!this.visibility()[i
- 1]) continue;
1980 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1983 // TODO(danvk): this method doesn't need to return anything.
1984 var out
= this.computeYAxisRanges_(extremes
);
1986 var seriesToAxisMap
= out
[1];
1987 this.layout_
.updateOptions( { yAxes
: axes
,
1988 seriesToAxisMap
: seriesToAxisMap
1993 // Tell PlotKit to use this new data and render itself
1994 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1995 this.layout_
.evaluateWithError();
1996 this.plotter_
.clear();
1997 this.plotter_
.render();
1998 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1999 this.canvas_
.height
);
2001 if (this.attr_("drawCallback") !== null) {
2002 this.attr_("drawCallback")(this, is_initial_draw
);
2007 * Determine properties of the y-axes which are independent of the data
2008 * currently being displayed. This includes things like the number of axes and
2009 * the style of the axes. It does not include the range of each axis and its
2011 * This fills in this.axes_ and this.seriesToAxisMap_.
2012 * axes_ = [ { options } ]
2013 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2014 * indices are into the axes_ array.
2016 Dygraph
.prototype.computeYAxes_
= function() {
2018 if (this.axes_
!= undefined
) {
2019 // Preserve valueWindow settings.
2021 for (var index
= 0; index
< this.axes_
.length
; index
++) {
2022 valueWindow
.push(this.axes_
[index
].valueWindow
);
2026 this.axes_
= [{}]; // always have at least one y-axis.
2027 this.seriesToAxisMap_
= {};
2029 // Get a list of series names.
2030 var labels
= this.attr_("labels");
2032 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2034 // all options which could be applied per-axis:
2042 'axisLabelFontSize',
2046 // Copy global axis options over to the first axis.
2047 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2048 var k
= axisOptions
[i
];
2049 var v
= this.attr_(k
);
2050 if (v
) this.axes_
[0][k
] = v
;
2053 // Go through once and add all the axes.
2054 for (var seriesName
in series
) {
2055 if (!series
.hasOwnProperty(seriesName
)) continue;
2056 var axis
= this.attr_("axis", seriesName
);
2058 this.seriesToAxisMap_
[seriesName
] = 0;
2061 if (typeof(axis
) == 'object') {
2062 // Add a new axis, making a copy of its per-axis options.
2064 Dygraph
.update(opts
, this.axes_
[0]);
2065 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2066 Dygraph
.update(opts
, axis
);
2067 this.axes_
.push(opts
);
2068 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2072 // Go through one more time and assign series to an axis defined by another
2073 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2074 for (var seriesName
in series
) {
2075 if (!series
.hasOwnProperty(seriesName
)) continue;
2076 var axis
= this.attr_("axis", seriesName
);
2077 if (typeof(axis
) == 'string') {
2078 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2079 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2080 "series " + axis
+ ", which does not define its own axis.");
2083 var idx
= this.seriesToAxisMap_
[axis
];
2084 this.seriesToAxisMap_
[seriesName
] = idx
;
2088 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2089 // this last so that hiding the first series doesn't destroy the axis
2090 // properties of the primary axis.
2091 var seriesToAxisFiltered
= {};
2092 var vis
= this.visibility();
2093 for (var i
= 1; i
< labels
.length
; i
++) {
2095 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2097 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2099 if (valueWindow
!= undefined
) {
2100 // Restore valueWindow settings.
2101 for (var index
= 0; index
< valueWindow
.length
; index
++) {
2102 this.axes_
[index
].valueWindow
= valueWindow
[index
];
2108 * Returns the number of y-axes on the chart.
2109 * @return {Number} the number of axes.
2111 Dygraph
.prototype.numAxes
= function() {
2113 for (var series
in this.seriesToAxisMap_
) {
2114 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2115 var idx
= this.seriesToAxisMap_
[series
];
2116 if (idx
> last_axis
) last_axis
= idx
;
2118 return 1 + last_axis
;
2122 * Determine the value range and tick marks for each axis.
2123 * @param {Object} extremes A mapping from seriesName -> [low, high]
2124 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2126 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2127 // Build a map from axis number -> [list of series names]
2128 var seriesForAxis
= [];
2129 for (var series
in this.seriesToAxisMap_
) {
2130 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2131 var idx
= this.seriesToAxisMap_
[series
];
2132 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2133 seriesForAxis
[idx
].push(series
);
2136 // Compute extreme values, a span and tick marks for each axis.
2137 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2138 var axis
= this.axes_
[i
];
2139 if (axis
.valueWindow
) {
2140 // This is only set if the user has zoomed on the y-axis. It is never set
2141 // by a user. It takes precedence over axis.valueRange because, if you set
2142 // valueRange, you'd still expect to be able to pan.
2143 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2144 } else if (axis
.valueRange
) {
2145 // This is a user-set value range for this axis.
2146 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2148 // Calculate the extremes of extremes.
2149 var series
= seriesForAxis
[i
];
2150 var minY
= Infinity
; // extremes[series[0]][0];
2151 var maxY
= -Infinity
; // extremes[series[0]][1];
2152 for (var j
= 0; j
< series
.length
; j
++) {
2153 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2154 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2156 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2158 // Add some padding and round up to an integer to be human-friendly.
2159 var span
= maxY
- minY
;
2160 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2161 if (span
== 0) { span
= maxY
; }
2162 var maxAxisY
= maxY
+ 0.1 * span
;
2163 var minAxisY
= minY
- 0.1 * span
;
2165 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2166 if (!this.attr_("avoidMinZero")) {
2167 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2168 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2171 if (this.attr_("includeZero")) {
2172 if (maxY
< 0) maxAxisY
= 0;
2173 if (minY
> 0) minAxisY
= 0;
2176 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2179 // Add ticks. By default, all axes inherit the tick positions of the
2180 // primary axis. However, if an axis is specifically marked as having
2181 // independent ticks, then that is permissible as well.
2182 if (i
== 0 || axis
.independentTicks
) {
2184 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2185 axis
.computedValueRange
[1],
2189 var p_axis
= this.axes_
[0];
2190 var p_ticks
= p_axis
.ticks
;
2191 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2192 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2193 var tick_values
= [];
2194 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2195 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2196 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2197 tick_values
.push(y_val
);
2201 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2202 axis
.computedValueRange
[1],
2203 this, axis
, tick_values
);
2207 return [this.axes_
, this.seriesToAxisMap_
];
2211 * Calculates the rolling average of a data set.
2212 * If originalData is [label, val], rolls the average of those.
2213 * If originalData is [label, [, it's interpreted as [value, stddev]
2214 * and the roll is returned in the same form, with appropriately reduced
2215 * stddev for each value.
2216 * Note that this is where fractional input (i.e. '5/10') is converted into
2218 * @param {Array} originalData The data in the appropriate format (see above)
2219 * @param {Number} rollPeriod The number of days over which to average the data
2221 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2222 if (originalData
.length
< 2)
2223 return originalData
;
2224 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2225 var rollingData
= [];
2226 var sigma
= this.attr_("sigma");
2228 if (this.fractions_
) {
2230 var den
= 0; // numerator/denominator
2232 for (var i
= 0; i
< originalData
.length
; i
++) {
2233 num
+= originalData
[i
][1][0];
2234 den
+= originalData
[i
][1][1];
2235 if (i
- rollPeriod
>= 0) {
2236 num
-= originalData
[i
- rollPeriod
][1][0];
2237 den
-= originalData
[i
- rollPeriod
][1][1];
2240 var date
= originalData
[i
][0];
2241 var value
= den
? num
/ den
: 0.0;
2242 if (this.attr_("errorBars")) {
2243 if (this.wilsonInterval_
) {
2244 // For more details on this confidence interval, see:
2245 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2247 var p
= value
< 0 ? 0 : value
, n
= den
;
2248 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2249 var denom
= 1 + sigma
* sigma
/ den
;
2250 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2251 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2252 rollingData
[i
] = [date
,
2253 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2255 rollingData
[i
] = [date
, [0, 0, 0]];
2258 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2259 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2262 rollingData
[i
] = [date
, mult
* value
];
2265 } else if (this.attr_("customBars")) {
2270 for (var i
= 0; i
< originalData
.length
; i
++) {
2271 var data
= originalData
[i
][1];
2273 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2275 if (y
!= null && !isNaN(y
)) {
2281 if (i
- rollPeriod
>= 0) {
2282 var prev
= originalData
[i
- rollPeriod
];
2283 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2290 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2291 1.0 * (mid
- low
) / count
,
2292 1.0 * (high
- mid
) / count
]];
2295 // Calculate the rolling average for the first rollPeriod - 1 points where
2296 // there is not enough data to roll over the full number of days
2297 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2298 if (!this.attr_("errorBars")){
2299 if (rollPeriod
== 1) {
2300 return originalData
;
2303 for (var i
= 0; i
< originalData
.length
; i
++) {
2306 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2307 var y
= originalData
[j
][1];
2308 if (y
== null || isNaN(y
)) continue;
2310 sum
+= originalData
[j
][1];
2313 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2315 rollingData
[i
] = [originalData
[i
][0], null];
2320 for (var i
= 0; i
< originalData
.length
; i
++) {
2324 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2325 var y
= originalData
[j
][1][0];
2326 if (y
== null || isNaN(y
)) continue;
2328 sum
+= originalData
[j
][1][0];
2329 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2332 var stddev
= Math
.sqrt(variance
) / num_ok
;
2333 rollingData
[i
] = [originalData
[i
][0],
2334 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2336 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2346 * Parses a date, returning the number of milliseconds since epoch. This can be
2347 * passed in as an xValueParser in the Dygraph constructor.
2348 * TODO(danvk): enumerate formats that this understands.
2349 * @param {String} A date in YYYYMMDD format.
2350 * @return {Number} Milliseconds since epoch.
2353 Dygraph
.dateParser
= function(dateStr
, self
) {
2356 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2357 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2358 while (dateStrSlashed
.search("-") != -1) {
2359 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2361 d
= Date
.parse(dateStrSlashed
);
2362 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2363 // TODO(danvk): remove support for this format. It's confusing.
2364 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2365 + "/" + dateStr
.substr(6,2);
2366 d
= Date
.parse(dateStrSlashed
);
2368 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2369 // "2009/07/12 12:34:56"
2370 d
= Date
.parse(dateStr
);
2373 if (!d
|| isNaN(d
)) {
2374 self
.error("Couldn't parse " + dateStr
+ " as a date");
2380 * Detects the type of the str (date or numeric) and sets the various
2381 * formatting attributes in this.attrs_ based on this type.
2382 * @param {String} str An x value.
2385 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2387 if (str
.indexOf('-') >= 0 ||
2388 str
.indexOf('/') >= 0 ||
2389 isNaN(parseFloat(str
))) {
2391 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2392 // TODO(danvk): remove support for this format.
2397 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2398 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2399 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2400 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2402 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2403 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2404 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2405 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2410 * Parses a string in a special csv format. We expect a csv file where each
2411 * line is a date point, and the first field in each line is the date string.
2412 * We also expect that all remaining fields represent series.
2413 * if the errorBars attribute is set, then interpret the fields as:
2414 * date, series1, stddev1, series2, stddev2, ...
2415 * @param {Array.<Object>} data See above.
2418 * @return Array.<Object> An array with one entry for each row. These entries
2419 * are an array of cells in that row. The first entry is the parsed x-value for
2420 * the row. The second, third, etc. are the y-values. These can take on one of
2421 * three forms, depending on the CSV and constructor parameters:
2423 * 2. [ value, stddev ]
2424 * 3. [ low value, center value, high value ]
2426 Dygraph
.prototype.parseCSV_
= function(data
) {
2428 var lines
= data
.split("\n");
2430 // Use the default delimiter or fall back to a tab if that makes sense.
2431 var delim
= this.attr_('delimiter');
2432 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2437 if (this.labelsFromCSV_
) {
2439 this.attrs_
.labels
= lines
[0].split(delim
);
2442 // Parse the x as a float or return null if it's not a number.
2443 var parseFloatOrNull
= function(x
) {
2444 var val
= parseFloat(x
);
2445 return isNaN(val
) ? null : val
;
2449 var defaultParserSet
= false; // attempt to auto-detect x value type
2450 var expectedCols
= this.attr_("labels").length
;
2451 var outOfOrder
= false;
2452 for (var i
= start
; i
< lines
.length
; i
++) {
2453 var line
= lines
[i
];
2454 if (line
.length
== 0) continue; // skip blank lines
2455 if (line
[0] == '#') continue; // skip comment lines
2456 var inFields
= line
.split(delim
);
2457 if (inFields
.length
< 2) continue;
2460 if (!defaultParserSet
) {
2461 this.detectTypeFromString_(inFields
[0]);
2462 xParser
= this.attr_("xValueParser");
2463 defaultParserSet
= true;
2465 fields
[0] = xParser(inFields
[0], this);
2467 // If fractions are expected, parse the numbers as "A/B
"
2468 if (this.fractions_) {
2469 for (var j = 1; j < inFields.length; j++) {
2470 // TODO(danvk): figure out an appropriate way to flag parse errors.
2471 var vals = inFields[j].split("/");
2472 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2474 } else if (this.attr_("errorBars
")) {
2475 // If there are error bars, values are (value, stddev) pairs
2476 for (var j = 1; j < inFields.length; j += 2)
2477 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2478 parseFloatOrNull(inFields[j + 1])];
2479 } else if (this.attr_("customBars
")) {
2480 // Bars are a low;center;high tuple
2481 for (var j = 1; j < inFields.length; j++) {
2482 var vals = inFields[j].split(";");
2483 fields[j] = [ parseFloatOrNull(vals[0]),
2484 parseFloatOrNull(vals[1]),
2485 parseFloatOrNull(vals[2]) ];
2488 // Values are just numbers
2489 for (var j = 1; j < inFields.length; j++) {
2490 fields[j] = parseFloatOrNull(inFields[j]);
2493 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2498 if (fields.length != expectedCols) {
2499 this.error("Number of columns
in line
" + i + " (" + fields.length +
2500 ") does not agree
with number of
labels (" + expectedCols +
2506 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2507 ret.sort(function(a,b) { return a[0] - b[0] });
2514 * The user has provided their data as a pre-packaged JS array. If the x values
2515 * are numeric, this is the same as dygraphs' internal format. If the x values
2516 * are dates, we need to convert them from Date objects to ms since epoch.
2517 * @param {Array.<Object>} data
2518 * @return {Array.<Object>} data with numeric x values.
2520 Dygraph.prototype.parseArray_ = function(data) {
2521 // Peek at the first x value to see if it's numeric.
2522 if (data.length == 0) {
2523 this.error("Can
't plot empty data set");
2526 if (data[0].length == 0) {
2527 this.error("Data set cannot contain an empty row");
2531 if (this.attr_("labels") == null) {
2532 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2533 "in the options parameter");
2534 this.attrs_.labels = [ "X" ];
2535 for (var i = 1; i < data[0].length; i++) {
2536 this.attrs_.labels.push("Y" + i);
2540 if (Dygraph.isDateLike(data[0][0])) {
2541 // Some intelligent defaults for a date x-axis.
2542 this.attrs_.xValueFormatter = Dygraph.dateString_;
2543 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2544 this.attrs_.xTicker = Dygraph.dateTicker;
2546 // Assume they're all dates
.
2547 var parsedData
= Dygraph
.clone(data
);
2548 for (var i
= 0; i
< data
.length
; i
++) {
2549 if (parsedData
[i
].length
== 0) {
2550 this.error("Row " + (1 + i
) + " of data is empty");
2553 if (parsedData
[i
][0] == null
2554 || typeof(parsedData
[i
][0].getTime
) != 'function'
2555 || isNaN(parsedData
[i
][0].getTime())) {
2556 this.error("x value in row " + (1 + i
) + " is not a Date");
2559 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2563 // Some intelligent defaults for a numeric x-axis.
2564 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2565 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2571 * Parses a DataTable object from gviz.
2572 * The data is expected to have a first column that is either a date or a
2573 * number. All subsequent columns must be numbers. If there is a clear mismatch
2574 * between this.xValueParser_ and the type of the first column, it will be
2575 * fixed. Fills out rawData_.
2576 * @param {Array.<Object>} data See above.
2579 Dygraph
.prototype.parseDataTable_
= function(data
) {
2580 var cols
= data
.getNumberOfColumns();
2581 var rows
= data
.getNumberOfRows();
2583 var indepType
= data
.getColumnType(0);
2584 if (indepType
== 'date' || indepType
== 'datetime') {
2585 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2586 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2587 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2588 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2589 } else if (indepType
== 'number') {
2590 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2591 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2592 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2593 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2595 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2596 "column 1 of DataTable input (Got '" + indepType
+ "')");
2600 // Array of the column indices which contain data (and not annotations).
2602 var annotationCols
= {}; // data index -> [annotation cols]
2603 var hasAnnotations
= false;
2604 for (var i
= 1; i
< cols
; i
++) {
2605 var type
= data
.getColumnType(i
);
2606 if (type
== 'number') {
2608 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2609 // This is OK -- it's an annotation column.
2610 var dataIdx
= colIdx
[colIdx
.length
- 1];
2611 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2612 annotationCols
[dataIdx
] = [i
];
2614 annotationCols
[dataIdx
].push(i
);
2616 hasAnnotations
= true;
2618 this.error("Only 'number' is supported as a dependent type with Gviz." +
2619 " 'string' is only supported if displayAnnotations is true");
2623 // Read column labels
2624 // TODO(danvk): add support back for errorBars
2625 var labels
= [data
.getColumnLabel(0)];
2626 for (var i
= 0; i
< colIdx
.length
; i
++) {
2627 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2628 if (this.attr_("errorBars")) i
+= 1;
2630 this.attrs_
.labels
= labels
;
2631 cols
= labels
.length
;
2634 var outOfOrder
= false;
2635 var annotations
= [];
2636 for (var i
= 0; i
< rows
; i
++) {
2638 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2639 data
.getValue(i
, 0) === null) {
2640 this.warn("Ignoring row " + i
+
2641 " of DataTable because of undefined or null first column.");
2645 if (indepType
== 'date' || indepType
== 'datetime') {
2646 row
.push(data
.getValue(i
, 0).getTime());
2648 row
.push(data
.getValue(i
, 0));
2650 if (!this.attr_("errorBars")) {
2651 for (var j
= 0; j
< colIdx
.length
; j
++) {
2652 var col
= colIdx
[j
];
2653 row
.push(data
.getValue(i
, col
));
2654 if (hasAnnotations
&&
2655 annotationCols
.hasOwnProperty(col
) &&
2656 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2658 ann
.series
= data
.getColumnLabel(col
);
2660 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2662 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2663 if (k
) ann
.text
+= "\n";
2664 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2666 annotations
.push(ann
);
2670 for (var j
= 0; j
< cols
- 1; j
++) {
2671 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2674 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2681 this.warn("DataTable is out of order; order it correctly to speed loading.");
2682 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2684 this.rawData_
= ret
;
2686 if (annotations
.length
> 0) {
2687 this.setAnnotations(annotations
, true);
2691 // These functions are all based on MochiKit.
2692 Dygraph
.update
= function (self
, o
) {
2693 if (typeof(o
) != 'undefined' && o
!== null) {
2695 if (o
.hasOwnProperty(k
)) {
2703 Dygraph
.isArrayLike
= function (o
) {
2704 var typ
= typeof(o
);
2706 (typ
!= 'object' && !(typ
== 'function' &&
2707 typeof(o
.item
) == 'function')) ||
2709 typeof(o
.length
) != 'number' ||
2717 Dygraph
.isDateLike
= function (o
) {
2718 if (typeof(o
) != "object" || o
=== null ||
2719 typeof(o
.getTime
) != 'function') {
2725 Dygraph
.clone
= function(o
) {
2726 // TODO(danvk): figure out how MochiKit's version works
2728 for (var i
= 0; i
< o
.length
; i
++) {
2729 if (Dygraph
.isArrayLike(o
[i
])) {
2730 r
.push(Dygraph
.clone(o
[i
]));
2740 * Get the CSV data. If it's in a function, call that function. If it's in a
2741 * file, do an XMLHttpRequest to get it.
2744 Dygraph
.prototype.start_
= function() {
2745 if (typeof this.file_
== 'function') {
2746 // CSV string. Pretend we got it via XHR.
2747 this.loadedEvent_(this.file_());
2748 } else if (Dygraph
.isArrayLike(this.file_
)) {
2749 this.rawData_
= this.parseArray_(this.file_
);
2751 } else if (typeof this.file_
== 'object' &&
2752 typeof this.file_
.getColumnRange
== 'function') {
2753 // must be a DataTable from gviz.
2754 this.parseDataTable_(this.file_
);
2756 } else if (typeof this.file_
== 'string') {
2757 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2758 if (this.file_
.indexOf('\n') >= 0) {
2759 this.loadedEvent_(this.file_
);
2761 var req
= new XMLHttpRequest();
2763 req
.onreadystatechange
= function () {
2764 if (req
.readyState
== 4) {
2765 if (req
.status
== 200) {
2766 caller
.loadedEvent_(req
.responseText
);
2771 req
.open("GET", this.file_
, true);
2775 this.error("Unknown data format: " + (typeof this.file_
));
2780 * Changes various properties of the graph. These can include:
2782 * <li>file: changes the source data for the graph</li>
2783 * <li>errorBars: changes whether the data contains stddev</li>
2785 * @param {Object} attrs The new properties and values
2787 Dygraph
.prototype.updateOptions
= function(attrs
) {
2788 // TODO(danvk): this is a mess. Rethink this function.
2789 if ('rollPeriod' in attrs
) {
2790 this.rollPeriod_
= attrs
.rollPeriod
;
2792 if ('dateWindow' in attrs
) {
2793 this.dateWindow_
= attrs
.dateWindow
;
2796 // TODO(danvk): validate per-series options.
2801 // highlightCircleSize
2803 Dygraph
.update(this.user_attrs_
, attrs
);
2804 Dygraph
.update(this.renderOptions_
, attrs
);
2806 this.labelsFromCSV_
= (this.attr_("labels") == null);
2808 // TODO(danvk): this doesn't match the constructor logic
2809 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2810 if (attrs
['file']) {
2811 this.file_
= attrs
['file'];
2819 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2820 * containing div (which has presumably changed size since the dygraph was
2821 * instantiated. If the width/height are specified, the div will be resized.
2823 * This is far more efficient than destroying and re-instantiating a
2824 * Dygraph, since it doesn't have to reparse the underlying data.
2826 * @param {Number} width Width (in pixels)
2827 * @param {Number} height Height (in pixels)
2829 Dygraph
.prototype.resize
= function(width
, height
) {
2830 if (this.resize_lock
) {
2833 this.resize_lock
= true;
2835 if ((width
=== null) != (height
=== null)) {
2836 this.warn("Dygraph.resize() should be called with zero parameters or " +
2837 "two non-NULL parameters. Pretending it was zero.");
2838 width
= height
= null;
2841 // TODO(danvk): there should be a clear() method.
2842 this.maindiv_
.innerHTML
= "";
2843 this.attrs_
.labelsDiv
= null;
2846 this.maindiv_
.style
.width
= width
+ "px";
2847 this.maindiv_
.style
.height
= height
+ "px";
2848 this.width_
= width
;
2849 this.height_
= height
;
2851 this.width_
= this.maindiv_
.offsetWidth
;
2852 this.height_
= this.maindiv_
.offsetHeight
;
2855 this.createInterface_();
2858 this.resize_lock
= false;
2862 * Adjusts the number of days in the rolling average. Updates the graph to
2863 * reflect the new averaging period.
2864 * @param {Number} length Number of days over which to average the data.
2866 Dygraph
.prototype.adjustRoll
= function(length
) {
2867 this.rollPeriod_
= length
;
2872 * Returns a boolean array of visibility statuses.
2874 Dygraph
.prototype.visibility
= function() {
2875 // Do lazy-initialization, so that this happens after we know the number of
2877 if (!this.attr_("visibility")) {
2878 this.attrs_
["visibility"] = [];
2880 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2881 this.attr_("visibility").push(true);
2883 return this.attr_("visibility");
2887 * Changes the visiblity of a series.
2889 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2890 var x
= this.visibility();
2891 if (num
< 0 || num
>= x
.length
) {
2892 this.warn("invalid series number in setVisibility: " + num
);
2900 * Update the list of annotations and redraw the chart.
2902 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2903 // Only add the annotation CSS rule once we know it will be used.
2904 Dygraph
.addAnnotationRule();
2905 this.annotations_
= ann
;
2906 this.layout_
.setAnnotations(this.annotations_
);
2907 if (!suppressDraw
) {
2913 * Return the list of annotations.
2915 Dygraph
.prototype.annotations
= function() {
2916 return this.annotations_
;
2920 * Get the index of a series (column) given its name. The first column is the
2921 * x-axis, so the data series start with index 1.
2923 Dygraph
.prototype.indexFromSetName
= function(name
) {
2924 var labels
= this.attr_("labels");
2925 for (var i
= 0; i
< labels
.length
; i
++) {
2926 if (labels
[i
] == name
) return i
;
2931 Dygraph
.addAnnotationRule
= function() {
2932 if (Dygraph
.addedAnnotationCSS
) return;
2934 var rule
= "border: 1px solid black; " +
2935 "background-color: white; " +
2936 "text-align: center;";
2938 var styleSheetElement
= document
.createElement("style");
2939 styleSheetElement
.type
= "text/css";
2940 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2942 // Find the first style sheet that we can access.
2943 // We may not add a rule to a style sheet from another domain for security
2944 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
2945 // adds its own style sheets from google.com.
2946 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
2947 if (document
.styleSheets
[i
].disabled
) continue;
2948 var mysheet
= document
.styleSheets
[i
];
2950 if (mysheet
.insertRule
) { // Firefox
2951 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2952 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2953 } else if (mysheet
.addRule
) { // IE
2954 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2956 Dygraph
.addedAnnotationCSS
= true;
2959 // Was likely a security exception.
2963 this.warn("Unable to add default annotation CSS rule; display may be off.");
2967 * Create a new canvas element. This is more complex than a simple
2968 * document.createElement("canvas") because of IE and excanvas.
2970 Dygraph
.createCanvas
= function() {
2971 var canvas
= document
.createElement("canvas");
2973 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2974 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
2975 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2983 * A wrapper around Dygraph that implements the gviz API.
2984 * @param {Object} container The DOM object the visualization should live in.
2986 Dygraph
.GVizChart
= function(container
) {
2987 this.container
= container
;
2990 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2991 // Clear out any existing dygraph.
2992 // TODO(danvk): would it make more sense to simply redraw using the current
2993 // date_graph object?
2994 this.container
.innerHTML
= '';
2995 if (typeof(this.date_graph
) != 'undefined') {
2996 this.date_graph
.destroy();
2999 this.date_graph
= new Dygraph(this.container
, data
, options
);
3003 * Google charts compatible setSelection
3004 * Only row selection is supported, all points in the row will be highlighted
3005 * @param {Array} array of the selected cells
3008 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
3010 if (selection_array
.length
) {
3011 row
= selection_array
[0].row
;
3013 this.date_graph
.setSelection(row
);
3017 * Google charts compatible getSelection implementation
3018 * @return {Array} array of the selected cells
3021 Dygraph
.GVizChart
.prototype.getSelection
= function() {
3024 var row
= this.date_graph
.getSelection();
3026 if (row
< 0) return selection
;
3029 for (var i
in this.date_graph
.layout_
.datasets
) {
3030 selection
.push({row
: row
, column
: col
});
3037 // Older pages may still use this name.
3038 DateGraph
= Dygraph
;