1 // Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
2 // All Rights Reserved.
5 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
6 * string. Dygraph can handle multiple series with or without error bars. The
7 * date/value ranges will be automatically set. Dygraph uses the
8 * <canvas> tag, so it only works in FF1.5+.
9 * @author danvdk@gmail.com (Dan Vanderkam)
12 <div id="graphdiv" style="width:800px; height:500px;"></div>
13 <script type="text/javascript">
14 new Dygraph(document.getElementById("graphdiv"),
15 "datafile.csv", // CSV file with headers
19 The CSV file is of the form
21 Date,SeriesA,SeriesB,SeriesC
25 If the 'errorBars' option is set in the constructor, the input should be of
28 Date,SeriesA,SeriesB,...
29 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
30 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
32 If the 'fractions' option is set, the input should be of the form:
34 Date,SeriesA,SeriesB,...
35 YYYYMMDD,A1/B1,A2/B2,...
36 YYYYMMDD,A1/B1,A2/B2,...
38 And error bars will be calculated automatically using a binomial distribution.
40 For further documentation and examples, see http://dygraphs.com/
45 * An interactive, zoomable graph
46 * @param {String | Function} file A file containing CSV data or a function that
47 * returns this data. The expected format for each line is
48 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
49 * YYYYMMDD,val1,stddev1,val2,stddev2,...
50 * @param {Object} attrs Various other attributes, e.g. errorBars determines
51 * whether the input data contains error ranges.
53 Dygraph
= function(div
, data
, opts
) {
54 if (arguments
.length
> 0) {
55 if (arguments
.length
== 4) {
56 // Old versions of dygraphs took in the series labels as a constructor
57 // parameter. This doesn't make sense anymore, but it's easy to continue
58 // to support this usage.
59 this.warn("Using deprecated four-argument dygraph constructor");
60 this.__old_init__(div
, data
, arguments
[2], arguments
[3]);
62 this.__init__(div
, data
, opts
);
67 Dygraph
.NAME
= "Dygraph";
68 Dygraph
.VERSION
= "1.2";
69 Dygraph
.__repr__
= function() {
70 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
72 Dygraph
.toString
= function() {
73 return this.__repr__();
76 // Various default values
77 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
78 Dygraph
.DEFAULT_WIDTH
= 480;
79 Dygraph
.DEFAULT_HEIGHT
= 320;
80 Dygraph
.AXIS_LINE_WIDTH
= 0.3;
82 Dygraph
.LOG_SCALE
= 10;
83 Dygraph
.LOG_BASE_E_OF_TEN
= Math
.log(Dygraph
.LOG_SCALE
);
84 Dygraph
.log10
= function(x
) {
85 return Math
.log(x
) / Dygraph
.LOG_BASE_E_OF_TEN
;
88 // Default attribute values.
89 Dygraph
.DEFAULT_ATTRS
= {
90 highlightCircleSize
: 3,
96 // TODO(danvk): move defaults from createStatusMessage_ here.
98 labelsSeparateLines
: false,
99 labelsShowZeroValues
: true,
102 showLabelsOnHighlight
: true,
104 yValueFormatter
: function(x
) { return Dygraph
.round_(x
, 2); },
109 axisLabelFontSize
: 14,
112 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
116 xValueFormatter
: Dygraph
.dateString_
,
117 xValueParser
: Dygraph
.dateParser
,
118 xTicker
: Dygraph
.dateTicker
,
126 wilsonInterval
: true, // only relevant if fractions is true
130 connectSeparatedPoints
: false,
133 hideOverlayOnMouseOut
: true,
138 interactionModel
: null // will be set to Dygraph.defaultInteractionModel.
141 // Various logging levels.
147 // Directions for panning and zooming. Use bit operations when combined
148 // values are possible.
149 Dygraph
.HORIZONTAL
= 1;
150 Dygraph
.VERTICAL
= 2;
152 // Used for initializing annotation CSS rules only once.
153 Dygraph
.addedAnnotationCSS
= false;
155 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
156 // Labels is no longer a constructor parameter, since it's typically set
157 // directly from the data source. It also conains a name for the x-axis,
158 // which the previous constructor form did not.
159 if (labels
!= null) {
160 var new_labels
= ["Date"];
161 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
162 Dygraph
.update(attrs
, { 'labels': new_labels
});
164 this.__init__(div
, file
, attrs
);
168 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
169 * and context <canvas> inside of it. See the constructor for details.
171 * @param {Element} div the Element to render the graph into.
172 * @param {String | Function} file Source data
173 * @param {Object} attrs Miscellaneous other options
176 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
177 // Hack for IE: if we're using excanvas and the document hasn't finished
178 // loading yet (and hence may not have initialized whatever it needs to
179 // initialize), then keep calling this routine periodically until it has.
180 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
181 typeof(G_vmlCanvasManager
) != 'undefined' &&
182 document
.readyState
!= 'complete') {
184 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
187 // Support two-argument constructor
188 if (attrs
== null) { attrs
= {}; }
190 // Copy the important bits into the object
191 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
194 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
195 this.previousVerticalX_
= -1;
196 this.fractions_
= attrs
.fractions
|| false;
197 this.dateWindow_
= attrs
.dateWindow
|| null;
199 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
200 this.is_initial_draw_
= true;
201 this.annotations_
= [];
203 // Clear the div. This ensure that, if multiple dygraphs are passed the same
204 // div, then only one will be drawn.
207 // If the div isn't already sized then inherit from our attrs or
208 // give it a default size.
209 if (div
.style
.width
== '') {
210 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
212 if (div
.style
.height
== '') {
213 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
215 this.width_
= parseInt(div
.style
.width
, 10);
216 this.height_
= parseInt(div
.style
.height
, 10);
217 // The div might have been specified as percent of the current window size,
218 // convert that to an appropriate number of pixels.
219 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
220 this.width_
= div
.offsetWidth
;
222 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
223 this.height_
= div
.offsetHeight
;
226 if (this.width_
== 0) {
227 this.error("dygraph has zero width. Please specify a width in pixels.");
229 if (this.height_
== 0) {
230 this.error("dygraph has zero height. Please specify a height in pixels.");
233 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
234 if (attrs
['stackedGraph']) {
235 attrs
['fillGraph'] = true;
236 // TODO(nikhilk): Add any other stackedGraph checks here.
239 // Dygraphs has many options, some of which interact with one another.
240 // To keep track of everything, we maintain two sets of options:
242 // this.user_attrs_ only options explicitly set by the user.
243 // this.attrs_ defaults, options derived from user_attrs_, data.
245 // Options are then accessed this.attr_('attr'), which first looks at
246 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
247 // defaults without overriding behavior that the user specifically asks for.
248 this.user_attrs_
= {};
249 Dygraph
.update(this.user_attrs_
, attrs
);
252 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
254 this.boundaryIds_
= [];
256 // Make a note of whether labels will be pulled from the CSV file.
257 this.labelsFromCSV_
= (this.attr_("labels") == null);
259 // Create the containing DIV and other interactive elements
260 this.createInterface_();
265 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
267 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
268 this.user_attrs_
[seriesName
] != null &&
269 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
270 return this.user_attrs_
[seriesName
][name
];
271 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
272 return this.user_attrs_
[name
];
273 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
274 return this.attrs_
[name
];
280 // TODO(danvk): any way I can get the line numbers to be this.warn call?
281 Dygraph
.prototype.log
= function(severity
, message
) {
282 if (typeof(console
) != 'undefined') {
285 console
.debug('dygraphs: ' + message
);
288 console
.info('dygraphs: ' + message
);
290 case Dygraph
.WARNING
:
291 console
.warn('dygraphs: ' + message
);
294 console
.error('dygraphs: ' + message
);
299 Dygraph
.prototype.info
= function(message
) {
300 this.log(Dygraph
.INFO
, message
);
302 Dygraph
.prototype.warn
= function(message
) {
303 this.log(Dygraph
.WARNING
, message
);
305 Dygraph
.prototype.error
= function(message
) {
306 this.log(Dygraph
.ERROR
, message
);
310 * Returns the current rolling period, as set by the user or an option.
311 * @return {Number} The number of days in the rolling window
313 Dygraph
.prototype.rollPeriod
= function() {
314 return this.rollPeriod_
;
318 * Returns the currently-visible x-range. This can be affected by zooming,
319 * panning or a call to updateOptions.
320 * Returns a two-element array: [left, right].
321 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
323 Dygraph
.prototype.xAxisRange
= function() {
324 if (this.dateWindow_
) return this.dateWindow_
;
326 // The entire chart is visible.
327 var left
= this.rawData_
[0][0];
328 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
329 return [left
, right
];
333 * Returns the currently-visible y-range for an axis. This can be affected by
334 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
335 * called with no arguments, returns the range of the first axis.
336 * Returns a two-element array: [bottom, top].
338 Dygraph
.prototype.yAxisRange
= function(idx
) {
339 if (typeof(idx
) == "undefined") idx
= 0;
340 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
341 return [ this.axes_
[idx
].computedValueRange
[0],
342 this.axes_
[idx
].computedValueRange
[1] ];
346 * Returns the currently-visible y-ranges for each axis. This can be affected by
347 * zooming, panning, calls to updateOptions, etc.
348 * Returns an array of [bottom, top] pairs, one for each y-axis.
350 Dygraph
.prototype.yAxisRanges
= function() {
352 for (var i
= 0; i
< this.axes_
.length
; i
++) {
353 ret
.push(this.yAxisRange(i
));
358 // TODO(danvk): use these functions throughout dygraphs.
360 * Convert from data coordinates to canvas/div X/Y coordinates.
361 * If specified, do this conversion for the coordinate system of a particular
362 * axis. Uses the first axis by default.
363 * Returns a two-element array: [X, Y]
365 * Note: use toDomXCoord instead of toDomCoords(x. null) and use toDomYCoord
366 * instead of toDomCoords(null, y, axis).
368 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
369 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
373 * Convert from data x coordinates to canvas/div X coordinate.
374 * If specified, do this conversion for the coordinate system of a particular
375 * axis. Uses the first axis by default.
376 * returns a single value or null if x is null.
378 Dygraph
.prototype.toDomXCoord
= function(x
) {
383 var area
= this.plotter_
.area
;
384 var xRange
= this.xAxisRange();
385 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
389 * Convert from data x coordinates to canvas/div Y coordinate and optional
390 * axis. Uses the first axis by default.
392 * returns a single value or null if y is null.
394 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
395 var pct
= toPercentYCoord(y
, axis
);
400 return area
.y
+ pct
* area
.h
;
404 * Convert from canvas/div coords to data coordinates.
405 * If specified, do this conversion for the coordinate system of a particular
406 * axis. Uses the first axis by default.
407 * Returns a two-element array: [X, Y].
409 * Note: use toDataXCoord instead of toDataCoords(x. null) and use toDataYCoord
410 * instead of toDataCoords(null, y, axis).
412 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
413 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
417 * Convert from canvas/div x coordinate to data coordinate.
419 * If x is null, this returns null.
421 Dygraph
.prototype.toDataXCoord
= function(x
) {
426 var area
= this.plotter_
.area
;
427 var xRange
= this.xAxisRange();
428 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
432 * Convert from canvas/div y coord to value.
434 * If y is null, this returns null.
435 * if axis is null, this uses the first axis.
437 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
442 var area
= this.plotter_
.area
;
443 var yRange
= this.yAxisRange(axis
);
445 if (!this.attr_("logscale")) {
446 return yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
448 // Computing the inverse of toDomCoord.
449 var pct
= (y
- area
.y
) / area
.h
451 // Computing the inverse of toPercentYCoord. The function was arrived at with
452 // the following steps:
454 // Original calcuation:
455 // pct = (logr1 - Dygraph.log10(y)) / (logr1
- Dygraph
.log10(yRange
[0]));
457 // Move denominator to both sides:
458 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
460 // subtract logr1, and take the negative value.
461 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
463 // Swap both sides of the equation, and we can compute the log of the
464 // return value. Which means we just need to use that as the exponent in
466 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
468 var logr1
= Dygraph
.log10(yRange
[1]);
469 var exponent
= logr1
- (pct
* (logr1
- Dygraph
.log10(yRange
[0])));
470 var value
= Math
.pow(Dygraph
.LOG_SCALE
, exponent
);
476 * Converts a y for an axis to a percentage from the top to the
479 * If the coordinate represents a value visible on the canvas, then
480 * the value will be between 0 and 1, where 0 is the top of the canvas.
481 * However, this method will return values outside the range, as
482 * values can fall outside the canvas.
484 * If y is null, this returns null.
485 * if axis is null, this uses the first axis.
487 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
492 var area
= this.plotter_
.area
;
493 var yRange
= this.yAxisRange(axis
);
496 if (!this.attr_("logscale")) {
497 // yrange[1] - y is unit distance from the bottom.
498 // yrange[1] - yrange[0] is the scale of the range.
499 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
500 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
502 var logr1
= Dygraph
.log10(yRange
[1]);
503 pct
= (logr1
- Dygraph
.log10(y
)) / (logr1
- Dygraph
.log10(yRange
[0]));
509 * Returns the number of columns (including the independent variable).
511 Dygraph
.prototype.numColumns
= function() {
512 return this.rawData_
[0].length
;
516 * Returns the number of rows (excluding any header/label row).
518 Dygraph
.prototype.numRows
= function() {
519 return this.rawData_
.length
;
523 * Returns the value in the given row and column. If the row and column exceed
524 * the bounds on the data, returns null. Also returns null if the value is
527 Dygraph
.prototype.getValue
= function(row
, col
) {
528 if (row
< 0 || row
> this.rawData_
.length
) return null;
529 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
531 return this.rawData_
[row
][col
];
534 Dygraph
.addEvent
= function(el
, evt
, fn
) {
535 var normed_fn
= function(e
) {
536 if (!e
) var e
= window
.event
;
539 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
540 el
.addEventListener(evt
, normed_fn
, false);
542 el
.attachEvent('on' + evt
, normed_fn
);
547 // Based on the article at
548 // http://www.switchonthecode.com/tutorials
/javascript
-tutorial
-the
-scroll
-wheel
549 Dygraph
.cancelEvent
= function(e
) {
550 e
= e
? e
: window
.event
;
551 if (e
.stopPropagation
) {
554 if (e
.preventDefault
) {
557 e
.cancelBubble
= true;
559 e
.returnValue
= false;
564 * Generates interface elements for the Dygraph: a containing div, a div to
565 * display the current point, and a textbox to adjust the rolling average
566 * period. Also creates the Renderer/Layout elements.
569 Dygraph
.prototype.createInterface_
= function() {
570 // Create the all-enclosing graph div
571 var enclosing
= this.maindiv_
;
573 this.graphDiv
= document
.createElement("div");
574 this.graphDiv
.style
.width
= this.width_
+ "px";
575 this.graphDiv
.style
.height
= this.height_
+ "px";
576 enclosing
.appendChild(this.graphDiv
);
578 // Create the canvas for interactive parts of the chart.
579 this.canvas_
= Dygraph
.createCanvas();
580 this.canvas_
.style
.position
= "absolute";
581 this.canvas_
.width
= this.width_
;
582 this.canvas_
.height
= this.height_
;
583 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
584 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
586 // ... and for static parts of the chart.
587 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
589 // The interactive parts of the graph are drawn on top of the chart.
590 this.graphDiv
.appendChild(this.hidden_
);
591 this.graphDiv
.appendChild(this.canvas_
);
592 this.mouseEventElement_
= this.canvas_
;
595 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
596 dygraph
.mouseMove_(e
);
598 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
599 dygraph
.mouseOut_(e
);
602 // Create the grapher
603 // TODO(danvk): why does the Layout need its own set of options?
604 this.layoutOptions_
= { 'xOriginIsZero': false };
605 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
606 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
607 Dygraph
.update(this.layoutOptions_
, {
608 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
610 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
612 // TODO(danvk): why does the Renderer need its own set of options?
613 this.renderOptions_
= { colorScheme
: this.colors_
,
615 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
616 Dygraph
.update(this.renderOptions_
, this.attrs_
);
617 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
619 this.createStatusMessage_();
620 this.createDragInterface_();
624 * Detach DOM elements in the dygraph and null out all data references.
625 * Calling this when you're done with a dygraph can dramatically reduce memory
626 * usage. See, e.g., the tests/perf.html example.
628 Dygraph
.prototype.destroy
= function() {
629 var removeRecursive
= function(node
) {
630 while (node
.hasChildNodes()) {
631 removeRecursive(node
.firstChild
);
632 node
.removeChild(node
.firstChild
);
635 removeRecursive(this.maindiv_
);
637 var nullOut
= function(obj
) {
639 if (typeof(obj
[n
]) === 'object') {
645 // These may not all be necessary, but it can't hurt...
646 nullOut(this.layout_
);
647 nullOut(this.plotter_
);
652 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
653 * this particular canvas. All Dygraph work is done on this.canvas_.
654 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
655 * @return {Object} The newly-created canvas
658 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
659 var h
= Dygraph
.createCanvas();
660 h
.style
.position
= "absolute";
661 // TODO(danvk): h should be offset from canvas. canvas needs to include
662 // some extra area to make it easier to zoom in on the far left and far
663 // right. h needs to be precisely the plot area, so that clipping occurs.
664 h
.style
.top
= canvas
.style
.top
;
665 h
.style
.left
= canvas
.style
.left
;
666 h
.width
= this.width_
;
667 h
.height
= this.height_
;
668 h
.style
.width
= this.width_
+ "px"; // for IE
669 h
.style
.height
= this.height_
+ "px"; // for IE
673 // Taken from MochiKit.Color
674 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
678 if (saturation
=== 0) {
683 var i
= Math
.floor(hue
* 6);
684 var f
= (hue
* 6) - i
;
685 var p
= value
* (1 - saturation
);
686 var q
= value
* (1 - (saturation
* f
));
687 var t
= value
* (1 - (saturation
* (1 - f
)));
689 case 1: red
= q
; green
= value
; blue
= p
; break;
690 case 2: red
= p
; green
= value
; blue
= t
; break;
691 case 3: red
= p
; green
= q
; blue
= value
; break;
692 case 4: red
= t
; green
= p
; blue
= value
; break;
693 case 5: red
= value
; green
= p
; blue
= q
; break;
694 case 6: // fall through
695 case 0: red
= value
; green
= t
; blue
= p
; break;
698 red
= Math
.floor(255 * red
+ 0.5);
699 green
= Math
.floor(255 * green
+ 0.5);
700 blue
= Math
.floor(255 * blue
+ 0.5);
701 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
706 * Generate a set of distinct colors for the data series. This is done with a
707 * color wheel. Saturation/Value are customizable, and the hue is
708 * equally-spaced around the color wheel. If a custom set of colors is
709 * specified, that is used instead.
712 Dygraph
.prototype.setColors_
= function() {
713 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
714 // away with this.renderOptions_.
715 var num
= this.attr_("labels").length
- 1;
717 var colors
= this.attr_('colors');
719 var sat
= this.attr_('colorSaturation') || 1.0;
720 var val
= this.attr_('colorValue') || 0.5;
721 var half
= Math
.ceil(num
/ 2);
722 for (var i
= 1; i
<= num
; i
++) {
723 if (!this.visibility()[i
-1]) continue;
724 // alternate colors for high contrast.
725 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
726 var hue
= (1.0 * idx
/ (1 + num
));
727 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
730 for (var i
= 0; i
< num
; i
++) {
731 if (!this.visibility()[i
]) continue;
732 var colorStr
= colors
[i
% colors
.length
];
733 this.colors_
.push(colorStr
);
737 // TODO(danvk): update this w/r
/t/ the
new options system
.
738 this.renderOptions_
.colorScheme
= this.colors_
;
739 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
740 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
741 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
745 * Return the list of colors. This is either the list of colors passed in the
746 * attributes, or the autogenerated list of rgb(r,g,b) strings.
747 * @return {Array<string>} The list of colors.
749 Dygraph
.prototype.getColors
= function() {
753 // The following functions are from quirksmode.org with a modification for Safari from
754 // http://blog.firetree.net/2005/07/04/javascript-find-position/
755 // http://www.quirksmode.org/js
/findpos
.html
756 Dygraph
.findPosX
= function(obj
) {
761 curleft
+= obj
.offsetLeft
;
762 if(!obj
.offsetParent
)
764 obj
= obj
.offsetParent
;
771 Dygraph
.findPosY
= function(obj
) {
776 curtop
+= obj
.offsetTop
;
777 if(!obj
.offsetParent
)
779 obj
= obj
.offsetParent
;
789 * Create the div that contains information on the selected point(s)
790 * This goes in the top right of the canvas, unless an external div has already
794 Dygraph
.prototype.createStatusMessage_
= function() {
795 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
796 if (userLabelsDiv
&& null != userLabelsDiv
797 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
798 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
800 if (!this.attr_("labelsDiv")) {
801 var divWidth
= this.attr_('labelsDivWidth');
803 "position": "absolute",
806 "width": divWidth
+ "px",
808 "left": (this.width_
- divWidth
- 2) + "px",
809 "background": "white",
811 "overflow": "hidden"};
812 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
813 var div
= document
.createElement("div");
814 for (var name
in messagestyle
) {
815 if (messagestyle
.hasOwnProperty(name
)) {
816 div
.style
[name
] = messagestyle
[name
];
819 this.graphDiv
.appendChild(div
);
820 this.attrs_
.labelsDiv
= div
;
825 * Position the labels div so that its right edge is flush with the right edge
826 * of the charting area.
828 Dygraph
.prototype.positionLabelsDiv_
= function() {
829 // Don't touch a user-specified labelsDiv.
830 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
832 var area
= this.plotter_
.area
;
833 var div
= this.attr_("labelsDiv");
834 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
838 * Create the text box to adjust the averaging period
841 Dygraph
.prototype.createRollInterface_
= function() {
842 // Create a roller if one doesn't exist already.
844 this.roller_
= document
.createElement("input");
845 this.roller_
.type
= "text";
846 this.roller_
.style
.display
= "none";
847 this.graphDiv
.appendChild(this.roller_
);
850 var display
= this.attr_('showRoller') ? 'block' : 'none';
852 var textAttr
= { "position": "absolute",
854 "top": (this.plotter_
.area
.h
- 25) + "px",
855 "left": (this.plotter_
.area
.x
+ 1) + "px",
858 this.roller_
.size
= "2";
859 this.roller_
.value
= this.rollPeriod_
;
860 for (var name
in textAttr
) {
861 if (textAttr
.hasOwnProperty(name
)) {
862 this.roller_
.style
[name
] = textAttr
[name
];
867 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
870 // These functions are taken from MochiKit.Signal
871 Dygraph
.pageX
= function(e
) {
873 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
876 var b
= document
.body
;
878 (de
.scrollLeft
|| b
.scrollLeft
) -
879 (de
.clientLeft
|| 0);
883 Dygraph
.pageY
= function(e
) {
885 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
888 var b
= document
.body
;
890 (de
.scrollTop
|| b
.scrollTop
) -
895 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
896 return Dygraph
.pageX(e
) - context
.px
899 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
900 return Dygraph
.pageY(e
) - context
.py
903 // Called in response to an interaction model operation that
904 // should start the default panning behavior.
906 // It's used in the default callback for "mousedown" operations.
907 // Custom interaction model builders can use it to provide the default
910 Dygraph
.startPan
= function(event
, g
, context
) {
911 context
.isPanning
= true;
912 var xRange
= g
.xAxisRange();
913 context
.dateRange
= xRange
[1] - xRange
[0];
915 // Record the range of each y-axis at the start of the drag.
916 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
917 context
.is2DPan
= false;
918 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
919 var axis
= g
.axes_
[i
];
920 var yRange
= g
.yAxisRange(i
);
921 axis
.dragValueRange
= yRange
[1] - yRange
[0];
922 axis
.draggingValue
= g
.toDataYCoord(context
.dragStartY
, i
);
923 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
926 // TODO(konigsberg): Switch from all this math to toDataCoords?
927 // Seems to work for the dragging value.
928 context
.draggingDate
= (context
.dragStartX
/ g
.width_
) * context
.dateRange
+ xRange
[0];
931 // Called in response to an interaction model operation that
932 // responds to an event that pans the view.
934 // It's used in the default callback for "mousemove" operations.
935 // Custom interaction model builders can use it to provide the default
938 Dygraph
.movePan
= function(event
, g
, context
) {
939 context
.dragEndX
= g
.dragGetX_(event
, context
);
940 context
.dragEndY
= g
.dragGetY_(event
, context
);
942 // TODO(danvk): update this comment
943 // Want to have it so that:
944 // 1. draggingDate appears at dragEndX, draggingValue appears at dragEndY.
945 // 2. daterange = (dateWindow_[1] - dateWindow_[0]) is unaltered.
946 // 3. draggingValue appears at dragEndY.
947 // 4. valueRange is unaltered.
949 var minDate
= context
.draggingDate
- (context
.dragEndX
/ g
.width_
) * context
.dateRange
;
950 var maxDate
= minDate
+ context
.dateRange
;
951 g
.dateWindow_
= [minDate
, maxDate
];
953 // y-axis scaling is automatic unless this is a full 2D pan.
954 if (context
.is2DPan
) {
955 // Adjust each axis appropriately.
956 // NOTE(konigsberg): I don't think this computation for y_frac is correct.
957 // I think it doesn't take into account the display of the x axis.
958 // See, when I tested this with console.log(y_frac), and move the mouse
959 // cursor to the botom, the largest y_frac was 0.94, and not 1.0. That
960 // could also explain why panning tends to start with a small jumpy shift.
961 var y_frac
= context
.dragEndY
/ g
.height_
;
963 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
964 var axis
= g
.axes_
[i
];
965 var maxValue
= axis
.draggingValue
+ y_frac
* axis
.dragValueRange
;
966 var minValue
= maxValue
- axis
.dragValueRange
;
967 console
.log(axis
.draggingValue
, axis
.dragValueRange
, minValue
, maxValue
, y_frac
);
968 axis
.valueWindow
= [ minValue
, maxValue
];
975 // Called in response to an interaction model operation that
976 // responds to an event that ends panning.
978 // It's used in the default callback for "mouseup" operations.
979 // Custom interaction model builders can use it to provide the default
982 Dygraph
.endPan
= function(event
, g
, context
) {
983 context
.isPanning
= false;
984 context
.is2DPan
= false;
985 context
.draggingDate
= null;
986 context
.dateRange
= null;
987 context
.valueRange
= null;
990 // Called in response to an interaction model operation that
991 // responds to an event that starts zooming.
993 // It's used in the default callback for "mousedown" operations.
994 // Custom interaction model builders can use it to provide the default
997 Dygraph
.startZoom
= function(event
, g
, context
) {
998 context
.isZooming
= true;
1001 // Called in response to an interaction model operation that
1002 // responds to an event that defines zoom boundaries.
1004 // It's used in the default callback for "mousemove" operations.
1005 // Custom interaction model builders can use it to provide the default
1006 // zooming behavior.
1008 Dygraph
.moveZoom
= function(event
, g
, context
) {
1009 context
.dragEndX
= g
.dragGetX_(event
, context
);
1010 context
.dragEndY
= g
.dragGetY_(event
, context
);
1012 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
1013 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
1015 // drag direction threshold for y axis is twice as large as x axis
1016 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
1019 context
.dragDirection
,
1024 context
.prevDragDirection
,
1028 context
.prevEndX
= context
.dragEndX
;
1029 context
.prevEndY
= context
.dragEndY
;
1030 context
.prevDragDirection
= context
.dragDirection
;
1033 // Called in response to an interaction model operation that
1034 // responds to an event that performs a zoom based on previously defined
1037 // It's used in the default callback for "mouseup" operations.
1038 // Custom interaction model builders can use it to provide the default
1039 // zooming behavior.
1041 Dygraph
.endZoom
= function(event
, g
, context
) {
1042 context
.isZooming
= false;
1043 context
.dragEndX
= g
.dragGetX_(event
, context
);
1044 context
.dragEndY
= g
.dragGetY_(event
, context
);
1045 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
1046 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
1048 if (regionWidth
< 2 && regionHeight
< 2 &&
1049 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
1050 // TODO(danvk): pass along more info about the points, e.g. 'x'
1051 if (g
.attr_('clickCallback') != null) {
1052 g
.attr_('clickCallback')(event
, g
.lastx_
, g
.selPoints_
);
1054 if (g
.attr_('pointClickCallback')) {
1055 // check if the click was on a particular point.
1056 var closestIdx
= -1;
1057 var closestDistance
= 0;
1058 for (var i
= 0; i
< g
.selPoints_
.length
; i
++) {
1059 var p
= g
.selPoints_
[i
];
1060 var distance
= Math
.pow(p
.canvasx
- context
.dragEndX
, 2) +
1061 Math
.pow(p
.canvasy
- context
.dragEndY
, 2);
1062 if (closestIdx
== -1 || distance
< closestDistance
) {
1063 closestDistance
= distance
;
1068 // Allow any click within two pixels of the dot.
1069 var radius
= g
.attr_('highlightCircleSize') + 2;
1070 if (closestDistance
<= 5 * 5) {
1071 g
.attr_('pointClickCallback')(event
, g
.selPoints_
[closestIdx
]);
1076 if (regionWidth
>= 10 && context
.dragDirection
== Dygraph
.HORIZONTAL
) {
1077 g
.doZoomX_(Math
.min(context
.dragStartX
, context
.dragEndX
),
1078 Math
.max(context
.dragStartX
, context
.dragEndX
));
1079 } else if (regionHeight
>= 10 && context
.dragDirection
== Dygraph
.VERTICAL
) {
1080 g
.doZoomY_(Math
.min(context
.dragStartY
, context
.dragEndY
),
1081 Math
.max(context
.dragStartY
, context
.dragEndY
));
1083 g
.canvas_
.getContext("2d").clearRect(0, 0,
1087 context
.dragStartX
= null;
1088 context
.dragStartY
= null;
1091 Dygraph
.defaultInteractionModel
= {
1092 // Track the beginning of drag events
1093 mousedown
: function(event
, g
, context
) {
1094 context
.initializeMouseDown(event
, g
, context
);
1096 if (event
.altKey
|| event
.shiftKey
) {
1097 Dygraph
.startPan(event
, g
, context
);
1099 Dygraph
.startZoom(event
, g
, context
);
1103 // Draw zoom rectangles when the mouse is down and the user moves around
1104 mousemove
: function(event
, g
, context
) {
1105 if (context
.isZooming
) {
1106 Dygraph
.moveZoom(event
, g
, context
);
1107 } else if (context
.isPanning
) {
1108 Dygraph
.movePan(event
, g
, context
);
1112 mouseup
: function(event
, g
, context
) {
1113 if (context
.isZooming
) {
1114 Dygraph
.endZoom(event
, g
, context
);
1115 } else if (context
.isPanning
) {
1116 Dygraph
.endPan(event
, g
, context
);
1120 // Temporarily cancel the dragging event when the mouse leaves the graph
1121 mouseout
: function(event
, g
, context
) {
1122 if (context
.isZooming
) {
1123 context
.dragEndX
= null;
1124 context
.dragEndY
= null;
1128 // Disable zooming out if panning.
1129 dblclick
: function(event
, g
, context
) {
1130 if (event
.altKey
|| event
.shiftKey
) {
1133 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1134 // friendlier to public use.
1139 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.defaultInteractionModel
;
1142 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1146 Dygraph
.prototype.createDragInterface_
= function() {
1148 // Tracks whether the mouse is down right now
1150 isPanning
: false, // is this drag part of a pan?
1151 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1156 dragDirection
: null,
1159 prevDragDirection
: null,
1161 // TODO(danvk): update this comment
1162 // draggingDate and draggingValue represent the [date,value] point on the
1163 // graph at which the mouse was pressed. As the mouse moves while panning,
1164 // the viewport must pan so that the mouse position points to
1165 // [draggingDate, draggingValue]
1168 // TODO(danvk): update this comment
1169 // The range in second/value units that the viewport encompasses during a
1170 // panning operation.
1173 // Utility function to convert page-wide coordinates to canvas coords
1177 initializeMouseDown
: function(event
, g
, context
) {
1178 // prevents mouse drags from selecting page text.
1179 if (event
.preventDefault
) {
1180 event
.preventDefault(); // Firefox, Chrome, etc.
1182 event
.returnValue
= false; // IE
1183 event
.cancelBubble
= true;
1186 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1187 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1188 context
.dragStartX
= g
.dragGetX_(event
, context
);
1189 context
.dragStartY
= g
.dragGetY_(event
, context
);
1193 var interactionModel
= this.attr_("interactionModel");
1195 // Self is the graph.
1198 // Function that binds the graph and context to the handler.
1199 var bindHandler
= function(handler
) {
1200 return function(event
) {
1201 handler(event
, self
, context
);
1205 for (var eventName
in interactionModel
) {
1206 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1207 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1208 bindHandler(interactionModel
[eventName
]));
1211 // If the user releases the mouse button during a drag, but not over the
1212 // canvas, then it doesn't count as a zooming action.
1213 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1214 if (context
.isZooming
|| context
.isPanning
) {
1215 context
.isZooming
= false;
1216 context
.dragStartX
= null;
1217 context
.dragStartY
= null;
1220 if (context
.isPanning
) {
1221 context
.isPanning
= false;
1222 context
.draggingDate
= null;
1223 context
.dateRange
= null;
1224 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1225 delete self
.axes_
[i
].draggingValue
;
1226 delete self
.axes_
[i
].dragValueRange
;
1233 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1234 * up any previous zoom rectangles that were drawn. This could be optimized to
1235 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1238 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1239 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1240 * @param {Number} startX The X position where the drag started, in canvas
1242 * @param {Number} endX The current X position of the drag, in canvas coords.
1243 * @param {Number} startY The Y position where the drag started, in canvas
1245 * @param {Number} endY The current Y position of the drag, in canvas coords.
1246 * @param {Number} prevDirection the value of direction on the previous call to
1247 * this function. Used to avoid excess redrawing
1248 * @param {Number} prevEndX The value of endX on the previous call to this
1249 * function. Used to avoid excess redrawing
1250 * @param {Number} prevEndY The value of endY on the previous call to this
1251 * function. Used to avoid excess redrawing
1254 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1255 prevDirection
, prevEndX
, prevEndY
) {
1256 var ctx
= this.canvas_
.getContext("2d");
1258 // Clean up from the previous rect if necessary
1259 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1260 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1261 Math
.abs(startX
- prevEndX
), this.height_
);
1262 } else if (prevDirection
== Dygraph
.VERTICAL
){
1263 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1264 this.width_
, Math
.abs(startY
- prevEndY
));
1267 // Draw a light-grey rectangle to show the new viewing area
1268 if (direction
== Dygraph
.HORIZONTAL
) {
1269 if (endX
&& startX
) {
1270 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1271 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1272 Math
.abs(endX
- startX
), this.height_
);
1275 if (direction
== Dygraph
.VERTICAL
) {
1276 if (endY
&& startY
) {
1277 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1278 ctx
.fillRect(0, Math
.min(startY
, endY
),
1279 this.width_
, Math
.abs(endY
- startY
));
1285 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1286 * the canvas. The exact zoom window may be slightly larger if there are no data
1287 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1288 * which accepts dates that match the raw data. This function redraws the graph.
1290 * @param {Number} lowX The leftmost pixel value that should be visible.
1291 * @param {Number} highX The rightmost pixel value that should be visible.
1294 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1295 // Find the earliest and latest dates contained in this canvasx range.
1296 // Convert the call to date ranges of the raw data.
1297 var minDate
= this.toDataXCoord(lowX
);
1298 var maxDate
= this.toDataXCoord(highX
);
1299 this.doZoomXDates_(minDate
, maxDate
);
1303 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1304 * method with doZoomX which accepts pixel coordinates. This function redraws
1307 * @param {Number} minDate The minimum date that should be visible.
1308 * @param {Number} maxDate The maximum date that should be visible.
1311 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1312 this.dateWindow_
= [minDate
, maxDate
];
1314 if (this.attr_("zoomCallback")) {
1315 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1320 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1321 * the canvas. This function redraws the graph.
1323 * @param {Number} lowY The topmost pixel value that should be visible.
1324 * @param {Number} highY The lowest pixel value that should be visible.
1327 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1328 // Find the highest and lowest values in pixel range for each axis.
1329 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1330 // This is because pixels increase as you go down on the screen, whereas data
1331 // coordinates increase as you go up the screen.
1332 var valueRanges
= [];
1333 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1334 var hi
= this.toDataYCoord(lowY
, i
);
1335 var low
= this.toDataYCoord(highY
, i
);
1336 this.axes_
[i
].valueWindow
= [low
, hi
];
1337 valueRanges
.push([low
, hi
]);
1341 if (this.attr_("zoomCallback")) {
1342 var xRange
= this.xAxisRange();
1343 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1348 * Reset the zoom to the original view coordinates. This is the same as
1349 * double-clicking on the graph.
1353 Dygraph
.prototype.doUnzoom_
= function() {
1355 if (this.dateWindow_
!= null) {
1357 this.dateWindow_
= null;
1360 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1361 if (this.axes_
[i
].valueWindow
!= null) {
1363 delete this.axes_
[i
].valueWindow
;
1368 // Putting the drawing operation before the callback because it resets
1371 if (this.attr_("zoomCallback")) {
1372 var minDate
= this.rawData_
[0][0];
1373 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1374 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1380 * When the mouse moves in the canvas, display information about a nearby data
1381 * point and draw dots over those points in the data series. This function
1382 * takes care of cleanup of previously-drawn dots.
1383 * @param {Object} event The mousemove event from the browser.
1386 Dygraph
.prototype.mouseMove_
= function(event
) {
1387 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1388 var points
= this.layout_
.points
;
1393 // Loop through all the points and find the date nearest to our current
1395 var minDist
= 1e+100;
1397 for (var i
= 0; i
< points
.length
; i
++) {
1398 var point
= points
[i
];
1399 if (point
== null) continue;
1400 var dist
= Math
.abs(point
.canvasx
- canvasx
);
1401 if (dist
> minDist
) continue;
1405 if (idx
>= 0) lastx
= points
[idx
].xval
;
1406 // Check that you can really highlight the last day's data
1407 var last
= points
[points
.length
-1];
1408 if (last
!= null && canvasx
> last
.canvasx
)
1409 lastx
= points
[points
.length
-1].xval
;
1411 // Extract the points we've selected
1412 this.selPoints_
= [];
1413 var l
= points
.length
;
1414 if (!this.attr_("stackedGraph")) {
1415 for (var i
= 0; i
< l
; i
++) {
1416 if (points
[i
].xval
== lastx
) {
1417 this.selPoints_
.push(points
[i
]);
1421 // Need to 'unstack' points starting from the bottom
1422 var cumulative_sum
= 0;
1423 for (var i
= l
- 1; i
>= 0; i
--) {
1424 if (points
[i
].xval
== lastx
) {
1425 var p
= {}; // Clone the point since we modify it
1426 for (var k
in points
[i
]) {
1427 p
[k
] = points
[i
][k
];
1429 p
.yval
-= cumulative_sum
;
1430 cumulative_sum
+= p
.yval
;
1431 this.selPoints_
.push(p
);
1434 this.selPoints_
.reverse();
1437 if (this.attr_("highlightCallback")) {
1438 var px
= this.lastx_
;
1439 if (px
!== null && lastx
!= px
) {
1440 // only fire if the selected point has changed.
1441 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1445 // Save last x position for callbacks.
1446 this.lastx_
= lastx
;
1448 this.updateSelection_();
1452 * Transforms layout_.points index into data row number.
1453 * @param int layout_.points index
1454 * @return int row number, or -1 if none could be found.
1457 Dygraph
.prototype.idxToRow_
= function(idx
) {
1458 if (idx
< 0) return -1;
1460 for (var i
in this.layout_
.datasets
) {
1461 if (idx
< this.layout_
.datasets
[i
].length
) {
1462 return this.boundaryIds_
[0][0]+idx
;
1464 idx
-= this.layout_
.datasets
[i
].length
;
1470 * Draw dots over the selectied points in the data series. This function
1471 * takes care of cleanup of previously-drawn dots.
1474 Dygraph
.prototype.updateSelection_
= function() {
1475 // Clear the previously drawn vertical, if there is one
1476 var ctx
= this.canvas_
.getContext("2d");
1477 if (this.previousVerticalX_
>= 0) {
1478 // Determine the maximum highlight circle size.
1479 var maxCircleSize
= 0;
1480 var labels
= this.attr_('labels');
1481 for (var i
= 1; i
< labels
.length
; i
++) {
1482 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1483 if (r
> maxCircleSize
) maxCircleSize
= r
;
1485 var px
= this.previousVerticalX_
;
1486 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1487 2 * maxCircleSize
+ 2, this.height_
);
1490 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1492 if (this.selPoints_
.length
> 0) {
1493 var canvasx
= this.selPoints_
[0].canvasx
;
1495 // Set the status message to indicate the selected point(s)
1496 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1497 var fmtFunc
= this.attr_('yValueFormatter');
1498 var clen
= this.colors_
.length
;
1500 if (this.attr_('showLabelsOnHighlight')) {
1501 // Set the status message to indicate the selected point(s)
1502 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1503 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1504 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1505 if (this.attr_("labelsSeparateLines")) {
1508 var point
= this.selPoints_
[i
];
1509 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1510 var yval
= fmtFunc(point
.yval
);
1511 replace
+= " <b><font color='" + c
.toHex() + "'>"
1512 + point
.name
+ "</font></b>:"
1516 this.attr_("labelsDiv").innerHTML
= replace
;
1519 // Draw colored circles over the center of each selected point
1521 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1522 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1524 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1526 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1527 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1528 0, 2 * Math
.PI
, false);
1533 this.previousVerticalX_
= canvasx
;
1538 * Set manually set selected dots, and display information about them
1539 * @param int row number that should by highlighted
1540 * false value clears the selection
1543 Dygraph
.prototype.setSelection
= function(row
) {
1544 // Extract the points we've selected
1545 this.selPoints_
= [];
1548 if (row
!== false) {
1549 row
= row
-this.boundaryIds_
[0][0];
1552 if (row
!== false && row
>= 0) {
1553 for (var i
in this.layout_
.datasets
) {
1554 if (row
< this.layout_
.datasets
[i
].length
) {
1555 var point
= this.layout_
.points
[pos
+row
];
1557 if (this.attr_("stackedGraph")) {
1558 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1561 this.selPoints_
.push(point
);
1563 pos
+= this.layout_
.datasets
[i
].length
;
1567 if (this.selPoints_
.length
) {
1568 this.lastx_
= this.selPoints_
[0].xval
;
1569 this.updateSelection_();
1572 this.clearSelection();
1578 * The mouse has left the canvas. Clear out whatever artifacts remain
1579 * @param {Object} event the mouseout event from the browser.
1582 Dygraph
.prototype.mouseOut_
= function(event
) {
1583 if (this.attr_("unhighlightCallback")) {
1584 this.attr_("unhighlightCallback")(event
);
1587 if (this.attr_("hideOverlayOnMouseOut")) {
1588 this.clearSelection();
1593 * Remove all selection from the canvas
1596 Dygraph
.prototype.clearSelection
= function() {
1597 // Get rid of the overlay data
1598 var ctx
= this.canvas_
.getContext("2d");
1599 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1600 this.attr_("labelsDiv").innerHTML
= "";
1601 this.selPoints_
= [];
1606 * Returns the number of the currently selected row
1607 * @return int row number, of -1 if nothing is selected
1610 Dygraph
.prototype.getSelection
= function() {
1611 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1615 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1616 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1617 return row
+ this.boundaryIds_
[0][0];
1623 Dygraph
.zeropad
= function(x
) {
1624 if (x
< 10) return "0" + x
; else return "" + x
;
1628 * Return a string version of the hours, minutes and seconds portion of a date.
1629 * @param {Number} date The JavaScript date (ms since epoch)
1630 * @return {String} A time of the form "HH:MM:SS"
1633 Dygraph
.hmsString_
= function(date
) {
1634 var zeropad
= Dygraph
.zeropad
;
1635 var d
= new Date(date
);
1636 if (d
.getSeconds()) {
1637 return zeropad(d
.getHours()) + ":" +
1638 zeropad(d
.getMinutes()) + ":" +
1639 zeropad(d
.getSeconds());
1641 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1646 * Convert a JS date to a string appropriate to display on an axis that
1647 * is displaying values at the stated granularity.
1648 * @param {Date} date The date to format
1649 * @param {Number} granularity One of the Dygraph granularity constants
1650 * @return {String} The formatted date
1653 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1654 if (granularity
>= Dygraph
.DECADAL
) {
1655 return date
.strftime('%Y');
1656 } else if (granularity
>= Dygraph
.MONTHLY
) {
1657 return date
.strftime('%b %y');
1659 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1660 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1661 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1663 return Dygraph
.hmsString_(date
.getTime());
1669 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1670 * @param {Number} date The JavaScript date (ms since epoch)
1671 * @return {String} A date of the form "YYYY/MM/DD"
1674 Dygraph
.dateString_
= function(date
, self
) {
1675 var zeropad
= Dygraph
.zeropad
;
1676 var d
= new Date(date
);
1679 var year
= "" + d
.getFullYear();
1680 // Get a 0 padded month string
1681 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1682 // Get a 0 padded day string
1683 var day
= zeropad(d
.getDate());
1686 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1687 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1689 return year
+ "/" + month + "/" + day
+ ret
;
1693 * Round a number to the specified number of digits past the decimal point.
1694 * @param {Number} num The number to round
1695 * @param {Number} places The number of decimals to which to round
1696 * @return {Number} The rounded number
1699 Dygraph
.round_
= function(num
, places
) {
1700 var shift
= Math
.pow(10, places
);
1701 return Math
.round(num
* shift
)/shift
;
1705 * Fires when there's data available to be graphed.
1706 * @param {String} data Raw CSV data to be plotted
1709 Dygraph
.prototype.loadedEvent_
= function(data
) {
1710 this.rawData_
= this.parseCSV_(data
);
1714 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1715 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1716 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1719 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1722 Dygraph
.prototype.addXTicks_
= function() {
1723 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1724 var startDate
, endDate
;
1725 if (this.dateWindow_
) {
1726 startDate
= this.dateWindow_
[0];
1727 endDate
= this.dateWindow_
[1];
1729 startDate
= this.rawData_
[0][0];
1730 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1733 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1734 this.layout_
.updateOptions({xTicks
: xTicks
});
1737 // Time granularity enumeration
1738 Dygraph
.SECONDLY
= 0;
1739 Dygraph
.TWO_SECONDLY
= 1;
1740 Dygraph
.FIVE_SECONDLY
= 2;
1741 Dygraph
.TEN_SECONDLY
= 3;
1742 Dygraph
.THIRTY_SECONDLY
= 4;
1743 Dygraph
.MINUTELY
= 5;
1744 Dygraph
.TWO_MINUTELY
= 6;
1745 Dygraph
.FIVE_MINUTELY
= 7;
1746 Dygraph
.TEN_MINUTELY
= 8;
1747 Dygraph
.THIRTY_MINUTELY
= 9;
1748 Dygraph
.HOURLY
= 10;
1749 Dygraph
.TWO_HOURLY
= 11;
1750 Dygraph
.SIX_HOURLY
= 12;
1752 Dygraph
.WEEKLY
= 14;
1753 Dygraph
.MONTHLY
= 15;
1754 Dygraph
.QUARTERLY
= 16;
1755 Dygraph
.BIANNUAL
= 17;
1756 Dygraph
.ANNUAL
= 18;
1757 Dygraph
.DECADAL
= 19;
1758 Dygraph
.CENTENNIAL
= 20;
1759 Dygraph
.NUM_GRANULARITIES
= 21;
1761 Dygraph
.SHORT_SPACINGS
= [];
1762 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1763 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1764 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1765 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1766 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1767 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1768 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1769 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1770 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1771 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1772 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1773 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1774 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1775 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1776 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1780 // If we used this time granularity, how many ticks would there be?
1781 // This is only an approximation, but it's generally good enough.
1783 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1784 if (granularity
< Dygraph
.MONTHLY
) {
1785 // Generate one tick mark for every fixed interval of time.
1786 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1787 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1789 var year_mod
= 1; // e.g. to only print one point every 10 years.
1790 var num_months
= 12;
1791 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1792 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1793 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1794 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1795 if (granularity
== Dygraph
.CENTENNIAL
) { num_months
= 1; year_mod
= 100; }
1797 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1798 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1799 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1805 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1806 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1808 // Returns an array containing {v: millis, label: label} dictionaries.
1810 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1811 var formatter
= this.attr_("xAxisLabelFormatter");
1813 if (granularity
< Dygraph
.MONTHLY
) {
1814 // Generate one tick mark for every fixed interval of time.
1815 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1816 var format
= '%d%b'; // e.g. "1Jan"
1818 // Find a time less than start_time which occurs on a "nice" time boundary
1819 // for this granularity.
1820 var g
= spacing
/ 1000;
1821 var d
= new Date(start_time
);
1822 if (g
<= 60) { // seconds
1823 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1827 if (g
<= 60) { // minutes
1828 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1833 if (g
<= 24) { // days
1834 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1839 if (g
== 7) { // one week
1840 d
.setDate(d
.getDate() - d
.getDay());
1845 start_time
= d
.getTime();
1847 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1848 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1851 // Display a tick mark on the first of a set of months of each year.
1852 // Years get a tick mark iff y % year_mod == 0. This is useful for
1853 // displaying a tick mark once every 10 years, say, on long time scales.
1855 var year_mod
= 1; // e.g. to only print one point every 10 years.
1857 if (granularity
== Dygraph
.MONTHLY
) {
1858 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1859 } else if (granularity
== Dygraph
.QUARTERLY
) {
1860 months
= [ 0, 3, 6, 9 ];
1861 } else if (granularity
== Dygraph
.BIANNUAL
) {
1863 } else if (granularity
== Dygraph
.ANNUAL
) {
1865 } else if (granularity
== Dygraph
.DECADAL
) {
1868 } else if (granularity
== Dygraph
.CENTENNIAL
) {
1872 this.warn("Span of dates is too long");
1875 var start_year
= new Date(start_time
).getFullYear();
1876 var end_year
= new Date(end_time
).getFullYear();
1877 var zeropad
= Dygraph
.zeropad
;
1878 for (var i
= start_year
; i
<= end_year
; i
++) {
1879 if (i
% year_mod
!= 0) continue;
1880 for (var j
= 0; j
< months
.length
; j
++) {
1881 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1882 var t
= Date
.parse(date_str
);
1883 if (t
< start_time
|| t
> end_time
) continue;
1884 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1894 * Add ticks to the x-axis based on a date range.
1895 * @param {Number} startDate Start of the date window (millis since epoch)
1896 * @param {Number} endDate End of the date window (millis since epoch)
1897 * @return {Array.<Object>} Array of {label, value} tuples.
1900 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1902 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1903 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1904 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1911 return self
.GetXAxis(startDate
, endDate
, chosen
);
1913 // TODO(danvk): signal error.
1918 * Add ticks when the x axis has numbers on it (instead of dates)
1919 * TODO(konigsberg): Update comment.
1921 * @param {Number} startDate Start of the date window (millis since epoch)
1922 * @param {Number} endDate End of the date window (millis since epoch)
1924 * @param {function} attribute accessor function.
1925 * @return {Array.<Object>} Array of {label, value} tuples.
1928 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
1929 var attr
= function(k
) {
1930 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
1931 return self
.attr_(k
);
1936 for (var i
= 0; i
< vals
.length
; i
++) {
1937 ticks
.push({v
: vals
[i
]});
1940 if (self
.attr_("logscale")) {
1941 // As opposed to the other ways for computing ticks, we're just going
1942 // for nearby values. There's no reasonable way to scale the values
1943 // (unless we want to show strings like "log(" + x + ")") in which case
1944 // x can be integer values.
1946 // so compute height / pixelsPerTick and move on
.
1947 var pixelsPerTick
= attr('pixelsPerYLabel');
1948 var nTicks
= Math
.floor(self
.height_
/ pixelsPerTick
);
1951 // Construct the set of ticks.
1952 for (var i
= 0; i
< nTicks
; i
++) {
1953 ticks
.push( {v
: vv
} );
1954 vv
= vv
* Dygraph
.LOG_SCALE
;
1958 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
1959 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
1960 // The first spacing greater than pixelsPerYLabel is what we use.
1961 // TODO(danvk): version that works on a log scale.
1962 if (attr("labelsKMG2")) {
1963 var mults
= [1, 2, 4, 8];
1965 var mults
= [1, 2, 5];
1967 var scale
, low_val
, high_val
, nTicks
;
1968 // TODO(danvk): make it possible to set this for x- and y-axes independently.
1969 var pixelsPerTick
= attr('pixelsPerYLabel');
1970 for (var i
= -10; i
< 50; i
++) {
1971 if (attr("labelsKMG2")) {
1972 var base_scale
= Math
.pow(16, i
);
1974 var base_scale
= Math
.pow(10, i
);
1976 for (var j
= 0; j
< mults
.length
; j
++) {
1977 scale
= base_scale
* mults
[j
];
1978 low_val
= Math
.floor(minV
/ scale
) * scale
;
1979 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
1980 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
1981 var spacing
= self
.height_
/ nTicks
;
1982 // wish I could break out of both loops at once...
1983 if (spacing
> pixelsPerTick
) break;
1985 if (spacing
> pixelsPerTick
) break;
1988 // Construct the set of ticks.
1989 // Allow reverse y-axis if it's explicitly requested.
1990 if (low_val
> high_val
) scale
*= -1;
1991 for (var i
= 0; i
< nTicks
; i
++) {
1992 var tickV
= low_val
+ i
* scale
;
1993 ticks
.push( {v
: tickV
} );
1998 // Add formatted labels to the ticks.
2001 if (attr("labelsKMB")) {
2003 k_labels
= [ "K", "M", "B", "T" ];
2005 if (attr("labelsKMG2")) {
2006 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2008 k_labels
= [ "k", "M", "G", "T" ];
2010 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
2012 for (var i
= 0; i
< ticks
.length
; i
++) {
2013 var tickV
= ticks
[i
].v
;
2014 var absTickV
= Math
.abs(tickV
);
2016 if (formatter
!= undefined
) {
2017 label
= formatter(tickV
);
2019 label
= Dygraph
.round_(tickV
, 2);
2021 if (k_labels
.length
) {
2022 // Round up to an appropriate unit.
2024 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
2025 if (absTickV
>= n
) {
2026 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
2031 ticks
[i
].label
= label
;
2036 // Computes the range of the data series (including confidence intervals).
2037 // series is either [ [x1, y1], [x2, y2], ... ] or
2038 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2039 // Returns [low, high]
2040 Dygraph
.prototype.extremeValues_
= function(series
) {
2041 var minY
= null, maxY
= null;
2043 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2045 // With custom bars, maxY is the max of the high values.
2046 for (var j
= 0; j
< series
.length
; j
++) {
2047 var y
= series
[j
][1][0];
2049 var low
= y
- series
[j
][1][1];
2050 var high
= y
+ series
[j
][1][2];
2051 if (low
> y
) low
= y
; // this can happen with custom bars,
2052 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
2053 if (maxY
== null || high
> maxY
) {
2056 if (minY
== null || low
< minY
) {
2061 for (var j
= 0; j
< series
.length
; j
++) {
2062 var y
= series
[j
][1];
2063 if (y
=== null || isNaN(y
)) continue;
2064 if (maxY
== null || y
> maxY
) {
2067 if (minY
== null || y
< minY
) {
2073 return [minY
, maxY
];
2077 * This function is called once when the chart's data is changed or the options
2078 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2079 * idea is that values derived from the chart's data can be computed here,
2080 * rather than every time the chart is drawn. This includes things like the
2081 * number of axes, rolling averages, etc.
2083 Dygraph
.prototype.predraw_
= function() {
2084 // TODO(danvk): move more computations out of drawGraph_ and into here.
2085 this.computeYAxes_();
2087 // Create a new plotter.
2088 if (this.plotter_
) this.plotter_
.clear();
2089 this.plotter_
= new DygraphCanvasRenderer(this,
2090 this.hidden_
, this.layout_
,
2091 this.renderOptions_
);
2093 // The roller sits in the bottom left corner of the chart. We don't know where
2094 // this will be until the options are available, so it's positioned here.
2095 this.createRollInterface_();
2097 // Same thing applies for the labelsDiv. It's right edge should be flush with
2098 // the right edge of the charting area (which may not be the same as the right
2099 // edge of the div, if we have two y-axes.
2100 this.positionLabelsDiv_();
2102 // If the data or options have changed, then we'd better redraw.
2107 * Update the graph with new data. This method is called when the viewing area
2108 * has changed. If the underlying data or options have changed, predraw_ will
2109 * be called before drawGraph_ is called.
2112 Dygraph
.prototype.drawGraph_
= function() {
2113 var data
= this.rawData_
;
2115 // This is used to set the second parameter to drawCallback, below.
2116 var is_initial_draw
= this.is_initial_draw_
;
2117 this.is_initial_draw_
= false;
2119 var minY
= null, maxY
= null;
2120 this.layout_
.removeAllDatasets();
2122 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
2124 // Loop over the fields (series). Go from the last to the first,
2125 // because if they're stacked that's how we accumulate the values.
2127 var cumulative_y
= []; // For stacked series.
2130 var extremes
= {}; // series name -> [low, high]
2132 // Loop over all fields and create datasets
2133 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
2134 if (!this.visibility()[i
- 1]) continue;
2136 var seriesName
= this.attr_("labels")[i
];
2137 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
2140 for (var j
= 0; j
< data
.length
; j
++) {
2141 if (data
[j
][i
] != null || !connectSeparatedPoints
) {
2142 var date
= data
[j
][0];
2143 series
.push([date
, data
[j
][i
]]);
2147 // TODO(danvk): move this into predraw_. It's insane to do it here.
2148 series
= this.rollingAverage(series
, this.rollPeriod_
);
2150 // Prune down to the desired range, if necessary (for zooming)
2151 // Because there can be lines going to points outside of the visible area,
2152 // we actually prune to visible points, plus one on either side.
2153 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2154 if (this.dateWindow_
) {
2155 var low
= this.dateWindow_
[0];
2156 var high
= this.dateWindow_
[1];
2158 // TODO(danvk): do binary search instead of linear search.
2159 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2160 var firstIdx
= null, lastIdx
= null;
2161 for (var k
= 0; k
< series
.length
; k
++) {
2162 if (series
[k
][0] >= low
&& firstIdx
=== null) {
2165 if (series
[k
][0] <= high
) {
2169 if (firstIdx
=== null) firstIdx
= 0;
2170 if (firstIdx
> 0) firstIdx
--;
2171 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2172 if (lastIdx
< series
.length
- 1) lastIdx
++;
2173 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
2174 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
2175 pruned
.push(series
[k
]);
2179 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
2182 var seriesExtremes
= this.extremeValues_(series
);
2185 for (var j
=0; j
<series
.length
; j
++) {
2186 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
2189 } else if (this.attr_("stackedGraph")) {
2190 var l
= series
.length
;
2192 for (var j
= 0; j
< l
; j
++) {
2193 // If one data set has a NaN, let all subsequent stacked
2194 // sets inherit the NaN -- only start at 0 for the first set.
2195 var x
= series
[j
][0];
2196 if (cumulative_y
[x
] === undefined
) {
2197 cumulative_y
[x
] = 0;
2200 actual_y
= series
[j
][1];
2201 cumulative_y
[x
] += actual_y
;
2203 series
[j
] = [x
, cumulative_y
[x
]]
2205 if (cumulative_y
[x
] > seriesExtremes
[1]) {
2206 seriesExtremes
[1] = cumulative_y
[x
];
2208 if (cumulative_y
[x
] < seriesExtremes
[0]) {
2209 seriesExtremes
[0] = cumulative_y
[x
];
2213 extremes
[seriesName
] = seriesExtremes
;
2215 datasets
[i
] = series
;
2218 for (var i
= 1; i
< datasets
.length
; i
++) {
2219 if (!this.visibility()[i
- 1]) continue;
2220 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
2223 // TODO(danvk): this method doesn't need to return anything.
2224 var out
= this.computeYAxisRanges_(extremes
);
2226 var seriesToAxisMap
= out
[1];
2227 this.layout_
.updateOptions( { yAxes
: axes
,
2228 seriesToAxisMap
: seriesToAxisMap
2233 // Tell PlotKit to use this new data and render itself
2234 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
2235 this.layout_
.evaluateWithError();
2236 this.plotter_
.clear();
2237 this.plotter_
.render();
2238 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
2239 this.canvas_
.height
);
2241 if (this.attr_("drawCallback") !== null) {
2242 this.attr_("drawCallback")(this, is_initial_draw
);
2247 * Determine properties of the y-axes which are independent of the data
2248 * currently being displayed. This includes things like the number of axes and
2249 * the style of the axes. It does not include the range of each axis and its
2251 * This fills in this.axes_ and this.seriesToAxisMap_.
2252 * axes_ = [ { options } ]
2253 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2254 * indices are into the axes_ array.
2256 Dygraph
.prototype.computeYAxes_
= function() {
2257 this.axes_
= [{}]; // always have at least one y-axis.
2258 this.seriesToAxisMap_
= {};
2260 // Get a list of series names.
2261 var labels
= this.attr_("labels");
2263 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2265 // all options which could be applied per-axis:
2273 'axisLabelFontSize',
2277 // Copy global axis options over to the first axis.
2278 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2279 var k
= axisOptions
[i
];
2280 var v
= this.attr_(k
);
2281 if (v
) this.axes_
[0][k
] = v
;
2284 // Go through once and add all the axes.
2285 for (var seriesName
in series
) {
2286 if (!series
.hasOwnProperty(seriesName
)) continue;
2287 var axis
= this.attr_("axis", seriesName
);
2289 this.seriesToAxisMap_
[seriesName
] = 0;
2292 if (typeof(axis
) == 'object') {
2293 // Add a new axis, making a copy of its per-axis options.
2295 Dygraph
.update(opts
, this.axes_
[0]);
2296 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2297 Dygraph
.update(opts
, axis
);
2298 this.axes_
.push(opts
);
2299 this.seriesToAxisMap_
[seriesName
] = this.axes_
.length
- 1;
2303 // Go through one more time and assign series to an axis defined by another
2304 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2305 for (var seriesName
in series
) {
2306 if (!series
.hasOwnProperty(seriesName
)) continue;
2307 var axis
= this.attr_("axis", seriesName
);
2308 if (typeof(axis
) == 'string') {
2309 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2310 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2311 "series " + axis
+ ", which does not define its own axis.");
2314 var idx
= this.seriesToAxisMap_
[axis
];
2315 this.seriesToAxisMap_
[seriesName
] = idx
;
2319 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2320 // this last so that hiding the first series doesn't destroy the axis
2321 // properties of the primary axis.
2322 var seriesToAxisFiltered
= {};
2323 var vis
= this.visibility();
2324 for (var i
= 1; i
< labels
.length
; i
++) {
2326 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2328 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2332 * Returns the number of y-axes on the chart.
2333 * @return {Number} the number of axes.
2335 Dygraph
.prototype.numAxes
= function() {
2337 for (var series
in this.seriesToAxisMap_
) {
2338 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2339 var idx
= this.seriesToAxisMap_
[series
];
2340 if (idx
> last_axis
) last_axis
= idx
;
2342 return 1 + last_axis
;
2346 * Determine the value range and tick marks for each axis.
2347 * @param {Object} extremes A mapping from seriesName -> [low, high]
2348 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2350 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2351 // Build a map from axis number -> [list of series names]
2352 var seriesForAxis
= [];
2353 for (var series
in this.seriesToAxisMap_
) {
2354 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2355 var idx
= this.seriesToAxisMap_
[series
];
2356 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2357 seriesForAxis
[idx
].push(series
);
2360 // Compute extreme values, a span and tick marks for each axis.
2361 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2362 var isLogScale
= this.attr_("logscale");
2363 var axis
= this.axes_
[i
];
2364 if (axis
.valueWindow
) {
2365 // This is only set if the user has zoomed on the y-axis. It is never set
2366 // by a user. It takes precedence over axis.valueRange because, if you set
2367 // valueRange, you'd still expect to be able to pan.
2368 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2369 } else if (axis
.valueRange
) {
2370 // This is a user-set value range for this axis.
2371 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2373 // Calculate the extremes of extremes.
2374 var series
= seriesForAxis
[i
];
2375 var minY
= Infinity
; // extremes[series[0]][0];
2376 var maxY
= -Infinity
; // extremes[series[0]][1];
2377 for (var j
= 0; j
< series
.length
; j
++) {
2378 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2379 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2381 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2383 // Add some padding and round up to an integer to be human-friendly.
2384 var span
= maxY
- minY
;
2385 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2386 if (span
== 0) { span
= maxY
; }
2391 var maxAxisY
= maxY
+ 0.1 * span
;
2392 var minAxisY
= minY
;
2394 var maxAxisY
= maxY
+ 0.1 * span
;
2395 var minAxisY
= minY
- 0.1 * span
;
2397 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2398 if (!this.attr_("avoidMinZero")) {
2399 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2400 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2403 if (this.attr_("includeZero")) {
2404 if (maxY
< 0) maxAxisY
= 0;
2405 if (minY
> 0) minAxisY
= 0;
2409 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2412 // Add ticks. By default, all axes inherit the tick positions of the
2413 // primary axis. However, if an axis is specifically marked as having
2414 // independent ticks, then that is permissible as well.
2415 if (i
== 0 || axis
.independentTicks
) {
2417 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2418 axis
.computedValueRange
[1],
2422 var p_axis
= this.axes_
[0];
2423 var p_ticks
= p_axis
.ticks
;
2424 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2425 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2426 var tick_values
= [];
2427 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2428 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2429 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2430 tick_values
.push(y_val
);
2434 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2435 axis
.computedValueRange
[1],
2436 this, axis
, tick_values
);
2440 return [this.axes_
, this.seriesToAxisMap_
];
2444 * Calculates the rolling average of a data set.
2445 * If originalData is [label, val], rolls the average of those.
2446 * If originalData is [label, [, it's interpreted as [value, stddev]
2447 * and the roll is returned in the same form, with appropriately reduced
2448 * stddev for each value.
2449 * Note that this is where fractional input (i.e. '5/10') is converted into
2451 * @param {Array} originalData The data in the appropriate format (see above)
2452 * @param {Number} rollPeriod The number of days over which to average the data
2454 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2455 if (originalData
.length
< 2)
2456 return originalData
;
2457 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2458 var rollingData
= [];
2459 var sigma
= this.attr_("sigma");
2461 if (this.fractions_
) {
2463 var den
= 0; // numerator/denominator
2465 for (var i
= 0; i
< originalData
.length
; i
++) {
2466 num
+= originalData
[i
][1][0];
2467 den
+= originalData
[i
][1][1];
2468 if (i
- rollPeriod
>= 0) {
2469 num
-= originalData
[i
- rollPeriod
][1][0];
2470 den
-= originalData
[i
- rollPeriod
][1][1];
2473 var date
= originalData
[i
][0];
2474 var value
= den
? num
/ den
: 0.0;
2475 if (this.attr_("errorBars")) {
2476 if (this.wilsonInterval_
) {
2477 // For more details on this confidence interval, see:
2478 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2480 var p
= value
< 0 ? 0 : value
, n
= den
;
2481 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2482 var denom
= 1 + sigma
* sigma
/ den
;
2483 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2484 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2485 rollingData
[i
] = [date
,
2486 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2488 rollingData
[i
] = [date
, [0, 0, 0]];
2491 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2492 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2495 rollingData
[i
] = [date
, mult
* value
];
2498 } else if (this.attr_("customBars")) {
2503 for (var i
= 0; i
< originalData
.length
; i
++) {
2504 var data
= originalData
[i
][1];
2506 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2508 if (y
!= null && !isNaN(y
)) {
2514 if (i
- rollPeriod
>= 0) {
2515 var prev
= originalData
[i
- rollPeriod
];
2516 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2523 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2524 1.0 * (mid
- low
) / count
,
2525 1.0 * (high
- mid
) / count
]];
2528 // Calculate the rolling average for the first rollPeriod - 1 points where
2529 // there is not enough data to roll over the full number of days
2530 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2531 if (!this.attr_("errorBars")){
2532 if (rollPeriod
== 1) {
2533 return originalData
;
2536 for (var i
= 0; i
< originalData
.length
; i
++) {
2539 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2540 var y
= originalData
[j
][1];
2541 if (y
== null || isNaN(y
)) continue;
2543 sum
+= originalData
[j
][1];
2546 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2548 rollingData
[i
] = [originalData
[i
][0], null];
2553 for (var i
= 0; i
< originalData
.length
; i
++) {
2557 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2558 var y
= originalData
[j
][1][0];
2559 if (y
== null || isNaN(y
)) continue;
2561 sum
+= originalData
[j
][1][0];
2562 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2565 var stddev
= Math
.sqrt(variance
) / num_ok
;
2566 rollingData
[i
] = [originalData
[i
][0],
2567 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2569 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2579 * Parses a date, returning the number of milliseconds since epoch. This can be
2580 * passed in as an xValueParser in the Dygraph constructor.
2581 * TODO(danvk): enumerate formats that this understands.
2582 * @param {String} A date in YYYYMMDD format.
2583 * @return {Number} Milliseconds since epoch.
2586 Dygraph
.dateParser
= function(dateStr
, self
) {
2589 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2590 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2591 while (dateStrSlashed
.search("-") != -1) {
2592 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2594 d
= Date
.parse(dateStrSlashed
);
2595 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2596 // TODO(danvk): remove support for this format. It's confusing.
2597 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2598 + "/" + dateStr
.substr(6,2);
2599 d
= Date
.parse(dateStrSlashed
);
2601 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2602 // "2009/07/12 12:34:56"
2603 d
= Date
.parse(dateStr
);
2606 if (!d
|| isNaN(d
)) {
2607 self
.error("Couldn't parse " + dateStr
+ " as a date");
2613 * Detects the type of the str (date or numeric) and sets the various
2614 * formatting attributes in this.attrs_ based on this type.
2615 * @param {String} str An x value.
2618 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2620 if (str
.indexOf('-') >= 0 ||
2621 str
.indexOf('/') >= 0 ||
2622 isNaN(parseFloat(str
))) {
2624 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2625 // TODO(danvk): remove support for this format.
2630 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2631 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2632 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2633 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2635 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2636 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2637 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2638 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2643 * Parses a string in a special csv format. We expect a csv file where each
2644 * line is a date point, and the first field in each line is the date string.
2645 * We also expect that all remaining fields represent series.
2646 * if the errorBars attribute is set, then interpret the fields as:
2647 * date, series1, stddev1, series2, stddev2, ...
2648 * @param {Array.<Object>} data See above.
2651 * @return Array.<Object> An array with one entry for each row. These entries
2652 * are an array of cells in that row. The first entry is the parsed x-value for
2653 * the row. The second, third, etc. are the y-values. These can take on one of
2654 * three forms, depending on the CSV and constructor parameters:
2656 * 2. [ value, stddev ]
2657 * 3. [ low value, center value, high value ]
2659 Dygraph
.prototype.parseCSV_
= function(data
) {
2661 var lines
= data
.split("\n");
2663 // Use the default delimiter or fall back to a tab if that makes sense.
2664 var delim
= this.attr_('delimiter');
2665 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2670 if (this.labelsFromCSV_
) {
2672 this.attrs_
.labels
= lines
[0].split(delim
);
2675 // Parse the x as a float or return null if it's not a number.
2676 var parseFloatOrNull
= function(x
) {
2677 var val
= parseFloat(x
);
2678 // isFinite() returns false for NaN and +/-Infinity
.
2679 return isFinite(val
) ? val
: null;
2683 var defaultParserSet
= false; // attempt to auto-detect x value type
2684 var expectedCols
= this.attr_("labels").length
;
2685 var outOfOrder
= false;
2686 for (var i
= start
; i
< lines
.length
; i
++) {
2687 var line
= lines
[i
];
2688 if (line
.length
== 0) continue; // skip blank lines
2689 if (line
[0] == '#') continue; // skip comment lines
2690 var inFields
= line
.split(delim
);
2691 if (inFields
.length
< 2) continue;
2694 if (!defaultParserSet
) {
2695 this.detectTypeFromString_(inFields
[0]);
2696 xParser
= this.attr_("xValueParser");
2697 defaultParserSet
= true;
2699 fields
[0] = xParser(inFields
[0], this);
2701 // If fractions are expected, parse the numbers as "A/B
"
2702 if (this.fractions_) {
2703 for (var j = 1; j < inFields.length; j++) {
2704 // TODO(danvk): figure out an appropriate way to flag parse errors.
2705 var vals = inFields[j].split("/");
2706 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2708 } else if (this.attr_("errorBars
")) {
2709 // If there are error bars, values are (value, stddev) pairs
2710 for (var j = 1; j < inFields.length; j += 2)
2711 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2712 parseFloatOrNull(inFields[j + 1])];
2713 } else if (this.attr_("customBars
")) {
2714 // Bars are a low;center;high tuple
2715 for (var j = 1; j < inFields.length; j++) {
2716 var vals = inFields[j].split(";");
2717 fields[j] = [ parseFloatOrNull(vals[0]),
2718 parseFloatOrNull(vals[1]),
2719 parseFloatOrNull(vals[2]) ];
2722 // Values are just numbers
2723 for (var j = 1; j < inFields.length; j++) {
2724 fields[j] = parseFloatOrNull(inFields[j]);
2727 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2732 if (fields.length != expectedCols) {
2733 this.error("Number of columns
in line
" + i + " (" + fields.length +
2734 ") does not agree
with number of
labels (" + expectedCols +
2740 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2741 ret.sort(function(a,b) { return a[0] - b[0] });
2748 * The user has provided their data as a pre-packaged JS array. If the x values
2749 * are numeric, this is the same as dygraphs' internal format. If the x values
2750 * are dates, we need to convert them from Date objects to ms since epoch.
2751 * @param {Array.<Object>} data
2752 * @return {Array.<Object>} data with numeric x values.
2754 Dygraph.prototype.parseArray_ = function(data) {
2755 // Peek at the first x value to see if it's numeric.
2756 if (data.length == 0) {
2757 this.error("Can
't plot empty data set");
2760 if (data[0].length == 0) {
2761 this.error("Data set cannot contain an empty row");
2765 if (this.attr_("labels") == null) {
2766 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2767 "in the options parameter");
2768 this.attrs_.labels = [ "X" ];
2769 for (var i = 1; i < data[0].length; i++) {
2770 this.attrs_.labels.push("Y" + i);
2774 if (Dygraph.isDateLike(data[0][0])) {
2775 // Some intelligent defaults for a date x-axis.
2776 this.attrs_.xValueFormatter = Dygraph.dateString_;
2777 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2778 this.attrs_.xTicker = Dygraph.dateTicker;
2780 // Assume they're all dates
.
2781 var parsedData
= Dygraph
.clone(data
);
2782 for (var i
= 0; i
< data
.length
; i
++) {
2783 if (parsedData
[i
].length
== 0) {
2784 this.error("Row " + (1 + i
) + " of data is empty");
2787 if (parsedData
[i
][0] == null
2788 || typeof(parsedData
[i
][0].getTime
) != 'function'
2789 || isNaN(parsedData
[i
][0].getTime())) {
2790 this.error("x value in row " + (1 + i
) + " is not a Date");
2793 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2797 // Some intelligent defaults for a numeric x-axis.
2798 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2799 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2805 * Parses a DataTable object from gviz.
2806 * The data is expected to have a first column that is either a date or a
2807 * number. All subsequent columns must be numbers. If there is a clear mismatch
2808 * between this.xValueParser_ and the type of the first column, it will be
2809 * fixed. Fills out rawData_.
2810 * @param {Array.<Object>} data See above.
2813 Dygraph
.prototype.parseDataTable_
= function(data
) {
2814 var cols
= data
.getNumberOfColumns();
2815 var rows
= data
.getNumberOfRows();
2817 var indepType
= data
.getColumnType(0);
2818 if (indepType
== 'date' || indepType
== 'datetime') {
2819 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2820 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2821 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2822 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2823 } else if (indepType
== 'number') {
2824 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2825 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2826 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2827 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2829 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2830 "column 1 of DataTable input (Got '" + indepType
+ "')");
2834 // Array of the column indices which contain data (and not annotations).
2836 var annotationCols
= {}; // data index -> [annotation cols]
2837 var hasAnnotations
= false;
2838 for (var i
= 1; i
< cols
; i
++) {
2839 var type
= data
.getColumnType(i
);
2840 if (type
== 'number') {
2842 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2843 // This is OK -- it's an annotation column.
2844 var dataIdx
= colIdx
[colIdx
.length
- 1];
2845 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2846 annotationCols
[dataIdx
] = [i
];
2848 annotationCols
[dataIdx
].push(i
);
2850 hasAnnotations
= true;
2852 this.error("Only 'number' is supported as a dependent type with Gviz." +
2853 " 'string' is only supported if displayAnnotations is true");
2857 // Read column labels
2858 // TODO(danvk): add support back for errorBars
2859 var labels
= [data
.getColumnLabel(0)];
2860 for (var i
= 0; i
< colIdx
.length
; i
++) {
2861 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2862 if (this.attr_("errorBars")) i
+= 1;
2864 this.attrs_
.labels
= labels
;
2865 cols
= labels
.length
;
2868 var outOfOrder
= false;
2869 var annotations
= [];
2870 for (var i
= 0; i
< rows
; i
++) {
2872 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2873 data
.getValue(i
, 0) === null) {
2874 this.warn("Ignoring row " + i
+
2875 " of DataTable because of undefined or null first column.");
2879 if (indepType
== 'date' || indepType
== 'datetime') {
2880 row
.push(data
.getValue(i
, 0).getTime());
2882 row
.push(data
.getValue(i
, 0));
2884 if (!this.attr_("errorBars")) {
2885 for (var j
= 0; j
< colIdx
.length
; j
++) {
2886 var col
= colIdx
[j
];
2887 row
.push(data
.getValue(i
, col
));
2888 if (hasAnnotations
&&
2889 annotationCols
.hasOwnProperty(col
) &&
2890 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2892 ann
.series
= data
.getColumnLabel(col
);
2894 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2896 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2897 if (k
) ann
.text
+= "\n";
2898 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2900 annotations
.push(ann
);
2904 for (var j
= 0; j
< cols
- 1; j
++) {
2905 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2908 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2912 // Strip out infinities, which give dygraphs problems later on.
2913 for (var j
= 0; j
< row
.length
; j
++) {
2914 if (!isFinite(row
[j
])) row
[j
] = null;
2920 this.warn("DataTable is out of order; order it correctly to speed loading.");
2921 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2923 this.rawData_
= ret
;
2925 if (annotations
.length
> 0) {
2926 this.setAnnotations(annotations
, true);
2930 // These functions are all based on MochiKit.
2931 Dygraph
.update
= function (self
, o
) {
2932 if (typeof(o
) != 'undefined' && o
!== null) {
2934 if (o
.hasOwnProperty(k
)) {
2942 Dygraph
.isArrayLike
= function (o
) {
2943 var typ
= typeof(o
);
2945 (typ
!= 'object' && !(typ
== 'function' &&
2946 typeof(o
.item
) == 'function')) ||
2948 typeof(o
.length
) != 'number' ||
2956 Dygraph
.isDateLike
= function (o
) {
2957 if (typeof(o
) != "object" || o
=== null ||
2958 typeof(o
.getTime
) != 'function') {
2964 Dygraph
.clone
= function(o
) {
2965 // TODO(danvk): figure out how MochiKit's version works
2967 for (var i
= 0; i
< o
.length
; i
++) {
2968 if (Dygraph
.isArrayLike(o
[i
])) {
2969 r
.push(Dygraph
.clone(o
[i
]));
2979 * Get the CSV data. If it's in a function, call that function. If it's in a
2980 * file, do an XMLHttpRequest to get it.
2983 Dygraph
.prototype.start_
= function() {
2984 if (typeof this.file_
== 'function') {
2985 // CSV string. Pretend we got it via XHR.
2986 this.loadedEvent_(this.file_());
2987 } else if (Dygraph
.isArrayLike(this.file_
)) {
2988 this.rawData_
= this.parseArray_(this.file_
);
2990 } else if (typeof this.file_
== 'object' &&
2991 typeof this.file_
.getColumnRange
== 'function') {
2992 // must be a DataTable from gviz.
2993 this.parseDataTable_(this.file_
);
2995 } else if (typeof this.file_
== 'string') {
2996 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2997 if (this.file_
.indexOf('\n') >= 0) {
2998 this.loadedEvent_(this.file_
);
3000 var req
= new XMLHttpRequest();
3002 req
.onreadystatechange
= function () {
3003 if (req
.readyState
== 4) {
3004 if (req
.status
== 200) {
3005 caller
.loadedEvent_(req
.responseText
);
3010 req
.open("GET", this.file_
, true);
3014 this.error("Unknown data format: " + (typeof this.file_
));
3019 * Changes various properties of the graph. These can include:
3021 * <li>file: changes the source data for the graph</li>
3022 * <li>errorBars: changes whether the data contains stddev</li>
3024 * @param {Object} attrs The new properties and values
3026 Dygraph
.prototype.updateOptions
= function(attrs
) {
3027 // TODO(danvk): this is a mess. Rethink this function.
3028 if ('rollPeriod' in attrs
) {
3029 this.rollPeriod_
= attrs
.rollPeriod
;
3031 if ('dateWindow' in attrs
) {
3032 this.dateWindow_
= attrs
.dateWindow
;
3035 // TODO(danvk): validate per-series options.
3040 // highlightCircleSize
3042 Dygraph
.update(this.user_attrs_
, attrs
);
3043 Dygraph
.update(this.renderOptions_
, attrs
);
3045 this.labelsFromCSV_
= (this.attr_("labels") == null);
3047 // TODO(danvk): this doesn't match the constructor logic
3048 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
3049 if (attrs
['file']) {
3050 this.file_
= attrs
['file'];
3058 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3059 * containing div (which has presumably changed size since the dygraph was
3060 * instantiated. If the width/height are specified, the div will be resized.
3062 * This is far more efficient than destroying and re-instantiating a
3063 * Dygraph, since it doesn't have to reparse the underlying data.
3065 * @param {Number} width Width (in pixels)
3066 * @param {Number} height Height (in pixels)
3068 Dygraph
.prototype.resize
= function(width
, height
) {
3069 if (this.resize_lock
) {
3072 this.resize_lock
= true;
3074 if ((width
=== null) != (height
=== null)) {
3075 this.warn("Dygraph.resize() should be called with zero parameters or " +
3076 "two non-NULL parameters. Pretending it was zero.");
3077 width
= height
= null;
3080 // TODO(danvk): there should be a clear() method.
3081 this.maindiv_
.innerHTML
= "";
3082 this.attrs_
.labelsDiv
= null;
3085 this.maindiv_
.style
.width
= width
+ "px";
3086 this.maindiv_
.style
.height
= height
+ "px";
3087 this.width_
= width
;
3088 this.height_
= height
;
3090 this.width_
= this.maindiv_
.offsetWidth
;
3091 this.height_
= this.maindiv_
.offsetHeight
;
3094 this.createInterface_();
3097 this.resize_lock
= false;
3101 * Adjusts the number of days in the rolling average. Updates the graph to
3102 * reflect the new averaging period.
3103 * @param {Number} length Number of days over which to average the data.
3105 Dygraph
.prototype.adjustRoll
= function(length
) {
3106 this.rollPeriod_
= length
;
3111 * Returns a boolean array of visibility statuses.
3113 Dygraph
.prototype.visibility
= function() {
3114 // Do lazy-initialization, so that this happens after we know the number of
3116 if (!this.attr_("visibility")) {
3117 this.attrs_
["visibility"] = [];
3119 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
3120 this.attr_("visibility").push(true);
3122 return this.attr_("visibility");
3126 * Changes the visiblity of a series.
3128 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3129 var x
= this.visibility();
3130 if (num
< 0 || num
>= x
.length
) {
3131 this.warn("invalid series number in setVisibility: " + num
);
3139 * Update the list of annotations and redraw the chart.
3141 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3142 // Only add the annotation CSS rule once we know it will be used.
3143 Dygraph
.addAnnotationRule();
3144 this.annotations_
= ann
;
3145 this.layout_
.setAnnotations(this.annotations_
);
3146 if (!suppressDraw
) {
3152 * Return the list of annotations.
3154 Dygraph
.prototype.annotations
= function() {
3155 return this.annotations_
;
3159 * Get the index of a series (column) given its name. The first column is the
3160 * x-axis, so the data series start with index 1.
3162 Dygraph
.prototype.indexFromSetName
= function(name
) {
3163 var labels
= this.attr_("labels");
3164 for (var i
= 0; i
< labels
.length
; i
++) {
3165 if (labels
[i
] == name
) return i
;
3170 Dygraph
.addAnnotationRule
= function() {
3171 if (Dygraph
.addedAnnotationCSS
) return;
3173 var rule
= "border: 1px solid black; " +
3174 "background-color: white; " +
3175 "text-align: center;";
3177 var styleSheetElement
= document
.createElement("style");
3178 styleSheetElement
.type
= "text/css";
3179 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3181 // Find the first style sheet that we can access.
3182 // We may not add a rule to a style sheet from another domain for security
3183 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3184 // adds its own style sheets from google.com.
3185 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3186 if (document
.styleSheets
[i
].disabled
) continue;
3187 var mysheet
= document
.styleSheets
[i
];
3189 if (mysheet
.insertRule
) { // Firefox
3190 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3191 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3192 } else if (mysheet
.addRule
) { // IE
3193 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3195 Dygraph
.addedAnnotationCSS
= true;
3198 // Was likely a security exception.
3202 this.warn("Unable to add default annotation CSS rule; display may be off.");
3206 * Create a new canvas element. This is more complex than a simple
3207 * document.createElement("canvas") because of IE and excanvas.
3209 Dygraph
.createCanvas
= function() {
3210 var canvas
= document
.createElement("canvas");
3212 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
3213 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
3214 canvas
= G_vmlCanvasManager
.initElement(canvas
);
3222 * A wrapper around Dygraph that implements the gviz API.
3223 * @param {Object} container The DOM object the visualization should live in.
3225 Dygraph
.GVizChart
= function(container
) {
3226 this.container
= container
;
3229 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
3230 // Clear out any existing dygraph.
3231 // TODO(danvk): would it make more sense to simply redraw using the current
3232 // date_graph object?
3233 this.container
.innerHTML
= '';
3234 if (typeof(this.date_graph
) != 'undefined') {
3235 this.date_graph
.destroy();
3238 this.date_graph
= new Dygraph(this.container
, data
, options
);
3242 * Google charts compatible setSelection
3243 * Only row selection is supported, all points in the row will be highlighted
3244 * @param {Array} array of the selected cells
3247 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
3249 if (selection_array
.length
) {
3250 row
= selection_array
[0].row
;
3252 this.date_graph
.setSelection(row
);
3256 * Google charts compatible getSelection implementation
3257 * @return {Array} array of the selected cells
3260 Dygraph
.GVizChart
.prototype.getSelection
= function() {
3263 var row
= this.date_graph
.getSelection();
3265 if (row
< 0) return selection
;
3268 for (var i
in this.date_graph
.layout_
.datasets
) {
3269 selection
.push({row
: row
, column
: col
});
3276 // Older pages may still use this name.
3277 DateGraph
= Dygraph
;