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 // Clear the div. This ensure that, if multiple dygraphs are passed the same
196 // div, then only one will be drawn.
199 // If the div isn't already sized then inherit from our attrs or
200 // give it a default size.
201 if (div
.style
.width
== '') {
202 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
204 if (div
.style
.height
== '') {
205 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
207 this.width_
= parseInt(div
.style
.width
, 10);
208 this.height_
= parseInt(div
.style
.height
, 10);
209 // The div might have been specified as percent of the current window size,
210 // convert that to an appropriate number of pixels.
211 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
212 this.width_
= div
.offsetWidth
;
214 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
215 this.height_
= div
.offsetHeight
;
218 if (this.width_
== 0) {
219 this.error("dygraph has zero width. Please specify a width in pixels.");
221 if (this.height_
== 0) {
222 this.error("dygraph has zero height. Please specify a height in pixels.");
225 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
226 if (attrs
['stackedGraph']) {
227 attrs
['fillGraph'] = true;
228 // TODO(nikhilk): Add any other stackedGraph checks here.
231 // Dygraphs has many options, some of which interact with one another.
232 // To keep track of everything, we maintain two sets of options:
234 // this.user_attrs_ only options explicitly set by the user.
235 // this.attrs_ defaults, options derived from user_attrs_, data.
237 // Options are then accessed this.attr_('attr'), which first looks at
238 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
239 // defaults without overriding behavior that the user specifically asks for.
240 this.user_attrs_
= {};
241 Dygraph
.update(this.user_attrs_
, attrs
);
244 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
246 this.boundaryIds_
= [];
248 // Make a note of whether labels will be pulled from the CSV file.
249 this.labelsFromCSV_
= (this.attr_("labels") == null);
251 // Create the containing DIV and other interactive elements
252 this.createInterface_();
257 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
259 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
260 this.user_attrs_
[seriesName
] != null &&
261 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
262 return this.user_attrs_
[seriesName
][name
];
263 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
264 return this.user_attrs_
[name
];
265 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
266 return this.attrs_
[name
];
272 // TODO(danvk): any way I can get the line numbers to be this.warn call?
273 Dygraph
.prototype.log
= function(severity
, message
) {
274 if (typeof(console
) != 'undefined') {
277 console
.debug('dygraphs: ' + message
);
280 console
.info('dygraphs: ' + message
);
282 case Dygraph
.WARNING
:
283 console
.warn('dygraphs: ' + message
);
286 console
.error('dygraphs: ' + message
);
291 Dygraph
.prototype.info
= function(message
) {
292 this.log(Dygraph
.INFO
, message
);
294 Dygraph
.prototype.warn
= function(message
) {
295 this.log(Dygraph
.WARNING
, message
);
297 Dygraph
.prototype.error
= function(message
) {
298 this.log(Dygraph
.ERROR
, message
);
302 * Returns the current rolling period, as set by the user or an option.
303 * @return {Number} The number of days in the rolling window
305 Dygraph
.prototype.rollPeriod
= function() {
306 return this.rollPeriod_
;
310 * Returns the currently-visible x-range. This can be affected by zooming,
311 * panning or a call to updateOptions.
312 * Returns a two-element array: [left, right].
313 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
315 Dygraph
.prototype.xAxisRange
= function() {
316 if (this.dateWindow_
) return this.dateWindow_
;
318 // The entire chart is visible.
319 var left
= this.rawData_
[0][0];
320 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
321 return [left
, right
];
325 * Returns the currently-visible y-range for an axis. This can be affected by
326 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
327 * called with no arguments, returns the range of the first axis.
328 * Returns a two-element array: [bottom, top].
330 Dygraph
.prototype.yAxisRange
= function(idx
) {
331 if (typeof(idx
) == "undefined") idx
= 0;
332 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
333 return [ this.axes_
[idx
].computedValueRange
[0],
334 this.axes_
[idx
].computedValueRange
[1] ];
338 * Returns the currently-visible y-ranges for each axis. This can be affected by
339 * zooming, panning, calls to updateOptions, etc.
340 * Returns an array of [bottom, top] pairs, one for each y-axis.
342 Dygraph
.prototype.yAxisRanges
= function() {
344 for (var i
= 0; i
< this.axes_
.length
; i
++) {
345 ret
.push(this.yAxisRange(i
));
350 // TODO(danvk): use these functions throughout dygraphs.
352 * Convert from data coordinates to canvas/div X/Y coordinates.
353 * If specified, do this conversion for the coordinate system of a particular
354 * axis. Uses the first axis by default.
355 * Returns a two-element array: [X, Y]
357 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
358 var ret
= [null, null];
359 var area
= this.plotter_
.area
;
361 var xRange
= this.xAxisRange();
362 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
366 var yRange
= this.yAxisRange(axis
);
367 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
374 * Convert from canvas/div coords to data coordinates.
375 * If specified, do this conversion for the coordinate system of a particular
376 * axis. Uses the first axis by default.
377 * Returns a two-element array: [X, Y]
379 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
380 var ret
= [null, null];
381 var area
= this.plotter_
.area
;
383 var xRange
= this.xAxisRange();
384 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
388 var yRange
= this.yAxisRange(axis
);
389 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
396 * Returns the number of columns (including the independent variable).
398 Dygraph
.prototype.numColumns
= function() {
399 return this.rawData_
[0].length
;
403 * Returns the number of rows (excluding any header/label row).
405 Dygraph
.prototype.numRows
= function() {
406 return this.rawData_
.length
;
410 * Returns the value in the given row and column. If the row and column exceed
411 * the bounds on the data, returns null. Also returns null if the value is
414 Dygraph
.prototype.getValue
= function(row
, col
) {
415 if (row
< 0 || row
> this.rawData_
.length
) return null;
416 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
418 return this.rawData_
[row
][col
];
421 Dygraph
.addEvent
= function(el
, evt
, fn
) {
422 var normed_fn
= function(e
) {
423 if (!e
) var e
= window
.event
;
426 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
427 el
.addEventListener(evt
, normed_fn
, false);
429 el
.attachEvent('on' + evt
, normed_fn
);
434 * Generates interface elements for the Dygraph: a containing div, a div to
435 * display the current point, and a textbox to adjust the rolling average
436 * period. Also creates the Renderer/Layout elements.
439 Dygraph
.prototype.createInterface_
= function() {
440 // Create the all-enclosing graph div
441 var enclosing
= this.maindiv_
;
443 this.graphDiv
= document
.createElement("div");
444 this.graphDiv
.style
.width
= this.width_
+ "px";
445 this.graphDiv
.style
.height
= this.height_
+ "px";
446 enclosing
.appendChild(this.graphDiv
);
448 // Create the canvas for interactive parts of the chart.
449 this.canvas_
= Dygraph
.createCanvas();
450 this.canvas_
.style
.position
= "absolute";
451 this.canvas_
.width
= this.width_
;
452 this.canvas_
.height
= this.height_
;
453 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
454 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
456 // ... and for static parts of the chart.
457 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
459 // The interactive parts of the graph are drawn on top of the chart.
460 this.graphDiv
.appendChild(this.hidden_
);
461 this.graphDiv
.appendChild(this.canvas_
);
462 this.mouseEventElement_
= this.canvas_
;
465 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
466 dygraph
.mouseMove_(e
);
468 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
469 dygraph
.mouseOut_(e
);
472 // Create the grapher
473 // TODO(danvk): why does the Layout need its own set of options?
474 this.layoutOptions_
= { 'xOriginIsZero': false };
475 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
476 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
477 Dygraph
.update(this.layoutOptions_
, {
478 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
480 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
482 // TODO(danvk): why does the Renderer need its own set of options?
483 this.renderOptions_
= { colorScheme
: this.colors_
,
485 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
486 Dygraph
.update(this.renderOptions_
, this.attrs_
);
487 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
489 this.createStatusMessage_();
490 this.createDragInterface_();
494 * Detach DOM elements in the dygraph and null out all data references.
495 * Calling this when you're done with a dygraph can dramatically reduce memory
496 * usage. See, e.g., the tests/perf.html example.
498 Dygraph
.prototype.destroy
= function() {
499 var removeRecursive
= function(node
) {
500 while (node
.hasChildNodes()) {
501 removeRecursive(node
.firstChild
);
502 node
.removeChild(node
.firstChild
);
505 removeRecursive(this.maindiv_
);
507 var nullOut
= function(obj
) {
509 if (typeof(obj
[n
]) === 'object') {
515 // These may not all be necessary, but it can't hurt...
516 nullOut(this.layout_
);
517 nullOut(this.plotter_
);
522 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
523 * this particular canvas. All Dygraph work is done on this.canvas_.
524 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
525 * @return {Object} The newly-created canvas
528 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
529 var h
= Dygraph
.createCanvas();
530 h
.style
.position
= "absolute";
531 // TODO(danvk): h should be offset from canvas. canvas needs to include
532 // some extra area to make it easier to zoom in on the far left and far
533 // right. h needs to be precisely the plot area, so that clipping occurs.
534 h
.style
.top
= canvas
.style
.top
;
535 h
.style
.left
= canvas
.style
.left
;
536 h
.width
= this.width_
;
537 h
.height
= this.height_
;
538 h
.style
.width
= this.width_
+ "px"; // for IE
539 h
.style
.height
= this.height_
+ "px"; // for IE
543 // Taken from MochiKit.Color
544 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
548 if (saturation
=== 0) {
553 var i
= Math
.floor(hue
* 6);
554 var f
= (hue
* 6) - i
;
555 var p
= value
* (1 - saturation
);
556 var q
= value
* (1 - (saturation
* f
));
557 var t
= value
* (1 - (saturation
* (1 - f
)));
559 case 1: red
= q
; green
= value
; blue
= p
; break;
560 case 2: red
= p
; green
= value
; blue
= t
; break;
561 case 3: red
= p
; green
= q
; blue
= value
; break;
562 case 4: red
= t
; green
= p
; blue
= value
; break;
563 case 5: red
= value
; green
= p
; blue
= q
; break;
564 case 6: // fall through
565 case 0: red
= value
; green
= t
; blue
= p
; break;
568 red
= Math
.floor(255 * red
+ 0.5);
569 green
= Math
.floor(255 * green
+ 0.5);
570 blue
= Math
.floor(255 * blue
+ 0.5);
571 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
576 * Generate a set of distinct colors for the data series. This is done with a
577 * color wheel. Saturation/Value are customizable, and the hue is
578 * equally-spaced around the color wheel. If a custom set of colors is
579 * specified, that is used instead.
582 Dygraph
.prototype.setColors_
= function() {
583 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
584 // away with this.renderOptions_.
585 var num
= this.attr_("labels").length
- 1;
587 var colors
= this.attr_('colors');
589 var sat
= this.attr_('colorSaturation') || 1.0;
590 var val
= this.attr_('colorValue') || 0.5;
591 var half
= Math
.ceil(num
/ 2);
592 for (var i
= 1; i
<= num
; i
++) {
593 if (!this.visibility()[i
-1]) continue;
594 // alternate colors for high contrast.
595 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
596 var hue
= (1.0 * idx
/ (1 + num
));
597 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
600 for (var i
= 0; i
< num
; i
++) {
601 if (!this.visibility()[i
]) continue;
602 var colorStr
= colors
[i
% colors
.length
];
603 this.colors_
.push(colorStr
);
607 // TODO(danvk): update this w/r
/t/ the
new options system
.
608 this.renderOptions_
.colorScheme
= this.colors_
;
609 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
610 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
611 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
615 * Return the list of colors. This is either the list of colors passed in the
616 * attributes, or the autogenerated list of rgb(r,g,b) strings.
617 * @return {Array<string>} The list of colors.
619 Dygraph
.prototype.getColors
= function() {
623 // The following functions are from quirksmode.org with a modification for Safari from
624 // http://blog.firetree.net/2005/07/04/javascript-find-position/
625 // http://www.quirksmode.org/js
/findpos
.html
626 Dygraph
.findPosX
= function(obj
) {
631 curleft
+= obj
.offsetLeft
;
632 if(!obj
.offsetParent
)
634 obj
= obj
.offsetParent
;
641 Dygraph
.findPosY
= function(obj
) {
646 curtop
+= obj
.offsetTop
;
647 if(!obj
.offsetParent
)
649 obj
= obj
.offsetParent
;
659 * Create the div that contains information on the selected point(s)
660 * This goes in the top right of the canvas, unless an external div has already
664 Dygraph
.prototype.createStatusMessage_
= function() {
665 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
666 if (userLabelsDiv
&& null != userLabelsDiv
667 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
668 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
670 if (!this.attr_("labelsDiv")) {
671 var divWidth
= this.attr_('labelsDivWidth');
673 "position": "absolute",
676 "width": divWidth
+ "px",
678 "left": (this.width_
- divWidth
- 2) + "px",
679 "background": "white",
681 "overflow": "hidden"};
682 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
683 var div
= document
.createElement("div");
684 for (var name
in messagestyle
) {
685 if (messagestyle
.hasOwnProperty(name
)) {
686 div
.style
[name
] = messagestyle
[name
];
689 this.graphDiv
.appendChild(div
);
690 this.attrs_
.labelsDiv
= div
;
695 * Position the labels div so that its right edge is flush with the right edge
696 * of the charting area.
698 Dygraph
.prototype.positionLabelsDiv_
= function() {
699 // Don't touch a user-specified labelsDiv.
700 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
702 var area
= this.plotter_
.area
;
703 var div
= this.attr_("labelsDiv");
704 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
708 * Create the text box to adjust the averaging period
711 Dygraph
.prototype.createRollInterface_
= function() {
712 // Create a roller if one doesn't exist already.
714 this.roller_
= document
.createElement("input");
715 this.roller_
.type
= "text";
716 this.roller_
.style
.display
= "none";
717 this.graphDiv
.appendChild(this.roller_
);
720 var display
= this.attr_('showRoller') ? 'block' : 'none';
722 var textAttr
= { "position": "absolute",
724 "top": (this.plotter_
.area
.h
- 25) + "px",
725 "left": (this.plotter_
.area
.x
+ 1) + "px",
728 this.roller_
.size
= "2";
729 this.roller_
.value
= this.rollPeriod_
;
730 for (var name
in textAttr
) {
731 if (textAttr
.hasOwnProperty(name
)) {
732 this.roller_
.style
[name
] = textAttr
[name
];
737 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
740 // These functions are taken from MochiKit.Signal
741 Dygraph
.pageX
= function(e
) {
743 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
746 var b
= document
.body
;
748 (de
.scrollLeft
|| b
.scrollLeft
) -
749 (de
.clientLeft
|| 0);
753 Dygraph
.pageY
= function(e
) {
755 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
758 var b
= document
.body
;
760 (de
.scrollTop
|| b
.scrollTop
) -
766 * Set up all the mouse handlers needed to capture dragging behavior for zoom
770 Dygraph
.prototype.createDragInterface_
= function() {
773 // Tracks whether the mouse is down right now
774 var isZooming
= false;
775 var isPanning
= false; // is this drag part of a pan?
776 var is2DPan
= false; // if so, is that pan 1- or 2-dimensional?
777 var dragStartX
= null;
778 var dragStartY
= null;
781 var dragDirection
= null;
784 var prevDragDirection
= null;
786 // TODO(danvk): update this comment
787 // draggingDate and draggingValue represent the [date,value] point on the
788 // graph at which the mouse was pressed. As the mouse moves while panning,
789 // the viewport must pan so that the mouse position points to
790 // [draggingDate, draggingValue]
791 var draggingDate
= null;
793 // TODO(danvk): update this comment
794 // The range in second/value units that the viewport encompasses during a
795 // panning operation.
796 var dateRange
= null;
798 // Utility function to convert page-wide coordinates to canvas coords
801 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
802 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
804 // Draw zoom rectangles when the mouse is down and the user moves around
805 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
807 dragEndX
= getX(event
);
808 dragEndY
= getY(event
);
810 var xDelta
= Math
.abs(dragStartX
- dragEndX
);
811 var yDelta
= Math
.abs(dragStartY
- dragEndY
);
813 // drag direction threshold for y axis is twice as large as x axis
814 dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
816 self
.drawZoomRect_(dragDirection
, dragStartX
, dragEndX
, dragStartY
, dragEndY
,
817 prevDragDirection
, prevEndX
, prevEndY
);
821 prevDragDirection
= dragDirection
;
822 } else if (isPanning
) {
823 dragEndX
= getX(event
);
824 dragEndY
= getY(event
);
826 // TODO(danvk): update this comment
827 // Want to have it so that:
828 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
829 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
830 // 3. draggingValue appears at dragEndY.
831 // 4. valueRange is unaltered.
833 var minDate
= draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
834 var maxDate
= minDate
+ dateRange
;
835 self
.dateWindow_
= [minDate
, maxDate
];
838 // y-axis scaling is automatic unless this is a full 2D pan.
840 // Adjust each axis appropriately.
841 var y_frac
= dragEndY
/ self
.height_
;
842 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
843 var axis
= self
.axes_
[i
];
844 var maxValue
= axis
.draggingValue
+ y_frac
* axis
.dragValueRange
;
845 var minValue
= maxValue
- axis
.dragValueRange
;
846 axis
.valueWindow
= [ minValue
, maxValue
];
854 // Track the beginning of drag events
855 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
856 // prevents mouse drags from selecting page text.
857 if (event
.preventDefault
) {
858 event
.preventDefault(); // Firefox, Chrome, etc.
860 event
.returnValue
= false; // IE
861 event
.cancelBubble
= true;
864 px
= Dygraph
.findPosX(self
.canvas_
);
865 py
= Dygraph
.findPosY(self
.canvas_
);
866 dragStartX
= getX(event
);
867 dragStartY
= getY(event
);
869 if (event
.altKey
|| event
.shiftKey
) {
870 // have to be zoomed in to pan.
872 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
873 if (self
.axes_
[i
].valueWindow
|| self
.axes_
[i
].valueRange
) {
878 if (!self
.dateWindow_
&& !zoomedY
) return;
881 var xRange
= self
.xAxisRange();
882 dateRange
= xRange
[1] - xRange
[0];
884 // Record the range of each y-axis at the start of the drag.
885 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
887 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
888 var axis
= self
.axes_
[i
];
889 var yRange
= self
.yAxisRange(i
);
890 axis
.dragValueRange
= yRange
[1] - yRange
[0];
891 var r
= self
.toDataCoords(null, dragStartY
, i
);
892 axis
.draggingValue
= r
[1];
893 if (axis
.valueWindow
|| axis
.valueRange
) is2DPan
= true;
896 // TODO(konigsberg): Switch from all this math to toDataCoords?
897 // Seems to work for the dragging value.
898 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+ xRange
[0];
904 // If the user releases the mouse button during a drag, but not over the
905 // canvas, then it doesn't count as a zooming action.
906 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
907 if (isZooming
|| isPanning
) {
917 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
918 delete self
.axes_
[i
].draggingValue
;
919 delete self
.axes_
[i
].dragValueRange
;
924 // Temporarily cancel the dragging event when the mouse leaves the graph
925 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
932 // If the mouse is released on the canvas during a drag event, then it's a
933 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
934 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
937 dragEndX
= getX(event
);
938 dragEndY
= getY(event
);
939 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
940 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
942 if (regionWidth
< 2 && regionHeight
< 2 &&
943 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
944 // TODO(danvk): pass along more info about the points, e.g. 'x'
945 if (self
.attr_('clickCallback') != null) {
946 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
948 if (self
.attr_('pointClickCallback')) {
949 // check if the click was on a particular point.
951 var closestDistance
= 0;
952 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
953 var p
= self
.selPoints_
[i
];
954 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
955 Math
.pow(p
.canvasy
- dragEndY
, 2);
956 if (closestIdx
== -1 || distance
< closestDistance
) {
957 closestDistance
= distance
;
962 // Allow any click within two pixels of the dot.
963 var radius
= self
.attr_('highlightCircleSize') + 2;
964 if (closestDistance
<= 5 * 5) {
965 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
970 if (regionWidth
>= 10 && dragDirection
== Dygraph
.HORIZONTAL
) {
971 self
.doZoomX_(Math
.min(dragStartX
, dragEndX
),
972 Math
.max(dragStartX
, dragEndX
));
973 } else if (regionHeight
>= 10 && dragDirection
== Dygraph
.VERTICAL
){
974 self
.doZoomY_(Math
.min(dragStartY
, dragEndY
),
975 Math
.max(dragStartY
, dragEndY
));
977 self
.canvas_
.getContext("2d").clearRect(0, 0,
979 self
.canvas_
.height
);
995 // Double-clicking zooms back out
996 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
997 // Disable zooming out if panning.
998 if (event
.altKey
|| event
.shiftKey
) return;
1005 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1006 * up any previous zoom rectangles that were drawn. This could be optimized to
1007 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1010 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1011 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1012 * @param {Number} startX The X position where the drag started, in canvas
1014 * @param {Number} endX The current X position of the drag, in canvas coords.
1015 * @param {Number} startY The Y position where the drag started, in canvas
1017 * @param {Number} endY The current Y position of the drag, in canvas coords.
1018 * @param {Number} prevDirection the value of direction on the previous call to
1019 * this function. Used to avoid excess redrawing
1020 * @param {Number} prevEndX The value of endX on the previous call to this
1021 * function. Used to avoid excess redrawing
1022 * @param {Number} prevEndY The value of endY on the previous call to this
1023 * function. Used to avoid excess redrawing
1026 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1027 prevDirection
, prevEndX
, prevEndY
) {
1028 var ctx
= this.canvas_
.getContext("2d");
1030 // Clean up from the previous rect if necessary
1031 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1032 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1033 Math
.abs(startX
- prevEndX
), this.height_
);
1034 } else if (prevDirection
== Dygraph
.VERTICAL
){
1035 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1036 this.width_
, Math
.abs(startY
- prevEndY
));
1039 // Draw a light-grey rectangle to show the new viewing area
1040 if (direction
== Dygraph
.HORIZONTAL
) {
1041 if (endX
&& startX
) {
1042 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1043 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1044 Math
.abs(endX
- startX
), this.height_
);
1047 if (direction
== Dygraph
.VERTICAL
) {
1048 if (endY
&& startY
) {
1049 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1050 ctx
.fillRect(0, Math
.min(startY
, endY
),
1051 this.width_
, Math
.abs(endY
- startY
));
1057 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1058 * the canvas. The exact zoom window may be slightly larger if there are no data
1059 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1060 * which accepts dates that match the raw data. This function redraws the graph.
1062 * @param {Number} lowX The leftmost pixel value that should be visible.
1063 * @param {Number} highX The rightmost pixel value that should be visible.
1066 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1067 // Find the earliest and latest dates contained in this canvasx range.
1068 // Convert the call to date ranges of the raw data.
1069 var r
= this.toDataCoords(lowX
, null);
1071 r
= this.toDataCoords(highX
, null);
1073 this.doZoomXDates_(minDate
, maxDate
);
1077 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1078 * method with doZoomX which accepts pixel coordinates. This function redraws
1081 * @param {Number} minDate The minimum date that should be visible.
1082 * @param {Number} maxDate The maximum date that should be visible.
1085 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1086 this.dateWindow_
= [minDate
, maxDate
];
1088 if (this.attr_("zoomCallback")) {
1089 var yRange
= this.yAxisRange();
1090 this.attr_("zoomCallback")(minDate
, maxDate
, yRange
[0], yRange
[1]);
1095 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1096 * the canvas. This function redraws the graph.
1098 * @param {Number} lowY The topmost pixel value that should be visible.
1099 * @param {Number} highY The lowest pixel value that should be visible.
1102 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1103 // Find the highest and lowest values in pixel range for each axis.
1104 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1105 // This is because pixels increase as you go down on the screen, whereas data
1106 // coordinates increase as you go up the screen.
1107 var valueRanges
= [];
1108 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1109 var hi
= this.toDataCoords(null, lowY
, i
);
1110 var low
= this.toDataCoords(null, highY
, i
);
1111 this.axes_
[i
].valueWindow
= [low
[1], hi
[1]];
1112 valueRanges
.push([low
[1], hi
[1]]);
1116 if (this.attr_("zoomCallback")) {
1117 var xRange
= this.xAxisRange();
1118 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1123 * Reset the zoom to the original view coordinates. This is the same as
1124 * double-clicking on the graph.
1128 Dygraph
.prototype.doUnzoom_
= function() {
1130 if (this.dateWindow_
!= null) {
1132 this.dateWindow_
= null;
1135 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1136 if (this.axes_
[i
].valueWindow
!= null) {
1138 delete this.axes_
[i
].valueWindow
;
1143 // Putting the drawing operation before the callback because it resets
1146 if (this.attr_("zoomCallback")) {
1147 var minDate
= this.rawData_
[0][0];
1148 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1149 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1155 * When the mouse moves in the canvas, display information about a nearby data
1156 * point and draw dots over those points in the data series. This function
1157 * takes care of cleanup of previously-drawn dots.
1158 * @param {Object} event The mousemove event from the browser.
1161 Dygraph
.prototype.mouseMove_
= function(event
) {
1162 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1163 var points
= this.layout_
.points
;
1168 // Loop through all the points and find the date nearest to our current
1170 var minDist
= 1e+100;
1172 for (var i
= 0; i
< points
.length
; i
++) {
1173 var point
= points
[i
];
1174 if (point
== null) continue;
1175 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1176 if (dist
> minDist
) continue;
1180 if (idx
>= 0) lastx
= points
[idx
].xval
;
1181 // Check that you can really highlight the last day's data
1182 var last
= points
[points
.length
-1];
1183 if (last
!= null && canvasx
> last
.canvasx
)
1184 lastx
= points
[points
.length
-1].xval
;
1186 // Extract the points we've selected
1187 this.selPoints_
= [];
1188 var l
= points
.length
;
1189 if (!this.attr_("stackedGraph")) {
1190 for (var i
= 0; i
< l
; i
++) {
1191 if (points
[i
].xval
== lastx
) {
1192 this.selPoints_
.push(points
[i
]);
1196 // Need to 'unstack' points starting from the bottom
1197 var cumulative_sum
= 0;
1198 for (var i
= l
- 1; i
>= 0; i
--) {
1199 if (points
[i
].xval
== lastx
) {
1200 var p
= {}; // Clone the point since we modify it
1201 for (var k
in points
[i
]) {
1202 p
[k
] = points
[i
][k
];
1204 p
.yval
-= cumulative_sum
;
1205 cumulative_sum
+= p
.yval
;
1206 this.selPoints_
.push(p
);
1209 this.selPoints_
.reverse();
1212 if (this.attr_("highlightCallback")) {
1213 var px
= this.lastx_
;
1214 if (px
!== null && lastx
!= px
) {
1215 // only fire if the selected point has changed.
1216 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1220 // Save last x position for callbacks.
1221 this.lastx_
= lastx
;
1223 this.updateSelection_();
1227 * Transforms layout_.points index into data row number.
1228 * @param int layout_.points index
1229 * @return int row number, or -1 if none could be found.
1232 Dygraph
.prototype.idxToRow_
= function(idx
) {
1233 if (idx
< 0) return -1;
1235 for (var i
in this.layout_
.datasets
) {
1236 if (idx
< this.layout_
.datasets
[i
].length
) {
1237 return this.boundaryIds_
[0][0]+idx
;
1239 idx
-= this.layout_
.datasets
[i
].length
;
1245 * Draw dots over the selectied points in the data series. This function
1246 * takes care of cleanup of previously-drawn dots.
1249 Dygraph
.prototype.updateSelection_
= function() {
1250 // Clear the previously drawn vertical, if there is one
1251 var ctx
= this.canvas_
.getContext("2d");
1252 if (this.previousVerticalX_
>= 0) {
1253 // Determine the maximum highlight circle size.
1254 var maxCircleSize
= 0;
1255 var labels
= this.attr_('labels');
1256 for (var i
= 1; i
< labels
.length
; i
++) {
1257 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1258 if (r
> maxCircleSize
) maxCircleSize
= r
;
1260 var px
= this.previousVerticalX_
;
1261 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1262 2 * maxCircleSize
+ 2, this.height_
);
1265 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1267 if (this.selPoints_
.length
> 0) {
1268 var canvasx
= this.selPoints_
[0].canvasx
;
1270 // Set the status message to indicate the selected point(s)
1271 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1272 var fmtFunc
= this.attr_('yValueFormatter');
1273 var clen
= this.colors_
.length
;
1275 if (this.attr_('showLabelsOnHighlight')) {
1276 // Set the status message to indicate the selected point(s)
1277 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1278 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1279 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1280 if (this.attr_("labelsSeparateLines")) {
1283 var point
= this.selPoints_
[i
];
1284 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1285 var yval
= fmtFunc(point
.yval
);
1286 replace
+= " <b><font color='" + c
.toHex() + "'>"
1287 + point
.name
+ "</font></b>:"
1291 this.attr_("labelsDiv").innerHTML
= replace
;
1294 // Draw colored circles over the center of each selected point
1296 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1297 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1299 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1301 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1302 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1303 0, 2 * Math
.PI
, false);
1308 this.previousVerticalX_
= canvasx
;
1313 * Set manually set selected dots, and display information about them
1314 * @param int row number that should by highlighted
1315 * false value clears the selection
1318 Dygraph
.prototype.setSelection
= function(row
) {
1319 // Extract the points we've selected
1320 this.selPoints_
= [];
1323 if (row
!== false) {
1324 row
= row
-this.boundaryIds_
[0][0];
1327 if (row
!== false && row
>= 0) {
1328 for (var i
in this.layout_
.datasets
) {
1329 if (row
< this.layout_
.datasets
[i
].length
) {
1330 var point
= this.layout_
.points
[pos
+row
];
1332 if (this.attr_("stackedGraph")) {
1333 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1336 this.selPoints_
.push(point
);
1338 pos
+= this.layout_
.datasets
[i
].length
;
1342 if (this.selPoints_
.length
) {
1343 this.lastx_
= this.selPoints_
[0].xval
;
1344 this.updateSelection_();
1347 this.clearSelection();
1353 * The mouse has left the canvas. Clear out whatever artifacts remain
1354 * @param {Object} event the mouseout event from the browser.
1357 Dygraph
.prototype.mouseOut_
= function(event
) {
1358 if (this.attr_("unhighlightCallback")) {
1359 this.attr_("unhighlightCallback")(event
);
1362 if (this.attr_("hideOverlayOnMouseOut")) {
1363 this.clearSelection();
1368 * Remove all selection from the canvas
1371 Dygraph
.prototype.clearSelection
= function() {
1372 // Get rid of the overlay data
1373 var ctx
= this.canvas_
.getContext("2d");
1374 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1375 this.attr_("labelsDiv").innerHTML
= "";
1376 this.selPoints_
= [];
1381 * Returns the number of the currently selected row
1382 * @return int row number, of -1 if nothing is selected
1385 Dygraph
.prototype.getSelection
= function() {
1386 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1390 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1391 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1392 return row
+ this.boundaryIds_
[0][0];
1398 Dygraph
.zeropad
= function(x
) {
1399 if (x
< 10) return "0" + x
; else return "" + x
;
1403 * Return a string version of the hours, minutes and seconds portion of a date.
1404 * @param {Number} date The JavaScript date (ms since epoch)
1405 * @return {String} A time of the form "HH:MM:SS"
1408 Dygraph
.hmsString_
= function(date
) {
1409 var zeropad
= Dygraph
.zeropad
;
1410 var d
= new Date(date
);
1411 if (d
.getSeconds()) {
1412 return zeropad(d
.getHours()) + ":" +
1413 zeropad(d
.getMinutes()) + ":" +
1414 zeropad(d
.getSeconds());
1416 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1421 * Convert a JS date to a string appropriate to display on an axis that
1422 * is displaying values at the stated granularity.
1423 * @param {Date} date The date to format
1424 * @param {Number} granularity One of the Dygraph granularity constants
1425 * @return {String} The formatted date
1428 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1429 if (granularity
>= Dygraph
.MONTHLY
) {
1430 return date
.strftime('%b %y');
1432 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1433 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1434 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1436 return Dygraph
.hmsString_(date
.getTime());
1442 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1443 * @param {Number} date The JavaScript date (ms since epoch)
1444 * @return {String} A date of the form "YYYY/MM/DD"
1447 Dygraph
.dateString_
= function(date
, self
) {
1448 var zeropad
= Dygraph
.zeropad
;
1449 var d
= new Date(date
);
1452 var year
= "" + d
.getFullYear();
1453 // Get a 0 padded month string
1454 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1455 // Get a 0 padded day string
1456 var day
= zeropad(d
.getDate());
1459 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1460 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1462 return year
+ "/" + month + "/" + day
+ ret
;
1466 * Round a number to the specified number of digits past the decimal point.
1467 * @param {Number} num The number to round
1468 * @param {Number} places The number of decimals to which to round
1469 * @return {Number} The rounded number
1472 Dygraph
.round_
= function(num
, places
) {
1473 var shift
= Math
.pow(10, places
);
1474 return Math
.round(num
* shift
)/shift
;
1478 * Fires when there's data available to be graphed.
1479 * @param {String} data Raw CSV data to be plotted
1482 Dygraph
.prototype.loadedEvent_
= function(data
) {
1483 this.rawData_
= this.parseCSV_(data
);
1487 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1488 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1489 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1492 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1495 Dygraph
.prototype.addXTicks_
= function() {
1496 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1497 var startDate
, endDate
;
1498 if (this.dateWindow_
) {
1499 startDate
= this.dateWindow_
[0];
1500 endDate
= this.dateWindow_
[1];
1502 startDate
= this.rawData_
[0][0];
1503 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1506 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1507 this.layout_
.updateOptions({xTicks
: xTicks
});
1510 // Time granularity enumeration
1511 Dygraph
.SECONDLY
= 0;
1512 Dygraph
.TWO_SECONDLY
= 1;
1513 Dygraph
.FIVE_SECONDLY
= 2;
1514 Dygraph
.TEN_SECONDLY
= 3;
1515 Dygraph
.THIRTY_SECONDLY
= 4;
1516 Dygraph
.MINUTELY
= 5;
1517 Dygraph
.TWO_MINUTELY
= 6;
1518 Dygraph
.FIVE_MINUTELY
= 7;
1519 Dygraph
.TEN_MINUTELY
= 8;
1520 Dygraph
.THIRTY_MINUTELY
= 9;
1521 Dygraph
.HOURLY
= 10;
1522 Dygraph
.TWO_HOURLY
= 11;
1523 Dygraph
.SIX_HOURLY
= 12;
1525 Dygraph
.WEEKLY
= 14;
1526 Dygraph
.MONTHLY
= 15;
1527 Dygraph
.QUARTERLY
= 16;
1528 Dygraph
.BIANNUAL
= 17;
1529 Dygraph
.ANNUAL
= 18;
1530 Dygraph
.DECADAL
= 19;
1531 Dygraph
.NUM_GRANULARITIES
= 20;
1533 Dygraph
.SHORT_SPACINGS
= [];
1534 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1535 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1536 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1537 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1538 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1539 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1540 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1541 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1542 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1543 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1544 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1545 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1546 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1547 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1548 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1552 // If we used this time granularity, how many ticks would there be?
1553 // This is only an approximation, but it's generally good enough.
1555 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1556 if (granularity
< Dygraph
.MONTHLY
) {
1557 // Generate one tick mark for every fixed interval of time.
1558 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1559 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1561 var year_mod
= 1; // e.g. to only print one point every 10 years.
1562 var num_months
= 12;
1563 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1564 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1565 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1566 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1568 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1569 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1570 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1576 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1577 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1579 // Returns an array containing {v: millis, label: label} dictionaries.
1581 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1582 var formatter
= this.attr_("xAxisLabelFormatter");
1584 if (granularity
< Dygraph
.MONTHLY
) {
1585 // Generate one tick mark for every fixed interval of time.
1586 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1587 var format
= '%d%b'; // e.g. "1Jan"
1589 // Find a time less than start_time which occurs on a "nice" time boundary
1590 // for this granularity.
1591 var g
= spacing
/ 1000;
1592 var d
= new Date(start_time
);
1593 if (g
<= 60) { // seconds
1594 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1598 if (g
<= 60) { // minutes
1599 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1604 if (g
<= 24) { // days
1605 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1610 if (g
== 7) { // one week
1611 d
.setDate(d
.getDate() - d
.getDay());
1616 start_time
= d
.getTime();
1618 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1619 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1622 // Display a tick mark on the first of a set of months of each year.
1623 // Years get a tick mark iff y % year_mod == 0. This is useful for
1624 // displaying a tick mark once every 10 years, say, on long time scales.
1626 var year_mod
= 1; // e.g. to only print one point every 10 years.
1628 if (granularity
== Dygraph
.MONTHLY
) {
1629 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1630 } else if (granularity
== Dygraph
.QUARTERLY
) {
1631 months
= [ 0, 3, 6, 9 ];
1632 } else if (granularity
== Dygraph
.BIANNUAL
) {
1634 } else if (granularity
== Dygraph
.ANNUAL
) {
1636 } else if (granularity
== Dygraph
.DECADAL
) {
1641 var start_year
= new Date(start_time
).getFullYear();
1642 var end_year
= new Date(end_time
).getFullYear();
1643 var zeropad
= Dygraph
.zeropad
;
1644 for (var i
= start_year
; i
<= end_year
; i
++) {
1645 if (i
% year_mod
!= 0) continue;
1646 for (var j
= 0; j
< months
.length
; j
++) {
1647 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1648 var t
= Date
.parse(date_str
);
1649 if (t
< start_time
|| t
> end_time
) continue;
1650 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1660 * Add ticks to the x-axis based on a date range.
1661 * @param {Number} startDate Start of the date window (millis since epoch)
1662 * @param {Number} endDate End of the date window (millis since epoch)
1663 * @return {Array.<Object>} Array of {label, value} tuples.
1666 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1668 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1669 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1670 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1677 return self
.GetXAxis(startDate
, endDate
, chosen
);
1679 // TODO(danvk): signal error.
1684 * Add ticks when the x axis has numbers on it (instead of dates)
1685 * @param {Number} startDate Start of the date window (millis since epoch)
1686 * @param {Number} endDate End of the date window (millis since epoch)
1688 * @param {function} attribute accessor function.
1689 * @return {Array.<Object>} Array of {label, value} tuples.
1692 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1693 var attr
= function(k
) {
1694 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1695 return self
.attr_(k
);
1700 for (var i
= 0; i
< vals
.length
; i
++) {
1701 ticks
.push({v
: vals
[i
]});
1705 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1706 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1707 // The first spacing greater than pixelsPerYLabel is what we use.
1708 // TODO(danvk): version that works on a log scale.
1709 if (attr("labelsKMG2")) {
1710 var mults
= [1, 2, 4, 8];
1712 var mults
= [1, 2, 5];
1714 var scale
, low_val
, high_val
, nTicks
;
1715 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1716 var pixelsPerTick
= attr('pixelsPerYLabel');
1717 for (var i
= -10; i
< 50; i
++) {
1718 if (attr("labelsKMG2")) {
1719 var base_scale
= Math
.pow(16, i
);
1721 var base_scale
= Math
.pow(10, i
);
1723 for (var j
= 0; j
< mults
.length
; j
++) {
1724 scale
= base_scale
* mults
[j
];
1725 low_val
= Math
.floor(minV
/ scale
) * scale
;
1726 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1727 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1728 var spacing
= self
.height_
/ nTicks
;
1729 // wish I could break out of both loops at once...
1730 if (spacing
> pixelsPerTick
) break;
1732 if (spacing
> pixelsPerTick
) break;
1735 // Construct the set of ticks.
1736 // Allow reverse y-axis if it's explicitly requested.
1737 if (low_val
> high_val
) scale
*= -1;
1738 for (var i
= 0; i
< nTicks
; i
++) {
1739 var tickV
= low_val
+ i
* scale
;
1740 ticks
.push( {v
: tickV
} );
1744 // Add formatted labels to the ticks.
1747 if (attr("labelsKMB")) {
1749 k_labels
= [ "K", "M", "B", "T" ];
1751 if (attr("labelsKMG2")) {
1752 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1754 k_labels
= [ "k", "M", "G", "T" ];
1756 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
1758 for (var i
= 0; i
< ticks
.length
; i
++) {
1759 var tickV
= ticks
[i
].v
;
1760 var absTickV
= Math
.abs(tickV
);
1762 if (formatter
!= undefined
) {
1763 label
= formatter(tickV
);
1765 label
= Dygraph
.round_(tickV
, 2);
1767 if (k_labels
.length
) {
1768 // Round up to an appropriate unit.
1770 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1771 if (absTickV
>= n
) {
1772 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1777 ticks
[i
].label
= label
;
1782 // Computes the range of the data series (including confidence intervals).
1783 // series is either [ [x1, y1], [x2, y2], ... ] or
1784 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1785 // Returns [low, high]
1786 Dygraph
.prototype.extremeValues_
= function(series
) {
1787 var minY
= null, maxY
= null;
1789 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1791 // With custom bars, maxY is the max of the high values.
1792 for (var j
= 0; j
< series
.length
; j
++) {
1793 var y
= series
[j
][1][0];
1795 var low
= y
- series
[j
][1][1];
1796 var high
= y
+ series
[j
][1][2];
1797 if (low
> y
) low
= y
; // this can happen with custom bars,
1798 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1799 if (maxY
== null || high
> maxY
) {
1802 if (minY
== null || low
< minY
) {
1807 for (var j
= 0; j
< series
.length
; j
++) {
1808 var y
= series
[j
][1];
1809 if (y
=== null || isNaN(y
)) continue;
1810 if (maxY
== null || y
> maxY
) {
1813 if (minY
== null || y
< minY
) {
1819 return [minY
, maxY
];
1823 * This function is called once when the chart's data is changed or the options
1824 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1825 * idea is that values derived from the chart's data can be computed here,
1826 * rather than every time the chart is drawn. This includes things like the
1827 * number of axes, rolling averages, etc.
1829 Dygraph
.prototype.predraw_
= function() {
1830 // TODO(danvk): move more computations out of drawGraph_ and into here.
1831 this.computeYAxes_();
1833 // Create a new plotter.
1834 if (this.plotter_
) this.plotter_
.clear();
1835 this.plotter_
= new DygraphCanvasRenderer(this,
1836 this.hidden_
, this.layout_
,
1837 this.renderOptions_
);
1839 // The roller sits in the bottom left corner of the chart. We don't know where
1840 // this will be until the options are available, so it's positioned here.
1841 this.createRollInterface_();
1843 // Same thing applies for the labelsDiv. It's right edge should be flush with
1844 // the right edge of the charting area (which may not be the same as the right
1845 // edge of the div, if we have two y-axes.
1846 this.positionLabelsDiv_();
1848 // If the data or options have changed, then we'd better redraw.
1854 * Update the graph with new data. This method is called when the viewing area
1855 * has changed. If the underlying data or options have changed, predraw_ will
1856 * be called before drawGraph_ is called.
1859 Dygraph
.prototype.drawGraph_
= function() {
1860 var data
= this.rawData_
;
1862 // This is used to set the second parameter to drawCallback, below.
1863 var is_initial_draw
= this.is_initial_draw_
;
1864 this.is_initial_draw_
= false;
1866 var minY
= null, maxY
= null;
1867 this.layout_
.removeAllDatasets();
1869 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1871 // Loop over the fields (series). Go from the last to the first,
1872 // because if they're stacked that's how we accumulate the values.
1874 var cumulative_y
= []; // For stacked series.
1877 var extremes
= {}; // series name -> [low, high]
1879 // Loop over all fields and create datasets
1880 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1881 if (!this.visibility()[i
- 1]) continue;
1883 var seriesName
= this.attr_("labels")[i
];
1884 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1887 for (var j
= 0; j
< data
.length
; j
++) {
1888 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1889 var date
= data
[j
][0];
1890 series
.push([date
, data
[j
][i
]]);
1894 // TODO(danvk): move this into predraw_. It's insane to do it here.
1895 series
= this.rollingAverage(series
, this.rollPeriod_
);
1897 // Prune down to the desired range, if necessary (for zooming)
1898 // Because there can be lines going to points outside of the visible area,
1899 // we actually prune to visible points, plus one on either side.
1900 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1901 if (this.dateWindow_
) {
1902 var low
= this.dateWindow_
[0];
1903 var high
= this.dateWindow_
[1];
1905 // TODO(danvk): do binary search instead of linear search.
1906 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1907 var firstIdx
= null, lastIdx
= null;
1908 for (var k
= 0; k
< series
.length
; k
++) {
1909 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1912 if (series
[k
][0] <= high
) {
1916 if (firstIdx
=== null) firstIdx
= 0;
1917 if (firstIdx
> 0) firstIdx
--;
1918 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1919 if (lastIdx
< series
.length
- 1) lastIdx
++;
1920 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1921 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1922 pruned
.push(series
[k
]);
1926 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1929 var seriesExtremes
= this.extremeValues_(series
);
1932 for (var j
=0; j
<series
.length
; j
++) {
1933 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1936 } else if (this.attr_("stackedGraph")) {
1937 var l
= series
.length
;
1939 for (var j
= 0; j
< l
; j
++) {
1940 // If one data set has a NaN, let all subsequent stacked
1941 // sets inherit the NaN -- only start at 0 for the first set.
1942 var x
= series
[j
][0];
1943 if (cumulative_y
[x
] === undefined
) {
1944 cumulative_y
[x
] = 0;
1947 actual_y
= series
[j
][1];
1948 cumulative_y
[x
] += actual_y
;
1950 series
[j
] = [x
, cumulative_y
[x
]]
1952 if (cumulative_y
[x
] > seriesExtremes
[1]) {
1953 seriesExtremes
[1] = cumulative_y
[x
];
1955 if (cumulative_y
[x
] < seriesExtremes
[0]) {
1956 seriesExtremes
[0] = cumulative_y
[x
];
1960 extremes
[seriesName
] = seriesExtremes
;
1962 datasets
[i
] = series
;
1965 for (var i
= 1; i
< datasets
.length
; i
++) {
1966 if (!this.visibility()[i
- 1]) continue;
1967 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1970 // TODO(danvk): this method doesn't need to return anything.
1971 var out
= this.computeYAxisRanges_(extremes
);
1973 var seriesToAxisMap
= out
[1];
1974 this.layout_
.updateOptions( { yAxes
: axes
,
1975 seriesToAxisMap
: seriesToAxisMap
1980 // Tell PlotKit to use this new data and render itself
1981 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1982 this.layout_
.evaluateWithError();
1983 this.plotter_
.clear();
1984 this.plotter_
.render();
1985 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1986 this.canvas_
.height
);
1988 if (this.attr_("drawCallback") !== null) {
1989 this.attr_("drawCallback")(this, is_initial_draw
);
1994 * Determine properties of the y-axes which are independent of the data
1995 * currently being displayed. This includes things like the number of axes and
1996 * the style of the axes. It does not include the range of each axis and its
1998 * This fills in this.axes_ and this.seriesToAxisMap_.
1999 * axes_ = [ { options } ]
2000 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2001 * indices are into the axes_ array.
2003 Dygraph
.prototype.computeYAxes_
= function() {
2004 this.axes_
= [{}]; // always have at least one y-axis.
2005 this.seriesToAxisMap_
= {};
2007 // Get a list of series names.
2008 var labels
= this.attr_("labels");
2010 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2012 // all options which could be applied per-axis:
2020 'axisLabelFontSize',
2024 // Copy global axis options over to the first axis.
2025 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2026 var k
= axisOptions
[i
];
2027 var v
= this.attr_(k
);
2028 if (v
) this.axes_
[0][k
] = v
;
2031 // Go through once and add all the axes.
2032 for (var seriesName
in series
) {
2033 if (!series
.hasOwnProperty(seriesName
)) continue;
2034 var axis
= this.attr_("axis", seriesName
);
2036 this.seriesToAxisMap_
[seriesName
] = 0;
2039 if (typeof(axis
) == 'object') {
2040 // Add a new axis, making a copy of its per-axis options.
2042 Dygraph
.update(opts
, this.axes_
[0]);
2043 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2044 Dygraph
.update(opts
, axis
);
2045 this.axes_
.push(opts
);
2046 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2050 // Go through one more time and assign series to an axis defined by another
2051 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2052 for (var seriesName
in series
) {
2053 if (!series
.hasOwnProperty(seriesName
)) continue;
2054 var axis
= this.attr_("axis", seriesName
);
2055 if (typeof(axis
) == 'string') {
2056 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2057 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2058 "series " + axis
+ ", which does not define its own axis.");
2061 var idx
= this.seriesToAxisMap_
[axis
];
2062 this.seriesToAxisMap_
[seriesName
] = idx
;
2066 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2067 // this last so that hiding the first series doesn't destroy the axis
2068 // properties of the primary axis.
2069 var seriesToAxisFiltered
= {};
2070 var vis
= this.visibility();
2071 for (var i
= 1; i
< labels
.length
; i
++) {
2073 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2075 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2079 * Returns the number of y-axes on the chart.
2080 * @return {Number} the number of axes.
2082 Dygraph
.prototype.numAxes
= function() {
2084 for (var series
in this.seriesToAxisMap_
) {
2085 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2086 var idx
= this.seriesToAxisMap_
[series
];
2087 if (idx
> last_axis
) last_axis
= idx
;
2089 return 1 + last_axis
;
2093 * Determine the value range and tick marks for each axis.
2094 * @param {Object} extremes A mapping from seriesName -> [low, high]
2095 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2097 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2098 // Build a map from axis number -> [list of series names]
2099 var seriesForAxis
= [];
2100 for (var series
in this.seriesToAxisMap_
) {
2101 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2102 var idx
= this.seriesToAxisMap_
[series
];
2103 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2104 seriesForAxis
[idx
].push(series
);
2107 // Compute extreme values, a span and tick marks for each axis.
2108 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2109 var axis
= this.axes_
[i
];
2110 if (axis
.valueWindow
) {
2111 // This is only set if the user has zoomed on the y-axis. It is never set
2112 // by a user. It takes precedence over axis.valueRange because, if you set
2113 // valueRange, you'd still expect to be able to pan.
2114 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2115 } else if (axis
.valueRange
) {
2116 // This is a user-set value range for this axis.
2117 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2119 // Calculate the extremes of extremes.
2120 var series
= seriesForAxis
[i
];
2121 var minY
= Infinity
; // extremes[series[0]][0];
2122 var maxY
= -Infinity
; // extremes[series[0]][1];
2123 for (var j
= 0; j
< series
.length
; j
++) {
2124 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2125 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2127 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2129 // Add some padding and round up to an integer to be human-friendly.
2130 var span
= maxY
- minY
;
2131 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2132 if (span
== 0) { span
= maxY
; }
2133 var maxAxisY
= maxY
+ 0.1 * span
;
2134 var minAxisY
= minY
- 0.1 * span
;
2136 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2137 if (!this.attr_("avoidMinZero")) {
2138 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2139 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2142 if (this.attr_("includeZero")) {
2143 if (maxY
< 0) maxAxisY
= 0;
2144 if (minY
> 0) minAxisY
= 0;
2147 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2150 // Add ticks. By default, all axes inherit the tick positions of the
2151 // primary axis. However, if an axis is specifically marked as having
2152 // independent ticks, then that is permissible as well.
2153 if (i
== 0 || axis
.independentTicks
) {
2155 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2156 axis
.computedValueRange
[1],
2160 var p_axis
= this.axes_
[0];
2161 var p_ticks
= p_axis
.ticks
;
2162 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2163 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2164 var tick_values
= [];
2165 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2166 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2167 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2168 tick_values
.push(y_val
);
2172 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2173 axis
.computedValueRange
[1],
2174 this, axis
, tick_values
);
2178 return [this.axes_
, this.seriesToAxisMap_
];
2182 * Calculates the rolling average of a data set.
2183 * If originalData is [label, val], rolls the average of those.
2184 * If originalData is [label, [, it's interpreted as [value, stddev]
2185 * and the roll is returned in the same form, with appropriately reduced
2186 * stddev for each value.
2187 * Note that this is where fractional input (i.e. '5/10') is converted into
2189 * @param {Array} originalData The data in the appropriate format (see above)
2190 * @param {Number} rollPeriod The number of days over which to average the data
2192 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2193 if (originalData
.length
< 2)
2194 return originalData
;
2195 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2196 var rollingData
= [];
2197 var sigma
= this.attr_("sigma");
2199 if (this.fractions_
) {
2201 var den
= 0; // numerator/denominator
2203 for (var i
= 0; i
< originalData
.length
; i
++) {
2204 num
+= originalData
[i
][1][0];
2205 den
+= originalData
[i
][1][1];
2206 if (i
- rollPeriod
>= 0) {
2207 num
-= originalData
[i
- rollPeriod
][1][0];
2208 den
-= originalData
[i
- rollPeriod
][1][1];
2211 var date
= originalData
[i
][0];
2212 var value
= den
? num
/ den
: 0.0;
2213 if (this.attr_("errorBars")) {
2214 if (this.wilsonInterval_
) {
2215 // For more details on this confidence interval, see:
2216 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2218 var p
= value
< 0 ? 0 : value
, n
= den
;
2219 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2220 var denom
= 1 + sigma
* sigma
/ den
;
2221 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2222 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2223 rollingData
[i
] = [date
,
2224 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2226 rollingData
[i
] = [date
, [0, 0, 0]];
2229 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2230 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2233 rollingData
[i
] = [date
, mult
* value
];
2236 } else if (this.attr_("customBars")) {
2241 for (var i
= 0; i
< originalData
.length
; i
++) {
2242 var data
= originalData
[i
][1];
2244 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2246 if (y
!= null && !isNaN(y
)) {
2252 if (i
- rollPeriod
>= 0) {
2253 var prev
= originalData
[i
- rollPeriod
];
2254 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2261 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2262 1.0 * (mid
- low
) / count
,
2263 1.0 * (high
- mid
) / count
]];
2266 // Calculate the rolling average for the first rollPeriod - 1 points where
2267 // there is not enough data to roll over the full number of days
2268 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2269 if (!this.attr_("errorBars")){
2270 if (rollPeriod
== 1) {
2271 return originalData
;
2274 for (var i
= 0; i
< originalData
.length
; i
++) {
2277 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2278 var y
= originalData
[j
][1];
2279 if (y
== null || isNaN(y
)) continue;
2281 sum
+= originalData
[j
][1];
2284 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2286 rollingData
[i
] = [originalData
[i
][0], null];
2291 for (var i
= 0; i
< originalData
.length
; i
++) {
2295 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2296 var y
= originalData
[j
][1][0];
2297 if (y
== null || isNaN(y
)) continue;
2299 sum
+= originalData
[j
][1][0];
2300 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2303 var stddev
= Math
.sqrt(variance
) / num_ok
;
2304 rollingData
[i
] = [originalData
[i
][0],
2305 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2307 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2317 * Parses a date, returning the number of milliseconds since epoch. This can be
2318 * passed in as an xValueParser in the Dygraph constructor.
2319 * TODO(danvk): enumerate formats that this understands.
2320 * @param {String} A date in YYYYMMDD format.
2321 * @return {Number} Milliseconds since epoch.
2324 Dygraph
.dateParser
= function(dateStr
, self
) {
2327 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2328 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2329 while (dateStrSlashed
.search("-") != -1) {
2330 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2332 d
= Date
.parse(dateStrSlashed
);
2333 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2334 // TODO(danvk): remove support for this format. It's confusing.
2335 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2336 + "/" + dateStr
.substr(6,2);
2337 d
= Date
.parse(dateStrSlashed
);
2339 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2340 // "2009/07/12 12:34:56"
2341 d
= Date
.parse(dateStr
);
2344 if (!d
|| isNaN(d
)) {
2345 self
.error("Couldn't parse " + dateStr
+ " as a date");
2351 * Detects the type of the str (date or numeric) and sets the various
2352 * formatting attributes in this.attrs_ based on this type.
2353 * @param {String} str An x value.
2356 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2358 if (str
.indexOf('-') >= 0 ||
2359 str
.indexOf('/') >= 0 ||
2360 isNaN(parseFloat(str
))) {
2362 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2363 // TODO(danvk): remove support for this format.
2368 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2369 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2370 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2371 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2373 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2374 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2375 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2376 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2381 * Parses a string in a special csv format. We expect a csv file where each
2382 * line is a date point, and the first field in each line is the date string.
2383 * We also expect that all remaining fields represent series.
2384 * if the errorBars attribute is set, then interpret the fields as:
2385 * date, series1, stddev1, series2, stddev2, ...
2386 * @param {Array.<Object>} data See above.
2389 * @return Array.<Object> An array with one entry for each row. These entries
2390 * are an array of cells in that row. The first entry is the parsed x-value for
2391 * the row. The second, third, etc. are the y-values. These can take on one of
2392 * three forms, depending on the CSV and constructor parameters:
2394 * 2. [ value, stddev ]
2395 * 3. [ low value, center value, high value ]
2397 Dygraph
.prototype.parseCSV_
= function(data
) {
2399 var lines
= data
.split("\n");
2401 // Use the default delimiter or fall back to a tab if that makes sense.
2402 var delim
= this.attr_('delimiter');
2403 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2408 if (this.labelsFromCSV_
) {
2410 this.attrs_
.labels
= lines
[0].split(delim
);
2413 // Parse the x as a float or return null if it's not a number.
2414 var parseFloatOrNull
= function(x
) {
2415 var val
= parseFloat(x
);
2416 return isNaN(val
) ? null : val
;
2420 var defaultParserSet
= false; // attempt to auto-detect x value type
2421 var expectedCols
= this.attr_("labels").length
;
2422 var outOfOrder
= false;
2423 for (var i
= start
; i
< lines
.length
; i
++) {
2424 var line
= lines
[i
];
2425 if (line
.length
== 0) continue; // skip blank lines
2426 if (line
[0] == '#') continue; // skip comment lines
2427 var inFields
= line
.split(delim
);
2428 if (inFields
.length
< 2) continue;
2431 if (!defaultParserSet
) {
2432 this.detectTypeFromString_(inFields
[0]);
2433 xParser
= this.attr_("xValueParser");
2434 defaultParserSet
= true;
2436 fields
[0] = xParser(inFields
[0], this);
2438 // If fractions are expected, parse the numbers as "A/B
"
2439 if (this.fractions_) {
2440 for (var j = 1; j < inFields.length; j++) {
2441 // TODO(danvk): figure out an appropriate way to flag parse errors.
2442 var vals = inFields[j].split("/");
2443 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2445 } else if (this.attr_("errorBars
")) {
2446 // If there are error bars, values are (value, stddev) pairs
2447 for (var j = 1; j < inFields.length; j += 2)
2448 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2449 parseFloatOrNull(inFields[j + 1])];
2450 } else if (this.attr_("customBars
")) {
2451 // Bars are a low;center;high tuple
2452 for (var j = 1; j < inFields.length; j++) {
2453 var vals = inFields[j].split(";");
2454 fields[j] = [ parseFloatOrNull(vals[0]),
2455 parseFloatOrNull(vals[1]),
2456 parseFloatOrNull(vals[2]) ];
2459 // Values are just numbers
2460 for (var j = 1; j < inFields.length; j++) {
2461 fields[j] = parseFloatOrNull(inFields[j]);
2464 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2469 if (fields.length != expectedCols) {
2470 this.error("Number of columns
in line
" + i + " (" + fields.length +
2471 ") does not agree
with number of
labels (" + expectedCols +
2477 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2478 ret.sort(function(a,b) { return a[0] - b[0] });
2485 * The user has provided their data as a pre-packaged JS array. If the x values
2486 * are numeric, this is the same as dygraphs' internal format. If the x values
2487 * are dates, we need to convert them from Date objects to ms since epoch.
2488 * @param {Array.<Object>} data
2489 * @return {Array.<Object>} data with numeric x values.
2491 Dygraph.prototype.parseArray_ = function(data) {
2492 // Peek at the first x value to see if it's numeric.
2493 if (data.length == 0) {
2494 this.error("Can
't plot empty data set");
2497 if (data[0].length == 0) {
2498 this.error("Data set cannot contain an empty row");
2502 if (this.attr_("labels") == null) {
2503 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2504 "in the options parameter");
2505 this.attrs_.labels = [ "X" ];
2506 for (var i = 1; i < data[0].length; i++) {
2507 this.attrs_.labels.push("Y" + i);
2511 if (Dygraph.isDateLike(data[0][0])) {
2512 // Some intelligent defaults for a date x-axis.
2513 this.attrs_.xValueFormatter = Dygraph.dateString_;
2514 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2515 this.attrs_.xTicker = Dygraph.dateTicker;
2517 // Assume they're all dates
.
2518 var parsedData
= Dygraph
.clone(data
);
2519 for (var i
= 0; i
< data
.length
; i
++) {
2520 if (parsedData
[i
].length
== 0) {
2521 this.error("Row " + (1 + i
) + " of data is empty");
2524 if (parsedData
[i
][0] == null
2525 || typeof(parsedData
[i
][0].getTime
) != 'function'
2526 || isNaN(parsedData
[i
][0].getTime())) {
2527 this.error("x value in row " + (1 + i
) + " is not a Date");
2530 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2534 // Some intelligent defaults for a numeric x-axis.
2535 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2536 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2542 * Parses a DataTable object from gviz.
2543 * The data is expected to have a first column that is either a date or a
2544 * number. All subsequent columns must be numbers. If there is a clear mismatch
2545 * between this.xValueParser_ and the type of the first column, it will be
2546 * fixed. Fills out rawData_.
2547 * @param {Array.<Object>} data See above.
2550 Dygraph
.prototype.parseDataTable_
= function(data
) {
2551 var cols
= data
.getNumberOfColumns();
2552 var rows
= data
.getNumberOfRows();
2554 var indepType
= data
.getColumnType(0);
2555 if (indepType
== 'date' || indepType
== 'datetime') {
2556 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2557 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2558 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2559 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2560 } else if (indepType
== 'number') {
2561 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2562 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2563 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2564 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2566 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2567 "column 1 of DataTable input (Got '" + indepType
+ "')");
2571 // Array of the column indices which contain data (and not annotations).
2573 var annotationCols
= {}; // data index -> [annotation cols]
2574 var hasAnnotations
= false;
2575 for (var i
= 1; i
< cols
; i
++) {
2576 var type
= data
.getColumnType(i
);
2577 if (type
== 'number') {
2579 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2580 // This is OK -- it's an annotation column.
2581 var dataIdx
= colIdx
[colIdx
.length
- 1];
2582 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2583 annotationCols
[dataIdx
] = [i
];
2585 annotationCols
[dataIdx
].push(i
);
2587 hasAnnotations
= true;
2589 this.error("Only 'number' is supported as a dependent type with Gviz." +
2590 " 'string' is only supported if displayAnnotations is true");
2594 // Read column labels
2595 // TODO(danvk): add support back for errorBars
2596 var labels
= [data
.getColumnLabel(0)];
2597 for (var i
= 0; i
< colIdx
.length
; i
++) {
2598 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2599 if (this.attr_("errorBars")) i
+= 1;
2601 this.attrs_
.labels
= labels
;
2602 cols
= labels
.length
;
2605 var outOfOrder
= false;
2606 var annotations
= [];
2607 for (var i
= 0; i
< rows
; i
++) {
2609 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2610 data
.getValue(i
, 0) === null) {
2611 this.warn("Ignoring row " + i
+
2612 " of DataTable because of undefined or null first column.");
2616 if (indepType
== 'date' || indepType
== 'datetime') {
2617 row
.push(data
.getValue(i
, 0).getTime());
2619 row
.push(data
.getValue(i
, 0));
2621 if (!this.attr_("errorBars")) {
2622 for (var j
= 0; j
< colIdx
.length
; j
++) {
2623 var col
= colIdx
[j
];
2624 row
.push(data
.getValue(i
, col
));
2625 if (hasAnnotations
&&
2626 annotationCols
.hasOwnProperty(col
) &&
2627 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2629 ann
.series
= data
.getColumnLabel(col
);
2631 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2633 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2634 if (k
) ann
.text
+= "\n";
2635 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2637 annotations
.push(ann
);
2641 for (var j
= 0; j
< cols
- 1; j
++) {
2642 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2645 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2652 this.warn("DataTable is out of order; order it correctly to speed loading.");
2653 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2655 this.rawData_
= ret
;
2657 if (annotations
.length
> 0) {
2658 this.setAnnotations(annotations
, true);
2662 // These functions are all based on MochiKit.
2663 Dygraph
.update
= function (self
, o
) {
2664 if (typeof(o
) != 'undefined' && o
!== null) {
2666 if (o
.hasOwnProperty(k
)) {
2674 Dygraph
.isArrayLike
= function (o
) {
2675 var typ
= typeof(o
);
2677 (typ
!= 'object' && !(typ
== 'function' &&
2678 typeof(o
.item
) == 'function')) ||
2680 typeof(o
.length
) != 'number' ||
2688 Dygraph
.isDateLike
= function (o
) {
2689 if (typeof(o
) != "object" || o
=== null ||
2690 typeof(o
.getTime
) != 'function') {
2696 Dygraph
.clone
= function(o
) {
2697 // TODO(danvk): figure out how MochiKit's version works
2699 for (var i
= 0; i
< o
.length
; i
++) {
2700 if (Dygraph
.isArrayLike(o
[i
])) {
2701 r
.push(Dygraph
.clone(o
[i
]));
2711 * Get the CSV data. If it's in a function, call that function. If it's in a
2712 * file, do an XMLHttpRequest to get it.
2715 Dygraph
.prototype.start_
= function() {
2716 if (typeof this.file_
== 'function') {
2717 // CSV string. Pretend we got it via XHR.
2718 this.loadedEvent_(this.file_());
2719 } else if (Dygraph
.isArrayLike(this.file_
)) {
2720 this.rawData_
= this.parseArray_(this.file_
);
2722 } else if (typeof this.file_
== 'object' &&
2723 typeof this.file_
.getColumnRange
== 'function') {
2724 // must be a DataTable from gviz.
2725 this.parseDataTable_(this.file_
);
2727 } else if (typeof this.file_
== 'string') {
2728 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2729 if (this.file_
.indexOf('\n') >= 0) {
2730 this.loadedEvent_(this.file_
);
2732 var req
= new XMLHttpRequest();
2734 req
.onreadystatechange
= function () {
2735 if (req
.readyState
== 4) {
2736 if (req
.status
== 200) {
2737 caller
.loadedEvent_(req
.responseText
);
2742 req
.open("GET", this.file_
, true);
2746 this.error("Unknown data format: " + (typeof this.file_
));
2751 * Changes various properties of the graph. These can include:
2753 * <li>file: changes the source data for the graph</li>
2754 * <li>errorBars: changes whether the data contains stddev</li>
2756 * @param {Object} attrs The new properties and values
2758 Dygraph
.prototype.updateOptions
= function(attrs
) {
2759 // TODO(danvk): this is a mess. Rethink this function.
2760 if ('rollPeriod' in attrs
) {
2761 this.rollPeriod_
= attrs
.rollPeriod
;
2763 if ('dateWindow' in attrs
) {
2764 this.dateWindow_
= attrs
.dateWindow
;
2767 // TODO(danvk): validate per-series options.
2772 // highlightCircleSize
2774 Dygraph
.update(this.user_attrs_
, attrs
);
2775 Dygraph
.update(this.renderOptions_
, attrs
);
2777 this.labelsFromCSV_
= (this.attr_("labels") == null);
2779 // TODO(danvk): this doesn't match the constructor logic
2780 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2781 if (attrs
['file']) {
2782 this.file_
= attrs
['file'];
2790 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2791 * containing div (which has presumably changed size since the dygraph was
2792 * instantiated. If the width/height are specified, the div will be resized.
2794 * This is far more efficient than destroying and re-instantiating a
2795 * Dygraph, since it doesn't have to reparse the underlying data.
2797 * @param {Number} width Width (in pixels)
2798 * @param {Number} height Height (in pixels)
2800 Dygraph
.prototype.resize
= function(width
, height
) {
2801 if (this.resize_lock
) {
2804 this.resize_lock
= true;
2806 if ((width
=== null) != (height
=== null)) {
2807 this.warn("Dygraph.resize() should be called with zero parameters or " +
2808 "two non-NULL parameters. Pretending it was zero.");
2809 width
= height
= null;
2812 // TODO(danvk): there should be a clear() method.
2813 this.maindiv_
.innerHTML
= "";
2814 this.attrs_
.labelsDiv
= null;
2817 this.maindiv_
.style
.width
= width
+ "px";
2818 this.maindiv_
.style
.height
= height
+ "px";
2819 this.width_
= width
;
2820 this.height_
= height
;
2822 this.width_
= this.maindiv_
.offsetWidth
;
2823 this.height_
= this.maindiv_
.offsetHeight
;
2826 this.createInterface_();
2829 this.resize_lock
= false;
2833 * Adjusts the number of days in the rolling average. Updates the graph to
2834 * reflect the new averaging period.
2835 * @param {Number} length Number of days over which to average the data.
2837 Dygraph
.prototype.adjustRoll
= function(length
) {
2838 this.rollPeriod_
= length
;
2843 * Returns a boolean array of visibility statuses.
2845 Dygraph
.prototype.visibility
= function() {
2846 // Do lazy-initialization, so that this happens after we know the number of
2848 if (!this.attr_("visibility")) {
2849 this.attrs_
["visibility"] = [];
2851 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2852 this.attr_("visibility").push(true);
2854 return this.attr_("visibility");
2858 * Changes the visiblity of a series.
2860 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2861 var x
= this.visibility();
2862 if (num
< 0 || num
>= x
.length
) {
2863 this.warn("invalid series number in setVisibility: " + num
);
2871 * Update the list of annotations and redraw the chart.
2873 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2874 // Only add the annotation CSS rule once we know it will be used.
2875 Dygraph
.addAnnotationRule();
2876 this.annotations_
= ann
;
2877 this.layout_
.setAnnotations(this.annotations_
);
2878 if (!suppressDraw
) {
2884 * Return the list of annotations.
2886 Dygraph
.prototype.annotations
= function() {
2887 return this.annotations_
;
2891 * Get the index of a series (column) given its name. The first column is the
2892 * x-axis, so the data series start with index 1.
2894 Dygraph
.prototype.indexFromSetName
= function(name
) {
2895 var labels
= this.attr_("labels");
2896 for (var i
= 0; i
< labels
.length
; i
++) {
2897 if (labels
[i
] == name
) return i
;
2902 Dygraph
.addAnnotationRule
= function() {
2903 if (Dygraph
.addedAnnotationCSS
) return;
2905 var rule
= "border: 1px solid black; " +
2906 "background-color: white; " +
2907 "text-align: center;";
2909 var styleSheetElement
= document
.createElement("style");
2910 styleSheetElement
.type
= "text/css";
2911 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2913 // Find the first style sheet that we can access.
2914 // We may not add a rule to a style sheet from another domain for security
2915 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
2916 // adds its own style sheets from google.com.
2917 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
2918 if (document
.styleSheets
[i
].disabled
) continue;
2919 var mysheet
= document
.styleSheets
[i
];
2921 if (mysheet
.insertRule
) { // Firefox
2922 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2923 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2924 } else if (mysheet
.addRule
) { // IE
2925 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2927 Dygraph
.addedAnnotationCSS
= true;
2930 // Was likely a security exception.
2934 this.warn("Unable to add default annotation CSS rule; display may be off.");
2938 * Create a new canvas element. This is more complex than a simple
2939 * document.createElement("canvas") because of IE and excanvas.
2941 Dygraph
.createCanvas
= function() {
2942 var canvas
= document
.createElement("canvas");
2944 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2945 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
2946 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2954 * A wrapper around Dygraph that implements the gviz API.
2955 * @param {Object} container The DOM object the visualization should live in.
2957 Dygraph
.GVizChart
= function(container
) {
2958 this.container
= container
;
2961 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2962 // Clear out any existing dygraph.
2963 // TODO(danvk): would it make more sense to simply redraw using the current
2964 // date_graph object?
2965 this.container
.innerHTML
= '';
2966 if (typeof(this.date_graph
) != 'undefined') {
2967 this.date_graph
.destroy();
2970 this.date_graph
= new Dygraph(this.container
, data
, options
);
2974 * Google charts compatible setSelection
2975 * Only row selection is supported, all points in the row will be highlighted
2976 * @param {Array} array of the selected cells
2979 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2981 if (selection_array
.length
) {
2982 row
= selection_array
[0].row
;
2984 this.date_graph
.setSelection(row
);
2988 * Google charts compatible getSelection implementation
2989 * @return {Array} array of the selected cells
2992 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2995 var row
= this.date_graph
.getSelection();
2997 if (row
< 0) return selection
;
3000 for (var i
in this.date_graph
.layout_
.datasets
) {
3001 selection
.push({row
: row
, column
: col
});
3008 // Older pages may still use this name.
3009 DateGraph
= Dygraph
;