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
27 Date,SeriesA,SeriesB,...
28 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
29 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
31 If the 'fractions' option is set, the input should be of the form:
33 Date,SeriesA,SeriesB,...
34 YYYYMMDD,A1/B1,A2/B2,...
35 YYYYMMDD,A1/B1,A2/B2,...
37 And error bars will be calculated automatically using a binomial distribution.
39 For further documentation and examples, see http://dygraphs.com/
44 * An interactive, zoomable graph
45 * @param {String | Function} file A file containing CSV data or a function that
46 * returns this data. The expected format for each line is
47 * YYYYMMDD,val1,val2,... or, if attrs.errorBars is set,
48 * YYYYMMDD,val1,stddev1,val2,stddev2,...
49 * @param {Object} attrs Various other attributes, e.g. errorBars determines
50 * whether the input data contains error ranges.
52 Dygraph
= function(div
, data
, opts
) {
53 if (arguments
.length
> 0) {
54 if (arguments
.length
== 4) {
55 // Old versions of dygraphs took in the series labels as a constructor
56 // parameter. This doesn't make sense anymore, but it's easy to continue
57 // to support this usage.
58 this.warn("Using deprecated four-argument dygraph constructor");
59 this.__old_init__(div
, data
, arguments
[2], arguments
[3]);
61 this.__init__(div
, data
, opts
);
66 Dygraph
.NAME
= "Dygraph";
67 Dygraph
.VERSION
= "1.2";
68 Dygraph
.__repr__
= function() {
69 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
71 Dygraph
.toString
= function() {
72 return this.__repr__();
75 // Various default values
76 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
77 Dygraph
.DEFAULT_WIDTH
= 480;
78 Dygraph
.DEFAULT_HEIGHT
= 320;
79 Dygraph
.AXIS_LINE_WIDTH
= 0.3;
81 Dygraph
.LOG_SCALE
= 10;
82 Dygraph
.LN_TEN
= Math
.log(Dygraph
.LOG_SCALE
);
83 Dygraph
.log10
= function(x
) {
84 return Math
.log(x
) / Dygraph
.LN_TEN
;
87 // Default attribute values.
88 Dygraph
.DEFAULT_ATTRS
= {
89 highlightCircleSize
: 3,
95 // TODO(danvk): move defaults from createStatusMessage_ here.
97 labelsSeparateLines
: false,
98 labelsShowZeroValues
: true,
101 showLabelsOnHighlight
: true,
103 yValueFormatter
: function(x
) { return Dygraph
.round_(x
, 2); },
108 axisLabelFontSize
: 14,
111 xAxisLabelFormatter
: Dygraph
.dateAxisFormatter
,
115 xValueFormatter
: Dygraph
.dateString_
,
116 xValueParser
: Dygraph
.dateParser
,
117 xTicker
: Dygraph
.dateTicker
,
124 wilsonInterval
: true, // only relevant if fractions is true
128 connectSeparatedPoints
: false,
131 hideOverlayOnMouseOut
: true,
136 interactionModel
: null // will be set to Dygraph.defaultInteractionModel.
139 // Various logging levels.
145 // Directions for panning and zooming. Use bit operations when combined
146 // values are possible.
147 Dygraph
.HORIZONTAL
= 1;
148 Dygraph
.VERTICAL
= 2;
150 // Used for initializing annotation CSS rules only once.
151 Dygraph
.addedAnnotationCSS
= false;
153 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
154 // Labels is no longer a constructor parameter, since it's typically set
155 // directly from the data source. It also conains a name for the x-axis,
156 // which the previous constructor form did not.
157 if (labels
!= null) {
158 var new_labels
= ["Date"];
159 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
160 Dygraph
.update(attrs
, { 'labels': new_labels
});
162 this.__init__(div
, file
, attrs
);
166 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
167 * and context <canvas> inside of it. See the constructor for details.
169 * @param {Element} div the Element to render the graph into.
170 * @param {String | Function} file Source data
171 * @param {Object} attrs Miscellaneous other options
174 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
175 // Hack for IE: if we're using excanvas and the document hasn't finished
176 // loading yet (and hence may not have initialized whatever it needs to
177 // initialize), then keep calling this routine periodically until it has.
178 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
179 typeof(G_vmlCanvasManager
) != 'undefined' &&
180 document
.readyState
!= 'complete') {
182 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
185 // Support two-argument constructor
186 if (attrs
== null) { attrs
= {}; }
188 // Copy the important bits into the object
189 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
192 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
193 this.previousVerticalX_
= -1;
194 this.fractions_
= attrs
.fractions
|| false;
195 this.dateWindow_
= attrs
.dateWindow
|| null;
197 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
198 this.is_initial_draw_
= true;
199 this.annotations_
= [];
201 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
202 this.zoomed_x_
= false;
203 this.zoomed_y_
= false;
205 // Clear the div. This ensure that, if multiple dygraphs are passed the same
206 // div, then only one will be drawn.
209 // If the div isn't already sized then inherit from our attrs or
210 // give it a default size.
211 if (div
.style
.width
== '') {
212 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
214 if (div
.style
.height
== '') {
215 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
217 this.width_
= parseInt(div
.style
.width
, 10);
218 this.height_
= parseInt(div
.style
.height
, 10);
219 // The div might have been specified as percent of the current window size,
220 // convert that to an appropriate number of pixels.
221 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
222 this.width_
= div
.offsetWidth
;
224 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
225 this.height_
= div
.offsetHeight
;
228 if (this.width_
== 0) {
229 this.error("dygraph has zero width. Please specify a width in pixels.");
231 if (this.height_
== 0) {
232 this.error("dygraph has zero height. Please specify a height in pixels.");
235 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
236 if (attrs
['stackedGraph']) {
237 attrs
['fillGraph'] = true;
238 // TODO(nikhilk): Add any other stackedGraph checks here.
241 // Dygraphs has many options, some of which interact with one another.
242 // To keep track of everything, we maintain two sets of options:
244 // this.user_attrs_ only options explicitly set by the user.
245 // this.attrs_ defaults, options derived from user_attrs_, data.
247 // Options are then accessed this.attr_('attr'), which first looks at
248 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
249 // defaults without overriding behavior that the user specifically asks for.
250 this.user_attrs_
= {};
251 Dygraph
.update(this.user_attrs_
, attrs
);
254 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
256 this.boundaryIds_
= [];
258 // Make a note of whether labels will be pulled from the CSV file.
259 this.labelsFromCSV_
= (this.attr_("labels") == null);
261 // Create the containing DIV and other interactive elements
262 this.createInterface_();
267 // Axis is an optional parameter. Can be set to 'x' or 'y'.
268 Dygraph
.prototype.isZoomed
= function(axis
) {
269 if (axis
== null) return this.zoomed_x_
|| this.zoomed_y_
;
270 if (axis
== 'x') return this.zoomed_x_
;
271 if (axis
== 'y') return this.zoomed_y_
;
272 throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
275 Dygraph
.prototype.toString
= function() {
276 var maindiv
= this.maindiv_
;
277 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
278 return "[Dygraph " + id
+ "]";
281 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
283 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
284 this.user_attrs_
[seriesName
] != null &&
285 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
286 return this.user_attrs_
[seriesName
][name
];
287 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
288 return this.user_attrs_
[name
];
289 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
290 return this.attrs_
[name
];
296 // TODO(danvk): any way I can get the line numbers to be this.warn call?
297 Dygraph
.prototype.log
= function(severity
, message
) {
298 if (typeof(console
) != 'undefined') {
301 console
.debug('dygraphs: ' + message
);
304 console
.info('dygraphs: ' + message
);
306 case Dygraph
.WARNING
:
307 console
.warn('dygraphs: ' + message
);
310 console
.error('dygraphs: ' + message
);
315 Dygraph
.prototype.info
= function(message
) {
316 this.log(Dygraph
.INFO
, message
);
318 Dygraph
.prototype.warn
= function(message
) {
319 this.log(Dygraph
.WARNING
, message
);
321 Dygraph
.prototype.error
= function(message
) {
322 this.log(Dygraph
.ERROR
, message
);
326 * Returns the current rolling period, as set by the user or an option.
327 * @return {Number} The number of days in the rolling window
329 Dygraph
.prototype.rollPeriod
= function() {
330 return this.rollPeriod_
;
334 * Returns the currently-visible x-range. This can be affected by zooming,
335 * panning or a call to updateOptions.
336 * Returns a two-element array: [left, right].
337 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
339 Dygraph
.prototype.xAxisRange
= function() {
340 if (this.dateWindow_
) return this.dateWindow_
;
342 // The entire chart is visible.
343 var left
= this.rawData_
[0][0];
344 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
345 return [left
, right
];
349 * Returns the currently-visible y-range for an axis. This can be affected by
350 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
351 * called with no arguments, returns the range of the first axis.
352 * Returns a two-element array: [bottom, top].
354 Dygraph
.prototype.yAxisRange
= function(idx
) {
355 if (typeof(idx
) == "undefined") idx
= 0;
356 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
357 return [ this.axes_
[idx
].computedValueRange
[0],
358 this.axes_
[idx
].computedValueRange
[1] ];
362 * Returns the currently-visible y-ranges for each axis. This can be affected by
363 * zooming, panning, calls to updateOptions, etc.
364 * Returns an array of [bottom, top] pairs, one for each y-axis.
366 Dygraph
.prototype.yAxisRanges
= function() {
368 for (var i
= 0; i
< this.axes_
.length
; i
++) {
369 ret
.push(this.yAxisRange(i
));
374 // TODO(danvk): use these functions throughout dygraphs.
376 * Convert from data coordinates to canvas/div X/Y coordinates.
377 * If specified, do this conversion for the coordinate system of a particular
378 * axis. Uses the first axis by default.
379 * Returns a two-element array: [X, Y]
381 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
382 * instead of toDomCoords(null, y, axis).
384 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
385 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
389 * Convert from data x coordinates to canvas/div X coordinate.
390 * If specified, do this conversion for the coordinate system of a particular
392 * Returns a single value or null if x is null.
394 Dygraph
.prototype.toDomXCoord
= function(x
) {
399 var area
= this.plotter_
.area
;
400 var xRange
= this.xAxisRange();
401 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
405 * Convert from data x coordinates to canvas/div Y coordinate and optional
406 * axis. Uses the first axis by default.
408 * returns a single value or null if y is null.
410 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
411 var pct
= this.toPercentYCoord(y
, axis
);
416 var area
= this.plotter_
.area
;
417 return area
.y
+ pct
* area
.h
;
421 * Convert from canvas/div coords to data coordinates.
422 * If specified, do this conversion for the coordinate system of a particular
423 * axis. Uses the first axis by default.
424 * Returns a two-element array: [X, Y].
426 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
427 * instead of toDataCoords(null, y, axis).
429 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
430 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
434 * Convert from canvas/div x coordinate to data coordinate.
436 * If x is null, this returns null.
438 Dygraph
.prototype.toDataXCoord
= function(x
) {
443 var area
= this.plotter_
.area
;
444 var xRange
= this.xAxisRange();
445 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
449 * Convert from canvas/div y coord to value.
451 * If y is null, this returns null.
452 * if axis is null, this uses the first axis.
454 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
459 var area
= this.plotter_
.area
;
460 var yRange
= this.yAxisRange(axis
);
462 if (typeof(axis
) == "undefined") axis
= 0;
463 if (!this.axes_
[axis
].logscale
) {
464 return yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
466 // Computing the inverse of toDomCoord.
467 var pct
= (y
- area
.y
) / area
.h
469 // Computing the inverse of toPercentYCoord. The function was arrived at with
470 // the following steps:
472 // Original calcuation:
473 // pct = (logr1 - Dygraph.log10(y)) / (logr1
- Dygraph
.log10(yRange
[0]));
475 // Move denominator to both sides:
476 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
478 // subtract logr1, and take the negative value.
479 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
481 // Swap both sides of the equation, and we can compute the log of the
482 // return value. Which means we just need to use that as the exponent in
484 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
486 var logr1
= Dygraph
.log10(yRange
[1]);
487 var exponent
= logr1
- (pct
* (logr1
- Dygraph
.log10(yRange
[0])));
488 var value
= Math
.pow(Dygraph
.LOG_SCALE
, exponent
);
494 * Converts a y for an axis to a percentage from the top to the
497 * If the coordinate represents a value visible on the canvas, then
498 * the value will be between 0 and 1, where 0 is the top of the canvas.
499 * However, this method will return values outside the range, as
500 * values can fall outside the canvas.
502 * If y is null, this returns null.
503 * if axis is null, this uses the first axis.
505 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
509 if (typeof(axis
) == "undefined") axis
= 0;
511 var area
= this.plotter_
.area
;
512 var yRange
= this.yAxisRange(axis
);
515 if (!this.axes_
[axis
].logscale
) {
516 // yrange[1] - y is unit distance from the bottom.
517 // yrange[1] - yrange[0] is the scale of the range.
518 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
519 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
521 var logr1
= Dygraph
.log10(yRange
[1]);
522 pct
= (logr1
- Dygraph
.log10(y
)) / (logr1
- Dygraph
.log10(yRange
[0]));
528 * Returns the number of columns (including the independent variable).
530 Dygraph
.prototype.numColumns
= function() {
531 return this.rawData_
[0].length
;
535 * Returns the number of rows (excluding any header/label row).
537 Dygraph
.prototype.numRows
= function() {
538 return this.rawData_
.length
;
542 * Returns the value in the given row and column. If the row and column exceed
543 * the bounds on the data, returns null. Also returns null if the value is
546 Dygraph
.prototype.getValue
= function(row
, col
) {
547 if (row
< 0 || row
> this.rawData_
.length
) return null;
548 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
550 return this.rawData_
[row
][col
];
553 Dygraph
.addEvent
= function(el
, evt
, fn
) {
554 var normed_fn
= function(e
) {
555 if (!e
) var e
= window
.event
;
558 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
559 el
.addEventListener(evt
, normed_fn
, false);
561 el
.attachEvent('on' + evt
, normed_fn
);
566 // Based on the article at
567 // http://www.switchonthecode.com/tutorials
/javascript
-tutorial
-the
-scroll
-wheel
568 Dygraph
.cancelEvent
= function(e
) {
569 e
= e
? e
: window
.event
;
570 if (e
.stopPropagation
) {
573 if (e
.preventDefault
) {
576 e
.cancelBubble
= true;
578 e
.returnValue
= false;
583 * Generates interface elements for the Dygraph: a containing div, a div to
584 * display the current point, and a textbox to adjust the rolling average
585 * period. Also creates the Renderer/Layout elements.
588 Dygraph
.prototype.createInterface_
= function() {
589 // Create the all-enclosing graph div
590 var enclosing
= this.maindiv_
;
592 this.graphDiv
= document
.createElement("div");
593 this.graphDiv
.style
.width
= this.width_
+ "px";
594 this.graphDiv
.style
.height
= this.height_
+ "px";
595 enclosing
.appendChild(this.graphDiv
);
597 // Create the canvas for interactive parts of the chart.
598 this.canvas_
= Dygraph
.createCanvas();
599 this.canvas_
.style
.position
= "absolute";
600 this.canvas_
.width
= this.width_
;
601 this.canvas_
.height
= this.height_
;
602 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
603 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
605 // ... and for static parts of the chart.
606 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
608 // The interactive parts of the graph are drawn on top of the chart.
609 this.graphDiv
.appendChild(this.hidden_
);
610 this.graphDiv
.appendChild(this.canvas_
);
611 this.mouseEventElement_
= this.canvas_
;
614 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
615 dygraph
.mouseMove_(e
);
617 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
618 dygraph
.mouseOut_(e
);
621 // Create the grapher
622 // TODO(danvk): why does the Layout need its own set of options?
623 this.layoutOptions_
= { 'xOriginIsZero': false };
624 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
625 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
626 Dygraph
.update(this.layoutOptions_
, {
627 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
629 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
631 // TODO(danvk): why does the Renderer need its own set of options?
632 this.renderOptions_
= { colorScheme
: this.colors_
,
634 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
635 Dygraph
.update(this.renderOptions_
, this.attrs_
);
636 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
638 this.createStatusMessage_();
639 this.createDragInterface_();
643 * Detach DOM elements in the dygraph and null out all data references.
644 * Calling this when you're done with a dygraph can dramatically reduce memory
645 * usage. See, e.g., the tests/perf.html example.
647 Dygraph
.prototype.destroy
= function() {
648 var removeRecursive
= function(node
) {
649 while (node
.hasChildNodes()) {
650 removeRecursive(node
.firstChild
);
651 node
.removeChild(node
.firstChild
);
654 removeRecursive(this.maindiv_
);
656 var nullOut
= function(obj
) {
658 if (typeof(obj
[n
]) === 'object') {
664 // These may not all be necessary, but it can't hurt...
665 nullOut(this.layout_
);
666 nullOut(this.plotter_
);
671 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
672 * this particular canvas. All Dygraph work is done on this.canvas_.
673 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
674 * @return {Object} The newly-created canvas
677 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
678 var h
= Dygraph
.createCanvas();
679 h
.style
.position
= "absolute";
680 // TODO(danvk): h should be offset from canvas. canvas needs to include
681 // some extra area to make it easier to zoom in on the far left and far
682 // right. h needs to be precisely the plot area, so that clipping occurs.
683 h
.style
.top
= canvas
.style
.top
;
684 h
.style
.left
= canvas
.style
.left
;
685 h
.width
= this.width_
;
686 h
.height
= this.height_
;
687 h
.style
.width
= this.width_
+ "px"; // for IE
688 h
.style
.height
= this.height_
+ "px"; // for IE
692 // Taken from MochiKit.Color
693 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
697 if (saturation
=== 0) {
702 var i
= Math
.floor(hue
* 6);
703 var f
= (hue
* 6) - i
;
704 var p
= value
* (1 - saturation
);
705 var q
= value
* (1 - (saturation
* f
));
706 var t
= value
* (1 - (saturation
* (1 - f
)));
708 case 1: red
= q
; green
= value
; blue
= p
; break;
709 case 2: red
= p
; green
= value
; blue
= t
; break;
710 case 3: red
= p
; green
= q
; blue
= value
; break;
711 case 4: red
= t
; green
= p
; blue
= value
; break;
712 case 5: red
= value
; green
= p
; blue
= q
; break;
713 case 6: // fall through
714 case 0: red
= value
; green
= t
; blue
= p
; break;
717 red
= Math
.floor(255 * red
+ 0.5);
718 green
= Math
.floor(255 * green
+ 0.5);
719 blue
= Math
.floor(255 * blue
+ 0.5);
720 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
725 * Generate a set of distinct colors for the data series. This is done with a
726 * color wheel. Saturation/Value are customizable, and the hue is
727 * equally-spaced around the color wheel. If a custom set of colors is
728 * specified, that is used instead.
731 Dygraph
.prototype.setColors_
= function() {
732 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
733 // away with this.renderOptions_.
734 var num
= this.attr_("labels").length
- 1;
736 var colors
= this.attr_('colors');
738 var sat
= this.attr_('colorSaturation') || 1.0;
739 var val
= this.attr_('colorValue') || 0.5;
740 var half
= Math
.ceil(num
/ 2);
741 for (var i
= 1; i
<= num
; i
++) {
742 if (!this.visibility()[i
-1]) continue;
743 // alternate colors for high contrast.
744 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
745 var hue
= (1.0 * idx
/ (1 + num
));
746 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
749 for (var i
= 0; i
< num
; i
++) {
750 if (!this.visibility()[i
]) continue;
751 var colorStr
= colors
[i
% colors
.length
];
752 this.colors_
.push(colorStr
);
756 // TODO(danvk): update this w/r
/t/ the
new options system
.
757 this.renderOptions_
.colorScheme
= this.colors_
;
758 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
759 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
760 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
764 * Return the list of colors. This is either the list of colors passed in the
765 * attributes, or the autogenerated list of rgb(r,g,b) strings.
766 * @return {Array<string>} The list of colors.
768 Dygraph
.prototype.getColors
= function() {
772 // The following functions are from quirksmode.org with a modification for Safari from
773 // http://blog.firetree.net/2005/07/04/javascript-find-position/
774 // http://www.quirksmode.org/js
/findpos
.html
775 Dygraph
.findPosX
= function(obj
) {
780 curleft
+= obj
.offsetLeft
;
781 if(!obj
.offsetParent
)
783 obj
= obj
.offsetParent
;
790 Dygraph
.findPosY
= function(obj
) {
795 curtop
+= obj
.offsetTop
;
796 if(!obj
.offsetParent
)
798 obj
= obj
.offsetParent
;
808 * Create the div that contains information on the selected point(s)
809 * This goes in the top right of the canvas, unless an external div has already
813 Dygraph
.prototype.createStatusMessage_
= function() {
814 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
815 if (userLabelsDiv
&& null != userLabelsDiv
816 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
817 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
819 if (!this.attr_("labelsDiv")) {
820 var divWidth
= this.attr_('labelsDivWidth');
822 "position": "absolute",
825 "width": divWidth
+ "px",
827 "left": (this.width_
- divWidth
- 2) + "px",
828 "background": "white",
830 "overflow": "hidden"};
831 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
832 var div
= document
.createElement("div");
833 for (var name
in messagestyle
) {
834 if (messagestyle
.hasOwnProperty(name
)) {
835 div
.style
[name
] = messagestyle
[name
];
838 this.graphDiv
.appendChild(div
);
839 this.attrs_
.labelsDiv
= div
;
844 * Position the labels div so that its right edge is flush with the right edge
845 * of the charting area.
847 Dygraph
.prototype.positionLabelsDiv_
= function() {
848 // Don't touch a user-specified labelsDiv.
849 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
851 var area
= this.plotter_
.area
;
852 var div
= this.attr_("labelsDiv");
853 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
857 * Create the text box to adjust the averaging period
860 Dygraph
.prototype.createRollInterface_
= function() {
861 // Create a roller if one doesn't exist already.
863 this.roller_
= document
.createElement("input");
864 this.roller_
.type
= "text";
865 this.roller_
.style
.display
= "none";
866 this.graphDiv
.appendChild(this.roller_
);
869 var display
= this.attr_('showRoller') ? 'block' : 'none';
871 var textAttr
= { "position": "absolute",
873 "top": (this.plotter_
.area
.h
- 25) + "px",
874 "left": (this.plotter_
.area
.x
+ 1) + "px",
877 this.roller_
.size
= "2";
878 this.roller_
.value
= this.rollPeriod_
;
879 for (var name
in textAttr
) {
880 if (textAttr
.hasOwnProperty(name
)) {
881 this.roller_
.style
[name
] = textAttr
[name
];
886 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
889 // These functions are taken from MochiKit.Signal
890 Dygraph
.pageX
= function(e
) {
892 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
895 var b
= document
.body
;
897 (de
.scrollLeft
|| b
.scrollLeft
) -
898 (de
.clientLeft
|| 0);
902 Dygraph
.pageY
= function(e
) {
904 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
907 var b
= document
.body
;
909 (de
.scrollTop
|| b
.scrollTop
) -
914 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
915 return Dygraph
.pageX(e
) - context
.px
918 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
919 return Dygraph
.pageY(e
) - context
.py
922 // Called in response to an interaction model operation that
923 // should start the default panning behavior.
925 // It's used in the default callback for "mousedown" operations.
926 // Custom interaction model builders can use it to provide the default
929 Dygraph
.startPan
= function(event
, g
, context
) {
930 context
.isPanning
= true;
931 var xRange
= g
.xAxisRange();
932 context
.dateRange
= xRange
[1] - xRange
[0];
933 context
.initialLeftmostDate
= xRange
[0];
934 context
.xUnitsPerPixel
= context
.dateRange
/ (g
.plotter_
.area
.w
- 1);
936 // Record the range of each y-axis at the start of the drag.
937 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
938 context
.is2DPan
= false;
939 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
940 var axis
= g
.axes_
[i
];
941 var yRange
= g
.yAxisRange(i
);
942 // TODO(konigsberg): These values should be in |context|.
943 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
945 axis
.initialTopValue
= Dygraph
.log10(yRange
[1]);
946 axis
.dragValueRange
= Dygraph
.log10(yRange
[1]) - Dygraph
.log10(yRange
[0]);
948 axis
.initialTopValue
= yRange
[1];
949 axis
.dragValueRange
= yRange
[1] - yRange
[0];
951 axis
.unitsPerPixel
= axis
.dragValueRange
/ (g
.plotter_
.area
.h
- 1);
953 // While calculating axes, set 2dpan.
954 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
958 // Called in response to an interaction model operation that
959 // responds to an event that pans the view.
961 // It's used in the default callback for "mousemove" operations.
962 // Custom interaction model builders can use it to provide the default
965 Dygraph
.movePan
= function(event
, g
, context
) {
966 context
.dragEndX
= g
.dragGetX_(event
, context
);
967 context
.dragEndY
= g
.dragGetY_(event
, context
);
969 var minDate
= context
.initialLeftmostDate
-
970 (context
.dragEndX
- context
.dragStartX
) * context
.xUnitsPerPixel
;
971 var maxDate
= minDate
+ context
.dateRange
;
972 g
.dateWindow_
= [minDate
, maxDate
];
974 // y-axis scaling is automatic unless this is a full 2D pan.
975 if (context
.is2DPan
) {
976 // Adjust each axis appropriately.
977 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
978 var axis
= g
.axes_
[i
];
980 var pixelsDragged
= context
.dragEndY
- context
.dragStartY
;
981 var unitsDragged
= pixelsDragged
* axis
.unitsPerPixel
;
983 // In log scale, maxValue and minValue are the logs of those values.
984 var maxValue
= axis
.initialTopValue
+ unitsDragged
;
985 var minValue
= maxValue
- axis
.dragValueRange
;
987 axis
.valueWindow
= [ Math
.pow(Dygraph
.LOG_SCALE
, minValue
),
988 Math
.pow(Dygraph
.LOG_SCALE
, maxValue
) ];
990 axis
.valueWindow
= [ minValue
, maxValue
];
998 // Called in response to an interaction model operation that
999 // responds to an event that ends panning.
1001 // It's used in the default callback for "mouseup" operations.
1002 // Custom interaction model builders can use it to provide the default
1003 // panning behavior.
1005 Dygraph
.endPan
= function(event
, g
, context
) {
1006 // TODO(konigsberg): Clear the context data from the axis.
1007 // TODO(konigsberg): mouseup should just delete the
1008 // context object, and mousedown should create a new one.
1009 context
.isPanning
= false;
1010 context
.is2DPan
= false;
1011 context
.initialLeftmostDate
= null;
1012 context
.dateRange
= null;
1013 context
.valueRange
= null;
1016 // Called in response to an interaction model operation that
1017 // responds to an event that starts zooming.
1019 // It's used in the default callback for "mousedown" operations.
1020 // Custom interaction model builders can use it to provide the default
1021 // zooming behavior.
1023 Dygraph
.startZoom
= function(event
, g
, context
) {
1024 context
.isZooming
= true;
1027 // Called in response to an interaction model operation that
1028 // responds to an event that defines zoom boundaries.
1030 // It's used in the default callback for "mousemove" operations.
1031 // Custom interaction model builders can use it to provide the default
1032 // zooming behavior.
1034 Dygraph
.moveZoom
= function(event
, g
, context
) {
1035 context
.dragEndX
= g
.dragGetX_(event
, context
);
1036 context
.dragEndY
= g
.dragGetY_(event
, context
);
1038 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
1039 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
1041 // drag direction threshold for y axis is twice as large as x axis
1042 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
1045 context
.dragDirection
,
1050 context
.prevDragDirection
,
1054 context
.prevEndX
= context
.dragEndX
;
1055 context
.prevEndY
= context
.dragEndY
;
1056 context
.prevDragDirection
= context
.dragDirection
;
1059 // Called in response to an interaction model operation that
1060 // responds to an event that performs a zoom based on previously defined
1063 // It's used in the default callback for "mouseup" operations.
1064 // Custom interaction model builders can use it to provide the default
1065 // zooming behavior.
1067 Dygraph
.endZoom
= function(event
, g
, context
) {
1068 context
.isZooming
= false;
1069 context
.dragEndX
= g
.dragGetX_(event
, context
);
1070 context
.dragEndY
= g
.dragGetY_(event
, context
);
1071 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
1072 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
1074 if (regionWidth
< 2 && regionHeight
< 2 &&
1075 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
1076 // TODO(danvk): pass along more info about the points, e.g. 'x'
1077 if (g
.attr_('clickCallback') != null) {
1078 g
.attr_('clickCallback')(event
, g
.lastx_
, g
.selPoints_
);
1080 if (g
.attr_('pointClickCallback')) {
1081 // check if the click was on a particular point.
1082 var closestIdx
= -1;
1083 var closestDistance
= 0;
1084 for (var i
= 0; i
< g
.selPoints_
.length
; i
++) {
1085 var p
= g
.selPoints_
[i
];
1086 var distance
= Math
.pow(p
.canvasx
- context
.dragEndX
, 2) +
1087 Math
.pow(p
.canvasy
- context
.dragEndY
, 2);
1088 if (closestIdx
== -1 || distance
< closestDistance
) {
1089 closestDistance
= distance
;
1094 // Allow any click within two pixels of the dot.
1095 var radius
= g
.attr_('highlightCircleSize') + 2;
1096 if (closestDistance
<= 5 * 5) {
1097 g
.attr_('pointClickCallback')(event
, g
.selPoints_
[closestIdx
]);
1102 if (regionWidth
>= 10 && context
.dragDirection
== Dygraph
.HORIZONTAL
) {
1103 g
.doZoomX_(Math
.min(context
.dragStartX
, context
.dragEndX
),
1104 Math
.max(context
.dragStartX
, context
.dragEndX
));
1105 } else if (regionHeight
>= 10 && context
.dragDirection
== Dygraph
.VERTICAL
) {
1106 g
.doZoomY_(Math
.min(context
.dragStartY
, context
.dragEndY
),
1107 Math
.max(context
.dragStartY
, context
.dragEndY
));
1109 g
.canvas_
.getContext("2d").clearRect(0, 0,
1113 context
.dragStartX
= null;
1114 context
.dragStartY
= null;
1117 Dygraph
.defaultInteractionModel
= {
1118 // Track the beginning of drag events
1119 mousedown
: function(event
, g
, context
) {
1120 context
.initializeMouseDown(event
, g
, context
);
1122 if (event
.altKey
|| event
.shiftKey
) {
1123 Dygraph
.startPan(event
, g
, context
);
1125 Dygraph
.startZoom(event
, g
, context
);
1129 // Draw zoom rectangles when the mouse is down and the user moves around
1130 mousemove
: function(event
, g
, context
) {
1131 if (context
.isZooming
) {
1132 Dygraph
.moveZoom(event
, g
, context
);
1133 } else if (context
.isPanning
) {
1134 Dygraph
.movePan(event
, g
, context
);
1138 mouseup
: function(event
, g
, context
) {
1139 if (context
.isZooming
) {
1140 Dygraph
.endZoom(event
, g
, context
);
1141 } else if (context
.isPanning
) {
1142 Dygraph
.endPan(event
, g
, context
);
1146 // Temporarily cancel the dragging event when the mouse leaves the graph
1147 mouseout
: function(event
, g
, context
) {
1148 if (context
.isZooming
) {
1149 context
.dragEndX
= null;
1150 context
.dragEndY
= null;
1154 // Disable zooming out if panning.
1155 dblclick
: function(event
, g
, context
) {
1156 if (event
.altKey
|| event
.shiftKey
) {
1159 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1160 // friendlier to public use.
1165 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.defaultInteractionModel
;
1168 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1172 Dygraph
.prototype.createDragInterface_
= function() {
1174 // Tracks whether the mouse is down right now
1176 isPanning
: false, // is this drag part of a pan?
1177 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1182 dragDirection
: null,
1185 prevDragDirection
: null,
1187 // The value on the left side of the graph when a pan operation starts.
1188 initialLeftmostDate
: null,
1190 // The number of units each pixel spans. (This won't be valid for log
1192 xUnitsPerPixel
: null,
1194 // TODO(danvk): update this comment
1195 // The range in second/value units that the viewport encompasses during a
1196 // panning operation.
1199 // Utility function to convert page-wide coordinates to canvas coords
1203 initializeMouseDown
: function(event
, g
, context
) {
1204 // prevents mouse drags from selecting page text.
1205 if (event
.preventDefault
) {
1206 event
.preventDefault(); // Firefox, Chrome, etc.
1208 event
.returnValue
= false; // IE
1209 event
.cancelBubble
= true;
1212 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1213 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1214 context
.dragStartX
= g
.dragGetX_(event
, context
);
1215 context
.dragStartY
= g
.dragGetY_(event
, context
);
1219 var interactionModel
= this.attr_("interactionModel");
1221 // Self is the graph.
1224 // Function that binds the graph and context to the handler.
1225 var bindHandler
= function(handler
) {
1226 return function(event
) {
1227 handler(event
, self
, context
);
1231 for (var eventName
in interactionModel
) {
1232 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1233 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1234 bindHandler(interactionModel
[eventName
]));
1237 // If the user releases the mouse button during a drag, but not over the
1238 // canvas, then it doesn't count as a zooming action.
1239 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1240 if (context
.isZooming
|| context
.isPanning
) {
1241 context
.isZooming
= false;
1242 context
.dragStartX
= null;
1243 context
.dragStartY
= null;
1246 if (context
.isPanning
) {
1247 context
.isPanning
= false;
1248 context
.draggingDate
= null;
1249 context
.dateRange
= null;
1250 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1251 delete self
.axes_
[i
].draggingValue
;
1252 delete self
.axes_
[i
].dragValueRange
;
1259 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1260 * up any previous zoom rectangles that were drawn. This could be optimized to
1261 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1264 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1265 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1266 * @param {Number} startX The X position where the drag started, in canvas
1268 * @param {Number} endX The current X position of the drag, in canvas coords.
1269 * @param {Number} startY The Y position where the drag started, in canvas
1271 * @param {Number} endY The current Y position of the drag, in canvas coords.
1272 * @param {Number} prevDirection the value of direction on the previous call to
1273 * this function. Used to avoid excess redrawing
1274 * @param {Number} prevEndX The value of endX on the previous call to this
1275 * function. Used to avoid excess redrawing
1276 * @param {Number} prevEndY The value of endY on the previous call to this
1277 * function. Used to avoid excess redrawing
1280 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1281 prevDirection
, prevEndX
, prevEndY
) {
1282 var ctx
= this.canvas_
.getContext("2d");
1284 // Clean up from the previous rect if necessary
1285 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1286 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1287 Math
.abs(startX
- prevEndX
), this.height_
);
1288 } else if (prevDirection
== Dygraph
.VERTICAL
){
1289 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1290 this.width_
, Math
.abs(startY
- prevEndY
));
1293 // Draw a light-grey rectangle to show the new viewing area
1294 if (direction
== Dygraph
.HORIZONTAL
) {
1295 if (endX
&& startX
) {
1296 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1297 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1298 Math
.abs(endX
- startX
), this.height_
);
1301 if (direction
== Dygraph
.VERTICAL
) {
1302 if (endY
&& startY
) {
1303 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1304 ctx
.fillRect(0, Math
.min(startY
, endY
),
1305 this.width_
, Math
.abs(endY
- startY
));
1311 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1312 * the canvas. The exact zoom window may be slightly larger if there are no data
1313 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1314 * which accepts dates that match the raw data. This function redraws the graph.
1316 * @param {Number} lowX The leftmost pixel value that should be visible.
1317 * @param {Number} highX The rightmost pixel value that should be visible.
1320 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1321 // Find the earliest and latest dates contained in this canvasx range.
1322 // Convert the call to date ranges of the raw data.
1323 var minDate
= this.toDataXCoord(lowX
);
1324 var maxDate
= this.toDataXCoord(highX
);
1325 this.doZoomXDates_(minDate
, maxDate
);
1329 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1330 * method with doZoomX which accepts pixel coordinates. This function redraws
1333 * @param {Number} minDate The minimum date that should be visible.
1334 * @param {Number} maxDate The maximum date that should be visible.
1337 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1338 this.dateWindow_
= [minDate
, maxDate
];
1339 this.zoomed_x_
= true;
1341 if (this.attr_("zoomCallback")) {
1342 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1347 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1348 * the canvas. This function redraws the graph.
1350 * @param {Number} lowY The topmost pixel value that should be visible.
1351 * @param {Number} highY The lowest pixel value that should be visible.
1354 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1355 // Find the highest and lowest values in pixel range for each axis.
1356 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1357 // This is because pixels increase as you go down on the screen, whereas data
1358 // coordinates increase as you go up the screen.
1359 var valueRanges
= [];
1360 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1361 var hi
= this.toDataYCoord(lowY
, i
);
1362 var low
= this.toDataYCoord(highY
, i
);
1363 this.axes_
[i
].valueWindow
= [low
, hi
];
1364 valueRanges
.push([low
, hi
]);
1367 this.zoomed_y_
= true;
1369 if (this.attr_("zoomCallback")) {
1370 var xRange
= this.xAxisRange();
1371 var yRange
= this.yAxisRange();
1372 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1377 * Reset the zoom to the original view coordinates. This is the same as
1378 * double-clicking on the graph.
1382 Dygraph
.prototype.doUnzoom_
= function() {
1384 if (this.dateWindow_
!= null) {
1386 this.dateWindow_
= null;
1389 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1390 if (this.axes_
[i
].valueWindow
!= null) {
1392 delete this.axes_
[i
].valueWindow
;
1397 // Putting the drawing operation before the callback because it resets
1399 this.zoomed_x_
= false;
1400 this.zoomed_y_
= false;
1402 if (this.attr_("zoomCallback")) {
1403 var minDate
= this.rawData_
[0][0];
1404 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1405 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1411 * When the mouse moves in the canvas, display information about a nearby data
1412 * point and draw dots over those points in the data series. This function
1413 * takes care of cleanup of previously-drawn dots.
1414 * @param {Object} event The mousemove event from the browser.
1417 Dygraph
.prototype.mouseMove_
= function(event
) {
1418 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1419 var points
= this.layout_
.points
;
1424 // Loop through all the points and find the date nearest to our current
1426 var minDist
= 1e+100;
1428 for (var i
= 0; i
< points
.length
; i
++) {
1429 var point
= points
[i
];
1430 if (point
== null) continue;
1431 var dist
= Math
.abs(point
.canvasx
- canvasx
);
1432 if (dist
> minDist
) continue;
1436 if (idx
>= 0) lastx
= points
[idx
].xval
;
1438 // Extract the points we've selected
1439 this.selPoints_
= [];
1440 var l
= points
.length
;
1441 if (!this.attr_("stackedGraph")) {
1442 for (var i
= 0; i
< l
; i
++) {
1443 if (points
[i
].xval
== lastx
) {
1444 this.selPoints_
.push(points
[i
]);
1448 // Need to 'unstack' points starting from the bottom
1449 var cumulative_sum
= 0;
1450 for (var i
= l
- 1; i
>= 0; i
--) {
1451 if (points
[i
].xval
== lastx
) {
1452 var p
= {}; // Clone the point since we modify it
1453 for (var k
in points
[i
]) {
1454 p
[k
] = points
[i
][k
];
1456 p
.yval
-= cumulative_sum
;
1457 cumulative_sum
+= p
.yval
;
1458 this.selPoints_
.push(p
);
1461 this.selPoints_
.reverse();
1464 if (this.attr_("highlightCallback")) {
1465 var px
= this.lastx_
;
1466 if (px
!== null && lastx
!= px
) {
1467 // only fire if the selected point has changed.
1468 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1472 // Save last x position for callbacks.
1473 this.lastx_
= lastx
;
1475 this.updateSelection_();
1479 * Transforms layout_.points index into data row number.
1480 * @param int layout_.points index
1481 * @return int row number, or -1 if none could be found.
1484 Dygraph
.prototype.idxToRow_
= function(idx
) {
1485 if (idx
< 0) return -1;
1487 for (var i
in this.layout_
.datasets
) {
1488 if (idx
< this.layout_
.datasets
[i
].length
) {
1489 return this.boundaryIds_
[0][0]+idx
;
1491 idx
-= this.layout_
.datasets
[i
].length
;
1497 * Draw dots over the selectied points in the data series. This function
1498 * takes care of cleanup of previously-drawn dots.
1501 Dygraph
.prototype.updateSelection_
= function() {
1502 // Clear the previously drawn vertical, if there is one
1503 var ctx
= this.canvas_
.getContext("2d");
1504 if (this.previousVerticalX_
>= 0) {
1505 // Determine the maximum highlight circle size.
1506 var maxCircleSize
= 0;
1507 var labels
= this.attr_('labels');
1508 for (var i
= 1; i
< labels
.length
; i
++) {
1509 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1510 if (r
> maxCircleSize
) maxCircleSize
= r
;
1512 var px
= this.previousVerticalX_
;
1513 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1514 2 * maxCircleSize
+ 2, this.height_
);
1517 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1519 if (this.selPoints_
.length
> 0) {
1520 var canvasx
= this.selPoints_
[0].canvasx
;
1522 // Set the status message to indicate the selected point(s)
1523 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1524 var fmtFunc
= this.attr_('yValueFormatter');
1525 var clen
= this.colors_
.length
;
1527 if (this.attr_('showLabelsOnHighlight')) {
1528 // Set the status message to indicate the selected point(s)
1529 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1530 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1531 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1532 if (this.attr_("labelsSeparateLines")) {
1535 var point
= this.selPoints_
[i
];
1536 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1537 var yval
= fmtFunc(point
.yval
);
1538 replace
+= " <b><font color='" + c
.toHex() + "'>"
1539 + point
.name
+ "</font></b>:"
1543 this.attr_("labelsDiv").innerHTML
= replace
;
1546 // Draw colored circles over the center of each selected point
1548 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1549 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1551 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1553 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1554 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1555 0, 2 * Math
.PI
, false);
1560 this.previousVerticalX_
= canvasx
;
1565 * Set manually set selected dots, and display information about them
1566 * @param int row number that should by highlighted
1567 * false value clears the selection
1570 Dygraph
.prototype.setSelection
= function(row
) {
1571 // Extract the points we've selected
1572 this.selPoints_
= [];
1575 if (row
!== false) {
1576 row
= row
-this.boundaryIds_
[0][0];
1579 if (row
!== false && row
>= 0) {
1580 for (var i
in this.layout_
.datasets
) {
1581 if (row
< this.layout_
.datasets
[i
].length
) {
1582 var point
= this.layout_
.points
[pos
+row
];
1584 if (this.attr_("stackedGraph")) {
1585 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1588 this.selPoints_
.push(point
);
1590 pos
+= this.layout_
.datasets
[i
].length
;
1594 if (this.selPoints_
.length
) {
1595 this.lastx_
= this.selPoints_
[0].xval
;
1596 this.updateSelection_();
1599 this.clearSelection();
1605 * The mouse has left the canvas. Clear out whatever artifacts remain
1606 * @param {Object} event the mouseout event from the browser.
1609 Dygraph
.prototype.mouseOut_
= function(event
) {
1610 if (this.attr_("unhighlightCallback")) {
1611 this.attr_("unhighlightCallback")(event
);
1614 if (this.attr_("hideOverlayOnMouseOut")) {
1615 this.clearSelection();
1620 * Remove all selection from the canvas
1623 Dygraph
.prototype.clearSelection
= function() {
1624 // Get rid of the overlay data
1625 var ctx
= this.canvas_
.getContext("2d");
1626 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1627 this.attr_("labelsDiv").innerHTML
= "";
1628 this.selPoints_
= [];
1633 * Returns the number of the currently selected row
1634 * @return int row number, of -1 if nothing is selected
1637 Dygraph
.prototype.getSelection
= function() {
1638 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1642 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1643 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1644 return row
+ this.boundaryIds_
[0][0];
1650 Dygraph
.zeropad
= function(x
) {
1651 if (x
< 10) return "0" + x
; else return "" + x
;
1655 * Return a string version of the hours, minutes and seconds portion of a date.
1656 * @param {Number} date The JavaScript date (ms since epoch)
1657 * @return {String} A time of the form "HH:MM:SS"
1660 Dygraph
.hmsString_
= function(date
) {
1661 var zeropad
= Dygraph
.zeropad
;
1662 var d
= new Date(date
);
1663 if (d
.getSeconds()) {
1664 return zeropad(d
.getHours()) + ":" +
1665 zeropad(d
.getMinutes()) + ":" +
1666 zeropad(d
.getSeconds());
1668 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1673 * Convert a JS date to a string appropriate to display on an axis that
1674 * is displaying values at the stated granularity.
1675 * @param {Date} date The date to format
1676 * @param {Number} granularity One of the Dygraph granularity constants
1677 * @return {String} The formatted date
1680 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1681 if (granularity
>= Dygraph
.DECADAL
) {
1682 return date
.strftime('%Y');
1683 } else if (granularity
>= Dygraph
.MONTHLY
) {
1684 return date
.strftime('%b %y');
1686 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1687 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1688 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1690 return Dygraph
.hmsString_(date
.getTime());
1696 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1697 * @param {Number} date The JavaScript date (ms since epoch)
1698 * @return {String} A date of the form "YYYY/MM/DD"
1701 Dygraph
.dateString_
= function(date
, self
) {
1702 var zeropad
= Dygraph
.zeropad
;
1703 var d
= new Date(date
);
1706 var year
= "" + d
.getFullYear();
1707 // Get a 0 padded month string
1708 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1709 // Get a 0 padded day string
1710 var day
= zeropad(d
.getDate());
1713 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1714 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1716 return year
+ "/" + month + "/" + day
+ ret
;
1720 * Round a number to the specified number of digits past the decimal point.
1721 * @param {Number} num The number to round
1722 * @param {Number} places The number of decimals to which to round
1723 * @return {Number} The rounded number
1726 Dygraph
.round_
= function(num
, places
) {
1727 var shift
= Math
.pow(10, places
);
1728 return Math
.round(num
* shift
)/shift
;
1732 * Fires when there's data available to be graphed.
1733 * @param {String} data Raw CSV data to be plotted
1736 Dygraph
.prototype.loadedEvent_
= function(data
) {
1737 this.rawData_
= this.parseCSV_(data
);
1741 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1742 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1743 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1746 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1749 Dygraph
.prototype.addXTicks_
= function() {
1750 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1751 var startDate
, endDate
;
1752 if (this.dateWindow_
) {
1753 startDate
= this.dateWindow_
[0];
1754 endDate
= this.dateWindow_
[1];
1756 startDate
= this.rawData_
[0][0];
1757 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1760 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1761 this.layout_
.updateOptions({xTicks
: xTicks
});
1764 // Time granularity enumeration
1765 Dygraph
.SECONDLY
= 0;
1766 Dygraph
.TWO_SECONDLY
= 1;
1767 Dygraph
.FIVE_SECONDLY
= 2;
1768 Dygraph
.TEN_SECONDLY
= 3;
1769 Dygraph
.THIRTY_SECONDLY
= 4;
1770 Dygraph
.MINUTELY
= 5;
1771 Dygraph
.TWO_MINUTELY
= 6;
1772 Dygraph
.FIVE_MINUTELY
= 7;
1773 Dygraph
.TEN_MINUTELY
= 8;
1774 Dygraph
.THIRTY_MINUTELY
= 9;
1775 Dygraph
.HOURLY
= 10;
1776 Dygraph
.TWO_HOURLY
= 11;
1777 Dygraph
.SIX_HOURLY
= 12;
1779 Dygraph
.WEEKLY
= 14;
1780 Dygraph
.MONTHLY
= 15;
1781 Dygraph
.QUARTERLY
= 16;
1782 Dygraph
.BIANNUAL
= 17;
1783 Dygraph
.ANNUAL
= 18;
1784 Dygraph
.DECADAL
= 19;
1785 Dygraph
.CENTENNIAL
= 20;
1786 Dygraph
.NUM_GRANULARITIES
= 21;
1788 Dygraph
.SHORT_SPACINGS
= [];
1789 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1790 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1791 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1792 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1793 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1794 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1795 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1796 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1797 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1798 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1799 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1800 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1801 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1802 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1803 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1807 // If we used this time granularity, how many ticks would there be?
1808 // This is only an approximation, but it's generally good enough.
1810 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1811 if (granularity
< Dygraph
.MONTHLY
) {
1812 // Generate one tick mark for every fixed interval of time.
1813 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1814 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1816 var year_mod
= 1; // e.g. to only print one point every 10 years.
1817 var num_months
= 12;
1818 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1819 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1820 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1821 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1822 if (granularity
== Dygraph
.CENTENNIAL
) { num_months
= 1; year_mod
= 100; }
1824 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1825 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1826 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1832 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1833 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1835 // Returns an array containing {v: millis, label: label} dictionaries.
1837 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1838 var formatter
= this.attr_("xAxisLabelFormatter");
1840 if (granularity
< Dygraph
.MONTHLY
) {
1841 // Generate one tick mark for every fixed interval of time.
1842 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1843 var format
= '%d%b'; // e.g. "1Jan"
1845 // Find a time less than start_time which occurs on a "nice" time boundary
1846 // for this granularity.
1847 var g
= spacing
/ 1000;
1848 var d
= new Date(start_time
);
1849 if (g
<= 60) { // seconds
1850 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1854 if (g
<= 60) { // minutes
1855 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1860 if (g
<= 24) { // days
1861 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1866 if (g
== 7) { // one week
1867 d
.setDate(d
.getDate() - d
.getDay());
1872 start_time
= d
.getTime();
1874 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1875 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1878 // Display a tick mark on the first of a set of months of each year.
1879 // Years get a tick mark iff y % year_mod == 0. This is useful for
1880 // displaying a tick mark once every 10 years, say, on long time scales.
1882 var year_mod
= 1; // e.g. to only print one point every 10 years.
1884 if (granularity
== Dygraph
.MONTHLY
) {
1885 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1886 } else if (granularity
== Dygraph
.QUARTERLY
) {
1887 months
= [ 0, 3, 6, 9 ];
1888 } else if (granularity
== Dygraph
.BIANNUAL
) {
1890 } else if (granularity
== Dygraph
.ANNUAL
) {
1892 } else if (granularity
== Dygraph
.DECADAL
) {
1895 } else if (granularity
== Dygraph
.CENTENNIAL
) {
1899 this.warn("Span of dates is too long");
1902 var start_year
= new Date(start_time
).getFullYear();
1903 var end_year
= new Date(end_time
).getFullYear();
1904 var zeropad
= Dygraph
.zeropad
;
1905 for (var i
= start_year
; i
<= end_year
; i
++) {
1906 if (i
% year_mod
!= 0) continue;
1907 for (var j
= 0; j
< months
.length
; j
++) {
1908 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1909 var t
= Date
.parse(date_str
);
1910 if (t
< start_time
|| t
> end_time
) continue;
1911 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1921 * Add ticks to the x-axis based on a date range.
1922 * @param {Number} startDate Start of the date window (millis since epoch)
1923 * @param {Number} endDate End of the date window (millis since epoch)
1924 * @return {Array.<Object>} Array of {label, value} tuples.
1927 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1929 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1930 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1931 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1938 return self
.GetXAxis(startDate
, endDate
, chosen
);
1940 // TODO(danvk): signal error.
1944 // This is a list of human-friendly values at which to show tick marks on a log
1945 // scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
1946 // ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
1947 // NOTE: this assumes that Dygraph.LOG_SCALE = 10.
1948 Dygraph
.PREFERRED_LOG_TICK_VALUES
= function() {
1950 for (var power
= -39; power
<= 39; power
++) {
1951 var range
= Math
.pow(10, power
);
1952 for (var mult
= 1; mult
<= 9; mult
++) {
1953 var val
= range
* mult
;
1960 // val is the value to search for
1961 // arry is the value over which to search
1962 // if abs > 0, find the lowest entry greater than val
1963 // if abs < 0, find the highest entry less than val
1964 // if abs == 0, find the entry that equals val.
1965 // Currently does not work when val is outside the range of arry's values.
1966 Dygraph
.binarySearch
= function(val
, arry
, abs
, low
, high
) {
1967 if (low
== null || high
== null) {
1969 high
= arry
.length
- 1;
1977 var validIndex
= function(idx
) {
1978 return idx
>= 0 && idx
< arry
.length
;
1980 var mid
= parseInt((low
+ high
) / 2);
1981 var element
= arry
[mid
];
1982 if (element
== val
) {
1985 if (element
> val
) {
1987 // Accept if element > val, but also if prior element < val.
1989 if (validIndex(idx
) && arry
[idx
] < val
) {
1993 return Dygraph
.binarySearch(val
, arry
, abs
, low
, mid
- 1);
1995 if (element
< val
) {
1997 // Accept if element < val, but also if prior element > val.
1999 if (validIndex(idx
) && arry
[idx
] > val
) {
2003 return Dygraph
.binarySearch(val
, arry
, abs
, mid
+ 1, high
);
2008 * Add ticks when the x axis has numbers on it (instead of dates)
2009 * TODO(konigsberg): Update comment.
2011 * @param {Number} minV minimum value
2012 * @param {Number} maxV maximum value
2014 * @param {function} attribute accessor function.
2015 * @return {Array.<Object>} Array of {label, value} tuples.
2018 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
2019 var attr
= function(k
) {
2020 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
2021 return self
.attr_(k
);
2026 for (var i
= 0; i
< vals
.length
; i
++) {
2027 ticks
.push({v
: vals
[i
]});
2030 if (axis_props
&& attr("logscale")) {
2031 var pixelsPerTick
= attr('pixelsPerYLabel');
2032 // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
2033 var nTicks
= Math
.floor(self
.height_
/ pixelsPerTick
);
2034 var minIdx
= Dygraph
.binarySearch(minV
, Dygraph
.PREFERRED_LOG_TICK_VALUES
, 1);
2035 var maxIdx
= Dygraph
.binarySearch(maxV
, Dygraph
.PREFERRED_LOG_TICK_VALUES
, -1);
2040 maxIdx
= Dygraph
.PREFERRED_LOG_TICK_VALUES
.length
- 1;
2042 // Count the number of tick values would appear, if we can get at least
2043 // nTicks / 4 accept them
.
2044 var lastDisplayed
= null;
2045 if (maxIdx
- minIdx
>= nTicks
/ 4) {
2046 var axisId
= axis_props
.yAxisId
;
2047 for (var idx
= maxIdx
; idx
>= minIdx
; idx
--) {
2048 var tickValue
= Dygraph
.PREFERRED_LOG_TICK_VALUES
[idx
];
2049 var domCoord
= axis_props
.g
.toDomYCoord(tickValue
, axisId
);
2050 var tick
= { v
: tickValue
};
2051 if (lastDisplayed
== null) {
2053 tickValue
: tickValue
,
2057 if (domCoord
- lastDisplayed
.domCoord
>= pixelsPerTick
) {
2059 tickValue
: tickValue
,
2068 // Since we went in backwards order.
2073 // ticks.length won't be 0 if the log scale function finds values to insert.
2074 if (ticks
.length
== 0) {
2076 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
2077 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
2078 // The first spacing greater than pixelsPerYLabel is what we use.
2079 // TODO(danvk): version that works on a log scale.
2080 if (attr("labelsKMG2")) {
2081 var mults
= [1, 2, 4, 8];
2083 var mults
= [1, 2, 5];
2085 var scale
, low_val
, high_val
, nTicks
;
2086 // TODO(danvk): make it possible to set this for x- and y-axes independently.
2087 var pixelsPerTick
= attr('pixelsPerYLabel');
2088 for (var i
= -10; i
< 50; i
++) {
2089 if (attr("labelsKMG2")) {
2090 var base_scale
= Math
.pow(16, i
);
2092 var base_scale
= Math
.pow(10, i
);
2094 for (var j
= 0; j
< mults
.length
; j
++) {
2095 scale
= base_scale
* mults
[j
];
2096 low_val
= Math
.floor(minV
/ scale
) * scale
;
2097 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
2098 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
2099 var spacing
= self
.height_
/ nTicks
;
2100 // wish I could break out of both loops at once...
2101 if (spacing
> pixelsPerTick
) break;
2103 if (spacing
> pixelsPerTick
) break;
2106 // Construct the set of ticks.
2107 // Allow reverse y-axis if it's explicitly requested.
2108 if (low_val
> high_val
) scale
*= -1;
2109 for (var i
= 0; i
< nTicks
; i
++) {
2110 var tickV
= low_val
+ i
* scale
;
2111 ticks
.push( {v
: tickV
} );
2116 // Add formatted labels to the ticks.
2119 if (attr("labelsKMB")) {
2121 k_labels
= [ "K", "M", "B", "T" ];
2123 if (attr("labelsKMG2")) {
2124 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2126 k_labels
= [ "k", "M", "G", "T" ];
2128 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
2130 // Add labels to the ticks.
2131 for (var i
= 0; i
< ticks
.length
; i
++) {
2132 if (ticks
[i
].label
== null) {
2133 var tickV
= ticks
[i
].v
;
2134 var absTickV
= Math
.abs(tickV
);
2136 if (formatter
!= undefined
) {
2137 label
= formatter(tickV
);
2139 label
= Dygraph
.round_(tickV
, 2);
2141 if (k_labels
.length
) {
2142 // Round up to an appropriate unit.
2144 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
2145 if (absTickV
>= n
) {
2146 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
2151 ticks
[i
].label
= label
;
2157 // Computes the range of the data series (including confidence intervals).
2158 // series is either [ [x1, y1], [x2, y2], ... ] or
2159 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2160 // Returns [low, high]
2161 Dygraph
.prototype.extremeValues_
= function(series
) {
2162 var minY
= null, maxY
= null;
2164 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2166 // With custom bars, maxY is the max of the high values.
2167 for (var j
= 0; j
< series
.length
; j
++) {
2168 var y
= series
[j
][1][0];
2170 var low
= y
- series
[j
][1][1];
2171 var high
= y
+ series
[j
][1][2];
2172 if (low
> y
) low
= y
; // this can happen with custom bars,
2173 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
2174 if (maxY
== null || high
> maxY
) {
2177 if (minY
== null || low
< minY
) {
2182 for (var j
= 0; j
< series
.length
; j
++) {
2183 var y
= series
[j
][1];
2184 if (y
=== null || isNaN(y
)) continue;
2185 if (maxY
== null || y
> maxY
) {
2188 if (minY
== null || y
< minY
) {
2194 return [minY
, maxY
];
2198 * This function is called once when the chart's data is changed or the options
2199 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2200 * idea is that values derived from the chart's data can be computed here,
2201 * rather than every time the chart is drawn. This includes things like the
2202 * number of axes, rolling averages, etc.
2204 Dygraph
.prototype.predraw_
= function() {
2205 // TODO(danvk): move more computations out of drawGraph_ and into here.
2206 this.computeYAxes_();
2208 // Create a new plotter.
2209 if (this.plotter_
) this.plotter_
.clear();
2210 this.plotter_
= new DygraphCanvasRenderer(this,
2211 this.hidden_
, this.layout_
,
2212 this.renderOptions_
);
2214 // The roller sits in the bottom left corner of the chart. We don't know where
2215 // this will be until the options are available, so it's positioned here.
2216 this.createRollInterface_();
2218 // Same thing applies for the labelsDiv. It's right edge should be flush with
2219 // the right edge of the charting area (which may not be the same as the right
2220 // edge of the div, if we have two y-axes.
2221 this.positionLabelsDiv_();
2223 // If the data or options have changed, then we'd better redraw.
2228 * Update the graph with new data. This method is called when the viewing area
2229 * has changed. If the underlying data or options have changed, predraw_ will
2230 * be called before drawGraph_ is called.
2233 Dygraph
.prototype.drawGraph_
= function() {
2234 var data
= this.rawData_
;
2236 // This is used to set the second parameter to drawCallback, below.
2237 var is_initial_draw
= this.is_initial_draw_
;
2238 this.is_initial_draw_
= false;
2240 var minY
= null, maxY
= null;
2241 this.layout_
.removeAllDatasets();
2243 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
2245 // Loop over the fields (series). Go from the last to the first,
2246 // because if they're stacked that's how we accumulate the values.
2248 var cumulative_y
= []; // For stacked series.
2251 var extremes
= {}; // series name -> [low, high]
2253 // Loop over all fields and create datasets
2254 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
2255 if (!this.visibility()[i
- 1]) continue;
2257 var seriesName
= this.attr_("labels")[i
];
2258 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
2259 var logScale
= this.attr_('logscale', i
);
2262 for (var j
= 0; j
< data
.length
; j
++) {
2263 var date
= data
[j
][0];
2264 var point
= data
[j
][i
];
2266 // On the log scale, points less than zero do not exist.
2267 // This will create a gap in the chart. Note that this ignores
2268 // connectSeparatedPoints.
2272 series
.push([date
, point
]);
2274 if (point
!= null || !connectSeparatedPoints
) {
2275 series
.push([date
, point
]);
2280 // TODO(danvk): move this into predraw_. It's insane to do it here.
2281 series
= this.rollingAverage(series
, this.rollPeriod_
);
2283 // Prune down to the desired range, if necessary (for zooming)
2284 // Because there can be lines going to points outside of the visible area,
2285 // we actually prune to visible points, plus one on either side.
2286 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2287 if (this.dateWindow_
) {
2288 var low
= this.dateWindow_
[0];
2289 var high
= this.dateWindow_
[1];
2291 // TODO(danvk): do binary search instead of linear search.
2292 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2293 var firstIdx
= null, lastIdx
= null;
2294 for (var k
= 0; k
< series
.length
; k
++) {
2295 if (series
[k
][0] >= low
&& firstIdx
=== null) {
2298 if (series
[k
][0] <= high
) {
2302 if (firstIdx
=== null) firstIdx
= 0;
2303 if (firstIdx
> 0) firstIdx
--;
2304 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2305 if (lastIdx
< series
.length
- 1) lastIdx
++;
2306 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
2307 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
2308 pruned
.push(series
[k
]);
2312 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
2315 var seriesExtremes
= this.extremeValues_(series
);
2318 for (var j
=0; j
<series
.length
; j
++) {
2319 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
2322 } else if (this.attr_("stackedGraph")) {
2323 var l
= series
.length
;
2325 for (var j
= 0; j
< l
; j
++) {
2326 // If one data set has a NaN, let all subsequent stacked
2327 // sets inherit the NaN -- only start at 0 for the first set.
2328 var x
= series
[j
][0];
2329 if (cumulative_y
[x
] === undefined
) {
2330 cumulative_y
[x
] = 0;
2333 actual_y
= series
[j
][1];
2334 cumulative_y
[x
] += actual_y
;
2336 series
[j
] = [x
, cumulative_y
[x
]]
2338 if (cumulative_y
[x
] > seriesExtremes
[1]) {
2339 seriesExtremes
[1] = cumulative_y
[x
];
2341 if (cumulative_y
[x
] < seriesExtremes
[0]) {
2342 seriesExtremes
[0] = cumulative_y
[x
];
2346 extremes
[seriesName
] = seriesExtremes
;
2348 datasets
[i
] = series
;
2351 for (var i
= 1; i
< datasets
.length
; i
++) {
2352 if (!this.visibility()[i
- 1]) continue;
2353 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
2356 if (datasets
.length
> 0) {
2357 // TODO(danvk): this method doesn't need to return anything.
2358 var out
= this.computeYAxisRanges_(extremes
);
2360 var seriesToAxisMap
= out
[1];
2361 this.layout_
.updateOptions( { yAxes
: axes
,
2362 seriesToAxisMap
: seriesToAxisMap
2367 // Save the X axis zoomed status as the updateOptions call will tend to set it errorneously
2368 var tmp_zoomed_x
= this.zoomed_x_
;
2369 // Tell PlotKit to use this new data and render itself
2370 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
2371 this.zoomed_x_
= tmp_zoomed_x
;
2372 this.layout_
.evaluateWithError();
2373 this.plotter_
.clear();
2374 this.plotter_
.render();
2375 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
2376 this.canvas_
.height
);
2378 if (this.attr_("drawCallback") !== null) {
2379 this.attr_("drawCallback")(this, is_initial_draw
);
2384 * Determine properties of the y-axes which are independent of the data
2385 * currently being displayed. This includes things like the number of axes and
2386 * the style of the axes. It does not include the range of each axis and its
2388 * This fills in this.axes_ and this.seriesToAxisMap_.
2389 * axes_ = [ { options } ]
2390 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2391 * indices are into the axes_ array.
2393 Dygraph
.prototype.computeYAxes_
= function() {
2395 if (this.axes_
!= undefined
) {
2396 // Preserve valueWindow settings.
2398 for (var index
= 0; index
< this.axes_
.length
; index
++) {
2399 valueWindows
.push(this.axes_
[index
].valueWindow
);
2403 this.axes_
= [{ yAxisId
: 0, g
: this }]; // always have at least one y-axis.
2404 this.seriesToAxisMap_
= {};
2406 // Get a list of series names.
2407 var labels
= this.attr_("labels");
2409 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2411 // all options which could be applied per-axis:
2419 'axisLabelFontSize',
2424 // Copy global axis options over to the first axis.
2425 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2426 var k
= axisOptions
[i
];
2427 var v
= this.attr_(k
);
2428 if (v
) this.axes_
[0][k
] = v
;
2431 // Go through once and add all the axes.
2432 for (var seriesName
in series
) {
2433 if (!series
.hasOwnProperty(seriesName
)) continue;
2434 var axis
= this.attr_("axis", seriesName
);
2436 this.seriesToAxisMap_
[seriesName
] = 0;
2439 if (typeof(axis
) == 'object') {
2440 // Add a new axis, making a copy of its per-axis options.
2442 Dygraph
.update(opts
, this.axes_
[0]);
2443 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2444 var yAxisId
= this.axes_
.length
;
2445 opts
.yAxisId
= yAxisId
;
2447 Dygraph
.update(opts
, axis
);
2448 this.axes_
.push(opts
);
2449 this.seriesToAxisMap_
[seriesName
] = yAxisId
;
2453 // Go through one more time and assign series to an axis defined by another
2454 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2455 for (var seriesName
in series
) {
2456 if (!series
.hasOwnProperty(seriesName
)) continue;
2457 var axis
= this.attr_("axis", seriesName
);
2458 if (typeof(axis
) == 'string') {
2459 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2460 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2461 "series " + axis
+ ", which does not define its own axis.");
2464 var idx
= this.seriesToAxisMap_
[axis
];
2465 this.seriesToAxisMap_
[seriesName
] = idx
;
2469 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2470 // this last so that hiding the first series doesn't destroy the axis
2471 // properties of the primary axis.
2472 var seriesToAxisFiltered
= {};
2473 var vis
= this.visibility();
2474 for (var i
= 1; i
< labels
.length
; i
++) {
2476 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2478 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2480 if (valueWindows
!= undefined
) {
2481 // Restore valueWindow settings.
2482 for (var index
= 0; index
< valueWindows
.length
; index
++) {
2483 this.axes_
[index
].valueWindow
= valueWindows
[index
];
2489 * Returns the number of y-axes on the chart.
2490 * @return {Number} the number of axes.
2492 Dygraph
.prototype.numAxes
= function() {
2494 for (var series
in this.seriesToAxisMap_
) {
2495 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2496 var idx
= this.seriesToAxisMap_
[series
];
2497 if (idx
> last_axis
) last_axis
= idx
;
2499 return 1 + last_axis
;
2503 * Determine the value range and tick marks for each axis.
2504 * @param {Object} extremes A mapping from seriesName -> [low, high]
2505 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2507 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2508 // Build a map from axis number -> [list of series names]
2509 var seriesForAxis
= [];
2510 for (var series
in this.seriesToAxisMap_
) {
2511 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2512 var idx
= this.seriesToAxisMap_
[series
];
2513 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2514 seriesForAxis
[idx
].push(series
);
2517 // Compute extreme values, a span and tick marks for each axis.
2518 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2519 var axis
= this.axes_
[i
];
2520 if (axis
.valueWindow
) {
2521 // This is only set if the user has zoomed on the y-axis. It is never set
2522 // by a user. It takes precedence over axis.valueRange because, if you set
2523 // valueRange, you'd still expect to be able to pan.
2524 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2525 } else if (axis
.valueRange
) {
2526 // This is a user-set value range for this axis.
2527 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2529 // Calculate the extremes of extremes.
2530 var series
= seriesForAxis
[i
];
2531 var minY
= Infinity
; // extremes[series[0]][0];
2532 var maxY
= -Infinity
; // extremes[series[0]][1];
2533 for (var j
= 0; j
< series
.length
; j
++) {
2534 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2535 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2537 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2539 // Add some padding and round up to an integer to be human-friendly.
2540 var span
= maxY
- minY
;
2541 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2542 if (span
== 0) { span
= maxY
; }
2546 if (axis
.logscale
) {
2547 var maxAxisY
= maxY
+ 0.1 * span
;
2548 var minAxisY
= minY
;
2550 var maxAxisY
= maxY
+ 0.1 * span
;
2551 var minAxisY
= minY
- 0.1 * span
;
2553 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2554 if (!this.attr_("avoidMinZero")) {
2555 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2556 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2559 if (this.attr_("includeZero")) {
2560 if (maxY
< 0) maxAxisY
= 0;
2561 if (minY
> 0) minAxisY
= 0;
2565 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2568 // Add ticks. By default, all axes inherit the tick positions of the
2569 // primary axis. However, if an axis is specifically marked as having
2570 // independent ticks, then that is permissible as well.
2571 if (i
== 0 || axis
.independentTicks
) {
2573 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2574 axis
.computedValueRange
[1],
2578 var p_axis
= this.axes_
[0];
2579 var p_ticks
= p_axis
.ticks
;
2580 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2581 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2582 var tick_values
= [];
2583 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2584 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2585 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2586 tick_values
.push(y_val
);
2590 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2591 axis
.computedValueRange
[1],
2592 this, axis
, tick_values
);
2596 return [this.axes_
, this.seriesToAxisMap_
];
2600 * Calculates the rolling average of a data set.
2601 * If originalData is [label, val], rolls the average of those.
2602 * If originalData is [label, [, it's interpreted as [value, stddev]
2603 * and the roll is returned in the same form, with appropriately reduced
2604 * stddev for each value.
2605 * Note that this is where fractional input (i.e. '5/10') is converted into
2607 * @param {Array} originalData The data in the appropriate format (see above)
2608 * @param {Number} rollPeriod The number of days over which to average the data
2610 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2611 if (originalData
.length
< 2)
2612 return originalData
;
2613 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2614 var rollingData
= [];
2615 var sigma
= this.attr_("sigma");
2617 if (this.fractions_
) {
2619 var den
= 0; // numerator/denominator
2621 for (var i
= 0; i
< originalData
.length
; i
++) {
2622 num
+= originalData
[i
][1][0];
2623 den
+= originalData
[i
][1][1];
2624 if (i
- rollPeriod
>= 0) {
2625 num
-= originalData
[i
- rollPeriod
][1][0];
2626 den
-= originalData
[i
- rollPeriod
][1][1];
2629 var date
= originalData
[i
][0];
2630 var value
= den
? num
/ den
: 0.0;
2631 if (this.attr_("errorBars")) {
2632 if (this.wilsonInterval_
) {
2633 // For more details on this confidence interval, see:
2634 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2636 var p
= value
< 0 ? 0 : value
, n
= den
;
2637 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2638 var denom
= 1 + sigma
* sigma
/ den
;
2639 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2640 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2641 rollingData
[i
] = [date
,
2642 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2644 rollingData
[i
] = [date
, [0, 0, 0]];
2647 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2648 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2651 rollingData
[i
] = [date
, mult
* value
];
2654 } else if (this.attr_("customBars")) {
2659 for (var i
= 0; i
< originalData
.length
; i
++) {
2660 var data
= originalData
[i
][1];
2662 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2664 if (y
!= null && !isNaN(y
)) {
2670 if (i
- rollPeriod
>= 0) {
2671 var prev
= originalData
[i
- rollPeriod
];
2672 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2679 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2680 1.0 * (mid
- low
) / count
,
2681 1.0 * (high
- mid
) / count
]];
2684 // Calculate the rolling average for the first rollPeriod - 1 points where
2685 // there is not enough data to roll over the full number of days
2686 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2687 if (!this.attr_("errorBars")){
2688 if (rollPeriod
== 1) {
2689 return originalData
;
2692 for (var i
= 0; i
< originalData
.length
; i
++) {
2695 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2696 var y
= originalData
[j
][1];
2697 if (y
== null || isNaN(y
)) continue;
2699 sum
+= originalData
[j
][1];
2702 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2704 rollingData
[i
] = [originalData
[i
][0], null];
2709 for (var i
= 0; i
< originalData
.length
; i
++) {
2713 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2714 var y
= originalData
[j
][1][0];
2715 if (y
== null || isNaN(y
)) continue;
2717 sum
+= originalData
[j
][1][0];
2718 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2721 var stddev
= Math
.sqrt(variance
) / num_ok
;
2722 rollingData
[i
] = [originalData
[i
][0],
2723 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2725 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2735 * Parses a date, returning the number of milliseconds since epoch. This can be
2736 * passed in as an xValueParser in the Dygraph constructor.
2737 * TODO(danvk): enumerate formats that this understands.
2738 * @param {String} A date in YYYYMMDD format.
2739 * @return {Number} Milliseconds since epoch.
2742 Dygraph
.dateParser
= function(dateStr
, self
) {
2745 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2746 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2747 while (dateStrSlashed
.search("-") != -1) {
2748 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2750 d
= Date
.parse(dateStrSlashed
);
2751 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2752 // TODO(danvk): remove support for this format. It's confusing.
2753 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2754 + "/" + dateStr
.substr(6,2);
2755 d
= Date
.parse(dateStrSlashed
);
2757 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2758 // "2009/07/12 12:34:56"
2759 d
= Date
.parse(dateStr
);
2762 if (!d
|| isNaN(d
)) {
2763 self
.error("Couldn't parse " + dateStr
+ " as a date");
2769 * Detects the type of the str (date or numeric) and sets the various
2770 * formatting attributes in this.attrs_ based on this type.
2771 * @param {String} str An x value.
2774 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2776 if (str
.indexOf('-') > 0 ||
2777 str
.indexOf('/') >= 0 ||
2778 isNaN(parseFloat(str
))) {
2780 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2781 // TODO(danvk): remove support for this format.
2786 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2787 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2788 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2789 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2791 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2792 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2793 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2794 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2799 * Parses a string in a special csv format. We expect a csv file where each
2800 * line is a date point, and the first field in each line is the date string.
2801 * We also expect that all remaining fields represent series.
2802 * if the errorBars attribute is set, then interpret the fields as:
2803 * date, series1, stddev1, series2, stddev2, ...
2804 * @param {Array.<Object>} data See above.
2807 * @return Array.<Object> An array with one entry for each row. These entries
2808 * are an array of cells in that row. The first entry is the parsed x-value for
2809 * the row. The second, third, etc. are the y-values. These can take on one of
2810 * three forms, depending on the CSV and constructor parameters:
2812 * 2. [ value, stddev ]
2813 * 3. [ low value, center value, high value ]
2815 Dygraph
.prototype.parseCSV_
= function(data
) {
2817 var lines
= data
.split("\n");
2819 // Use the default delimiter or fall back to a tab if that makes sense.
2820 var delim
= this.attr_('delimiter');
2821 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2826 if (this.labelsFromCSV_
) {
2828 this.attrs_
.labels
= lines
[0].split(delim
);
2831 // Parse the x as a float or return null if it's not a number.
2832 var parseFloatOrNull
= function(x
) {
2833 var val
= parseFloat(x
);
2834 // isFinite() returns false for NaN and +/-Infinity
.
2835 return isFinite(val
) ? val
: null;
2839 var defaultParserSet
= false; // attempt to auto-detect x value type
2840 var expectedCols
= this.attr_("labels").length
;
2841 var outOfOrder
= false;
2842 for (var i
= start
; i
< lines
.length
; i
++) {
2843 var line
= lines
[i
];
2844 if (line
.length
== 0) continue; // skip blank lines
2845 if (line
[0] == '#') continue; // skip comment lines
2846 var inFields
= line
.split(delim
);
2847 if (inFields
.length
< 2) continue;
2850 if (!defaultParserSet
) {
2851 this.detectTypeFromString_(inFields
[0]);
2852 xParser
= this.attr_("xValueParser");
2853 defaultParserSet
= true;
2855 fields
[0] = xParser(inFields
[0], this);
2857 // If fractions are expected, parse the numbers as "A/B
"
2858 if (this.fractions_) {
2859 for (var j = 1; j < inFields.length; j++) {
2860 // TODO(danvk): figure out an appropriate way to flag parse errors.
2861 var vals = inFields[j].split("/");
2862 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2864 } else if (this.attr_("errorBars
")) {
2865 // If there are error bars, values are (value, stddev) pairs
2866 for (var j = 1; j < inFields.length; j += 2)
2867 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2868 parseFloatOrNull(inFields[j + 1])];
2869 } else if (this.attr_("customBars
")) {
2870 // Bars are a low;center;high tuple
2871 for (var j = 1; j < inFields.length; j++) {
2872 var vals = inFields[j].split(";");
2873 fields[j] = [ parseFloatOrNull(vals[0]),
2874 parseFloatOrNull(vals[1]),
2875 parseFloatOrNull(vals[2]) ];
2878 // Values are just numbers
2879 for (var j = 1; j < inFields.length; j++) {
2880 fields[j] = parseFloatOrNull(inFields[j]);
2883 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2888 if (fields.length != expectedCols) {
2889 this.error("Number of columns
in line
" + i + " (" + fields.length +
2890 ") does not agree
with number of
labels (" + expectedCols +
2896 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2897 ret.sort(function(a,b) { return a[0] - b[0] });
2904 * The user has provided their data as a pre-packaged JS array. If the x values
2905 * are numeric, this is the same as dygraphs' internal format. If the x values
2906 * are dates, we need to convert them from Date objects to ms since epoch.
2907 * @param {Array.<Object>} data
2908 * @return {Array.<Object>} data with numeric x values.
2910 Dygraph.prototype.parseArray_ = function(data) {
2911 // Peek at the first x value to see if it's numeric.
2912 if (data.length == 0) {
2913 this.error("Can
't plot empty data set");
2916 if (data[0].length == 0) {
2917 this.error("Data set cannot contain an empty row");
2921 if (this.attr_("labels") == null) {
2922 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2923 "in the options parameter");
2924 this.attrs_.labels = [ "X" ];
2925 for (var i = 1; i < data[0].length; i++) {
2926 this.attrs_.labels.push("Y" + i);
2930 if (Dygraph.isDateLike(data[0][0])) {
2931 // Some intelligent defaults for a date x-axis.
2932 this.attrs_.xValueFormatter = Dygraph.dateString_;
2933 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2934 this.attrs_.xTicker = Dygraph.dateTicker;
2936 // Assume they're all dates
.
2937 var parsedData
= Dygraph
.clone(data
);
2938 for (var i
= 0; i
< data
.length
; i
++) {
2939 if (parsedData
[i
].length
== 0) {
2940 this.error("Row " + (1 + i
) + " of data is empty");
2943 if (parsedData
[i
][0] == null
2944 || typeof(parsedData
[i
][0].getTime
) != 'function'
2945 || isNaN(parsedData
[i
][0].getTime())) {
2946 this.error("x value in row " + (1 + i
) + " is not a Date");
2949 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2953 // Some intelligent defaults for a numeric x-axis.
2954 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2955 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2961 * Parses a DataTable object from gviz.
2962 * The data is expected to have a first column that is either a date or a
2963 * number. All subsequent columns must be numbers. If there is a clear mismatch
2964 * between this.xValueParser_ and the type of the first column, it will be
2965 * fixed. Fills out rawData_.
2966 * @param {Array.<Object>} data See above.
2969 Dygraph
.prototype.parseDataTable_
= function(data
) {
2970 var cols
= data
.getNumberOfColumns();
2971 var rows
= data
.getNumberOfRows();
2973 var indepType
= data
.getColumnType(0);
2974 if (indepType
== 'date' || indepType
== 'datetime') {
2975 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2976 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2977 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2978 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2979 } else if (indepType
== 'number') {
2980 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2981 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2982 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2983 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2985 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2986 "column 1 of DataTable input (Got '" + indepType
+ "')");
2990 // Array of the column indices which contain data (and not annotations).
2992 var annotationCols
= {}; // data index -> [annotation cols]
2993 var hasAnnotations
= false;
2994 for (var i
= 1; i
< cols
; i
++) {
2995 var type
= data
.getColumnType(i
);
2996 if (type
== 'number') {
2998 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2999 // This is OK -- it's an annotation column.
3000 var dataIdx
= colIdx
[colIdx
.length
- 1];
3001 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
3002 annotationCols
[dataIdx
] = [i
];
3004 annotationCols
[dataIdx
].push(i
);
3006 hasAnnotations
= true;
3008 this.error("Only 'number' is supported as a dependent type with Gviz." +
3009 " 'string' is only supported if displayAnnotations is true");
3013 // Read column labels
3014 // TODO(danvk): add support back for errorBars
3015 var labels
= [data
.getColumnLabel(0)];
3016 for (var i
= 0; i
< colIdx
.length
; i
++) {
3017 labels
.push(data
.getColumnLabel(colIdx
[i
]));
3018 if (this.attr_("errorBars")) i
+= 1;
3020 this.attrs_
.labels
= labels
;
3021 cols
= labels
.length
;
3024 var outOfOrder
= false;
3025 var annotations
= [];
3026 for (var i
= 0; i
< rows
; i
++) {
3028 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
3029 data
.getValue(i
, 0) === null) {
3030 this.warn("Ignoring row " + i
+
3031 " of DataTable because of undefined or null first column.");
3035 if (indepType
== 'date' || indepType
== 'datetime') {
3036 row
.push(data
.getValue(i
, 0).getTime());
3038 row
.push(data
.getValue(i
, 0));
3040 if (!this.attr_("errorBars")) {
3041 for (var j
= 0; j
< colIdx
.length
; j
++) {
3042 var col
= colIdx
[j
];
3043 row
.push(data
.getValue(i
, col
));
3044 if (hasAnnotations
&&
3045 annotationCols
.hasOwnProperty(col
) &&
3046 data
.getValue(i
, annotationCols
[col
][0]) != null) {
3048 ann
.series
= data
.getColumnLabel(col
);
3050 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
3052 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
3053 if (k
) ann
.text
+= "\n";
3054 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
3056 annotations
.push(ann
);
3060 for (var j
= 0; j
< cols
- 1; j
++) {
3061 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
3064 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
3068 // Strip out infinities, which give dygraphs problems later on.
3069 for (var j
= 0; j
< row
.length
; j
++) {
3070 if (!isFinite(row
[j
])) row
[j
] = null;
3076 this.warn("DataTable is out of order; order it correctly to speed loading.");
3077 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
3079 this.rawData_
= ret
;
3081 if (annotations
.length
> 0) {
3082 this.setAnnotations(annotations
, true);
3086 // These functions are all based on MochiKit.
3087 Dygraph
.update
= function (self
, o
) {
3088 if (typeof(o
) != 'undefined' && o
!== null) {
3090 if (o
.hasOwnProperty(k
)) {
3098 Dygraph
.isArrayLike
= function (o
) {
3099 var typ
= typeof(o
);
3101 (typ
!= 'object' && !(typ
== 'function' &&
3102 typeof(o
.item
) == 'function')) ||
3104 typeof(o
.length
) != 'number' ||
3112 Dygraph
.isDateLike
= function (o
) {
3113 if (typeof(o
) != "object" || o
=== null ||
3114 typeof(o
.getTime
) != 'function') {
3120 Dygraph
.clone
= function(o
) {
3121 // TODO(danvk): figure out how MochiKit's version works
3123 for (var i
= 0; i
< o
.length
; i
++) {
3124 if (Dygraph
.isArrayLike(o
[i
])) {
3125 r
.push(Dygraph
.clone(o
[i
]));
3135 * Get the CSV data. If it's in a function, call that function. If it's in a
3136 * file, do an XMLHttpRequest to get it.
3139 Dygraph
.prototype.start_
= function() {
3140 if (typeof this.file_
== 'function') {
3141 // CSV string. Pretend we got it via XHR.
3142 this.loadedEvent_(this.file_());
3143 } else if (Dygraph
.isArrayLike(this.file_
)) {
3144 this.rawData_
= this.parseArray_(this.file_
);
3146 } else if (typeof this.file_
== 'object' &&
3147 typeof this.file_
.getColumnRange
== 'function') {
3148 // must be a DataTable from gviz.
3149 this.parseDataTable_(this.file_
);
3151 } else if (typeof this.file_
== 'string') {
3152 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3153 if (this.file_
.indexOf('\n') >= 0) {
3154 this.loadedEvent_(this.file_
);
3156 var req
= new XMLHttpRequest();
3158 req
.onreadystatechange
= function () {
3159 if (req
.readyState
== 4) {
3160 if (req
.status
== 200) {
3161 caller
.loadedEvent_(req
.responseText
);
3166 req
.open("GET", this.file_
, true);
3170 this.error("Unknown data format: " + (typeof this.file_
));
3175 * Changes various properties of the graph. These can include:
3177 * <li>file: changes the source data for the graph</li>
3178 * <li>errorBars: changes whether the data contains stddev</li>
3180 * @param {Object} attrs The new properties and values
3182 Dygraph
.prototype.updateOptions
= function(attrs
) {
3183 // TODO(danvk): this is a mess. Rethink this function.
3184 if ('rollPeriod' in attrs
) {
3185 this.rollPeriod_
= attrs
.rollPeriod
;
3187 if ('dateWindow' in attrs
) {
3188 this.dateWindow_
= attrs
.dateWindow
;
3189 if (!('noZoomFlagChange' in attrs
)) {
3190 this.zoomed_x_
= attrs
.dateWindow
!= null;
3193 if ('valueRange' in attrs
&& !('noZoomFlagChange' in attrs
)) {
3194 this.zoomed_y_
= attrs
.valueRange
!= null;
3197 // TODO(danvk): validate per-series options.
3202 // highlightCircleSize
3204 Dygraph
.update(this.user_attrs_
, attrs
);
3205 Dygraph
.update(this.renderOptions_
, attrs
);
3207 this.labelsFromCSV_
= (this.attr_("labels") == null);
3209 // TODO(danvk): this doesn't match the constructor logic
3210 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
3211 if (attrs
['file']) {
3212 this.file_
= attrs
['file'];
3220 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3221 * containing div (which has presumably changed size since the dygraph was
3222 * instantiated. If the width/height are specified, the div will be resized.
3224 * This is far more efficient than destroying and re-instantiating a
3225 * Dygraph, since it doesn't have to reparse the underlying data.
3227 * @param {Number} width Width (in pixels)
3228 * @param {Number} height Height (in pixels)
3230 Dygraph
.prototype.resize
= function(width
, height
) {
3231 if (this.resize_lock
) {
3234 this.resize_lock
= true;
3236 if ((width
=== null) != (height
=== null)) {
3237 this.warn("Dygraph.resize() should be called with zero parameters or " +
3238 "two non-NULL parameters. Pretending it was zero.");
3239 width
= height
= null;
3242 // TODO(danvk): there should be a clear() method.
3243 this.maindiv_
.innerHTML
= "";
3244 this.attrs_
.labelsDiv
= null;
3247 this.maindiv_
.style
.width
= width
+ "px";
3248 this.maindiv_
.style
.height
= height
+ "px";
3249 this.width_
= width
;
3250 this.height_
= height
;
3252 this.width_
= this.maindiv_
.offsetWidth
;
3253 this.height_
= this.maindiv_
.offsetHeight
;
3256 this.createInterface_();
3259 this.resize_lock
= false;
3263 * Adjusts the number of days in the rolling average. Updates the graph to
3264 * reflect the new averaging period.
3265 * @param {Number} length Number of days over which to average the data.
3267 Dygraph
.prototype.adjustRoll
= function(length
) {
3268 this.rollPeriod_
= length
;
3273 * Returns a boolean array of visibility statuses.
3275 Dygraph
.prototype.visibility
= function() {
3276 // Do lazy-initialization, so that this happens after we know the number of
3278 if (!this.attr_("visibility")) {
3279 this.attrs_
["visibility"] = [];
3281 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
3282 this.attr_("visibility").push(true);
3284 return this.attr_("visibility");
3288 * Changes the visiblity of a series.
3290 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3291 var x
= this.visibility();
3292 if (num
< 0 || num
>= x
.length
) {
3293 this.warn("invalid series number in setVisibility: " + num
);
3301 * Update the list of annotations and redraw the chart.
3303 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3304 // Only add the annotation CSS rule once we know it will be used.
3305 Dygraph
.addAnnotationRule();
3306 this.annotations_
= ann
;
3307 this.layout_
.setAnnotations(this.annotations_
);
3308 if (!suppressDraw
) {
3314 * Return the list of annotations.
3316 Dygraph
.prototype.annotations
= function() {
3317 return this.annotations_
;
3321 * Get the index of a series (column) given its name. The first column is the
3322 * x-axis, so the data series start with index 1.
3324 Dygraph
.prototype.indexFromSetName
= function(name
) {
3325 var labels
= this.attr_("labels");
3326 for (var i
= 0; i
< labels
.length
; i
++) {
3327 if (labels
[i
] == name
) return i
;
3332 Dygraph
.addAnnotationRule
= function() {
3333 if (Dygraph
.addedAnnotationCSS
) return;
3335 var rule
= "border: 1px solid black; " +
3336 "background-color: white; " +
3337 "text-align: center;";
3339 var styleSheetElement
= document
.createElement("style");
3340 styleSheetElement
.type
= "text/css";
3341 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3343 // Find the first style sheet that we can access.
3344 // We may not add a rule to a style sheet from another domain for security
3345 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3346 // adds its own style sheets from google.com.
3347 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3348 if (document
.styleSheets
[i
].disabled
) continue;
3349 var mysheet
= document
.styleSheets
[i
];
3351 if (mysheet
.insertRule
) { // Firefox
3352 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3353 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3354 } else if (mysheet
.addRule
) { // IE
3355 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3357 Dygraph
.addedAnnotationCSS
= true;
3360 // Was likely a security exception.
3364 this.warn("Unable to add default annotation CSS rule; display may be off.");
3368 * Create a new canvas element. This is more complex than a simple
3369 * document.createElement("canvas") because of IE and excanvas.
3371 Dygraph
.createCanvas
= function() {
3372 var canvas
= document
.createElement("canvas");
3374 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
3375 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
3376 canvas
= G_vmlCanvasManager
.initElement(canvas
);
3384 * A wrapper around Dygraph that implements the gviz API.
3385 * @param {Object} container The DOM object the visualization should live in.
3387 Dygraph
.GVizChart
= function(container
) {
3388 this.container
= container
;
3391 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
3392 // Clear out any existing dygraph.
3393 // TODO(danvk): would it make more sense to simply redraw using the current
3394 // date_graph object?
3395 this.container
.innerHTML
= '';
3396 if (typeof(this.date_graph
) != 'undefined') {
3397 this.date_graph
.destroy();
3400 this.date_graph
= new Dygraph(this.container
, data
, options
);
3404 * Google charts compatible setSelection
3405 * Only row selection is supported, all points in the row will be highlighted
3406 * @param {Array} array of the selected cells
3409 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
3411 if (selection_array
.length
) {
3412 row
= selection_array
[0].row
;
3414 this.date_graph
.setSelection(row
);
3418 * Google charts compatible getSelection implementation
3419 * @return {Array} array of the selected cells
3422 Dygraph
.GVizChart
.prototype.getSelection
= function() {
3425 var row
= this.date_graph
.getSelection();
3427 if (row
< 0) return selection
;
3430 for (var i
in this.date_graph
.layout_
.datasets
) {
3431 selection
.push({row
: row
, column
: col
});
3438 // Older pages may still use this name.
3439 DateGraph
= Dygraph
;