3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
8 * @fileoverview Creates an interactive, zoomable graph based on a CSV file or
9 * string. Dygraph can handle multiple series with or without error bars. The
10 * date/value ranges will be automatically set. Dygraph uses the
11 * <canvas> tag, so it only works in FF1.5+.
12 * @author danvdk@gmail.com (Dan Vanderkam)
15 <div id="graphdiv" style="width:800px; height:500px;"></div>
16 <script type="text/javascript">
17 new Dygraph(document.getElementById("graphdiv"),
18 "datafile.csv", // CSV file with headers
22 The CSV file is of the form
24 Date,SeriesA,SeriesB,SeriesC
28 If the 'errorBars' option is set in the constructor, the input should be of
30 Date,SeriesA,SeriesB,...
31 YYYYMMDD,A1,sigmaA1,B1,sigmaB1,...
32 YYYYMMDD,A2,sigmaA2,B2,sigmaB2,...
34 If the 'fractions' option is set, the input should be of the form:
36 Date,SeriesA,SeriesB,...
37 YYYYMMDD,A1/B1,A2/B2,...
38 YYYYMMDD,A1/B1,A2/B2,...
40 And error bars will be calculated automatically using a binomial distribution.
42 For further documentation and examples, see http://dygraphs.com/
47 * Creates an interactive, zoomable chart.
50 * @param {div | String} div A div or the id of a div into which to construct
52 * @param {String | Function} file A file containing CSV data or a function
53 * that returns this data. The most basic expected format for each line is
54 * "YYYY/MM/DD,val1,val2,...". For more information, see
55 * http://dygraphs.com/data.html.
56 * @param {Object} attrs Various other attributes, e.g. errorBars determines
57 * whether the input data contains error ranges. For a complete list of
58 * options, see http://dygraphs.com/options.html.
60 Dygraph
= function(div
, data
, opts
) {
61 if (arguments
.length
> 0) {
62 if (arguments
.length
== 4) {
63 // Old versions of dygraphs took in the series labels as a constructor
64 // parameter. This doesn't make sense anymore, but it's easy to continue
65 // to support this usage.
66 this.warn("Using deprecated four-argument dygraph constructor");
67 this.__old_init__(div
, data
, arguments
[2], arguments
[3]);
69 this.__init__(div
, data
, opts
);
74 Dygraph
.NAME
= "Dygraph";
75 Dygraph
.VERSION
= "1.2";
76 Dygraph
.__repr__
= function() {
77 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
81 * Returns information about the Dygraph class.
83 Dygraph
.toString
= function() {
84 return this.__repr__();
87 // Various default values
88 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
89 Dygraph
.DEFAULT_WIDTH
= 480;
90 Dygraph
.DEFAULT_HEIGHT
= 320;
92 // These are defined before DEFAULT_ATTRS so that it can refer to them.
95 * Return a string version of a number. This respects the digitsAfterDecimal
96 * and maxNumberWidth options.
97 * @param {Number} x The number to be formatted
98 * @param {Dygraph} opts An options view
99 * @param {String} name The name of the point's data series
100 * @param {Dygraph} g The dygraph object
102 Dygraph
.numberValueFormatter
= function(x
, opts
, pt
, g
) {
103 var sigFigs
= opts('sigFigs');
105 if (sigFigs
!== null) {
106 // User has opted for a fixed number of significant figures.
107 return Dygraph
.floatFormat(x
, sigFigs
);
110 var digits
= opts('digitsAfterDecimal');
111 var maxNumberWidth
= opts('maxNumberWidth');
113 // switch to scientific notation if we underflow or overflow fixed display.
115 (Math
.abs(x
) >= Math
.pow(10, maxNumberWidth
) ||
116 Math
.abs(x
) < Math
.pow(10, -digits
))) {
117 return x
.toExponential(digits
);
119 return '' + Dygraph
.round_(x
, digits
);
124 * variant for use as an axisLabelFormatter.
127 Dygraph
.numberAxisLabelFormatter
= function(x
, granularity
, opts
, g
) {
128 return Dygraph
.numberValueFormatter(x
, opts
, g
);
132 * Convert a JS date (millis since epoch) to YYYY/MM/DD
133 * @param {Number} date The JavaScript date (ms since epoch)
134 * @return {String} A date of the form "YYYY/MM/DD"
137 Dygraph
.dateString_
= function(date
) {
138 var zeropad
= Dygraph
.zeropad
;
139 var d
= new Date(date
);
142 var year
= "" + d
.getFullYear();
143 // Get a 0 padded month string
144 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
145 // Get a 0 padded day string
146 var day
= zeropad(d
.getDate());
149 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
150 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
152 return year
+ "/" + month + "/" + day
+ ret
;
156 * Convert a JS date to a string appropriate to display on an axis that
157 * is displaying values at the stated granularity.
158 * @param {Date} date The date to format
159 * @param {Number} granularity One of the Dygraph granularity constants
160 * @return {String} The formatted date
163 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
164 if (granularity
>= Dygraph
.DECADAL
) {
165 return date
.strftime('%Y');
166 } else if (granularity
>= Dygraph
.MONTHLY
) {
167 return date
.strftime('%b %y');
169 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
170 if (frac
== 0 || granularity
>= Dygraph
.DAILY
) {
171 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
173 return Dygraph
.hmsString_(date
.getTime());
179 // Default attribute values.
180 Dygraph
.DEFAULT_ATTRS
= {
181 highlightCircleSize
: 3,
185 // TODO(danvk): move defaults from createStatusMessage_ here.
187 labelsSeparateLines
: false,
188 labelsShowZeroValues
: true,
191 showLabelsOnHighlight
: true,
193 digitsAfterDecimal
: 2,
200 axisLabelFontSize
: 14,
206 xValueParser
: Dygraph
.dateParser
,
213 wilsonInterval
: true, // only relevant if fractions is true
217 connectSeparatedPoints
: false,
220 hideOverlayOnMouseOut
: true,
222 // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
223 legend
: 'onmouseover', // the only relevant value at the moment is 'always'.
228 // Sizes of the various chart labels.
235 axisLineColor
: "black",
238 axisLabelColor
: "black",
239 axisLabelFont
: "Arial", // TODO(danvk): is this implemented?
243 gridLineColor
: "rgb(128,128,128)",
245 interactionModel
: null, // will be set to Dygraph.Interaction.defaultModel
247 // Range selector options
248 showRangeSelector
: false,
249 rangeSelectorHeight
: 40,
250 rangeSelectorPlotStrokeColor
: "#808FAB",
251 rangeSelectorPlotFillColor
: "#A7B1C4",
257 axisLabelFormatter
: Dygraph
.dateAxisFormatter
,
258 valueFormatter
: Dygraph
.dateString_
,
259 ticker
: null // will be set in dygraph-tickers.js
263 valueFormatter
: Dygraph
.numberValueFormatter
,
264 axisLabelFormatter
: Dygraph
.numberAxisLabelFormatter
,
265 ticker
: null // will be set in dygraph-tickers.js
269 valueFormatter
: Dygraph
.numberValueFormatter
,
270 axisLabelFormatter
: Dygraph
.numberAxisLabelFormatter
,
271 ticker
: null // will be set in dygraph-tickers.js
276 // Directions for panning and zooming. Use bit operations when combined
277 // values are possible.
278 Dygraph
.HORIZONTAL
= 1;
279 Dygraph
.VERTICAL
= 2;
281 // Used for initializing annotation CSS rules only once.
282 Dygraph
.addedAnnotationCSS
= false;
284 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
285 // Labels is no longer a constructor parameter, since it's typically set
286 // directly from the data source. It also conains a name for the x-axis,
287 // which the previous constructor form did not.
288 if (labels
!= null) {
289 var new_labels
= ["Date"];
290 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
291 Dygraph
.update(attrs
, { 'labels': new_labels
});
293 this.__init__(div
, file
, attrs
);
297 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
298 * and context <canvas> inside of it. See the constructor for details.
300 * @param {Element} div the Element to render the graph into.
301 * @param {String | Function} file Source data
302 * @param {Object} attrs Miscellaneous other options
305 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
306 // Hack for IE: if we're using excanvas and the document hasn't finished
307 // loading yet (and hence may not have initialized whatever it needs to
308 // initialize), then keep calling this routine periodically until it has.
309 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
310 typeof(G_vmlCanvasManager
) != 'undefined' &&
311 document
.readyState
!= 'complete') {
313 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
317 // Support two-argument constructor
318 if (attrs
== null) { attrs
= {}; }
320 attrs
= Dygraph
.mapLegacyOptions_(attrs
);
323 Dygraph
.error("Constructing dygraph with a non-existent div!");
327 // Copy the important bits into the object
328 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
331 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
332 this.previousVerticalX_
= -1;
333 this.fractions_
= attrs
.fractions
|| false;
334 this.dateWindow_
= attrs
.dateWindow
|| null;
336 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
337 this.is_initial_draw_
= true;
338 this.annotations_
= [];
340 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
341 this.zoomed_x_
= false;
342 this.zoomed_y_
= false;
344 // Clear the div. This ensure that, if multiple dygraphs are passed the same
345 // div, then only one will be drawn.
348 // For historical reasons, the 'width' and 'height' options trump all CSS
349 // rules _except_ for an explicit 'width' or 'height' on the div.
350 // As an added convenience, if the div has zero height (like <div></div> does
351 // without any styles), then we use a default height/width
.
352 if (div
.style
.width
== '' && attrs
.width
) {
353 div
.style
.width
= attrs
.width
+ "px";
355 if (div
.style
.height
== '' && attrs
.height
) {
356 div
.style
.height
= attrs
.height
+ "px";
358 if (div
.style
.height
== '' && div
.clientHeight
== 0) {
359 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
360 if (div
.style
.width
== '') {
361 div
.style
.width
= Dygraph
.DEFAULT_WIDTH
+ "px";
364 // these will be zero if the dygraph's div is hidden.
365 this.width_
= div
.clientWidth
;
366 this.height_
= div
.clientHeight
;
368 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
369 if (attrs
['stackedGraph']) {
370 attrs
['fillGraph'] = true;
371 // TODO(nikhilk): Add any other stackedGraph checks here.
374 // Dygraphs has many options, some of which interact with one another.
375 // To keep track of everything, we maintain two sets of options:
377 // this.user_attrs_ only options explicitly set by the user.
378 // this.attrs_ defaults, options derived from user_attrs_, data.
380 // Options are then accessed this.attr_('attr'), which first looks at
381 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
382 // defaults without overriding behavior that the user specifically asks for.
383 this.user_attrs_
= {};
384 Dygraph
.update(this.user_attrs_
, attrs
);
386 // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
388 Dygraph
.updateDeep(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
390 this.boundaryIds_
= [];
392 // Create the containing DIV and other interactive elements
393 this.createInterface_();
399 * Returns the zoomed status of the chart for one or both axes.
401 * Axis is an optional parameter. Can be set to 'x' or 'y'.
403 * The zoomed status for an axis is set whenever a user zooms using the mouse
404 * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
405 * option is also specified).
407 Dygraph
.prototype.isZoomed
= function(axis
) {
408 if (axis
== null) return this.zoomed_x_
|| this.zoomed_y_
;
409 if (axis
== 'x') return this.zoomed_x_
;
410 if (axis
== 'y') return this.zoomed_y_
;
411 throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
415 * Returns information about the Dygraph object, including its containing ID.
417 Dygraph
.prototype.toString
= function() {
418 var maindiv
= this.maindiv_
;
419 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
420 return "[Dygraph " + id
+ "]";
425 * Returns the value of an option. This may be set by the user (either in the
426 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
428 * @param { String } name The name of the option, e.g. 'rollPeriod'.
429 * @param { String } [seriesName] The name of the series to which the option
430 * will be applied. If no per-series value of this option is available, then
431 * the global value is returned. This is optional.
432 * @return { ... } The value of the option.
434 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
435 // <REMOVE_FOR_COMBINED>
436 if (typeof(Dygraph
.OPTIONS_REFERENCE
) === 'undefined') {
437 this.error('Must include options reference JS for testing');
438 } else if (!Dygraph
.OPTIONS_REFERENCE
.hasOwnProperty(name
)) {
439 this.error('Dygraphs is using property ' + name
+ ', which has no entry ' +
440 'in the Dygraphs.OPTIONS_REFERENCE listing.');
441 // Only log this error once.
442 Dygraph
.OPTIONS_REFERENCE
[name
] = true;
444 // </REMOVE_FOR_COMBINED
>
446 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
447 this.user_attrs_
[seriesName
] != null &&
448 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
449 return this.user_attrs_
[seriesName
][name
];
450 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
451 return this.user_attrs_
[name
];
452 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
453 return this.attrs_
[name
];
461 * @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2')
462 * @return { ... } A function mapping string -> option value
464 Dygraph
.prototype.optionsViewForAxis_
= function(axis
) {
466 return function(opt
) {
467 var axis_opts
= self
.user_attrs_
['axes'];
468 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
][opt
]) {
469 return axis_opts
[axis
][opt
];
471 // user-specified attributes always trump defaults, even if they're less
473 if (typeof(self
.user_attrs_
[opt
]) != 'undefined') {
474 return self
.user_attrs_
[opt
];
477 axis_opts
= self
.attrs_
['axes'];
478 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
][opt
]) {
479 return axis_opts
[axis
][opt
];
481 // check old-style axis options
482 // TODO(danvk): add a deprecation warning if either of these match.
483 if (axis
== 'y' && self
.axes_
[0].hasOwnProperty(opt
)) {
484 return self
.axes_
[0][opt
];
485 } else if (axis
== 'y2' && self
.axes_
[1].hasOwnProperty(opt
)) {
486 return self
.axes_
[1][opt
];
488 return self
.attr_(opt
);
493 * Returns the current rolling period, as set by the user or an option.
494 * @return {Number} The number of points in the rolling window
496 Dygraph
.prototype.rollPeriod
= function() {
497 return this.rollPeriod_
;
501 * Returns the currently-visible x-range. This can be affected by zooming,
502 * panning or a call to updateOptions.
503 * Returns a two-element array: [left, right].
504 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
506 Dygraph
.prototype.xAxisRange
= function() {
507 return this.dateWindow_
? this.dateWindow_
: this.xAxisExtremes();
511 * Returns the lower- and upper-bound x-axis values of the
514 Dygraph
.prototype.xAxisExtremes
= function() {
515 var left
= this.rawData_
[0][0];
516 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
517 return [left
, right
];
521 * Returns the currently-visible y-range for an axis. This can be affected by
522 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
523 * called with no arguments, returns the range of the first axis.
524 * Returns a two-element array: [bottom, top].
526 Dygraph
.prototype.yAxisRange
= function(idx
) {
527 if (typeof(idx
) == "undefined") idx
= 0;
528 if (idx
< 0 || idx
>= this.axes_
.length
) {
531 var axis
= this.axes_
[idx
];
532 return [ axis
.computedValueRange
[0], axis
.computedValueRange
[1] ];
536 * Returns the currently-visible y-ranges for each axis. This can be affected by
537 * zooming, panning, calls to updateOptions, etc.
538 * Returns an array of [bottom, top] pairs, one for each y-axis.
540 Dygraph
.prototype.yAxisRanges
= function() {
542 for (var i
= 0; i
< this.axes_
.length
; i
++) {
543 ret
.push(this.yAxisRange(i
));
548 // TODO(danvk): use these functions throughout dygraphs.
550 * Convert from data coordinates to canvas/div X/Y coordinates.
551 * If specified, do this conversion for the coordinate system of a particular
552 * axis. Uses the first axis by default.
553 * Returns a two-element array: [X, Y]
555 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
556 * instead of toDomCoords(null, y, axis).
558 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
559 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
563 * Convert from data x coordinates to canvas/div X coordinate.
564 * If specified, do this conversion for the coordinate system of a particular
566 * Returns a single value or null if x is null.
568 Dygraph
.prototype.toDomXCoord
= function(x
) {
573 var area
= this.plotter_
.area
;
574 var xRange
= this.xAxisRange();
575 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
579 * Convert from data x coordinates to canvas/div Y coordinate and optional
580 * axis. Uses the first axis by default.
582 * returns a single value or null if y is null.
584 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
585 var pct
= this.toPercentYCoord(y
, axis
);
590 var area
= this.plotter_
.area
;
591 return area
.y
+ pct
* area
.h
;
595 * Convert from canvas/div coords to data coordinates.
596 * If specified, do this conversion for the coordinate system of a particular
597 * axis. Uses the first axis by default.
598 * Returns a two-element array: [X, Y].
600 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
601 * instead of toDataCoords(null, y, axis).
603 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
604 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
608 * Convert from canvas/div x coordinate to data coordinate.
610 * If x is null, this returns null.
612 Dygraph
.prototype.toDataXCoord
= function(x
) {
617 var area
= this.plotter_
.area
;
618 var xRange
= this.xAxisRange();
619 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
623 * Convert from canvas/div y coord to value.
625 * If y is null, this returns null.
626 * if axis is null, this uses the first axis.
628 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
633 var area
= this.plotter_
.area
;
634 var yRange
= this.yAxisRange(axis
);
636 if (typeof(axis
) == "undefined") axis
= 0;
637 if (!this.axes_
[axis
].logscale
) {
638 return yRange
[0] + (area
.y
+ area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
640 // Computing the inverse of toDomCoord.
641 var pct
= (y
- area
.y
) / area
.h
643 // Computing the inverse of toPercentYCoord. The function was arrived at with
644 // the following steps:
646 // Original calcuation:
647 // pct = (logr1 - Dygraph.log10(y)) / (logr1
- Dygraph
.log10(yRange
[0]));
649 // Move denominator to both sides:
650 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
652 // subtract logr1, and take the negative value.
653 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
655 // Swap both sides of the equation, and we can compute the log of the
656 // return value. Which means we just need to use that as the exponent in
658 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
660 var logr1
= Dygraph
.log10(yRange
[1]);
661 var exponent
= logr1
- (pct
* (logr1
- Dygraph
.log10(yRange
[0])));
662 var value
= Math
.pow(Dygraph
.LOG_SCALE
, exponent
);
668 * Converts a y for an axis to a percentage from the top to the
669 * bottom of the drawing area.
671 * If the coordinate represents a value visible on the canvas, then
672 * the value will be between 0 and 1, where 0 is the top of the canvas.
673 * However, this method will return values outside the range, as
674 * values can fall outside the canvas.
676 * If y is null, this returns null.
677 * if axis is null, this uses the first axis.
679 * @param { Number } y The data y-coordinate.
680 * @param { Number } [axis] The axis number on which the data coordinate lives.
681 * @return { Number } A fraction in [0, 1] where 0 = the top edge.
683 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
687 if (typeof(axis
) == "undefined") axis
= 0;
689 var area
= this.plotter_
.area
;
690 var yRange
= this.yAxisRange(axis
);
693 if (!this.axes_
[axis
].logscale
) {
694 // yRange[1] - y is unit distance from the bottom.
695 // yRange[1] - yRange[0] is the scale of the range.
696 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
697 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
699 var logr1
= Dygraph
.log10(yRange
[1]);
700 pct
= (logr1
- Dygraph
.log10(y
)) / (logr1
- Dygraph
.log10(yRange
[0]));
706 * Converts an x value to a percentage from the left to the right of
709 * If the coordinate represents a value visible on the canvas, then
710 * the value will be between 0 and 1, where 0 is the left of the canvas.
711 * However, this method will return values outside the range, as
712 * values can fall outside the canvas.
714 * If x is null, this returns null.
715 * @param { Number } x The data x-coordinate.
716 * @return { Number } A fraction in [0, 1] where 0 = the left edge.
718 Dygraph
.prototype.toPercentXCoord
= function(x
) {
723 var xRange
= this.xAxisRange();
724 return (x
- xRange
[0]) / (xRange
[1] - xRange
[0]);
728 * Returns the number of columns (including the independent variable).
729 * @return { Integer } The number of columns.
731 Dygraph
.prototype.numColumns
= function() {
732 return this.rawData_
[0].length
;
736 * Returns the number of rows (excluding any header/label row).
737 * @return { Integer } The number of rows, less any header.
739 Dygraph
.prototype.numRows
= function() {
740 return this.rawData_
.length
;
744 * Returns the value in the given row and column. If the row and column exceed
745 * the bounds on the data, returns null. Also returns null if the value is
747 * @param { Number} row The row number of the data (0-based). Row 0 is the
748 * first row of data, not a header row.
749 * @param { Number} col The column number of the data (0-based)
750 * @return { Number } The value in the specified cell or null if the row/col
753 Dygraph
.prototype.getValue
= function(row
, col
) {
754 if (row
< 0 || row
> this.rawData_
.length
) return null;
755 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
757 return this.rawData_
[row
][col
];
761 * Generates interface elements for the Dygraph: a containing div, a div to
762 * display the current point, and a textbox to adjust the rolling average
763 * period. Also creates the Renderer/Layout elements.
766 Dygraph
.prototype.createInterface_
= function() {
767 // Create the all-enclosing graph div
768 var enclosing
= this.maindiv_
;
770 this.graphDiv
= document
.createElement("div");
771 this.graphDiv
.style
.width
= this.width_
+ "px";
772 this.graphDiv
.style
.height
= this.height_
+ "px";
773 enclosing
.appendChild(this.graphDiv
);
775 // Create the canvas for interactive parts of the chart.
776 this.canvas_
= Dygraph
.createCanvas();
777 this.canvas_
.style
.position
= "absolute";
778 this.canvas_
.width
= this.width_
;
779 this.canvas_
.height
= this.height_
;
780 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
781 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
783 this.canvas_ctx_
= Dygraph
.getContext(this.canvas_
);
785 // ... and for static parts of the chart.
786 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
787 this.hidden_ctx_
= Dygraph
.getContext(this.hidden_
);
789 if (this.attr_('showRangeSelector')) {
790 // The range selector must be created here so that its canvases and contexts get created here.
791 // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
792 // The range selector also sets xAxisHeight in order to reserve space.
793 this.rangeSelector_
= new DygraphRangeSelector(this);
796 // The interactive parts of the graph are drawn on top of the chart.
797 this.graphDiv
.appendChild(this.hidden_
);
798 this.graphDiv
.appendChild(this.canvas_
);
799 this.mouseEventElement_
= this.canvas_
;
801 // Create the grapher
802 this.layout_
= new DygraphLayout(this);
804 if (this.rangeSelector_
) {
805 // This needs to happen after the graph canvases are added to the div and the layout object is created.
806 this.rangeSelector_
.addToGraph(this.graphDiv
, this.layout_
);
810 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
811 dygraph
.mouseMove_(e
);
813 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
814 dygraph
.mouseOut_(e
);
817 this.createStatusMessage_();
818 this.createDragInterface_();
820 // Update when the window is resized.
821 // TODO(danvk): drop frames depending on complexity of the chart.
822 Dygraph
.addEvent(window
, 'resize', function(e
) {
828 * Detach DOM elements in the dygraph and null out all data references.
829 * Calling this when you're done with a dygraph can dramatically reduce memory
830 * usage. See, e.g., the tests/perf.html example.
832 Dygraph
.prototype.destroy
= function() {
833 var removeRecursive
= function(node
) {
834 while (node
.hasChildNodes()) {
835 removeRecursive(node
.firstChild
);
836 node
.removeChild(node
.firstChild
);
839 removeRecursive(this.maindiv_
);
841 var nullOut
= function(obj
) {
843 if (typeof(obj
[n
]) === 'object') {
849 // These may not all be necessary, but it can't hurt...
850 nullOut(this.layout_
);
851 nullOut(this.plotter_
);
856 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
857 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
858 * or the zoom rectangles) is done on this.canvas_.
859 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
860 * @return {Object} The newly-created canvas
863 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
864 var h
= Dygraph
.createCanvas();
865 h
.style
.position
= "absolute";
866 // TODO(danvk): h should be offset from canvas. canvas needs to include
867 // some extra area to make it easier to zoom in on the far left and far
868 // right. h needs to be precisely the plot area, so that clipping occurs.
869 h
.style
.top
= canvas
.style
.top
;
870 h
.style
.left
= canvas
.style
.left
;
871 h
.width
= this.width_
;
872 h
.height
= this.height_
;
873 h
.style
.width
= this.width_
+ "px"; // for IE
874 h
.style
.height
= this.height_
+ "px"; // for IE
879 * Generate a set of distinct colors for the data series. This is done with a
880 * color wheel. Saturation/Value are customizable, and the hue is
881 * equally-spaced around the color wheel. If a custom set of colors is
882 * specified, that is used instead.
885 Dygraph
.prototype.setColors_
= function() {
886 var num
= this.attr_("labels").length
- 1;
888 var colors
= this.attr_('colors');
890 var sat
= this.attr_('colorSaturation') || 1.0;
891 var val
= this.attr_('colorValue') || 0.5;
892 var half
= Math
.ceil(num
/ 2);
893 for (var i
= 1; i
<= num
; i
++) {
894 if (!this.visibility()[i
-1]) continue;
895 // alternate colors for high contrast.
896 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
897 var hue
= (1.0 * idx
/ (1 + num
));
898 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
901 for (var i
= 0; i
< num
; i
++) {
902 if (!this.visibility()[i
]) continue;
903 var colorStr
= colors
[i
% colors
.length
];
904 this.colors_
.push(colorStr
);
908 this.plotter_
.setColors(this.colors_
);
912 * Return the list of colors. This is either the list of colors passed in the
913 * attributes or the autogenerated list of rgb(r,g,b) strings.
914 * @return {Array<string>} The list of colors.
916 Dygraph
.prototype.getColors
= function() {
921 * Create the div that contains information on the selected point(s)
922 * This goes in the top right of the canvas, unless an external div has already
926 Dygraph
.prototype.createStatusMessage_
= function() {
927 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
928 if (userLabelsDiv
&& null != userLabelsDiv
929 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
930 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
932 if (!this.attr_("labelsDiv")) {
933 var divWidth
= this.attr_('labelsDivWidth');
935 "position": "absolute",
938 "width": divWidth
+ "px",
940 "left": (this.width_
- divWidth
- 2) + "px",
941 "background": "white",
943 "overflow": "hidden"};
944 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
945 var div
= document
.createElement("div");
946 div
.className
= "dygraph-legend";
947 for (var name
in messagestyle
) {
948 if (messagestyle
.hasOwnProperty(name
)) {
949 div
.style
[name
] = messagestyle
[name
];
952 this.graphDiv
.appendChild(div
);
953 this.attrs_
.labelsDiv
= div
;
958 * Position the labels div so that:
959 * - its right edge is flush with the right edge of the charting area
960 * - its top edge is flush with the top edge of the charting area
963 Dygraph
.prototype.positionLabelsDiv_
= function() {
964 // Don't touch a user-specified labelsDiv.
965 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
967 var area
= this.plotter_
.area
;
968 var div
= this.attr_("labelsDiv");
969 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
970 div
.style
.top
= area
.y
+ "px";
974 * Create the text box to adjust the averaging period
977 Dygraph
.prototype.createRollInterface_
= function() {
978 // Create a roller if one doesn't exist already.
980 this.roller_
= document
.createElement("input");
981 this.roller_
.type
= "text";
982 this.roller_
.style
.display
= "none";
983 this.graphDiv
.appendChild(this.roller_
);
986 var display
= this.attr_('showRoller') ? 'block' : 'none';
988 var area
= this.plotter_
.area
;
989 var textAttr
= { "position": "absolute",
991 "top": (area
.y
+ area
.h
- 25) + "px",
992 "left": (area
.x
+ 1) + "px",
995 this.roller_
.size
= "2";
996 this.roller_
.value
= this.rollPeriod_
;
997 for (var name
in textAttr
) {
998 if (textAttr
.hasOwnProperty(name
)) {
999 this.roller_
.style
[name
] = textAttr
[name
];
1004 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
1009 * Converts page the x-coordinate of the event to pixel x-coordinates on the
1010 * canvas (i.e. DOM Coords).
1012 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
1013 return Dygraph
.pageX(e
) - context
.px
1018 * Converts page the y-coordinate of the event to pixel y-coordinates on the
1019 * canvas (i.e. DOM Coords).
1021 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
1022 return Dygraph
.pageY(e
) - context
.py
1026 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1030 Dygraph
.prototype.createDragInterface_
= function() {
1032 // Tracks whether the mouse is down right now
1034 isPanning
: false, // is this drag part of a pan?
1035 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1036 dragStartX
: null, // pixel coordinates
1037 dragStartY
: null, // pixel coordinates
1038 dragEndX
: null, // pixel coordinates
1039 dragEndY
: null, // pixel coordinates
1040 dragDirection
: null,
1041 prevEndX
: null, // pixel coordinates
1042 prevEndY
: null, // pixel coordinates
1043 prevDragDirection
: null,
1045 // The value on the left side of the graph when a pan operation starts.
1046 initialLeftmostDate
: null,
1048 // The number of units each pixel spans. (This won't be valid for log
1050 xUnitsPerPixel
: null,
1052 // TODO(danvk): update this comment
1053 // The range in second/value units that the viewport encompasses during a
1054 // panning operation.
1057 // Top-left corner of the canvas, in DOM coords
1058 // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1062 // Values for use with panEdgeFraction, which limit how far outside the
1063 // graph's data boundaries it can be panned.
1064 boundedDates
: null, // [minDate, maxDate]
1065 boundedValues
: null, // [[minValue, maxValue] ...]
1067 initializeMouseDown
: function(event
, g
, context
) {
1068 // prevents mouse drags from selecting page text.
1069 if (event
.preventDefault
) {
1070 event
.preventDefault(); // Firefox, Chrome, etc.
1072 event
.returnValue
= false; // IE
1073 event
.cancelBubble
= true;
1076 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1077 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1078 context
.dragStartX
= g
.dragGetX_(event
, context
);
1079 context
.dragStartY
= g
.dragGetY_(event
, context
);
1083 var interactionModel
= this.attr_("interactionModel");
1085 // Self is the graph.
1088 // Function that binds the graph and context to the handler.
1089 var bindHandler
= function(handler
) {
1090 return function(event
) {
1091 handler(event
, self
, context
);
1095 for (var eventName
in interactionModel
) {
1096 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1097 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1098 bindHandler(interactionModel
[eventName
]));
1101 // If the user releases the mouse button during a drag, but not over the
1102 // canvas, then it doesn't count as a zooming action.
1103 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1104 if (context
.isZooming
|| context
.isPanning
) {
1105 context
.isZooming
= false;
1106 context
.dragStartX
= null;
1107 context
.dragStartY
= null;
1110 if (context
.isPanning
) {
1111 context
.isPanning
= false;
1112 context
.draggingDate
= null;
1113 context
.dateRange
= null;
1114 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1115 delete self
.axes_
[i
].draggingValue
;
1116 delete self
.axes_
[i
].dragValueRange
;
1124 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1125 * up any previous zoom rectangles that were drawn. This could be optimized to
1126 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1129 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1130 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1131 * @param {Number} startX The X position where the drag started, in canvas
1133 * @param {Number} endX The current X position of the drag, in canvas coords.
1134 * @param {Number} startY The Y position where the drag started, in canvas
1136 * @param {Number} endY The current Y position of the drag, in canvas coords.
1137 * @param {Number} prevDirection the value of direction on the previous call to
1138 * this function. Used to avoid excess redrawing
1139 * @param {Number} prevEndX The value of endX on the previous call to this
1140 * function. Used to avoid excess redrawing
1141 * @param {Number} prevEndY The value of endY on the previous call to this
1142 * function. Used to avoid excess redrawing
1145 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
,
1146 endY
, prevDirection
, prevEndX
,
1148 var ctx
= this.canvas_ctx_
;
1150 // Clean up from the previous rect if necessary
1151 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1152 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1153 Math
.abs(startX
- prevEndX
), this.height_
);
1154 } else if (prevDirection
== Dygraph
.VERTICAL
){
1155 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1156 this.width_
, Math
.abs(startY
- prevEndY
));
1159 // Draw a light-grey rectangle to show the new viewing area
1160 if (direction
== Dygraph
.HORIZONTAL
) {
1161 if (endX
&& startX
) {
1162 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1163 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1164 Math
.abs(endX
- startX
), this.height_
);
1167 if (direction
== Dygraph
.VERTICAL
) {
1168 if (endY
&& startY
) {
1169 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1170 ctx
.fillRect(0, Math
.min(startY
, endY
),
1171 this.width_
, Math
.abs(endY
- startY
));
1177 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1178 * the canvas. The exact zoom window may be slightly larger if there are no data
1179 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1180 * which accepts dates that match the raw data. This function redraws the graph.
1182 * @param {Number} lowX The leftmost pixel value that should be visible.
1183 * @param {Number} highX The rightmost pixel value that should be visible.
1186 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1187 // Find the earliest and latest dates contained in this canvasx range.
1188 // Convert the call to date ranges of the raw data.
1189 var minDate
= this.toDataXCoord(lowX
);
1190 var maxDate
= this.toDataXCoord(highX
);
1191 this.doZoomXDates_(minDate
, maxDate
);
1195 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1196 * method with doZoomX which accepts pixel coordinates. This function redraws
1199 * @param {Number} minDate The minimum date that should be visible.
1200 * @param {Number} maxDate The maximum date that should be visible.
1203 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1204 this.dateWindow_
= [minDate
, maxDate
];
1205 this.zoomed_x_
= true;
1207 if (this.attr_("zoomCallback")) {
1208 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1213 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1214 * the canvas. This function redraws the graph.
1216 * @param {Number} lowY The topmost pixel value that should be visible.
1217 * @param {Number} highY The lowest pixel value that should be visible.
1220 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1221 // Find the highest and lowest values in pixel range for each axis.
1222 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1223 // This is because pixels increase as you go down on the screen, whereas data
1224 // coordinates increase as you go up the screen.
1225 var valueRanges
= [];
1226 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1227 var hi
= this.toDataYCoord(lowY
, i
);
1228 var low
= this.toDataYCoord(highY
, i
);
1229 this.axes_
[i
].valueWindow
= [low
, hi
];
1230 valueRanges
.push([low
, hi
]);
1233 this.zoomed_y_
= true;
1235 if (this.attr_("zoomCallback")) {
1236 var xRange
= this.xAxisRange();
1237 var yRange
= this.yAxisRange();
1238 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1243 * Reset the zoom to the original view coordinates. This is the same as
1244 * double-clicking on the graph.
1248 Dygraph
.prototype.doUnzoom_
= function() {
1250 if (this.dateWindow_
!= null) {
1252 this.dateWindow_
= null;
1255 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1256 if (this.axes_
[i
].valueWindow
!= null) {
1258 delete this.axes_
[i
].valueWindow
;
1262 // Clear any selection, since it's likely to be drawn in the wrong place.
1263 this.clearSelection();
1266 // Putting the drawing operation before the callback because it resets
1268 this.zoomed_x_
= false;
1269 this.zoomed_y_
= false;
1271 if (this.attr_("zoomCallback")) {
1272 var minDate
= this.rawData_
[0][0];
1273 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1274 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1280 * When the mouse moves in the canvas, display information about a nearby data
1281 * point and draw dots over those points in the data series. This function
1282 * takes care of cleanup of previously-drawn dots.
1283 * @param {Object} event The mousemove event from the browser.
1286 Dygraph
.prototype.mouseMove_
= function(event
) {
1287 // This prevents JS errors when mousing over the canvas before data loads.
1288 var points
= this.layout_
.points
;
1289 if (points
=== undefined
) return;
1291 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1296 // Loop through all the points and find the date nearest to our current
1298 var minDist
= 1e+100;
1300 for (var i
= 0; i
< points
.length
; i
++) {
1301 var point
= points
[i
];
1302 if (point
== null) continue;
1303 var dist
= Math
.abs(point
.canvasx
- canvasx
);
1304 if (dist
> minDist
) continue;
1308 if (idx
>= 0) lastx
= points
[idx
].xval
;
1310 // Extract the points we've selected
1311 this.selPoints_
= [];
1312 var l
= points
.length
;
1313 if (!this.attr_("stackedGraph")) {
1314 for (var i
= 0; i
< l
; i
++) {
1315 if (points
[i
].xval
== lastx
) {
1316 this.selPoints_
.push(points
[i
]);
1320 // Need to 'unstack' points starting from the bottom
1321 var cumulative_sum
= 0;
1322 for (var i
= l
- 1; i
>= 0; i
--) {
1323 if (points
[i
].xval
== lastx
) {
1324 var p
= {}; // Clone the point since we modify it
1325 for (var k
in points
[i
]) {
1326 p
[k
] = points
[i
][k
];
1328 p
.yval
-= cumulative_sum
;
1329 cumulative_sum
+= p
.yval
;
1330 this.selPoints_
.push(p
);
1333 this.selPoints_
.reverse();
1336 if (this.attr_("highlightCallback")) {
1337 var px
= this.lastx_
;
1338 if (px
!== null && lastx
!= px
) {
1339 // only fire if the selected point has changed.
1340 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1344 // Save last x position for callbacks.
1345 this.lastx_
= lastx
;
1347 this.updateSelection_();
1351 * Transforms layout_.points index into data row number.
1352 * @param int layout_.points index
1353 * @return int row number, or -1 if none could be found.
1356 Dygraph
.prototype.idxToRow_
= function(idx
) {
1357 if (idx
< 0) return -1;
1359 for (var i
in this.layout_
.datasets
) {
1360 if (idx
< this.layout_
.datasets
[i
].length
) {
1361 return this.boundaryIds_
[0][0]+idx
;
1363 idx
-= this.layout_
.datasets
[i
].length
;
1370 * Generates HTML for the legend which is displayed when hovering over the
1371 * chart. If no selected points are specified, a default legend is returned
1372 * (this may just be the empty string).
1373 * @param { Number } [x] The x-value of the selected points.
1374 * @param { [Object] } [sel_points] List of selected points for the given
1375 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1377 Dygraph
.prototype.generateLegendHTML_
= function(x
, sel_points
) {
1378 // If no points are selected, we display a default legend. Traditionally,
1379 // this has been blank. But a better default would be a conventional legend,
1380 // which provides essential information for a non-interactive chart.
1381 if (typeof(x
) === 'undefined') {
1382 if (this.attr_('legend') != 'always') return '';
1384 var sepLines
= this.attr_('labelsSeparateLines');
1385 var labels
= this.attr_('labels');
1387 for (var i
= 1; i
< labels
.length
; i
++) {
1388 if (!this.visibility()[i
- 1]) continue;
1389 var c
= this.plotter_
.colors
[labels
[i
]];
1390 if (html
!= '') html
+= (sepLines
? '<br/>' : ' ');
1391 html
+= "<b><span style='color: " + c
+ ";'>—" + labels
[i
] +
1397 var xOptView
= this.optionsViewForAxis_('x');
1398 var xvf
= xOptView('valueFormatter');
1399 var html
= xvf(x
, xOptView
, this.attr_('labels')[0], this) + ":";
1402 var num_axes
= this.numAxes();
1403 for (var i
= 0; i
< num_axes
; i
++) {
1404 yOptViews
[i
] = this.optionsViewForAxis_('y' + (i
? 1 + i
: ''));
1406 var showZeros
= this.attr_("labelsShowZeroValues");
1407 var sepLines
= this.attr_("labelsSeparateLines");
1408 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1409 var pt
= this.selPoints_
[i
];
1410 if (pt
.yval
== 0 && !showZeros
) continue;
1411 if (!Dygraph
.isOK(pt
.canvasy
)) continue;
1412 if (sepLines
) html
+= "<br/>";
1414 var yOptView
= yOptViews
[this.seriesToAxisMap_
[pt
.name
]];
1415 var fmtFunc
= yOptView('valueFormatter');
1416 var c
= this.plotter_
.colors
[pt
.name
];
1417 var yval
= fmtFunc(pt
.yval
, yOptView
, pt
.name
, this);
1419 // TODO(danvk): use a template string here and make it an attribute.
1420 html
+= " <b><span style='color: " + c
+ ";'>"
1421 + pt
.name
+ "</span></b>:"
1429 * Displays information about the selected points in the legend. If there is no
1430 * selection, the legend will be cleared.
1431 * @param { Number } [x] The x-value of the selected points.
1432 * @param { [Object] } [sel_points] List of selected points for the given
1433 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1435 Dygraph
.prototype.setLegendHTML_
= function(x
, sel_points
) {
1436 var html
= this.generateLegendHTML_(x
, sel_points
);
1437 var labelsDiv
= this.attr_("labelsDiv");
1438 if (labelsDiv
!== null) {
1439 labelsDiv
.innerHTML
= html
;
1441 if (typeof(this.shown_legend_error_
) == 'undefined') {
1442 this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
1443 this.shown_legend_error_
= true;
1449 * Draw dots over the selectied points in the data series. This function
1450 * takes care of cleanup of previously-drawn dots.
1453 Dygraph
.prototype.updateSelection_
= function() {
1454 // Clear the previously drawn vertical, if there is one
1455 var ctx
= this.canvas_ctx_
;
1456 if (this.previousVerticalX_
>= 0) {
1457 // Determine the maximum highlight circle size.
1458 var maxCircleSize
= 0;
1459 var labels
= this.attr_('labels');
1460 for (var i
= 1; i
< labels
.length
; i
++) {
1461 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1462 if (r
> maxCircleSize
) maxCircleSize
= r
;
1464 var px
= this.previousVerticalX_
;
1465 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1466 2 * maxCircleSize
+ 2, this.height_
);
1469 if (this.selPoints_
.length
> 0) {
1470 // Set the status message to indicate the selected point(s)
1471 if (this.attr_('showLabelsOnHighlight')) {
1472 this.setLegendHTML_(this.lastx_
, this.selPoints_
);
1475 // Draw colored circles over the center of each selected point
1476 var canvasx
= this.selPoints_
[0].canvasx
;
1478 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1479 var pt
= this.selPoints_
[i
];
1480 if (!Dygraph
.isOK(pt
.canvasy
)) continue;
1482 var circleSize
= this.attr_('highlightCircleSize', pt
.name
);
1484 ctx
.fillStyle
= this.plotter_
.colors
[pt
.name
];
1485 ctx
.arc(canvasx
, pt
.canvasy
, circleSize
, 0, 2 * Math
.PI
, false);
1490 this.previousVerticalX_
= canvasx
;
1495 * Manually set the selected points and display information about them in the
1496 * legend. The selection can be cleared using clearSelection() and queried
1497 * using getSelection().
1498 * @param { Integer } row number that should be highlighted (i.e. appear with
1499 * hover dots on the chart). Set to false to clear any selection.
1501 Dygraph
.prototype.setSelection
= function(row
) {
1502 // Extract the points we've selected
1503 this.selPoints_
= [];
1506 if (row
!== false) {
1507 row
= row
-this.boundaryIds_
[0][0];
1510 if (row
!== false && row
>= 0) {
1511 for (var i
in this.layout_
.datasets
) {
1512 if (row
< this.layout_
.datasets
[i
].length
) {
1513 var point
= this.layout_
.points
[pos
+row
];
1515 if (this.attr_("stackedGraph")) {
1516 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1519 this.selPoints_
.push(point
);
1521 pos
+= this.layout_
.datasets
[i
].length
;
1525 if (this.selPoints_
.length
) {
1526 this.lastx_
= this.selPoints_
[0].xval
;
1527 this.updateSelection_();
1529 this.clearSelection();
1535 * The mouse has left the canvas. Clear out whatever artifacts remain
1536 * @param {Object} event the mouseout event from the browser.
1539 Dygraph
.prototype.mouseOut_
= function(event
) {
1540 if (this.attr_("unhighlightCallback")) {
1541 this.attr_("unhighlightCallback")(event
);
1544 if (this.attr_("hideOverlayOnMouseOut")) {
1545 this.clearSelection();
1550 * Clears the current selection (i.e. points that were highlighted by moving
1551 * the mouse over the chart).
1553 Dygraph
.prototype.clearSelection
= function() {
1554 // Get rid of the overlay data
1555 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1556 this.setLegendHTML_();
1557 this.selPoints_
= [];
1562 * Returns the number of the currently selected row. To get data for this row,
1563 * you can use the getValue method.
1564 * @return { Integer } row number, or -1 if nothing is selected
1566 Dygraph
.prototype.getSelection
= function() {
1567 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1571 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1572 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1573 return row
+ this.boundaryIds_
[0][0];
1580 * Fires when there's data available to be graphed.
1581 * @param {String} data Raw CSV data to be plotted
1584 Dygraph
.prototype.loadedEvent_
= function(data
) {
1585 this.rawData_
= this.parseCSV_(data
);
1590 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1593 Dygraph
.prototype.addXTicks_
= function() {
1594 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1596 if (this.dateWindow_
) {
1597 range
= [this.dateWindow_
[0], this.dateWindow_
[1]];
1599 range
= [this.rawData_
[0][0], this.rawData_
[this.rawData_
.length
- 1][0]];
1602 var xAxisOptionsView
= this.optionsViewForAxis_('x');
1603 var xTicks
= xAxisOptionsView('ticker')(
1606 this.width_
, // TODO(danvk): should be area.width
1609 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
1610 // console.log(msg);
1611 this.layout_
.setXTicks(xTicks
);
1616 * Computes the range of the data series (including confidence intervals).
1617 * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
1618 * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1619 * @return [low, high]
1621 Dygraph
.prototype.extremeValues_
= function(series
) {
1622 var minY
= null, maxY
= null;
1624 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1626 // With custom bars, maxY is the max of the high values.
1627 for (var j
= 0; j
< series
.length
; j
++) {
1628 var y
= series
[j
][1][0];
1630 var low
= y
- series
[j
][1][1];
1631 var high
= y
+ series
[j
][1][2];
1632 if (low
> y
) low
= y
; // this can happen with custom bars,
1633 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1634 if (maxY
== null || high
> maxY
) {
1637 if (minY
== null || low
< minY
) {
1642 for (var j
= 0; j
< series
.length
; j
++) {
1643 var y
= series
[j
][1];
1644 if (y
=== null || isNaN(y
)) continue;
1645 if (maxY
== null || y
> maxY
) {
1648 if (minY
== null || y
< minY
) {
1654 return [minY
, maxY
];
1659 * This function is called once when the chart's data is changed or the options
1660 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1661 * idea is that values derived from the chart's data can be computed here,
1662 * rather than every time the chart is drawn. This includes things like the
1663 * number of axes, rolling averages, etc.
1665 Dygraph
.prototype.predraw_
= function() {
1666 var start
= new Date();
1668 // TODO(danvk): move more computations out of drawGraph_ and into here.
1669 this.computeYAxes_();
1671 // Create a new plotter.
1672 if (this.plotter_
) this.plotter_
.clear();
1673 this.plotter_
= new DygraphCanvasRenderer(this,
1678 // The roller sits in the bottom left corner of the chart. We don't know where
1679 // this will be until the options are available, so it's positioned here.
1680 this.createRollInterface_();
1682 // Same thing applies for the labelsDiv. It's right edge should be flush with
1683 // the right edge of the charting area (which may not be the same as the right
1684 // edge of the div, if we have two y-axes.
1685 this.positionLabelsDiv_();
1687 if (this.rangeSelector_
) {
1688 this.rangeSelector_
.renderStaticLayer();
1691 // If the data or options have changed, then we'd better redraw.
1694 // This is used to determine whether to do various animations.
1695 var end
= new Date();
1696 this.drawingTimeMs_
= (end
- start
);
1700 * Update the graph with new data. This method is called when the viewing area
1701 * has changed. If the underlying data or options have changed, predraw_ will
1702 * be called before drawGraph_ is called.
1704 * clearSelection, when undefined or true, causes this.clearSelection to be
1705 * called at the end of the draw operation. This should rarely be defined,
1706 * and never true (that is it should be undefined most of the time, and
1711 Dygraph
.prototype.drawGraph_
= function(clearSelection
) {
1712 var start
= new Date();
1714 if (typeof(clearSelection
) === 'undefined') {
1715 clearSelection
= true;
1718 var data
= this.rawData_
;
1720 // This is used to set the second parameter to drawCallback, below.
1721 var is_initial_draw
= this.is_initial_draw_
;
1722 this.is_initial_draw_
= false;
1724 var minY
= null, maxY
= null;
1725 this.layout_
.removeAllDatasets();
1727 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1729 // Loop over the fields (series). Go from the last to the first,
1730 // because if they're stacked that's how we accumulate the values.
1732 var cumulative_y
= []; // For stacked series.
1735 var extremes
= {}; // series name -> [low, high]
1737 // Loop over all fields and create datasets
1738 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1739 if (!this.visibility()[i
- 1]) continue;
1741 var seriesName
= this.attr_("labels")[i
];
1742 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1743 var logScale
= this.attr_('logscale', i
);
1746 for (var j
= 0; j
< data
.length
; j
++) {
1747 var date
= data
[j
][0];
1748 var point
= data
[j
][i
];
1750 // On the log scale, points less than zero do not exist.
1751 // This will create a gap in the chart. Note that this ignores
1752 // connectSeparatedPoints.
1756 series
.push([date
, point
]);
1758 if (point
!= null || !connectSeparatedPoints
) {
1759 series
.push([date
, point
]);
1764 // TODO(danvk): move this into predraw_. It's insane to do it here.
1765 series
= this.rollingAverage(series
, this.rollPeriod_
);
1767 // Prune down to the desired range, if necessary (for zooming)
1768 // Because there can be lines going to points outside of the visible area,
1769 // we actually prune to visible points, plus one on either side.
1770 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1771 if (this.dateWindow_
) {
1772 var low
= this.dateWindow_
[0];
1773 var high
= this.dateWindow_
[1];
1775 // TODO(danvk): do binary search instead of linear search.
1776 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1777 var firstIdx
= null, lastIdx
= null;
1778 for (var k
= 0; k
< series
.length
; k
++) {
1779 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1782 if (series
[k
][0] <= high
) {
1786 if (firstIdx
=== null) firstIdx
= 0;
1787 if (firstIdx
> 0) firstIdx
--;
1788 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1789 if (lastIdx
< series
.length
- 1) lastIdx
++;
1790 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1791 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1792 pruned
.push(series
[k
]);
1796 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1799 var seriesExtremes
= this.extremeValues_(series
);
1802 for (var j
=0; j
<series
.length
; j
++) {
1803 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1806 } else if (this.attr_("stackedGraph")) {
1807 var l
= series
.length
;
1809 for (var j
= 0; j
< l
; j
++) {
1810 // If one data set has a NaN, let all subsequent stacked
1811 // sets inherit the NaN -- only start at 0 for the first set.
1812 var x
= series
[j
][0];
1813 if (cumulative_y
[x
] === undefined
) {
1814 cumulative_y
[x
] = 0;
1817 actual_y
= series
[j
][1];
1818 cumulative_y
[x
] += actual_y
;
1820 series
[j
] = [x
, cumulative_y
[x
]]
1822 if (cumulative_y
[x
] > seriesExtremes
[1]) {
1823 seriesExtremes
[1] = cumulative_y
[x
];
1825 if (cumulative_y
[x
] < seriesExtremes
[0]) {
1826 seriesExtremes
[0] = cumulative_y
[x
];
1830 extremes
[seriesName
] = seriesExtremes
;
1832 datasets
[i
] = series
;
1835 for (var i
= 1; i
< datasets
.length
; i
++) {
1836 if (!this.visibility()[i
- 1]) continue;
1837 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1840 this.computeYAxisRanges_(extremes
);
1841 this.layout_
.setYAxes(this.axes_
);
1845 // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
1846 var tmp_zoomed_x
= this.zoomed_x_
;
1847 // Tell PlotKit to use this new data and render itself
1848 this.layout_
.setDateWindow(this.dateWindow_
);
1849 this.zoomed_x_
= tmp_zoomed_x
;
1850 this.layout_
.evaluateWithError();
1851 this.renderGraph_(is_initial_draw
, false);
1853 if (this.attr_("timingName")) {
1854 var end
= new Date();
1856 console
.log(this.attr_("timingName") + " - drawGraph: " + (end
- start
) + "ms")
1861 Dygraph
.prototype.renderGraph_
= function(is_initial_draw
, clearSelection
) {
1862 this.plotter_
.clear();
1863 this.plotter_
.render();
1864 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1865 this.canvas_
.height
);
1867 if (is_initial_draw
) {
1868 // Generate a static legend before any particular point is selected.
1869 this.setLegendHTML_();
1871 if (clearSelection
) {
1872 if (typeof(this.selPoints_
) !== 'undefined' && this.selPoints_
.length
) {
1873 // We should select the point nearest the page x/y here
, but it
's easier
1874 // to just clear the selection. This prevents erroneous hover dots from
1876 this.clearSelection();
1878 this.clearSelection();
1883 if (this.rangeSelector_) {
1884 this.rangeSelector_.renderInteractiveLayer();
1887 if (this.attr_("drawCallback") !== null) {
1888 this.attr_("drawCallback")(this, is_initial_draw);
1894 * Determine properties of the y-axes which are independent of the data
1895 * currently being displayed. This includes things like the number of axes and
1896 * the style of the axes. It does not include the range of each axis and its
1898 * This fills in this.axes_ and this.seriesToAxisMap_.
1899 * axes_ = [ { options } ]
1900 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
1901 * indices are into the axes_ array.
1903 Dygraph.prototype.computeYAxes_ = function() {
1904 // Preserve valueWindow settings if they exist, and if the user hasn't
1905 // specified a new valueRange.
1907 if (this.axes_
!= undefined
&& this.user_attrs_
.hasOwnProperty("valueRange") == false) {
1909 for (var index
= 0; index
< this.axes_
.length
; index
++) {
1910 valueWindows
.push(this.axes_
[index
].valueWindow
);
1914 this.axes_
= [{ yAxisId
: 0, g
: this }]; // always have at least one y-axis.
1915 this.seriesToAxisMap_
= {};
1917 // Get a list of series names.
1918 var labels
= this.attr_("labels");
1920 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
1922 // all options which could be applied per-axis:
1930 'axisLabelFontSize',
1935 // Copy global axis options over to the first axis.
1936 for (var i
= 0; i
< axisOptions
.length
; i
++) {
1937 var k
= axisOptions
[i
];
1938 var v
= this.attr_(k
);
1939 if (v
) this.axes_
[0][k
] = v
;
1942 // Go through once and add all the axes.
1943 for (var seriesName
in series
) {
1944 if (!series
.hasOwnProperty(seriesName
)) continue;
1945 var axis
= this.attr_("axis", seriesName
);
1947 this.seriesToAxisMap_
[seriesName
] = 0;
1950 if (typeof(axis
) == 'object') {
1951 // Add a new axis, making a copy of its per-axis options.
1953 Dygraph
.update(opts
, this.axes_
[0]);
1954 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
1955 var yAxisId
= this.axes_
.length
;
1956 opts
.yAxisId
= yAxisId
;
1958 Dygraph
.update(opts
, axis
);
1959 this.axes_
.push(opts
);
1960 this.seriesToAxisMap_
[seriesName
] = yAxisId
;
1964 // Go through one more time and assign series to an axis defined by another
1965 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
1966 for (var seriesName
in series
) {
1967 if (!series
.hasOwnProperty(seriesName
)) continue;
1968 var axis
= this.attr_("axis", seriesName
);
1969 if (typeof(axis
) == 'string') {
1970 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
1971 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
1972 "series " + axis
+ ", which does not define its own axis.");
1975 var idx
= this.seriesToAxisMap_
[axis
];
1976 this.seriesToAxisMap_
[seriesName
] = idx
;
1980 // Now we remove series from seriesToAxisMap_ which are not visible. We do
1981 // this last so that hiding the first series doesn't destroy the axis
1982 // properties of the primary axis.
1983 var seriesToAxisFiltered
= {};
1984 var vis
= this.visibility();
1985 for (var i
= 1; i
< labels
.length
; i
++) {
1987 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
1989 this.seriesToAxisMap_
= seriesToAxisFiltered
;
1991 if (valueWindows
!= undefined
) {
1992 // Restore valueWindow settings.
1993 for (var index
= 0; index
< valueWindows
.length
; index
++) {
1994 this.axes_
[index
].valueWindow
= valueWindows
[index
];
2000 * Returns the number of y-axes on the chart.
2001 * @return {Number} the number of axes.
2003 Dygraph
.prototype.numAxes
= function() {
2005 for (var series
in this.seriesToAxisMap_
) {
2006 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2007 var idx
= this.seriesToAxisMap_
[series
];
2008 if (idx
> last_axis
) last_axis
= idx
;
2010 return 1 + last_axis
;
2015 * Returns axis properties for the given series.
2016 * @param { String } setName The name of the series for which to get axis
2017 * properties, e.g. 'Y1'.
2018 * @return { Object } The axis properties.
2020 Dygraph
.prototype.axisPropertiesForSeries
= function(series
) {
2021 // TODO(danvk): handle errors.
2022 return this.axes_
[this.seriesToAxisMap_
[series
]];
2027 * Determine the value range and tick marks for each axis.
2028 * @param {Object} extremes A mapping from seriesName -> [low, high]
2029 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2031 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2032 // Build a map from axis number -> [list of series names]
2033 var seriesForAxis
= [];
2034 for (var series
in this.seriesToAxisMap_
) {
2035 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2036 var idx
= this.seriesToAxisMap_
[series
];
2037 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2038 seriesForAxis
[idx
].push(series
);
2041 // Compute extreme values, a span and tick marks for each axis.
2042 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2043 var axis
= this.axes_
[i
];
2045 if (!seriesForAxis
[i
]) {
2046 // If no series are defined or visible then use a reasonable default
2047 axis
.extremeRange
= [0, 1];
2049 // Calculate the extremes of extremes.
2050 var series
= seriesForAxis
[i
];
2051 var minY
= Infinity
; // extremes[series[0]][0];
2052 var maxY
= -Infinity
; // extremes[series[0]][1];
2053 var extremeMinY
, extremeMaxY
;
2054 for (var j
= 0; j
< series
.length
; j
++) {
2055 // Only use valid extremes to stop null data series' from corrupting the scale.
2056 extremeMinY
= extremes
[series
[j
]][0];
2057 if (extremeMinY
!= null) {
2058 minY
= Math
.min(extremeMinY
, minY
);
2060 extremeMaxY
= extremes
[series
[j
]][1];
2061 if (extremeMaxY
!= null) {
2062 maxY
= Math
.max(extremeMaxY
, maxY
);
2065 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2067 // Ensure we have a valid scale, otherwise defualt to zero for safety.
2068 if (minY
== Infinity
) minY
= 0;
2069 if (maxY
== -Infinity
) maxY
= 0;
2071 // Add some padding and round up to an integer to be human-friendly.
2072 var span
= maxY
- minY
;
2073 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2074 if (span
== 0) { span
= maxY
; }
2078 if (axis
.logscale
) {
2079 var maxAxisY
= maxY
+ 0.1 * span
;
2080 var minAxisY
= minY
;
2082 var maxAxisY
= maxY
+ 0.1 * span
;
2083 var minAxisY
= minY
- 0.1 * span
;
2085 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2086 if (!this.attr_("avoidMinZero")) {
2087 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2088 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2091 if (this.attr_("includeZero")) {
2092 if (maxY
< 0) maxAxisY
= 0;
2093 if (minY
> 0) minAxisY
= 0;
2096 axis
.extremeRange
= [minAxisY
, maxAxisY
];
2098 if (axis
.valueWindow
) {
2099 // This is only set if the user has zoomed on the y-axis. It is never set
2100 // by a user. It takes precedence over axis.valueRange because, if you set
2101 // valueRange, you'd still expect to be able to pan.
2102 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2103 } else if (axis
.valueRange
) {
2104 // This is a user-set value range for this axis.
2105 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2107 axis
.computedValueRange
= axis
.extremeRange
;
2110 // Add ticks. By default, all axes inherit the tick positions of the
2111 // primary axis. However, if an axis is specifically marked as having
2112 // independent ticks, then that is permissible as well.
2113 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2114 var ticker
= opts('ticker');
2115 if (i
== 0 || axis
.independentTicks
) {
2116 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2117 axis
.computedValueRange
[1],
2118 this.height_
, // TODO(danvk): should be area.height
2122 var p_axis
= this.axes_
[0];
2123 var p_ticks
= p_axis
.ticks
;
2124 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2125 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2126 var tick_values
= [];
2127 for (var k
= 0; k
< p_ticks
.length
; k
++) {
2128 var y_frac
= (p_ticks
[k
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2129 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2130 tick_values
.push(y_val
);
2133 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2134 axis
.computedValueRange
[1],
2135 this.height_
, // TODO(danvk): should be area.height
2145 * Calculates the rolling average of a data set.
2146 * If originalData is [label, val], rolls the average of those.
2147 * If originalData is [label, [, it's interpreted as [value, stddev]
2148 * and the roll is returned in the same form, with appropriately reduced
2149 * stddev for each value.
2150 * Note that this is where fractional input (i.e. '5/10') is converted into
2152 * @param {Array} originalData The data in the appropriate format (see above)
2153 * @param {Number} rollPeriod The number of points over which to average the
2156 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2157 if (originalData
.length
< 2)
2158 return originalData
;
2159 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
);
2160 var rollingData
= [];
2161 var sigma
= this.attr_("sigma");
2163 if (this.fractions_
) {
2165 var den
= 0; // numerator/denominator
2167 for (var i
= 0; i
< originalData
.length
; i
++) {
2168 num
+= originalData
[i
][1][0];
2169 den
+= originalData
[i
][1][1];
2170 if (i
- rollPeriod
>= 0) {
2171 num
-= originalData
[i
- rollPeriod
][1][0];
2172 den
-= originalData
[i
- rollPeriod
][1][1];
2175 var date
= originalData
[i
][0];
2176 var value
= den
? num
/ den
: 0.0;
2177 if (this.attr_("errorBars")) {
2178 if (this.wilsonInterval_
) {
2179 // For more details on this confidence interval, see:
2180 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2182 var p
= value
< 0 ? 0 : value
, n
= den
;
2183 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2184 var denom
= 1 + sigma
* sigma
/ den
;
2185 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2186 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2187 rollingData
[i
] = [date
,
2188 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2190 rollingData
[i
] = [date
, [0, 0, 0]];
2193 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2194 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2197 rollingData
[i
] = [date
, mult
* value
];
2200 } else if (this.attr_("customBars")) {
2205 for (var i
= 0; i
< originalData
.length
; i
++) {
2206 var data
= originalData
[i
][1];
2208 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2210 if (y
!= null && !isNaN(y
)) {
2216 if (i
- rollPeriod
>= 0) {
2217 var prev
= originalData
[i
- rollPeriod
];
2218 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2226 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2227 1.0 * (mid
- low
) / count
,
2228 1.0 * (high
- mid
) / count
]];
2230 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2234 // Calculate the rolling average for the first rollPeriod - 1 points where
2235 // there is not enough data to roll over the full number of points
2236 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2237 if (!this.attr_("errorBars")){
2238 if (rollPeriod
== 1) {
2239 return originalData
;
2242 for (var i
= 0; i
< originalData
.length
; i
++) {
2245 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2246 var y
= originalData
[j
][1];
2247 if (y
== null || isNaN(y
)) continue;
2249 sum
+= originalData
[j
][1];
2252 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2254 rollingData
[i
] = [originalData
[i
][0], null];
2259 for (var i
= 0; i
< originalData
.length
; i
++) {
2263 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2264 var y
= originalData
[j
][1][0];
2265 if (y
== null || isNaN(y
)) continue;
2267 sum
+= originalData
[j
][1][0];
2268 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2271 var stddev
= Math
.sqrt(variance
) / num_ok
;
2272 rollingData
[i
] = [originalData
[i
][0],
2273 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2275 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2285 * Detects the type of the str (date or numeric) and sets the various
2286 * formatting attributes in this.attrs_ based on this type.
2287 * @param {String} str An x value.
2290 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2292 if (str
.indexOf('-') > 0 ||
2293 str
.indexOf('/') >= 0 ||
2294 isNaN(parseFloat(str
))) {
2296 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2297 // TODO(danvk): remove support for this format.
2302 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2303 this.attrs_
.axes
.x
.valueFormatter
= Dygraph
.dateString_
;
2304 this.attrs_
.axes
.x
.ticker
= Dygraph
.dateTicker
;
2305 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2307 /** @private (shut up, jsdoc!) */
2308 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2309 // TODO(danvk): use Dygraph.numberValueFormatter here?
2310 /** @private (shut up, jsdoc!) */
2311 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2312 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2313 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2318 * Parses the value as a floating point number. This is like the parseFloat()
2319 * built-in, but with a few differences:
2320 * - the empty string is parsed as null, rather than NaN.
2321 * - if the string cannot be parsed at all, an error is logged.
2322 * If the string can't be parsed, this method returns null.
2323 * @param {String} x The string to be parsed
2324 * @param {Number} opt_line_no The line number from which the string comes.
2325 * @param {String} opt_line The text of the line from which the string comes.
2329 // Parse the x as a float or return null if it's not a number.
2330 Dygraph
.prototype.parseFloat_
= function(x
, opt_line_no
, opt_line
) {
2331 var val
= parseFloat(x
);
2332 if (!isNaN(val
)) return val
;
2334 // Try to figure out what happeend.
2335 // If the value is the empty string, parse it as null.
2336 if (/^ *$/.test(x
)) return null;
2338 // If it was actually "NaN", return it as NaN.
2339 if (/^ *nan *$/i.test(x
)) return NaN
;
2341 // Looks like a parsing error.
2342 var msg
= "Unable to parse '" + x
+ "' as a number";
2343 if (opt_line
!== null && opt_line_no
!== null) {
2344 msg
+= " on line " + (1+opt_line_no
) + " ('" + opt_line
+ "') of CSV.";
2353 * Parses a string in a special csv format. We expect a csv file where each
2354 * line is a date point, and the first field in each line is the date string.
2355 * We also expect that all remaining fields represent series.
2356 * if the errorBars attribute is set, then interpret the fields as:
2357 * date, series1, stddev1, series2, stddev2, ...
2358 * @param {[Object]} data See above.
2360 * @return [Object] An array with one entry for each row. These entries
2361 * are an array of cells in that row. The first entry is the parsed x-value for
2362 * the row. The second, third, etc. are the y-values. These can take on one of
2363 * three forms, depending on the CSV and constructor parameters:
2365 * 2. [ value, stddev ]
2366 * 3. [ low value, center value, high value ]
2368 Dygraph
.prototype.parseCSV_
= function(data
) {
2370 var lines
= data
.split("\n");
2372 // Use the default delimiter or fall back to a tab if that makes sense.
2373 var delim
= this.attr_('delimiter');
2374 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2379 if (!('labels' in this.user_attrs_
)) {
2380 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2382 this.attrs_
.labels
= lines
[0].split(delim
); // NOTE: _not_ user_attrs_.
2387 var defaultParserSet
= false; // attempt to auto-detect x value type
2388 var expectedCols
= this.attr_("labels").length
;
2389 var outOfOrder
= false;
2390 for (var i
= start
; i
< lines
.length
; i
++) {
2391 var line
= lines
[i
];
2393 if (line
.length
== 0) continue; // skip blank lines
2394 if (line
[0] == '#') continue; // skip comment lines
2395 var inFields
= line
.split(delim
);
2396 if (inFields
.length
< 2) continue;
2399 if (!defaultParserSet
) {
2400 this.detectTypeFromString_(inFields
[0]);
2401 xParser
= this.attr_("xValueParser");
2402 defaultParserSet
= true;
2404 fields
[0] = xParser(inFields
[0], this);
2406 // If fractions are expected, parse the numbers as "A/B
"
2407 if (this.fractions_) {
2408 for (var j = 1; j < inFields.length; j++) {
2409 // TODO(danvk): figure out an appropriate way to flag parse errors.
2410 var vals = inFields[j].split("/");
2411 if (vals.length != 2) {
2412 this.error('Expected fractional "num
/den
" values in CSV data ' +
2413 "but found a value
'" + inFields[j] + "' on line
" +
2414 (1 + i) + " ('" + line + "') which is not of
this form
.");
2417 fields[j] = [this.parseFloat_(vals[0], i, line),
2418 this.parseFloat_(vals[1], i, line)];
2421 } else if (this.attr_("errorBars
")) {
2422 // If there are error bars, values are (value, stddev) pairs
2423 if (inFields.length % 2 != 1) {
2424 this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2425 'but line ' + (1 + i) + ' has an odd number of values (' +
2426 (inFields.length - 1) + "): '" + line + "'");
2428 for (var j = 1; j < inFields.length; j += 2) {
2429 fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
2430 this.parseFloat_(inFields[j + 1], i, line)];
2432 } else if (this.attr_("customBars
")) {
2433 // Bars are a low;center;high tuple
2434 for (var j = 1; j < inFields.length; j++) {
2435 var val = inFields[j];
2436 if (/^ *$/.test(val)) {
2437 fields[j] = [null, null, null];
2439 var vals = val.split(";");
2440 if (vals.length == 3) {
2441 fields[j] = [ this.parseFloat_(vals[0], i, line),
2442 this.parseFloat_(vals[1], i, line),
2443 this.parseFloat_(vals[2], i, line) ];
2445 this.warning('When using customBars, values must be either blank ' +
2446 'or "low
;center
;high
" tuples (got "' + val +
2447 '" on line ' + (1+i));
2452 // Values are just numbers
2453 for (var j = 1; j < inFields.length; j++) {
2454 fields[j] = this.parseFloat_(inFields[j], i, line);
2457 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2461 if (fields.length != expectedCols) {
2462 this.error("Number of columns
in line
" + i + " (" + fields.length +
2463 ") does not agree
with number of
labels (" + expectedCols +
2467 // If the user specified the 'labels' option and none of the cells of the
2468 // first row parsed correctly, then they probably double-specified the
2469 // labels. We go with the values set in the option, discard this row and
2470 // log a warning to the JS console.
2471 if (i == 0 && this.attr_('labels')) {
2472 var all_null = true;
2473 for (var j = 0; all_null && j < fields.length; j++) {
2474 if (fields[j]) all_null = false;
2477 this.warn("The dygraphs
'labels' option is set
, but the first row of
" +
2478 "CSV
data ('" + line + "') appears to also contain labels
. " +
2479 "Will drop the CSV labels and
use the option labels
.");
2487 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2488 ret.sort(function(a,b) { return a[0] - b[0] });
2496 * The user has provided their data as a pre-packaged JS array. If the x values
2497 * are numeric, this is the same as dygraphs' internal format. If the x values
2498 * are dates, we need to convert them from Date objects to ms since epoch.
2499 * @param {[Object]} data
2500 * @return {[Object]} data with numeric x values.
2502 Dygraph.prototype.parseArray_ = function(data) {
2503 // Peek at the first x value to see if it's numeric.
2504 if (data.length == 0) {
2505 this.error("Can
't plot empty data set");
2508 if (data[0].length == 0) {
2509 this.error("Data set cannot contain an empty row");
2513 if (this.attr_("labels") == null) {
2514 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2515 "in the options parameter");
2516 this.attrs_.labels = [ "X" ];
2517 for (var i = 1; i < data[0].length; i++) {
2518 this.attrs_.labels.push("Y" + i);
2522 if (Dygraph.isDateLike(data[0][0])) {
2523 // Some intelligent defaults for a date x-axis.
2524 this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
2525 this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
2526 this.attrs_.axes.x.ticker = Dygraph.dateTicker;
2528 // Assume they're all dates
.
2529 var parsedData
= Dygraph
.clone(data
);
2530 for (var i
= 0; i
< data
.length
; i
++) {
2531 if (parsedData
[i
].length
== 0) {
2532 this.error("Row " + (1 + i
) + " of data is empty");
2535 if (parsedData
[i
][0] == null
2536 || typeof(parsedData
[i
][0].getTime
) != 'function'
2537 || isNaN(parsedData
[i
][0].getTime())) {
2538 this.error("x value in row " + (1 + i
) + " is not a Date");
2541 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2545 // Some intelligent defaults for a numeric x-axis.
2546 /** @private (shut up, jsdoc!) */
2547 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2548 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.numberAxisLabelFormatter
;
2549 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2555 * Parses a DataTable object from gviz.
2556 * The data is expected to have a first column that is either a date or a
2557 * number. All subsequent columns must be numbers. If there is a clear mismatch
2558 * between this.xValueParser_ and the type of the first column, it will be
2559 * fixed. Fills out rawData_.
2560 * @param {[Object]} data See above.
2563 Dygraph
.prototype.parseDataTable_
= function(data
) {
2564 var cols
= data
.getNumberOfColumns();
2565 var rows
= data
.getNumberOfRows();
2567 var indepType
= data
.getColumnType(0);
2568 if (indepType
== 'date' || indepType
== 'datetime') {
2569 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2570 this.attrs_
.axes
.x
.valueFormatter
= Dygraph
.dateString_
;
2571 this.attrs_
.axes
.x
.ticker
= Dygraph
.dateTicker
;
2572 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2573 } else if (indepType
== 'number') {
2574 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2575 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2576 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2577 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2579 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2580 "column 1 of DataTable input (Got '" + indepType
+ "')");
2584 // Array of the column indices which contain data (and not annotations).
2586 var annotationCols
= {}; // data index -> [annotation cols]
2587 var hasAnnotations
= false;
2588 for (var i
= 1; i
< cols
; i
++) {
2589 var type
= data
.getColumnType(i
);
2590 if (type
== 'number') {
2592 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2593 // This is OK -- it's an annotation column.
2594 var dataIdx
= colIdx
[colIdx
.length
- 1];
2595 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2596 annotationCols
[dataIdx
] = [i
];
2598 annotationCols
[dataIdx
].push(i
);
2600 hasAnnotations
= true;
2602 this.error("Only 'number' is supported as a dependent type with Gviz." +
2603 " 'string' is only supported if displayAnnotations is true");
2607 // Read column labels
2608 // TODO(danvk): add support back for errorBars
2609 var labels
= [data
.getColumnLabel(0)];
2610 for (var i
= 0; i
< colIdx
.length
; i
++) {
2611 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2612 if (this.attr_("errorBars")) i
+= 1;
2614 this.attrs_
.labels
= labels
;
2615 cols
= labels
.length
;
2618 var outOfOrder
= false;
2619 var annotations
= [];
2620 for (var i
= 0; i
< rows
; i
++) {
2622 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2623 data
.getValue(i
, 0) === null) {
2624 this.warn("Ignoring row " + i
+
2625 " of DataTable because of undefined or null first column.");
2629 if (indepType
== 'date' || indepType
== 'datetime') {
2630 row
.push(data
.getValue(i
, 0).getTime());
2632 row
.push(data
.getValue(i
, 0));
2634 if (!this.attr_("errorBars")) {
2635 for (var j
= 0; j
< colIdx
.length
; j
++) {
2636 var col
= colIdx
[j
];
2637 row
.push(data
.getValue(i
, col
));
2638 if (hasAnnotations
&&
2639 annotationCols
.hasOwnProperty(col
) &&
2640 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2642 ann
.series
= data
.getColumnLabel(col
);
2644 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2646 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2647 if (k
) ann
.text
+= "\n";
2648 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2650 annotations
.push(ann
);
2654 // Strip out infinities, which give dygraphs problems later on.
2655 for (var j
= 0; j
< row
.length
; j
++) {
2656 if (!isFinite(row
[j
])) row
[j
] = null;
2659 for (var j
= 0; j
< cols
- 1; j
++) {
2660 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2663 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2670 this.warn("DataTable is out of order; order it correctly to speed loading.");
2671 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2673 this.rawData_
= ret
;
2675 if (annotations
.length
> 0) {
2676 this.setAnnotations(annotations
, true);
2681 * Get the CSV data. If it's in a function, call that function. If it's in a
2682 * file, do an XMLHttpRequest to get it.
2685 Dygraph
.prototype.start_
= function() {
2686 if (typeof this.file_
== 'function') {
2687 // CSV string. Pretend we got it via XHR.
2688 this.loadedEvent_(this.file_());
2689 } else if (Dygraph
.isArrayLike(this.file_
)) {
2690 this.rawData_
= this.parseArray_(this.file_
);
2692 } else if (typeof this.file_
== 'object' &&
2693 typeof this.file_
.getColumnRange
== 'function') {
2694 // must be a DataTable from gviz.
2695 this.parseDataTable_(this.file_
);
2697 } else if (typeof this.file_
== 'string') {
2698 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2699 if (this.file_
.indexOf('\n') >= 0) {
2700 this.loadedEvent_(this.file_
);
2702 var req
= new XMLHttpRequest();
2704 req
.onreadystatechange
= function () {
2705 if (req
.readyState
== 4) {
2706 if (req
.status
== 200 || // Normal http
2707 req
.status
== 0) { // Chrome w/ --allow
-file
-access
-from
-files
2708 caller
.loadedEvent_(req
.responseText
);
2713 req
.open("GET", this.file_
, true);
2717 this.error("Unknown data format: " + (typeof this.file_
));
2722 * Changes various properties of the graph. These can include:
2724 * <li>file: changes the source data for the graph</li>
2725 * <li>errorBars: changes whether the data contains stddev</li>
2728 * There's a huge variety of options that can be passed to this method. For a
2729 * full list, see http://dygraphs.com/options.html.
2731 * @param {Object} attrs The new properties and values
2732 * @param {Boolean} [block_redraw] Usually the chart is redrawn after every
2733 * call to updateOptions(). If you know better, you can pass true to explicitly
2734 * block the redraw. This can be useful for chaining updateOptions() calls,
2735 * avoiding the occasional infinite loop and preventing redraws when it's not
2736 * necessary (e.g. when updating a callback).
2738 Dygraph
.prototype.updateOptions
= function(input_attrs
, block_redraw
) {
2739 if (typeof(block_redraw
) == 'undefined') block_redraw
= false;
2741 // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
2742 var file
= input_attrs
['file'];
2743 var attrs
= Dygraph
.mapLegacyOptions_(input_attrs
);
2745 // TODO(danvk): this is a mess. Move these options into attr_.
2746 if ('rollPeriod' in attrs
) {
2747 this.rollPeriod_
= attrs
.rollPeriod
;
2749 if ('dateWindow' in attrs
) {
2750 this.dateWindow_
= attrs
.dateWindow
;
2751 if (!('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
2752 this.zoomed_x_
= attrs
.dateWindow
!= null;
2755 if ('valueRange' in attrs
&& !('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
2756 this.zoomed_y_
= attrs
.valueRange
!= null;
2759 // TODO(danvk): validate per-series options.
2764 // highlightCircleSize
2766 // Check if this set options will require new points.
2767 var requiresNewPoints
= Dygraph
.isPixelChangingOptionList(this.attr_("labels"), attrs
);
2769 Dygraph
.updateDeep(this.user_attrs_
, attrs
);
2773 if (!block_redraw
) this.start_();
2775 if (!block_redraw
) {
2776 if (requiresNewPoints
) {
2779 this.renderGraph_(false, false);
2786 * Returns a copy of the options with deprecated names converted into current
2787 * names. Also drops the (potentially-large) 'file' attribute. If the caller is
2788 * interested in that, they should save a copy before calling this.
2791 Dygraph
.mapLegacyOptions_
= function(attrs
) {
2793 for (var k
in attrs
) {
2794 if (k
== 'file') continue;
2795 if (attrs
.hasOwnProperty(k
)) my_attrs
[k
] = attrs
[k
];
2798 var set
= function(axis
, opt
, value
) {
2799 if (!my_attrs
.axes
) my_attrs
.axes
= {};
2800 if (!my_attrs
.axes
[axis
]) my_attrs
.axes
[axis
] = {};
2801 my_attrs
.axes
[axis
][opt
] = value
;
2803 var map
= function(opt
, axis
, new_opt
) {
2804 if (typeof(attrs
[opt
]) != 'undefined') {
2805 set(axis
, new_opt
, attrs
[opt
]);
2806 delete my_attrs
[opt
];
2810 // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
2811 map('xValueFormatter', 'x', 'valueFormatter');
2812 map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
2813 map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
2814 map('xTicker', 'x', 'ticker');
2815 map('yValueFormatter', 'y', 'valueFormatter');
2816 map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
2817 map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
2818 map('yTicker', 'y', 'ticker');
2823 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2824 * containing div (which has presumably changed size since the dygraph was
2825 * instantiated. If the width/height are specified, the div will be resized.
2827 * This is far more efficient than destroying and re-instantiating a
2828 * Dygraph, since it doesn't have to reparse the underlying data.
2830 * @param {Number} [width] Width (in pixels)
2831 * @param {Number} [height] Height (in pixels)
2833 Dygraph
.prototype.resize
= function(width
, height
) {
2834 if (this.resize_lock
) {
2837 this.resize_lock
= true;
2839 if ((width
=== null) != (height
=== null)) {
2840 this.warn("Dygraph.resize() should be called with zero parameters or " +
2841 "two non-NULL parameters. Pretending it was zero.");
2842 width
= height
= null;
2845 var old_width
= this.width_
;
2846 var old_height
= this.height_
;
2849 this.maindiv_
.style
.width
= width
+ "px";
2850 this.maindiv_
.style
.height
= height
+ "px";
2851 this.width_
= width
;
2852 this.height_
= height
;
2854 this.width_
= this.maindiv_
.clientWidth
;
2855 this.height_
= this.maindiv_
.clientHeight
;
2858 if (old_width
!= this.width_
|| old_height
!= this.height_
) {
2859 // TODO(danvk): there should be a clear() method.
2860 this.maindiv_
.innerHTML
= "";
2861 this.roller_
= null;
2862 this.attrs_
.labelsDiv
= null;
2863 this.createInterface_();
2864 if (this.annotations_
.length
) {
2865 // createInterface_ reset the layout, so we need to do this.
2866 this.layout_
.setAnnotations(this.annotations_
);
2871 this.resize_lock
= false;
2875 * Adjusts the number of points in the rolling average. Updates the graph to
2876 * reflect the new averaging period.
2877 * @param {Number} length Number of points over which to average the data.
2879 Dygraph
.prototype.adjustRoll
= function(length
) {
2880 this.rollPeriod_
= length
;
2885 * Returns a boolean array of visibility statuses.
2887 Dygraph
.prototype.visibility
= function() {
2888 // Do lazy-initialization, so that this happens after we know the number of
2890 if (!this.attr_("visibility")) {
2891 this.attrs_
["visibility"] = [];
2893 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2894 this.attr_("visibility").push(true);
2896 return this.attr_("visibility");
2900 * Changes the visiblity of a series.
2902 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2903 var x
= this.visibility();
2904 if (num
< 0 || num
>= x
.length
) {
2905 this.warn("invalid series number in setVisibility: " + num
);
2913 * How large of an area will the dygraph render itself in?
2914 * This is used for testing.
2915 * @return A {width: w, height: h} object.
2918 Dygraph
.prototype.size
= function() {
2919 return { width
: this.width_
, height
: this.height_
};
2923 * Update the list of annotations and redraw the chart.
2925 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2926 // Only add the annotation CSS rule once we know it will be used.
2927 Dygraph
.addAnnotationRule();
2928 this.annotations_
= ann
;
2929 this.layout_
.setAnnotations(this.annotations_
);
2930 if (!suppressDraw
) {
2936 * Return the list of annotations.
2938 Dygraph
.prototype.annotations
= function() {
2939 return this.annotations_
;
2943 * Get the index of a series (column) given its name. The first column is the
2944 * x-axis, so the data series start with index 1.
2946 Dygraph
.prototype.indexFromSetName
= function(name
) {
2947 var labels
= this.attr_("labels");
2948 for (var i
= 0; i
< labels
.length
; i
++) {
2949 if (labels
[i
] == name
) return i
;
2956 * Adds a default style for the annotation CSS classes to the document. This is
2957 * only executed when annotations are actually used. It is designed to only be
2958 * called once -- all calls after the first will return immediately.
2960 Dygraph
.addAnnotationRule
= function() {
2961 if (Dygraph
.addedAnnotationCSS
) return;
2963 var rule
= "border: 1px solid black; " +
2964 "background-color: white; " +
2965 "text-align: center;";
2967 var styleSheetElement
= document
.createElement("style");
2968 styleSheetElement
.type
= "text/css";
2969 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2971 // Find the first style sheet that we can access.
2972 // We may not add a rule to a style sheet from another domain for security
2973 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
2974 // adds its own style sheets from google.com.
2975 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
2976 if (document
.styleSheets
[i
].disabled
) continue;
2977 var mysheet
= document
.styleSheets
[i
];
2979 if (mysheet
.insertRule
) { // Firefox
2980 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2981 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2982 } else if (mysheet
.addRule
) { // IE
2983 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2985 Dygraph
.addedAnnotationCSS
= true;
2988 // Was likely a security exception.
2992 this.warn("Unable to add default annotation CSS rule; display may be off.");
2995 // Older pages may still use this name.
2996 DateGraph
= Dygraph
;