1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
6 * string. Dygraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. Dygraph uses the
8 * <canvas> tag, so it only works in FF1.5+.
9 * @author danvdk@gmail.com (Dan Vanderkam)
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
14 new Dygraph(document.getElementById("graphdiv"),
15 "datafile.csv", // CSV file with headers
19 The CSV file is of the form
21 Date,SeriesA,SeriesB,SeriesC
25 If the 'errorBars' option is set in the constructor, the input should be of
28 Date,SeriesA,SeriesB,...
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
32 If the 'fractions' option is set, the input should be of the form:
34 Date,SeriesA,SeriesB,...
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
38 And error bars will be calculated automatically using a binomial distribution.
40 For further documentation and examples, see http://www.danvk.org/dygraphs
45 * An interactive, zoomable graph
46 * @param {String | Function} file A file containing CSV data or a function that
47 * returns this data. The expected format for each line is
48 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
49 * YYYYMMDD,val1,stddev1,val2,stddev2,...
50 * @param {Object} attrs Various other attributes, e.g. errorBars determines
51 * whether the input data contains error ranges.
53 Dygraph
= function(div
, data
, opts
) {
54 if (arguments
.length
> 0) {
55 if (arguments
.length
== 4) {
56 // Old versions of dygraphs took in the series labels as a constructor
57 // parameter. This doesn't make sense anymore, but it's easy to continue
58 // to support this usage.
59 this.warn("Using deprecated four-argument dygraph constructor");
60 this.__old_init__(div
, data
, arguments
[2], arguments
[3]);
62 this.__init__(div
, data
, opts
);
67 Dygraph
.NAME
= "Dygraph";
68 Dygraph
.VERSION
= "1.2";
69 Dygraph
.__repr__
= function() {
70 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
72 Dygraph
.toString
= function() {
73 return this.__repr__();
76 // Various default values
77 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
78 Dygraph
.DEFAULT_WIDTH
= 480;
79 Dygraph
.DEFAULT_HEIGHT
= 320;
80 Dygraph
.AXIS_LINE_WIDTH
= 0.3;
82 // Default attribute values.
83 Dygraph
.DEFAULT_ATTRS
= {
84 highlightCircleSize
: 3,
90 // TODO(danvk): move defaults from createStatusMessage_ here.
92 labelsSeparateLines
: false,
93 labelsShowZeroValues
: true,
96 showLabelsOnHighlight
: true,
98 yValueFormatter
: function(x
) { return Dygraph
.round_(x
, 2); },
103 axisLabelFontSize
: 14,
106 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
110 xValueFormatter
: Dygraph
.dateString_
,
111 xValueParser
: Dygraph
.dateParser
,
112 xTicker
: Dygraph
.dateTicker
,
120 wilsonInterval
: true, // only relevant if fractions is true
124 connectSeparatedPoints
: false,
127 hideOverlayOnMouseOut
: true,
133 // Various logging levels.
139 // Directions for panning and zooming. Use bit operations when combined
140 // values are possible.
141 Dygraph
.HORIZONTAL
= 1;
142 Dygraph
.VERTICAL
= 2;
144 // Used for initializing annotation CSS rules only once.
145 Dygraph
.addedAnnotationCSS
= false;
147 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
148 // Labels is no longer a constructor parameter, since it's typically set
149 // directly from the data source. It also conains a name for the x-axis,
150 // which the previous constructor form did not.
151 if (labels
!= null) {
152 var new_labels
= ["Date"];
153 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
154 Dygraph
.update(attrs
, { 'labels': new_labels
});
156 this.__init__(div
, file
, attrs
);
160 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
161 * and interaction <canvas> inside of it. See the constructor for details
163 * @param {Element} div the Element to render the graph into.
164 * @param {String | Function} file Source data
165 * @param {Object} attrs Miscellaneous other options
168 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
169 // Support two-argument constructor
170 if (attrs
== null) { attrs
= {}; }
172 // Copy the important bits into the object
173 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
176 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
177 this.previousVerticalX_
= -1;
178 this.fractions_
= attrs
.fractions
|| false;
179 this.dateWindow_
= attrs
.dateWindow
|| null;
181 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
182 this.is_initial_draw_
= true;
183 this.annotations_
= [];
185 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
187 this.zoomedX
= false;
188 this.zoomedY
= false;
190 // Clear the div. This ensure that, if multiple dygraphs are passed the same
191 // div, then only one will be drawn.
194 // If the div isn't already sized then inherit from our attrs or
195 // give it a default size.
196 if (div
.style
.width
== '') {
197 div
.style
.width
= attrs
.width
|| Dygraph
.DEFAULT_WIDTH
+ "px";
199 if (div
.style
.height
== '') {
200 div
.style
.height
= attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
+ "px";
202 this.width_
= parseInt(div
.style
.width
, 10);
203 this.height_
= parseInt(div
.style
.height
, 10);
204 // The div might have been specified as percent of the current window size,
205 // convert that to an appropriate number of pixels.
206 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
207 this.width_
= div
.offsetWidth
;
209 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
210 this.height_
= div
.offsetHeight
;
213 if (this.width_
== 0) {
214 this.error("dygraph has zero width. Please specify a width in pixels.");
216 if (this.height_
== 0) {
217 this.error("dygraph has zero height. Please specify a height in pixels.");
220 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
221 if (attrs
['stackedGraph']) {
222 attrs
['fillGraph'] = true;
223 // TODO(nikhilk): Add any other stackedGraph checks here.
226 // Dygraphs has many options, some of which interact with one another.
227 // To keep track of everything, we maintain two sets of options:
229 // this.user_attrs_ only options explicitly set by the user.
230 // this.attrs_ defaults, options derived from user_attrs_, data.
232 // Options are then accessed this.attr_('attr'), which first looks at
233 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
234 // defaults without overriding behavior that the user specifically asks for.
235 this.user_attrs_
= {};
236 Dygraph
.update(this.user_attrs_
, attrs
);
239 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
241 this.boundaryIds_
= [];
243 // Make a note of whether labels will be pulled from the CSV file.
244 this.labelsFromCSV_
= (this.attr_("labels") == null);
246 // Create the containing DIV and other interactive elements
247 this.createInterface_();
252 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
254 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
255 this.user_attrs_
[seriesName
] != null &&
256 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
257 return this.user_attrs_
[seriesName
][name
];
258 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
259 return this.user_attrs_
[name
];
260 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
261 return this.attrs_
[name
];
267 // TODO(danvk): any way I can get the line numbers to be this.warn call?
268 Dygraph
.prototype.log
= function(severity
, message
) {
269 if (typeof(console
) != 'undefined') {
272 console
.debug('dygraphs: ' + message
);
275 console
.info('dygraphs: ' + message
);
277 case Dygraph
.WARNING
:
278 console
.warn('dygraphs: ' + message
);
281 console
.error('dygraphs: ' + message
);
286 Dygraph
.prototype.info
= function(message
) {
287 this.log(Dygraph
.INFO
, message
);
289 Dygraph
.prototype.warn
= function(message
) {
290 this.log(Dygraph
.WARNING
, message
);
292 Dygraph
.prototype.error
= function(message
) {
293 this.log(Dygraph
.ERROR
, message
);
297 * Returns the current rolling period, as set by the user or an option.
298 * @return {Number} The number of days in the rolling window
300 Dygraph
.prototype.rollPeriod
= function() {
301 return this.rollPeriod_
;
305 * Returns the currently-visible x-range. This can be affected by zooming,
306 * panning or a call to updateOptions.
307 * Returns a two-element array: [left, right].
308 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
310 Dygraph
.prototype.xAxisRange
= function() {
311 if (this.dateWindow_
) return this.dateWindow_
;
313 // The entire chart is visible.
314 var left
= this.rawData_
[0][0];
315 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
316 return [left
, right
];
320 * Returns the currently-visible y-range for an axis. This can be affected by
321 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
322 * called with no arguments, returns the range of the first axis.
323 * Returns a two-element array: [bottom, top].
325 Dygraph
.prototype.yAxisRange
= function(idx
) {
326 if (typeof(idx
) == "undefined") idx
= 0;
327 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
328 return [ this.axes_
[idx
].computedValueRange
[0],
329 this.axes_
[idx
].computedValueRange
[1] ];
333 * Returns the currently-visible y-ranges for each axis. This can be affected by
334 * zooming, panning, calls to updateOptions, etc.
335 * Returns an array of [bottom, top] pairs, one for each y-axis.
337 Dygraph
.prototype.yAxisRanges
= function() {
339 for (var i
= 0; i
< this.axes_
.length
; i
++) {
340 ret
.push(this.yAxisRange(i
));
345 // TODO(danvk): use these functions throughout dygraphs.
347 * Convert from data coordinates to canvas/div X/Y coordinates.
348 * If specified, do this conversion for the coordinate system of a particular
349 * axis. Uses the first axis by default.
350 * Returns a two-element array: [X, Y]
352 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
353 var ret
= [null, null];
354 var area
= this.plotter_
.area
;
356 var xRange
= this.xAxisRange();
357 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
361 var yRange
= this.yAxisRange(axis
);
362 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
369 * Convert from canvas/div coords to data coordinates.
370 * If specified, do this conversion for the coordinate system of a particular
371 * axis. Uses the first axis by default.
372 * Returns a two-element array: [X, Y]
374 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
375 var ret
= [null, null];
376 var area
= this.plotter_
.area
;
378 var xRange
= this.xAxisRange();
379 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
383 var yRange
= this.yAxisRange(axis
);
384 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
391 * Returns the number of columns (including the independent variable).
393 Dygraph
.prototype.numColumns
= function() {
394 return this.rawData_
[0].length
;
398 * Returns the number of rows (excluding any header/label row).
400 Dygraph
.prototype.numRows
= function() {
401 return this.rawData_
.length
;
405 * Returns the value in the given row and column. If the row and column exceed
406 * the bounds on the data, returns null. Also returns null if the value is
409 Dygraph
.prototype.getValue
= function(row
, col
) {
410 if (row
< 0 || row
> this.rawData_
.length
) return null;
411 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
413 return this.rawData_
[row
][col
];
416 Dygraph
.addEvent
= function(el
, evt
, fn
) {
417 var normed_fn
= function(e
) {
418 if (!e
) var e
= window
.event
;
421 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
422 el
.addEventListener(evt
, normed_fn
, false);
424 el
.attachEvent('on' + evt
, normed_fn
);
429 * Generates interface elements for the Dygraph: a containing div, a div to
430 * display the current point, and a textbox to adjust the rolling average
431 * period. Also creates the Renderer/Layout elements.
434 Dygraph
.prototype.createInterface_
= function() {
435 // Create the all-enclosing graph div
436 var enclosing
= this.maindiv_
;
438 this.graphDiv
= document
.createElement("div");
439 this.graphDiv
.style
.width
= this.width_
+ "px";
440 this.graphDiv
.style
.height
= this.height_
+ "px";
441 enclosing
.appendChild(this.graphDiv
);
443 // Create the canvas for interactive parts of the chart.
444 this.canvas_
= Dygraph
.createCanvas();
445 this.canvas_
.style
.position
= "absolute";
446 this.canvas_
.width
= this.width_
;
447 this.canvas_
.height
= this.height_
;
448 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
449 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
451 // ... and for static parts of the chart.
452 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
454 // The interactive parts of the graph are drawn on top of the chart.
455 this.graphDiv
.appendChild(this.hidden_
);
456 this.graphDiv
.appendChild(this.canvas_
);
457 this.mouseEventElement_
= this.canvas_
;
460 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
461 dygraph
.mouseMove_(e
);
463 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
464 dygraph
.mouseOut_(e
);
467 // Create the grapher
468 // TODO(danvk): why does the Layout need its own set of options?
469 this.layoutOptions_
= { 'xOriginIsZero': false };
470 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
471 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
472 Dygraph
.update(this.layoutOptions_
, {
473 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
475 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
477 // TODO(danvk): why does the Renderer need its own set of options?
478 this.renderOptions_
= { colorScheme
: this.colors_
,
480 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
481 Dygraph
.update(this.renderOptions_
, this.attrs_
);
482 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
484 this.createStatusMessage_();
485 this.createDragInterface_();
489 * Detach DOM elements in the dygraph and null out all data references.
490 * Calling this when you're done with a dygraph can dramatically reduce memory
491 * usage. See, e.g., the tests/perf.html example.
493 Dygraph
.prototype.destroy
= function() {
494 var removeRecursive
= function(node
) {
495 while (node
.hasChildNodes()) {
496 removeRecursive(node
.firstChild
);
497 node
.removeChild(node
.firstChild
);
500 removeRecursive(this.maindiv_
);
502 var nullOut
= function(obj
) {
504 if (typeof(obj
[n
]) === 'object') {
510 // These may not all be necessary, but it can't hurt...
511 nullOut(this.layout_
);
512 nullOut(this.plotter_
);
517 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
518 * this particular canvas. All Dygraph work is done on this.canvas_.
519 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
520 * @return {Object} The newly-created canvas
523 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
524 var h
= Dygraph
.createCanvas();
525 h
.style
.position
= "absolute";
526 // TODO(danvk): h should be offset from canvas. canvas needs to include
527 // some extra area to make it easier to zoom in on the far left and far
528 // right. h needs to be precisely the plot area, so that clipping occurs.
529 h
.style
.top
= canvas
.style
.top
;
530 h
.style
.left
= canvas
.style
.left
;
531 h
.width
= this.width_
;
532 h
.height
= this.height_
;
533 h
.style
.width
= this.width_
+ "px"; // for IE
534 h
.style
.height
= this.height_
+ "px"; // for IE
538 // Taken from MochiKit.Color
539 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
543 if (saturation
=== 0) {
548 var i
= Math
.floor(hue
* 6);
549 var f
= (hue
* 6) - i
;
550 var p
= value
* (1 - saturation
);
551 var q
= value
* (1 - (saturation
* f
));
552 var t
= value
* (1 - (saturation
* (1 - f
)));
554 case 1: red
= q
; green
= value
; blue
= p
; break;
555 case 2: red
= p
; green
= value
; blue
= t
; break;
556 case 3: red
= p
; green
= q
; blue
= value
; break;
557 case 4: red
= t
; green
= p
; blue
= value
; break;
558 case 5: red
= value
; green
= p
; blue
= q
; break;
559 case 6: // fall through
560 case 0: red
= value
; green
= t
; blue
= p
; break;
563 red
= Math
.floor(255 * red
+ 0.5);
564 green
= Math
.floor(255 * green
+ 0.5);
565 blue
= Math
.floor(255 * blue
+ 0.5);
566 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
571 * Generate a set of distinct colors for the data series. This is done with a
572 * color wheel. Saturation/Value are customizable, and the hue is
573 * equally-spaced around the color wheel. If a custom set of colors is
574 * specified, that is used instead.
577 Dygraph
.prototype.setColors_
= function() {
578 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
579 // away with this.renderOptions_.
580 var num
= this.attr_("labels").length
- 1;
582 var colors
= this.attr_('colors');
584 var sat
= this.attr_('colorSaturation') || 1.0;
585 var val
= this.attr_('colorValue') || 0.5;
586 var half
= Math
.ceil(num
/ 2);
587 for (var i
= 1; i
<= num
; i
++) {
588 if (!this.visibility()[i
-1]) continue;
589 // alternate colors for high contrast.
590 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
591 var hue
= (1.0 * idx
/ (1 + num
));
592 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
595 for (var i
= 0; i
< num
; i
++) {
596 if (!this.visibility()[i
]) continue;
597 var colorStr
= colors
[i
% colors
.length
];
598 this.colors_
.push(colorStr
);
602 // TODO(danvk): update this w/r
/t/ the
new options system
.
603 this.renderOptions_
.colorScheme
= this.colors_
;
604 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
605 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
606 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
610 * Return the list of colors. This is either the list of colors passed in the
611 * attributes, or the autogenerated list of rgb(r,g,b) strings.
612 * @return {Array<string>} The list of colors.
614 Dygraph
.prototype.getColors
= function() {
618 // The following functions are from quirksmode.org with a modification for Safari from
619 // http://blog.firetree.net/2005/07/04/javascript-find-position/
620 // http://www.quirksmode.org/js
/findpos
.html
621 Dygraph
.findPosX
= function(obj
) {
626 curleft
+= obj
.offsetLeft
;
627 if(!obj
.offsetParent
)
629 obj
= obj
.offsetParent
;
636 Dygraph
.findPosY
= function(obj
) {
641 curtop
+= obj
.offsetTop
;
642 if(!obj
.offsetParent
)
644 obj
= obj
.offsetParent
;
654 * Create the div that contains information on the selected point(s)
655 * This goes in the top right of the canvas, unless an external div has already
659 Dygraph
.prototype.createStatusMessage_
= function() {
660 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
661 if (userLabelsDiv
&& null != userLabelsDiv
662 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
663 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
665 if (!this.attr_("labelsDiv")) {
666 var divWidth
= this.attr_('labelsDivWidth');
668 "position": "absolute",
671 "width": divWidth
+ "px",
673 "left": (this.width_
- divWidth
- 2) + "px",
674 "background": "white",
676 "overflow": "hidden"};
677 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
678 var div
= document
.createElement("div");
679 for (var name
in messagestyle
) {
680 if (messagestyle
.hasOwnProperty(name
)) {
681 div
.style
[name
] = messagestyle
[name
];
684 this.graphDiv
.appendChild(div
);
685 this.attrs_
.labelsDiv
= div
;
690 * Position the labels div so that its right edge is flush with the right edge
691 * of the charting area.
693 Dygraph
.prototype.positionLabelsDiv_
= function() {
694 // Don't touch a user-specified labelsDiv.
695 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
697 var area
= this.plotter_
.area
;
698 var div
= this.attr_("labelsDiv");
699 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
703 * Create the text box to adjust the averaging period
706 Dygraph
.prototype.createRollInterface_
= function() {
707 // Create a roller if one doesn't exist already.
709 this.roller_
= document
.createElement("input");
710 this.roller_
.type
= "text";
711 this.roller_
.style
.display
= "none";
712 this.graphDiv
.appendChild(this.roller_
);
715 var display
= this.attr_('showRoller') ? 'block' : 'none';
717 var textAttr
= { "position": "absolute",
719 "top": (this.plotter_
.area
.h
- 25) + "px",
720 "left": (this.plotter_
.area
.x
+ 1) + "px",
723 this.roller_
.size
= "2";
724 this.roller_
.value
= this.rollPeriod_
;
725 for (var name
in textAttr
) {
726 if (textAttr
.hasOwnProperty(name
)) {
727 this.roller_
.style
[name
] = textAttr
[name
];
732 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
735 // These functions are taken from MochiKit.Signal
736 Dygraph
.pageX
= function(e
) {
738 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
741 var b
= document
.body
;
743 (de
.scrollLeft
|| b
.scrollLeft
) -
744 (de
.clientLeft
|| 0);
748 Dygraph
.pageY
= function(e
) {
750 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
753 var b
= document
.body
;
755 (de
.scrollTop
|| b
.scrollTop
) -
761 * Set up all the mouse handlers needed to capture dragging behavior for zoom
765 Dygraph
.prototype.createDragInterface_
= function() {
768 // Tracks whether the mouse is down right now
769 var isZooming
= false;
770 var isPanning
= false; // is this drag part of a pan?
771 var is2DPan
= false; // if so, is that pan 1- or 2-dimensional?
772 var dragStartX
= null;
773 var dragStartY
= null;
776 var dragDirection
= null;
779 var prevDragDirection
= null;
781 // TODO(danvk): update this comment
782 // draggingDate and draggingValue represent the [date,value] point on the
783 // graph at which the mouse was pressed. As the mouse moves while panning,
784 // the viewport must pan so that the mouse position points to
785 // [draggingDate, draggingValue]
786 var draggingDate
= null;
788 // TODO(danvk): update this comment
789 // The range in second/value units that the viewport encompasses during a
790 // panning operation.
791 var dateRange
= null;
793 // Utility function to convert page-wide coordinates to canvas coords
796 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
797 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
799 // Draw zoom rectangles when the mouse is down and the user moves around
800 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
802 dragEndX
= getX(event
);
803 dragEndY
= getY(event
);
805 var xDelta
= Math
.abs(dragStartX
- dragEndX
);
806 var yDelta
= Math
.abs(dragStartY
- dragEndY
);
808 // drag direction threshold for y axis is twice as large as x axis
809 dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
811 self
.drawZoomRect_(dragDirection
, dragStartX
, dragEndX
, dragStartY
, dragEndY
,
812 prevDragDirection
, prevEndX
, prevEndY
);
816 prevDragDirection
= dragDirection
;
817 } else if (isPanning
) {
818 dragEndX
= getX(event
);
819 dragEndY
= getY(event
);
821 // TODO(danvk): update this comment
822 // Want to have it so that:
823 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
824 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
825 // 3. draggingValue appears at dragEndY.
826 // 4. valueRange is unaltered.
828 var minDate
= draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
829 var maxDate
= minDate
+ dateRange
;
830 self
.dateWindow_
= [minDate
, maxDate
];
833 // y-axis scaling is automatic unless this is a full 2D pan.
835 // Adjust each axis appropriately.
836 var y_frac
= dragEndY
/ self
.height_
;
837 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
838 var axis
= self
.axes_
[i
];
839 var maxValue
= axis
.draggingValue
+ y_frac
* axis
.dragValueRange
;
840 var minValue
= maxValue
- axis
.dragValueRange
;
841 axis
.valueWindow
= [ minValue
, maxValue
];
849 // Track the beginning of drag events
850 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
851 // prevents mouse drags from selecting page text.
852 if (event
.preventDefault
) {
853 event
.preventDefault(); // Firefox, Chrome, etc.
855 event
.returnValue
= false; // IE
856 event
.cancelBubble
= true;
859 px
= Dygraph
.findPosX(self
.canvas_
);
860 py
= Dygraph
.findPosY(self
.canvas_
);
861 dragStartX
= getX(event
);
862 dragStartY
= getY(event
);
864 if (event
.altKey
|| event
.shiftKey
) {
865 // have to be zoomed in to pan.
867 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
868 if (self
.axes_
[i
].valueWindow
|| self
.axes_
[i
].valueRange
) {
873 if (!self
.dateWindow_
&& !zoomedY
) return;
876 var xRange
= self
.xAxisRange();
877 dateRange
= xRange
[1] - xRange
[0];
879 // Record the range of each y-axis at the start of the drag.
880 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
882 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
883 var axis
= self
.axes_
[i
];
884 var yRange
= self
.yAxisRange(i
);
885 axis
.dragValueRange
= yRange
[1] - yRange
[0];
886 var r
= self
.toDataCoords(null, dragStartY
, i
);
887 axis
.draggingValue
= r
[1];
888 if (axis
.valueWindow
|| axis
.valueRange
) is2DPan
= true;
891 // TODO(konigsberg): Switch from all this math to toDataCoords?
892 // Seems to work for the dragging value.
893 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+ xRange
[0];
899 // If the user releases the mouse button during a drag, but not over the
900 // canvas, then it doesn't count as a zooming action.
901 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
902 if (isZooming
|| isPanning
) {
912 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
913 delete self
.axes_
[i
].draggingValue
;
914 delete self
.axes_
[i
].dragValueRange
;
919 // Temporarily cancel the dragging event when the mouse leaves the graph
920 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
927 // If the mouse is released on the canvas during a drag event, then it's a
928 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
929 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
932 dragEndX
= getX(event
);
933 dragEndY
= getY(event
);
934 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
935 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
937 if (regionWidth
< 2 && regionHeight
< 2 &&
938 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
939 // TODO(danvk): pass along more info about the points, e.g. 'x'
940 if (self
.attr_('clickCallback') != null) {
941 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
943 if (self
.attr_('pointClickCallback')) {
944 // check if the click was on a particular point.
946 var closestDistance
= 0;
947 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
948 var p
= self
.selPoints_
[i
];
949 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
950 Math
.pow(p
.canvasy
- dragEndY
, 2);
951 if (closestIdx
== -1 || distance
< closestDistance
) {
952 closestDistance
= distance
;
957 // Allow any click within two pixels of the dot.
958 var radius
= self
.attr_('highlightCircleSize') + 2;
959 if (closestDistance
<= 5 * 5) {
960 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
965 if (regionWidth
>= 10 && dragDirection
== Dygraph
.HORIZONTAL
) {
966 self
.doZoomX_(Math
.min(dragStartX
, dragEndX
),
967 Math
.max(dragStartX
, dragEndX
));
968 } else if (regionHeight
>= 10 && dragDirection
== Dygraph
.VERTICAL
){
969 self
.doZoomY_(Math
.min(dragStartY
, dragEndY
),
970 Math
.max(dragStartY
, dragEndY
));
972 self
.canvas_
.getContext("2d").clearRect(0, 0,
974 self
.canvas_
.height
);
990 // Double-clicking zooms back out
991 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
992 // Disable zooming out if panning.
993 if (event
.altKey
|| event
.shiftKey
) return;
1000 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1001 * up any previous zoom rectangles that were drawn. This could be optimized to
1002 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1005 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1006 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1007 * @param {Number} startX The X position where the drag started, in canvas
1009 * @param {Number} endX The current X position of the drag, in canvas coords.
1010 * @param {Number} startY The Y position where the drag started, in canvas
1012 * @param {Number} endY The current Y position of the drag, in canvas coords.
1013 * @param {Number} prevDirection the value of direction on the previous call to
1014 * this function. Used to avoid excess redrawing
1015 * @param {Number} prevEndX The value of endX on the previous call to this
1016 * function. Used to avoid excess redrawing
1017 * @param {Number} prevEndY The value of endY on the previous call to this
1018 * function. Used to avoid excess redrawing
1021 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1022 prevDirection
, prevEndX
, prevEndY
) {
1023 var ctx
= this.canvas_
.getContext("2d");
1025 // Clean up from the previous rect if necessary
1026 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1027 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1028 Math
.abs(startX
- prevEndX
), this.height_
);
1029 } else if (prevDirection
== Dygraph
.VERTICAL
){
1030 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1031 this.width_
, Math
.abs(startY
- prevEndY
));
1034 // Draw a light-grey rectangle to show the new viewing area
1035 if (direction
== Dygraph
.HORIZONTAL
) {
1036 if (endX
&& startX
) {
1037 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1038 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1039 Math
.abs(endX
- startX
), this.height_
);
1042 if (direction
== Dygraph
.VERTICAL
) {
1043 if (endY
&& startY
) {
1044 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1045 ctx
.fillRect(0, Math
.min(startY
, endY
),
1046 this.width_
, Math
.abs(endY
- startY
));
1052 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1053 * the canvas. The exact zoom window may be slightly larger if there are no data
1054 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1055 * which accepts dates that match the raw data. This function redraws the graph.
1057 * @param {Number} lowX The leftmost pixel value that should be visible.
1058 * @param {Number} highX The rightmost pixel value that should be visible.
1061 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1062 // Find the earliest and latest dates contained in this canvasx range.
1063 // Convert the call to date ranges of the raw data.
1064 var r
= this.toDataCoords(lowX
, null);
1066 r
= this.toDataCoords(highX
, null);
1068 this.doZoomXDates_(minDate
, maxDate
);
1072 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1073 * method with doZoomX which accepts pixel coordinates. This function redraws
1076 * @param {Number} minDate The minimum date that should be visible.
1077 * @param {Number} maxDate The maximum date that should be visible.
1080 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1081 this.dateWindow_
= [minDate
, maxDate
];
1083 this.zoomedX
= true;
1085 if (this.attr_("zoomCallback")) {
1086 var yRange
= this.yAxisRange();
1087 this.attr_("zoomCallback")(minDate
, maxDate
, yRange
[0], yRange
[1]);
1092 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1093 * the canvas. This function redraws the graph.
1095 * @param {Number} lowY The topmost pixel value that should be visible.
1096 * @param {Number} highY The lowest pixel value that should be visible.
1099 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1100 // Find the highest and lowest values in pixel range for each axis.
1101 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1102 // This is because pixels increase as you go down on the screen, whereas data
1103 // coordinates increase as you go up the screen.
1104 var valueRanges
= [];
1105 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1106 var hi
= this.toDataCoords(null, lowY
, i
);
1107 var low
= this.toDataCoords(null, highY
, i
);
1108 this.axes_
[i
].valueWindow
= [low
[1], hi
[1]];
1109 valueRanges
.push([low
[1], hi
[1]]);
1113 this.zoomedY
= true;
1115 if (this.attr_("zoomCallback")) {
1116 var xRange
= this.xAxisRange();
1117 var yRange
= this.yAxisRange();
1118 this.attr_("zoomCallback")(xRange
[0], xRange
[1], yRange
[0], yRange
[1]);
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
1145 this.zoomed
= false;
1146 this.zoomedX
= false;
1147 this.zoomedY
= false;
1149 if (this.attr_("zoomCallback")) {
1150 var minDate
= this.rawData_
[0][0];
1151 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1152 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1158 * When the mouse moves in the canvas, display information about a nearby data
1159 * point and draw dots over those points in the data series. This function
1160 * takes care of cleanup of previously-drawn dots.
1161 * @param {Object} event The mousemove event from the browser.
1164 Dygraph
.prototype.mouseMove_
= function(event
) {
1165 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1166 var points
= this.layout_
.points
;
1171 // Loop through all the points and find the date nearest to our current
1173 var minDist
= 1e+100;
1175 for (var i
= 0; i
< points
.length
; i
++) {
1176 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1177 if (dist
> minDist
) continue;
1181 if (idx
>= 0) lastx
= points
[idx
].xval
;
1182 // Check that you can really highlight the last day's data
1183 if (canvasx
> points
[points
.length
-1].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_
);
1220 // Save last x position for callbacks.
1221 this.lastx_
= lastx
;
1223 this.updateSelection_();
1227 * Draw dots over the selectied points in the data series. This function
1228 * takes care of cleanup of previously-drawn dots.
1231 Dygraph
.prototype.updateSelection_
= function() {
1232 // Clear the previously drawn vertical, if there is one
1233 var ctx
= this.canvas_
.getContext("2d");
1234 if (this.previousVerticalX_
>= 0) {
1235 // Determine the maximum highlight circle size.
1236 var maxCircleSize
= 0;
1237 var labels
= this.attr_('labels');
1238 for (var i
= 1; i
< labels
.length
; i
++) {
1239 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1240 if (r
> maxCircleSize
) maxCircleSize
= r
;
1242 var px
= this.previousVerticalX_
;
1243 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1244 2 * maxCircleSize
+ 2, this.height_
);
1247 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1249 if (this.selPoints_
.length
> 0) {
1250 var canvasx
= this.selPoints_
[0].canvasx
;
1252 // Set the status message to indicate the selected point(s)
1253 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1254 var fmtFunc
= this.attr_('yValueFormatter');
1255 var clen
= this.colors_
.length
;
1257 if (this.attr_('showLabelsOnHighlight')) {
1258 // Set the status message to indicate the selected point(s)
1259 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1260 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1261 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1262 if (this.attr_("labelsSeparateLines")) {
1265 var point
= this.selPoints_
[i
];
1266 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1267 var yval
= fmtFunc(point
.yval
);
1268 replace
+= " <b><font color='" + c
.toHex() + "'>"
1269 + point
.name
+ "</font></b>:"
1273 this.attr_("labelsDiv").innerHTML
= replace
;
1276 // Draw colored circles over the center of each selected point
1278 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1279 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1281 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1283 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1284 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1285 0, 2 * Math
.PI
, false);
1290 this.previousVerticalX_
= canvasx
;
1295 * Set manually set selected dots, and display information about them
1296 * @param int row number that should by highlighted
1297 * false value clears the selection
1300 Dygraph
.prototype.setSelection
= function(row
) {
1301 // Extract the points we've selected
1302 this.selPoints_
= [];
1305 if (row
!== false) {
1306 row
= row
-this.boundaryIds_
[0][0];
1309 if (row
!== false && row
>= 0) {
1310 for (var i
in this.layout_
.datasets
) {
1311 if (row
< this.layout_
.datasets
[i
].length
) {
1312 var point
= this.layout_
.points
[pos
+row
];
1314 if (this.attr_("stackedGraph")) {
1315 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1318 this.selPoints_
.push(point
);
1320 pos
+= this.layout_
.datasets
[i
].length
;
1324 if (this.selPoints_
.length
) {
1325 this.lastx_
= this.selPoints_
[0].xval
;
1326 this.updateSelection_();
1329 this.clearSelection();
1335 * The mouse has left the canvas. Clear out whatever artifacts remain
1336 * @param {Object} event the mouseout event from the browser.
1339 Dygraph
.prototype.mouseOut_
= function(event
) {
1340 if (this.attr_("unhighlightCallback")) {
1341 this.attr_("unhighlightCallback")(event
);
1344 if (this.attr_("hideOverlayOnMouseOut")) {
1345 this.clearSelection();
1350 * Remove all selection from the canvas
1353 Dygraph
.prototype.clearSelection
= function() {
1354 // Get rid of the overlay data
1355 var ctx
= this.canvas_
.getContext("2d");
1356 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1357 this.attr_("labelsDiv").innerHTML
= "";
1358 this.selPoints_
= [];
1363 * Returns the number of the currently selected row
1364 * @return int row number, of -1 if nothing is selected
1367 Dygraph
.prototype.getSelection
= function() {
1368 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1372 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1373 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1374 return row
+ this.boundaryIds_
[0][0];
1380 Dygraph
.zeropad
= function(x
) {
1381 if (x
< 10) return "0" + x
; else return "" + x
;
1385 * Return a string version of the hours, minutes and seconds portion of a date.
1386 * @param {Number} date The JavaScript date (ms since epoch)
1387 * @return {String} A time of the form "HH:MM:SS"
1390 Dygraph
.hmsString_
= function(date
) {
1391 var zeropad
= Dygraph
.zeropad
;
1392 var d
= new Date(date
);
1393 if (d
.getSeconds()) {
1394 return zeropad(d
.getHours()) + ":" +
1395 zeropad(d
.getMinutes()) + ":" +
1396 zeropad(d
.getSeconds());
1398 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1403 * Convert a JS date to a string appropriate to display on an axis that
1404 * is displaying values at the stated granularity.
1405 * @param {Date} date The date to format
1406 * @param {Number} granularity One of the Dygraph granularity constants
1407 * @return {String} The formatted date
1410 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1411 if (granularity
>= Dygraph
.MONTHLY
) {
1412 return date
.strftime('%b %y');
1414 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1415 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1416 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1418 return Dygraph
.hmsString_(date
.getTime());
1424 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1425 * @param {Number} date The JavaScript date (ms since epoch)
1426 * @return {String} A date of the form "YYYY/MM/DD"
1429 Dygraph
.dateString_
= function(date
, self
) {
1430 var zeropad
= Dygraph
.zeropad
;
1431 var d
= new Date(date
);
1434 var year
= "" + d
.getFullYear();
1435 // Get a 0 padded month string
1436 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1437 // Get a 0 padded day string
1438 var day
= zeropad(d
.getDate());
1441 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1442 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1444 return year
+ "/" + month + "/" + day
+ ret
;
1448 * Round a number to the specified number of digits past the decimal point.
1449 * @param {Number} num The number to round
1450 * @param {Number} places The number of decimals to which to round
1451 * @return {Number} The rounded number
1454 Dygraph
.round_
= function(num
, places
) {
1455 var shift
= Math
.pow(10, places
);
1456 return Math
.round(num
* shift
)/shift
;
1460 * Fires when there's data available to be graphed.
1461 * @param {String} data Raw CSV data to be plotted
1464 Dygraph
.prototype.loadedEvent_
= function(data
) {
1465 this.rawData_
= this.parseCSV_(data
);
1469 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1470 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1471 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1474 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1477 Dygraph
.prototype.addXTicks_
= function() {
1478 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1479 var startDate
, endDate
;
1480 if (this.dateWindow_
) {
1481 startDate
= this.dateWindow_
[0];
1482 endDate
= this.dateWindow_
[1];
1484 startDate
= this.rawData_
[0][0];
1485 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1488 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1489 this.layout_
.updateOptions({xTicks
: xTicks
});
1492 // Time granularity enumeration
1493 Dygraph
.SECONDLY
= 0;
1494 Dygraph
.TWO_SECONDLY
= 1;
1495 Dygraph
.FIVE_SECONDLY
= 2;
1496 Dygraph
.TEN_SECONDLY
= 3;
1497 Dygraph
.THIRTY_SECONDLY
= 4;
1498 Dygraph
.MINUTELY
= 5;
1499 Dygraph
.TWO_MINUTELY
= 6;
1500 Dygraph
.FIVE_MINUTELY
= 7;
1501 Dygraph
.TEN_MINUTELY
= 8;
1502 Dygraph
.THIRTY_MINUTELY
= 9;
1503 Dygraph
.HOURLY
= 10;
1504 Dygraph
.TWO_HOURLY
= 11;
1505 Dygraph
.SIX_HOURLY
= 12;
1507 Dygraph
.WEEKLY
= 14;
1508 Dygraph
.MONTHLY
= 15;
1509 Dygraph
.QUARTERLY
= 16;
1510 Dygraph
.BIANNUAL
= 17;
1511 Dygraph
.ANNUAL
= 18;
1512 Dygraph
.DECADAL
= 19;
1513 Dygraph
.NUM_GRANULARITIES
= 20;
1515 Dygraph
.SHORT_SPACINGS
= [];
1516 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1517 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1518 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1519 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1520 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1521 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1522 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1523 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1524 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1525 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1526 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1527 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1528 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1529 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1530 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1534 // If we used this time granularity, how many ticks would there be?
1535 // This is only an approximation, but it's generally good enough.
1537 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1538 if (granularity
< Dygraph
.MONTHLY
) {
1539 // Generate one tick mark for every fixed interval of time.
1540 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1541 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1543 var year_mod
= 1; // e.g. to only print one point every 10 years.
1544 var num_months
= 12;
1545 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1546 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1547 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1548 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1550 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1551 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1552 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1558 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1559 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1561 // Returns an array containing {v: millis, label: label} dictionaries.
1563 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1564 var formatter
= this.attr_("xAxisLabelFormatter");
1566 if (granularity
< Dygraph
.MONTHLY
) {
1567 // Generate one tick mark for every fixed interval of time.
1568 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1569 var format
= '%d%b'; // e.g. "1Jan"
1571 // Find a time less than start_time which occurs on a "nice" time boundary
1572 // for this granularity.
1573 var g
= spacing
/ 1000;
1574 var d
= new Date(start_time
);
1575 if (g
<= 60) { // seconds
1576 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1580 if (g
<= 60) { // minutes
1581 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1586 if (g
<= 24) { // days
1587 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1592 if (g
== 7) { // one week
1593 d
.setDate(d
.getDate() - d
.getDay());
1598 start_time
= d
.getTime();
1600 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1601 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1604 // Display a tick mark on the first of a set of months of each year.
1605 // Years get a tick mark iff y % year_mod == 0. This is useful for
1606 // displaying a tick mark once every 10 years, say, on long time scales.
1608 var year_mod
= 1; // e.g. to only print one point every 10 years.
1610 if (granularity
== Dygraph
.MONTHLY
) {
1611 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1612 } else if (granularity
== Dygraph
.QUARTERLY
) {
1613 months
= [ 0, 3, 6, 9 ];
1614 } else if (granularity
== Dygraph
.BIANNUAL
) {
1616 } else if (granularity
== Dygraph
.ANNUAL
) {
1618 } else if (granularity
== Dygraph
.DECADAL
) {
1623 var start_year
= new Date(start_time
).getFullYear();
1624 var end_year
= new Date(end_time
).getFullYear();
1625 var zeropad
= Dygraph
.zeropad
;
1626 for (var i
= start_year
; i
<= end_year
; i
++) {
1627 if (i
% year_mod
!= 0) continue;
1628 for (var j
= 0; j
< months
.length
; j
++) {
1629 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1630 var t
= Date
.parse(date_str
);
1631 if (t
< start_time
|| t
> end_time
) continue;
1632 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1642 * Add ticks to the x-axis based on a date range.
1643 * @param {Number} startDate Start of the date window (millis since epoch)
1644 * @param {Number} endDate End of the date window (millis since epoch)
1645 * @return {Array.<Object>} Array of {label, value} tuples.
1648 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1650 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1651 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1652 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1659 return self
.GetXAxis(startDate
, endDate
, chosen
);
1661 // TODO(danvk): signal error.
1666 * Add ticks when the x axis has numbers on it (instead of dates)
1667 * @param {Number} startDate Start of the date window (millis since epoch)
1668 * @param {Number} endDate End of the date window (millis since epoch)
1670 * @param {function} attribute accessor function.
1671 * @return {Array.<Object>} Array of {label, value} tuples.
1674 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1675 var attr
= function(k
) {
1676 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1677 return self
.attr_(k
);
1682 for (var i
= 0; i
< vals
.length
; i
++) {
1683 ticks
.push({v
: vals
[i
]});
1687 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1688 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1689 // The first spacing greater than pixelsPerYLabel is what we use.
1690 // TODO(danvk): version that works on a log scale.
1691 if (attr("labelsKMG2")) {
1692 var mults
= [1, 2, 4, 8];
1694 var mults
= [1, 2, 5];
1696 var scale
, low_val
, high_val
, nTicks
;
1697 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1698 var pixelsPerTick
= attr('pixelsPerYLabel');
1699 for (var i
= -10; i
< 50; i
++) {
1700 if (attr("labelsKMG2")) {
1701 var base_scale
= Math
.pow(16, i
);
1703 var base_scale
= Math
.pow(10, i
);
1705 for (var j
= 0; j
< mults
.length
; j
++) {
1706 scale
= base_scale
* mults
[j
];
1707 low_val
= Math
.floor(minV
/ scale
) * scale
;
1708 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1709 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1710 var spacing
= self
.height_
/ nTicks
;
1711 // wish I could break out of both loops at once...
1712 if (spacing
> pixelsPerTick
) break;
1714 if (spacing
> pixelsPerTick
) break;
1717 // Construct the set of ticks.
1718 // Allow reverse y-axis if it's explicitly requested.
1719 if (low_val
> high_val
) scale
*= -1;
1720 for (var i
= 0; i
< nTicks
; i
++) {
1721 var tickV
= low_val
+ i
* scale
;
1722 ticks
.push( {v
: tickV
} );
1726 // Add formatted labels to the ticks.
1729 if (attr("labelsKMB")) {
1731 k_labels
= [ "K", "M", "B", "T" ];
1733 if (attr("labelsKMG2")) {
1734 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1736 k_labels
= [ "k", "M", "G", "T" ];
1738 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
1740 for (var i
= 0; i
< ticks
.length
; i
++) {
1741 var tickV
= ticks
[i
].v
;
1742 var absTickV
= Math
.abs(tickV
);
1744 if (formatter
!= undefined
) {
1745 label
= formatter(tickV
);
1747 label
= Dygraph
.round_(tickV
, 2);
1749 if (k_labels
.length
) {
1750 // Round up to an appropriate unit.
1752 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1753 if (absTickV
>= n
) {
1754 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
1759 ticks
[i
].label
= label
;
1764 // Computes the range of the data series (including confidence intervals).
1765 // series is either [ [x1, y1], [x2, y2], ... ] or
1766 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1767 // Returns [low, high]
1768 Dygraph
.prototype.extremeValues_
= function(series
) {
1769 var minY
= null, maxY
= null;
1771 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1773 // With custom bars, maxY is the max of the high values.
1774 for (var j
= 0; j
< series
.length
; j
++) {
1775 var y
= series
[j
][1][0];
1777 var low
= y
- series
[j
][1][1];
1778 var high
= y
+ series
[j
][1][2];
1779 if (low
> y
) low
= y
; // this can happen with custom bars,
1780 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1781 if (maxY
== null || high
> maxY
) {
1784 if (minY
== null || low
< minY
) {
1789 for (var j
= 0; j
< series
.length
; j
++) {
1790 var y
= series
[j
][1];
1791 if (y
=== null || isNaN(y
)) continue;
1792 if (maxY
== null || y
> maxY
) {
1795 if (minY
== null || y
< minY
) {
1801 return [minY
, maxY
];
1805 * This function is called once when the chart's data is changed or the options
1806 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1807 * idea is that values derived from the chart's data can be computed here,
1808 * rather than every time the chart is drawn. This includes things like the
1809 * number of axes, rolling averages, etc.
1811 Dygraph
.prototype.predraw_
= function() {
1812 // TODO(danvk): move more computations out of drawGraph_ and into here.
1813 this.computeYAxes_();
1815 // Create a new plotter.
1816 if (this.plotter_
) this.plotter_
.clear();
1817 this.plotter_
= new DygraphCanvasRenderer(this,
1818 this.hidden_
, this.layout_
,
1819 this.renderOptions_
);
1821 // The roller sits in the bottom left corner of the chart. We don't know where
1822 // this will be until the options are available, so it's positioned here.
1823 this.createRollInterface_();
1825 // Same thing applies for the labelsDiv. It's right edge should be flush with
1826 // the right edge of the charting area (which may not be the same as the right
1827 // edge of the div, if we have two y-axes.
1828 this.positionLabelsDiv_();
1830 // If the data or options have changed, then we'd better redraw.
1836 * Update the graph with new data. This method is called when the viewing area
1837 * has changed. If the underlying data or options have changed, predraw_ will
1838 * be called before drawGraph_ is called.
1841 Dygraph
.prototype.drawGraph_
= function() {
1842 var data
= this.rawData_
;
1844 // This is used to set the second parameter to drawCallback, below.
1845 var is_initial_draw
= this.is_initial_draw_
;
1846 this.is_initial_draw_
= false;
1848 var minY
= null, maxY
= null;
1849 this.layout_
.removeAllDatasets();
1851 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1853 // Loop over the fields (series). Go from the last to the first,
1854 // because if they're stacked that's how we accumulate the values.
1856 var cumulative_y
= []; // For stacked series.
1859 var extremes
= {}; // series name -> [low, high]
1861 // Loop over all fields and create datasets
1862 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1863 if (!this.visibility()[i
- 1]) continue;
1865 var seriesName
= this.attr_("labels")[i
];
1866 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1869 for (var j
= 0; j
< data
.length
; j
++) {
1870 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1871 var date
= data
[j
][0];
1872 series
.push([date
, data
[j
][i
]]);
1876 // TODO(danvk): move this into predraw_. It's insane to do it here.
1877 series
= this.rollingAverage(series
, this.rollPeriod_
);
1879 // Prune down to the desired range, if necessary (for zooming)
1880 // Because there can be lines going to points outside of the visible area,
1881 // we actually prune to visible points, plus one on either side.
1882 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1883 if (this.dateWindow_
) {
1884 var low
= this.dateWindow_
[0];
1885 var high
= this.dateWindow_
[1];
1887 // TODO(danvk): do binary search instead of linear search.
1888 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1889 var firstIdx
= null, lastIdx
= null;
1890 for (var k
= 0; k
< series
.length
; k
++) {
1891 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1894 if (series
[k
][0] <= high
) {
1898 if (firstIdx
=== null) firstIdx
= 0;
1899 if (firstIdx
> 0) firstIdx
--;
1900 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1901 if (lastIdx
< series
.length
- 1) lastIdx
++;
1902 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1903 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1904 pruned
.push(series
[k
]);
1908 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1911 var seriesExtremes
= this.extremeValues_(series
);
1914 for (var j
=0; j
<series
.length
; j
++) {
1915 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1918 } else if (this.attr_("stackedGraph")) {
1919 var l
= series
.length
;
1921 for (var j
= 0; j
< l
; j
++) {
1922 // If one data set has a NaN, let all subsequent stacked
1923 // sets inherit the NaN -- only start at 0 for the first set.
1924 var x
= series
[j
][0];
1925 if (cumulative_y
[x
] === undefined
) {
1926 cumulative_y
[x
] = 0;
1929 actual_y
= series
[j
][1];
1930 cumulative_y
[x
] += actual_y
;
1932 series
[j
] = [x
, cumulative_y
[x
]]
1934 if (cumulative_y
[x
] > seriesExtremes
[1]) {
1935 seriesExtremes
[1] = cumulative_y
[x
];
1937 if (cumulative_y
[x
] < seriesExtremes
[0]) {
1938 seriesExtremes
[0] = cumulative_y
[x
];
1942 extremes
[seriesName
] = seriesExtremes
;
1944 datasets
[i
] = series
;
1947 for (var i
= 1; i
< datasets
.length
; i
++) {
1948 if (!this.visibility()[i
- 1]) continue;
1949 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1952 // TODO(danvk): this method doesn't need to return anything.
1953 var out
= this.computeYAxisRanges_(extremes
);
1955 var seriesToAxisMap
= out
[1];
1956 this.layout_
.updateOptions( { yAxes
: axes
,
1957 seriesToAxisMap
: seriesToAxisMap
1962 // Tell PlotKit to use this new data and render itself
1963 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
1964 this.layout_
.evaluateWithError();
1965 this.plotter_
.clear();
1966 this.plotter_
.render();
1967 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1968 this.canvas_
.height
);
1970 if (this.attr_("drawCallback") !== null) {
1971 this.attr_("drawCallback")(this, is_initial_draw
);
1976 * Determine properties of the y-axes which are independent of the data
1977 * currently being displayed. This includes things like the number of axes and
1978 * the style of the axes. It does not include the range of each axis and its
1980 * This fills in this.axes_ and this.seriesToAxisMap_.
1981 * axes_ = [ { options } ]
1982 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
1983 * indices are into the axes_ array.
1985 Dygraph
.prototype.computeYAxes_
= function() {
1987 if (this.axes_
!= undefined
) {
1988 // Preserve valueWindow settings.
1990 for (var index
= 0; index
< this.axes_
.length
; index
++) {
1991 valueWindow
.push(this.axes_
[index
].valueWindow
);
1995 this.axes_
= [{}]; // always have at least one y-axis.
1996 this.seriesToAxisMap_
= {};
1998 // Get a list of series names.
1999 var labels
= this.attr_("labels");
2001 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2003 // all options which could be applied per-axis:
2011 'axisLabelFontSize',
2015 // Copy global axis options over to the first axis.
2016 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2017 var k
= axisOptions
[i
];
2018 var v
= this.attr_(k
);
2019 if (v
) this.axes_
[0][k
] = v
;
2022 // Go through once and add all the axes.
2023 for (var seriesName
in series
) {
2024 if (!series
.hasOwnProperty(seriesName
)) continue;
2025 var axis
= this.attr_("axis", seriesName
);
2027 this.seriesToAxisMap_
[seriesName
] = 0;
2030 if (typeof(axis
) == 'object') {
2031 // Add a new axis, making a copy of its per-axis options.
2033 Dygraph
.update(opts
, this.axes_
[0]);
2034 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2035 Dygraph
.update(opts
, axis
);
2036 this.axes_
.push(opts
);
2037 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2041 // Go through one more time and assign series to an axis defined by another
2042 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2043 for (var seriesName
in series
) {
2044 if (!series
.hasOwnProperty(seriesName
)) continue;
2045 var axis
= this.attr_("axis", seriesName
);
2046 if (typeof(axis
) == 'string') {
2047 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2048 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2049 "series " + axis
+ ", which does not define its own axis.");
2052 var idx
= this.seriesToAxisMap_
[axis
];
2053 this.seriesToAxisMap_
[seriesName
] = idx
;
2057 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2058 // this last so that hiding the first series doesn't destroy the axis
2059 // properties of the primary axis.
2060 var seriesToAxisFiltered
= {};
2061 var vis
= this.visibility();
2062 for (var i
= 1; i
< labels
.length
; i
++) {
2064 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2066 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2068 if (valueWindow
!= undefined
) {
2069 // Restore valueWindow settings.
2070 for (var index
= 0; index
< valueWindow
.length
; index
++) {
2071 this.axes_
[index
].valueWindow
= valueWindow
[index
];
2077 * Returns the number of y-axes on the chart.
2078 * @return {Number} the number of axes.
2080 Dygraph
.prototype.numAxes
= function() {
2082 for (var series
in this.seriesToAxisMap_
) {
2083 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2084 var idx
= this.seriesToAxisMap_
[series
];
2085 if (idx
> last_axis
) last_axis
= idx
;
2087 return 1 + last_axis
;
2091 * Determine the value range and tick marks for each axis.
2092 * @param {Object} extremes A mapping from seriesName -> [low, high]
2093 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2095 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2096 // Build a map from axis number -> [list of series names]
2097 var seriesForAxis
= [];
2098 for (var series
in this.seriesToAxisMap_
) {
2099 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2100 var idx
= this.seriesToAxisMap_
[series
];
2101 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2102 seriesForAxis
[idx
].push(series
);
2105 // Compute extreme values, a span and tick marks for each axis.
2106 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2107 var axis
= this.axes_
[i
];
2108 if (axis
.valueWindow
) {
2109 // This is only set if the user has zoomed on the y-axis. It is never set
2110 // by a user. It takes precedence over axis.valueRange because, if you set
2111 // valueRange, you'd still expect to be able to pan.
2112 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2113 } else if (axis
.valueRange
) {
2114 // This is a user-set value range for this axis.
2115 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2117 // Calculate the extremes of extremes.
2118 var series
= seriesForAxis
[i
];
2119 var minY
= Infinity
; // extremes[series[0]][0];
2120 var maxY
= -Infinity
; // extremes[series[0]][1];
2121 for (var j
= 0; j
< series
.length
; j
++) {
2122 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2123 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2125 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2127 // Add some padding and round up to an integer to be human-friendly.
2128 var span
= maxY
- minY
;
2129 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2130 if (span
== 0) { span
= maxY
; }
2131 var maxAxisY
= maxY
+ 0.1 * span
;
2132 var minAxisY
= minY
- 0.1 * span
;
2134 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2135 if (!this.attr_("avoidMinZero")) {
2136 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2137 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2140 if (this.attr_("includeZero")) {
2141 if (maxY
< 0) maxAxisY
= 0;
2142 if (minY
> 0) minAxisY
= 0;
2145 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2148 // Add ticks. By default, all axes inherit the tick positions of the
2149 // primary axis. However, if an axis is specifically marked as having
2150 // independent ticks, then that is permissible as well.
2151 if (i
== 0 || axis
.independentTicks
) {
2153 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2154 axis
.computedValueRange
[1],
2158 var p_axis
= this.axes_
[0];
2159 var p_ticks
= p_axis
.ticks
;
2160 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2161 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2162 var tick_values
= [];
2163 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2164 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2165 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2166 tick_values
.push(y_val
);
2170 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2171 axis
.computedValueRange
[1],
2172 this, axis
, tick_values
);
2176 return [this.axes_
, this.seriesToAxisMap_
];
2180 * Calculates the rolling average of a data set.
2181 * If originalData is [label, val], rolls the average of those.
2182 * If originalData is [label, [, it's interpreted as [value, stddev]
2183 * and the roll is returned in the same form, with appropriately reduced
2184 * stddev for each value.
2185 * Note that this is where fractional input (i.e. '5/10') is converted into
2187 * @param {Array} originalData The data in the appropriate format (see above)
2188 * @param {Number} rollPeriod The number of days over which to average the data
2190 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2191 if (originalData
.length
< 2)
2192 return originalData
;
2193 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2194 var rollingData
= [];
2195 var sigma
= this.attr_("sigma");
2197 if (this.fractions_
) {
2199 var den
= 0; // numerator/denominator
2201 for (var i
= 0; i
< originalData
.length
; i
++) {
2202 num
+= originalData
[i
][1][0];
2203 den
+= originalData
[i
][1][1];
2204 if (i
- rollPeriod
>= 0) {
2205 num
-= originalData
[i
- rollPeriod
][1][0];
2206 den
-= originalData
[i
- rollPeriod
][1][1];
2209 var date
= originalData
[i
][0];
2210 var value
= den
? num
/ den
: 0.0;
2211 if (this.attr_("errorBars")) {
2212 if (this.wilsonInterval_
) {
2213 // For more details on this confidence interval, see:
2214 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2216 var p
= value
< 0 ? 0 : value
, n
= den
;
2217 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2218 var denom
= 1 + sigma
* sigma
/ den
;
2219 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2220 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2221 rollingData
[i
] = [date
,
2222 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2224 rollingData
[i
] = [date
, [0, 0, 0]];
2227 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2228 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2231 rollingData
[i
] = [date
, mult
* value
];
2234 } else if (this.attr_("customBars")) {
2239 for (var i
= 0; i
< originalData
.length
; i
++) {
2240 var data
= originalData
[i
][1];
2242 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2244 if (y
!= null && !isNaN(y
)) {
2250 if (i
- rollPeriod
>= 0) {
2251 var prev
= originalData
[i
- rollPeriod
];
2252 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2259 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2260 1.0 * (mid
- low
) / count
,
2261 1.0 * (high
- mid
) / count
]];
2264 // Calculate the rolling average for the first rollPeriod - 1 points where
2265 // there is not enough data to roll over the full number of days
2266 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2267 if (!this.attr_("errorBars")){
2268 if (rollPeriod
== 1) {
2269 return originalData
;
2272 for (var i
= 0; i
< originalData
.length
; i
++) {
2275 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2276 var y
= originalData
[j
][1];
2277 if (y
== null || isNaN(y
)) continue;
2279 sum
+= originalData
[j
][1];
2282 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2284 rollingData
[i
] = [originalData
[i
][0], null];
2289 for (var i
= 0; i
< originalData
.length
; i
++) {
2293 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2294 var y
= originalData
[j
][1][0];
2295 if (y
== null || isNaN(y
)) continue;
2297 sum
+= originalData
[j
][1][0];
2298 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2301 var stddev
= Math
.sqrt(variance
) / num_ok
;
2302 rollingData
[i
] = [originalData
[i
][0],
2303 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2305 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2315 * Parses a date, returning the number of milliseconds since epoch. This can be
2316 * passed in as an xValueParser in the Dygraph constructor.
2317 * TODO(danvk): enumerate formats that this understands.
2318 * @param {String} A date in YYYYMMDD format.
2319 * @return {Number} Milliseconds since epoch.
2322 Dygraph
.dateParser
= function(dateStr
, self
) {
2325 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2326 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2327 while (dateStrSlashed
.search("-") != -1) {
2328 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2330 d
= Date
.parse(dateStrSlashed
);
2331 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2332 // TODO(danvk): remove support for this format. It's confusing.
2333 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2334 + "/" + dateStr
.substr(6,2);
2335 d
= Date
.parse(dateStrSlashed
);
2337 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2338 // "2009/07/12 12:34:56"
2339 d
= Date
.parse(dateStr
);
2342 if (!d
|| isNaN(d
)) {
2343 self
.error("Couldn't parse " + dateStr
+ " as a date");
2349 * Detects the type of the str (date or numeric) and sets the various
2350 * formatting attributes in this.attrs_ based on this type.
2351 * @param {String} str An x value.
2354 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2356 if (str
.indexOf('-') >= 0 ||
2357 str
.indexOf('/') >= 0 ||
2358 isNaN(parseFloat(str
))) {
2360 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2361 // TODO(danvk): remove support for this format.
2366 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2367 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2368 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2369 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2371 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2372 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2373 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2374 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2379 * Parses a string in a special csv format. We expect a csv file where each
2380 * line is a date point, and the first field in each line is the date string.
2381 * We also expect that all remaining fields represent series.
2382 * if the errorBars attribute is set, then interpret the fields as:
2383 * date, series1, stddev1, series2, stddev2, ...
2384 * @param {Array.<Object>} data See above.
2387 * @return Array.<Object> An array with one entry for each row. These entries
2388 * are an array of cells in that row. The first entry is the parsed x-value for
2389 * the row. The second, third, etc. are the y-values. These can take on one of
2390 * three forms, depending on the CSV and constructor parameters:
2392 * 2. [ value, stddev ]
2393 * 3. [ low value, center value, high value ]
2395 Dygraph
.prototype.parseCSV_
= function(data
) {
2397 var lines
= data
.split("\n");
2399 // Use the default delimiter or fall back to a tab if that makes sense.
2400 var delim
= this.attr_('delimiter');
2401 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2406 if (this.labelsFromCSV_
) {
2408 this.attrs_
.labels
= lines
[0].split(delim
);
2411 // Parse the x as a float or return null if it's not a number.
2412 var parseFloatOrNull
= function(x
) {
2413 var val
= parseFloat(x
);
2414 return isNaN(val
) ? null : val
;
2418 var defaultParserSet
= false; // attempt to auto-detect x value type
2419 var expectedCols
= this.attr_("labels").length
;
2420 var outOfOrder
= false;
2421 for (var i
= start
; i
< lines
.length
; i
++) {
2422 var line
= lines
[i
];
2423 if (line
.length
== 0) continue; // skip blank lines
2424 if (line
[0] == '#') continue; // skip comment lines
2425 var inFields
= line
.split(delim
);
2426 if (inFields
.length
< 2) continue;
2429 if (!defaultParserSet
) {
2430 this.detectTypeFromString_(inFields
[0]);
2431 xParser
= this.attr_("xValueParser");
2432 defaultParserSet
= true;
2434 fields
[0] = xParser(inFields
[0], this);
2436 // If fractions are expected, parse the numbers as "A/B
"
2437 if (this.fractions_) {
2438 for (var j = 1; j < inFields.length; j++) {
2439 // TODO(danvk): figure out an appropriate way to flag parse errors.
2440 var vals = inFields[j].split("/");
2441 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2443 } else if (this.attr_("errorBars
")) {
2444 // If there are error bars, values are (value, stddev) pairs
2445 for (var j = 1; j < inFields.length; j += 2)
2446 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2447 parseFloatOrNull(inFields[j + 1])];
2448 } else if (this.attr_("customBars
")) {
2449 // Bars are a low;center;high tuple
2450 for (var j = 1; j < inFields.length; j++) {
2451 var vals = inFields[j].split(";");
2452 fields[j] = [ parseFloatOrNull(vals[0]),
2453 parseFloatOrNull(vals[1]),
2454 parseFloatOrNull(vals[2]) ];
2457 // Values are just numbers
2458 for (var j = 1; j < inFields.length; j++) {
2459 fields[j] = parseFloatOrNull(inFields[j]);
2462 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2467 if (fields.length != expectedCols) {
2468 this.error("Number of columns
in line
" + i + " (" + fields.length +
2469 ") does not agree
with number of
labels (" + expectedCols +
2475 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2476 ret.sort(function(a,b) { return a[0] - b[0] });
2483 * The user has provided their data as a pre-packaged JS array. If the x values
2484 * are numeric, this is the same as dygraphs' internal format. If the x values
2485 * are dates, we need to convert them from Date objects to ms since epoch.
2486 * @param {Array.<Object>} data
2487 * @return {Array.<Object>} data with numeric x values.
2489 Dygraph.prototype.parseArray_ = function(data) {
2490 // Peek at the first x value to see if it's numeric.
2491 if (data.length == 0) {
2492 this.error("Can
't plot empty data set");
2495 if (data[0].length == 0) {
2496 this.error("Data set cannot contain an empty row");
2500 if (this.attr_("labels") == null) {
2501 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2502 "in the options parameter");
2503 this.attrs_.labels = [ "X" ];
2504 for (var i = 1; i < data[0].length; i++) {
2505 this.attrs_.labels.push("Y" + i);
2509 if (Dygraph.isDateLike(data[0][0])) {
2510 // Some intelligent defaults for a date x-axis.
2511 this.attrs_.xValueFormatter = Dygraph.dateString_;
2512 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2513 this.attrs_.xTicker = Dygraph.dateTicker;
2515 // Assume they're all dates
.
2516 var parsedData
= Dygraph
.clone(data
);
2517 for (var i
= 0; i
< data
.length
; i
++) {
2518 if (parsedData
[i
].length
== 0) {
2519 this.error("Row " + (1 + i
) + " of data is empty");
2522 if (parsedData
[i
][0] == null
2523 || typeof(parsedData
[i
][0].getTime
) != 'function'
2524 || isNaN(parsedData
[i
][0].getTime())) {
2525 this.error("x value in row " + (1 + i
) + " is not a Date");
2528 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2532 // Some intelligent defaults for a numeric x-axis.
2533 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2534 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2540 * Parses a DataTable object from gviz.
2541 * The data is expected to have a first column that is either a date or a
2542 * number. All subsequent columns must be numbers. If there is a clear mismatch
2543 * between this.xValueParser_ and the type of the first column, it will be
2544 * fixed. Fills out rawData_.
2545 * @param {Array.<Object>} data See above.
2548 Dygraph
.prototype.parseDataTable_
= function(data
) {
2549 var cols
= data
.getNumberOfColumns();
2550 var rows
= data
.getNumberOfRows();
2552 var indepType
= data
.getColumnType(0);
2553 if (indepType
== 'date' || indepType
== 'datetime') {
2554 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2555 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2556 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2557 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2558 } else if (indepType
== 'number') {
2559 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2560 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2561 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2562 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2564 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2565 "column 1 of DataTable input (Got '" + indepType
+ "')");
2569 // Array of the column indices which contain data (and not annotations).
2571 var annotationCols
= {}; // data index -> [annotation cols]
2572 var hasAnnotations
= false;
2573 for (var i
= 1; i
< cols
; i
++) {
2574 var type
= data
.getColumnType(i
);
2575 if (type
== 'number') {
2577 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2578 // This is OK -- it's an annotation column.
2579 var dataIdx
= colIdx
[colIdx
.length
- 1];
2580 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2581 annotationCols
[dataIdx
] = [i
];
2583 annotationCols
[dataIdx
].push(i
);
2585 hasAnnotations
= true;
2587 this.error("Only 'number' is supported as a dependent type with Gviz." +
2588 " 'string' is only supported if displayAnnotations is true");
2592 // Read column labels
2593 // TODO(danvk): add support back for errorBars
2594 var labels
= [data
.getColumnLabel(0)];
2595 for (var i
= 0; i
< colIdx
.length
; i
++) {
2596 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2597 if (this.attr_("errorBars")) i
+= 1;
2599 this.attrs_
.labels
= labels
;
2600 cols
= labels
.length
;
2603 var outOfOrder
= false;
2604 var annotations
= [];
2605 for (var i
= 0; i
< rows
; i
++) {
2607 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2608 data
.getValue(i
, 0) === null) {
2609 this.warn("Ignoring row " + i
+
2610 " of DataTable because of undefined or null first column.");
2614 if (indepType
== 'date' || indepType
== 'datetime') {
2615 row
.push(data
.getValue(i
, 0).getTime());
2617 row
.push(data
.getValue(i
, 0));
2619 if (!this.attr_("errorBars")) {
2620 for (var j
= 0; j
< colIdx
.length
; j
++) {
2621 var col
= colIdx
[j
];
2622 row
.push(data
.getValue(i
, col
));
2623 if (hasAnnotations
&&
2624 annotationCols
.hasOwnProperty(col
) &&
2625 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2627 ann
.series
= data
.getColumnLabel(col
);
2629 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2631 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2632 if (k
) ann
.text
+= "\n";
2633 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2635 annotations
.push(ann
);
2639 for (var j
= 0; j
< cols
- 1; j
++) {
2640 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2643 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2650 this.warn("DataTable is out of order; order it correctly to speed loading.");
2651 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2653 this.rawData_
= ret
;
2655 if (annotations
.length
> 0) {
2656 this.setAnnotations(annotations
, true);
2660 // These functions are all based on MochiKit.
2661 Dygraph
.update
= function (self
, o
) {
2662 if (typeof(o
) != 'undefined' && o
!== null) {
2664 if (o
.hasOwnProperty(k
)) {
2672 Dygraph
.isArrayLike
= function (o
) {
2673 var typ
= typeof(o
);
2675 (typ
!= 'object' && !(typ
== 'function' &&
2676 typeof(o
.item
) == 'function')) ||
2678 typeof(o
.length
) != 'number' ||
2686 Dygraph
.isDateLike
= function (o
) {
2687 if (typeof(o
) != "object" || o
=== null ||
2688 typeof(o
.getTime
) != 'function') {
2694 Dygraph
.clone
= function(o
) {
2695 // TODO(danvk): figure out how MochiKit's version works
2697 for (var i
= 0; i
< o
.length
; i
++) {
2698 if (Dygraph
.isArrayLike(o
[i
])) {
2699 r
.push(Dygraph
.clone(o
[i
]));
2709 * Get the CSV data. If it's in a function, call that function. If it's in a
2710 * file, do an XMLHttpRequest to get it.
2713 Dygraph
.prototype.start_
= function() {
2714 if (typeof this.file_
== 'function') {
2715 // CSV string. Pretend we got it via XHR.
2716 this.loadedEvent_(this.file_());
2717 } else if (Dygraph
.isArrayLike(this.file_
)) {
2718 this.rawData_
= this.parseArray_(this.file_
);
2720 } else if (typeof this.file_
== 'object' &&
2721 typeof this.file_
.getColumnRange
== 'function') {
2722 // must be a DataTable from gviz.
2723 this.parseDataTable_(this.file_
);
2725 } else if (typeof this.file_
== 'string') {
2726 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2727 if (this.file_
.indexOf('\n') >= 0) {
2728 this.loadedEvent_(this.file_
);
2730 var req
= new XMLHttpRequest();
2732 req
.onreadystatechange
= function () {
2733 if (req
.readyState
== 4) {
2734 if (req
.status
== 200) {
2735 caller
.loadedEvent_(req
.responseText
);
2740 req
.open("GET", this.file_
, true);
2744 this.error("Unknown data format: " + (typeof this.file_
));
2749 * Changes various properties of the graph. These can include:
2751 * <li>file: changes the source data for the graph</li>
2752 * <li>errorBars: changes whether the data contains stddev</li>
2754 * @param {Object} attrs The new properties and values
2756 Dygraph
.prototype.updateOptions
= function(attrs
) {
2757 // TODO(danvk): this is a mess. Rethink this function.
2758 if ('rollPeriod' in attrs
) {
2759 this.rollPeriod_
= attrs
.rollPeriod
;
2761 if ('dateWindow' in attrs
) {
2762 this.dateWindow_
= attrs
.dateWindow
;
2765 // TODO(danvk): validate per-series options.
2770 // highlightCircleSize
2772 Dygraph
.update(this.user_attrs_
, attrs
);
2773 Dygraph
.update(this.renderOptions_
, attrs
);
2775 this.labelsFromCSV_
= (this.attr_("labels") == null);
2777 // TODO(danvk): this doesn't match the constructor logic
2778 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2779 if (attrs
['file']) {
2780 this.file_
= attrs
['file'];
2788 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2789 * containing div (which has presumably changed size since the dygraph was
2790 * instantiated. If the width/height are specified, the div will be resized.
2792 * This is far more efficient than destroying and re-instantiating a
2793 * Dygraph, since it doesn't have to reparse the underlying data.
2795 * @param {Number} width Width (in pixels)
2796 * @param {Number} height Height (in pixels)
2798 Dygraph
.prototype.resize
= function(width
, height
) {
2799 if (this.resize_lock
) {
2802 this.resize_lock
= true;
2804 if ((width
=== null) != (height
=== null)) {
2805 this.warn("Dygraph.resize() should be called with zero parameters or " +
2806 "two non-NULL parameters. Pretending it was zero.");
2807 width
= height
= null;
2810 // TODO(danvk): there should be a clear() method.
2811 this.maindiv_
.innerHTML
= "";
2812 this.attrs_
.labelsDiv
= null;
2815 this.maindiv_
.style
.width
= width
+ "px";
2816 this.maindiv_
.style
.height
= height
+ "px";
2817 this.width_
= width
;
2818 this.height_
= height
;
2820 this.width_
= this.maindiv_
.offsetWidth
;
2821 this.height_
= this.maindiv_
.offsetHeight
;
2824 this.createInterface_();
2827 this.resize_lock
= false;
2831 * Adjusts the number of days in the rolling average. Updates the graph to
2832 * reflect the new averaging period.
2833 * @param {Number} length Number of days over which to average the data.
2835 Dygraph
.prototype.adjustRoll
= function(length
) {
2836 this.rollPeriod_
= length
;
2841 * Returns a boolean array of visibility statuses.
2843 Dygraph
.prototype.visibility
= function() {
2844 // Do lazy-initialization, so that this happens after we know the number of
2846 if (!this.attr_("visibility")) {
2847 this.attrs_
["visibility"] = [];
2849 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2850 this.attr_("visibility").push(true);
2852 return this.attr_("visibility");
2856 * Changes the visiblity of a series.
2858 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2859 var x
= this.visibility();
2860 if (num
< 0 || num
>= x
.length
) {
2861 this.warn("invalid series number in setVisibility: " + num
);
2869 * Update the list of annotations and redraw the chart.
2871 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2872 // Only add the annotation CSS rule once we know it will be used.
2873 Dygraph
.addAnnotationRule();
2874 this.annotations_
= ann
;
2875 this.layout_
.setAnnotations(this.annotations_
);
2876 if (!suppressDraw
) {
2882 * Return the list of annotations.
2884 Dygraph
.prototype.annotations
= function() {
2885 return this.annotations_
;
2889 * Get the index of a series (column) given its name. The first column is the
2890 * x-axis, so the data series start with index 1.
2892 Dygraph
.prototype.indexFromSetName
= function(name
) {
2893 var labels
= this.attr_("labels");
2894 for (var i
= 0; i
< labels
.length
; i
++) {
2895 if (labels
[i
] == name
) return i
;
2900 Dygraph
.addAnnotationRule
= function() {
2901 if (Dygraph
.addedAnnotationCSS
) return;
2904 if (document
.styleSheets
.length
> 0) {
2905 mysheet
= document
.styleSheets
[0];
2907 var styleSheetElement
= document
.createElement("style");
2908 styleSheetElement
.type
= "text/css";
2909 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2910 for(i
= 0; i
< document
.styleSheets
.length
; i
++) {
2911 if (document
.styleSheets
[i
].disabled
) continue;
2912 mysheet
= document
.styleSheets
[i
];
2916 var rule
= "border: 1px solid black; " +
2917 "background-color: white; " +
2918 "text-align: center;";
2919 if (mysheet
.insertRule
) { // Firefox
2920 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2921 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2922 } else if (mysheet
.addRule
) { // IE
2923 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2926 Dygraph
.addedAnnotationCSS
= true;
2930 * Create a new canvas element. This is more complex than a simple
2931 * document.createElement("canvas") because of IE and excanvas.
2933 Dygraph
.createCanvas
= function() {
2934 var canvas
= document
.createElement("canvas");
2936 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
2937 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
2938 canvas
= G_vmlCanvasManager
.initElement(canvas
);
2946 * A wrapper around Dygraph that implements the gviz API.
2947 * @param {Object} container The DOM object the visualization should live in.
2949 Dygraph
.GVizChart
= function(container
) {
2950 this.container
= container
;
2953 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
2954 this.container
.innerHTML
= '';
2955 this.date_graph
= new Dygraph(this.container
, data
, options
);
2959 * Google charts compatible setSelection
2960 * Only row selection is supported, all points in the row will be highlighted
2961 * @param {Array} array of the selected cells
2964 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
2966 if (selection_array
.length
) {
2967 row
= selection_array
[0].row
;
2969 this.date_graph
.setSelection(row
);
2973 * Google charts compatible getSelection implementation
2974 * @return {Array} array of the selected cells
2977 Dygraph
.GVizChart
.prototype.getSelection
= function() {
2980 var row
= this.date_graph
.getSelection();
2982 if (row
< 0) return selection
;
2985 for (var i
in this.date_graph
.layout_
.datasets
) {
2986 selection
.push({row
: row
, column
: col
});
2993 // Older pages may still use this name.
2994 DateGraph
= Dygraph
;