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 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRange());
1094 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1095 * the canvas. This function redraws the graph.
1097 * @param {Number} lowY The topmost pixel value that should be visible.
1098 * @param {Number} highY The lowest pixel value that should be visible.
1101 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1102 // Find the highest and lowest values in pixel range for each axis.
1103 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1104 // This is because pixels increase as you go down on the screen, whereas data
1105 // coordinates increase as you go up the screen.
1106 var valueRanges
= [];
1107 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1108 var hi
= this.toDataCoords(null, lowY
, i
);
1109 var low
= this.toDataCoords(null, highY
, i
);
1110 this.axes_
[i
].valueWindow
= [low
[1], hi
[1]];
1111 valueRanges
.push([low
[1], hi
[1]]);
1115 if (this.attr_("zoomCallback")) {
1116 var xRange
= this.xAxisRange();
1117 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1122 * Reset the zoom to the original view coordinates. This is the same as
1123 * double-clicking on the graph.
1127 Dygraph
.prototype.doUnzoom_
= function() {
1129 if (this.dateWindow_
!= null) {
1131 this.dateWindow_
= null;
1134 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1135 if (this.axes_
[i
].valueWindow
!= null) {
1137 delete this.axes_
[i
].valueWindow
;
1142 // Putting the drawing operation before the callback because it resets
1145 if (this.attr_("zoomCallback")) {
1146 var minDate
= this.rawData_
[0][0];
1147 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1148 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1154 * When the mouse moves in the canvas, display information about a nearby data
1155 * point and draw dots over those points in the data series. This function
1156 * takes care of cleanup of previously-drawn dots.
1157 * @param {Object} event The mousemove event from the browser.
1160 Dygraph
.prototype.mouseMove_
= function(event
) {
1161 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1162 var points
= this.layout_
.points
;
1167 // Loop through all the points and find the date nearest to our current
1169 var minDist
= 1e+100;
1171 for (var i
= 0; i
< points
.length
; i
++) {
1172 var point
= points
[i
];
1173 if (point
== null) continue;
1174 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1175 if (dist
> minDist
) continue;
1179 if (idx
>= 0) lastx
= points
[idx
].xval
;
1180 // Check that you can really highlight the last day's data
1181 var last
= points
[points
.length
-1];
1182 if (last
!= null && canvasx
> last
.canvasx
)
1183 lastx
= points
[points
.length
-1].xval
;
1185 // Extract the points we've selected
1186 this.selPoints_
= [];
1187 var l
= points
.length
;
1188 if (!this.attr_("stackedGraph")) {
1189 for (var i
= 0; i
< l
; i
++) {
1190 if (points
[i
].xval
== lastx
) {
1191 this.selPoints_
.push(points
[i
]);
1195 // Need to 'unstack' points starting from the bottom
1196 var cumulative_sum
= 0;
1197 for (var i
= l
- 1; i
>= 0; i
--) {
1198 if (points
[i
].xval
== lastx
) {
1199 var p
= {}; // Clone the point since we modify it
1200 for (var k
in points
[i
]) {
1201 p
[k
] = points
[i
][k
];
1203 p
.yval
-= cumulative_sum
;
1204 cumulative_sum
+= p
.yval
;
1205 this.selPoints_
.push(p
);
1208 this.selPoints_
.reverse();
1211 if (this.attr_("highlightCallback")) {
1212 var px
= this.lastx_
;
1213 if (px
!== null && lastx
!= px
) {
1214 // only fire if the selected point has changed.
1215 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1219 // Save last x position for callbacks.
1220 this.lastx_
= lastx
;
1222 this.updateSelection_();
1226 * Transforms layout_.points index into data row number.
1227 * @param int layout_.points index
1228 * @return int row number, or -1 if none could be found.
1231 Dygraph
.prototype.idxToRow_
= function(idx
) {
1232 if (idx
< 0) return -1;
1234 for (var i
in this.layout_
.datasets
) {
1235 if (idx
< this.layout_
.datasets
[i
].length
) {
1236 return this.boundaryIds_
[0][0]+idx
;
1238 idx
-= this.layout_
.datasets
[i
].length
;
1244 * Draw dots over the selectied points in the data series. This function
1245 * takes care of cleanup of previously-drawn dots.
1248 Dygraph
.prototype.updateSelection_
= function() {
1249 // Clear the previously drawn vertical, if there is one
1250 var ctx
= this.canvas_
.getContext("2d");
1251 if (this.previousVerticalX_
>= 0) {
1252 // Determine the maximum highlight circle size.
1253 var maxCircleSize
= 0;
1254 var labels
= this.attr_('labels');
1255 for (var i
= 1; i
< labels
.length
; i
++) {
1256 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1257 if (r
> maxCircleSize
) maxCircleSize
= r
;
1259 var px
= this.previousVerticalX_
;
1260 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1261 2 * maxCircleSize
+ 2, this.height_
);
1264 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1266 if (this.selPoints_
.length
> 0) {
1267 var canvasx
= this.selPoints_
[0].canvasx
;
1269 // Set the status message to indicate the selected point(s)
1270 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1271 var fmtFunc
= this.attr_('yValueFormatter');
1272 var clen
= this.colors_
.length
;
1274 if (this.attr_('showLabelsOnHighlight')) {
1275 // Set the status message to indicate the selected point(s)
1276 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1277 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1278 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1279 if (this.attr_("labelsSeparateLines")) {
1282 var point
= this.selPoints_
[i
];
1283 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1284 var yval
= fmtFunc(point
.yval
);
1285 replace
+= " <b><font color='" + c
.toHex() + "'>"
1286 + point
.name
+ "</font></b>:"
1290 this.attr_("labelsDiv").innerHTML
= replace
;
1293 // Draw colored circles over the center of each selected point
1295 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1296 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1298 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1300 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1301 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1302 0, 2 * Math
.PI
, false);
1307 this.previousVerticalX_
= canvasx
;
1312 * Set manually set selected dots, and display information about them
1313 * @param int row number that should by highlighted
1314 * false value clears the selection
1317 Dygraph
.prototype.setSelection
= function(row
) {
1318 // Extract the points we've selected
1319 this.selPoints_
= [];
1322 if (row
!== false) {
1323 row
= row
-this.boundaryIds_
[0][0];
1326 if (row
!== false && row
>= 0) {
1327 for (var i
in this.layout_
.datasets
) {
1328 if (row
< this.layout_
.datasets
[i
].length
) {
1329 var point
= this.layout_
.points
[pos
+row
];
1331 if (this.attr_("stackedGraph")) {
1332 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1335 this.selPoints_
.push(point
);
1337 pos
+= this.layout_
.datasets
[i
].length
;
1341 if (this.selPoints_
.length
) {
1342 this.lastx_
= this.selPoints_
[0].xval
;
1343 this.updateSelection_();
1346 this.clearSelection();
1352 * The mouse has left the canvas. Clear out whatever artifacts remain
1353 * @param {Object} event the mouseout event from the browser.
1356 Dygraph
.prototype.mouseOut_
= function(event
) {
1357 if (this.attr_("unhighlightCallback")) {
1358 this.attr_("unhighlightCallback")(event
);
1361 if (this.attr_("hideOverlayOnMouseOut")) {
1362 this.clearSelection();
1367 * Remove all selection from the canvas
1370 Dygraph
.prototype.clearSelection
= function() {
1371 // Get rid of the overlay data
1372 var ctx
= this.canvas_
.getContext("2d");
1373 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1374 this.attr_("labelsDiv").innerHTML
= "";
1375 this.selPoints_
= [];
1380 * Returns the number of the currently selected row
1381 * @return int row number, of -1 if nothing is selected
1384 Dygraph
.prototype.getSelection
= function() {
1385 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1389 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1390 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1391 return row
+ this.boundaryIds_
[0][0];
1397 Dygraph
.zeropad
= function(x
) {
1398 if (x
< 10) return "0" + x
; else return "" + x
;
1402 * Return a string version of the hours, minutes and seconds portion of a date.
1403 * @param {Number} date The JavaScript date (ms since epoch)
1404 * @return {String} A time of the form "HH:MM:SS"
1407 Dygraph
.hmsString_
= function(date
) {
1408 var zeropad
= Dygraph
.zeropad
;
1409 var d
= new Date(date
);
1410 if (d
.getSeconds()) {
1411 return zeropad(d
.getHours()) + ":" +
1412 zeropad(d
.getMinutes()) + ":" +
1413 zeropad(d
.getSeconds());
1415 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1420 * Convert a JS date to a string appropriate to display on an axis that
1421 * is displaying values at the stated granularity.
1422 * @param {Date} date The date to format
1423 * @param {Number} granularity One of the Dygraph granularity constants
1424 * @return {String} The formatted date
1427 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1428 if (granularity
>= Dygraph
.MONTHLY
) {
1429 return date
.strftime('%b %y');
1431 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1432 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1433 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1435 return Dygraph
.hmsString_(date
.getTime());
1441 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1442 * @param {Number} date The JavaScript date (ms since epoch)
1443 * @return {String} A date of the form "YYYY/MM/DD"
1446 Dygraph
.dateString_
= function(date
, self
) {
1447 var zeropad
= Dygraph
.zeropad
;
1448 var d
= new Date(date
);
1451 var year
= "" + d
.getFullYear();
1452 // Get a 0 padded month string
1453 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1454 // Get a 0 padded day string
1455 var day
= zeropad(d
.getDate());
1458 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1459 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1461 return year
+ "/" + month + "/" + day
+ ret
;
1465 * Round a number to the specified number of digits past the decimal point.
1466 * @param {Number} num The number to round
1467 * @param {Number} places The number of decimals to which to round
1468 * @return {Number} The rounded number
1471 Dygraph
.round_
= function(num
, places
) {
1472 var shift
= Math
.pow(10, places
);
1473 return Math
.round(num
* shift
)/shift
;
1477 * Fires when there's data available to be graphed.
1478 * @param {String} data Raw CSV data to be plotted
1481 Dygraph
.prototype.loadedEvent_
= function(data
) {
1482 this.rawData_
= this.parseCSV_(data
);
1486 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1487 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1488 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1491 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1494 Dygraph
.prototype.addXTicks_
= function() {
1495 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1496 var startDate
, endDate
;
1497 if (this.dateWindow_
) {
1498 startDate
= this.dateWindow_
[0];
1499 endDate
= this.dateWindow_
[1];
1501 startDate
= this.rawData_
[0][0];
1502 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1505 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1506 this.layout_
.updateOptions({xTicks
: xTicks
});
1509 // Time granularity enumeration
1510 Dygraph
.SECONDLY
= 0;
1511 Dygraph
.TWO_SECONDLY
= 1;
1512 Dygraph
.FIVE_SECONDLY
= 2;
1513 Dygraph
.TEN_SECONDLY
= 3;
1514 Dygraph
.THIRTY_SECONDLY
= 4;
1515 Dygraph
.MINUTELY
= 5;
1516 Dygraph
.TWO_MINUTELY
= 6;
1517 Dygraph
.FIVE_MINUTELY
= 7;
1518 Dygraph
.TEN_MINUTELY
= 8;
1519 Dygraph
.THIRTY_MINUTELY
= 9;
1520 Dygraph
.HOURLY
= 10;
1521 Dygraph
.TWO_HOURLY
= 11;
1522 Dygraph
.SIX_HOURLY
= 12;
1524 Dygraph
.WEEKLY
= 14;
1525 Dygraph
.MONTHLY
= 15;
1526 Dygraph
.QUARTERLY
= 16;
1527 Dygraph
.BIANNUAL
= 17;
1528 Dygraph
.ANNUAL
= 18;
1529 Dygraph
.DECADAL
= 19;
1530 Dygraph
.NUM_GRANULARITIES
= 20;
1532 Dygraph
.SHORT_SPACINGS
= [];
1533 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1534 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1535 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1536 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1537 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1538 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1539 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1540 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1541 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1542 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1543 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1544 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1545 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1546 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1547 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1551 // If we used this time granularity, how many ticks would there be?
1552 // This is only an approximation, but it's generally good enough.
1554 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1555 if (granularity
< Dygraph
.MONTHLY
) {
1556 // Generate one tick mark for every fixed interval of time.
1557 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1558 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1560 var year_mod
= 1; // e.g. to only print one point every 10 years.
1561 var num_months
= 12;
1562 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1563 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1564 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1565 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1567 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1568 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1569 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1575 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1576 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1578 // Returns an array containing {v: millis, label: label} dictionaries.
1580 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1581 var formatter
= this.attr_("xAxisLabelFormatter");
1583 if (granularity
< Dygraph
.MONTHLY
) {
1584 // Generate one tick mark for every fixed interval of time.
1585 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1586 var format
= '%d%b'; // e.g. "1Jan"
1588 // Find a time less than start_time which occurs on a "nice" time boundary
1589 // for this granularity.
1590 var g
= spacing
/ 1000;
1591 var d
= new Date(start_time
);
1592 if (g
<= 60) { // seconds
1593 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1597 if (g
<= 60) { // minutes
1598 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1603 if (g
<= 24) { // days
1604 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1609 if (g
== 7) { // one week
1610 d
.setDate(d
.getDate() - d
.getDay());
1615 start_time
= d
.getTime();
1617 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1618 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1621 // Display a tick mark on the first of a set of months of each year.
1622 // Years get a tick mark iff y % year_mod == 0. This is useful for
1623 // displaying a tick mark once every 10 years, say, on long time scales.
1625 var year_mod
= 1; // e.g. to only print one point every 10 years.
1627 if (granularity
== Dygraph
.MONTHLY
) {
1628 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1629 } else if (granularity
== Dygraph
.QUARTERLY
) {
1630 months
= [ 0, 3, 6, 9 ];
1631 } else if (granularity
== Dygraph
.BIANNUAL
) {
1633 } else if (granularity
== Dygraph
.ANNUAL
) {
1635 } else if (granularity
== Dygraph
.DECADAL
) {
1640 var start_year
= new Date(start_time
).getFullYear();
1641 var end_year
= new Date(end_time
).getFullYear();
1642 var zeropad
= Dygraph
.zeropad
;
1643 for (var i
= start_year
; i
<= end_year
; i
++) {
1644 if (i
% year_mod
!= 0) continue;
1645 for (var j
= 0; j
< months
.length
; j
++) {
1646 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1647 var t
= Date
.parse(date_str
);
1648 if (t
< start_time
|| t
> end_time
) continue;
1649 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1659 * Add ticks to the x-axis based on a date range.
1660 * @param {Number} startDate Start of the date window (millis since epoch)
1661 * @param {Number} endDate End of the date window (millis since epoch)
1662 * @return {Array.<Object>} Array of {label, value} tuples.
1665 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1667 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1668 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1669 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1676 return self
.GetXAxis(startDate
, endDate
, chosen
);
1678 // TODO(danvk): signal error.
1683 * Add ticks when the x axis has numbers on it (instead of dates)
1684 * @param {Number} startDate Start of the date window (millis since epoch)
1685 * @param {Number} endDate End of the date window (millis since epoch)
1687 * @param {function} attribute accessor function.
1688 * @return {Array.<Object>} Array of {label, value} tuples.
1691 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1692 var attr
= function(k
) {
1693 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1694 return self
.attr_(k
);
1699 for (var i
= 0; i
< vals
.length
; i
++) {
1700 ticks
.push({v
: vals
[i
]});
1704 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1705 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1706 // The first spacing greater than pixelsPerYLabel is what we use.
1707 // TODO(danvk): version that works on a log scale.
1708 if (attr("labelsKMG2")) {
1709 var mults
= [1, 2, 4, 8];
1711 var mults
= [1, 2, 5];
1713 var scale
, low_val
, high_val
, nTicks
;
1714 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1715 var pixelsPerTick
= attr('pixelsPerYLabel');
1716 for (var i
= -10; i
< 50; i
++) {
1717 if (attr("labelsKMG2")) {
1718 var base_scale
= Math
.pow(16, i
);
1720 var base_scale
= Math
.pow(10, i
);
1722 for (var j
= 0; j
< mults
.length
; j
++) {
1723 scale
= base_scale
* mults
[j
];
1724 low_val
= Math
.floor(minV
/ scale
) * scale
;
1725 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1726 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1727 var spacing
= self
.height_
/ nTicks
;
1728 // wish I could break out of both loops at once...
1729 if (spacing
> pixelsPerTick
) break;
1731 if (spacing
> pixelsPerTick
) break;
1734 // Construct the set of ticks.
1735 // Allow reverse y-axis if it's explicitly requested.
1736 if (low_val
> high_val
) scale
*= -1;
1737 for (var i
= 0; i
< nTicks
; i
++) {
1738 var tickV
= low_val
+ i
* scale
;
1739 ticks
.push( {v
: tickV
} );
1743 // Add formatted labels to the ticks.
1746 if (attr("labelsKMB")) {
1748 k_labels
= [ "K", "M", "B", "T" ];
1750 if (attr("labelsKMG2")) {
1751 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1753 k_labels
= [ "k", "M", "G", "T" ];
1755 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
1757 for (var i
= 0; i
< ticks
.length
; i
++) {
1758 var tickV
= ticks
[i
].v
;
1759 var absTickV
= Math
.abs(tickV
);
1761 if (formatter
!= undefined
) {
1762 label
= formatter(tickV
);
1764 label
= Dygraph
.round_(tickV
, 2);
1766 if (k_labels
.length
) {
1767 // Round up to an appropriate unit.
1769 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1770 if (absTickV
>= n
) {
1771 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1776 ticks
[i
].label
= label
;
1781 // Computes the range of the data series (including confidence intervals).
1782 // series is either [ [x1, y1], [x2, y2], ... ] or
1783 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1784 // Returns [low, high]
1785 Dygraph
.prototype.extremeValues_
= function(series
) {
1786 var minY
= null, maxY
= null;
1788 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1790 // With custom bars, maxY is the max of the high values.
1791 for (var j
= 0; j
< series
.length
; j
++) {
1792 var y
= series
[j
][1][0];
1794 var low
= y
- series
[j
][1][1];
1795 var high
= y
+ series
[j
][1][2];
1796 if (low
> y
) low
= y
; // this can happen with custom bars,
1797 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1798 if (maxY
== null || high
> maxY
) {
1801 if (minY
== null || low
< minY
) {
1806 for (var j
= 0; j
< series
.length
; j
++) {
1807 var y
= series
[j
][1];
1808 if (y
=== null || isNaN(y
)) continue;
1809 if (maxY
== null || y
> maxY
) {
1812 if (minY
== null || y
< minY
) {
1818 return [minY
, maxY
];
1822 * This function is called once when the chart's data is changed or the options
1823 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1824 * idea is that values derived from the chart's data can be computed here,
1825 * rather than every time the chart is drawn. This includes things like the
1826 * number of axes, rolling averages, etc.
1828 Dygraph
.prototype.predraw_
= function() {
1829 // TODO(danvk): move more computations out of drawGraph_ and into here.
1830 this.computeYAxes_();
1832 // Create a new plotter.
1833 if (this.plotter_
) this.plotter_
.clear();
1834 this.plotter_
= new DygraphCanvasRenderer(this,
1835 this.hidden_
, this.layout_
,
1836 this.renderOptions_
);
1838 // The roller sits in the bottom left corner of the chart. We don't know where
1839 // this will be until the options are available, so it's positioned here.
1840 this.createRollInterface_();
1842 // Same thing applies for the labelsDiv. It's right edge should be flush with
1843 // the right edge of the charting area (which may not be the same as the right
1844 // edge of the div, if we have two y-axes.
1845 this.positionLabelsDiv_();
1847 // If the data or options have changed, then we'd better redraw.
1853 * Update the graph with new data. This method is called when the viewing area
1854 * has changed. If the underlying data or options have changed, predraw_ will
1855 * be called before drawGraph_ is called.
1858 Dygraph
.prototype.drawGraph_
= function() {
1859 var data
= this.rawData_
;
1861 // This is used to set the second parameter to drawCallback, below.
1862 var is_initial_draw
= this.is_initial_draw_
;
1863 this.is_initial_draw_
= false;
1865 var minY
= null, maxY
= null;
1866 this.layout_
.removeAllDatasets();
1868 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1870 // Loop over the fields (series). Go from the last to the first,
1871 // because if they're stacked that's how we accumulate the values.
1873 var cumulative_y
= []; // For stacked series.
1876 var extremes
= {}; // series name -> [low, high]
1878 // Loop over all fields and create datasets
1879 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1880 if (!this.visibility()[i
- 1]) continue;
1882 var seriesName
= this.attr_("labels")[i
];
1883 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1886 for (var j
= 0; j
< data
.length
; j
++) {
1887 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1888 var date
= data
[j
][0];
1889 series
.push([date
, data
[j
][i
]]);
1893 // TODO(danvk): move this into predraw_. It's insane to do it here.
1894 series
= this.rollingAverage(series
, this.rollPeriod_
);
1896 // Prune down to the desired range, if necessary (for zooming)
1897 // Because there can be lines going to points outside of the visible area,
1898 // we actually prune to visible points, plus one on either side.
1899 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1900 if (this.dateWindow_
) {
1901 var low
= this.dateWindow_
[0];
1902 var high
= this.dateWindow_
[1];
1904 // TODO(danvk): do binary search instead of linear search.
1905 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1906 var firstIdx
= null, lastIdx
= null;
1907 for (var k
= 0; k
< series
.length
; k
++) {
1908 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1911 if (series
[k
][0] <= high
) {
1915 if (firstIdx
=== null) firstIdx
= 0;
1916 if (firstIdx
> 0) firstIdx
--;
1917 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1918 if (lastIdx
< series
.length
- 1) lastIdx
++;
1919 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1920 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1921 pruned
.push(series
[k
]);
1925 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1928 var seriesExtremes
= this.extremeValues_(series
);
1931 for (var j
=0; j
<series
.length
; j
++) {
1932 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1935 } else if (this.attr_("stackedGraph")) {
1936 var l
= series
.length
;
1938 for (var j
= 0; j
< l
; j
++) {
1939 // If one data set has a NaN, let all subsequent stacked
1940 // sets inherit the NaN -- only start at 0 for the first set.
1941 var x
= series
[j
][0];
1942 if (cumulative_y
[x
] === undefined
) {
1943 cumulative_y
[x
] = 0;
1946 actual_y
= series
[j
][1];
1947 cumulative_y
[x
] += actual_y
;
1949 series
[j
] = [x
, cumulative_y
[x
]]
1951 if (cumulative_y
[x
] > seriesExtremes
[1]) {
1952 seriesExtremes
[1] = cumulative_y
[x
];
1954 if (cumulative_y
[x
] < seriesExtremes
[0]) {
1955 seriesExtremes
[0] = cumulative_y
[x
];
1959 extremes
[seriesName
] = seriesExtremes
;
1961 datasets
[i
] = series
;
1964 for (var i
= 1; i
< datasets
.length
; i
++) {
1965 if (!this.visibility()[i
- 1]) continue;
1966 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1969 // TODO(danvk): this method doesn't need to return anything.
1970 var out
= this.computeYAxisRanges_(extremes
);
1972 var seriesToAxisMap
= out
[1];
1973 this.layout_
.updateOptions( { yAxes
: axes
,
1974 seriesToAxisMap
: seriesToAxisMap
1979 // Tell PlotKit to use this new data and render itself
1980 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1981 this.layout_
.evaluateWithError();
1982 this.plotter_
.clear();
1983 this.plotter_
.render();
1984 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1985 this.canvas_
.height
);
1987 if (this.attr_("drawCallback") !== null) {
1988 this.attr_("drawCallback")(this, is_initial_draw
);
1993 * Determine properties of the y-axes which are independent of the data
1994 * currently being displayed. This includes things like the number of axes and
1995 * the style of the axes. It does not include the range of each axis and its
1997 * This fills in this.axes_ and this.seriesToAxisMap_.
1998 * axes_ = [ { options } ]
1999 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2000 * indices are into the axes_ array.
2002 Dygraph
.prototype.computeYAxes_
= function() {
2003 this.axes_
= [{}]; // always have at least one y-axis.
2004 this.seriesToAxisMap_
= {};
2006 // Get a list of series names.
2007 var labels
= this.attr_("labels");
2009 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2011 // all options which could be applied per-axis:
2019 'axisLabelFontSize',
2023 // Copy global axis options over to the first axis.
2024 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2025 var k
= axisOptions
[i
];
2026 var v
= this.attr_(k
);
2027 if (v
) this.axes_
[0][k
] = v
;
2030 // Go through once and add all the axes.
2031 for (var seriesName
in series
) {
2032 if (!series
.hasOwnProperty(seriesName
)) continue;
2033 var axis
= this.attr_("axis", seriesName
);
2035 this.seriesToAxisMap_
[seriesName
] = 0;
2038 if (typeof(axis
) == 'object') {
2039 // Add a new axis, making a copy of its per-axis options.
2041 Dygraph
.update(opts
, this.axes_
[0]);
2042 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2043 Dygraph
.update(opts
, axis
);
2044 this.axes_
.push(opts
);
2045 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2049 // Go through one more time and assign series to an axis defined by another
2050 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2051 for (var seriesName
in series
) {
2052 if (!series
.hasOwnProperty(seriesName
)) continue;
2053 var axis
= this.attr_("axis", seriesName
);
2054 if (typeof(axis
) == 'string') {
2055 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2056 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2057 "series " + axis
+ ", which does not define its own axis.");
2060 var idx
= this.seriesToAxisMap_
[axis
];
2061 this.seriesToAxisMap_
[seriesName
] = idx
;
2065 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2066 // this last so that hiding the first series doesn't destroy the axis
2067 // properties of the primary axis.
2068 var seriesToAxisFiltered
= {};
2069 var vis
= this.visibility();
2070 for (var i
= 1; i
< labels
.length
; i
++) {
2072 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2074 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2078 * Returns the number of y-axes on the chart.
2079 * @return {Number} the number of axes.
2081 Dygraph
.prototype.numAxes
= function() {
2083 for (var series
in this.seriesToAxisMap_
) {
2084 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2085 var idx
= this.seriesToAxisMap_
[series
];
2086 if (idx
> last_axis
) last_axis
= idx
;
2088 return 1 + last_axis
;
2092 * Determine the value range and tick marks for each axis.
2093 * @param {Object} extremes A mapping from seriesName -> [low, high]
2094 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2096 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2097 // Build a map from axis number -> [list of series names]
2098 var seriesForAxis
= [];
2099 for (var series
in this.seriesToAxisMap_
) {
2100 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2101 var idx
= this.seriesToAxisMap_
[series
];
2102 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2103 seriesForAxis
[idx
].push(series
);
2106 // Compute extreme values, a span and tick marks for each axis.
2107 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2108 var axis
= this.axes_
[i
];
2109 if (axis
.valueWindow
) {
2110 // This is only set if the user has zoomed on the y-axis. It is never set
2111 // by a user. It takes precedence over axis.valueRange because, if you set
2112 // valueRange, you'd still expect to be able to pan.
2113 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2114 } else if (axis
.valueRange
) {
2115 // This is a user-set value range for this axis.
2116 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2118 // Calculate the extremes of extremes.
2119 var series
= seriesForAxis
[i
];
2120 var minY
= Infinity
; // extremes[series[0]][0];
2121 var maxY
= -Infinity
; // extremes[series[0]][1];
2122 for (var j
= 0; j
< series
.length
; j
++) {
2123 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2124 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2126 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2128 // Add some padding and round up to an integer to be human-friendly.
2129 var span
= maxY
- minY
;
2130 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2131 if (span
== 0) { span
= maxY
; }
2132 var maxAxisY
= maxY
+ 0.1 * span
;
2133 var minAxisY
= minY
- 0.1 * span
;
2135 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2136 if (!this.attr_("avoidMinZero")) {
2137 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2138 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2141 if (this.attr_("includeZero")) {
2142 if (maxY
< 0) maxAxisY
= 0;
2143 if (minY
> 0) minAxisY
= 0;
2146 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2149 // Add ticks. By default, all axes inherit the tick positions of the
2150 // primary axis. However, if an axis is specifically marked as having
2151 // independent ticks, then that is permissible as well.
2152 if (i
== 0 || axis
.independentTicks
) {
2154 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2155 axis
.computedValueRange
[1],
2159 var p_axis
= this.axes_
[0];
2160 var p_ticks
= p_axis
.ticks
;
2161 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2162 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2163 var tick_values
= [];
2164 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2165 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2166 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2167 tick_values
.push(y_val
);
2171 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2172 axis
.computedValueRange
[1],
2173 this, axis
, tick_values
);
2177 return [this.axes_
, this.seriesToAxisMap_
];
2181 * Calculates the rolling average of a data set.
2182 * If originalData is [label, val], rolls the average of those.
2183 * If originalData is [label, [, it's interpreted as [value, stddev]
2184 * and the roll is returned in the same form, with appropriately reduced
2185 * stddev for each value.
2186 * Note that this is where fractional input (i.e. '5/10') is converted into
2188 * @param {Array} originalData The data in the appropriate format (see above)
2189 * @param {Number} rollPeriod The number of days over which to average the data
2191 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2192 if (originalData
.length
< 2)
2193 return originalData
;
2194 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2195 var rollingData
= [];
2196 var sigma
= this.attr_("sigma");
2198 if (this.fractions_
) {
2200 var den
= 0; // numerator/denominator
2202 for (var i
= 0; i
< originalData
.length
; i
++) {
2203 num
+= originalData
[i
][1][0];
2204 den
+= originalData
[i
][1][1];
2205 if (i
- rollPeriod
>= 0) {
2206 num
-= originalData
[i
- rollPeriod
][1][0];
2207 den
-= originalData
[i
- rollPeriod
][1][1];
2210 var date
= originalData
[i
][0];
2211 var value
= den
? num
/ den
: 0.0;
2212 if (this.attr_("errorBars")) {
2213 if (this.wilsonInterval_
) {
2214 // For more details on this confidence interval, see:
2215 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2217 var p
= value
< 0 ? 0 : value
, n
= den
;
2218 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2219 var denom
= 1 + sigma
* sigma
/ den
;
2220 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2221 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2222 rollingData
[i
] = [date
,
2223 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2225 rollingData
[i
] = [date
, [0, 0, 0]];
2228 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2229 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2232 rollingData
[i
] = [date
, mult
* value
];
2235 } else if (this.attr_("customBars")) {
2240 for (var i
= 0; i
< originalData
.length
; i
++) {
2241 var data
= originalData
[i
][1];
2243 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2245 if (y
!= null && !isNaN(y
)) {
2251 if (i
- rollPeriod
>= 0) {
2252 var prev
= originalData
[i
- rollPeriod
];
2253 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2260 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2261 1.0 * (mid
- low
) / count
,
2262 1.0 * (high
- mid
) / count
]];
2265 // Calculate the rolling average for the first rollPeriod - 1 points where
2266 // there is not enough data to roll over the full number of days
2267 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2268 if (!this.attr_("errorBars")){
2269 if (rollPeriod
== 1) {
2270 return originalData
;
2273 for (var i
= 0; i
< originalData
.length
; i
++) {
2276 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2277 var y
= originalData
[j
][1];
2278 if (y
== null || isNaN(y
)) continue;
2280 sum
+= originalData
[j
][1];
2283 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2285 rollingData
[i
] = [originalData
[i
][0], null];
2290 for (var i
= 0; i
< originalData
.length
; i
++) {
2294 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2295 var y
= originalData
[j
][1][0];
2296 if (y
== null || isNaN(y
)) continue;
2298 sum
+= originalData
[j
][1][0];
2299 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2302 var stddev
= Math
.sqrt(variance
) / num_ok
;
2303 rollingData
[i
] = [originalData
[i
][0],
2304 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2306 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2316 * Parses a date, returning the number of milliseconds since epoch. This can be
2317 * passed in as an xValueParser in the Dygraph constructor.
2318 * TODO(danvk): enumerate formats that this understands.
2319 * @param {String} A date in YYYYMMDD format.
2320 * @return {Number} Milliseconds since epoch.
2323 Dygraph
.dateParser
= function(dateStr
, self
) {
2326 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2327 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2328 while (dateStrSlashed
.search("-") != -1) {
2329 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2331 d
= Date
.parse(dateStrSlashed
);
2332 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2333 // TODO(danvk): remove support for this format. It's confusing.
2334 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2335 + "/" + dateStr
.substr(6,2);
2336 d
= Date
.parse(dateStrSlashed
);
2338 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2339 // "2009/07/12 12:34:56"
2340 d
= Date
.parse(dateStr
);
2343 if (!d
|| isNaN(d
)) {
2344 self
.error("Couldn't parse " + dateStr
+ " as a date");
2350 * Detects the type of the str (date or numeric) and sets the various
2351 * formatting attributes in this.attrs_ based on this type.
2352 * @param {String} str An x value.
2355 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2357 if (str
.indexOf('-') >= 0 ||
2358 str
.indexOf('/') >= 0 ||
2359 isNaN(parseFloat(str
))) {
2361 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2362 // TODO(danvk): remove support for this format.
2367 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2368 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2369 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2370 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2372 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2373 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2374 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2375 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2380 * Parses a string in a special csv format. We expect a csv file where each
2381 * line is a date point, and the first field in each line is the date string.
2382 * We also expect that all remaining fields represent series.
2383 * if the errorBars attribute is set, then interpret the fields as:
2384 * date, series1, stddev1, series2, stddev2, ...
2385 * @param {Array.<Object>} data See above.
2388 * @return Array.<Object> An array with one entry for each row. These entries
2389 * are an array of cells in that row. The first entry is the parsed x-value for
2390 * the row. The second, third, etc. are the y-values. These can take on one of
2391 * three forms, depending on the CSV and constructor parameters:
2393 * 2. [ value, stddev ]
2394 * 3. [ low value, center value, high value ]
2396 Dygraph
.prototype.parseCSV_
= function(data
) {
2398 var lines
= data
.split("\n");
2400 // Use the default delimiter or fall back to a tab if that makes sense.
2401 var delim
= this.attr_('delimiter');
2402 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2407 if (this.labelsFromCSV_
) {
2409 this.attrs_
.labels
= lines
[0].split(delim
);
2412 // Parse the x as a float or return null if it's not a number.
2413 var parseFloatOrNull
= function(x
) {
2414 var val
= parseFloat(x
);
2415 return isNaN(val
) ? null : val
;
2419 var defaultParserSet
= false; // attempt to auto-detect x value type
2420 var expectedCols
= this.attr_("labels").length
;
2421 var outOfOrder
= false;
2422 for (var i
= start
; i
< lines
.length
; i
++) {
2423 var line
= lines
[i
];
2424 if (line
.length
== 0) continue; // skip blank lines
2425 if (line
[0] == '#') continue; // skip comment lines
2426 var inFields
= line
.split(delim
);
2427 if (inFields
.length
< 2) continue;
2430 if (!defaultParserSet
) {
2431 this.detectTypeFromString_(inFields
[0]);
2432 xParser
= this.attr_("xValueParser");
2433 defaultParserSet
= true;
2435 fields
[0] = xParser(inFields
[0], this);
2437 // If fractions are expected, parse the numbers as "A/B
"
2438 if (this.fractions_) {
2439 for (var j = 1; j < inFields.length; j++) {
2440 // TODO(danvk): figure out an appropriate way to flag parse errors.
2441 var vals = inFields[j].split("/");
2442 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2444 } else if (this.attr_("errorBars
")) {
2445 // If there are error bars, values are (value, stddev) pairs
2446 for (var j = 1; j < inFields.length; j += 2)
2447 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2448 parseFloatOrNull(inFields[j + 1])];
2449 } else if (this.attr_("customBars
")) {
2450 // Bars are a low;center;high tuple
2451 for (var j = 1; j < inFields.length; j++) {
2452 var vals = inFields[j].split(";");
2453 fields[j] = [ parseFloatOrNull(vals[0]),
2454 parseFloatOrNull(vals[1]),
2455 parseFloatOrNull(vals[2]) ];
2458 // Values are just numbers
2459 for (var j = 1; j < inFields.length; j++) {
2460 fields[j] = parseFloatOrNull(inFields[j]);
2463 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2468 if (fields.length != expectedCols) {
2469 this.error("Number of columns
in line
" + i + " (" + fields.length +
2470 ") does not agree
with number of
labels (" + expectedCols +
2476 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2477 ret.sort(function(a,b) { return a[0] - b[0] });
2484 * The user has provided their data as a pre-packaged JS array. If the x values
2485 * are numeric, this is the same as dygraphs' internal format. If the x values
2486 * are dates, we need to convert them from Date objects to ms since epoch.
2487 * @param {Array.<Object>} data
2488 * @return {Array.<Object>} data with numeric x values.
2490 Dygraph.prototype.parseArray_ = function(data) {
2491 // Peek at the first x value to see if it's numeric.
2492 if (data.length == 0) {
2493 this.error("Can
't plot empty data set");
2496 if (data[0].length == 0) {
2497 this.error("Data set cannot contain an empty row");
2501 if (this.attr_("labels") == null) {
2502 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2503 "in the options parameter");
2504 this.attrs_.labels = [ "X" ];
2505 for (var i = 1; i < data[0].length; i++) {
2506 this.attrs_.labels.push("Y" + i);
2510 if (Dygraph.isDateLike(data[0][0])) {
2511 // Some intelligent defaults for a date x-axis.
2512 this.attrs_.xValueFormatter = Dygraph.dateString_;
2513 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2514 this.attrs_.xTicker = Dygraph.dateTicker;
2516 // Assume they're all dates
.
2517 var parsedData
= Dygraph
.clone(data
);
2518 for (var i
= 0; i
< data
.length
; i
++) {
2519 if (parsedData
[i
].length
== 0) {
2520 this.error("Row " + (1 + i
) + " of data is empty");
2523 if (parsedData
[i
][0] == null
2524 || typeof(parsedData
[i
][0].getTime
) != 'function'
2525 || isNaN(parsedData
[i
][0].getTime())) {
2526 this.error("x value in row " + (1 + i
) + " is not a Date");
2529 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2533 // Some intelligent defaults for a numeric x-axis.
2534 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2535 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2541 * Parses a DataTable object from gviz.
2542 * The data is expected to have a first column that is either a date or a
2543 * number. All subsequent columns must be numbers. If there is a clear mismatch
2544 * between this.xValueParser_ and the type of the first column, it will be
2545 * fixed. Fills out rawData_.
2546 * @param {Array.<Object>} data See above.
2549 Dygraph
.prototype.parseDataTable_
= function(data
) {
2550 var cols
= data
.getNumberOfColumns();
2551 var rows
= data
.getNumberOfRows();
2553 var indepType
= data
.getColumnType(0);
2554 if (indepType
== 'date' || indepType
== 'datetime') {
2555 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2556 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2557 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2558 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2559 } else if (indepType
== 'number') {
2560 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2561 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2562 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2563 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2565 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2566 "column 1 of DataTable input (Got '" + indepType
+ "')");
2570 // Array of the column indices which contain data (and not annotations).
2572 var annotationCols
= {}; // data index -> [annotation cols]
2573 var hasAnnotations
= false;
2574 for (var i
= 1; i
< cols
; i
++) {
2575 var type
= data
.getColumnType(i
);
2576 if (type
== 'number') {
2578 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2579 // This is OK -- it's an annotation column.
2580 var dataIdx
= colIdx
[colIdx
.length
- 1];
2581 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2582 annotationCols
[dataIdx
] = [i
];
2584 annotationCols
[dataIdx
].push(i
);
2586 hasAnnotations
= true;
2588 this.error("Only 'number' is supported as a dependent type with Gviz." +
2589 " 'string' is only supported if displayAnnotations is true");
2593 // Read column labels
2594 // TODO(danvk): add support back for errorBars
2595 var labels
= [data
.getColumnLabel(0)];
2596 for (var i
= 0; i
< colIdx
.length
; i
++) {
2597 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2598 if (this.attr_("errorBars")) i
+= 1;
2600 this.attrs_
.labels
= labels
;
2601 cols
= labels
.length
;
2604 var outOfOrder
= false;
2605 var annotations
= [];
2606 for (var i
= 0; i
< rows
; i
++) {
2608 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2609 data
.getValue(i
, 0) === null) {
2610 this.warn("Ignoring row " + i
+
2611 " of DataTable because of undefined or null first column.");
2615 if (indepType
== 'date' || indepType
== 'datetime') {
2616 row
.push(data
.getValue(i
, 0).getTime());
2618 row
.push(data
.getValue(i
, 0));
2620 if (!this.attr_("errorBars")) {
2621 for (var j
= 0; j
< colIdx
.length
; j
++) {
2622 var col
= colIdx
[j
];
2623 row
.push(data
.getValue(i
, col
));
2624 if (hasAnnotations
&&
2625 annotationCols
.hasOwnProperty(col
) &&
2626 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2628 ann
.series
= data
.getColumnLabel(col
);
2630 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2632 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2633 if (k
) ann
.text
+= "\n";
2634 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2636 annotations
.push(ann
);
2640 for (var j
= 0; j
< cols
- 1; j
++) {
2641 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2644 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2651 this.warn("DataTable is out of order; order it correctly to speed loading.");
2652 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2654 this.rawData_
= ret
;
2656 if (annotations
.length
> 0) {
2657 this.setAnnotations(annotations
, true);
2661 // These functions are all based on MochiKit.
2662 Dygraph
.update
= function (self
, o
) {
2663 if (typeof(o
) != 'undefined' && o
!== null) {
2665 if (o
.hasOwnProperty(k
)) {
2673 Dygraph
.isArrayLike
= function (o
) {
2674 var typ
= typeof(o
);
2676 (typ
!= 'object' && !(typ
== 'function' &&
2677 typeof(o
.item
) == 'function')) ||
2679 typeof(o
.length
) != 'number' ||
2687 Dygraph
.isDateLike
= function (o
) {
2688 if (typeof(o
) != "object" || o
=== null ||
2689 typeof(o
.getTime
) != 'function') {
2695 Dygraph
.clone
= function(o
) {
2696 // TODO(danvk): figure out how MochiKit's version works
2698 for (var i
= 0; i
< o
.length
; i
++) {
2699 if (Dygraph
.isArrayLike(o
[i
])) {
2700 r
.push(Dygraph
.clone(o
[i
]));
2710 * Get the CSV data. If it's in a function, call that function. If it's in a
2711 * file, do an XMLHttpRequest to get it.
2714 Dygraph
.prototype.start_
= function() {
2715 if (typeof this.file_
== 'function') {
2716 // CSV string. Pretend we got it via XHR.
2717 this.loadedEvent_(this.file_());
2718 } else if (Dygraph
.isArrayLike(this.file_
)) {
2719 this.rawData_
= this.parseArray_(this.file_
);
2721 } else if (typeof this.file_
== 'object' &&
2722 typeof this.file_
.getColumnRange
== 'function') {
2723 // must be a DataTable from gviz.
2724 this.parseDataTable_(this.file_
);
2726 } else if (typeof this.file_
== 'string') {
2727 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2728 if (this.file_
.indexOf('\n') >= 0) {
2729 this.loadedEvent_(this.file_
);
2731 var req
= new XMLHttpRequest();
2733 req
.onreadystatechange
= function () {
2734 if (req
.readyState
== 4) {
2735 if (req
.status
== 200) {
2736 caller
.loadedEvent_(req
.responseText
);
2741 req
.open("GET", this.file_
, true);
2745 this.error("Unknown data format: " + (typeof this.file_
));
2750 * Changes various properties of the graph. These can include:
2752 * <li>file: changes the source data for the graph</li>
2753 * <li>errorBars: changes whether the data contains stddev</li>
2755 * @param {Object} attrs The new properties and values
2757 Dygraph
.prototype.updateOptions
= function(attrs
) {
2758 // TODO(danvk): this is a mess. Rethink this function.
2759 if ('rollPeriod' in attrs
) {
2760 this.rollPeriod_
= attrs
.rollPeriod
;
2762 if ('dateWindow' in attrs
) {
2763 this.dateWindow_
= attrs
.dateWindow
;
2766 // TODO(danvk): validate per-series options.
2771 // highlightCircleSize
2773 Dygraph
.update(this.user_attrs_
, attrs
);
2774 Dygraph
.update(this.renderOptions_
, attrs
);
2776 this.labelsFromCSV_
= (this.attr_("labels") == null);
2778 // TODO(danvk): this doesn't match the constructor logic
2779 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2780 if (attrs
['file']) {
2781 this.file_
= attrs
['file'];
2789 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2790 * containing div (which has presumably changed size since the dygraph was
2791 * instantiated. If the width/height are specified, the div will be resized.
2793 * This is far more efficient than destroying and re-instantiating a
2794 * Dygraph, since it doesn't have to reparse the underlying data.
2796 * @param {Number} width Width (in pixels)
2797 * @param {Number} height Height (in pixels)
2799 Dygraph
.prototype.resize
= function(width
, height
) {
2800 if (this.resize_lock
) {
2803 this.resize_lock
= true;
2805 if ((width
=== null) != (height
=== null)) {
2806 this.warn("Dygraph.resize() should be called with zero parameters or " +
2807 "two non-NULL parameters. Pretending it was zero.");
2808 width
= height
= null;
2811 // TODO(danvk): there should be a clear() method.
2812 this.maindiv_
.innerHTML
= "";
2813 this.attrs_
.labelsDiv
= null;
2816 this.maindiv_
.style
.width
= width
+ "px";
2817 this.maindiv_
.style
.height
= height
+ "px";
2818 this.width_
= width
;
2819 this.height_
= height
;
2821 this.width_
= this.maindiv_
.offsetWidth
;
2822 this.height_
= this.maindiv_
.offsetHeight
;
2825 this.createInterface_();
2828 this.resize_lock
= false;
2832 * Adjusts the number of days in the rolling average. Updates the graph to
2833 * reflect the new averaging period.
2834 * @param {Number} length Number of days over which to average the data.
2836 Dygraph
.prototype.adjustRoll
= function(length
) {
2837 this.rollPeriod_
= length
;
2842 * Returns a boolean array of visibility statuses.
2844 Dygraph
.prototype.visibility
= function() {
2845 // Do lazy-initialization, so that this happens after we know the number of
2847 if (!this.attr_("visibility")) {
2848 this.attrs_
["visibility"] = [];
2850 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2851 this.attr_("visibility").push(true);
2853 return this.attr_("visibility");
2857 * Changes the visiblity of a series.
2859 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2860 var x
= this.visibility();
2861 if (num
< 0 || num
>= x
.length
) {
2862 this.warn("invalid series number in setVisibility: " + num
);
2870 * Update the list of annotations and redraw the chart.
2872 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2873 // Only add the annotation CSS rule once we know it will be used.
2874 Dygraph
.addAnnotationRule();
2875 this.annotations_
= ann
;
2876 this.layout_
.setAnnotations(this.annotations_
);
2877 if (!suppressDraw
) {
2883 * Return the list of annotations.
2885 Dygraph
.prototype.annotations
= function() {
2886 return this.annotations_
;
2890 * Get the index of a series (column) given its name. The first column is the
2891 * x-axis, so the data series start with index 1.
2893 Dygraph
.prototype.indexFromSetName
= function(name
) {
2894 var labels
= this.attr_("labels");
2895 for (var i
= 0; i
< labels
.length
; i
++) {
2896 if (labels
[i
] == name
) return i
;
2901 Dygraph
.addAnnotationRule
= function() {
2902 if (Dygraph
.addedAnnotationCSS
) return;
2904 var rule
= "border: 1px solid black; " +
2905 "background-color: white; " +
2906 "text-align: center;";
2908 var styleSheetElement
= document
.createElement("style");
2909 styleSheetElement
.type
= "text/css";
2910 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2912 // Find the first style sheet that we can access.
2913 // We may not add a rule to a style sheet from another domain for security
2914 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
2915 // adds its own style sheets from google.com.
2916 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
2917 if (document
.styleSheets
[i
].disabled
) continue;
2918 var mysheet
= document
.styleSheets
[i
];
2920 if (mysheet
.insertRule
) { // Firefox
2921 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2922 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2923 } else if (mysheet
.addRule
) { // IE
2924 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2926 Dygraph
.addedAnnotationCSS
= true;
2929 // Was likely a security exception.
2933 this.warn("Unable to add default annotation CSS rule; display may be off.");
2937 * Create a new canvas element. This is more complex than a simple
2938 * document.createElement("canvas") because of IE and excanvas.
2940 Dygraph
.createCanvas
= function() {
2941 var canvas
= document
.createElement("canvas");
2943 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2944 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
2945 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2953 * A wrapper around Dygraph that implements the gviz API.
2954 * @param {Object} container The DOM object the visualization should live in.
2956 Dygraph
.GVizChart
= function(container
) {
2957 this.container
= container
;
2960 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2961 // Clear out any existing dygraph.
2962 // TODO(danvk): would it make more sense to simply redraw using the current
2963 // date_graph object?
2964 this.container
.innerHTML
= '';
2965 if (typeof(this.date_graph
) != 'undefined') {
2966 this.date_graph
.destroy();
2969 this.date_graph
= new Dygraph(this.container
, data
, options
);
2973 * Google charts compatible setSelection
2974 * Only row selection is supported, all points in the row will be highlighted
2975 * @param {Array} array of the selected cells
2978 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2980 if (selection_array
.length
) {
2981 row
= selection_array
[0].row
;
2983 this.date_graph
.setSelection(row
);
2987 * Google charts compatible getSelection implementation
2988 * @return {Array} array of the selected cells
2991 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2994 var row
= this.date_graph
.getSelection();
2996 if (row
< 0) return selection
;
2999 for (var i
in this.date_graph
.layout_
.datasets
) {
3000 selection
.push({row
: row
, column
: col
});
3007 // Older pages may still use this name.
3008 DateGraph
= Dygraph
;