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__();
77 * Number formatting function which mimicks the behavior of %g in printf, i.e.
78 * either exponential or fixed format (without trailing 0s) is used depending on
79 * the length of the generated string. The advantage of this format is that
80 * there is a predictable upper bound on the resulting string length and
81 * significant figures are not dropped.
83 * NOTE: JavaScript's native toPrecision() is NOT a drop-in replacement for %g.
84 * It creates strings which are too long for absolute values between 10^-4 and
85 * 10^-6. See tests/number-format.html for examples.
87 * @param {Number} x The number to format
88 * @param {Number} opt_precision The precision to use, default 2.
89 * @return {String} A string formatted like %g in printf. The max generated
90 * string length should be precision +
92 Dygraph
.defaultFormat
= function(x
, opt_precision
) {
93 // Avoid invalid precision values; [1, 21] is the valid range.
94 var p
= Math
.min(Math
.max(1, opt_precision
|| 2), 21);
96 // This is deceptively simple. The actual algorithm comes from:
98 // Max allowed length = p + 4
99 // where 4 comes from 'e+n' and '.'.
101 // Length of fixed format = 2 + y + p
102 // where 2 comes from '0.' and y = # of leading zeroes.
104 // Equating the two and solving for y yields y = 2, or 0.00xxxx which is
107 // Since the behavior of toPrecision() is identical for larger numbers, we
108 // don't have to worry about the other bound.
110 // Finally, the argument for toExponential() is the number of trailing digits,
111 // so we take off 1 for the value before the '.'.
112 return (Math
.abs(x
) < 1.0e-3 && x
!= 0.0) ?
113 x
.toExponential(p
- 1) : x
.toPrecision(p
);
116 // Various default values
117 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
118 Dygraph
.DEFAULT_WIDTH
= 480;
119 Dygraph
.DEFAULT_HEIGHT
= 320;
120 Dygraph
.AXIS_LINE_WIDTH
= 0.3;
122 // Default attribute values.
123 Dygraph
.DEFAULT_ATTRS
= {
124 highlightCircleSize
: 3,
130 // TODO(danvk): move defaults from createStatusMessage_ here.
132 labelsSeparateLines
: false,
133 labelsShowZeroValues
: true,
136 showLabelsOnHighlight
: true,
138 yValueFormatter
: Dygraph
.defaultFormat
,
143 axisLabelFontSize
: 14,
146 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
150 xValueFormatter
: Dygraph
.dateString_
,
151 xValueParser
: Dygraph
.dateParser
,
152 xTicker
: Dygraph
.dateTicker
,
160 wilsonInterval
: true, // only relevant if fractions is true
164 connectSeparatedPoints
: false,
167 hideOverlayOnMouseOut
: true,
173 // Various logging levels.
179 // Directions for panning and zooming. Use bit operations when combined
180 // values are possible.
181 Dygraph
.HORIZONTAL
= 1;
182 Dygraph
.VERTICAL
= 2;
184 // Used for initializing annotation CSS rules only once.
185 Dygraph
.addedAnnotationCSS
= false;
187 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
188 // Labels is no longer a constructor parameter, since it's typically set
189 // directly from the data source. It also conains a name for the x-axis,
190 // which the previous constructor form did not.
191 if (labels
!= null) {
192 var new_labels
= ["Date"];
193 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
194 Dygraph
.update(attrs
, { 'labels': new_labels
});
196 this.__init__(div
, file
, attrs
);
200 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
201 * and interaction <canvas> inside of it. See the constructor for details
203 * @param {Element} div the Element to render the graph into.
204 * @param {String | Function} file Source data
205 * @param {Object} attrs Miscellaneous other options
208 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
209 // Hack for IE: if we're using excanvas and the document hasn't finished
210 // loading yet (and hence may not have initialized whatever it needs to
211 // initialize), then keep calling this routine periodically until it has.
212 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
213 typeof(G_vmlCanvasManager
) != 'undefined' &&
214 document
.readyState
!= 'complete') {
216 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
219 // Support two-argument constructor
220 if (attrs
== null) { attrs
= {}; }
222 // Copy the important bits into the object
223 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
226 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
227 this.previousVerticalX_
= -1;
228 this.fractions_
= attrs
.fractions
|| false;
229 this.dateWindow_
= attrs
.dateWindow
|| null;
231 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
232 this.is_initial_draw_
= true;
233 this.annotations_
= [];
235 // Number of digits to use when labeling the x (if numeric) and y axis
237 this.numXDigits_
= 2;
238 this.numYDigits_
= 2;
240 // When labeling x (if numeric) or y values in the legend, there are
241 // numDigits + numExtraDigits of precision used. For axes labels with N
242 // digits of precision, the data should be displayed with at least N+1 digits
243 // of precision. The reason for this is to divide each interval between
244 // successive ticks into tenths (for 1) or hundredths (for 2), etc. For
245 // example, if the labels are [0, 1, 2], we want data to be displayed as
247 this.numExtraDigits_
= 1;
249 // Clear the div. This ensure that, if multiple dygraphs are passed the same
250 // div, then only one will be drawn.
253 // If the div isn't already sized then inherit from our attrs or
254 // give it a default size.
255 if (div
.style
.width
== '') {
256 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
258 if (div
.style
.height
== '') {
259 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
261 this.width_
= parseInt(div
.style
.width
, 10);
262 this.height_
= parseInt(div
.style
.height
, 10);
263 // The div might have been specified as percent of the current window size,
264 // convert that to an appropriate number of pixels.
265 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
266 this.width_
= div
.offsetWidth
;
268 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
269 this.height_
= div
.offsetHeight
;
272 if (this.width_
== 0) {
273 this.error("dygraph has zero width. Please specify a width in pixels.");
275 if (this.height_
== 0) {
276 this.error("dygraph has zero height. Please specify a height in pixels.");
279 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
280 if (attrs
['stackedGraph']) {
281 attrs
['fillGraph'] = true;
282 // TODO(nikhilk): Add any other stackedGraph checks here.
285 // Dygraphs has many options, some of which interact with one another.
286 // To keep track of everything, we maintain two sets of options:
288 // this.user_attrs_ only options explicitly set by the user.
289 // this.attrs_ defaults, options derived from user_attrs_, data.
291 // Options are then accessed this.attr_('attr'), which first looks at
292 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
293 // defaults without overriding behavior that the user specifically asks for.
294 this.user_attrs_
= {};
295 Dygraph
.update(this.user_attrs_
, attrs
);
298 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
300 this.boundaryIds_
= [];
302 // Make a note of whether labels will be pulled from the CSV file.
303 this.labelsFromCSV_
= (this.attr_("labels") == null);
305 // Create the containing DIV and other interactive elements
306 this.createInterface_();
311 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
313 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
314 this.user_attrs_
[seriesName
] != null &&
315 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
316 return this.user_attrs_
[seriesName
][name
];
317 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
318 return this.user_attrs_
[name
];
319 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
320 return this.attrs_
[name
];
326 // TODO(danvk): any way I can get the line numbers to be this.warn call?
327 Dygraph
.prototype.log
= function(severity
, message
) {
328 if (typeof(console
) != 'undefined') {
331 console
.debug('dygraphs: ' + message
);
334 console
.info('dygraphs: ' + message
);
336 case Dygraph
.WARNING
:
337 console
.warn('dygraphs: ' + message
);
340 console
.error('dygraphs: ' + message
);
345 Dygraph
.prototype.info
= function(message
) {
346 this.log(Dygraph
.INFO
, message
);
348 Dygraph
.prototype.warn
= function(message
) {
349 this.log(Dygraph
.WARNING
, message
);
351 Dygraph
.prototype.error
= function(message
) {
352 this.log(Dygraph
.ERROR
, message
);
356 * Returns the current rolling period, as set by the user or an option.
357 * @return {Number} The number of points in the rolling window
359 Dygraph
.prototype.rollPeriod
= function() {
360 return this.rollPeriod_
;
364 * Returns the currently-visible x-range. This can be affected by zooming,
365 * panning or a call to updateOptions.
366 * Returns a two-element array: [left, right].
367 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
369 Dygraph
.prototype.xAxisRange
= function() {
370 if (this.dateWindow_
) return this.dateWindow_
;
372 // The entire chart is visible.
373 var left
= this.rawData_
[0][0];
374 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
375 return [left
, right
];
379 * Returns the currently-visible y-range for an axis. This can be affected by
380 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
381 * called with no arguments, returns the range of the first axis.
382 * Returns a two-element array: [bottom, top].
384 Dygraph
.prototype.yAxisRange
= function(idx
) {
385 if (typeof(idx
) == "undefined") idx
= 0;
386 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
387 return [ this.axes_
[idx
].computedValueRange
[0],
388 this.axes_
[idx
].computedValueRange
[1] ];
392 * Returns the currently-visible y-ranges for each axis. This can be affected by
393 * zooming, panning, calls to updateOptions, etc.
394 * Returns an array of [bottom, top] pairs, one for each y-axis.
396 Dygraph
.prototype.yAxisRanges
= function() {
398 for (var i
= 0; i
< this.axes_
.length
; i
++) {
399 ret
.push(this.yAxisRange(i
));
404 // TODO(danvk): use these functions throughout dygraphs.
406 * Convert from data coordinates to canvas/div X/Y coordinates.
407 * If specified, do this conversion for the coordinate system of a particular
408 * axis. Uses the first axis by default.
409 * Returns a two-element array: [X, Y]
411 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
412 var ret
= [null, null];
413 var area
= this.plotter_
.area
;
415 var xRange
= this.xAxisRange();
416 ret
[0] = area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
420 var yRange
= this.yAxisRange(axis
);
421 ret
[1] = area
.y
+ (yRange
[1] - y
) / (yRange
[1] - yRange
[0]) * area
.h
;
428 * Convert from canvas/div coords to data coordinates.
429 * If specified, do this conversion for the coordinate system of a particular
430 * axis. Uses the first axis by default.
431 * Returns a two-element array: [X, Y]
433 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
434 var ret
= [null, null];
435 var area
= this.plotter_
.area
;
437 var xRange
= this.xAxisRange();
438 ret
[0] = xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
442 var yRange
= this.yAxisRange(axis
);
443 ret
[1] = yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
450 * Returns the number of columns (including the independent variable).
452 Dygraph
.prototype.numColumns
= function() {
453 return this.rawData_
[0].length
;
457 * Returns the number of rows (excluding any header/label row).
459 Dygraph
.prototype.numRows
= function() {
460 return this.rawData_
.length
;
464 * Returns the value in the given row and column. If the row and column exceed
465 * the bounds on the data, returns null. Also returns null if the value is
468 Dygraph
.prototype.getValue
= function(row
, col
) {
469 if (row
< 0 || row
> this.rawData_
.length
) return null;
470 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
472 return this.rawData_
[row
][col
];
475 Dygraph
.addEvent
= function(el
, evt
, fn
) {
476 var normed_fn
= function(e
) {
477 if (!e
) var e
= window
.event
;
480 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
481 el
.addEventListener(evt
, normed_fn
, false);
483 el
.attachEvent('on' + evt
, normed_fn
);
488 * Generates interface elements for the Dygraph: a containing div, a div to
489 * display the current point, and a textbox to adjust the rolling average
490 * period. Also creates the Renderer/Layout elements.
493 Dygraph
.prototype.createInterface_
= function() {
494 // Create the all-enclosing graph div
495 var enclosing
= this.maindiv_
;
497 this.graphDiv
= document
.createElement("div");
498 this.graphDiv
.style
.width
= this.width_
+ "px";
499 this.graphDiv
.style
.height
= this.height_
+ "px";
500 enclosing
.appendChild(this.graphDiv
);
502 // Create the canvas for interactive parts of the chart.
503 this.canvas_
= Dygraph
.createCanvas();
504 this.canvas_
.style
.position
= "absolute";
505 this.canvas_
.width
= this.width_
;
506 this.canvas_
.height
= this.height_
;
507 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
508 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
510 // ... and for static parts of the chart.
511 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
513 // The interactive parts of the graph are drawn on top of the chart.
514 this.graphDiv
.appendChild(this.hidden_
);
515 this.graphDiv
.appendChild(this.canvas_
);
516 this.mouseEventElement_
= this.canvas_
;
519 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
520 dygraph
.mouseMove_(e
);
522 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
523 dygraph
.mouseOut_(e
);
526 // Create the grapher
527 // TODO(danvk): why does the Layout need its own set of options?
528 this.layoutOptions_
= { 'xOriginIsZero': false };
529 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
530 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
531 Dygraph
.update(this.layoutOptions_
, {
532 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
534 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
536 // TODO(danvk): why does the Renderer need its own set of options?
537 this.renderOptions_
= { colorScheme
: this.colors_
,
539 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
540 Dygraph
.update(this.renderOptions_
, this.attrs_
);
541 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
543 this.createStatusMessage_();
544 this.createDragInterface_();
548 * Detach DOM elements in the dygraph and null out all data references.
549 * Calling this when you're done with a dygraph can dramatically reduce memory
550 * usage. See, e.g., the tests/perf.html example.
552 Dygraph
.prototype.destroy
= function() {
553 var removeRecursive
= function(node
) {
554 while (node
.hasChildNodes()) {
555 removeRecursive(node
.firstChild
);
556 node
.removeChild(node
.firstChild
);
559 removeRecursive(this.maindiv_
);
561 var nullOut
= function(obj
) {
563 if (typeof(obj
[n
]) === 'object') {
569 // These may not all be necessary, but it can't hurt...
570 nullOut(this.layout_
);
571 nullOut(this.plotter_
);
576 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
577 * this particular canvas. All Dygraph work is done on this.canvas_.
578 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
579 * @return {Object} The newly-created canvas
582 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
583 var h
= Dygraph
.createCanvas();
584 h
.style
.position
= "absolute";
585 // TODO(danvk): h should be offset from canvas. canvas needs to include
586 // some extra area to make it easier to zoom in on the far left and far
587 // right. h needs to be precisely the plot area, so that clipping occurs.
588 h
.style
.top
= canvas
.style
.top
;
589 h
.style
.left
= canvas
.style
.left
;
590 h
.width
= this.width_
;
591 h
.height
= this.height_
;
592 h
.style
.width
= this.width_
+ "px"; // for IE
593 h
.style
.height
= this.height_
+ "px"; // for IE
597 // Taken from MochiKit.Color
598 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
602 if (saturation
=== 0) {
607 var i
= Math
.floor(hue
* 6);
608 var f
= (hue
* 6) - i
;
609 var p
= value
* (1 - saturation
);
610 var q
= value
* (1 - (saturation
* f
));
611 var t
= value
* (1 - (saturation
* (1 - f
)));
613 case 1: red
= q
; green
= value
; blue
= p
; break;
614 case 2: red
= p
; green
= value
; blue
= t
; break;
615 case 3: red
= p
; green
= q
; blue
= value
; break;
616 case 4: red
= t
; green
= p
; blue
= value
; break;
617 case 5: red
= value
; green
= p
; blue
= q
; break;
618 case 6: // fall through
619 case 0: red
= value
; green
= t
; blue
= p
; break;
622 red
= Math
.floor(255 * red
+ 0.5);
623 green
= Math
.floor(255 * green
+ 0.5);
624 blue
= Math
.floor(255 * blue
+ 0.5);
625 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
630 * Generate a set of distinct colors for the data series. This is done with a
631 * color wheel. Saturation/Value are customizable, and the hue is
632 * equally-spaced around the color wheel. If a custom set of colors is
633 * specified, that is used instead.
636 Dygraph
.prototype.setColors_
= function() {
637 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
638 // away with this.renderOptions_.
639 var num
= this.attr_("labels").length
- 1;
641 var colors
= this.attr_('colors');
643 var sat
= this.attr_('colorSaturation') || 1.0;
644 var val
= this.attr_('colorValue') || 0.5;
645 var half
= Math
.ceil(num
/ 2);
646 for (var i
= 1; i
<= num
; i
++) {
647 if (!this.visibility()[i
-1]) continue;
648 // alternate colors for high contrast.
649 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
650 var hue
= (1.0 * idx
/ (1 + num
));
651 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
654 for (var i
= 0; i
< num
; i
++) {
655 if (!this.visibility()[i
]) continue;
656 var colorStr
= colors
[i
% colors
.length
];
657 this.colors_
.push(colorStr
);
661 // TODO(danvk): update this w/r
/t/ the
new options system
.
662 this.renderOptions_
.colorScheme
= this.colors_
;
663 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
664 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
665 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
669 * Return the list of colors. This is either the list of colors passed in the
670 * attributes, or the autogenerated list of rgb(r,g,b) strings.
671 * @return {Array<string>} The list of colors.
673 Dygraph
.prototype.getColors
= function() {
677 // The following functions are from quirksmode.org with a modification for Safari from
678 // http://blog.firetree.net/2005/07/04/javascript-find-position/
679 // http://www.quirksmode.org/js
/findpos
.html
680 Dygraph
.findPosX
= function(obj
) {
685 curleft
+= obj
.offsetLeft
;
686 if(!obj
.offsetParent
)
688 obj
= obj
.offsetParent
;
695 Dygraph
.findPosY
= function(obj
) {
700 curtop
+= obj
.offsetTop
;
701 if(!obj
.offsetParent
)
703 obj
= obj
.offsetParent
;
713 * Create the div that contains information on the selected point(s)
714 * This goes in the top right of the canvas, unless an external div has already
718 Dygraph
.prototype.createStatusMessage_
= function() {
719 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
720 if (userLabelsDiv
&& null != userLabelsDiv
721 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
722 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
724 if (!this.attr_("labelsDiv")) {
725 var divWidth
= this.attr_('labelsDivWidth');
727 "position": "absolute",
730 "width": divWidth
+ "px",
732 "left": (this.width_
- divWidth
- 2) + "px",
733 "background": "white",
735 "overflow": "hidden"};
736 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
737 var div
= document
.createElement("div");
738 for (var name
in messagestyle
) {
739 if (messagestyle
.hasOwnProperty(name
)) {
740 div
.style
[name
] = messagestyle
[name
];
743 this.graphDiv
.appendChild(div
);
744 this.attrs_
.labelsDiv
= div
;
749 * Position the labels div so that its right edge is flush with the right edge
750 * of the charting area.
752 Dygraph
.prototype.positionLabelsDiv_
= function() {
753 // Don't touch a user-specified labelsDiv.
754 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
756 var area
= this.plotter_
.area
;
757 var div
= this.attr_("labelsDiv");
758 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
762 * Create the text box to adjust the averaging period
765 Dygraph
.prototype.createRollInterface_
= function() {
766 // Create a roller if one doesn't exist already.
768 this.roller_
= document
.createElement("input");
769 this.roller_
.type
= "text";
770 this.roller_
.style
.display
= "none";
771 this.graphDiv
.appendChild(this.roller_
);
774 var display
= this.attr_('showRoller') ? 'block' : 'none';
776 var textAttr
= { "position": "absolute",
778 "top": (this.plotter_
.area
.h
- 25) + "px",
779 "left": (this.plotter_
.area
.x
+ 1) + "px",
782 this.roller_
.size
= "2";
783 this.roller_
.value
= this.rollPeriod_
;
784 for (var name
in textAttr
) {
785 if (textAttr
.hasOwnProperty(name
)) {
786 this.roller_
.style
[name
] = textAttr
[name
];
791 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
794 // These functions are taken from MochiKit.Signal
795 Dygraph
.pageX
= function(e
) {
797 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
800 var b
= document
.body
;
802 (de
.scrollLeft
|| b
.scrollLeft
) -
803 (de
.clientLeft
|| 0);
807 Dygraph
.pageY
= function(e
) {
809 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
812 var b
= document
.body
;
814 (de
.scrollTop
|| b
.scrollTop
) -
820 * Set up all the mouse handlers needed to capture dragging behavior for zoom
824 Dygraph
.prototype.createDragInterface_
= function() {
827 // Tracks whether the mouse is down right now
828 var isZooming
= false;
829 var isPanning
= false; // is this drag part of a pan?
830 var is2DPan
= false; // if so, is that pan 1- or 2-dimensional?
831 var dragStartX
= null;
832 var dragStartY
= null;
835 var dragDirection
= null;
838 var prevDragDirection
= null;
840 // TODO(danvk): update this comment
841 // draggingDate and draggingValue represent the [date,value] point on the
842 // graph at which the mouse was pressed. As the mouse moves while panning,
843 // the viewport must pan so that the mouse position points to
844 // [draggingDate, draggingValue]
845 var draggingDate
= null;
847 // TODO(danvk): update this comment
848 // The range in second/value units that the viewport encompasses during a
849 // panning operation.
850 var dateRange
= null;
852 // Utility function to convert page-wide coordinates to canvas coords
855 var getX
= function(e
) { return Dygraph
.pageX(e
) - px
};
856 var getY
= function(e
) { return Dygraph
.pageY(e
) - py
};
858 // Draw zoom rectangles when the mouse is down and the user moves around
859 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(event
) {
861 dragEndX
= getX(event
);
862 dragEndY
= getY(event
);
864 var xDelta
= Math
.abs(dragStartX
- dragEndX
);
865 var yDelta
= Math
.abs(dragStartY
- dragEndY
);
867 // drag direction threshold for y axis is twice as large as x axis
868 dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
870 self
.drawZoomRect_(dragDirection
, dragStartX
, dragEndX
, dragStartY
, dragEndY
,
871 prevDragDirection
, prevEndX
, prevEndY
);
875 prevDragDirection
= dragDirection
;
876 } else if (isPanning
) {
877 dragEndX
= getX(event
);
878 dragEndY
= getY(event
);
880 // TODO(danvk): update this comment
881 // Want to have it so that:
882 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
883 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
884 // 3. draggingValue appears at dragEndY.
885 // 4. valueRange is unaltered.
887 var minDate
= draggingDate
- (dragEndX
/ self
.width_
) * dateRange
;
888 var maxDate
= minDate
+ dateRange
;
889 self
.dateWindow_
= [minDate
, maxDate
];
892 // y-axis scaling is automatic unless this is a full 2D pan.
894 // Adjust each axis appropriately.
895 var y_frac
= dragEndY
/ self
.height_
;
896 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
897 var axis
= self
.axes_
[i
];
898 var maxValue
= axis
.draggingValue
+ y_frac
* axis
.dragValueRange
;
899 var minValue
= maxValue
- axis
.dragValueRange
;
900 axis
.valueWindow
= [ minValue
, maxValue
];
908 // Track the beginning of drag events
909 Dygraph
.addEvent(this.mouseEventElement_
, 'mousedown', function(event
) {
910 // prevents mouse drags from selecting page text.
911 if (event
.preventDefault
) {
912 event
.preventDefault(); // Firefox, Chrome, etc.
914 event
.returnValue
= false; // IE
915 event
.cancelBubble
= true;
918 px
= Dygraph
.findPosX(self
.canvas_
);
919 py
= Dygraph
.findPosY(self
.canvas_
);
920 dragStartX
= getX(event
);
921 dragStartY
= getY(event
);
923 if (event
.altKey
|| event
.shiftKey
) {
924 // have to be zoomed in to pan.
926 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
927 if (self
.axes_
[i
].valueWindow
|| self
.axes_
[i
].valueRange
) {
932 if (!self
.dateWindow_
&& !zoomedY
) return;
935 var xRange
= self
.xAxisRange();
936 dateRange
= xRange
[1] - xRange
[0];
938 // Record the range of each y-axis at the start of the drag.
939 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
941 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
942 var axis
= self
.axes_
[i
];
943 var yRange
= self
.yAxisRange(i
);
944 axis
.dragValueRange
= yRange
[1] - yRange
[0];
945 var r
= self
.toDataCoords(null, dragStartY
, i
);
946 axis
.draggingValue
= r
[1];
947 if (axis
.valueWindow
|| axis
.valueRange
) is2DPan
= true;
950 // TODO(konigsberg): Switch from all this math to toDataCoords?
951 // Seems to work for the dragging value.
952 draggingDate
= (dragStartX
/ self
.width_
) * dateRange
+ xRange
[0];
958 // If the user releases the mouse button during a drag, but not over the
959 // canvas, then it doesn't count as a zooming action.
960 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
961 if (isZooming
|| isPanning
) {
971 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
972 delete self
.axes_
[i
].draggingValue
;
973 delete self
.axes_
[i
].dragValueRange
;
978 // Temporarily cancel the dragging event when the mouse leaves the graph
979 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(event
) {
986 // If the mouse is released on the canvas during a drag event, then it's a
987 // zoom. Only do the zoom if it's over a large enough area (>= 10 pixels)
988 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseup', function(event
) {
991 dragEndX
= getX(event
);
992 dragEndY
= getY(event
);
993 var regionWidth
= Math
.abs(dragEndX
- dragStartX
);
994 var regionHeight
= Math
.abs(dragEndY
- dragStartY
);
996 if (regionWidth
< 2 && regionHeight
< 2 &&
997 self
.lastx_
!= undefined
&& self
.lastx_
!= -1) {
998 // TODO(danvk): pass along more info about the points, e.g. 'x'
999 if (self
.attr_('clickCallback') != null) {
1000 self
.attr_('clickCallback')(event
, self
.lastx_
, self
.selPoints_
);
1002 if (self
.attr_('pointClickCallback')) {
1003 // check if the click was on a particular point.
1004 var closestIdx
= -1;
1005 var closestDistance
= 0;
1006 for (var i
= 0; i
< self
.selPoints_
.length
; i
++) {
1007 var p
= self
.selPoints_
[i
];
1008 var distance
= Math
.pow(p
.canvasx
- dragEndX
, 2) +
1009 Math
.pow(p
.canvasy
- dragEndY
, 2);
1010 if (closestIdx
== -1 || distance
< closestDistance
) {
1011 closestDistance
= distance
;
1016 // Allow any click within two pixels of the dot.
1017 var radius
= self
.attr_('highlightCircleSize') + 2;
1018 if (closestDistance
<= 5 * 5) {
1019 self
.attr_('pointClickCallback')(event
, self
.selPoints_
[closestIdx
]);
1024 if (regionWidth
>= 10 && dragDirection
== Dygraph
.HORIZONTAL
) {
1025 self
.doZoomX_(Math
.min(dragStartX
, dragEndX
),
1026 Math
.max(dragStartX
, dragEndX
));
1027 } else if (regionHeight
>= 10 && dragDirection
== Dygraph
.VERTICAL
){
1028 self
.doZoomY_(Math
.min(dragStartY
, dragEndY
),
1029 Math
.max(dragStartY
, dragEndY
));
1031 self
.canvas_
.getContext("2d").clearRect(0, 0,
1033 self
.canvas_
.height
);
1043 draggingDate
= null;
1049 // Double-clicking zooms back out
1050 Dygraph
.addEvent(this.mouseEventElement_
, 'dblclick', function(event
) {
1051 // Disable zooming out if panning.
1052 if (event
.altKey
|| event
.shiftKey
) return;
1059 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1060 * up any previous zoom rectangles that were drawn. This could be optimized to
1061 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1064 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1065 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1066 * @param {Number} startX The X position where the drag started, in canvas
1068 * @param {Number} endX The current X position of the drag, in canvas coords.
1069 * @param {Number} startY The Y position where the drag started, in canvas
1071 * @param {Number} endY The current Y position of the drag, in canvas coords.
1072 * @param {Number} prevDirection the value of direction on the previous call to
1073 * this function. Used to avoid excess redrawing
1074 * @param {Number} prevEndX The value of endX on the previous call to this
1075 * function. Used to avoid excess redrawing
1076 * @param {Number} prevEndY The value of endY on the previous call to this
1077 * function. Used to avoid excess redrawing
1080 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1081 prevDirection
, prevEndX
, prevEndY
) {
1082 var ctx
= this.canvas_
.getContext("2d");
1084 // Clean up from the previous rect if necessary
1085 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1086 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1087 Math
.abs(startX
- prevEndX
), this.height_
);
1088 } else if (prevDirection
== Dygraph
.VERTICAL
){
1089 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1090 this.width_
, Math
.abs(startY
- prevEndY
));
1093 // Draw a light-grey rectangle to show the new viewing area
1094 if (direction
== Dygraph
.HORIZONTAL
) {
1095 if (endX
&& startX
) {
1096 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1097 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1098 Math
.abs(endX
- startX
), this.height_
);
1101 if (direction
== Dygraph
.VERTICAL
) {
1102 if (endY
&& startY
) {
1103 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1104 ctx
.fillRect(0, Math
.min(startY
, endY
),
1105 this.width_
, Math
.abs(endY
- startY
));
1111 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1112 * the canvas. The exact zoom window may be slightly larger if there are no data
1113 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1114 * which accepts dates that match the raw data. This function redraws the graph.
1116 * @param {Number} lowX The leftmost pixel value that should be visible.
1117 * @param {Number} highX The rightmost pixel value that should be visible.
1120 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1121 // Find the earliest and latest dates contained in this canvasx range.
1122 // Convert the call to date ranges of the raw data.
1123 var r
= this.toDataCoords(lowX
, null);
1125 r
= this.toDataCoords(highX
, null);
1127 this.doZoomXDates_(minDate
, maxDate
);
1131 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1132 * method with doZoomX which accepts pixel coordinates. This function redraws
1135 * @param {Number} minDate The minimum date that should be visible.
1136 * @param {Number} maxDate The maximum date that should be visible.
1139 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1140 this.dateWindow_
= [minDate
, maxDate
];
1142 if (this.attr_("zoomCallback")) {
1143 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1148 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1149 * the canvas. This function redraws the graph.
1151 * @param {Number} lowY The topmost pixel value that should be visible.
1152 * @param {Number} highY The lowest pixel value that should be visible.
1155 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1156 // Find the highest and lowest values in pixel range for each axis.
1157 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1158 // This is because pixels increase as you go down on the screen, whereas data
1159 // coordinates increase as you go up the screen.
1160 var valueRanges
= [];
1161 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1162 var hi
= this.toDataCoords(null, lowY
, i
);
1163 var low
= this.toDataCoords(null, highY
, i
);
1164 this.axes_
[i
].valueWindow
= [low
[1], hi
[1]];
1165 valueRanges
.push([low
[1], hi
[1]]);
1169 if (this.attr_("zoomCallback")) {
1170 var xRange
= this.xAxisRange();
1171 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1176 * Reset the zoom to the original view coordinates. This is the same as
1177 * double-clicking on the graph.
1181 Dygraph
.prototype.doUnzoom_
= function() {
1183 if (this.dateWindow_
!= null) {
1185 this.dateWindow_
= null;
1188 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1189 if (this.axes_
[i
].valueWindow
!= null) {
1191 delete this.axes_
[i
].valueWindow
;
1196 // Putting the drawing operation before the callback because it resets
1199 if (this.attr_("zoomCallback")) {
1200 var minDate
= this.rawData_
[0][0];
1201 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1202 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1208 * When the mouse moves in the canvas, display information about a nearby data
1209 * point and draw dots over those points in the data series. This function
1210 * takes care of cleanup of previously-drawn dots.
1211 * @param {Object} event The mousemove event from the browser.
1214 Dygraph
.prototype.mouseMove_
= function(event
) {
1215 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1216 var points
= this.layout_
.points
;
1221 // Loop through all the points and find the date nearest to our current
1223 var minDist
= 1e+100;
1225 for (var i
= 0; i
< points
.length
; i
++) {
1226 var point
= points
[i
];
1227 if (point
== null) continue;
1228 var dist
= Math
.abs(points
[i
].canvasx
- canvasx
);
1229 if (dist
> minDist
) continue;
1233 if (idx
>= 0) lastx
= points
[idx
].xval
;
1234 // Check that you can really highlight the last day's data
1235 var last
= points
[points
.length
-1];
1236 if (last
!= null && canvasx
> last
.canvasx
)
1237 lastx
= points
[points
.length
-1].xval
;
1239 // Extract the points we've selected
1240 this.selPoints_
= [];
1241 var l
= points
.length
;
1242 if (!this.attr_("stackedGraph")) {
1243 for (var i
= 0; i
< l
; i
++) {
1244 if (points
[i
].xval
== lastx
) {
1245 this.selPoints_
.push(points
[i
]);
1249 // Need to 'unstack' points starting from the bottom
1250 var cumulative_sum
= 0;
1251 for (var i
= l
- 1; i
>= 0; i
--) {
1252 if (points
[i
].xval
== lastx
) {
1253 var p
= {}; // Clone the point since we modify it
1254 for (var k
in points
[i
]) {
1255 p
[k
] = points
[i
][k
];
1257 p
.yval
-= cumulative_sum
;
1258 cumulative_sum
+= p
.yval
;
1259 this.selPoints_
.push(p
);
1262 this.selPoints_
.reverse();
1265 if (this.attr_("highlightCallback")) {
1266 var px
= this.lastx_
;
1267 if (px
!== null && lastx
!= px
) {
1268 // only fire if the selected point has changed.
1269 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1273 // Save last x position for callbacks.
1274 this.lastx_
= lastx
;
1276 this.updateSelection_();
1280 * Transforms layout_.points index into data row number.
1281 * @param int layout_.points index
1282 * @return int row number, or -1 if none could be found.
1285 Dygraph
.prototype.idxToRow_
= function(idx
) {
1286 if (idx
< 0) return -1;
1288 for (var i
in this.layout_
.datasets
) {
1289 if (idx
< this.layout_
.datasets
[i
].length
) {
1290 return this.boundaryIds_
[0][0]+idx
;
1292 idx
-= this.layout_
.datasets
[i
].length
;
1298 * Draw dots over the selectied points in the data series. This function
1299 * takes care of cleanup of previously-drawn dots.
1302 Dygraph
.prototype.updateSelection_
= function() {
1303 // Clear the previously drawn vertical, if there is one
1304 var ctx
= this.canvas_
.getContext("2d");
1305 if (this.previousVerticalX_
>= 0) {
1306 // Determine the maximum highlight circle size.
1307 var maxCircleSize
= 0;
1308 var labels
= this.attr_('labels');
1309 for (var i
= 1; i
< labels
.length
; i
++) {
1310 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1311 if (r
> maxCircleSize
) maxCircleSize
= r
;
1313 var px
= this.previousVerticalX_
;
1314 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1315 2 * maxCircleSize
+ 2, this.height_
);
1318 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1320 if (this.selPoints_
.length
> 0) {
1321 var canvasx
= this.selPoints_
[0].canvasx
;
1323 // Set the status message to indicate the selected point(s)
1324 var replace
= this.attr_('xValueFormatter')(
1325 this.lastx_
, this.numXDigits_
+ this.numExtraDigits_
) + ":";
1326 var fmtFunc
= this.attr_('yValueFormatter');
1327 var clen
= this.colors_
.length
;
1329 if (this.attr_('showLabelsOnHighlight')) {
1330 // Set the status message to indicate the selected point(s)
1331 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1332 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1333 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1334 if (this.attr_("labelsSeparateLines")) {
1337 var point
= this.selPoints_
[i
];
1338 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1339 var yval
= fmtFunc(point
.yval
, this.numYDigits_
+ this.numExtraDigits_
);
1340 replace
+= " <b><font color='" + c
.toHex() + "'>"
1341 + point
.name
+ "</font></b>:"
1345 this.attr_("labelsDiv").innerHTML
= replace
;
1348 // Draw colored circles over the center of each selected point
1350 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1351 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1353 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1355 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1356 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1357 0, 2 * Math
.PI
, false);
1362 this.previousVerticalX_
= canvasx
;
1367 * Set manually set selected dots, and display information about them
1368 * @param int row number that should by highlighted
1369 * false value clears the selection
1372 Dygraph
.prototype.setSelection
= function(row
) {
1373 // Extract the points we've selected
1374 this.selPoints_
= [];
1377 if (row
!== false) {
1378 row
= row
-this.boundaryIds_
[0][0];
1381 if (row
!== false && row
>= 0) {
1382 for (var i
in this.layout_
.datasets
) {
1383 if (row
< this.layout_
.datasets
[i
].length
) {
1384 var point
= this.layout_
.points
[pos
+row
];
1386 if (this.attr_("stackedGraph")) {
1387 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1390 this.selPoints_
.push(point
);
1392 pos
+= this.layout_
.datasets
[i
].length
;
1396 if (this.selPoints_
.length
) {
1397 this.lastx_
= this.selPoints_
[0].xval
;
1398 this.updateSelection_();
1401 this.clearSelection();
1407 * The mouse has left the canvas. Clear out whatever artifacts remain
1408 * @param {Object} event the mouseout event from the browser.
1411 Dygraph
.prototype.mouseOut_
= function(event
) {
1412 if (this.attr_("unhighlightCallback")) {
1413 this.attr_("unhighlightCallback")(event
);
1416 if (this.attr_("hideOverlayOnMouseOut")) {
1417 this.clearSelection();
1422 * Remove all selection from the canvas
1425 Dygraph
.prototype.clearSelection
= function() {
1426 // Get rid of the overlay data
1427 var ctx
= this.canvas_
.getContext("2d");
1428 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1429 this.attr_("labelsDiv").innerHTML
= "";
1430 this.selPoints_
= [];
1435 * Returns the number of the currently selected row
1436 * @return int row number, of -1 if nothing is selected
1439 Dygraph
.prototype.getSelection
= function() {
1440 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1444 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1445 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1446 return row
+ this.boundaryIds_
[0][0];
1452 Dygraph
.zeropad
= function(x
) {
1453 if (x
< 10) return "0" + x
; else return "" + x
;
1457 * Return a string version of the hours, minutes and seconds portion of a date.
1458 * @param {Number} date The JavaScript date (ms since epoch)
1459 * @return {String} A time of the form "HH:MM:SS"
1462 Dygraph
.hmsString_
= function(date
) {
1463 var zeropad
= Dygraph
.zeropad
;
1464 var d
= new Date(date
);
1465 if (d
.getSeconds()) {
1466 return zeropad(d
.getHours()) + ":" +
1467 zeropad(d
.getMinutes()) + ":" +
1468 zeropad(d
.getSeconds());
1470 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1475 * Convert a JS date to a string appropriate to display on an axis that
1476 * is displaying values at the stated granularity.
1477 * @param {Date} date The date to format
1478 * @param {Number} granularity One of the Dygraph granularity constants
1479 * @return {String} The formatted date
1482 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1483 if (granularity
>= Dygraph
.MONTHLY
) {
1484 return date
.strftime('%b %y');
1486 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1487 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1488 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1490 return Dygraph
.hmsString_(date
.getTime());
1496 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1497 * @param {Number} date The JavaScript date (ms since epoch)
1498 * @return {String} A date of the form "YYYY/MM/DD"
1501 Dygraph
.dateString_
= function(date
) {
1502 var zeropad
= Dygraph
.zeropad
;
1503 var d
= new Date(date
);
1506 var year
= "" + d
.getFullYear();
1507 // Get a 0 padded month string
1508 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1509 // Get a 0 padded day string
1510 var day
= zeropad(d
.getDate());
1513 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1514 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1516 return year
+ "/" + month + "/" + day
+ ret
;
1520 * Fires when there's data available to be graphed.
1521 * @param {String} data Raw CSV data to be plotted
1524 Dygraph
.prototype.loadedEvent_
= function(data
) {
1525 this.rawData_
= this.parseCSV_(data
);
1529 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1530 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1531 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1534 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1537 Dygraph
.prototype.addXTicks_
= function() {
1538 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1539 var opts
= {xTicks
: []};
1540 var formatter
= this.attr_('xTicker');
1541 if (this.dateWindow_
) {
1542 opts
.xTicks
= formatter(this.dateWindow_
[0], this.dateWindow_
[1], this);
1544 // numericTicks() returns multiple values.
1545 var ret
= formatter(this.rawData_
[0][0],
1546 this.rawData_
[this.rawData_
.length
- 1][0], this);
1547 opts
.xTicks
= ret
.ticks
;
1548 this.numXDigits_
= ret
.numDigits
;
1550 this.layout_
.updateOptions(opts
);
1553 // Time granularity enumeration
1554 Dygraph
.SECONDLY
= 0;
1555 Dygraph
.TWO_SECONDLY
= 1;
1556 Dygraph
.FIVE_SECONDLY
= 2;
1557 Dygraph
.TEN_SECONDLY
= 3;
1558 Dygraph
.THIRTY_SECONDLY
= 4;
1559 Dygraph
.MINUTELY
= 5;
1560 Dygraph
.TWO_MINUTELY
= 6;
1561 Dygraph
.FIVE_MINUTELY
= 7;
1562 Dygraph
.TEN_MINUTELY
= 8;
1563 Dygraph
.THIRTY_MINUTELY
= 9;
1564 Dygraph
.HOURLY
= 10;
1565 Dygraph
.TWO_HOURLY
= 11;
1566 Dygraph
.SIX_HOURLY
= 12;
1568 Dygraph
.WEEKLY
= 14;
1569 Dygraph
.MONTHLY
= 15;
1570 Dygraph
.QUARTERLY
= 16;
1571 Dygraph
.BIANNUAL
= 17;
1572 Dygraph
.ANNUAL
= 18;
1573 Dygraph
.DECADAL
= 19;
1574 Dygraph
.NUM_GRANULARITIES
= 20;
1576 Dygraph
.SHORT_SPACINGS
= [];
1577 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1578 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1579 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1580 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1581 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1582 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1583 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1584 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1585 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1586 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1587 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1588 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1589 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1590 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1591 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1595 // If we used this time granularity, how many ticks would there be?
1596 // This is only an approximation, but it's generally good enough.
1598 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1599 if (granularity
< Dygraph
.MONTHLY
) {
1600 // Generate one tick mark for every fixed interval of time.
1601 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1602 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1604 var year_mod
= 1; // e.g. to only print one point every 10 years.
1605 var num_months
= 12;
1606 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1607 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1608 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1609 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1611 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1612 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1613 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1619 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1620 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1622 // Returns an array containing {v: millis, label: label} dictionaries.
1624 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1625 var formatter
= this.attr_("xAxisLabelFormatter");
1627 if (granularity
< Dygraph
.MONTHLY
) {
1628 // Generate one tick mark for every fixed interval of time.
1629 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1630 var format
= '%d%b'; // e.g. "1Jan"
1632 // Find a time less than start_time which occurs on a "nice" time boundary
1633 // for this granularity.
1634 var g
= spacing
/ 1000;
1635 var d
= new Date(start_time
);
1636 if (g
<= 60) { // seconds
1637 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1641 if (g
<= 60) { // minutes
1642 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1647 if (g
<= 24) { // days
1648 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1653 if (g
== 7) { // one week
1654 d
.setDate(d
.getDate() - d
.getDay());
1659 start_time
= d
.getTime();
1661 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1662 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1665 // Display a tick mark on the first of a set of months of each year.
1666 // Years get a tick mark iff y % year_mod == 0. This is useful for
1667 // displaying a tick mark once every 10 years, say, on long time scales.
1669 var year_mod
= 1; // e.g. to only print one point every 10 years.
1671 if (granularity
== Dygraph
.MONTHLY
) {
1672 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1673 } else if (granularity
== Dygraph
.QUARTERLY
) {
1674 months
= [ 0, 3, 6, 9 ];
1675 } else if (granularity
== Dygraph
.BIANNUAL
) {
1677 } else if (granularity
== Dygraph
.ANNUAL
) {
1679 } else if (granularity
== Dygraph
.DECADAL
) {
1684 var start_year
= new Date(start_time
).getFullYear();
1685 var end_year
= new Date(end_time
).getFullYear();
1686 var zeropad
= Dygraph
.zeropad
;
1687 for (var i
= start_year
; i
<= end_year
; i
++) {
1688 if (i
% year_mod
!= 0) continue;
1689 for (var j
= 0; j
< months
.length
; j
++) {
1690 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1691 var t
= Date
.parse(date_str
);
1692 if (t
< start_time
|| t
> end_time
) continue;
1693 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1703 * Add ticks to the x-axis based on a date range.
1704 * @param {Number} startDate Start of the date window (millis since epoch)
1705 * @param {Number} endDate End of the date window (millis since epoch)
1706 * @return {Array.<Object>} Array of {label, value} tuples.
1709 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1711 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1712 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1713 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1720 return self
.GetXAxis(startDate
, endDate
, chosen
);
1722 // TODO(danvk): signal error.
1727 * Determine the number of significant figures in a Number up to the specified
1728 * precision. Note that there is no way to determine if a trailing '0' is
1729 * significant or not, so by convention we return 1 for all of the following
1730 * inputs: 1, 1.0, 1.00, 1.000 etc.
1731 * @param {Number} x The input value.
1732 * @param {Number} opt_maxPrecision Optional maximum precision to consider.
1733 * Default and maximum allowed value is 13.
1734 * @return {Number} The number of significant figures which is >= 1.
1736 Dygraph
.significantFigures
= function(x
, opt_maxPrecision
) {
1737 var precision
= Math
.max(opt_maxPrecision
|| 13, 13);
1739 // Convert the number to its exponential notation form and work backwards,
1740 // ignoring the 'e+xx' bit. This may seem like a hack, but doing a loop and
1741 // dividing by 10 leads to roundoff errors. By using toExponential(), we let
1742 // the JavaScript interpreter handle the low level bits of the Number for us.
1743 var s
= x
.toExponential(precision
);
1744 var ePos
= s
.lastIndexOf('e'); // -1 case handled by return below.
1746 for (var i
= ePos
- 1; i
>= 0; i
--) {
1748 // Got to the decimal place. We'll call this 1 digit of precision because
1749 // we can't know for sure how many trailing 0s are significant.
1751 } else if (s
[i
] != '0') {
1752 // Found the first non-zero digit. Return the number of characters
1753 // except for the '.'.
1754 return i
; // This is i - 1 + 1 (-1 is for '.', +1 is for 0 based index).
1758 // Occurs if toExponential() doesn't return a string containing 'e', which
1759 // should never happen.
1764 * Add ticks when the x axis has numbers on it (instead of dates)
1765 * @param {Number} startDate Start of the date window (millis since epoch)
1766 * @param {Number} endDate End of the date window (millis since epoch)
1768 * @param {function} attribute accessor function.
1769 * @return {Array.<Object>} Array of {label, value} tuples.
1772 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1773 var attr
= function(k
) {
1774 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1775 return self
.attr_(k
);
1780 for (var i
= 0; i
< vals
.length
; i
++) {
1781 ticks
[i
].push({v
: vals
[i
]});
1785 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1786 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1787 // The first spacing greater than pixelsPerYLabel is what we use.
1788 // TODO(danvk): version that works on a log scale.
1789 if (attr("labelsKMG2")) {
1790 var mults
= [1, 2, 4, 8];
1792 var mults
= [1, 2, 5];
1794 var scale
, low_val
, high_val
, nTicks
;
1795 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1796 var pixelsPerTick
= attr('pixelsPerYLabel');
1797 for (var i
= -10; i
< 50; i
++) {
1798 if (attr("labelsKMG2")) {
1799 var base_scale
= Math
.pow(16, i
);
1801 var base_scale
= Math
.pow(10, i
);
1803 for (var j
= 0; j
< mults
.length
; j
++) {
1804 scale
= base_scale
* mults
[j
];
1805 low_val
= Math
.floor(minV
/ scale
) * scale
;
1806 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1807 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1808 var spacing
= self
.height_
/ nTicks
;
1809 // wish I could break out of both loops at once...
1810 if (spacing
> pixelsPerTick
) break;
1812 if (spacing
> pixelsPerTick
) break;
1815 // Construct the set of ticks.
1816 // Allow reverse y-axis if it's explicitly requested.
1817 if (low_val
> high_val
) scale
*= -1;
1818 for (var i
= 0; i
< nTicks
; i
++) {
1819 var tickV
= low_val
+ i
* scale
;
1820 ticks
.push( {v
: tickV
} );
1824 // Add formatted labels to the ticks.
1827 if (attr("labelsKMB")) {
1829 k_labels
= [ "K", "M", "B", "T" ];
1831 if (attr("labelsKMG2")) {
1832 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
1834 k_labels
= [ "k", "M", "G", "T" ];
1836 var formatter
= attr('yAxisLabelFormatter') ?
1837 attr('yAxisLabelFormatter') : attr('yValueFormatter');
1839 // Determine the number of decimal places needed for the labels below by
1840 // taking the maximum number of significant figures for any label. We must
1841 // take the max because we can't tell if trailing 0s are significant.
1843 for (var i
= 0; i
< ticks
.length
; i
++) {
1844 numDigits
= Math
.max(Dygraph
.significantFigures(ticks
[i
].v
), numDigits
);
1847 for (var i
= 0; i
< ticks
.length
; i
++) {
1848 var tickV
= ticks
[i
].v
;
1849 var absTickV
= Math
.abs(tickV
);
1850 var label
= (formatter
!== undefined
) ?
1851 formatter(tickV
, numDigits
) : tickV
.toPrecision(numDigits
);
1852 if (k_labels
.length
> 0) {
1853 // Round up to an appropriate unit.
1855 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
1856 if (absTickV
>= n
) {
1857 label
= (tickV
/ n
).toPrecision(numDigits
) + k_labels
[j
];
1862 ticks
[i
].label
= label
;
1864 return {ticks
: ticks
, numDigits
: numDigits
};
1867 // Computes the range of the data series (including confidence intervals).
1868 // series is either [ [x1, y1], [x2, y2], ... ] or
1869 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1870 // Returns [low, high]
1871 Dygraph
.prototype.extremeValues_
= function(series
) {
1872 var minY
= null, maxY
= null;
1874 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1876 // With custom bars, maxY is the max of the high values.
1877 for (var j
= 0; j
< series
.length
; j
++) {
1878 var y
= series
[j
][1][0];
1880 var low
= y
- series
[j
][1][1];
1881 var high
= y
+ series
[j
][1][2];
1882 if (low
> y
) low
= y
; // this can happen with custom bars,
1883 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1884 if (maxY
== null || high
> maxY
) {
1887 if (minY
== null || low
< minY
) {
1892 for (var j
= 0; j
< series
.length
; j
++) {
1893 var y
= series
[j
][1];
1894 if (y
=== null || isNaN(y
)) continue;
1895 if (maxY
== null || y
> maxY
) {
1898 if (minY
== null || y
< minY
) {
1904 return [minY
, maxY
];
1908 * This function is called once when the chart's data is changed or the options
1909 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1910 * idea is that values derived from the chart's data can be computed here,
1911 * rather than every time the chart is drawn. This includes things like the
1912 * number of axes, rolling averages, etc.
1914 Dygraph
.prototype.predraw_
= function() {
1915 // TODO(danvk): move more computations out of drawGraph_ and into here.
1916 this.computeYAxes_();
1918 // Create a new plotter.
1919 if (this.plotter_
) this.plotter_
.clear();
1920 this.plotter_
= new DygraphCanvasRenderer(this,
1921 this.hidden_
, this.layout_
,
1922 this.renderOptions_
);
1924 // The roller sits in the bottom left corner of the chart. We don't know where
1925 // this will be until the options are available, so it's positioned here.
1926 this.createRollInterface_();
1928 // Same thing applies for the labelsDiv. It's right edge should be flush with
1929 // the right edge of the charting area (which may not be the same as the right
1930 // edge of the div, if we have two y-axes.
1931 this.positionLabelsDiv_();
1933 // If the data or options have changed, then we'd better redraw.
1939 * Update the graph with new data. This method is called when the viewing area
1940 * has changed. If the underlying data or options have changed, predraw_ will
1941 * be called before drawGraph_ is called.
1944 Dygraph
.prototype.drawGraph_
= function() {
1945 var data
= this.rawData_
;
1947 // This is used to set the second parameter to drawCallback, below.
1948 var is_initial_draw
= this.is_initial_draw_
;
1949 this.is_initial_draw_
= false;
1951 var minY
= null, maxY
= null;
1952 this.layout_
.removeAllDatasets();
1954 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1956 // Loop over the fields (series). Go from the last to the first,
1957 // because if they're stacked that's how we accumulate the values.
1959 var cumulative_y
= []; // For stacked series.
1962 var extremes
= {}; // series name -> [low, high]
1964 // Loop over all fields and create datasets
1965 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1966 if (!this.visibility()[i
- 1]) continue;
1968 var seriesName
= this.attr_("labels")[i
];
1969 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1972 for (var j
= 0; j
< data
.length
; j
++) {
1973 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
1974 var date
= data
[j
][0];
1975 series
.push([date
, data
[j
][i
]]);
1979 // TODO(danvk): move this into predraw_. It's insane to do it here.
1980 series
= this.rollingAverage(series
, this.rollPeriod_
);
1982 // Prune down to the desired range, if necessary (for zooming)
1983 // Because there can be lines going to points outside of the visible area,
1984 // we actually prune to visible points, plus one on either side.
1985 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1986 if (this.dateWindow_
) {
1987 var low
= this.dateWindow_
[0];
1988 var high
= this.dateWindow_
[1];
1990 // TODO(danvk): do binary search instead of linear search.
1991 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1992 var firstIdx
= null, lastIdx
= null;
1993 for (var k
= 0; k
< series
.length
; k
++) {
1994 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1997 if (series
[k
][0] <= high
) {
2001 if (firstIdx
=== null) firstIdx
= 0;
2002 if (firstIdx
> 0) firstIdx
--;
2003 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2004 if (lastIdx
< series
.length
- 1) lastIdx
++;
2005 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
2006 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
2007 pruned
.push(series
[k
]);
2011 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
2014 var seriesExtremes
= this.extremeValues_(series
);
2017 for (var j
=0; j
<series
.length
; j
++) {
2018 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
2021 } else if (this.attr_("stackedGraph")) {
2022 var l
= series
.length
;
2024 for (var j
= 0; j
< l
; j
++) {
2025 // If one data set has a NaN, let all subsequent stacked
2026 // sets inherit the NaN -- only start at 0 for the first set.
2027 var x
= series
[j
][0];
2028 if (cumulative_y
[x
] === undefined
) {
2029 cumulative_y
[x
] = 0;
2032 actual_y
= series
[j
][1];
2033 cumulative_y
[x
] += actual_y
;
2035 series
[j
] = [x
, cumulative_y
[x
]]
2037 if (cumulative_y
[x
] > seriesExtremes
[1]) {
2038 seriesExtremes
[1] = cumulative_y
[x
];
2040 if (cumulative_y
[x
] < seriesExtremes
[0]) {
2041 seriesExtremes
[0] = cumulative_y
[x
];
2045 extremes
[seriesName
] = seriesExtremes
;
2047 datasets
[i
] = series
;
2050 for (var i
= 1; i
< datasets
.length
; i
++) {
2051 if (!this.visibility()[i
- 1]) continue;
2052 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
2055 this.computeYAxisRanges_(extremes
);
2056 this.layout_
.updateOptions( { yAxes
: this.axes_
,
2057 seriesToAxisMap
: this.seriesToAxisMap_
2062 // Tell PlotKit to use this new data and render itself
2063 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
2064 this.layout_
.evaluateWithError();
2065 this.plotter_
.clear();
2066 this.plotter_
.render();
2067 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
2068 this.canvas_
.height
);
2070 if (this.attr_("drawCallback") !== null) {
2071 this.attr_("drawCallback")(this, is_initial_draw
);
2076 * Determine properties of the y-axes which are independent of the data
2077 * currently being displayed. This includes things like the number of axes and
2078 * the style of the axes. It does not include the range of each axis and its
2080 * This fills in this.axes_ and this.seriesToAxisMap_.
2081 * axes_ = [ { options } ]
2082 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2083 * indices are into the axes_ array.
2085 Dygraph
.prototype.computeYAxes_
= function() {
2086 this.axes_
= [{}]; // always have at least one y-axis.
2087 this.seriesToAxisMap_
= {};
2089 // Get a list of series names.
2090 var labels
= this.attr_("labels");
2092 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2094 // all options which could be applied per-axis:
2102 'axisLabelFontSize',
2106 // Copy global axis options over to the first axis.
2107 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2108 var k
= axisOptions
[i
];
2109 var v
= this.attr_(k
);
2110 if (v
) this.axes_
[0][k
] = v
;
2113 // Go through once and add all the axes.
2114 for (var seriesName
in series
) {
2115 if (!series
.hasOwnProperty(seriesName
)) continue;
2116 var axis
= this.attr_("axis", seriesName
);
2118 this.seriesToAxisMap_
[seriesName
] = 0;
2121 if (typeof(axis
) == 'object') {
2122 // Add a new axis, making a copy of its per-axis options.
2124 Dygraph
.update(opts
, this.axes_
[0]);
2125 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2126 Dygraph
.update(opts
, axis
);
2127 this.axes_
.push(opts
);
2128 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2132 // Go through one more time and assign series to an axis defined by another
2133 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2134 for (var seriesName
in series
) {
2135 if (!series
.hasOwnProperty(seriesName
)) continue;
2136 var axis
= this.attr_("axis", seriesName
);
2137 if (typeof(axis
) == 'string') {
2138 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2139 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2140 "series " + axis
+ ", which does not define its own axis.");
2143 var idx
= this.seriesToAxisMap_
[axis
];
2144 this.seriesToAxisMap_
[seriesName
] = idx
;
2148 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2149 // this last so that hiding the first series doesn't destroy the axis
2150 // properties of the primary axis.
2151 var seriesToAxisFiltered
= {};
2152 var vis
= this.visibility();
2153 for (var i
= 1; i
< labels
.length
; i
++) {
2155 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2157 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2161 * Returns the number of y-axes on the chart.
2162 * @return {Number} the number of axes.
2164 Dygraph
.prototype.numAxes
= function() {
2166 for (var series
in this.seriesToAxisMap_
) {
2167 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2168 var idx
= this.seriesToAxisMap_
[series
];
2169 if (idx
> last_axis
) last_axis
= idx
;
2171 return 1 + last_axis
;
2175 * Determine the value range and tick marks for each axis.
2176 * @param {Object} extremes A mapping from seriesName -> [low, high]
2177 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2179 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2180 // Build a map from axis number -> [list of series names]
2181 var seriesForAxis
= [];
2182 for (var series
in this.seriesToAxisMap_
) {
2183 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2184 var idx
= this.seriesToAxisMap_
[series
];
2185 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2186 seriesForAxis
[idx
].push(series
);
2189 // Compute extreme values, a span and tick marks for each axis.
2190 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2191 var axis
= this.axes_
[i
];
2192 if (axis
.valueWindow
) {
2193 // This is only set if the user has zoomed on the y-axis. It is never set
2194 // by a user. It takes precedence over axis.valueRange because, if you set
2195 // valueRange, you'd still expect to be able to pan.
2196 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2197 } else if (axis
.valueRange
) {
2198 // This is a user-set value range for this axis.
2199 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2201 // Calculate the extremes of extremes.
2202 var series
= seriesForAxis
[i
];
2203 var minY
= Infinity
; // extremes[series[0]][0];
2204 var maxY
= -Infinity
; // extremes[series[0]][1];
2205 for (var j
= 0; j
< series
.length
; j
++) {
2206 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2207 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2209 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2211 // Add some padding and round up to an integer to be human-friendly.
2212 var span
= maxY
- minY
;
2213 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2214 if (span
== 0) { span
= maxY
; }
2215 var maxAxisY
= maxY
+ 0.1 * span
;
2216 var minAxisY
= minY
- 0.1 * span
;
2218 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2219 if (!this.attr_("avoidMinZero")) {
2220 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2221 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2224 if (this.attr_("includeZero")) {
2225 if (maxY
< 0) maxAxisY
= 0;
2226 if (minY
> 0) minAxisY
= 0;
2229 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2232 // Add ticks. By default, all axes inherit the tick positions of the
2233 // primary axis. However, if an axis is specifically marked as having
2234 // independent ticks, then that is permissible as well.
2235 if (i
== 0 || axis
.independentTicks
) {
2237 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2238 axis
.computedValueRange
[1],
2241 axis
.ticks
= ret
.ticks
;
2242 this.numYDigits_
= ret
.numDigits
;
2244 var p_axis
= this.axes_
[0];
2245 var p_ticks
= p_axis
.ticks
;
2246 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2247 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2248 var tick_values
= [];
2249 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2250 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2251 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2252 tick_values
.push(y_val
);
2256 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2257 axis
.computedValueRange
[1],
2258 this, axis
, tick_values
);
2259 axis
.ticks
= ret
.ticks
;
2260 this.numYDigits_
= ret
.numDigits
;
2266 * Calculates the rolling average of a data set.
2267 * If originalData is [label, val], rolls the average of those.
2268 * If originalData is [label, [, it's interpreted as [value, stddev]
2269 * and the roll is returned in the same form, with appropriately reduced
2270 * stddev for each value.
2271 * Note that this is where fractional input (i.e. '5/10') is converted into
2273 * @param {Array} originalData The data in the appropriate format (see above)
2274 * @param {Number} rollPeriod The number of points over which to average the
2277 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2278 if (originalData
.length
< 2)
2279 return originalData
;
2280 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2281 var rollingData
= [];
2282 var sigma
= this.attr_("sigma");
2284 if (this.fractions_
) {
2286 var den
= 0; // numerator/denominator
2288 for (var i
= 0; i
< originalData
.length
; i
++) {
2289 num
+= originalData
[i
][1][0];
2290 den
+= originalData
[i
][1][1];
2291 if (i
- rollPeriod
>= 0) {
2292 num
-= originalData
[i
- rollPeriod
][1][0];
2293 den
-= originalData
[i
- rollPeriod
][1][1];
2296 var date
= originalData
[i
][0];
2297 var value
= den
? num
/ den
: 0.0;
2298 if (this.attr_("errorBars")) {
2299 if (this.wilsonInterval_
) {
2300 // For more details on this confidence interval, see:
2301 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2303 var p
= value
< 0 ? 0 : value
, n
= den
;
2304 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2305 var denom
= 1 + sigma
* sigma
/ den
;
2306 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2307 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2308 rollingData
[i
] = [date
,
2309 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2311 rollingData
[i
] = [date
, [0, 0, 0]];
2314 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2315 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2318 rollingData
[i
] = [date
, mult
* value
];
2321 } else if (this.attr_("customBars")) {
2326 for (var i
= 0; i
< originalData
.length
; i
++) {
2327 var data
= originalData
[i
][1];
2329 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2331 if (y
!= null && !isNaN(y
)) {
2337 if (i
- rollPeriod
>= 0) {
2338 var prev
= originalData
[i
- rollPeriod
];
2339 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2346 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2347 1.0 * (mid
- low
) / count
,
2348 1.0 * (high
- mid
) / count
]];
2351 // Calculate the rolling average for the first rollPeriod - 1 points where
2352 // there is not enough data to roll over the full number of points
2353 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2354 if (!this.attr_("errorBars")){
2355 if (rollPeriod
== 1) {
2356 return originalData
;
2359 for (var i
= 0; i
< originalData
.length
; i
++) {
2362 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2363 var y
= originalData
[j
][1];
2364 if (y
== null || isNaN(y
)) continue;
2366 sum
+= originalData
[j
][1];
2369 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2371 rollingData
[i
] = [originalData
[i
][0], null];
2376 for (var i
= 0; i
< originalData
.length
; i
++) {
2380 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2381 var y
= originalData
[j
][1][0];
2382 if (y
== null || isNaN(y
)) continue;
2384 sum
+= originalData
[j
][1][0];
2385 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2388 var stddev
= Math
.sqrt(variance
) / num_ok
;
2389 rollingData
[i
] = [originalData
[i
][0],
2390 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2392 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2402 * Parses a date, returning the number of milliseconds since epoch. This can be
2403 * passed in as an xValueParser in the Dygraph constructor.
2404 * TODO(danvk): enumerate formats that this understands.
2405 * @param {String} A date in YYYYMMDD format.
2406 * @return {Number} Milliseconds since epoch.
2409 Dygraph
.dateParser
= function(dateStr
, self
) {
2412 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2413 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2414 while (dateStrSlashed
.search("-") != -1) {
2415 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2417 d
= Date
.parse(dateStrSlashed
);
2418 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2419 // TODO(danvk): remove support for this format. It's confusing.
2420 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2421 + "/" + dateStr
.substr(6,2);
2422 d
= Date
.parse(dateStrSlashed
);
2424 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2425 // "2009/07/12 12:34:56"
2426 d
= Date
.parse(dateStr
);
2429 if (!d
|| isNaN(d
)) {
2430 self
.error("Couldn't parse " + dateStr
+ " as a date");
2436 * Detects the type of the str (date or numeric) and sets the various
2437 * formatting attributes in this.attrs_ based on this type.
2438 * @param {String} str An x value.
2441 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2443 if (str
.indexOf('-') >= 0 ||
2444 str
.indexOf('/') >= 0 ||
2445 isNaN(parseFloat(str
))) {
2447 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2448 // TODO(danvk): remove support for this format.
2453 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2454 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2455 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2456 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2458 this.attrs_
.xValueFormatter
= this.attrs_
.yValueFormatter
;
2459 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2460 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2461 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2466 * Parses a string in a special csv format. We expect a csv file where each
2467 * line is a date point, and the first field in each line is the date string.
2468 * We also expect that all remaining fields represent series.
2469 * if the errorBars attribute is set, then interpret the fields as:
2470 * date, series1, stddev1, series2, stddev2, ...
2471 * @param {Array.<Object>} data See above.
2474 * @return Array.<Object> An array with one entry for each row. These entries
2475 * are an array of cells in that row. The first entry is the parsed x-value for
2476 * the row. The second, third, etc. are the y-values. These can take on one of
2477 * three forms, depending on the CSV and constructor parameters:
2479 * 2. [ value, stddev ]
2480 * 3. [ low value, center value, high value ]
2482 Dygraph
.prototype.parseCSV_
= function(data
) {
2484 var lines
= data
.split("\n");
2486 // Use the default delimiter or fall back to a tab if that makes sense.
2487 var delim
= this.attr_('delimiter');
2488 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2493 if (this.labelsFromCSV_
) {
2495 this.attrs_
.labels
= lines
[0].split(delim
);
2498 // Parse the x as a float or return null if it's not a number.
2499 var parseFloatOrNull
= function(x
) {
2500 var val
= parseFloat(x
);
2501 // isFinite() returns false for NaN and +/-Infinity
.
2502 return isFinite(val
) ? val
: null;
2506 var defaultParserSet
= false; // attempt to auto-detect x value type
2507 var expectedCols
= this.attr_("labels").length
;
2508 var outOfOrder
= false;
2509 for (var i
= start
; i
< lines
.length
; i
++) {
2510 var line
= lines
[i
];
2511 if (line
.length
== 0) continue; // skip blank lines
2512 if (line
[0] == '#') continue; // skip comment lines
2513 var inFields
= line
.split(delim
);
2514 if (inFields
.length
< 2) continue;
2517 if (!defaultParserSet
) {
2518 this.detectTypeFromString_(inFields
[0]);
2519 xParser
= this.attr_("xValueParser");
2520 defaultParserSet
= true;
2522 fields
[0] = xParser(inFields
[0], this);
2524 // If fractions are expected, parse the numbers as "A/B
"
2525 if (this.fractions_) {
2526 for (var j = 1; j < inFields.length; j++) {
2527 // TODO(danvk): figure out an appropriate way to flag parse errors.
2528 var vals = inFields[j].split("/");
2529 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2531 } else if (this.attr_("errorBars
")) {
2532 // If there are error bars, values are (value, stddev) pairs
2533 for (var j = 1; j < inFields.length; j += 2)
2534 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2535 parseFloatOrNull(inFields[j + 1])];
2536 } else if (this.attr_("customBars
")) {
2537 // Bars are a low;center;high tuple
2538 for (var j = 1; j < inFields.length; j++) {
2539 var vals = inFields[j].split(";");
2540 fields[j] = [ parseFloatOrNull(vals[0]),
2541 parseFloatOrNull(vals[1]),
2542 parseFloatOrNull(vals[2]) ];
2545 // Values are just numbers
2546 for (var j = 1; j < inFields.length; j++) {
2547 fields[j] = parseFloatOrNull(inFields[j]);
2550 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2555 if (fields.length != expectedCols) {
2556 this.error("Number of columns
in line
" + i + " (" + fields.length +
2557 ") does not agree
with number of
labels (" + expectedCols +
2563 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2564 ret.sort(function(a,b) { return a[0] - b[0] });
2571 * The user has provided their data as a pre-packaged JS array. If the x values
2572 * are numeric, this is the same as dygraphs' internal format. If the x values
2573 * are dates, we need to convert them from Date objects to ms since epoch.
2574 * @param {Array.<Object>} data
2575 * @return {Array.<Object>} data with numeric x values.
2577 Dygraph.prototype.parseArray_ = function(data) {
2578 // Peek at the first x value to see if it's numeric.
2579 if (data.length == 0) {
2580 this.error("Can
't plot empty data set");
2583 if (data[0].length == 0) {
2584 this.error("Data set cannot contain an empty row");
2588 if (this.attr_("labels") == null) {
2589 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2590 "in the options parameter");
2591 this.attrs_.labels = [ "X" ];
2592 for (var i = 1; i < data[0].length; i++) {
2593 this.attrs_.labels.push("Y" + i);
2597 if (Dygraph.isDateLike(data[0][0])) {
2598 // Some intelligent defaults for a date x-axis.
2599 this.attrs_.xValueFormatter = Dygraph.dateString_;
2600 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2601 this.attrs_.xTicker = Dygraph.dateTicker;
2603 // Assume they're all dates
.
2604 var parsedData
= Dygraph
.clone(data
);
2605 for (var i
= 0; i
< data
.length
; i
++) {
2606 if (parsedData
[i
].length
== 0) {
2607 this.error("Row " + (1 + i
) + " of data is empty");
2610 if (parsedData
[i
][0] == null
2611 || typeof(parsedData
[i
][0].getTime
) != 'function'
2612 || isNaN(parsedData
[i
][0].getTime())) {
2613 this.error("x value in row " + (1 + i
) + " is not a Date");
2616 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2620 // Some intelligent defaults for a numeric x-axis.
2621 this.attrs_
.xValueFormatter
= this.attrs_
.yValueFormatter
;
2622 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2628 * Parses a DataTable object from gviz.
2629 * The data is expected to have a first column that is either a date or a
2630 * number. All subsequent columns must be numbers. If there is a clear mismatch
2631 * between this.xValueParser_ and the type of the first column, it will be
2632 * fixed. Fills out rawData_.
2633 * @param {Array.<Object>} data See above.
2636 Dygraph
.prototype.parseDataTable_
= function(data
) {
2637 var cols
= data
.getNumberOfColumns();
2638 var rows
= data
.getNumberOfRows();
2640 var indepType
= data
.getColumnType(0);
2641 if (indepType
== 'date' || indepType
== 'datetime') {
2642 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2643 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2644 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2645 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2646 } else if (indepType
== 'number') {
2647 this.attrs_
.xValueFormatter
= this.attrs_
.yValueFormatter
;
2648 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2649 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2650 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2652 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2653 "column 1 of DataTable input (Got '" + indepType
+ "')");
2657 // Array of the column indices which contain data (and not annotations).
2659 var annotationCols
= {}; // data index -> [annotation cols]
2660 var hasAnnotations
= false;
2661 for (var i
= 1; i
< cols
; i
++) {
2662 var type
= data
.getColumnType(i
);
2663 if (type
== 'number') {
2665 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2666 // This is OK -- it's an annotation column.
2667 var dataIdx
= colIdx
[colIdx
.length
- 1];
2668 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2669 annotationCols
[dataIdx
] = [i
];
2671 annotationCols
[dataIdx
].push(i
);
2673 hasAnnotations
= true;
2675 this.error("Only 'number' is supported as a dependent type with Gviz." +
2676 " 'string' is only supported if displayAnnotations is true");
2680 // Read column labels
2681 // TODO(danvk): add support back for errorBars
2682 var labels
= [data
.getColumnLabel(0)];
2683 for (var i
= 0; i
< colIdx
.length
; i
++) {
2684 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2685 if (this.attr_("errorBars")) i
+= 1;
2687 this.attrs_
.labels
= labels
;
2688 cols
= labels
.length
;
2691 var outOfOrder
= false;
2692 var annotations
= [];
2693 for (var i
= 0; i
< rows
; i
++) {
2695 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2696 data
.getValue(i
, 0) === null) {
2697 this.warn("Ignoring row " + i
+
2698 " of DataTable because of undefined or null first column.");
2702 if (indepType
== 'date' || indepType
== 'datetime') {
2703 row
.push(data
.getValue(i
, 0).getTime());
2705 row
.push(data
.getValue(i
, 0));
2707 if (!this.attr_("errorBars")) {
2708 for (var j
= 0; j
< colIdx
.length
; j
++) {
2709 var col
= colIdx
[j
];
2710 row
.push(data
.getValue(i
, col
));
2711 if (hasAnnotations
&&
2712 annotationCols
.hasOwnProperty(col
) &&
2713 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2715 ann
.series
= data
.getColumnLabel(col
);
2717 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2719 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2720 if (k
) ann
.text
+= "\n";
2721 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2723 annotations
.push(ann
);
2727 for (var j
= 0; j
< cols
- 1; j
++) {
2728 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2731 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2735 // Strip out infinities, which give dygraphs problems later on.
2736 for (var j
= 0; j
< row
.length
; j
++) {
2737 if (!isFinite(row
[j
])) row
[j
] = null;
2743 this.warn("DataTable is out of order; order it correctly to speed loading.");
2744 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2746 this.rawData_
= ret
;
2748 if (annotations
.length
> 0) {
2749 this.setAnnotations(annotations
, true);
2753 // These functions are all based on MochiKit.
2754 Dygraph
.update
= function (self
, o
) {
2755 if (typeof(o
) != 'undefined' && o
!== null) {
2757 if (o
.hasOwnProperty(k
)) {
2765 Dygraph
.isArrayLike
= function (o
) {
2766 var typ
= typeof(o
);
2768 (typ
!= 'object' && !(typ
== 'function' &&
2769 typeof(o
.item
) == 'function')) ||
2771 typeof(o
.length
) != 'number' ||
2779 Dygraph
.isDateLike
= function (o
) {
2780 if (typeof(o
) != "object" || o
=== null ||
2781 typeof(o
.getTime
) != 'function') {
2787 Dygraph
.clone
= function(o
) {
2788 // TODO(danvk): figure out how MochiKit's version works
2790 for (var i
= 0; i
< o
.length
; i
++) {
2791 if (Dygraph
.isArrayLike(o
[i
])) {
2792 r
.push(Dygraph
.clone(o
[i
]));
2802 * Get the CSV data. If it's in a function, call that function. If it's in a
2803 * file, do an XMLHttpRequest to get it.
2806 Dygraph
.prototype.start_
= function() {
2807 if (typeof this.file_
== 'function') {
2808 // CSV string. Pretend we got it via XHR.
2809 this.loadedEvent_(this.file_());
2810 } else if (Dygraph
.isArrayLike(this.file_
)) {
2811 this.rawData_
= this.parseArray_(this.file_
);
2813 } else if (typeof this.file_
== 'object' &&
2814 typeof this.file_
.getColumnRange
== 'function') {
2815 // must be a DataTable from gviz.
2816 this.parseDataTable_(this.file_
);
2818 } else if (typeof this.file_
== 'string') {
2819 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2820 if (this.file_
.indexOf('\n') >= 0) {
2821 this.loadedEvent_(this.file_
);
2823 var req
= new XMLHttpRequest();
2825 req
.onreadystatechange
= function () {
2826 if (req
.readyState
== 4) {
2827 if (req
.status
== 200) {
2828 caller
.loadedEvent_(req
.responseText
);
2833 req
.open("GET", this.file_
, true);
2837 this.error("Unknown data format: " + (typeof this.file_
));
2842 * Changes various properties of the graph. These can include:
2844 * <li>file: changes the source data for the graph</li>
2845 * <li>errorBars: changes whether the data contains stddev</li>
2847 * @param {Object} attrs The new properties and values
2849 Dygraph
.prototype.updateOptions
= function(attrs
) {
2850 // TODO(danvk): this is a mess. Rethink this function.
2851 if ('rollPeriod' in attrs
) {
2852 this.rollPeriod_
= attrs
.rollPeriod
;
2854 if ('dateWindow' in attrs
) {
2855 this.dateWindow_
= attrs
.dateWindow
;
2858 // TODO(danvk): validate per-series options.
2863 // highlightCircleSize
2865 Dygraph
.update(this.user_attrs_
, attrs
);
2866 Dygraph
.update(this.renderOptions_
, attrs
);
2868 this.labelsFromCSV_
= (this.attr_("labels") == null);
2870 // TODO(danvk): this doesn't match the constructor logic
2871 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
2872 if (attrs
['file']) {
2873 this.file_
= attrs
['file'];
2881 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2882 * containing div (which has presumably changed size since the dygraph was
2883 * instantiated. If the width/height are specified, the div will be resized.
2885 * This is far more efficient than destroying and re-instantiating a
2886 * Dygraph, since it doesn't have to reparse the underlying data.
2888 * @param {Number} width Width (in pixels)
2889 * @param {Number} height Height (in pixels)
2891 Dygraph
.prototype.resize
= function(width
, height
) {
2892 if (this.resize_lock
) {
2895 this.resize_lock
= true;
2897 if ((width
=== null) != (height
=== null)) {
2898 this.warn("Dygraph.resize() should be called with zero parameters or " +
2899 "two non-NULL parameters. Pretending it was zero.");
2900 width
= height
= null;
2903 // TODO(danvk): there should be a clear() method.
2904 this.maindiv_
.innerHTML
= "";
2905 this.attrs_
.labelsDiv
= null;
2908 this.maindiv_
.style
.width
= width
+ "px";
2909 this.maindiv_
.style
.height
= height
+ "px";
2910 this.width_
= width
;
2911 this.height_
= height
;
2913 this.width_
= this.maindiv_
.offsetWidth
;
2914 this.height_
= this.maindiv_
.offsetHeight
;
2917 this.createInterface_();
2920 this.resize_lock
= false;
2924 * Adjusts the number of points in the rolling average. Updates the graph to
2925 * reflect the new averaging period.
2926 * @param {Number} length Number of points over which to average the data.
2928 Dygraph
.prototype.adjustRoll
= function(length
) {
2929 this.rollPeriod_
= length
;
2934 * Returns a boolean array of visibility statuses.
2936 Dygraph
.prototype.visibility
= function() {
2937 // Do lazy-initialization, so that this happens after we know the number of
2939 if (!this.attr_("visibility")) {
2940 this.attrs_
["visibility"] = [];
2942 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2943 this.attr_("visibility").push(true);
2945 return this.attr_("visibility");
2949 * Changes the visiblity of a series.
2951 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2952 var x
= this.visibility();
2953 if (num
< 0 || num
>= x
.length
) {
2954 this.warn("invalid series number in setVisibility: " + num
);
2962 * Update the list of annotations and redraw the chart.
2964 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2965 // Only add the annotation CSS rule once we know it will be used.
2966 Dygraph
.addAnnotationRule();
2967 this.annotations_
= ann
;
2968 this.layout_
.setAnnotations(this.annotations_
);
2969 if (!suppressDraw
) {
2975 * Return the list of annotations.
2977 Dygraph
.prototype.annotations
= function() {
2978 return this.annotations_
;
2982 * Get the index of a series (column) given its name. The first column is the
2983 * x-axis, so the data series start with index 1.
2985 Dygraph
.prototype.indexFromSetName
= function(name
) {
2986 var labels
= this.attr_("labels");
2987 for (var i
= 0; i
< labels
.length
; i
++) {
2988 if (labels
[i
] == name
) return i
;
2993 Dygraph
.addAnnotationRule
= function() {
2994 if (Dygraph
.addedAnnotationCSS
) return;
2996 var rule
= "border: 1px solid black; " +
2997 "background-color: white; " +
2998 "text-align: center;";
3000 var styleSheetElement
= document
.createElement("style");
3001 styleSheetElement
.type
= "text/css";
3002 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3004 // Find the first style sheet that we can access.
3005 // We may not add a rule to a style sheet from another domain for security
3006 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3007 // adds its own style sheets from google.com.
3008 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3009 if (document
.styleSheets
[i
].disabled
) continue;
3010 var mysheet
= document
.styleSheets
[i
];
3012 if (mysheet
.insertRule
) { // Firefox
3013 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3014 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3015 } else if (mysheet
.addRule
) { // IE
3016 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3018 Dygraph
.addedAnnotationCSS
= true;
3021 // Was likely a security exception.
3025 this.warn("Unable to add default annotation CSS rule; display may be off.");
3029 * Create a new canvas element. This is more complex than a simple
3030 * document.createElement("canvas") because of IE and excanvas.
3032 Dygraph
.createCanvas
= function() {
3033 var canvas
= document
.createElement("canvas");
3035 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
3036 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
3037 canvas
= G_vmlCanvasManager
.initElement(canvas
);
3045 * A wrapper around Dygraph that implements the gviz API.
3046 * @param {Object} container The DOM object the visualization should live in.
3048 Dygraph
.GVizChart
= function(container
) {
3049 this.container
= container
;
3052 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
3053 // Clear out any existing dygraph.
3054 // TODO(danvk): would it make more sense to simply redraw using the current
3055 // date_graph object?
3056 this.container
.innerHTML
= '';
3057 if (typeof(this.date_graph
) != 'undefined') {
3058 this.date_graph
.destroy();
3061 this.date_graph
= new Dygraph(this.container
, data
, options
);
3065 * Google charts compatible setSelection
3066 * Only row selection is supported, all points in the row will be highlighted
3067 * @param {Array} array of the selected cells
3070 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
3072 if (selection_array
.length
) {
3073 row
= selection_array
[0].row
;
3075 this.date_graph
.setSelection(row
);
3079 * Google charts compatible getSelection implementation
3080 * @return {Array} array of the selected cells
3083 Dygraph
.GVizChart
.prototype.getSelection
= function() {
3086 var row
= this.date_graph
.getSelection();
3088 if (row
< 0) return selection
;
3091 for (var i
in this.date_graph
.layout_
.datasets
) {
3092 selection
.push({row
: row
, column
: col
});
3099 // Older pages may still use this name.
3100 DateGraph
= Dygraph
;