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 // Clear the div. This ensure that, if multiple dygraphs are passed the same
202 // div, then only one will be drawn.
205 // If the div isn't already sized then inherit from our attrs or
206 // give it a default size.
207 if (div
.style
.width
== '') {
208 div
.style
.width
= (attrs
.width
|| Dygraph
.DEFAULT_WIDTH
) + "px";
210 if (div
.style
.height
== '') {
211 div
.style
.height
= (attrs
.height
|| Dygraph
.DEFAULT_HEIGHT
) + "px";
213 this.width_
= parseInt(div
.style
.width
, 10);
214 this.height_
= parseInt(div
.style
.height
, 10);
215 // The div might have been specified as percent of the current window size,
216 // convert that to an appropriate number of pixels.
217 if (div
.style
.width
.indexOf("%") == div
.style
.width
.length
- 1) {
218 this.width_
= div
.offsetWidth
;
220 if (div
.style
.height
.indexOf("%") == div
.style
.height
.length
- 1) {
221 this.height_
= div
.offsetHeight
;
224 if (this.width_
== 0) {
225 this.error("dygraph has zero width. Please specify a width in pixels.");
227 if (this.height_
== 0) {
228 this.error("dygraph has zero height. Please specify a height in pixels.");
231 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
232 if (attrs
['stackedGraph']) {
233 attrs
['fillGraph'] = true;
234 // TODO(nikhilk): Add any other stackedGraph checks here.
237 // Dygraphs has many options, some of which interact with one another.
238 // To keep track of everything, we maintain two sets of options:
240 // this.user_attrs_ only options explicitly set by the user.
241 // this.attrs_ defaults, options derived from user_attrs_, data.
243 // Options are then accessed this.attr_('attr'), which first looks at
244 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
245 // defaults without overriding behavior that the user specifically asks for.
246 this.user_attrs_
= {};
247 Dygraph
.update(this.user_attrs_
, attrs
);
250 Dygraph
.update(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
252 this.boundaryIds_
= [];
254 // Make a note of whether labels will be pulled from the CSV file.
255 this.labelsFromCSV_
= (this.attr_("labels") == null);
257 // Create the containing DIV and other interactive elements
258 this.createInterface_();
263 Dygraph
.prototype.toString
= function() {
264 var maindiv
= this.maindiv_
;
265 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
266 return "[Dygraph " + id
+ "]";
269 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
271 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
272 this.user_attrs_
[seriesName
] != null &&
273 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
274 return this.user_attrs_
[seriesName
][name
];
275 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
276 return this.user_attrs_
[name
];
277 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
278 return this.attrs_
[name
];
284 // TODO(danvk): any way I can get the line numbers to be this.warn call?
285 Dygraph
.prototype.log
= function(severity
, message
) {
286 if (typeof(console
) != 'undefined') {
289 console
.debug('dygraphs: ' + message
);
292 console
.info('dygraphs: ' + message
);
294 case Dygraph
.WARNING
:
295 console
.warn('dygraphs: ' + message
);
298 console
.error('dygraphs: ' + message
);
303 Dygraph
.prototype.info
= function(message
) {
304 this.log(Dygraph
.INFO
, message
);
306 Dygraph
.prototype.warn
= function(message
) {
307 this.log(Dygraph
.WARNING
, message
);
309 Dygraph
.prototype.error
= function(message
) {
310 this.log(Dygraph
.ERROR
, message
);
314 * Returns the current rolling period, as set by the user or an option.
315 * @return {Number} The number of days in the rolling window
317 Dygraph
.prototype.rollPeriod
= function() {
318 return this.rollPeriod_
;
322 * Returns the currently-visible x-range. This can be affected by zooming,
323 * panning or a call to updateOptions.
324 * Returns a two-element array: [left, right].
325 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
327 Dygraph
.prototype.xAxisRange
= function() {
328 if (this.dateWindow_
) return this.dateWindow_
;
330 // The entire chart is visible.
331 var left
= this.rawData_
[0][0];
332 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
333 return [left
, right
];
337 * Returns the currently-visible y-range for an axis. This can be affected by
338 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
339 * called with no arguments, returns the range of the first axis.
340 * Returns a two-element array: [bottom, top].
342 Dygraph
.prototype.yAxisRange
= function(idx
) {
343 if (typeof(idx
) == "undefined") idx
= 0;
344 if (idx
< 0 || idx
>= this.axes_
.length
) return null;
345 return [ this.axes_
[idx
].computedValueRange
[0],
346 this.axes_
[idx
].computedValueRange
[1] ];
350 * Returns the currently-visible y-ranges for each axis. This can be affected by
351 * zooming, panning, calls to updateOptions, etc.
352 * Returns an array of [bottom, top] pairs, one for each y-axis.
354 Dygraph
.prototype.yAxisRanges
= function() {
356 for (var i
= 0; i
< this.axes_
.length
; i
++) {
357 ret
.push(this.yAxisRange(i
));
362 // TODO(danvk): use these functions throughout dygraphs.
364 * Convert from data coordinates to canvas/div X/Y coordinates.
365 * If specified, do this conversion for the coordinate system of a particular
366 * axis. Uses the first axis by default.
367 * Returns a two-element array: [X, Y]
369 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
370 * instead of toDomCoords(null, y, axis).
372 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
373 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
377 * Convert from data x coordinates to canvas/div X coordinate.
378 * If specified, do this conversion for the coordinate system of a particular
380 * Returns a single value or null if x is null.
382 Dygraph
.prototype.toDomXCoord
= function(x
) {
387 var area
= this.plotter_
.area
;
388 var xRange
= this.xAxisRange();
389 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
393 * Convert from data x coordinates to canvas/div Y coordinate and optional
394 * axis. Uses the first axis by default.
396 * returns a single value or null if y is null.
398 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
399 var pct
= this.toPercentYCoord(y
, axis
);
404 var area
= this.plotter_
.area
;
405 return area
.y
+ pct
* area
.h
;
409 * Convert from canvas/div coords to data coordinates.
410 * If specified, do this conversion for the coordinate system of a particular
411 * axis. Uses the first axis by default.
412 * Returns a two-element array: [X, Y].
414 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
415 * instead of toDataCoords(null, y, axis).
417 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
418 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
422 * Convert from canvas/div x coordinate to data coordinate.
424 * If x is null, this returns null.
426 Dygraph
.prototype.toDataXCoord
= function(x
) {
431 var area
= this.plotter_
.area
;
432 var xRange
= this.xAxisRange();
433 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
437 * Convert from canvas/div y coord to value.
439 * If y is null, this returns null.
440 * if axis is null, this uses the first axis.
442 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
447 var area
= this.plotter_
.area
;
448 var yRange
= this.yAxisRange(axis
);
450 if (typeof(axis
) == "undefined") axis
= 0;
451 if (!this.axes_
[axis
].logscale
) {
452 return yRange
[0] + (area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
454 // Computing the inverse of toDomCoord.
455 var pct
= (y
- area
.y
) / area
.h
457 // Computing the inverse of toPercentYCoord. The function was arrived at with
458 // the following steps:
460 // Original calcuation:
461 // pct = (logr1 - Dygraph.log10(y)) / (logr1
- Dygraph
.log10(yRange
[0]));
463 // Move denominator to both sides:
464 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
466 // subtract logr1, and take the negative value.
467 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
469 // Swap both sides of the equation, and we can compute the log of the
470 // return value. Which means we just need to use that as the exponent in
472 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
474 var logr1
= Dygraph
.log10(yRange
[1]);
475 var exponent
= logr1
- (pct
* (logr1
- Dygraph
.log10(yRange
[0])));
476 var value
= Math
.pow(Dygraph
.LOG_SCALE
, exponent
);
482 * Converts a y for an axis to a percentage from the top to the
485 * If the coordinate represents a value visible on the canvas, then
486 * the value will be between 0 and 1, where 0 is the top of the canvas.
487 * However, this method will return values outside the range, as
488 * values can fall outside the canvas.
490 * If y is null, this returns null.
491 * if axis is null, this uses the first axis.
493 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
497 if (typeof(axis
) == "undefined") axis
= 0;
499 var area
= this.plotter_
.area
;
500 var yRange
= this.yAxisRange(axis
);
503 if (!this.axes_
[axis
].logscale
) {
504 // yrange[1] - y is unit distance from the bottom.
505 // yrange[1] - yrange[0] is the scale of the range.
506 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
507 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
509 var logr1
= Dygraph
.log10(yRange
[1]);
510 pct
= (logr1
- Dygraph
.log10(y
)) / (logr1
- Dygraph
.log10(yRange
[0]));
516 * Returns the number of columns (including the independent variable).
518 Dygraph
.prototype.numColumns
= function() {
519 return this.rawData_
[0].length
;
523 * Returns the number of rows (excluding any header/label row).
525 Dygraph
.prototype.numRows
= function() {
526 return this.rawData_
.length
;
530 * Returns the value in the given row and column. If the row and column exceed
531 * the bounds on the data, returns null. Also returns null if the value is
534 Dygraph
.prototype.getValue
= function(row
, col
) {
535 if (row
< 0 || row
> this.rawData_
.length
) return null;
536 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
538 return this.rawData_
[row
][col
];
541 Dygraph
.addEvent
= function(el
, evt
, fn
) {
542 var normed_fn
= function(e
) {
543 if (!e
) var e
= window
.event
;
546 if (window
.addEventListener
) { // Mozilla, Netscape, Firefox
547 el
.addEventListener(evt
, normed_fn
, false);
549 el
.attachEvent('on' + evt
, normed_fn
);
554 // Based on the article at
555 // http://www.switchonthecode.com/tutorials
/javascript
-tutorial
-the
-scroll
-wheel
556 Dygraph
.cancelEvent
= function(e
) {
557 e
= e
? e
: window
.event
;
558 if (e
.stopPropagation
) {
561 if (e
.preventDefault
) {
564 e
.cancelBubble
= true;
566 e
.returnValue
= false;
571 * Generates interface elements for the Dygraph: a containing div, a div to
572 * display the current point, and a textbox to adjust the rolling average
573 * period. Also creates the Renderer/Layout elements.
576 Dygraph
.prototype.createInterface_
= function() {
577 // Create the all-enclosing graph div
578 var enclosing
= this.maindiv_
;
580 this.graphDiv
= document
.createElement("div");
581 this.graphDiv
.style
.width
= this.width_
+ "px";
582 this.graphDiv
.style
.height
= this.height_
+ "px";
583 enclosing
.appendChild(this.graphDiv
);
585 // Create the canvas for interactive parts of the chart.
586 this.canvas_
= Dygraph
.createCanvas();
587 this.canvas_
.style
.position
= "absolute";
588 this.canvas_
.width
= this.width_
;
589 this.canvas_
.height
= this.height_
;
590 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
591 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
593 // ... and for static parts of the chart.
594 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
596 // The interactive parts of the graph are drawn on top of the chart.
597 this.graphDiv
.appendChild(this.hidden_
);
598 this.graphDiv
.appendChild(this.canvas_
);
599 this.mouseEventElement_
= this.canvas_
;
602 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
603 dygraph
.mouseMove_(e
);
605 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
606 dygraph
.mouseOut_(e
);
609 // Create the grapher
610 // TODO(danvk): why does the Layout need its own set of options?
611 this.layoutOptions_
= { 'xOriginIsZero': false };
612 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
613 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
614 Dygraph
.update(this.layoutOptions_
, {
615 'errorBars': (this.attr_("errorBars") || this.attr_("customBars")) });
617 this.layout_
= new DygraphLayout(this, this.layoutOptions_
);
619 // TODO(danvk): why does the Renderer need its own set of options?
620 this.renderOptions_
= { colorScheme
: this.colors_
,
622 axisLineWidth
: Dygraph
.AXIS_LINE_WIDTH
};
623 Dygraph
.update(this.renderOptions_
, this.attrs_
);
624 Dygraph
.update(this.renderOptions_
, this.user_attrs_
);
626 this.createStatusMessage_();
627 this.createDragInterface_();
631 * Detach DOM elements in the dygraph and null out all data references.
632 * Calling this when you're done with a dygraph can dramatically reduce memory
633 * usage. See, e.g., the tests/perf.html example.
635 Dygraph
.prototype.destroy
= function() {
636 var removeRecursive
= function(node
) {
637 while (node
.hasChildNodes()) {
638 removeRecursive(node
.firstChild
);
639 node
.removeChild(node
.firstChild
);
642 removeRecursive(this.maindiv_
);
644 var nullOut
= function(obj
) {
646 if (typeof(obj
[n
]) === 'object') {
652 // These may not all be necessary, but it can't hurt...
653 nullOut(this.layout_
);
654 nullOut(this.plotter_
);
659 * Creates the canvas containing the PlotKit graph. Only plotkit ever draws on
660 * this particular canvas. All Dygraph work is done on this.canvas_.
661 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
662 * @return {Object} The newly-created canvas
665 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
666 var h
= Dygraph
.createCanvas();
667 h
.style
.position
= "absolute";
668 // TODO(danvk): h should be offset from canvas. canvas needs to include
669 // some extra area to make it easier to zoom in on the far left and far
670 // right. h needs to be precisely the plot area, so that clipping occurs.
671 h
.style
.top
= canvas
.style
.top
;
672 h
.style
.left
= canvas
.style
.left
;
673 h
.width
= this.width_
;
674 h
.height
= this.height_
;
675 h
.style
.width
= this.width_
+ "px"; // for IE
676 h
.style
.height
= this.height_
+ "px"; // for IE
680 // Taken from MochiKit.Color
681 Dygraph
.hsvToRGB
= function (hue
, saturation
, value
) {
685 if (saturation
=== 0) {
690 var i
= Math
.floor(hue
* 6);
691 var f
= (hue
* 6) - i
;
692 var p
= value
* (1 - saturation
);
693 var q
= value
* (1 - (saturation
* f
));
694 var t
= value
* (1 - (saturation
* (1 - f
)));
696 case 1: red
= q
; green
= value
; blue
= p
; break;
697 case 2: red
= p
; green
= value
; blue
= t
; break;
698 case 3: red
= p
; green
= q
; blue
= value
; break;
699 case 4: red
= t
; green
= p
; blue
= value
; break;
700 case 5: red
= value
; green
= p
; blue
= q
; break;
701 case 6: // fall through
702 case 0: red
= value
; green
= t
; blue
= p
; break;
705 red
= Math
.floor(255 * red
+ 0.5);
706 green
= Math
.floor(255 * green
+ 0.5);
707 blue
= Math
.floor(255 * blue
+ 0.5);
708 return 'rgb(' + red
+ ',' + green
+ ',' + blue
+ ')';
713 * Generate a set of distinct colors for the data series. This is done with a
714 * color wheel. Saturation/Value are customizable, and the hue is
715 * equally-spaced around the color wheel. If a custom set of colors is
716 * specified, that is used instead.
719 Dygraph
.prototype.setColors_
= function() {
720 // TODO(danvk): compute this directly into this.attrs_['colorScheme'] and do
721 // away with this.renderOptions_.
722 var num
= this.attr_("labels").length
- 1;
724 var colors
= this.attr_('colors');
726 var sat
= this.attr_('colorSaturation') || 1.0;
727 var val
= this.attr_('colorValue') || 0.5;
728 var half
= Math
.ceil(num
/ 2);
729 for (var i
= 1; i
<= num
; i
++) {
730 if (!this.visibility()[i
-1]) continue;
731 // alternate colors for high contrast.
732 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
733 var hue
= (1.0 * idx
/ (1 + num
));
734 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
737 for (var i
= 0; i
< num
; i
++) {
738 if (!this.visibility()[i
]) continue;
739 var colorStr
= colors
[i
% colors
.length
];
740 this.colors_
.push(colorStr
);
744 // TODO(danvk): update this w/r
/t/ the
new options system
.
745 this.renderOptions_
.colorScheme
= this.colors_
;
746 Dygraph
.update(this.plotter_
.options
, this.renderOptions_
);
747 Dygraph
.update(this.layoutOptions_
, this.user_attrs_
);
748 Dygraph
.update(this.layoutOptions_
, this.attrs_
);
752 * Return the list of colors. This is either the list of colors passed in the
753 * attributes, or the autogenerated list of rgb(r,g,b) strings.
754 * @return {Array<string>} The list of colors.
756 Dygraph
.prototype.getColors
= function() {
760 // The following functions are from quirksmode.org with a modification for Safari from
761 // http://blog.firetree.net/2005/07/04/javascript-find-position/
762 // http://www.quirksmode.org/js
/findpos
.html
763 Dygraph
.findPosX
= function(obj
) {
768 curleft
+= obj
.offsetLeft
;
769 if(!obj
.offsetParent
)
771 obj
= obj
.offsetParent
;
778 Dygraph
.findPosY
= function(obj
) {
783 curtop
+= obj
.offsetTop
;
784 if(!obj
.offsetParent
)
786 obj
= obj
.offsetParent
;
796 * Create the div that contains information on the selected point(s)
797 * This goes in the top right of the canvas, unless an external div has already
801 Dygraph
.prototype.createStatusMessage_
= function() {
802 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
803 if (userLabelsDiv
&& null != userLabelsDiv
804 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
805 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
807 if (!this.attr_("labelsDiv")) {
808 var divWidth
= this.attr_('labelsDivWidth');
810 "position": "absolute",
813 "width": divWidth
+ "px",
815 "left": (this.width_
- divWidth
- 2) + "px",
816 "background": "white",
818 "overflow": "hidden"};
819 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
820 var div
= document
.createElement("div");
821 for (var name
in messagestyle
) {
822 if (messagestyle
.hasOwnProperty(name
)) {
823 div
.style
[name
] = messagestyle
[name
];
826 this.graphDiv
.appendChild(div
);
827 this.attrs_
.labelsDiv
= div
;
832 * Position the labels div so that its right edge is flush with the right edge
833 * of the charting area.
835 Dygraph
.prototype.positionLabelsDiv_
= function() {
836 // Don't touch a user-specified labelsDiv.
837 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
839 var area
= this.plotter_
.area
;
840 var div
= this.attr_("labelsDiv");
841 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
845 * Create the text box to adjust the averaging period
848 Dygraph
.prototype.createRollInterface_
= function() {
849 // Create a roller if one doesn't exist already.
851 this.roller_
= document
.createElement("input");
852 this.roller_
.type
= "text";
853 this.roller_
.style
.display
= "none";
854 this.graphDiv
.appendChild(this.roller_
);
857 var display
= this.attr_('showRoller') ? 'block' : 'none';
859 var textAttr
= { "position": "absolute",
861 "top": (this.plotter_
.area
.h
- 25) + "px",
862 "left": (this.plotter_
.area
.x
+ 1) + "px",
865 this.roller_
.size
= "2";
866 this.roller_
.value
= this.rollPeriod_
;
867 for (var name
in textAttr
) {
868 if (textAttr
.hasOwnProperty(name
)) {
869 this.roller_
.style
[name
] = textAttr
[name
];
874 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
877 // These functions are taken from MochiKit.Signal
878 Dygraph
.pageX
= function(e
) {
880 return (!e
.pageX
|| e
.pageX
< 0) ? 0 : e
.pageX
;
883 var b
= document
.body
;
885 (de
.scrollLeft
|| b
.scrollLeft
) -
886 (de
.clientLeft
|| 0);
890 Dygraph
.pageY
= function(e
) {
892 return (!e
.pageY
|| e
.pageY
< 0) ? 0 : e
.pageY
;
895 var b
= document
.body
;
897 (de
.scrollTop
|| b
.scrollTop
) -
902 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
903 return Dygraph
.pageX(e
) - context
.px
906 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
907 return Dygraph
.pageY(e
) - context
.py
910 // Called in response to an interaction model operation that
911 // should start the default panning behavior.
913 // It's used in the default callback for "mousedown" operations.
914 // Custom interaction model builders can use it to provide the default
917 Dygraph
.startPan
= function(event
, g
, context
) {
918 context
.isPanning
= true;
919 var xRange
= g
.xAxisRange();
920 context
.dateRange
= xRange
[1] - xRange
[0];
921 context
.initialLeftmostDate
= xRange
[0];
922 context
.xUnitsPerPixel
= context
.dateRange
/ (g
.plotter_
.area
.w
- 1);
924 // Record the range of each y-axis at the start of the drag.
925 // If any axis has a valueRange or valueWindow, then we want a 2D pan.
926 context
.is2DPan
= false;
927 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
928 var axis
= g
.axes_
[i
];
929 var yRange
= g
.yAxisRange(i
);
930 // TODO(konigsberg): These values should be in |context|.
931 // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
933 axis
.initialTopValue
= Dygraph
.log10(yRange
[1]);
934 axis
.dragValueRange
= Dygraph
.log10(yRange
[1]) - Dygraph
.log10(yRange
[0]);
936 axis
.initialTopValue
= yRange
[1];
937 axis
.dragValueRange
= yRange
[1] - yRange
[0];
939 axis
.unitsPerPixel
= axis
.dragValueRange
/ (g
.plotter_
.area
.h
- 1);
941 // While calculating axes, set 2dpan.
942 if (axis
.valueWindow
|| axis
.valueRange
) context
.is2DPan
= true;
946 // Called in response to an interaction model operation that
947 // responds to an event that pans the view.
949 // It's used in the default callback for "mousemove" operations.
950 // Custom interaction model builders can use it to provide the default
953 Dygraph
.movePan
= function(event
, g
, context
) {
954 context
.dragEndX
= g
.dragGetX_(event
, context
);
955 context
.dragEndY
= g
.dragGetY_(event
, context
);
957 var minDate
= context
.initialLeftmostDate
-
958 (context
.dragEndX
- context
.dragStartX
) * context
.xUnitsPerPixel
;
959 var maxDate
= minDate
+ context
.dateRange
;
960 g
.dateWindow_
= [minDate
, maxDate
];
962 // y-axis scaling is automatic unless this is a full 2D pan.
963 if (context
.is2DPan
) {
964 // Adjust each axis appropriately.
965 for (var i
= 0; i
< g
.axes_
.length
; i
++) {
966 var axis
= g
.axes_
[i
];
968 var pixelsDragged
= context
.dragEndY
- context
.dragStartY
;
969 var unitsDragged
= pixelsDragged
* axis
.unitsPerPixel
;
971 // In log scale, maxValue and minValue are the logs of those values.
972 var maxValue
= axis
.initialTopValue
+ unitsDragged
;
973 var minValue
= maxValue
- axis
.dragValueRange
;
975 axis
.valueWindow
= [ Math
.pow(Dygraph
.LOG_SCALE
, minValue
),
976 Math
.pow(Dygraph
.LOG_SCALE
, maxValue
) ];
978 axis
.valueWindow
= [ minValue
, maxValue
];
986 // Called in response to an interaction model operation that
987 // responds to an event that ends panning.
989 // It's used in the default callback for "mouseup" operations.
990 // Custom interaction model builders can use it to provide the default
993 Dygraph
.endPan
= function(event
, g
, context
) {
994 // TODO(konigsberg): Clear the context data from the axis.
995 // TODO(konigsberg): mouseup should just delete the
996 // context object, and mousedown should create a new one.
997 context
.isPanning
= false;
998 context
.is2DPan
= false;
999 context
.initialLeftmostDate
= null;
1000 context
.dateRange
= null;
1001 context
.valueRange
= null;
1004 // Called in response to an interaction model operation that
1005 // responds to an event that starts zooming.
1007 // It's used in the default callback for "mousedown" operations.
1008 // Custom interaction model builders can use it to provide the default
1009 // zooming behavior.
1011 Dygraph
.startZoom
= function(event
, g
, context
) {
1012 context
.isZooming
= true;
1015 // Called in response to an interaction model operation that
1016 // responds to an event that defines zoom boundaries.
1018 // It's used in the default callback for "mousemove" operations.
1019 // Custom interaction model builders can use it to provide the default
1020 // zooming behavior.
1022 Dygraph
.moveZoom
= function(event
, g
, context
) {
1023 context
.dragEndX
= g
.dragGetX_(event
, context
);
1024 context
.dragEndY
= g
.dragGetY_(event
, context
);
1026 var xDelta
= Math
.abs(context
.dragStartX
- context
.dragEndX
);
1027 var yDelta
= Math
.abs(context
.dragStartY
- context
.dragEndY
);
1029 // drag direction threshold for y axis is twice as large as x axis
1030 context
.dragDirection
= (xDelta
< yDelta
/ 2) ? Dygraph
.VERTICAL
: Dygraph
.HORIZONTAL
;
1033 context
.dragDirection
,
1038 context
.prevDragDirection
,
1042 context
.prevEndX
= context
.dragEndX
;
1043 context
.prevEndY
= context
.dragEndY
;
1044 context
.prevDragDirection
= context
.dragDirection
;
1047 // Called in response to an interaction model operation that
1048 // responds to an event that performs a zoom based on previously defined
1051 // It's used in the default callback for "mouseup" operations.
1052 // Custom interaction model builders can use it to provide the default
1053 // zooming behavior.
1055 Dygraph
.endZoom
= function(event
, g
, context
) {
1056 context
.isZooming
= false;
1057 context
.dragEndX
= g
.dragGetX_(event
, context
);
1058 context
.dragEndY
= g
.dragGetY_(event
, context
);
1059 var regionWidth
= Math
.abs(context
.dragEndX
- context
.dragStartX
);
1060 var regionHeight
= Math
.abs(context
.dragEndY
- context
.dragStartY
);
1062 if (regionWidth
< 2 && regionHeight
< 2 &&
1063 g
.lastx_
!= undefined
&& g
.lastx_
!= -1) {
1064 // TODO(danvk): pass along more info about the points, e.g. 'x'
1065 if (g
.attr_('clickCallback') != null) {
1066 g
.attr_('clickCallback')(event
, g
.lastx_
, g
.selPoints_
);
1068 if (g
.attr_('pointClickCallback')) {
1069 // check if the click was on a particular point.
1070 var closestIdx
= -1;
1071 var closestDistance
= 0;
1072 for (var i
= 0; i
< g
.selPoints_
.length
; i
++) {
1073 var p
= g
.selPoints_
[i
];
1074 var distance
= Math
.pow(p
.canvasx
- context
.dragEndX
, 2) +
1075 Math
.pow(p
.canvasy
- context
.dragEndY
, 2);
1076 if (closestIdx
== -1 || distance
< closestDistance
) {
1077 closestDistance
= distance
;
1082 // Allow any click within two pixels of the dot.
1083 var radius
= g
.attr_('highlightCircleSize') + 2;
1084 if (closestDistance
<= 5 * 5) {
1085 g
.attr_('pointClickCallback')(event
, g
.selPoints_
[closestIdx
]);
1090 if (regionWidth
>= 10 && context
.dragDirection
== Dygraph
.HORIZONTAL
) {
1091 g
.doZoomX_(Math
.min(context
.dragStartX
, context
.dragEndX
),
1092 Math
.max(context
.dragStartX
, context
.dragEndX
));
1093 } else if (regionHeight
>= 10 && context
.dragDirection
== Dygraph
.VERTICAL
) {
1094 g
.doZoomY_(Math
.min(context
.dragStartY
, context
.dragEndY
),
1095 Math
.max(context
.dragStartY
, context
.dragEndY
));
1097 g
.canvas_
.getContext("2d").clearRect(0, 0,
1101 context
.dragStartX
= null;
1102 context
.dragStartY
= null;
1105 Dygraph
.defaultInteractionModel
= {
1106 // Track the beginning of drag events
1107 mousedown
: function(event
, g
, context
) {
1108 context
.initializeMouseDown(event
, g
, context
);
1110 if (event
.altKey
|| event
.shiftKey
) {
1111 Dygraph
.startPan(event
, g
, context
);
1113 Dygraph
.startZoom(event
, g
, context
);
1117 // Draw zoom rectangles when the mouse is down and the user moves around
1118 mousemove
: function(event
, g
, context
) {
1119 if (context
.isZooming
) {
1120 Dygraph
.moveZoom(event
, g
, context
);
1121 } else if (context
.isPanning
) {
1122 Dygraph
.movePan(event
, g
, context
);
1126 mouseup
: function(event
, g
, context
) {
1127 if (context
.isZooming
) {
1128 Dygraph
.endZoom(event
, g
, context
);
1129 } else if (context
.isPanning
) {
1130 Dygraph
.endPan(event
, g
, context
);
1134 // Temporarily cancel the dragging event when the mouse leaves the graph
1135 mouseout
: function(event
, g
, context
) {
1136 if (context
.isZooming
) {
1137 context
.dragEndX
= null;
1138 context
.dragEndY
= null;
1142 // Disable zooming out if panning.
1143 dblclick
: function(event
, g
, context
) {
1144 if (event
.altKey
|| event
.shiftKey
) {
1147 // TODO(konigsberg): replace g.doUnzoom()_ with something that is
1148 // friendlier to public use.
1153 Dygraph
.DEFAULT_ATTRS
.interactionModel
= Dygraph
.defaultInteractionModel
;
1156 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1160 Dygraph
.prototype.createDragInterface_
= function() {
1162 // Tracks whether the mouse is down right now
1164 isPanning
: false, // is this drag part of a pan?
1165 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1170 dragDirection
: null,
1173 prevDragDirection
: null,
1175 // The value on the left side of the graph when a pan operation starts.
1176 initialLeftmostDate
: null,
1178 // The number of units each pixel spans. (This won't be valid for log
1180 xUnitsPerPixel
: null,
1182 // TODO(danvk): update this comment
1183 // The range in second/value units that the viewport encompasses during a
1184 // panning operation.
1187 // Utility function to convert page-wide coordinates to canvas coords
1191 initializeMouseDown
: function(event
, g
, context
) {
1192 // prevents mouse drags from selecting page text.
1193 if (event
.preventDefault
) {
1194 event
.preventDefault(); // Firefox, Chrome, etc.
1196 event
.returnValue
= false; // IE
1197 event
.cancelBubble
= true;
1200 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1201 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1202 context
.dragStartX
= g
.dragGetX_(event
, context
);
1203 context
.dragStartY
= g
.dragGetY_(event
, context
);
1207 var interactionModel
= this.attr_("interactionModel");
1209 // Self is the graph.
1212 // Function that binds the graph and context to the handler.
1213 var bindHandler
= function(handler
) {
1214 return function(event
) {
1215 handler(event
, self
, context
);
1219 for (var eventName
in interactionModel
) {
1220 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1221 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1222 bindHandler(interactionModel
[eventName
]));
1225 // If the user releases the mouse button during a drag, but not over the
1226 // canvas, then it doesn't count as a zooming action.
1227 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1228 if (context
.isZooming
|| context
.isPanning
) {
1229 context
.isZooming
= false;
1230 context
.dragStartX
= null;
1231 context
.dragStartY
= null;
1234 if (context
.isPanning
) {
1235 context
.isPanning
= false;
1236 context
.draggingDate
= null;
1237 context
.dateRange
= null;
1238 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1239 delete self
.axes_
[i
].draggingValue
;
1240 delete self
.axes_
[i
].dragValueRange
;
1247 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1248 * up any previous zoom rectangles that were drawn. This could be optimized to
1249 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1252 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1253 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1254 * @param {Number} startX The X position where the drag started, in canvas
1256 * @param {Number} endX The current X position of the drag, in canvas coords.
1257 * @param {Number} startY The Y position where the drag started, in canvas
1259 * @param {Number} endY The current Y position of the drag, in canvas coords.
1260 * @param {Number} prevDirection the value of direction on the previous call to
1261 * this function. Used to avoid excess redrawing
1262 * @param {Number} prevEndX The value of endX on the previous call to this
1263 * function. Used to avoid excess redrawing
1264 * @param {Number} prevEndY The value of endY on the previous call to this
1265 * function. Used to avoid excess redrawing
1268 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
, endY
,
1269 prevDirection
, prevEndX
, prevEndY
) {
1270 var ctx
= this.canvas_
.getContext("2d");
1272 // Clean up from the previous rect if necessary
1273 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1274 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1275 Math
.abs(startX
- prevEndX
), this.height_
);
1276 } else if (prevDirection
== Dygraph
.VERTICAL
){
1277 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1278 this.width_
, Math
.abs(startY
- prevEndY
));
1281 // Draw a light-grey rectangle to show the new viewing area
1282 if (direction
== Dygraph
.HORIZONTAL
) {
1283 if (endX
&& startX
) {
1284 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1285 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1286 Math
.abs(endX
- startX
), this.height_
);
1289 if (direction
== Dygraph
.VERTICAL
) {
1290 if (endY
&& startY
) {
1291 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1292 ctx
.fillRect(0, Math
.min(startY
, endY
),
1293 this.width_
, Math
.abs(endY
- startY
));
1299 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1300 * the canvas. The exact zoom window may be slightly larger if there are no data
1301 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1302 * which accepts dates that match the raw data. This function redraws the graph.
1304 * @param {Number} lowX The leftmost pixel value that should be visible.
1305 * @param {Number} highX The rightmost pixel value that should be visible.
1308 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1309 // Find the earliest and latest dates contained in this canvasx range.
1310 // Convert the call to date ranges of the raw data.
1311 var minDate
= this.toDataXCoord(lowX
);
1312 var maxDate
= this.toDataXCoord(highX
);
1313 this.doZoomXDates_(minDate
, maxDate
);
1317 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1318 * method with doZoomX which accepts pixel coordinates. This function redraws
1321 * @param {Number} minDate The minimum date that should be visible.
1322 * @param {Number} maxDate The maximum date that should be visible.
1325 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1326 this.dateWindow_
= [minDate
, maxDate
];
1328 if (this.attr_("zoomCallback")) {
1329 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1334 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1335 * the canvas. This function redraws the graph.
1337 * @param {Number} lowY The topmost pixel value that should be visible.
1338 * @param {Number} highY The lowest pixel value that should be visible.
1341 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1342 // Find the highest and lowest values in pixel range for each axis.
1343 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1344 // This is because pixels increase as you go down on the screen, whereas data
1345 // coordinates increase as you go up the screen.
1346 var valueRanges
= [];
1347 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1348 var hi
= this.toDataYCoord(lowY
, i
);
1349 var low
= this.toDataYCoord(highY
, i
);
1350 this.axes_
[i
].valueWindow
= [low
, hi
];
1351 valueRanges
.push([low
, hi
]);
1355 if (this.attr_("zoomCallback")) {
1356 var xRange
= this.xAxisRange();
1357 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1362 * Reset the zoom to the original view coordinates. This is the same as
1363 * double-clicking on the graph.
1367 Dygraph
.prototype.doUnzoom_
= function() {
1369 if (this.dateWindow_
!= null) {
1371 this.dateWindow_
= null;
1374 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1375 if (this.axes_
[i
].valueWindow
!= null) {
1377 delete this.axes_
[i
].valueWindow
;
1382 // Putting the drawing operation before the callback because it resets
1385 if (this.attr_("zoomCallback")) {
1386 var minDate
= this.rawData_
[0][0];
1387 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1388 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1394 * When the mouse moves in the canvas, display information about a nearby data
1395 * point and draw dots over those points in the data series. This function
1396 * takes care of cleanup of previously-drawn dots.
1397 * @param {Object} event The mousemove event from the browser.
1400 Dygraph
.prototype.mouseMove_
= function(event
) {
1401 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1402 var points
= this.layout_
.points
;
1407 // Loop through all the points and find the date nearest to our current
1409 var minDist
= 1e+100;
1411 for (var i
= 0; i
< points
.length
; i
++) {
1412 var point
= points
[i
];
1413 if (point
== null) continue;
1414 var dist
= Math
.abs(point
.canvasx
- canvasx
);
1415 if (dist
> minDist
) continue;
1419 if (idx
>= 0) lastx
= points
[idx
].xval
;
1421 // Extract the points we've selected
1422 this.selPoints_
= [];
1423 var l
= points
.length
;
1424 if (!this.attr_("stackedGraph")) {
1425 for (var i
= 0; i
< l
; i
++) {
1426 if (points
[i
].xval
== lastx
) {
1427 this.selPoints_
.push(points
[i
]);
1431 // Need to 'unstack' points starting from the bottom
1432 var cumulative_sum
= 0;
1433 for (var i
= l
- 1; i
>= 0; i
--) {
1434 if (points
[i
].xval
== lastx
) {
1435 var p
= {}; // Clone the point since we modify it
1436 for (var k
in points
[i
]) {
1437 p
[k
] = points
[i
][k
];
1439 p
.yval
-= cumulative_sum
;
1440 cumulative_sum
+= p
.yval
;
1441 this.selPoints_
.push(p
);
1444 this.selPoints_
.reverse();
1447 if (this.attr_("highlightCallback")) {
1448 var px
= this.lastx_
;
1449 if (px
!== null && lastx
!= px
) {
1450 // only fire if the selected point has changed.
1451 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1455 // Save last x position for callbacks.
1456 this.lastx_
= lastx
;
1458 this.updateSelection_();
1462 * Transforms layout_.points index into data row number.
1463 * @param int layout_.points index
1464 * @return int row number, or -1 if none could be found.
1467 Dygraph
.prototype.idxToRow_
= function(idx
) {
1468 if (idx
< 0) return -1;
1470 for (var i
in this.layout_
.datasets
) {
1471 if (idx
< this.layout_
.datasets
[i
].length
) {
1472 return this.boundaryIds_
[0][0]+idx
;
1474 idx
-= this.layout_
.datasets
[i
].length
;
1480 * Draw dots over the selectied points in the data series. This function
1481 * takes care of cleanup of previously-drawn dots.
1484 Dygraph
.prototype.updateSelection_
= function() {
1485 // Clear the previously drawn vertical, if there is one
1486 var ctx
= this.canvas_
.getContext("2d");
1487 if (this.previousVerticalX_
>= 0) {
1488 // Determine the maximum highlight circle size.
1489 var maxCircleSize
= 0;
1490 var labels
= this.attr_('labels');
1491 for (var i
= 1; i
< labels
.length
; i
++) {
1492 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1493 if (r
> maxCircleSize
) maxCircleSize
= r
;
1495 var px
= this.previousVerticalX_
;
1496 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1497 2 * maxCircleSize
+ 2, this.height_
);
1500 var isOK
= function(x
) { return x
&& !isNaN(x
); };
1502 if (this.selPoints_
.length
> 0) {
1503 var canvasx
= this.selPoints_
[0].canvasx
;
1505 // Set the status message to indicate the selected point(s)
1506 var replace
= this.attr_('xValueFormatter')(this.lastx_
, this) + ":";
1507 var fmtFunc
= this.attr_('yValueFormatter');
1508 var clen
= this.colors_
.length
;
1510 if (this.attr_('showLabelsOnHighlight')) {
1511 // Set the status message to indicate the selected point(s)
1512 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1513 if (!this.attr_("labelsShowZeroValues") && this.selPoints_
[i
].yval
== 0) continue;
1514 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1515 if (this.attr_("labelsSeparateLines")) {
1518 var point
= this.selPoints_
[i
];
1519 var c
= new RGBColor(this.plotter_
.colors
[point
.name
]);
1520 var yval
= fmtFunc(point
.yval
);
1521 replace
+= " <b><font color='" + c
.toHex() + "'>"
1522 + point
.name
+ "</font></b>:"
1526 this.attr_("labelsDiv").innerHTML
= replace
;
1529 // Draw colored circles over the center of each selected point
1531 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1532 if (!isOK(this.selPoints_
[i
].canvasy
)) continue;
1534 this.attr_('highlightCircleSize', this.selPoints_
[i
].name
);
1536 ctx
.fillStyle
= this.plotter_
.colors
[this.selPoints_
[i
].name
];
1537 ctx
.arc(canvasx
, this.selPoints_
[i
].canvasy
, circleSize
,
1538 0, 2 * Math
.PI
, false);
1543 this.previousVerticalX_
= canvasx
;
1548 * Set manually set selected dots, and display information about them
1549 * @param int row number that should by highlighted
1550 * false value clears the selection
1553 Dygraph
.prototype.setSelection
= function(row
) {
1554 // Extract the points we've selected
1555 this.selPoints_
= [];
1558 if (row
!== false) {
1559 row
= row
-this.boundaryIds_
[0][0];
1562 if (row
!== false && row
>= 0) {
1563 for (var i
in this.layout_
.datasets
) {
1564 if (row
< this.layout_
.datasets
[i
].length
) {
1565 var point
= this.layout_
.points
[pos
+row
];
1567 if (this.attr_("stackedGraph")) {
1568 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1571 this.selPoints_
.push(point
);
1573 pos
+= this.layout_
.datasets
[i
].length
;
1577 if (this.selPoints_
.length
) {
1578 this.lastx_
= this.selPoints_
[0].xval
;
1579 this.updateSelection_();
1582 this.clearSelection();
1588 * The mouse has left the canvas. Clear out whatever artifacts remain
1589 * @param {Object} event the mouseout event from the browser.
1592 Dygraph
.prototype.mouseOut_
= function(event
) {
1593 if (this.attr_("unhighlightCallback")) {
1594 this.attr_("unhighlightCallback")(event
);
1597 if (this.attr_("hideOverlayOnMouseOut")) {
1598 this.clearSelection();
1603 * Remove all selection from the canvas
1606 Dygraph
.prototype.clearSelection
= function() {
1607 // Get rid of the overlay data
1608 var ctx
= this.canvas_
.getContext("2d");
1609 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1610 this.attr_("labelsDiv").innerHTML
= "";
1611 this.selPoints_
= [];
1616 * Returns the number of the currently selected row
1617 * @return int row number, of -1 if nothing is selected
1620 Dygraph
.prototype.getSelection
= function() {
1621 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1625 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1626 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1627 return row
+ this.boundaryIds_
[0][0];
1633 Dygraph
.zeropad
= function(x
) {
1634 if (x
< 10) return "0" + x
; else return "" + x
;
1638 * Return a string version of the hours, minutes and seconds portion of a date.
1639 * @param {Number} date The JavaScript date (ms since epoch)
1640 * @return {String} A time of the form "HH:MM:SS"
1643 Dygraph
.hmsString_
= function(date
) {
1644 var zeropad
= Dygraph
.zeropad
;
1645 var d
= new Date(date
);
1646 if (d
.getSeconds()) {
1647 return zeropad(d
.getHours()) + ":" +
1648 zeropad(d
.getMinutes()) + ":" +
1649 zeropad(d
.getSeconds());
1651 return zeropad(d
.getHours()) + ":" + zeropad(d
.getMinutes());
1656 * Convert a JS date to a string appropriate to display on an axis that
1657 * is displaying values at the stated granularity.
1658 * @param {Date} date The date to format
1659 * @param {Number} granularity One of the Dygraph granularity constants
1660 * @return {String} The formatted date
1663 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
1664 if (granularity
>= Dygraph
.DECADAL
) {
1665 return date
.strftime('%Y');
1666 } else if (granularity
>= Dygraph
.MONTHLY
) {
1667 return date
.strftime('%b %y');
1669 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
1670 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
1671 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
1673 return Dygraph
.hmsString_(date
.getTime());
1679 * Convert a JS date (millis since epoch) to YYYY/MM/DD
1680 * @param {Number} date The JavaScript date (ms since epoch)
1681 * @return {String} A date of the form "YYYY/MM/DD"
1684 Dygraph
.dateString_
= function(date
, self
) {
1685 var zeropad
= Dygraph
.zeropad
;
1686 var d
= new Date(date
);
1689 var year
= "" + d
.getFullYear();
1690 // Get a 0 padded month string
1691 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
1692 // Get a 0 padded day string
1693 var day
= zeropad(d
.getDate());
1696 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
1697 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
1699 return year
+ "/" + month + "/" + day
+ ret
;
1703 * Round a number to the specified number of digits past the decimal point.
1704 * @param {Number} num The number to round
1705 * @param {Number} places The number of decimals to which to round
1706 * @return {Number} The rounded number
1709 Dygraph
.round_
= function(num
, places
) {
1710 var shift
= Math
.pow(10, places
);
1711 return Math
.round(num
* shift
)/shift
;
1715 * Fires when there's data available to be graphed.
1716 * @param {String} data Raw CSV data to be plotted
1719 Dygraph
.prototype.loadedEvent_
= function(data
) {
1720 this.rawData_
= this.parseCSV_(data
);
1724 Dygraph
.prototype.months
= ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
1725 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1726 Dygraph
.prototype.quarters
= ["Jan", "Apr", "Jul", "Oct"];
1729 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1732 Dygraph
.prototype.addXTicks_
= function() {
1733 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1734 var startDate
, endDate
;
1735 if (this.dateWindow_
) {
1736 startDate
= this.dateWindow_
[0];
1737 endDate
= this.dateWindow_
[1];
1739 startDate
= this.rawData_
[0][0];
1740 endDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1743 var xTicks
= this.attr_('xTicker')(startDate
, endDate
, this);
1744 this.layout_
.updateOptions({xTicks
: xTicks
});
1747 // Time granularity enumeration
1748 Dygraph
.SECONDLY
= 0;
1749 Dygraph
.TWO_SECONDLY
= 1;
1750 Dygraph
.FIVE_SECONDLY
= 2;
1751 Dygraph
.TEN_SECONDLY
= 3;
1752 Dygraph
.THIRTY_SECONDLY
= 4;
1753 Dygraph
.MINUTELY
= 5;
1754 Dygraph
.TWO_MINUTELY
= 6;
1755 Dygraph
.FIVE_MINUTELY
= 7;
1756 Dygraph
.TEN_MINUTELY
= 8;
1757 Dygraph
.THIRTY_MINUTELY
= 9;
1758 Dygraph
.HOURLY
= 10;
1759 Dygraph
.TWO_HOURLY
= 11;
1760 Dygraph
.SIX_HOURLY
= 12;
1762 Dygraph
.WEEKLY
= 14;
1763 Dygraph
.MONTHLY
= 15;
1764 Dygraph
.QUARTERLY
= 16;
1765 Dygraph
.BIANNUAL
= 17;
1766 Dygraph
.ANNUAL
= 18;
1767 Dygraph
.DECADAL
= 19;
1768 Dygraph
.CENTENNIAL
= 20;
1769 Dygraph
.NUM_GRANULARITIES
= 21;
1771 Dygraph
.SHORT_SPACINGS
= [];
1772 Dygraph
.SHORT_SPACINGS
[Dygraph
.SECONDLY
] = 1000 * 1;
1773 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_SECONDLY
] = 1000 * 2;
1774 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_SECONDLY
] = 1000 * 5;
1775 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_SECONDLY
] = 1000 * 10;
1776 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_SECONDLY
] = 1000 * 30;
1777 Dygraph
.SHORT_SPACINGS
[Dygraph
.MINUTELY
] = 1000 * 60;
1778 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_MINUTELY
] = 1000 * 60 * 2;
1779 Dygraph
.SHORT_SPACINGS
[Dygraph
.FIVE_MINUTELY
] = 1000 * 60 * 5;
1780 Dygraph
.SHORT_SPACINGS
[Dygraph
.TEN_MINUTELY
] = 1000 * 60 * 10;
1781 Dygraph
.SHORT_SPACINGS
[Dygraph
.THIRTY_MINUTELY
] = 1000 * 60 * 30;
1782 Dygraph
.SHORT_SPACINGS
[Dygraph
.HOURLY
] = 1000 * 3600;
1783 Dygraph
.SHORT_SPACINGS
[Dygraph
.TWO_HOURLY
] = 1000 * 3600 * 2;
1784 Dygraph
.SHORT_SPACINGS
[Dygraph
.SIX_HOURLY
] = 1000 * 3600 * 6;
1785 Dygraph
.SHORT_SPACINGS
[Dygraph
.DAILY
] = 1000 * 86400;
1786 Dygraph
.SHORT_SPACINGS
[Dygraph
.WEEKLY
] = 1000 * 604800;
1790 // If we used this time granularity, how many ticks would there be?
1791 // This is only an approximation, but it's generally good enough.
1793 Dygraph
.prototype.NumXTicks
= function(start_time
, end_time
, granularity
) {
1794 if (granularity
< Dygraph
.MONTHLY
) {
1795 // Generate one tick mark for every fixed interval of time.
1796 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1797 return Math
.floor(0.5 + 1.0 * (end_time
- start_time
) / spacing
);
1799 var year_mod
= 1; // e.g. to only print one point every 10 years.
1800 var num_months
= 12;
1801 if (granularity
== Dygraph
.QUARTERLY
) num_months
= 3;
1802 if (granularity
== Dygraph
.BIANNUAL
) num_months
= 2;
1803 if (granularity
== Dygraph
.ANNUAL
) num_months
= 1;
1804 if (granularity
== Dygraph
.DECADAL
) { num_months
= 1; year_mod
= 10; }
1805 if (granularity
== Dygraph
.CENTENNIAL
) { num_months
= 1; year_mod
= 100; }
1807 var msInYear
= 365.2524 * 24 * 3600 * 1000;
1808 var num_years
= 1.0 * (end_time
- start_time
) / msInYear
;
1809 return Math
.floor(0.5 + 1.0 * num_years
* num_months
/ year_mod
);
1815 // Construct an x-axis of nicely-formatted times on meaningful boundaries
1816 // (e.g. 'Jan 09' rather than 'Jan 22, 2009').
1818 // Returns an array containing {v: millis, label: label} dictionaries.
1820 Dygraph
.prototype.GetXAxis
= function(start_time
, end_time
, granularity
) {
1821 var formatter
= this.attr_("xAxisLabelFormatter");
1823 if (granularity
< Dygraph
.MONTHLY
) {
1824 // Generate one tick mark for every fixed interval of time.
1825 var spacing
= Dygraph
.SHORT_SPACINGS
[granularity
];
1826 var format
= '%d%b'; // e.g. "1Jan"
1828 // Find a time less than start_time which occurs on a "nice" time boundary
1829 // for this granularity.
1830 var g
= spacing
/ 1000;
1831 var d
= new Date(start_time
);
1832 if (g
<= 60) { // seconds
1833 var x
= d
.getSeconds(); d
.setSeconds(x
- x
% g
);
1837 if (g
<= 60) { // minutes
1838 var x
= d
.getMinutes(); d
.setMinutes(x
- x
% g
);
1843 if (g
<= 24) { // days
1844 var x
= d
.getHours(); d
.setHours(x
- x
% g
);
1849 if (g
== 7) { // one week
1850 d
.setDate(d
.getDate() - d
.getDay());
1855 start_time
= d
.getTime();
1857 for (var t
= start_time
; t
<= end_time
; t
+= spacing
) {
1858 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1861 // Display a tick mark on the first of a set of months of each year.
1862 // Years get a tick mark iff y % year_mod == 0. This is useful for
1863 // displaying a tick mark once every 10 years, say, on long time scales.
1865 var year_mod
= 1; // e.g. to only print one point every 10 years.
1867 if (granularity
== Dygraph
.MONTHLY
) {
1868 months
= [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ];
1869 } else if (granularity
== Dygraph
.QUARTERLY
) {
1870 months
= [ 0, 3, 6, 9 ];
1871 } else if (granularity
== Dygraph
.BIANNUAL
) {
1873 } else if (granularity
== Dygraph
.ANNUAL
) {
1875 } else if (granularity
== Dygraph
.DECADAL
) {
1878 } else if (granularity
== Dygraph
.CENTENNIAL
) {
1882 this.warn("Span of dates is too long");
1885 var start_year
= new Date(start_time
).getFullYear();
1886 var end_year
= new Date(end_time
).getFullYear();
1887 var zeropad
= Dygraph
.zeropad
;
1888 for (var i
= start_year
; i
<= end_year
; i
++) {
1889 if (i
% year_mod
!= 0) continue;
1890 for (var j
= 0; j
< months
.length
; j
++) {
1891 var date_str
= i
+ "/" + zeropad(1 + months[j]) + "/01";
1892 var t
= Date
.parse(date_str
);
1893 if (t
< start_time
|| t
> end_time
) continue;
1894 ticks
.push({ v
:t
, label
: formatter(new Date(t
), granularity
) });
1904 * Add ticks to the x-axis based on a date range.
1905 * @param {Number} startDate Start of the date window (millis since epoch)
1906 * @param {Number} endDate End of the date window (millis since epoch)
1907 * @return {Array.<Object>} Array of {label, value} tuples.
1910 Dygraph
.dateTicker
= function(startDate
, endDate
, self
) {
1912 for (var i
= 0; i
< Dygraph
.NUM_GRANULARITIES
; i
++) {
1913 var num_ticks
= self
.NumXTicks(startDate
, endDate
, i
);
1914 if (self
.width_
/ num_ticks
>= self
.attr_('pixelsPerXLabel')) {
1921 return self
.GetXAxis(startDate
, endDate
, chosen
);
1923 // TODO(danvk): signal error.
1927 // This is a list of human-friendly values at which to show tick marks on a log
1928 // scale. It is k * 10^n, where k=1..9 and n=-39..+39, so:
1929 // ..., 1, 2, 3, 4, 5, ..., 9, 10, 20, 30, ..., 90, 100, 200, 300, ...
1930 // NOTE: this assumes that Dygraph.LOG_SCALE = 10.
1931 Dygraph
.PREFERRED_LOG_TICK_VALUES
= function() {
1933 for (var power
= -39; power
<= 39; power
++) {
1934 var range
= Math
.pow(10, power
);
1935 for (var mult
= 1; mult
<= 9; mult
++) {
1936 var val
= range
* mult
;
1943 // val is the value to search for
1944 // arry is the value over which to search
1945 // if abs > 0, find the lowest entry greater than val
1946 // if abs < 0, find the highest entry less than val
1947 // if abs == 0, find the entry that equals val.
1948 // Currently does not work when val is outside the range of arry's values.
1949 Dygraph
.binarySearch
= function(val
, arry
, abs
, low
, high
) {
1950 if (low
== null || high
== null) {
1952 high
= arry
.length
- 1;
1960 var validIndex
= function(idx
) {
1961 return idx
>= 0 && idx
< arry
.length
;
1963 var mid
= parseInt((low
+ high
) / 2);
1964 var element
= arry
[mid
];
1965 if (element
== val
) {
1968 if (element
> val
) {
1970 // Accept if element > val, but also if prior element < val.
1972 if (validIndex(idx
) && arry
[idx
] < val
) {
1976 return Dygraph
.binarySearch(val
, arry
, abs
, low
, mid
- 1);
1978 if (element
< val
) {
1980 // Accept if element < val, but also if prior element > val.
1982 if (validIndex(idx
) && arry
[idx
] > val
) {
1986 return Dygraph
.binarySearch(val
, arry
, abs
, mid
+ 1, high
);
1991 * Add ticks when the x axis has numbers on it (instead of dates)
1992 * TODO(konigsberg): Update comment.
1994 * @param {Number} minV minimum value
1995 * @param {Number} maxV maximum value
1997 * @param {function} attribute accessor function.
1998 * @return {Array.<Object>} Array of {label, value} tuples.
2001 Dygraph
.numericTicks
= function(minV
, maxV
, self
, axis_props
, vals
) {
2002 var attr
= function(k
) {
2003 if (axis_props
&& axis_props
.hasOwnProperty(k
)) return axis_props
[k
];
2004 return self
.attr_(k
);
2009 for (var i
= 0; i
< vals
.length
; i
++) {
2010 ticks
.push({v
: vals
[i
]});
2013 if (axis_props
&& attr("logscale")) {
2014 var pixelsPerTick
= attr('pixelsPerYLabel');
2015 // NOTE(konigsberg): Dan, should self.height_ be self.plotter_.area.h?
2016 var nTicks
= Math
.floor(self
.height_
/ pixelsPerTick
);
2017 var minIdx
= Dygraph
.binarySearch(minV
, Dygraph
.PREFERRED_LOG_TICK_VALUES
, 1);
2018 var maxIdx
= Dygraph
.binarySearch(maxV
, Dygraph
.PREFERRED_LOG_TICK_VALUES
, -1);
2023 maxIdx
= Dygraph
.PREFERRED_LOG_TICK_VALUES
.length
- 1;
2025 // Count the number of tick values would appear, if we can get at least
2026 // nTicks / 4 accept them
.
2027 var lastDisplayed
= null;
2028 if (maxIdx
- minIdx
>= nTicks
/ 4) {
2029 var axisId
= axis_props
.yAxisId
;
2030 for (var idx
= maxIdx
; idx
>= minIdx
; idx
--) {
2031 var tickValue
= Dygraph
.PREFERRED_LOG_TICK_VALUES
[idx
];
2032 var domCoord
= axis_props
.g
.toDomYCoord(tickValue
, axisId
);
2033 var tick
= { v
: tickValue
};
2034 if (lastDisplayed
== null) {
2036 tickValue
: tickValue
,
2040 if (domCoord
- lastDisplayed
.domCoord
>= pixelsPerTick
) {
2042 tickValue
: tickValue
,
2051 // Since we went in backwards order.
2056 // ticks.length won't be 0 if the log scale function finds values to insert.
2057 if (ticks
.length
== 0) {
2059 // Try labels every 1, 2, 5, 10, 20, 50, 100, etc.
2060 // Calculate the resulting tick spacing (i.e. this.height_ / nTicks
).
2061 // The first spacing greater than pixelsPerYLabel is what we use.
2062 // TODO(danvk): version that works on a log scale.
2063 if (attr("labelsKMG2")) {
2064 var mults
= [1, 2, 4, 8];
2066 var mults
= [1, 2, 5];
2068 var scale
, low_val
, high_val
, nTicks
;
2069 // TODO(danvk): make it possible to set this for x- and y-axes independently.
2070 var pixelsPerTick
= attr('pixelsPerYLabel');
2071 for (var i
= -10; i
< 50; i
++) {
2072 if (attr("labelsKMG2")) {
2073 var base_scale
= Math
.pow(16, i
);
2075 var base_scale
= Math
.pow(10, i
);
2077 for (var j
= 0; j
< mults
.length
; j
++) {
2078 scale
= base_scale
* mults
[j
];
2079 low_val
= Math
.floor(minV
/ scale
) * scale
;
2080 high_val
= Math
.ceil(maxV
/ scale
) * scale
;
2081 nTicks
= Math
.abs(high_val
- low_val
) / scale
;
2082 var spacing
= self
.height_
/ nTicks
;
2083 // wish I could break out of both loops at once...
2084 if (spacing
> pixelsPerTick
) break;
2086 if (spacing
> pixelsPerTick
) break;
2089 // Construct the set of ticks.
2090 // Allow reverse y-axis if it's explicitly requested.
2091 if (low_val
> high_val
) scale
*= -1;
2092 for (var i
= 0; i
< nTicks
; i
++) {
2093 var tickV
= low_val
+ i
* scale
;
2094 ticks
.push( {v
: tickV
} );
2099 // Add formatted labels to the ticks.
2102 if (attr("labelsKMB")) {
2104 k_labels
= [ "K", "M", "B", "T" ];
2106 if (attr("labelsKMG2")) {
2107 if (k
) self
.warn("Setting both labelsKMB and labelsKMG2. Pick one!");
2109 k_labels
= [ "k", "M", "G", "T" ];
2111 var formatter
= attr('yAxisLabelFormatter') ? attr('yAxisLabelFormatter') : attr('yValueFormatter');
2113 // Add labels to the ticks.
2114 for (var i
= 0; i
< ticks
.length
; i
++) {
2115 if (ticks
[i
].label
== null) {
2116 var tickV
= ticks
[i
].v
;
2117 var absTickV
= Math
.abs(tickV
);
2119 if (formatter
!= undefined
) {
2120 label
= formatter(tickV
);
2122 label
= Dygraph
.round_(tickV
, 2);
2124 if (k_labels
.length
) {
2125 // Round up to an appropriate unit.
2127 for (var j
= 3; j
>= 0; j
--, n
/= k
) {
2128 if (absTickV
>= n
) {
2129 label
= Dygraph
.round_(tickV
/ n
, 1) + k_labels
[j
];
2134 ticks
[i
].label
= label
;
2140 // Computes the range of the data series (including confidence intervals).
2141 // series is either [ [x1, y1], [x2, y2], ... ] or
2142 // [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2143 // Returns [low, high]
2144 Dygraph
.prototype.extremeValues_
= function(series
) {
2145 var minY
= null, maxY
= null;
2147 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2149 // With custom bars, maxY is the max of the high values.
2150 for (var j
= 0; j
< series
.length
; j
++) {
2151 var y
= series
[j
][1][0];
2153 var low
= y
- series
[j
][1][1];
2154 var high
= y
+ series
[j
][1][2];
2155 if (low
> y
) low
= y
; // this can happen with custom bars,
2156 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
2157 if (maxY
== null || high
> maxY
) {
2160 if (minY
== null || low
< minY
) {
2165 for (var j
= 0; j
< series
.length
; j
++) {
2166 var y
= series
[j
][1];
2167 if (y
=== null || isNaN(y
)) continue;
2168 if (maxY
== null || y
> maxY
) {
2171 if (minY
== null || y
< minY
) {
2177 return [minY
, maxY
];
2181 * This function is called once when the chart's data is changed or the options
2182 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2183 * idea is that values derived from the chart's data can be computed here,
2184 * rather than every time the chart is drawn. This includes things like the
2185 * number of axes, rolling averages, etc.
2187 Dygraph
.prototype.predraw_
= function() {
2188 // TODO(danvk): move more computations out of drawGraph_ and into here.
2189 this.computeYAxes_();
2191 // Create a new plotter.
2192 if (this.plotter_
) this.plotter_
.clear();
2193 this.plotter_
= new DygraphCanvasRenderer(this,
2194 this.hidden_
, this.layout_
,
2195 this.renderOptions_
);
2197 // The roller sits in the bottom left corner of the chart. We don't know where
2198 // this will be until the options are available, so it's positioned here.
2199 this.createRollInterface_();
2201 // Same thing applies for the labelsDiv. It's right edge should be flush with
2202 // the right edge of the charting area (which may not be the same as the right
2203 // edge of the div, if we have two y-axes.
2204 this.positionLabelsDiv_();
2206 // If the data or options have changed, then we'd better redraw.
2211 * Update the graph with new data. This method is called when the viewing area
2212 * has changed. If the underlying data or options have changed, predraw_ will
2213 * be called before drawGraph_ is called.
2216 Dygraph
.prototype.drawGraph_
= function() {
2217 var data
= this.rawData_
;
2219 // This is used to set the second parameter to drawCallback, below.
2220 var is_initial_draw
= this.is_initial_draw_
;
2221 this.is_initial_draw_
= false;
2223 var minY
= null, maxY
= null;
2224 this.layout_
.removeAllDatasets();
2226 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
2228 // Loop over the fields (series). Go from the last to the first,
2229 // because if they're stacked that's how we accumulate the values.
2231 var cumulative_y
= []; // For stacked series.
2234 var extremes
= {}; // series name -> [low, high]
2236 // Loop over all fields and create datasets
2237 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
2238 if (!this.visibility()[i
- 1]) continue;
2240 var seriesName
= this.attr_("labels")[i
];
2241 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
2242 var logScale
= this.attr_('logscale', i
);
2245 for (var j
= 0; j
< data
.length
; j
++) {
2246 var date
= data
[j
][0];
2247 var point
= data
[j
][i
];
2249 // On the log scale, points less than zero do not exist.
2250 // This will create a gap in the chart. Note that this ignores
2251 // connectSeparatedPoints.
2255 series
.push([date
, point
]);
2257 if (point
!= null || !connectSeparatedPoints
) {
2258 series
.push([date
, point
]);
2263 // TODO(danvk): move this into predraw_. It's insane to do it here.
2264 series
= this.rollingAverage(series
, this.rollPeriod_
);
2266 // Prune down to the desired range, if necessary (for zooming)
2267 // Because there can be lines going to points outside of the visible area,
2268 // we actually prune to visible points, plus one on either side.
2269 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2270 if (this.dateWindow_
) {
2271 var low
= this.dateWindow_
[0];
2272 var high
= this.dateWindow_
[1];
2274 // TODO(danvk): do binary search instead of linear search.
2275 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2276 var firstIdx
= null, lastIdx
= null;
2277 for (var k
= 0; k
< series
.length
; k
++) {
2278 if (series
[k
][0] >= low
&& firstIdx
=== null) {
2281 if (series
[k
][0] <= high
) {
2285 if (firstIdx
=== null) firstIdx
= 0;
2286 if (firstIdx
> 0) firstIdx
--;
2287 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2288 if (lastIdx
< series
.length
- 1) lastIdx
++;
2289 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
2290 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
2291 pruned
.push(series
[k
]);
2295 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
2298 var seriesExtremes
= this.extremeValues_(series
);
2301 for (var j
=0; j
<series
.length
; j
++) {
2302 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
2305 } else if (this.attr_("stackedGraph")) {
2306 var l
= series
.length
;
2308 for (var j
= 0; j
< l
; j
++) {
2309 // If one data set has a NaN, let all subsequent stacked
2310 // sets inherit the NaN -- only start at 0 for the first set.
2311 var x
= series
[j
][0];
2312 if (cumulative_y
[x
] === undefined
) {
2313 cumulative_y
[x
] = 0;
2316 actual_y
= series
[j
][1];
2317 cumulative_y
[x
] += actual_y
;
2319 series
[j
] = [x
, cumulative_y
[x
]]
2321 if (cumulative_y
[x
] > seriesExtremes
[1]) {
2322 seriesExtremes
[1] = cumulative_y
[x
];
2324 if (cumulative_y
[x
] < seriesExtremes
[0]) {
2325 seriesExtremes
[0] = cumulative_y
[x
];
2329 extremes
[seriesName
] = seriesExtremes
;
2331 datasets
[i
] = series
;
2334 for (var i
= 1; i
< datasets
.length
; i
++) {
2335 if (!this.visibility()[i
- 1]) continue;
2336 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
2339 // TODO(danvk): this method doesn't need to return anything.
2340 var out
= this.computeYAxisRanges_(extremes
);
2342 var seriesToAxisMap
= out
[1];
2343 this.layout_
.updateOptions( { yAxes
: axes
,
2344 seriesToAxisMap
: seriesToAxisMap
2349 // Tell PlotKit to use this new data and render itself
2350 this.layout_
.updateOptions({dateWindow
: this.dateWindow_
});
2351 this.layout_
.evaluateWithError();
2352 this.plotter_
.clear();
2353 this.plotter_
.render();
2354 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
2355 this.canvas_
.height
);
2357 if (this.attr_("drawCallback") !== null) {
2358 this.attr_("drawCallback")(this, is_initial_draw
);
2363 * Determine properties of the y-axes which are independent of the data
2364 * currently being displayed. This includes things like the number of axes and
2365 * the style of the axes. It does not include the range of each axis and its
2367 * This fills in this.axes_ and this.seriesToAxisMap_.
2368 * axes_ = [ { options } ]
2369 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2370 * indices are into the axes_ array.
2372 Dygraph
.prototype.computeYAxes_
= function() {
2373 this.axes_
= [{ yAxisId
: 0, g
: this }]; // always have at least one y-axis.
2374 this.seriesToAxisMap_
= {};
2376 // Get a list of series names.
2377 var labels
= this.attr_("labels");
2379 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2381 // all options which could be applied per-axis:
2389 'axisLabelFontSize',
2394 // Copy global axis options over to the first axis.
2395 for (var i
= 0; i
< axisOptions
.length
; i
++) {
2396 var k
= axisOptions
[i
];
2397 var v
= this.attr_(k
);
2398 if (v
) this.axes_
[0][k
] = v
;
2401 // Go through once and add all the axes.
2402 for (var seriesName
in series
) {
2403 if (!series
.hasOwnProperty(seriesName
)) continue;
2404 var axis
= this.attr_("axis", seriesName
);
2406 this.seriesToAxisMap_
[seriesName
] = 0;
2409 if (typeof(axis
) == 'object') {
2410 // Add a new axis, making a copy of its per-axis options.
2412 Dygraph
.update(opts
, this.axes_
[0]);
2413 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2414 var yAxisId
= this.axes_
.length
;
2415 opts
.yAxisId
= yAxisId
;
2417 Dygraph
.update(opts
, axis
);
2418 this.axes_
.push(opts
);
2419 this.seriesToAxisMap_
[seriesName
] = yAxisId
;
2423 // Go through one more time and assign series to an axis defined by another
2424 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2425 for (var seriesName
in series
) {
2426 if (!series
.hasOwnProperty(seriesName
)) continue;
2427 var axis
= this.attr_("axis", seriesName
);
2428 if (typeof(axis
) == 'string') {
2429 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2430 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2431 "series " + axis
+ ", which does not define its own axis.");
2434 var idx
= this.seriesToAxisMap_
[axis
];
2435 this.seriesToAxisMap_
[seriesName
] = idx
;
2439 // Now we remove series from seriesToAxisMap_ which are not visible. We do
2440 // this last so that hiding the first series doesn't destroy the axis
2441 // properties of the primary axis.
2442 var seriesToAxisFiltered
= {};
2443 var vis
= this.visibility();
2444 for (var i
= 1; i
< labels
.length
; i
++) {
2446 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
2448 this.seriesToAxisMap_
= seriesToAxisFiltered
;
2452 * Returns the number of y-axes on the chart.
2453 * @return {Number} the number of axes.
2455 Dygraph
.prototype.numAxes
= function() {
2457 for (var series
in this.seriesToAxisMap_
) {
2458 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2459 var idx
= this.seriesToAxisMap_
[series
];
2460 if (idx
> last_axis
) last_axis
= idx
;
2462 return 1 + last_axis
;
2466 * Determine the value range and tick marks for each axis.
2467 * @param {Object} extremes A mapping from seriesName -> [low, high]
2468 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2470 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2471 // Build a map from axis number -> [list of series names]
2472 var seriesForAxis
= [];
2473 for (var series
in this.seriesToAxisMap_
) {
2474 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2475 var idx
= this.seriesToAxisMap_
[series
];
2476 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2477 seriesForAxis
[idx
].push(series
);
2480 // Compute extreme values, a span and tick marks for each axis.
2481 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2482 var axis
= this.axes_
[i
];
2483 if (axis
.valueWindow
) {
2484 // This is only set if the user has zoomed on the y-axis. It is never set
2485 // by a user. It takes precedence over axis.valueRange because, if you set
2486 // valueRange, you'd still expect to be able to pan.
2487 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2488 } else if (axis
.valueRange
) {
2489 // This is a user-set value range for this axis.
2490 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2492 // Calculate the extremes of extremes.
2493 var series
= seriesForAxis
[i
];
2494 var minY
= Infinity
; // extremes[series[0]][0];
2495 var maxY
= -Infinity
; // extremes[series[0]][1];
2496 for (var j
= 0; j
< series
.length
; j
++) {
2497 minY
= Math
.min(extremes
[series
[j
]][0], minY
);
2498 maxY
= Math
.max(extremes
[series
[j
]][1], maxY
);
2500 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2502 // Add some padding and round up to an integer to be human-friendly.
2503 var span
= maxY
- minY
;
2504 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2505 if (span
== 0) { span
= maxY
; }
2509 if (axis
.logscale
) {
2510 var maxAxisY
= maxY
+ 0.1 * span
;
2511 var minAxisY
= minY
;
2513 var maxAxisY
= maxY
+ 0.1 * span
;
2514 var minAxisY
= minY
- 0.1 * span
;
2516 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2517 if (!this.attr_("avoidMinZero")) {
2518 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2519 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2522 if (this.attr_("includeZero")) {
2523 if (maxY
< 0) maxAxisY
= 0;
2524 if (minY
> 0) minAxisY
= 0;
2528 axis
.computedValueRange
= [minAxisY
, maxAxisY
];
2531 // Add ticks. By default, all axes inherit the tick positions of the
2532 // primary axis. However, if an axis is specifically marked as having
2533 // independent ticks, then that is permissible as well.
2534 if (i
== 0 || axis
.independentTicks
) {
2536 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2537 axis
.computedValueRange
[1],
2541 var p_axis
= this.axes_
[0];
2542 var p_ticks
= p_axis
.ticks
;
2543 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2544 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2545 var tick_values
= [];
2546 for (var i
= 0; i
< p_ticks
.length
; i
++) {
2547 var y_frac
= (p_ticks
[i
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2548 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2549 tick_values
.push(y_val
);
2553 Dygraph
.numericTicks(axis
.computedValueRange
[0],
2554 axis
.computedValueRange
[1],
2555 this, axis
, tick_values
);
2559 return [this.axes_
, this.seriesToAxisMap_
];
2563 * Calculates the rolling average of a data set.
2564 * If originalData is [label, val], rolls the average of those.
2565 * If originalData is [label, [, it's interpreted as [value, stddev]
2566 * and the roll is returned in the same form, with appropriately reduced
2567 * stddev for each value.
2568 * Note that this is where fractional input (i.e. '5/10') is converted into
2570 * @param {Array} originalData The data in the appropriate format (see above)
2571 * @param {Number} rollPeriod The number of days over which to average the data
2573 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2574 if (originalData
.length
< 2)
2575 return originalData
;
2576 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
- 1);
2577 var rollingData
= [];
2578 var sigma
= this.attr_("sigma");
2580 if (this.fractions_
) {
2582 var den
= 0; // numerator/denominator
2584 for (var i
= 0; i
< originalData
.length
; i
++) {
2585 num
+= originalData
[i
][1][0];
2586 den
+= originalData
[i
][1][1];
2587 if (i
- rollPeriod
>= 0) {
2588 num
-= originalData
[i
- rollPeriod
][1][0];
2589 den
-= originalData
[i
- rollPeriod
][1][1];
2592 var date
= originalData
[i
][0];
2593 var value
= den
? num
/ den
: 0.0;
2594 if (this.attr_("errorBars")) {
2595 if (this.wilsonInterval_
) {
2596 // For more details on this confidence interval, see:
2597 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2599 var p
= value
< 0 ? 0 : value
, n
= den
;
2600 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2601 var denom
= 1 + sigma
* sigma
/ den
;
2602 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2603 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2604 rollingData
[i
] = [date
,
2605 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2607 rollingData
[i
] = [date
, [0, 0, 0]];
2610 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2611 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2614 rollingData
[i
] = [date
, mult
* value
];
2617 } else if (this.attr_("customBars")) {
2622 for (var i
= 0; i
< originalData
.length
; i
++) {
2623 var data
= originalData
[i
][1];
2625 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2627 if (y
!= null && !isNaN(y
)) {
2633 if (i
- rollPeriod
>= 0) {
2634 var prev
= originalData
[i
- rollPeriod
];
2635 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2642 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2643 1.0 * (mid
- low
) / count
,
2644 1.0 * (high
- mid
) / count
]];
2647 // Calculate the rolling average for the first rollPeriod - 1 points where
2648 // there is not enough data to roll over the full number of days
2649 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2650 if (!this.attr_("errorBars")){
2651 if (rollPeriod
== 1) {
2652 return originalData
;
2655 for (var i
= 0; i
< originalData
.length
; i
++) {
2658 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2659 var y
= originalData
[j
][1];
2660 if (y
== null || isNaN(y
)) continue;
2662 sum
+= originalData
[j
][1];
2665 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2667 rollingData
[i
] = [originalData
[i
][0], null];
2672 for (var i
= 0; i
< originalData
.length
; i
++) {
2676 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2677 var y
= originalData
[j
][1][0];
2678 if (y
== null || isNaN(y
)) continue;
2680 sum
+= originalData
[j
][1][0];
2681 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2684 var stddev
= Math
.sqrt(variance
) / num_ok
;
2685 rollingData
[i
] = [originalData
[i
][0],
2686 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2688 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2698 * Parses a date, returning the number of milliseconds since epoch. This can be
2699 * passed in as an xValueParser in the Dygraph constructor.
2700 * TODO(danvk): enumerate formats that this understands.
2701 * @param {String} A date in YYYYMMDD format.
2702 * @return {Number} Milliseconds since epoch.
2705 Dygraph
.dateParser
= function(dateStr
, self
) {
2708 if (dateStr
.search("-") != -1) { // e.g. '2009-7-12' or '2009-07-12'
2709 dateStrSlashed
= dateStr
.replace("-", "/", "g");
2710 while (dateStrSlashed
.search("-") != -1) {
2711 dateStrSlashed
= dateStrSlashed
.replace("-", "/");
2713 d
= Date
.parse(dateStrSlashed
);
2714 } else if (dateStr
.length
== 8) { // e.g. '20090712'
2715 // TODO(danvk): remove support for this format. It's confusing.
2716 dateStrSlashed
= dateStr
.substr(0,4) + "/" + dateStr
.substr(4,2)
2717 + "/" + dateStr
.substr(6,2);
2718 d
= Date
.parse(dateStrSlashed
);
2720 // Any format that Date.parse will accept, e.g. "2009/07/12" or
2721 // "2009/07/12 12:34:56"
2722 d
= Date
.parse(dateStr
);
2725 if (!d
|| isNaN(d
)) {
2726 self
.error("Couldn't parse " + dateStr
+ " as a date");
2732 * Detects the type of the str (date or numeric) and sets the various
2733 * formatting attributes in this.attrs_ based on this type.
2734 * @param {String} str An x value.
2737 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2739 if (str
.indexOf('-') > 0 ||
2740 str
.indexOf('/') >= 0 ||
2741 isNaN(parseFloat(str
))) {
2743 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2744 // TODO(danvk): remove support for this format.
2749 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2750 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2751 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2752 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2754 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2755 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2756 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2757 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2762 * Parses a string in a special csv format. We expect a csv file where each
2763 * line is a date point, and the first field in each line is the date string.
2764 * We also expect that all remaining fields represent series.
2765 * if the errorBars attribute is set, then interpret the fields as:
2766 * date, series1, stddev1, series2, stddev2, ...
2767 * @param {Array.<Object>} data See above.
2770 * @return Array.<Object> An array with one entry for each row. These entries
2771 * are an array of cells in that row. The first entry is the parsed x-value for
2772 * the row. The second, third, etc. are the y-values. These can take on one of
2773 * three forms, depending on the CSV and constructor parameters:
2775 * 2. [ value, stddev ]
2776 * 3. [ low value, center value, high value ]
2778 Dygraph
.prototype.parseCSV_
= function(data
) {
2780 var lines
= data
.split("\n");
2782 // Use the default delimiter or fall back to a tab if that makes sense.
2783 var delim
= this.attr_('delimiter');
2784 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2789 if (this.labelsFromCSV_
) {
2791 this.attrs_
.labels
= lines
[0].split(delim
);
2794 // Parse the x as a float or return null if it's not a number.
2795 var parseFloatOrNull
= function(x
) {
2796 var val
= parseFloat(x
);
2797 // isFinite() returns false for NaN and +/-Infinity
.
2798 return isFinite(val
) ? val
: null;
2802 var defaultParserSet
= false; // attempt to auto-detect x value type
2803 var expectedCols
= this.attr_("labels").length
;
2804 var outOfOrder
= false;
2805 for (var i
= start
; i
< lines
.length
; i
++) {
2806 var line
= lines
[i
];
2807 if (line
.length
== 0) continue; // skip blank lines
2808 if (line
[0] == '#') continue; // skip comment lines
2809 var inFields
= line
.split(delim
);
2810 if (inFields
.length
< 2) continue;
2813 if (!defaultParserSet
) {
2814 this.detectTypeFromString_(inFields
[0]);
2815 xParser
= this.attr_("xValueParser");
2816 defaultParserSet
= true;
2818 fields
[0] = xParser(inFields
[0], this);
2820 // If fractions are expected, parse the numbers as "A/B
"
2821 if (this.fractions_) {
2822 for (var j = 1; j < inFields.length; j++) {
2823 // TODO(danvk): figure out an appropriate way to flag parse errors.
2824 var vals = inFields[j].split("/");
2825 fields[j] = [parseFloatOrNull(vals[0]), parseFloatOrNull(vals[1])];
2827 } else if (this.attr_("errorBars
")) {
2828 // If there are error bars, values are (value, stddev) pairs
2829 for (var j = 1; j < inFields.length; j += 2)
2830 fields[(j + 1) / 2] = [parseFloatOrNull(inFields[j]),
2831 parseFloatOrNull(inFields[j + 1])];
2832 } else if (this.attr_("customBars
")) {
2833 // Bars are a low;center;high tuple
2834 for (var j = 1; j < inFields.length; j++) {
2835 var vals = inFields[j].split(";");
2836 fields[j] = [ parseFloatOrNull(vals[0]),
2837 parseFloatOrNull(vals[1]),
2838 parseFloatOrNull(vals[2]) ];
2841 // Values are just numbers
2842 for (var j = 1; j < inFields.length; j++) {
2843 fields[j] = parseFloatOrNull(inFields[j]);
2846 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2851 if (fields.length != expectedCols) {
2852 this.error("Number of columns
in line
" + i + " (" + fields.length +
2853 ") does not agree
with number of
labels (" + expectedCols +
2859 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2860 ret.sort(function(a,b) { return a[0] - b[0] });
2867 * The user has provided their data as a pre-packaged JS array. If the x values
2868 * are numeric, this is the same as dygraphs' internal format. If the x values
2869 * are dates, we need to convert them from Date objects to ms since epoch.
2870 * @param {Array.<Object>} data
2871 * @return {Array.<Object>} data with numeric x values.
2873 Dygraph.prototype.parseArray_ = function(data) {
2874 // Peek at the first x value to see if it's numeric.
2875 if (data.length == 0) {
2876 this.error("Can
't plot empty data set");
2879 if (data[0].length == 0) {
2880 this.error("Data set cannot contain an empty row");
2884 if (this.attr_("labels") == null) {
2885 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2886 "in the options parameter");
2887 this.attrs_.labels = [ "X" ];
2888 for (var i = 1; i < data[0].length; i++) {
2889 this.attrs_.labels.push("Y" + i);
2893 if (Dygraph.isDateLike(data[0][0])) {
2894 // Some intelligent defaults for a date x-axis.
2895 this.attrs_.xValueFormatter = Dygraph.dateString_;
2896 this.attrs_.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
2897 this.attrs_.xTicker = Dygraph.dateTicker;
2899 // Assume they're all dates
.
2900 var parsedData
= Dygraph
.clone(data
);
2901 for (var i
= 0; i
< data
.length
; i
++) {
2902 if (parsedData
[i
].length
== 0) {
2903 this.error("Row " + (1 + i
) + " of data is empty");
2906 if (parsedData
[i
][0] == null
2907 || typeof(parsedData
[i
][0].getTime
) != 'function'
2908 || isNaN(parsedData
[i
][0].getTime())) {
2909 this.error("x value in row " + (1 + i
) + " is not a Date");
2912 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2916 // Some intelligent defaults for a numeric x-axis.
2917 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2918 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2924 * Parses a DataTable object from gviz.
2925 * The data is expected to have a first column that is either a date or a
2926 * number. All subsequent columns must be numbers. If there is a clear mismatch
2927 * between this.xValueParser_ and the type of the first column, it will be
2928 * fixed. Fills out rawData_.
2929 * @param {Array.<Object>} data See above.
2932 Dygraph
.prototype.parseDataTable_
= function(data
) {
2933 var cols
= data
.getNumberOfColumns();
2934 var rows
= data
.getNumberOfRows();
2936 var indepType
= data
.getColumnType(0);
2937 if (indepType
== 'date' || indepType
== 'datetime') {
2938 this.attrs_
.xValueFormatter
= Dygraph
.dateString_
;
2939 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2940 this.attrs_
.xTicker
= Dygraph
.dateTicker
;
2941 this.attrs_
.xAxisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2942 } else if (indepType
== 'number') {
2943 this.attrs_
.xValueFormatter
= function(x
) { return x
; };
2944 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2945 this.attrs_
.xTicker
= Dygraph
.numericTicks
;
2946 this.attrs_
.xAxisLabelFormatter
= this.attrs_
.xValueFormatter
;
2948 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2949 "column 1 of DataTable input (Got '" + indepType
+ "')");
2953 // Array of the column indices which contain data (and not annotations).
2955 var annotationCols
= {}; // data index -> [annotation cols]
2956 var hasAnnotations
= false;
2957 for (var i
= 1; i
< cols
; i
++) {
2958 var type
= data
.getColumnType(i
);
2959 if (type
== 'number') {
2961 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2962 // This is OK -- it's an annotation column.
2963 var dataIdx
= colIdx
[colIdx
.length
- 1];
2964 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2965 annotationCols
[dataIdx
] = [i
];
2967 annotationCols
[dataIdx
].push(i
);
2969 hasAnnotations
= true;
2971 this.error("Only 'number' is supported as a dependent type with Gviz." +
2972 " 'string' is only supported if displayAnnotations is true");
2976 // Read column labels
2977 // TODO(danvk): add support back for errorBars
2978 var labels
= [data
.getColumnLabel(0)];
2979 for (var i
= 0; i
< colIdx
.length
; i
++) {
2980 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2981 if (this.attr_("errorBars")) i
+= 1;
2983 this.attrs_
.labels
= labels
;
2984 cols
= labels
.length
;
2987 var outOfOrder
= false;
2988 var annotations
= [];
2989 for (var i
= 0; i
< rows
; i
++) {
2991 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2992 data
.getValue(i
, 0) === null) {
2993 this.warn("Ignoring row " + i
+
2994 " of DataTable because of undefined or null first column.");
2998 if (indepType
== 'date' || indepType
== 'datetime') {
2999 row
.push(data
.getValue(i
, 0).getTime());
3001 row
.push(data
.getValue(i
, 0));
3003 if (!this.attr_("errorBars")) {
3004 for (var j
= 0; j
< colIdx
.length
; j
++) {
3005 var col
= colIdx
[j
];
3006 row
.push(data
.getValue(i
, col
));
3007 if (hasAnnotations
&&
3008 annotationCols
.hasOwnProperty(col
) &&
3009 data
.getValue(i
, annotationCols
[col
][0]) != null) {
3011 ann
.series
= data
.getColumnLabel(col
);
3013 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
3015 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
3016 if (k
) ann
.text
+= "\n";
3017 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
3019 annotations
.push(ann
);
3023 for (var j
= 0; j
< cols
- 1; j
++) {
3024 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
3027 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
3031 // Strip out infinities, which give dygraphs problems later on.
3032 for (var j
= 0; j
< row
.length
; j
++) {
3033 if (!isFinite(row
[j
])) row
[j
] = null;
3039 this.warn("DataTable is out of order; order it correctly to speed loading.");
3040 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
3042 this.rawData_
= ret
;
3044 if (annotations
.length
> 0) {
3045 this.setAnnotations(annotations
, true);
3049 // These functions are all based on MochiKit.
3050 Dygraph
.update
= function (self
, o
) {
3051 if (typeof(o
) != 'undefined' && o
!== null) {
3053 if (o
.hasOwnProperty(k
)) {
3061 Dygraph
.isArrayLike
= function (o
) {
3062 var typ
= typeof(o
);
3064 (typ
!= 'object' && !(typ
== 'function' &&
3065 typeof(o
.item
) == 'function')) ||
3067 typeof(o
.length
) != 'number' ||
3075 Dygraph
.isDateLike
= function (o
) {
3076 if (typeof(o
) != "object" || o
=== null ||
3077 typeof(o
.getTime
) != 'function') {
3083 Dygraph
.clone
= function(o
) {
3084 // TODO(danvk): figure out how MochiKit's version works
3086 for (var i
= 0; i
< o
.length
; i
++) {
3087 if (Dygraph
.isArrayLike(o
[i
])) {
3088 r
.push(Dygraph
.clone(o
[i
]));
3098 * Get the CSV data. If it's in a function, call that function. If it's in a
3099 * file, do an XMLHttpRequest to get it.
3102 Dygraph
.prototype.start_
= function() {
3103 if (typeof this.file_
== 'function') {
3104 // CSV string. Pretend we got it via XHR.
3105 this.loadedEvent_(this.file_());
3106 } else if (Dygraph
.isArrayLike(this.file_
)) {
3107 this.rawData_
= this.parseArray_(this.file_
);
3109 } else if (typeof this.file_
== 'object' &&
3110 typeof this.file_
.getColumnRange
== 'function') {
3111 // must be a DataTable from gviz.
3112 this.parseDataTable_(this.file_
);
3114 } else if (typeof this.file_
== 'string') {
3115 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3116 if (this.file_
.indexOf('\n') >= 0) {
3117 this.loadedEvent_(this.file_
);
3119 var req
= new XMLHttpRequest();
3121 req
.onreadystatechange
= function () {
3122 if (req
.readyState
== 4) {
3123 if (req
.status
== 200) {
3124 caller
.loadedEvent_(req
.responseText
);
3129 req
.open("GET", this.file_
, true);
3133 this.error("Unknown data format: " + (typeof this.file_
));
3138 * Changes various properties of the graph. These can include:
3140 * <li>file: changes the source data for the graph</li>
3141 * <li>errorBars: changes whether the data contains stddev</li>
3143 * @param {Object} attrs The new properties and values
3145 Dygraph
.prototype.updateOptions
= function(attrs
) {
3146 // TODO(danvk): this is a mess. Rethink this function.
3147 if ('rollPeriod' in attrs
) {
3148 this.rollPeriod_
= attrs
.rollPeriod
;
3150 if ('dateWindow' in attrs
) {
3151 this.dateWindow_
= attrs
.dateWindow
;
3154 // TODO(danvk): validate per-series options.
3159 // highlightCircleSize
3161 Dygraph
.update(this.user_attrs_
, attrs
);
3162 Dygraph
.update(this.renderOptions_
, attrs
);
3164 this.labelsFromCSV_
= (this.attr_("labels") == null);
3166 // TODO(danvk): this doesn't match the constructor logic
3167 this.layout_
.updateOptions({ 'errorBars': this.attr_("errorBars") });
3168 if (attrs
['file']) {
3169 this.file_
= attrs
['file'];
3177 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3178 * containing div (which has presumably changed size since the dygraph was
3179 * instantiated. If the width/height are specified, the div will be resized.
3181 * This is far more efficient than destroying and re-instantiating a
3182 * Dygraph, since it doesn't have to reparse the underlying data.
3184 * @param {Number} width Width (in pixels)
3185 * @param {Number} height Height (in pixels)
3187 Dygraph
.prototype.resize
= function(width
, height
) {
3188 if (this.resize_lock
) {
3191 this.resize_lock
= true;
3193 if ((width
=== null) != (height
=== null)) {
3194 this.warn("Dygraph.resize() should be called with zero parameters or " +
3195 "two non-NULL parameters. Pretending it was zero.");
3196 width
= height
= null;
3199 // TODO(danvk): there should be a clear() method.
3200 this.maindiv_
.innerHTML
= "";
3201 this.attrs_
.labelsDiv
= null;
3204 this.maindiv_
.style
.width
= width
+ "px";
3205 this.maindiv_
.style
.height
= height
+ "px";
3206 this.width_
= width
;
3207 this.height_
= height
;
3209 this.width_
= this.maindiv_
.offsetWidth
;
3210 this.height_
= this.maindiv_
.offsetHeight
;
3213 this.createInterface_();
3216 this.resize_lock
= false;
3220 * Adjusts the number of days in the rolling average. Updates the graph to
3221 * reflect the new averaging period.
3222 * @param {Number} length Number of days over which to average the data.
3224 Dygraph
.prototype.adjustRoll
= function(length
) {
3225 this.rollPeriod_
= length
;
3230 * Returns a boolean array of visibility statuses.
3232 Dygraph
.prototype.visibility
= function() {
3233 // Do lazy-initialization, so that this happens after we know the number of
3235 if (!this.attr_("visibility")) {
3236 this.attrs_
["visibility"] = [];
3238 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
3239 this.attr_("visibility").push(true);
3241 return this.attr_("visibility");
3245 * Changes the visiblity of a series.
3247 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3248 var x
= this.visibility();
3249 if (num
< 0 || num
>= x
.length
) {
3250 this.warn("invalid series number in setVisibility: " + num
);
3258 * Update the list of annotations and redraw the chart.
3260 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3261 // Only add the annotation CSS rule once we know it will be used.
3262 Dygraph
.addAnnotationRule();
3263 this.annotations_
= ann
;
3264 this.layout_
.setAnnotations(this.annotations_
);
3265 if (!suppressDraw
) {
3271 * Return the list of annotations.
3273 Dygraph
.prototype.annotations
= function() {
3274 return this.annotations_
;
3278 * Get the index of a series (column) given its name. The first column is the
3279 * x-axis, so the data series start with index 1.
3281 Dygraph
.prototype.indexFromSetName
= function(name
) {
3282 var labels
= this.attr_("labels");
3283 for (var i
= 0; i
< labels
.length
; i
++) {
3284 if (labels
[i
] == name
) return i
;
3289 Dygraph
.addAnnotationRule
= function() {
3290 if (Dygraph
.addedAnnotationCSS
) return;
3292 var rule
= "border: 1px solid black; " +
3293 "background-color: white; " +
3294 "text-align: center;";
3296 var styleSheetElement
= document
.createElement("style");
3297 styleSheetElement
.type
= "text/css";
3298 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3300 // Find the first style sheet that we can access.
3301 // We may not add a rule to a style sheet from another domain for security
3302 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3303 // adds its own style sheets from google.com.
3304 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3305 if (document
.styleSheets
[i
].disabled
) continue;
3306 var mysheet
= document
.styleSheets
[i
];
3308 if (mysheet
.insertRule
) { // Firefox
3309 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3310 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3311 } else if (mysheet
.addRule
) { // IE
3312 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3314 Dygraph
.addedAnnotationCSS
= true;
3317 // Was likely a security exception.
3321 this.warn("Unable to add default annotation CSS rule; display may be off.");
3325 * Create a new canvas element. This is more complex than a simple
3326 * document.createElement("canvas") because of IE and excanvas.
3328 Dygraph
.createCanvas
= function() {
3329 var canvas
= document
.createElement("canvas");
3331 isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
3332 if (isIE
&& (typeof(G_vmlCanvasManager
) != 'undefined')) {
3333 canvas
= G_vmlCanvasManager
.initElement(canvas
);
3341 * A wrapper around Dygraph that implements the gviz API.
3342 * @param {Object} container The DOM object the visualization should live in.
3344 Dygraph
.GVizChart
= function(container
) {
3345 this.container
= container
;
3348 Dygraph
.GVizChart
.prototype.draw
= function(data
, options
) {
3349 // Clear out any existing dygraph.
3350 // TODO(danvk): would it make more sense to simply redraw using the current
3351 // date_graph object?
3352 this.container
.innerHTML
= '';
3353 if (typeof(this.date_graph
) != 'undefined') {
3354 this.date_graph
.destroy();
3357 this.date_graph
= new Dygraph(this.container
, data
, options
);
3361 * Google charts compatible setSelection
3362 * Only row selection is supported, all points in the row will be highlighted
3363 * @param {Array} array of the selected cells
3366 Dygraph
.GVizChart
.prototype.setSelection
= function(selection_array
) {
3368 if (selection_array
.length
) {
3369 row
= selection_array
[0].row
;
3371 this.date_graph
.setSelection(row
);
3375 * Google charts compatible getSelection implementation
3376 * @return {Array} array of the selected cells
3379 Dygraph
.GVizChart
.prototype.getSelection
= function() {
3382 var row
= this.date_graph
.getSelection();
3384 if (row
< 0) return selection
;
3387 for (var i
in this.date_graph
.layout_
.datasets
) {
3388 selection
.push({row
: row
, column
: col
});
3395 // Older pages may still use this name.
3396 DateGraph
= Dygraph
;