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://dygraphs.com/
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;
83 // Default attribute values.
84 Dygraph
.DEFAULT_ATTRS
= {
85 highlightCircleSize
: 3,
91 // TODO(danvk): move defaults from createStatusMessage_ here.
93 labelsSeparateLines
: false,
94 labelsShowZeroValues
: true,
97 showLabelsOnHighlight
: true,
99 yValueFormatter
: function(x
, opt_numDigits
) {
100 return x
.toPrecision(Math
.min(21, Math
.max(1, opt_numDigits
|| 2)));
106 axisLabelFontSize
: 14,
109 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
113 xValueFormatter
: Dygraph
.dateString_
,
114 xValueParser
: Dygraph
.dateParser
,
115 xTicker
: Dygraph
.dateTicker
,
123 wilsonInterval
: true, // only relevant if fractions is true
127 connectSeparatedPoints
: false,
130 hideOverlayOnMouseOut
: true,
135 interactionModel
: null // will be set to Dygraph.defaultInteractionModel.
138 // Various logging levels.
144 // Directions for panning and zooming. Use bit operations when combined
145 // values are possible.
146 Dygraph
.HORIZONTAL
= 1;
147 Dygraph
.VERTICAL
= 2;
149 // Used for initializing annotation CSS rules only once.
150 Dygraph
.addedAnnotationCSS
= false;
152 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
153 // Labels is no longer a constructor parameter, since it's typically set
154 // directly from the data source. It also conains a name for the x-axis,
155 // which the previous constructor form did not.
156 if (labels
!= null) {
157 var new_labels
= ["Date"];
158 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
159 Dygraph
.update(attrs
, { 'labels': new_labels
});
161 this.__init__(div
, file
, attrs
);
165 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
166 * and context <canvas> inside of it. See the constructor for details.
168 * @param {Element} div the Element to render the graph into.
169 * @param {String | Function} file Source data
170 * @param {Object} attrs Miscellaneous other options
173 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
174 // Hack for IE: if we're using excanvas and the document hasn't finished
175 // loading yet (and hence may not have initialized whatever it needs to
176 // initialize), then keep calling this routine periodically until it has.
177 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
178 typeof(G_vmlCanvasManager
) != 'undefined' &&
179 document
.readyState
!= 'complete') {
181 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
184 // Support two-argument constructor
185 if (attrs
== null) { attrs
= {}; }
187 // Copy the important bits into the object
188 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
191 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
192 this.previousVerticalX_
= -1;
193 this.fractions_
= attrs
.fractions
|| false;
194 this.dateWindow_
= attrs
.dateWindow
|| null;
196 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
197 this.is_initial_draw_
= true;
198 this.annotations_
= [];
200 // Number of digits to use when labeling the x (if numeric) and y axis
202 this.numXDigits_
= 2;
203 this.numYDigits_
= 2;
205 // When labeling x (if numeric) or y values in the legend, there are
206 // numDigits + numExtraDigits of precision used. For axes labels with N
207 // digits of precision, the data should be displayed with at least N+1 digits
208 // of precision. The reason for this is to divide each interval between
209 // successive ticks into tenths (for 1) or hundredths (for 2), etc. For
210 // example, if the labels are [0, 1, 2], we want data to be displayed as
212 this.numExtraDigits_
= 1;
214 // Clear the div. This ensure that, if multiple dygraphs are passed the same
215 // div, then only one will be drawn.
218 // If the div isn't already sized then inherit from our attrs or
219 // give it a default size.
220 if (div
.style
.width
== '') {
221 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
223 if (div
.style
.height
== '') {
224 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
226 this.width_
= parseInt(div
.style
.width
, 10);
227 this.height_
= parseInt(div
.style
.height
, 10);
228 // The div might have been specified as percent of the current window size,
229 // convert that to an appropriate number of pixels.
230 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
231 this.width_
= div
.offsetWidth
;
233 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
234 this.height_
= div
.offsetHeight
;
237 if (this.width_
== 0) {
238 this.error("dygraph has zero width. Please specify a width in pixels.");
240 if (this.height_
== 0) {
241 this.error("dygraph has zero height. Please specify a height in pixels.");
244 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
245 if (attrs
['stackedGraph']) {
246 attrs
['fillGraph'] = true;
247 // TODO(nikhilk): Add any other stackedGraph checks here.
250 // Dygraphs has many options, some of which interact with one another.
251 // To keep track of everything, we maintain two sets of options:
253 // this.user_attrs_ only options explicitly set by the user.
254 // this.attrs_ defaults, options derived from user_attrs_, data.
256 // Options are then accessed this.attr_('attr'), which first looks at
257 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
258 // defaults without overriding behavior that the user specifically asks for.
259 this.user_attrs_
= {};
260 Dygraph
.update(this.user_attrs_
, attrs
);
263 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
265 this.boundaryIds_
= [];
267 // Make a note of whether labels will be pulled from the CSV file.
268 this.labelsFromCSV_
= (this.attr_("labels") == null);
270 // Create the containing DIV and other interactive elements
271 this.createInterface_();
276 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
278 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
279 this.user_attrs_
[seriesName
] != null &&
280 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
281 return this.user_attrs_
[seriesName
][name
];
282 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
283 return this.user_attrs_
[name
];
284 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
285 return this.attrs_
[name
];
291 // TODO(danvk): any way I can get the line numbers to be this.warn call?
292 Dygraph
.prototype.log
= function(severity
, message
) {
293 if (typeof(console
) != 'undefined') {
296 console
.debug('dygraphs: ' + message
);
299 console
.info('dygraphs: ' + message
);
301 case Dygraph
.WARNING
:
302 console
.warn('dygraphs: ' + message
);
305 console
.error('dygraphs: ' + message
);
310 Dygraph
.prototype.info
= function(message
) {
311 this.log(Dygraph
.INFO
, message
);
313 Dygraph
.prototype.warn
= function(message
) {
314 this.log(Dygraph
.WARNING
, message
);
316 Dygraph
.prototype.error
= function(message
) {
317 this.log(Dygraph
.ERROR
, message
);
321 * Returns the current rolling period, as set by the user or an option.
322 * @return {Number} The number of points in the rolling window
324 Dygraph
.prototype.rollPeriod
= function() {
325 return this.rollPeriod_
;
329 * Returns the currently-visible x-range. This can be affected by zooming,
330 * panning or a call to updateOptions.
331 * Returns a two-element array: [left, right].
332 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
334 Dygraph
.prototype.xAxisRange
= function() {
335 if (this.dateWindow_
) return this.dateWindow_
;
337 // The entire chart is visible.
338 var left
= this.rawData_
[0][0];
339 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
340 return [left
, right
];
344 * Returns the currently-visible y-range for an axis. This can be affected by
345 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
346 * called with no arguments, returns the range of the first axis.
347 * Returns a two-element array: [bottom, top].
349 Dygraph
.prototype.yAxisRange
= function(idx
) {
350 if (typeof(idx
) == "undefined") idx
= 0;
351 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
352 return [ this.axes_
[idx
].computedValueRange
[0],
353 this.axes_
[idx
].computedValueRange
[1] ];
357 * Returns the currently-visible y-ranges for each axis. This can be affected by
358 * zooming, panning, calls to updateOptions, etc.
359 * Returns an array of [bottom, top] pairs, one for each y-axis.
361 Dygraph
.prototype.yAxisRanges
= function() {
363 for (var i
= 0; i
< this.axes_
.length
; i
++) {
364 ret
.push(this.yAxisRange(i
));
369 // TODO(danvk): use these functions throughout dygraphs.
371 * Convert from data coordinates to canvas/div X/Y coordinates.
372 * If specified, do this conversion for the coordinate system of a particular
373 * axis. Uses the first axis by default.
374 * Returns a two-element array: [X, Y]
376 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
377 var ret
= [null, null];
378 var area
= this.plotter_
.area
;
380 var xRange
= this.xAxisRange();
381 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
385 var yRange
= this.yAxisRange(axis
);
386 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
393 * Convert from canvas/div coords to data coordinates.
394 * If specified, do this conversion for the coordinate system of a particular
395 * axis. Uses the first axis by default.
396 * Returns a two-element array: [X, Y]
398 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
399 var ret
= [null, null];
400 var area
= this.plotter_
.area
;
402 var xRange
= this.xAxisRange();
403 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
407 var yRange
= this.yAxisRange(axis
);
408 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
415 * Returns the number of columns (including the independent variable).
417 Dygraph
.prototype.numColumns
= function() {
418 return this.rawData_
[0].length
;
422 * Returns the number of rows (excluding any header/label row).
424 Dygraph
.prototype.numRows
= function() {
425 return this.rawData_
.length
;
429 * Returns the value in the given row and column. If the row and column exceed
430 * the bounds on the data, returns null. Also returns null if the value is
433 Dygraph
.prototype.getValue
= function(row
, col
) {
434 if (row
< 0 || row
> this.rawData_
.length
) return null;
435 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
437 return this.rawData_
[row
][col
];
440 Dygraph
.addEvent
= function(el
, evt
, fn
) {
441 var normed_fn
= function(e
) {
442 if (!e
) var e
= window
.event
;
445 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
446 el
.addEventListener(evt
, normed_fn
, false);
448 el
.attachEvent('on' + evt
, normed_fn
);
453 // Based on the article at
454 // http://www.switchonthecode.com/tutorials
/javascript
-tutorial
-the
-scroll
-wheel
455 Dygraph
.cancelEvent
= function(e
) {
456 e
= e
? e
: window
.event
;
457 if (e
.stopPropagation
) {
460 if (e
.preventDefault
) {
463 e
.cancelBubble
= true;
465 e
.returnValue
= false;
470 * Generates interface elements for the Dygraph: a containing div, a div to
471 * display the current point, and a textbox to adjust the rolling average
472 * period. Also creates the Renderer/Layout elements.
475 Dygraph
.prototype.createInterface_
= function() {
476 // Create the all-enclosing graph div
477 var enclosing
= this.maindiv_
;
479 this.graphDiv
= document
.createElement("div");
480 this.graphDiv
.style
.width
= this.width_
+ "px";
481 this.graphDiv
.style
.height
= this.height_
+ "px";
482 enclosing
.appendChild(this.graphDiv
);
484 // Create the canvas for interactive parts of the chart.
485 this.canvas_
= Dygraph
.createCanvas();
486 this.canvas_
.style
.position
= "absolute";
487 this.canvas_
.width
= this.width_
;
488 this.canvas_
.height
= this.height_
;
489 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
490 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
492 // ... and for static parts of the chart.
493 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
495 // The interactive parts of the graph are drawn on top of the chart.
496 this.graphDiv
.appendChild(this.hidden_
);
497 this.graphDiv
.appendChild(this.canvas_
);
498 this.mouseEventElement_
= this.canvas_
;
501 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
502 dygraph
.mouseMove_(e
);
504 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
505 dygraph
.mouseOut_(e
);
508 // Create the grapher
509 // TODO(danvk): why does the Layout need its own set of options?
510 this.layoutOptions_
= { 'xOriginIsZero': false };
511 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
512 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
513 Dygraph
.update(this.layoutOptions_
, {
514 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
516 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
518 // TODO(danvk): why does the Renderer need its own set of options?
519 this.renderOptions_
= { colorScheme
: this.colors_
,
521 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
522 Dygraph
.update(this.renderOptions_
, this.attrs_
);
523 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
525 this.createStatusMessage_();
526 this.createDragInterface_();
530 * Detach DOM elements in the dygraph and null out all data references.
531 * Calling this when you're done with a dygraph can dramatically reduce memory
532 * usage. See, e.g., the tests/perf.html example.
534 Dygraph
.prototype.destroy
= function() {
535 var removeRecursive
= function(node
) {
536 while (node
.hasChildNodes()) {
537 removeRecursive(node
.firstChild
);
538 node
.removeChild(node
.firstChild
);
541 removeRecursive(this.maindiv_
);
543 var nullOut
= function(obj
) {
545 if (typeof(obj
[n
]) === 'object') {
551 // These may not all be necessary, but it can't hurt...
552 nullOut(this.layout_
);
553 nullOut(this.plotter_
);
558 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
559 * this particular canvas. All Dygraph work is done on this.canvas_.
560 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
561 * @return {Object} The newly-created canvas
564 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
565 var h
= Dygraph
.createCanvas();
566 h
.style
.position
= "absolute";
567 // TODO(danvk): h should be offset from canvas. canvas needs to include
568 // some extra area to make it easier to zoom in on the far left and far
569 // right. h needs to be precisely the plot area, so that clipping occurs.
570 h
.style
.top
= canvas
.style
.top
;
571 h
.style
.left
= canvas
.style
.left
;
572 h
.width
= this.width_
;
573 h
.height
= this.height_
;
574 h
.style
.width
= this.width_
+ "px"; // for IE
575 h
.style
.height
= this.height_
+ "px"; // for IE
579 // Taken from MochiKit.Color
580 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
584 if (saturation
=== 0) {
589 var i
= Math
.floor(hue
* 6);
590 var f
= (hue
* 6) - i
;
591 var p
= value
* (1 - saturation
);
592 var q
= value
* (1 - (saturation
* f
));
593 var t
= value
* (1 - (saturation
* (1 - f
)));
595 case 1: red
= q
; green
= value
; blue
= p
; break;
596 case 2: red
= p
; green
= value
; blue
= t
; break;
597 case 3: red
= p
; green
= q
; blue
= value
; break;
598 case 4: red
= t
; green
= p
; blue
= value
; break;
599 case 5: red
= value
; green
= p
; blue
= q
; break;
600 case 6: // fall through
601 case 0: red
= value
; green
= t
; blue
= p
; break;
604 red
= Math
.floor(255 * red
+ 0.5);
605 green
= Math
.floor(255 * green
+ 0.5);
606 blue
= Math
.floor(255 * blue
+ 0.5);
607 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
612 * Generate a set of distinct colors for the data series. This is done with a
613 * color wheel. Saturation/Value are customizable, and the hue is
614 * equally-spaced around the color wheel. If a custom set of colors is
615 * specified, that is used instead.
618 Dygraph
.prototype.setColors_
= function() {
619 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
620 // away with this.renderOptions_.
621 var num
= this.attr_("labels").length
- 1;
623 var colors
= this.attr_('colors');
625 var sat
= this.attr_('colorSaturation') || 1.0;
626 var val
= this.attr_('colorValue') || 0.5;
627 var half
= Math
.ceil(num
/ 2);
628 for (var i
= 1; i
<= num
; i
++) {
629 if (!this.visibility()[i
-1]) continue;
630 // alternate colors for high contrast.
631 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
632 var hue
= (1.0 * idx
/ (1 + num
));
633 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
636 for (var i
= 0; i
< num
; i
++) {
637 if (!this.visibility()[i
]) continue;
638 var colorStr
= colors
[i
% colors
.length
];
639 this.colors_
.push(colorStr
);
643 // TODO(danvk): update this w/r
/t/ the
new options system
.
644 this.renderOptions_
.colorScheme
= this.colors_
;
645 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
646 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
647 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
651 * Return the list of colors. This is either the list of colors passed in the
652 * attributes, or the autogenerated list of rgb(r,g,b) strings.
653 * @return {Array<string>} The list of colors.
655 Dygraph
.prototype.getColors
= function() {
659 // The following functions are from quirksmode.org with a modification for Safari from
660 // http://blog.firetree.net/2005/07/04/javascript-find-position/
661 // http://www.quirksmode.org/js
/findpos
.html
662 Dygraph
.findPosX
= function(obj
) {
667 curleft
+= obj
.offsetLeft
;
668 if(!obj
.offsetParent
)
670 obj
= obj
.offsetParent
;
677 Dygraph
.findPosY
= function(obj
) {
682 curtop
+= obj
.offsetTop
;
683 if(!obj
.offsetParent
)
685 obj
= obj
.offsetParent
;
695 * Create the div that contains information on the selected point(s)
696 * This goes in the top right of the canvas, unless an external div has already
700 Dygraph
.prototype.createStatusMessage_
= function() {
701 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
702 if (userLabelsDiv
&& null != userLabelsDiv
703 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
704 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
706 if (!this.attr_("labelsDiv")) {
707 var divWidth
= this.attr_('labelsDivWidth');
709 "position": "absolute",
712 "width": divWidth
+ "px",
714 "left": (this.width_
- divWidth
- 2) + "px",
715 "background": "white",
717 "overflow": "hidden"};
718 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
719 var div
= document
.createElement("div");
720 for (var name
in messagestyle
) {
721 if (messagestyle
.hasOwnProperty(name
)) {
722 div
.style
[name
] = messagestyle
[name
];
725 this.graphDiv
.appendChild(div
);
726 this.attrs_
.labelsDiv
= div
;
731 * Position the labels div so that its right edge is flush with the right edge
732 * of the charting area.
734 Dygraph
.prototype.positionLabelsDiv_
= function() {
735 // Don't touch a user-specified labelsDiv.
736 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
738 var area
= this.plotter_
.area
;
739 var div
= this.attr_("labelsDiv");
740 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
744 * Create the text box to adjust the averaging period
747 Dygraph
.prototype.createRollInterface_
= function() {
748 // Create a roller if one doesn't exist already.
750 this.roller_
= document
.createElement("input");
751 this.roller_
.type
= "text";
752 this.roller_
.style
.display
= "none";
753 this.graphDiv
.appendChild(this.roller_
);
756 var display
= this.attr_('showRoller') ? 'block' : 'none';
758 var textAttr
= { "position": "absolute",
760 "top": (this.plotter_
.area
.h
- 25) + "px",
761 "left": (this.plotter_
.area
.x
+ 1) + "px",
764 this.roller_
.size
= "2";
765 this.roller_
.value
= this.rollPeriod_
;
766 for (var name
in textAttr
) {
767 if (textAttr
.hasOwnProperty(name
)) {
768 this.roller_
.style
[name
] = textAttr
[name
];
773 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
776 // These functions are taken from MochiKit.Signal
777 Dygraph
.pageX
= function(e
) {
779 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
782 var b
= document
.body
;
784 (de
.scrollLeft
|| b
.scrollLeft
) -
785 (de
.clientLeft
|| 0);
789 Dygraph
.pageY
= function(e
) {
791 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
794 var b
= document
.body
;
796 (de
.scrollTop
|| b
.scrollTop
) -
801 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
802 return Dygraph
.pageX(e
) - context
.px
805 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
806 return Dygraph
.pageY(e
) - context
.py
809 // Called in response to an interaction model operation that
810 // should start the default panning behavior.
812 // It's used in the default callback for "mousedown" operations.
813 // Custom interaction model builders can use it to provide the default
816 Dygraph
.startPan
= function(event
, g
, context
) {
817 // have to be zoomed in to pan.
818 // TODO(konigsberg): Let's loosen this zoom-to-pan restriction, also
819 // perhaps create panning boundaries? A more flexible pan would make it,
820 // ahem, 'pan-useful'.
822 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
823 if (g
.axes_
[i
].valueWindow
|| g
.axes_
[i
].valueRange
) {
828 if (!g
.dateWindow_
&& !zoomedY
) return;
830 context
.isPanning
= true;
831 var xRange
= g
.xAxisRange();
832 context
.dateRange
= xRange
[1] - xRange
[0];
834 // Record the range of each y-axis at the start of the drag.
835 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
836 context
.is2DPan
= false;
837 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
838 var axis
= g
.axes_
[i
];
839 var yRange
= g
.yAxisRange(i
);
840 axis
.dragValueRange
= yRange
[1] - yRange
[0];
841 var r
= g
.toDataCoords(null, context
.dragStartY
, i
);
842 axis
.draggingValue
= r
[1];
843 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
846 // TODO(konigsberg): Switch from all this math to toDataCoords?
847 // Seems to work for the dragging value.
848 context
.draggingDate
= (context
.dragStartX
/ g
.width_
) * context
.dateRange
+ xRange
[0];
851 // Called in response to an interaction model operation that
852 // responds to an event that pans the view.
854 // It's used in the default callback for "mousemove" operations.
855 // Custom interaction model builders can use it to provide the default
858 Dygraph
.movePan
= function(event
, g
, context
) {
859 context
.dragEndX
= g
.dragGetX_(event
, context
);
860 context
.dragEndY
= g
.dragGetY_(event
, context
);
862 // TODO(danvk): update this comment
863 // Want to have it so that:
864 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
865 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
866 // 3. draggingValue appears at dragEndY.
867 // 4. valueRange is unaltered.
869 var minDate
= context
.draggingDate
- (context
.dragEndX
/ g
.width_
) * context
.dateRange
;
870 var maxDate
= minDate
+ context
.dateRange
;
871 g
.dateWindow_
= [minDate
, maxDate
];
873 // y-axis scaling is automatic unless this is a full 2D pan.
874 if (context
.is2DPan
) {
875 // Adjust each axis appropriately.
876 var y_frac
= context
.dragEndY
/ g
.height_
;
877 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
878 var axis
= g
.axes_
[i
];
879 var maxValue
= axis
.draggingValue
+ y_frac
* axis
.dragValueRange
;
880 var minValue
= maxValue
- axis
.dragValueRange
;
881 axis
.valueWindow
= [ minValue
, maxValue
];
888 // Called in response to an interaction model operation that
889 // responds to an event that ends panning.
891 // It's used in the default callback for "mouseup" operations.
892 // Custom interaction model builders can use it to provide the default
895 Dygraph
.endPan
= function(event
, g
, context
) {
896 context
.isPanning
= false;
897 context
.is2DPan
= false;
898 context
.draggingDate
= null;
899 context
.dateRange
= null;
900 context
.valueRange
= null;
903 // Called in response to an interaction model operation that
904 // responds to an event that starts zooming.
906 // It's used in the default callback for "mousedown" operations.
907 // Custom interaction model builders can use it to provide the default
910 Dygraph
.startZoom
= function(event
, g
, context
) {
911 context
.isZooming
= true;
914 // Called in response to an interaction model operation that
915 // responds to an event that defines zoom boundaries.
917 // It's used in the default callback for "mousemove" operations.
918 // Custom interaction model builders can use it to provide the default
921 Dygraph
.moveZoom
= function(event
, g
, context
) {
922 context
.dragEndX
= g
.dragGetX_(event
, context
);
923 context
.dragEndY
= g
.dragGetY_(event
, context
);
925 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
926 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
928 // drag direction threshold for y axis is twice as large as x axis
929 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
932 context
.dragDirection
,
937 context
.prevDragDirection
,
941 context
.prevEndX
= context
.dragEndX
;
942 context
.prevEndY
= context
.dragEndY
;
943 context
.prevDragDirection
= context
.dragDirection
;
946 // Called in response to an interaction model operation that
947 // responds to an event that performs a zoom based on previously defined
950 // It's used in the default callback for "mouseup" operations.
951 // Custom interaction model builders can use it to provide the default
954 Dygraph
.endZoom
= function(event
, g
, context
) {
955 context
.isZooming
= false;
956 context
.dragEndX
= g
.dragGetX_(event
, context
);
957 context
.dragEndY
= g
.dragGetY_(event
, context
);
958 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
959 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
961 if (regionWidth
< 2 && regionHeight
< 2 &&
962 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
963 // TODO(danvk): pass along more info about the points, e.g. 'x'
964 if (g
.attr_('clickCallback') != null) {
965 g
.attr_('clickCallback')(event
, g
.lastx_
, g
.selPoints_
);
967 if (g
.attr_('pointClickCallback')) {
968 // check if the click was on a particular point.
970 var closestDistance
= 0;
971 for (var i
= 0; i
< g
.selPoints_
.length
; i
++) {
972 var p
= g
.selPoints_
[i
];
973 var distance
= Math
.pow(p
.canvasx
- context
.dragEndX
, 2) +
974 Math
.pow(p
.canvasy
- context
.dragEndY
, 2);
975 if (closestIdx
== -1 || distance
< closestDistance
) {
976 closestDistance
= distance
;
981 // Allow any click within two pixels of the dot.
982 var radius
= g
.attr_('highlightCircleSize') + 2;
983 if (closestDistance
<= 5 * 5) {
984 g
.attr_('pointClickCallback')(event
, g
.selPoints_
[closestIdx
]);
989 if (regionWidth
>= 10 && context
.dragDirection
== Dygraph
.HORIZONTAL
) {
990 g
.doZoomX_(Math
.min(context
.dragStartX
, context
.dragEndX
),
991 Math
.max(context
.dragStartX
, context
.dragEndX
));
992 } else if (regionHeight
>= 10 && context
.dragDirection
== Dygraph
.VERTICAL
) {
993 g
.doZoomY_(Math
.min(context
.dragStartY
, context
.dragEndY
),
994 Math
.max(context
.dragStartY
, context
.dragEndY
));
996 g
.canvas_
.getContext("2d").clearRect(0, 0,
1000 context
.dragStartX
= null;
1001 context
.dragStartY
= null;
1004 Dygraph
.defaultInteractionModel
= {
1005 // Track the beginning of drag events
1006 mousedown
: function(event
, g
, context
) {
1007 context
.initializeMouseDown(event
, g
, context
);
1009 if (event
.altKey
|| event
.shiftKey
) {
1010 Dygraph
.startPan(event
, g
, context
);
1012 Dygraph
.startZoom(event
, g
, context
);
1016 // Draw zoom rectangles when the mouse is down and the user moves around
1017 mousemove
: function(event
, g
, context
) {
1018 if (context
.isZooming
) {
1019 Dygraph
.moveZoom(event
, g
, context
);
1020 } else if (context
.isPanning
) {
1021 Dygraph
.movePan(event
, g
, context
);
1025 mouseup
: function(event
, g
, context
) {
1026 if (context
.isZooming
) {
1027 Dygraph
.endZoom(event
, g
, context
);
1028 } else if (context
.isPanning
) {
1029 Dygraph
.endPan(event
, g
, context
);
1033 // Temporarily cancel the dragging event when the mouse leaves the graph
1034 mouseout
: function(event
, g
, context
) {
1035 if (context
.isZooming
) {
1036 context
.dragEndX
= null;
1037 context
.dragEndY
= null;
1041 // Disable zooming out if panning.
1042 dblclick
: function(event
, g
, context
) {
1043 if (event
.altKey
|| event
.shiftKey
) {
1046 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1047 // friendlier to public use.
1052 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.defaultInteractionModel
;
1055 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1059 Dygraph
.prototype.createDragInterface_
= function() {
1061 // Tracks whether the mouse is down right now
1063 isPanning
: false, // is this drag part of a pan?
1064 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1069 dragDirection
: null,
1072 prevDragDirection
: null,
1074 // TODO(danvk): update this comment
1075 // draggingDate and draggingValue represent the [date,value] point on the
1076 // graph at which the mouse was pressed. As the mouse moves while panning,
1077 // the viewport must pan so that the mouse position points to
1078 // [draggingDate, draggingValue]
1081 // TODO(danvk): update this comment
1082 // The range in second/value units that the viewport encompasses during a
1083 // panning operation.
1086 // Utility function to convert page-wide coordinates to canvas coords
1090 initializeMouseDown
: function(event
, g
, context
) {
1091 // prevents mouse drags from selecting page text.
1092 if (event
.preventDefault
) {
1093 event
.preventDefault(); // Firefox, Chrome, etc.
1095 event
.returnValue
= false; // IE
1096 event
.cancelBubble
= true;
1099 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1100 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1101 context
.dragStartX
= g
.dragGetX_(event
, context
);
1102 context
.dragStartY
= g
.dragGetY_(event
, context
);
1106 var interactionModel
= this.attr_("interactionModel");
1108 // Self is the graph.
1111 // Function that binds the graph and context to the handler.
1112 var bindHandler
= function(handler
) {
1113 return function(event
) {
1114 handler(event
, self
, context
);
1118 for (var eventName
in interactionModel
) {
1119 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1120 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1121 bindHandler(interactionModel
[eventName
]));
1124 // If the user releases the mouse button during a drag, but not over the
1125 // canvas, then it doesn't count as a zooming action.
1126 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1127 if (context
.isZooming
|| context
.isPanning
) {
1128 context
.isZooming
= false;
1129 context
.dragStartX
= null;
1130 context
.dragStartY
= null;
1133 if (context
.isPanning
) {
1134 context
.isPanning
= false;
1135 context
.draggingDate
= null;
1136 context
.dateRange
= null;
1137 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1138 delete self
.axes_
[i
].draggingValue
;
1139 delete self
.axes_
[i
].dragValueRange
;
1146 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1147 * up any previous zoom rectangles that were drawn. This could be optimized to
1148 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1151 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1152 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1153 * @param {Number} startX The X position where the drag started, in canvas
1155 * @param {Number} endX The current X position of the drag, in canvas coords.
1156 * @param {Number} startY The Y position where the drag started, in canvas
1158 * @param {Number} endY The current Y position of the drag, in canvas coords.
1159 * @param {Number} prevDirection the value of direction on the previous call to
1160 * this function. Used to avoid excess redrawing
1161 * @param {Number} prevEndX The value of endX on the previous call to this
1162 * function. Used to avoid excess redrawing
1163 * @param {Number} prevEndY The value of endY on the previous call to this
1164 * function. Used to avoid excess redrawing
1167 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1168 prevDirection
, prevEndX
, prevEndY
) {
1169 var ctx
= this.canvas_
.getContext("2d");
1171 // Clean up from the previous rect if necessary
1172 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1173 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1174 Math
.abs(startX
- prevEndX
), this.height_
);
1175 } else if (prevDirection
== Dygraph
.VERTICAL
){
1176 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1177 this.width_
, Math
.abs(startY
- prevEndY
));
1180 // Draw a light-grey rectangle to show the new viewing area
1181 if (direction
== Dygraph
.HORIZONTAL
) {
1182 if (endX
&& startX
) {
1183 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1184 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1185 Math
.abs(endX
- startX
), this.height_
);
1188 if (direction
== Dygraph
.VERTICAL
) {
1189 if (endY
&& startY
) {
1190 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1191 ctx
.fillRect(0, Math
.min(startY
, endY
),
1192 this.width_
, Math
.abs(endY
- startY
));
1198 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1199 * the canvas. The exact zoom window may be slightly larger if there are no data
1200 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1201 * which accepts dates that match the raw data. This function redraws the graph.
1203 * @param {Number} lowX The leftmost pixel value that should be visible.
1204 * @param {Number} highX The rightmost pixel value that should be visible.
1207 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1208 // Find the earliest and latest dates contained in this canvasx range.
1209 // Convert the call to date ranges of the raw data.
1210 var r
= this.toDataCoords(lowX
, null);
1212 r
= this.toDataCoords(highX
, null);
1214 this.doZoomXDates_(minDate
, maxDate
);
1218 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1219 * method with doZoomX which accepts pixel coordinates. This function redraws
1222 * @param {Number} minDate The minimum date that should be visible.
1223 * @param {Number} maxDate The maximum date that should be visible.
1226 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1227 this.dateWindow_
= [minDate
, maxDate
];
1229 if (this.attr_("zoomCallback")) {
1230 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1235 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1236 * the canvas. This function redraws the graph.
1238 * @param {Number} lowY The topmost pixel value that should be visible.
1239 * @param {Number} highY The lowest pixel value that should be visible.
1242 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1243 // Find the highest and lowest values in pixel range for each axis.
1244 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1245 // This is because pixels increase as you go down on the screen, whereas data
1246 // coordinates increase as you go up the screen.
1247 var valueRanges
= [];
1248 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1249 var hi
= this.toDataCoords(null, lowY
, i
);
1250 var low
= this.toDataCoords(null, highY
, i
);
1251 this.axes_
[i
].valueWindow
= [low
[1], hi
[1]];
1252 valueRanges
.push([low
[1], hi
[1]]);
1256 if (this.attr_("zoomCallback")) {
1257 var xRange
= this.xAxisRange();
1258 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1263 * Reset the zoom to the original view coordinates. This is the same as
1264 * double-clicking on the graph.
1268 Dygraph
.prototype.doUnzoom_
= function() {
1270 if (this.dateWindow_
!= null) {
1272 this.dateWindow_
= null;
1275 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1276 if (this.axes_
[i
].valueWindow
!= null) {
1278 delete this.axes_
[i
].valueWindow
;
1283 // Putting the drawing operation before the callback because it resets
1286 if (this.attr_("zoomCallback")) {
1287 var minDate
= this.rawData_
[0][0];
1288 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1289 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1295 * When the mouse moves in the canvas, display information about a nearby data
1296 * point and draw dots over those points in the data series. This function
1297 * takes care of cleanup of previously-drawn dots.
1298 * @param {Object} event The mousemove event from the browser.
1301 Dygraph
.prototype.mouseMove_
= function(event
) {
1302 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1303 var points
= this.layout_
.points
;
1308 // Loop through all the points and find the date nearest to our current
1310 var minDist
= 1e+100;
1312 for (var i
= 0; i
< points
.length
; i
++) {
1313 var point
= points
[i
];
1314 if (point
== null) continue;
1315 var dist
= Math
.abs(point
.canvasx
- canvasx
);
1316 if (dist
> minDist
) continue;
1320 if (idx
>= 0) lastx
= points
[idx
].xval
;
1321 // Check that you can really highlight the last day's data
1322 var last
= points
[points
.length
-1];
1323 if (last
!= null && canvasx
> last
.canvasx
)
1324 lastx
= points
[points
.length
-1].xval
;
1326 // Extract the points we've selected
1327 this.selPoints_
= [];
1328 var l
= points
.length
;
1329 if (!this.attr_("stackedGraph")) {
1330 for (var i
= 0; i
< l
; i
++) {
1331 if (points
[i
].xval
== lastx
) {
1332 this.selPoints_
.push(points
[i
]);
1336 // Need to 'unstack' points starting from the bottom
1337 var cumulative_sum
= 0;
1338 for (var i
= l
- 1; i
>= 0; i
--) {
1339 if (points
[i
].xval
== lastx
) {
1340 var p
= {}; // Clone the point since we modify it
1341 for (var k
in points
[i
]) {
1342 p
[k
] = points
[i
][k
];
1344 p
.yval
-= cumulative_sum
;
1345 cumulative_sum
+= p
.yval
;
1346 this.selPoints_
.push(p
);
1349 this.selPoints_
.reverse();
1352 if (this.attr_("highlightCallback")) {
1353 var px
= this.lastx_
;
1354 if (px
!== null && lastx
!= px
) {
1355 // only fire if the selected point has changed.
1356 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1360 // Save last x position for callbacks.
1361 this.lastx_
= lastx
;
1363 this.updateSelection_();
1367 * Transforms layout_.points index into data row number.
1368 * @param int layout_.points index
1369 * @return int row number, or -1 if none could be found.
1372 Dygraph
.prototype.idxToRow_
= function(idx
) {
1373 if (idx
< 0) return -1;
1375 for (var i
in this.layout_
.datasets
) {
1376 if (idx
< this.layout_
.datasets
[i
].length
) {
1377 return this.boundaryIds_
[0][0]+idx
;
1379 idx
-= this.layout_
.datasets
[i
].length
;
1385 * Draw dots over the selectied points in the data series. This function
1386 * takes care of cleanup of previously-drawn dots.
1389 Dygraph
.prototype.updateSelection_
= function() {
1390 // Clear the previously drawn vertical, if there is one
1391 var ctx
= this.canvas_
.getContext("2d");
1392 if (this.previousVerticalX_
>= 0) {
1393 // Determine the maximum highlight circle size.
1394 var maxCircleSize
= 0;
1395 var labels
= this.attr_('labels');
1396 for (var i
= 1; i
< labels
.length
; i
++) {
1397 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1398 if (r
> maxCircleSize
) maxCircleSize
= r
;
1400 var px
= this.previousVerticalX_
;
1401 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1402 2 * maxCircleSize
+ 2, this.height_
);
1405 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1407 if (this.selPoints_
.length
> 0) {
1408 var canvasx
= this.selPoints_
[0].canvasx
;
1410 // Set the status message to indicate the selected point(s)
1411 var replace
= this.attr_('xValueFormatter')(
1412 this.lastx_
, this.numXDigits_
+ this.numExtraDigits_
) + ":";
1413 var fmtFunc
= this.attr_('yValueFormatter');
1414 var clen
= this.colors_
.length
;
1416 if (this.attr_('showLabelsOnHighlight')) {
1417 // Set the status message to indicate the selected point(s)
1418 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1419 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1420 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1421 if (this.attr_("labelsSeparateLines")) {
1424 var point
= this.selPoints_
[i
];
1425 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1426 var yval
= fmtFunc(point
.yval
, this.numYDigits_
+ this.numExtraDigits_
);
1427 replace
+= " <b><font color='" + c
.toHex() + "'>"
1428 + point
.name
+ "</font></b>:"
1432 this.attr_("labelsDiv").innerHTML
= replace
;
1435 // Draw colored circles over the center of each selected point
1437 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1438 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1440 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1442 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1443 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1444 0, 2 * Math
.PI
, false);
1449 this.previousVerticalX_
= canvasx
;
1454 * Set manually set selected dots, and display information about them
1455 * @param int row number that should by highlighted
1456 * false value clears the selection
1459 Dygraph
.prototype.setSelection
= function(row
) {
1460 // Extract the points we've selected
1461 this.selPoints_
= [];
1464 if (row
!== false) {
1465 row
= row
-this.boundaryIds_
[0][0];
1468 if (row
!== false && row
>= 0) {
1469 for (var i
in this.layout_
.datasets
) {
1470 if (row
< this.layout_
.datasets
[i
].length
) {
1471 var point
= this.layout_
.points
[pos
+row
];
1473 if (this.attr_("stackedGraph")) {
1474 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1477 this.selPoints_
.push(point
);
1479 pos
+= this.layout_
.datasets
[i
].length
;
1483 if (this.selPoints_
.length
) {
1484 this.lastx_
= this.selPoints_
[0].xval
;
1485 this.updateSelection_();
1488 this.clearSelection();
1494 * The mouse has left the canvas. Clear out whatever artifacts remain
1495 * @param {Object} event the mouseout event from the browser.
1498 Dygraph
.prototype.mouseOut_
= function(event
) {
1499 if (this.attr_("unhighlightCallback")) {
1500 this.attr_("unhighlightCallback")(event
);
1503 if (this.attr_("hideOverlayOnMouseOut")) {
1504 this.clearSelection();
1509 * Remove all selection from the canvas
1512 Dygraph
.prototype.clearSelection
= function() {
1513 // Get rid of the overlay data
1514 var ctx
= this.canvas_
.getContext("2d");
1515 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1516 this.attr_("labelsDiv").innerHTML
= "";
1517 this.selPoints_
= [];
1522 * Returns the number of the currently selected row
1523 * @return int row number, of -1 if nothing is selected
1526 Dygraph
.prototype.getSelection
= function() {
1527 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1531 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1532 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1533 return row
+ this.boundaryIds_
[0][0];
1539 Dygraph
.zeropad
= function(x
) {
1540 if (x
< 10) return "0" + x
; else return "" + x
;
1544 * Return a string version of the hours, minutes and seconds portion of a date.
1545 * @param {Number} date The JavaScript date (ms since epoch)
1546 * @return {String} A time of the form "HH:MM:SS"
1549 Dygraph
.hmsString_
= function(date
) {
1550 var zeropad
= Dygraph
.zeropad
;
1551 var d
= new Date(date
);
1552 if (d
.getSeconds()) {
1553 return zeropad(d
.getHours()) + ":" +
1554 zeropad(d
.getMinutes()) + ":" +
1555 zeropad(d
.getSeconds());
1557 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1562 * Convert a JS date to a string appropriate to display on an axis that
1563 * is displaying values at the stated granularity.
1564 * @param {Date} date The date to format
1565 * @param {Number} granularity One of the Dygraph granularity constants
1566 * @return {String} The formatted date
1569 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1570 if (granularity
>= Dygraph
.DECADAL
) {
1571 return date
.strftime('%Y');
1572 } else if (granularity
>= Dygraph
.MONTHLY
) {
1573 return date
.strftime('%b %y');
1575 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1576 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1577 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1579 return Dygraph
.hmsString_(date
.getTime());
1585 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1586 * @param {Number} date The JavaScript date (ms since epoch)
1587 * @return {String} A date of the form "YYYY/MM/DD"
1590 Dygraph
.dateString_
= function(date
) {
1591 var zeropad
= Dygraph
.zeropad
;
1592 var d
= new Date(date
);
1595 var year
= "" + d
.getFullYear();
1596 // Get a 0 padded month string
1597 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1598 // Get a 0 padded day string
1599 var day
= zeropad(d
.getDate());
1602 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1603 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1605 return year
+ "/" + month + "/" + day
+ ret
;
1609 * Fires when there's data available to be graphed.
1610 * @param {String} data Raw CSV data to be plotted
1613 Dygraph
.prototype.loadedEvent_
= function(data
) {
1614 this.rawData_
= this.parseCSV_(data
);
1618 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1619 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1620 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1623 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1626 Dygraph
.prototype.addXTicks_
= function() {
1627 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1628 var opts
= {xTicks
: []};
1629 var formatter
= this.attr_('xTicker');
1630 if (this.dateWindow_
) {
1631 opts
.xTicks
= formatter(this.dateWindow_
[0], this.dateWindow_
[1], this);
1633 // numericTicks() returns multiple values.
1634 var ret
= formatter(this.rawData_
[0][0],
1635 this.rawData_
[this.rawData_
.length
- 1][0], this);
1636 opts
.xTicks
= ret
.ticks
;
1637 this.numXDigits_
= ret
.numDigits
;
1639 this.layout_
.updateOptions(opts
);
1642 // Time granularity enumeration
1643 Dygraph
.SECONDLY
= 0;
1644 Dygraph
.TWO_SECONDLY
= 1;
1645 Dygraph
.FIVE_SECONDLY
= 2;
1646 Dygraph
.TEN_SECONDLY
= 3;
1647 Dygraph
.THIRTY_SECONDLY
= 4;
1648 Dygraph
.MINUTELY
= 5;
1649 Dygraph
.TWO_MINUTELY
= 6;
1650 Dygraph
.FIVE_MINUTELY
= 7;
1651 Dygraph
.TEN_MINUTELY
= 8;
1652 Dygraph
.THIRTY_MINUTELY
= 9;
1653 Dygraph
.HOURLY
= 10;
1654 Dygraph
.TWO_HOURLY
= 11;
1655 Dygraph
.SIX_HOURLY
= 12;
1657 Dygraph
.WEEKLY
= 14;
1658 Dygraph
.MONTHLY
= 15;
1659 Dygraph
.QUARTERLY
= 16;
1660 Dygraph
.BIANNUAL
= 17;
1661 Dygraph
.ANNUAL
= 18;
1662 Dygraph
.DECADAL
= 19;
1663 Dygraph
.CENTENNIAL
= 20;
1664 Dygraph
.NUM_GRANULARITIES
= 21;
1666 Dygraph
.SHORT_SPACINGS
= [];
1667 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1668 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1669 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1670 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1671 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1672 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1673 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1674 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1675 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1676 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1677 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1678 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1679 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1680 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1681 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1685 // If we used this time granularity, how many ticks would there be?
1686 // This is only an approximation, but it's generally good enough.
1688 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1689 if (granularity
< Dygraph
.MONTHLY
) {
1690 // Generate one tick mark for every fixed interval of time.
1691 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1692 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1694 var year_mod
= 1; // e.g. to only print one point every 10 years.
1695 var num_months
= 12;
1696 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1697 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1698 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1699 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1700 if (granularity
== Dygraph
.CENTENNIAL
) { num_months
= 1; year_mod
= 100; }
1702 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1703 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1704 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1710 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1711 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1713 // Returns an array containing {v: millis, label: label} dictionaries.
1715 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1716 var formatter
= this.attr_("xAxisLabelFormatter");
1718 if (granularity
< Dygraph
.MONTHLY
) {
1719 // Generate one tick mark for every fixed interval of time.
1720 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1721 var format
= '%d%b'; // e.g. "1Jan"
1723 // Find a time less than start_time which occurs on a "nice" time boundary
1724 // for this granularity.
1725 var g
= spacing
/ 1000;
1726 var d
= new Date(start_time
);
1727 if (g
<= 60) { // seconds
1728 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1732 if (g
<= 60) { // minutes
1733 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1738 if (g
<= 24) { // days
1739 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1744 if (g
== 7) { // one week
1745 d
.setDate(d
.getDate() - d
.getDay());
1750 start_time
= d
.getTime();
1752 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1753 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1756 // Display a tick mark on the first of a set of months of each year.
1757 // Years get a tick mark iff y % year_mod == 0. This is useful for
1758 // displaying a tick mark once every 10 years, say, on long time scales.
1760 var year_mod
= 1; // e.g. to only print one point every 10 years.
1762 if (granularity
== Dygraph
.MONTHLY
) {
1763 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1764 } else if (granularity
== Dygraph
.QUARTERLY
) {
1765 months
= [ 0, 3, 6, 9 ];
1766 } else if (granularity
== Dygraph
.BIANNUAL
) {
1768 } else if (granularity
== Dygraph
.ANNUAL
) {
1770 } else if (granularity
== Dygraph
.DECADAL
) {
1773 } else if (granularity
== Dygraph
.CENTENNIAL
) {
1777 this.warn("Span of dates is too long");
1780 var start_year
= new Date(start_time
).getFullYear();
1781 var end_year
= new Date(end_time
).getFullYear();
1782 var zeropad
= Dygraph
.zeropad
;
1783 for (var i
= start_year
; i
<= end_year
; i
++) {
1784 if (i
% year_mod
!= 0) continue;
1785 for (var j
= 0; j
< months
.length
; j
++) {
1786 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1787 var t
= Date
.parse(date_str
);
1788 if (t
< start_time
|| t
> end_time
) continue;
1789 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1799 * Add ticks to the x-axis based on a date range.
1800 * @param {Number} startDate Start of the date window (millis since epoch)
1801 * @param {Number} endDate End of the date window (millis since epoch)
1802 * @return {Array.<Object>} Array of {label, value} tuples.
1805 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1807 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1808 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1809 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1816 return self
.GetXAxis(startDate
, endDate
, chosen
);
1818 // TODO(danvk): signal error.
1823 * Determine the number of significant figures in a Number up to the specified
1824 * precision. Note that there is no way to determine if a trailing '0' is
1825 * significant or not, so by convention we return 1 for all of the following
1826 * inputs: 1, 1.0, 1.00, 1.000 etc.
1827 * @param {Number} x The input value.
1828 * @param {Number} opt_maxPrecision Optional maximum precision to consider.
1829 * Default and maximum allowed value is 13.
1830 * @return {Number} The number of significant figures which is >= 1.
1832 Dygraph
.significantFigures
= function(x
, opt_maxPrecision
) {
1833 var precision
= Math
.max(opt_maxPrecision
|| 13, 13);
1835 // Convert the number to its exponential notation form and work backwards,
1836 // ignoring the 'e+xx' bit. This may seem like a hack, but doing a loop and
1837 // dividing by 10 leads to roundoff errors. By using toExponential(), we let
1838 // the JavaScript interpreter handle the low level bits of the Number for us.
1839 var s
= x
.toExponential(precision
);
1840 var ePos
= s
.lastIndexOf('e'); // -1 case handled by return below.
1842 for (var i
= ePos
- 1; i
>= 0; i
--) {
1844 // Got to the decimal place. We'll call this 1 digit of precision because
1845 // we can't know for sure how many trailing 0s are significant.
1847 } else if (s
[i
] != '0') {
1848 // Found the first non-zero digit. Return the number of characters
1849 // except for the '.'.
1850 return i
; // This is i - 1 + 1 (-1 is for '.', +1 is for 0 based index).
1854 // Occurs if toExponential() doesn't return a string containing 'e', which
1855 // should never happen.
1860 * Add ticks when the x axis has numbers on it (instead of dates)
1861 * @param {Number} startDate Start of the date window (millis since epoch)
1862 * @param {Number} endDate End of the date window (millis since epoch)
1864 * @param {function} attribute accessor function.
1865 * @return {Array.<Object>} Array of {label, value} tuples.
1868 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1869 var attr
= function(k
) {
1870 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1871 return self
.attr_(k
);
1876 for (var i
= 0; i
< vals
.length
; i
++) {
1877 ticks
[i
].push({v
: vals
[i
]});
1881 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1882 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1883 // The first spacing greater than pixelsPerYLabel is what we use.
1884 // TODO(danvk): version that works on a log scale.
1885 if (attr("labelsKMG2")) {
1886 var mults
= [1, 2, 4, 8];
1888 var mults
= [1, 2, 5];
1890 var scale
, low_val
, high_val
, nTicks
;
1891 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1892 var pixelsPerTick
= attr('pixelsPerYLabel');
1893 for (var i
= -10; i
< 50; i
++) {
1894 if (attr("labelsKMG2")) {
1895 var base_scale
= Math
.pow(16, i
);
1897 var base_scale
= Math
.pow(10, i
);
1899 for (var j
= 0; j
< mults
.length
; j
++) {
1900 scale
= base_scale
* mults
[j
];
1901 low_val
= Math
.floor(minV
/ scale
) * scale
;
1902 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1903 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1904 var spacing
= self
.height_
/ nTicks
;
1905 // wish I could break out of both loops at once...
1906 if (spacing
> pixelsPerTick
) break;
1908 if (spacing
> pixelsPerTick
) break;
1911 // Construct the set of ticks.
1912 // Allow reverse y-axis if it's explicitly requested.
1913 if (low_val
> high_val
) scale
*= -1;
1914 for (var i
= 0; i
< nTicks
; i
++) {
1915 var tickV
= low_val
+ i
* scale
;
1916 ticks
.push( {v
: tickV
} );
1920 // Add formatted labels to the ticks.
1923 if (attr("labelsKMB")) {
1925 k_labels
= [ "K", "M", "B", "T" ];
1927 if (attr("labelsKMG2")) {
1928 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1930 k_labels
= [ "k", "M", "G", "T" ];
1932 var formatter
= attr('yAxisLabelFormatter') ?
1933 attr('yAxisLabelFormatter') : attr('yValueFormatter');
1935 // Determine the number of decimal places needed for the labels below by
1936 // taking the maximum number of significant figures for any label. We must
1937 // take the max because we can't tell if trailing 0s are significant.
1939 for (var i
= 0; i
< ticks
.length
; i
++) {
1940 numDigits
= Math
.max(Dygraph
.significantFigures(ticks
[i
].v
), numDigits
);
1943 for (var i
= 0; i
< ticks
.length
; i
++) {
1944 var tickV
= ticks
[i
].v
;
1945 var absTickV
= Math
.abs(tickV
);
1946 var label
= (formatter
!== undefined
) ?
1947 formatter(tickV
, numDigits
) : tickV
.toPrecision(numDigits
);
1948 if (k_labels
.length
> 0) {
1949 // Round up to an appropriate unit.
1951 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1952 if (absTickV
>= n
) {
1953 label
= (tickV
/ n
).toPrecision(numDigits
) + k_labels
[j
];
1958 ticks
[i
].label
= label
;
1960 return {ticks
: ticks
, numDigits
: numDigits
};
1963 // Computes the range of the data series (including confidence intervals).
1964 // series is either [ [x1, y1], [x2, y2], ... ] or
1965 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1966 // Returns [low, high]
1967 Dygraph
.prototype.extremeValues_
= function(series
) {
1968 var minY
= null, maxY
= null;
1970 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1972 // With custom bars, maxY is the max of the high values.
1973 for (var j
= 0; j
< series
.length
; j
++) {
1974 var y
= series
[j
][1][0];
1976 var low
= y
- series
[j
][1][1];
1977 var high
= y
+ series
[j
][1][2];
1978 if (low
> y
) low
= y
; // this can happen with custom bars,
1979 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1980 if (maxY
== null || high
> maxY
) {
1983 if (minY
== null || low
< minY
) {
1988 for (var j
= 0; j
< series
.length
; j
++) {
1989 var y
= series
[j
][1];
1990 if (y
=== null || isNaN(y
)) continue;
1991 if (maxY
== null || y
> maxY
) {
1994 if (minY
== null || y
< minY
) {
2000 return [minY
, maxY
];
2004 * This function is called once when the chart's data is changed or the options
2005 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2006 * idea is that values derived from the chart's data can be computed here,
2007 * rather than every time the chart is drawn. This includes things like the
2008 * number of axes, rolling averages, etc.
2010 Dygraph
.prototype.predraw_
= function() {
2011 // TODO(danvk): move more computations out of drawGraph_ and into here.
2012 this.computeYAxes_();
2014 // Create a new plotter.
2015 if (this.plotter_
) this.plotter_
.clear();
2016 this.plotter_
= new DygraphCanvasRenderer(this,
2017 this.hidden_
, this.layout_
,
2018 this.renderOptions_
);
2020 // The roller sits in the bottom left corner of the chart. We don't know where
2021 // this will be until the options are available, so it's positioned here.
2022 this.createRollInterface_();
2024 // Same thing applies for the labelsDiv. It's right edge should be flush with
2025 // the right edge of the charting area (which may not be the same as the right
2026 // edge of the div, if we have two y-axes.
2027 this.positionLabelsDiv_();
2029 // If the data or options have changed, then we'd better redraw.
2035 * Update the graph with new data. This method is called when the viewing area
2036 * has changed. If the underlying data or options have changed, predraw_ will
2037 * be called before drawGraph_ is called.
2040 Dygraph
.prototype.drawGraph_
= function() {
2041 var data
= this.rawData_
;
2043 // This is used to set the second parameter to drawCallback, below.
2044 var is_initial_draw
= this.is_initial_draw_
;
2045 this.is_initial_draw_
= false;
2047 var minY
= null, maxY
= null;
2048 this.layout_
.removeAllDatasets();
2050 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
2052 // Loop over the fields (series). Go from the last to the first,
2053 // because if they're stacked that's how we accumulate the values.
2055 var cumulative_y
= []; // For stacked series.
2058 var extremes
= {}; // series name -> [low, high]
2060 // Loop over all fields and create datasets
2061 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
2062 if (!this.visibility()[i
- 1]) continue;
2064 var seriesName
= this.attr_("labels")[i
];
2065 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
2068 for (var j
= 0; j
< data
.length
; j
++) {
2069 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
2070 var date
= data
[j
][0];
2071 series
.push([date
, data
[j
][i
]]);
2075 // TODO(danvk): move this into predraw_. It's insane to do it here.
2076 series
= this.rollingAverage(series
, this.rollPeriod_
);
2078 // Prune down to the desired range, if necessary (for zooming)
2079 // Because there can be lines going to points outside of the visible area,
2080 // we actually prune to visible points, plus one on either side.
2081 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2082 if (this.dateWindow_
) {
2083 var low
= this.dateWindow_
[0];
2084 var high
= this.dateWindow_
[1];
2086 // TODO(danvk): do binary search instead of linear search.
2087 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2088 var firstIdx
= null, lastIdx
= null;
2089 for (var k
= 0; k
< series
.length
; k
++) {
2090 if (series
[k
][0] >= low
&& firstIdx
=== null) {
2093 if (series
[k
][0] <= high
) {
2097 if (firstIdx
=== null) firstIdx
= 0;
2098 if (firstIdx
> 0) firstIdx
--;
2099 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2100 if (lastIdx
< series
.length
- 1) lastIdx
++;
2101 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
2102 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
2103 pruned
.push(series
[k
]);
2107 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
2110 var seriesExtremes
= this.extremeValues_(series
);
2113 for (var j
=0; j
<series
.length
; j
++) {
2114 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
2117 } else if (this.attr_("stackedGraph")) {
2118 var l
= series
.length
;
2120 for (var j
= 0; j
< l
; j
++) {
2121 // If one data set has a NaN, let all subsequent stacked
2122 // sets inherit the NaN -- only start at 0 for the first set.
2123 var x
= series
[j
][0];
2124 if (cumulative_y
[x
] === undefined
) {
2125 cumulative_y
[x
] = 0;
2128 actual_y
= series
[j
][1];
2129 cumulative_y
[x
] += actual_y
;
2131 series
[j
] = [x
, cumulative_y
[x
]]
2133 if (cumulative_y
[x
] > seriesExtremes
[1]) {
2134 seriesExtremes
[1] = cumulative_y
[x
];
2136 if (cumulative_y
[x
] < seriesExtremes
[0]) {
2137 seriesExtremes
[0] = cumulative_y
[x
];
2141 extremes
[seriesName
] = seriesExtremes
;
2143 datasets
[i
] = series
;
2146 for (var i
= 1; i
< datasets
.length
; i
++) {
2147 if (!this.visibility()[i
- 1]) continue;
2148 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
2151 this.computeYAxisRanges_(extremes
);
2152 this.layout_
.updateOptions( { yAxes
: this.axes_
,
2153 seriesToAxisMap
: this.seriesToAxisMap_
2158 // Tell PlotKit to use this new data and render itself
2159 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
2160 this.layout_
.evaluateWithError();
2161 this.plotter_
.clear();
2162 this.plotter_
.render();
2163 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
2164 this.canvas_
.height
);
2166 if (this.attr_("drawCallback") !== null) {
2167 this.attr_("drawCallback")(this, is_initial_draw
);
2172 * Determine properties of the y-axes which are independent of the data
2173 * currently being displayed. This includes things like the number of axes and
2174 * the style of the axes. It does not include the range of each axis and its
2176 * This fills in this.axes_ and this.seriesToAxisMap_.
2177 * axes_ = [ { options } ]
2178 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2179 * indices are into the axes_ array.
2181 Dygraph
.prototype.computeYAxes_
= function() {
2182 this.axes_
= [{}]; // always have at least one y-axis.
2183 this.seriesToAxisMap_
= {};
2185 // Get a list of series names.
2186 var labels
= this.attr_("labels");
2188 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2190 // all options which could be applied per-axis:
2198 'axisLabelFontSize',
2202 // Copy global axis options over to the first axis.
2203 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2204 var k
= axisOptions
[i
];
2205 var v
= this.attr_(k
);
2206 if (v
) this.axes_
[0][k
] = v
;
2209 // Go through once and add all the axes.
2210 for (var seriesName
in series
) {
2211 if (!series
.hasOwnProperty(seriesName
)) continue;
2212 var axis
= this.attr_("axis", seriesName
);
2214 this.seriesToAxisMap_
[seriesName
] = 0;
2217 if (typeof(axis
) == 'object') {
2218 // Add a new axis, making a copy of its per-axis options.
2220 Dygraph
.update(opts
, this.axes_
[0]);
2221 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2222 Dygraph
.update(opts
, axis
);
2223 this.axes_
.push(opts
);
2224 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2228 // Go through one more time and assign series to an axis defined by another
2229 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2230 for (var seriesName
in series
) {
2231 if (!series
.hasOwnProperty(seriesName
)) continue;
2232 var axis
= this.attr_("axis", seriesName
);
2233 if (typeof(axis
) == 'string') {
2234 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2235 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2236 "series " + axis
+ ", which does not define its own axis.");
2239 var idx
= this.seriesToAxisMap_
[axis
];
2240 this.seriesToAxisMap_
[seriesName
] = idx
;
2244 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2245 // this last so that hiding the first series doesn't destroy the axis
2246 // properties of the primary axis.
2247 var seriesToAxisFiltered
= {};
2248 var vis
= this.visibility();
2249 for (var i
= 1; i
< labels
.length
; i
++) {
2251 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2253 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2257 * Returns the number of y-axes on the chart.
2258 * @return {Number} the number of axes.
2260 Dygraph
.prototype.numAxes
= function() {
2262 for (var series
in this.seriesToAxisMap_
) {
2263 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2264 var idx
= this.seriesToAxisMap_
[series
];
2265 if (idx
> last_axis
) last_axis
= idx
;
2267 return 1 + last_axis
;
2271 * Determine the value range and tick marks for each axis.
2272 * @param {Object} extremes A mapping from seriesName -> [low, high]
2273 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2275 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2276 // Build a map from axis number -> [list of series names]
2277 var seriesForAxis
= [];
2278 for (var series
in this.seriesToAxisMap_
) {
2279 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2280 var idx
= this.seriesToAxisMap_
[series
];
2281 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2282 seriesForAxis
[idx
].push(series
);
2285 // Compute extreme values, a span and tick marks for each axis.
2286 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2287 var axis
= this.axes_
[i
];
2288 if (axis
.valueWindow
) {
2289 // This is only set if the user has zoomed on the y-axis. It is never set
2290 // by a user. It takes precedence over axis.valueRange because, if you set
2291 // valueRange, you'd still expect to be able to pan.
2292 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2293 } else if (axis
.valueRange
) {
2294 // This is a user-set value range for this axis.
2295 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2297 // Calculate the extremes of extremes.
2298 var series
= seriesForAxis
[i
];
2299 var minY
= Infinity
; // extremes[series[0]][0];
2300 var maxY
= -Infinity
; // extremes[series[0]][1];
2301 for (var j
= 0; j
< series
.length
; j
++) {
2302 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2303 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2305 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2307 // Add some padding and round up to an integer to be human-friendly.
2308 var span
= maxY
- minY
;
2309 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2310 if (span
== 0) { span
= maxY
; }
2311 var maxAxisY
= maxY
+ 0.1 * span
;
2312 var minAxisY
= minY
- 0.1 * span
;
2314 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2315 if (!this.attr_("avoidMinZero")) {
2316 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2317 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2320 if (this.attr_("includeZero")) {
2321 if (maxY
< 0) maxAxisY
= 0;
2322 if (minY
> 0) minAxisY
= 0;
2325 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2328 // Add ticks. By default, all axes inherit the tick positions of the
2329 // primary axis. However, if an axis is specifically marked as having
2330 // independent ticks, then that is permissible as well.
2331 if (i
== 0 || axis
.independentTicks
) {
2333 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2334 axis
.computedValueRange
[1],
2337 axis
.ticks
= ret
.ticks
;
2338 this.numYDigits_
= ret
.numDigits
;
2340 var p_axis
= this.axes_
[0];
2341 var p_ticks
= p_axis
.ticks
;
2342 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2343 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2344 var tick_values
= [];
2345 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2346 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2347 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2348 tick_values
.push(y_val
);
2352 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2353 axis
.computedValueRange
[1],
2354 this, axis
, tick_values
);
2355 axis
.ticks
= ret
.ticks
;
2356 this.numYDigits_
= ret
.numDigits
;
2362 * Calculates the rolling average of a data set.
2363 * If originalData is [label, val], rolls the average of those.
2364 * If originalData is [label, [, it's interpreted as [value, stddev]
2365 * and the roll is returned in the same form, with appropriately reduced
2366 * stddev for each value.
2367 * Note that this is where fractional input (i.e. '5/10') is converted into
2369 * @param {Array} originalData The data in the appropriate format (see above)
2370 * @param {Number} rollPeriod The number of points over which to average the
2373 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2374 if (originalData
.length
< 2)
2375 return originalData
;
2376 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2377 var rollingData
= [];
2378 var sigma
= this.attr_("sigma");
2380 if (this.fractions_
) {
2382 var den
= 0; // numerator/denominator
2384 for (var i
= 0; i
< originalData
.length
; i
++) {
2385 num
+= originalData
[i
][1][0];
2386 den
+= originalData
[i
][1][1];
2387 if (i
- rollPeriod
>= 0) {
2388 num
-= originalData
[i
- rollPeriod
][1][0];
2389 den
-= originalData
[i
- rollPeriod
][1][1];
2392 var date
= originalData
[i
][0];
2393 var value
= den
? num
/ den
: 0.0;
2394 if (this.attr_("errorBars")) {
2395 if (this.wilsonInterval_
) {
2396 // For more details on this confidence interval, see:
2397 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2399 var p
= value
< 0 ? 0 : value
, n
= den
;
2400 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2401 var denom
= 1 + sigma
* sigma
/ den
;
2402 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2403 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2404 rollingData
[i
] = [date
,
2405 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2407 rollingData
[i
] = [date
, [0, 0, 0]];
2410 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2411 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2414 rollingData
[i
] = [date
, mult
* value
];
2417 } else if (this.attr_("customBars")) {
2422 for (var i
= 0; i
< originalData
.length
; i
++) {
2423 var data
= originalData
[i
][1];
2425 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2427 if (y
!= null && !isNaN(y
)) {
2433 if (i
- rollPeriod
>= 0) {
2434 var prev
= originalData
[i
- rollPeriod
];
2435 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2442 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2443 1.0 * (mid
- low
) / count
,
2444 1.0 * (high
- mid
) / count
]];
2447 // Calculate the rolling average for the first rollPeriod - 1 points where
2448 // there is not enough data to roll over the full number of points
2449 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2450 if (!this.attr_("errorBars")){
2451 if (rollPeriod
== 1) {
2452 return originalData
;
2455 for (var i
= 0; i
< originalData
.length
; i
++) {
2458 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2459 var y
= originalData
[j
][1];
2460 if (y
== null || isNaN(y
)) continue;
2462 sum
+= originalData
[j
][1];
2465 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2467 rollingData
[i
] = [originalData
[i
][0], null];
2472 for (var i
= 0; i
< originalData
.length
; i
++) {
2476 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2477 var y
= originalData
[j
][1][0];
2478 if (y
== null || isNaN(y
)) continue;
2480 sum
+= originalData
[j
][1][0];
2481 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2484 var stddev
= Math
.sqrt(variance
) / num_ok
;
2485 rollingData
[i
] = [originalData
[i
][0],
2486 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2488 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2498 * Parses a date, returning the number of milliseconds since epoch. This can be
2499 * passed in as an xValueParser in the Dygraph constructor.
2500 * TODO(danvk): enumerate formats that this understands.
2501 * @param {String} A date in YYYYMMDD format.
2502 * @return {Number} Milliseconds since epoch.
2505 Dygraph
.dateParser
= function(dateStr
, self
) {
2508 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2509 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2510 while (dateStrSlashed
.search("-") != -1) {
2511 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2513 d
= Date
.parse(dateStrSlashed
);
2514 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2515 // TODO(danvk): remove support for this format. It's confusing.
2516 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2517 + "/" + dateStr
.substr(6,2);
2518 d
= Date
.parse(dateStrSlashed
);
2520 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2521 // "2009/07/12 12:34:56"
2522 d
= Date
.parse(dateStr
);
2525 if (!d
|| isNaN(d
)) {
2526 self
.error("Couldn't parse " + dateStr
+ " as a date");
2532 * Detects the type of the str (date or numeric) and sets the various
2533 * formatting attributes in this.attrs_ based on this type.
2534 * @param {String} str An x value.
2537 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2539 if (str
.indexOf('-') >= 0 ||
2540 str
.indexOf('/') >= 0 ||
2541 isNaN(parseFloat(str
))) {
2543 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2544 // TODO(danvk): remove support for this format.
2549 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2550 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2551 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2552 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2554 this.attrs_
.xValueFormatter
= this.attrs_
.yValueFormatter
;
2555 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2556 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2557 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2562 * Parses a string in a special csv format. We expect a csv file where each
2563 * line is a date point, and the first field in each line is the date string.
2564 * We also expect that all remaining fields represent series.
2565 * if the errorBars attribute is set, then interpret the fields as:
2566 * date, series1, stddev1, series2, stddev2, ...
2567 * @param {Array.<Object>} data See above.
2570 * @return Array.<Object> An array with one entry for each row. These entries
2571 * are an array of cells in that row. The first entry is the parsed x-value for
2572 * the row. The second, third, etc. are the y-values. These can take on one of
2573 * three forms, depending on the CSV and constructor parameters:
2575 * 2. [ value, stddev ]
2576 * 3. [ low value, center value, high value ]
2578 Dygraph
.prototype.parseCSV_
= function(data
) {
2580 var lines
= data
.split("\n");
2582 // Use the default delimiter or fall back to a tab if that makes sense.
2583 var delim
= this.attr_('delimiter');
2584 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2589 if (this.labelsFromCSV_
) {
2591 this.attrs_
.labels
= lines
[0].split(delim
);
2594 // Parse the x as a float or return null if it's not a number.
2595 var parseFloatOrNull
= function(x
) {
2596 var val
= parseFloat(x
);
2597 // isFinite() returns false for NaN and +/-Infinity
.
2598 return isFinite(val
) ? val
: null;
2602 var defaultParserSet
= false; // attempt to auto-detect x value type
2603 var expectedCols
= this.attr_("labels").length
;
2604 var outOfOrder
= false;
2605 for (var i
= start
; i
< lines
.length
; i
++) {
2606 var line
= lines
[i
];
2607 if (line
.length
== 0) continue; // skip blank lines
2608 if (line
[0] == '#') continue; // skip comment lines
2609 var inFields
= line
.split(delim
);
2610 if (inFields
.length
< 2) continue;
2613 if (!defaultParserSet
) {
2614 this.detectTypeFromString_(inFields
[0]);
2615 xParser
= this.attr_("xValueParser");
2616 defaultParserSet
= true;
2618 fields
[0] = xParser(inFields
[0], this);
2620 // If fractions are expected, parse the numbers as "A/B
"
2621 if (this.fractions_) {
2622 for (var j = 1; j < inFields.length; j++) {
2623 // TODO(danvk): figure out an appropriate way to flag parse errors.
2624 var vals = inFields[j].split("/");
2625 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2627 } else if (this.attr_("errorBars
")) {
2628 // If there are error bars, values are (value, stddev) pairs
2629 for (var j = 1; j < inFields.length; j += 2)
2630 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2631 parseFloatOrNull(inFields[j + 1])];
2632 } else if (this.attr_("customBars
")) {
2633 // Bars are a low;center;high tuple
2634 for (var j = 1; j < inFields.length; j++) {
2635 var vals = inFields[j].split(";");
2636 fields[j] = [ parseFloatOrNull(vals[0]),
2637 parseFloatOrNull(vals[1]),
2638 parseFloatOrNull(vals[2]) ];
2641 // Values are just numbers
2642 for (var j = 1; j < inFields.length; j++) {
2643 fields[j] = parseFloatOrNull(inFields[j]);
2646 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2651 if (fields.length != expectedCols) {
2652 this.error("Number of columns
in line
" + i + " (" + fields.length +
2653 ") does not agree
with number of
labels (" + expectedCols +
2659 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2660 ret.sort(function(a,b) { return a[0] - b[0] });
2667 * The user has provided their data as a pre-packaged JS array. If the x values
2668 * are numeric, this is the same as dygraphs' internal format. If the x values
2669 * are dates, we need to convert them from Date objects to ms since epoch.
2670 * @param {Array.<Object>} data
2671 * @return {Array.<Object>} data with numeric x values.
2673 Dygraph.prototype.parseArray_ = function(data) {
2674 // Peek at the first x value to see if it's numeric.
2675 if (data.length == 0) {
2676 this.error("Can
't plot empty data set");
2679 if (data[0].length == 0) {
2680 this.error("Data set cannot contain an empty row");
2684 if (this.attr_("labels") == null) {
2685 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2686 "in the options parameter");
2687 this.attrs_.labels = [ "X" ];
2688 for (var i = 1; i < data[0].length; i++) {
2689 this.attrs_.labels.push("Y" + i);
2693 if (Dygraph.isDateLike(data[0][0])) {
2694 // Some intelligent defaults for a date x-axis.
2695 this.attrs_.xValueFormatter = Dygraph.dateString_;
2696 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2697 this.attrs_.xTicker = Dygraph.dateTicker;
2699 // Assume they're all dates
.
2700 var parsedData
= Dygraph
.clone(data
);
2701 for (var i
= 0; i
< data
.length
; i
++) {
2702 if (parsedData
[i
].length
== 0) {
2703 this.error("Row " + (1 + i
) + " of data is empty");
2706 if (parsedData
[i
][0] == null
2707 || typeof(parsedData
[i
][0].getTime
) != 'function'
2708 || isNaN(parsedData
[i
][0].getTime())) {
2709 this.error("x value in row " + (1 + i
) + " is not a Date");
2712 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2716 // Some intelligent defaults for a numeric x-axis.
2717 this.attrs_
.xValueFormatter
= this.attrs_
.yValueFormatter
;
2718 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2724 * Parses a DataTable object from gviz.
2725 * The data is expected to have a first column that is either a date or a
2726 * number. All subsequent columns must be numbers. If there is a clear mismatch
2727 * between this.xValueParser_ and the type of the first column, it will be
2728 * fixed. Fills out rawData_.
2729 * @param {Array.<Object>} data See above.
2732 Dygraph
.prototype.parseDataTable_
= function(data
) {
2733 var cols
= data
.getNumberOfColumns();
2734 var rows
= data
.getNumberOfRows();
2736 var indepType
= data
.getColumnType(0);
2737 if (indepType
== 'date' || indepType
== 'datetime') {
2738 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2739 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2740 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2741 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2742 } else if (indepType
== 'number') {
2743 this.attrs_
.xValueFormatter
= this.attrs_
.yValueFormatter
;
2744 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2745 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2746 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2748 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2749 "column 1 of DataTable input (Got '" + indepType
+ "')");
2753 // Array of the column indices which contain data (and not annotations).
2755 var annotationCols
= {}; // data index -> [annotation cols]
2756 var hasAnnotations
= false;
2757 for (var i
= 1; i
< cols
; i
++) {
2758 var type
= data
.getColumnType(i
);
2759 if (type
== 'number') {
2761 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2762 // This is OK -- it's an annotation column.
2763 var dataIdx
= colIdx
[colIdx
.length
- 1];
2764 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2765 annotationCols
[dataIdx
] = [i
];
2767 annotationCols
[dataIdx
].push(i
);
2769 hasAnnotations
= true;
2771 this.error("Only 'number' is supported as a dependent type with Gviz." +
2772 " 'string' is only supported if displayAnnotations is true");
2776 // Read column labels
2777 // TODO(danvk): add support back for errorBars
2778 var labels
= [data
.getColumnLabel(0)];
2779 for (var i
= 0; i
< colIdx
.length
; i
++) {
2780 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2781 if (this.attr_("errorBars")) i
+= 1;
2783 this.attrs_
.labels
= labels
;
2784 cols
= labels
.length
;
2787 var outOfOrder
= false;
2788 var annotations
= [];
2789 for (var i
= 0; i
< rows
; i
++) {
2791 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2792 data
.getValue(i
, 0) === null) {
2793 this.warn("Ignoring row " + i
+
2794 " of DataTable because of undefined or null first column.");
2798 if (indepType
== 'date' || indepType
== 'datetime') {
2799 row
.push(data
.getValue(i
, 0).getTime());
2801 row
.push(data
.getValue(i
, 0));
2803 if (!this.attr_("errorBars")) {
2804 for (var j
= 0; j
< colIdx
.length
; j
++) {
2805 var col
= colIdx
[j
];
2806 row
.push(data
.getValue(i
, col
));
2807 if (hasAnnotations
&&
2808 annotationCols
.hasOwnProperty(col
) &&
2809 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2811 ann
.series
= data
.getColumnLabel(col
);
2813 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2815 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2816 if (k
) ann
.text
+= "\n";
2817 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2819 annotations
.push(ann
);
2823 for (var j
= 0; j
< cols
- 1; j
++) {
2824 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2827 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2831 // Strip out infinities, which give dygraphs problems later on.
2832 for (var j
= 0; j
< row
.length
; j
++) {
2833 if (!isFinite(row
[j
])) row
[j
] = null;
2839 this.warn("DataTable is out of order; order it correctly to speed loading.");
2840 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2842 this.rawData_
= ret
;
2844 if (annotations
.length
> 0) {
2845 this.setAnnotations(annotations
, true);
2849 // These functions are all based on MochiKit.
2850 Dygraph
.update
= function (self
, o
) {
2851 if (typeof(o
) != 'undefined' && o
!== null) {
2853 if (o
.hasOwnProperty(k
)) {
2861 Dygraph
.isArrayLike
= function (o
) {
2862 var typ
= typeof(o
);
2864 (typ
!= 'object' && !(typ
== 'function' &&
2865 typeof(o
.item
) == 'function')) ||
2867 typeof(o
.length
) != 'number' ||
2875 Dygraph
.isDateLike
= function (o
) {
2876 if (typeof(o
) != "object" || o
=== null ||
2877 typeof(o
.getTime
) != 'function') {
2883 Dygraph
.clone
= function(o
) {
2884 // TODO(danvk): figure out how MochiKit's version works
2886 for (var i
= 0; i
< o
.length
; i
++) {
2887 if (Dygraph
.isArrayLike(o
[i
])) {
2888 r
.push(Dygraph
.clone(o
[i
]));
2898 * Get the CSV data. If it's in a function, call that function. If it's in a
2899 * file, do an XMLHttpRequest to get it.
2902 Dygraph
.prototype.start_
= function() {
2903 if (typeof this.file_
== 'function') {
2904 // CSV string. Pretend we got it via XHR.
2905 this.loadedEvent_(this.file_());
2906 } else if (Dygraph
.isArrayLike(this.file_
)) {
2907 this.rawData_
= this.parseArray_(this.file_
);
2909 } else if (typeof this.file_
== 'object' &&
2910 typeof this.file_
.getColumnRange
== 'function') {
2911 // must be a DataTable from gviz.
2912 this.parseDataTable_(this.file_
);
2914 } else if (typeof this.file_
== 'string') {
2915 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2916 if (this.file_
.indexOf('\n') >= 0) {
2917 this.loadedEvent_(this.file_
);
2919 var req
= new XMLHttpRequest();
2921 req
.onreadystatechange
= function () {
2922 if (req
.readyState
== 4) {
2923 if (req
.status
== 200) {
2924 caller
.loadedEvent_(req
.responseText
);
2929 req
.open("GET", this.file_
, true);
2933 this.error("Unknown data format: " + (typeof this.file_
));
2938 * Changes various properties of the graph. These can include:
2940 * <li>file: changes the source data for the graph</li>
2941 * <li>errorBars: changes whether the data contains stddev</li>
2943 * @param {Object} attrs The new properties and values
2945 Dygraph
.prototype.updateOptions
= function(attrs
) {
2946 // TODO(danvk): this is a mess. Rethink this function.
2947 if ('rollPeriod' in attrs
) {
2948 this.rollPeriod_
= attrs
.rollPeriod
;
2950 if ('dateWindow' in attrs
) {
2951 this.dateWindow_
= attrs
.dateWindow
;
2954 // TODO(danvk): validate per-series options.
2959 // highlightCircleSize
2961 Dygraph
.update(this.user_attrs_
, attrs
);
2962 Dygraph
.update(this.renderOptions_
, attrs
);
2964 this.labelsFromCSV_
= (this.attr_("labels") == null);
2966 // TODO(danvk): this doesn't match the constructor logic
2967 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2968 if (attrs
['file']) {
2969 this.file_
= attrs
['file'];
2977 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2978 * containing div (which has presumably changed size since the dygraph was
2979 * instantiated. If the width/height are specified, the div will be resized.
2981 * This is far more efficient than destroying and re-instantiating a
2982 * Dygraph, since it doesn't have to reparse the underlying data.
2984 * @param {Number} width Width (in pixels)
2985 * @param {Number} height Height (in pixels)
2987 Dygraph
.prototype.resize
= function(width
, height
) {
2988 if (this.resize_lock
) {
2991 this.resize_lock
= true;
2993 if ((width
=== null) != (height
=== null)) {
2994 this.warn("Dygraph.resize() should be called with zero parameters or " +
2995 "two non-NULL parameters. Pretending it was zero.");
2996 width
= height
= null;
2999 // TODO(danvk): there should be a clear() method.
3000 this.maindiv_
.innerHTML
= "";
3001 this.attrs_
.labelsDiv
= null;
3004 this.maindiv_
.style
.width
= width
+ "px";
3005 this.maindiv_
.style
.height
= height
+ "px";
3006 this.width_
= width
;
3007 this.height_
= height
;
3009 this.width_
= this.maindiv_
.offsetWidth
;
3010 this.height_
= this.maindiv_
.offsetHeight
;
3013 this.createInterface_();
3016 this.resize_lock
= false;
3020 * Adjusts the number of points in the rolling average. Updates the graph to
3021 * reflect the new averaging period.
3022 * @param {Number} length Number of points over which to average the data.
3024 Dygraph
.prototype.adjustRoll
= function(length
) {
3025 this.rollPeriod_
= length
;
3030 * Returns a boolean array of visibility statuses.
3032 Dygraph
.prototype.visibility
= function() {
3033 // Do lazy-initialization, so that this happens after we know the number of
3035 if (!this.attr_("visibility")) {
3036 this.attrs_
["visibility"] = [];
3038 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
3039 this.attr_("visibility").push(true);
3041 return this.attr_("visibility");
3045 * Changes the visiblity of a series.
3047 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3048 var x
= this.visibility();
3049 if (num
< 0 || num
>= x
.length
) {
3050 this.warn("invalid series number in setVisibility: " + num
);
3058 * Update the list of annotations and redraw the chart.
3060 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3061 // Only add the annotation CSS rule once we know it will be used.
3062 Dygraph
.addAnnotationRule();
3063 this.annotations_
= ann
;
3064 this.layout_
.setAnnotations(this.annotations_
);
3065 if (!suppressDraw
) {
3071 * Return the list of annotations.
3073 Dygraph
.prototype.annotations
= function() {
3074 return this.annotations_
;
3078 * Get the index of a series (column) given its name. The first column is the
3079 * x-axis, so the data series start with index 1.
3081 Dygraph
.prototype.indexFromSetName
= function(name
) {
3082 var labels
= this.attr_("labels");
3083 for (var i
= 0; i
< labels
.length
; i
++) {
3084 if (labels
[i
] == name
) return i
;
3089 Dygraph
.addAnnotationRule
= function() {
3090 if (Dygraph
.addedAnnotationCSS
) return;
3092 var rule
= "border: 1px solid black; " +
3093 "background-color: white; " +
3094 "text-align: center;";
3096 var styleSheetElement
= document
.createElement("style");
3097 styleSheetElement
.type
= "text/css";
3098 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3100 // Find the first style sheet that we can access.
3101 // We may not add a rule to a style sheet from another domain for security
3102 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3103 // adds its own style sheets from google.com.
3104 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3105 if (document
.styleSheets
[i
].disabled
) continue;
3106 var mysheet
= document
.styleSheets
[i
];
3108 if (mysheet
.insertRule
) { // Firefox
3109 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3110 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3111 } else if (mysheet
.addRule
) { // IE
3112 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3114 Dygraph
.addedAnnotationCSS
= true;
3117 // Was likely a security exception.
3121 this.warn("Unable to add default annotation CSS rule; display may be off.");
3125 * Create a new canvas element. This is more complex than a simple
3126 * document.createElement("canvas") because of IE and excanvas.
3128 Dygraph
.createCanvas
= function() {
3129 var canvas
= document
.createElement("canvas");
3131 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
3132 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
3133 canvas
= G_vmlCanvasManager
.initElement(canvas
);
3141 * A wrapper around Dygraph that implements the gviz API.
3142 * @param {Object} container The DOM object the visualization should live in.
3144 Dygraph
.GVizChart
= function(container
) {
3145 this.container
= container
;
3148 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
3149 // Clear out any existing dygraph.
3150 // TODO(danvk): would it make more sense to simply redraw using the current
3151 // date_graph object?
3152 this.container
.innerHTML
= '';
3153 if (typeof(this.date_graph
) != 'undefined') {
3154 this.date_graph
.destroy();
3157 this.date_graph
= new Dygraph(this.container
, data
, options
);
3161 * Google charts compatible setSelection
3162 * Only row selection is supported, all points in the row will be highlighted
3163 * @param {Array} array of the selected cells
3166 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
3168 if (selection_array
.length
) {
3169 row
= selection_array
[0].row
;
3171 this.date_graph
.setSelection(row
);
3175 * Google charts compatible getSelection implementation
3176 * @return {Array} array of the selected cells
3179 Dygraph
.GVizChart
.prototype.getSelection
= function() {
3182 var row
= this.date_graph
.getSelection();
3184 if (row
< 0) return selection
;
3187 for (var i
in this.date_graph
.layout_
.datasets
) {
3188 selection
.push({row
: row
, column
: col
});
3195 // Older pages may still use this name.
3196 DateGraph
= Dygraph
;