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
251 axisLabelFormatter
: Dygraph
.dateAxisFormatter
,
252 valueFormatter
: Dygraph
.dateString_
,
253 ticker
: null // will be set in dygraph-tickers.js
257 valueFormatter
: Dygraph
.numberValueFormatter
,
258 axisLabelFormatter
: Dygraph
.numberAxisLabelFormatter
,
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
270 // Directions for panning and zooming. Use bit operations when combined
271 // values are possible.
272 Dygraph
.HORIZONTAL
= 1;
273 Dygraph
.VERTICAL
= 2;
275 // Used for initializing annotation CSS rules only once.
276 Dygraph
.addedAnnotationCSS
= false;
278 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
279 // Labels is no longer a constructor parameter, since it's typically set
280 // directly from the data source. It also conains a name for the x-axis,
281 // which the previous constructor form did not.
282 if (labels
!= null) {
283 var new_labels
= ["Date"];
284 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
285 Dygraph
.update(attrs
, { 'labels': new_labels
});
287 this.__init__(div
, file
, attrs
);
291 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
292 * and context <canvas> inside of it. See the constructor for details.
294 * @param {Element} div the Element to render the graph into.
295 * @param {String | Function} file Source data
296 * @param {Object} attrs Miscellaneous other options
299 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
300 // Hack for IE: if we're using excanvas and the document hasn't finished
301 // loading yet (and hence may not have initialized whatever it needs to
302 // initialize), then keep calling this routine periodically until it has.
303 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
304 typeof(G_vmlCanvasManager
) != 'undefined' &&
305 document
.readyState
!= 'complete') {
307 setTimeout(function() { self
.__init__(div
, file
, attrs
) }, 100);
310 // Support two-argument constructor
311 if (attrs
== null) { attrs
= {}; }
313 attrs
= Dygraph
.mapLegacyOptions_(attrs
);
316 Dygraph
.error("Constructing dygraph with a non-existent div!");
320 // Copy the important bits into the object
321 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
324 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
325 this.previousVerticalX_
= -1;
326 this.fractions_
= attrs
.fractions
|| false;
327 this.dateWindow_
= attrs
.dateWindow
|| null;
329 this.wilsonInterval_
= attrs
.wilsonInterval
|| true;
330 this.is_initial_draw_
= true;
331 this.annotations_
= [];
333 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
334 this.zoomed_x_
= false;
335 this.zoomed_y_
= false;
337 // Clear the div. This ensure that, if multiple dygraphs are passed the same
338 // div, then only one will be drawn.
341 // For historical reasons, the 'width' and 'height' options trump all CSS
342 // rules _except_ for an explicit 'width' or 'height' on the div.
343 // As an added convenience, if the div has zero height (like <div></div> does
344 // without any styles), then we use a default height/width
.
345 if (div
.style
.width
== '' && attrs
.width
) {
346 div
.style
.width
= attrs
.width
+ "px";
348 if (div
.style
.height
== '' && attrs
.height
) {
349 div
.style
.height
= attrs
.height
+ "px";
351 if (div
.style
.height
== '' && div
.offsetHeight
== 0) {
352 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
353 if (div
.style
.width
== '') {
354 div
.style
.width
= Dygraph
.DEFAULT_WIDTH
+ "px";
357 // these will be zero if the dygraph's div is hidden.
358 this.width_
= div
.offsetWidth
;
359 this.height_
= div
.offsetHeight
;
361 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
362 if (attrs
['stackedGraph']) {
363 attrs
['fillGraph'] = true;
364 // TODO(nikhilk): Add any other stackedGraph checks here.
367 // Dygraphs has many options, some of which interact with one another.
368 // To keep track of everything, we maintain two sets of options:
370 // this.user_attrs_ only options explicitly set by the user.
371 // this.attrs_ defaults, options derived from user_attrs_, data.
373 // Options are then accessed this.attr_('attr'), which first looks at
374 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
375 // defaults without overriding behavior that the user specifically asks for.
376 this.user_attrs_
= {};
377 Dygraph
.update(this.user_attrs_
, attrs
);
379 // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
381 Dygraph
.updateDeep(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
383 this.boundaryIds_
= [];
385 // Create the containing DIV and other interactive elements
386 this.createInterface_();
392 * Returns the zoomed status of the chart for one or both axes.
394 * Axis is an optional parameter. Can be set to 'x' or 'y'.
396 * The zoomed status for an axis is set whenever a user zooms using the mouse
397 * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
398 * option is also specified).
400 Dygraph
.prototype.isZoomed
= function(axis
) {
401 if (axis
== null) return this.zoomed_x_
|| this.zoomed_y_
;
402 if (axis
== 'x') return this.zoomed_x_
;
403 if (axis
== 'y') return this.zoomed_y_
;
404 throw "axis parameter to Dygraph.isZoomed must be missing, 'x' or 'y'.";
408 * Returns information about the Dygraph object, including its containing ID.
410 Dygraph
.prototype.toString
= function() {
411 var maindiv
= this.maindiv_
;
412 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
413 return "[Dygraph " + id
+ "]";
418 * Returns the value of an option. This may be set by the user (either in the
419 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
421 * @param { String } name The name of the option, e.g. 'rollPeriod'.
422 * @param { String } [seriesName] The name of the series to which the option
423 * will be applied. If no per-series value of this option is available, then
424 * the global value is returned. This is optional.
425 * @return { ... } The value of the option.
427 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
428 // <REMOVE_FOR_COMBINED>
429 if (typeof(Dygraph
.OPTIONS_REFERENCE
) === 'undefined') {
430 this.error('Must include options reference JS for testing');
431 } else if (!Dygraph
.OPTIONS_REFERENCE
.hasOwnProperty(name
)) {
432 this.error('Dygraphs is using property ' + name
+ ', which has no entry ' +
433 'in the Dygraphs.OPTIONS_REFERENCE listing.');
434 // Only log this error once.
435 Dygraph
.OPTIONS_REFERENCE
[name
] = true;
437 // </REMOVE_FOR_COMBINED
>
439 typeof(this.user_attrs_
[seriesName
]) != 'undefined' &&
440 this.user_attrs_
[seriesName
] != null &&
441 typeof(this.user_attrs_
[seriesName
][name
]) != 'undefined') {
442 return this.user_attrs_
[seriesName
][name
];
443 } else if (typeof(this.user_attrs_
[name
]) != 'undefined') {
444 return this.user_attrs_
[name
];
445 } else if (typeof(this.attrs_
[name
]) != 'undefined') {
446 return this.attrs_
[name
];
454 * @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2')
455 * @return { ... } A function mapping string -> option value
457 Dygraph
.prototype.optionsViewForAxis_
= function(axis
) {
459 return function(opt
) {
460 var axis_opts
= self
.user_attrs_
['axes'];
461 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
][opt
]) {
462 return axis_opts
[axis
][opt
];
464 // user-specified attributes always trump defaults, even if they're less
466 if (typeof(self
.user_attrs_
[opt
]) != 'undefined') {
467 return self
.user_attrs_
[opt
];
470 axis_opts
= self
.attrs_
['axes'];
471 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
][opt
]) {
472 return axis_opts
[axis
][opt
];
474 // check old-style axis options
475 // TODO(danvk): add a deprecation warning if either of these match.
476 if (axis
== 'y' && self
.axes_
[0].hasOwnProperty(opt
)) {
477 return self
.axes_
[0][opt
];
478 } else if (axis
== 'y2' && self
.axes_
[1].hasOwnProperty(opt
)) {
479 return self
.axes_
[1][opt
];
481 return self
.attr_(opt
);
486 * Returns the current rolling period, as set by the user or an option.
487 * @return {Number} The number of points in the rolling window
489 Dygraph
.prototype.rollPeriod
= function() {
490 return this.rollPeriod_
;
494 * Returns the currently-visible x-range. This can be affected by zooming,
495 * panning or a call to updateOptions.
496 * Returns a two-element array: [left, right].
497 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
499 Dygraph
.prototype.xAxisRange
= function() {
500 return this.dateWindow_
? this.dateWindow_
: this.xAxisExtremes();
504 * Returns the lower- and upper-bound x-axis values of the
507 Dygraph
.prototype.xAxisExtremes
= function() {
508 var left
= this.rawData_
[0][0];
509 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
510 return [left
, right
];
514 * Returns the currently-visible y-range for an axis. This can be affected by
515 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
516 * called with no arguments, returns the range of the first axis.
517 * Returns a two-element array: [bottom, top].
519 Dygraph
.prototype.yAxisRange
= function(idx
) {
520 if (typeof(idx
) == "undefined") idx
= 0;
521 if (idx
< 0 || idx
>= this.axes_
.length
) {
524 var axis
= this.axes_
[idx
];
525 return [ axis
.computedValueRange
[0], axis
.computedValueRange
[1] ];
529 * Returns the currently-visible y-ranges for each axis. This can be affected by
530 * zooming, panning, calls to updateOptions, etc.
531 * Returns an array of [bottom, top] pairs, one for each y-axis.
533 Dygraph
.prototype.yAxisRanges
= function() {
535 for (var i
= 0; i
< this.axes_
.length
; i
++) {
536 ret
.push(this.yAxisRange(i
));
541 // TODO(danvk): use these functions throughout dygraphs.
543 * Convert from data coordinates to canvas/div X/Y coordinates.
544 * If specified, do this conversion for the coordinate system of a particular
545 * axis. Uses the first axis by default.
546 * Returns a two-element array: [X, Y]
548 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
549 * instead of toDomCoords(null, y, axis).
551 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
552 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
556 * Convert from data x coordinates to canvas/div X coordinate.
557 * If specified, do this conversion for the coordinate system of a particular
559 * Returns a single value or null if x is null.
561 Dygraph
.prototype.toDomXCoord
= function(x
) {
566 var area
= this.plotter_
.area
;
567 var xRange
= this.xAxisRange();
568 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
572 * Convert from data x coordinates to canvas/div Y coordinate and optional
573 * axis. Uses the first axis by default.
575 * returns a single value or null if y is null.
577 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
578 var pct
= this.toPercentYCoord(y
, axis
);
583 var area
= this.plotter_
.area
;
584 return area
.y
+ pct
* area
.h
;
588 * Convert from canvas/div coords to data coordinates.
589 * If specified, do this conversion for the coordinate system of a particular
590 * axis. Uses the first axis by default.
591 * Returns a two-element array: [X, Y].
593 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
594 * instead of toDataCoords(null, y, axis).
596 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
597 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
601 * Convert from canvas/div x coordinate to data coordinate.
603 * If x is null, this returns null.
605 Dygraph
.prototype.toDataXCoord
= function(x
) {
610 var area
= this.plotter_
.area
;
611 var xRange
= this.xAxisRange();
612 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
616 * Convert from canvas/div y coord to value.
618 * If y is null, this returns null.
619 * if axis is null, this uses the first axis.
621 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
626 var area
= this.plotter_
.area
;
627 var yRange
= this.yAxisRange(axis
);
629 if (typeof(axis
) == "undefined") axis
= 0;
630 if (!this.axes_
[axis
].logscale
) {
631 return yRange
[0] + (area
.y
+ area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
633 // Computing the inverse of toDomCoord.
634 var pct
= (y
- area
.y
) / area
.h
636 // Computing the inverse of toPercentYCoord. The function was arrived at with
637 // the following steps:
639 // Original calcuation:
640 // pct = (logr1 - Dygraph.log10(y)) / (logr1
- Dygraph
.log10(yRange
[0]));
642 // Move denominator to both sides:
643 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
645 // subtract logr1, and take the negative value.
646 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
648 // Swap both sides of the equation, and we can compute the log of the
649 // return value. Which means we just need to use that as the exponent in
651 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
653 var logr1
= Dygraph
.log10(yRange
[1]);
654 var exponent
= logr1
- (pct
* (logr1
- Dygraph
.log10(yRange
[0])));
655 var value
= Math
.pow(Dygraph
.LOG_SCALE
, exponent
);
661 * Converts a y for an axis to a percentage from the top to the
662 * bottom of the drawing area.
664 * If the coordinate represents a value visible on the canvas, then
665 * the value will be between 0 and 1, where 0 is the top of the canvas.
666 * However, this method will return values outside the range, as
667 * values can fall outside the canvas.
669 * If y is null, this returns null.
670 * if axis is null, this uses the first axis.
672 * @param { Number } y The data y-coordinate.
673 * @param { Number } [axis] The axis number on which the data coordinate lives.
674 * @return { Number } A fraction in [0, 1] where 0 = the top edge.
676 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
680 if (typeof(axis
) == "undefined") axis
= 0;
682 var area
= this.plotter_
.area
;
683 var yRange
= this.yAxisRange(axis
);
686 if (!this.axes_
[axis
].logscale
) {
687 // yRange[1] - y is unit distance from the bottom.
688 // yRange[1] - yRange[0] is the scale of the range.
689 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
690 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
692 var logr1
= Dygraph
.log10(yRange
[1]);
693 pct
= (logr1
- Dygraph
.log10(y
)) / (logr1
- Dygraph
.log10(yRange
[0]));
699 * Converts an x value to a percentage from the left to the right of
702 * If the coordinate represents a value visible on the canvas, then
703 * the value will be between 0 and 1, where 0 is the left of the canvas.
704 * However, this method will return values outside the range, as
705 * values can fall outside the canvas.
707 * If x is null, this returns null.
708 * @param { Number } x The data x-coordinate.
709 * @return { Number } A fraction in [0, 1] where 0 = the left edge.
711 Dygraph
.prototype.toPercentXCoord
= function(x
) {
716 var xRange
= this.xAxisRange();
717 return (x
- xRange
[0]) / (xRange
[1] - xRange
[0]);
721 * Returns the number of columns (including the independent variable).
722 * @return { Integer } The number of columns.
724 Dygraph
.prototype.numColumns
= function() {
725 return this.rawData_
[0].length
;
729 * Returns the number of rows (excluding any header/label row).
730 * @return { Integer } The number of rows, less any header.
732 Dygraph
.prototype.numRows
= function() {
733 return this.rawData_
.length
;
737 * Returns the value in the given row and column. If the row and column exceed
738 * the bounds on the data, returns null. Also returns null if the value is
740 * @param { Number} row The row number of the data (0-based). Row 0 is the
741 * first row of data, not a header row.
742 * @param { Number} col The column number of the data (0-based)
743 * @return { Number } The value in the specified cell or null if the row/col
746 Dygraph
.prototype.getValue
= function(row
, col
) {
747 if (row
< 0 || row
> this.rawData_
.length
) return null;
748 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
750 return this.rawData_
[row
][col
];
754 * Generates interface elements for the Dygraph: a containing div, a div to
755 * display the current point, and a textbox to adjust the rolling average
756 * period. Also creates the Renderer/Layout elements.
759 Dygraph
.prototype.createInterface_
= function() {
760 // Create the all-enclosing graph div
761 var enclosing
= this.maindiv_
;
763 this.graphDiv
= document
.createElement("div");
764 this.graphDiv
.style
.width
= this.width_
+ "px";
765 this.graphDiv
.style
.height
= this.height_
+ "px";
766 enclosing
.appendChild(this.graphDiv
);
768 // Create the canvas for interactive parts of the chart.
769 this.canvas_
= Dygraph
.createCanvas();
770 this.canvas_
.style
.position
= "absolute";
771 this.canvas_
.width
= this.width_
;
772 this.canvas_
.height
= this.height_
;
773 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
774 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
776 this.canvas_ctx_
= Dygraph
.getContext(this.canvas_
);
778 // ... and for static parts of the chart.
779 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
780 this.hidden_ctx_
= Dygraph
.getContext(this.hidden_
);
782 // The interactive parts of the graph are drawn on top of the chart.
783 this.graphDiv
.appendChild(this.hidden_
);
784 this.graphDiv
.appendChild(this.canvas_
);
785 this.mouseEventElement_
= this.canvas_
;
788 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', function(e
) {
789 dygraph
.mouseMove_(e
);
791 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', function(e
) {
792 dygraph
.mouseOut_(e
);
795 // Create the grapher
796 this.layout_
= new DygraphLayout(this);
798 this.createStatusMessage_();
799 this.createDragInterface_();
801 // Update when the window is resized.
802 // TODO(danvk): drop frames depending on complexity of the chart.
803 Dygraph
.addEvent(window
, 'resize', function(e
) {
809 * Detach DOM elements in the dygraph and null out all data references.
810 * Calling this when you're done with a dygraph can dramatically reduce memory
811 * usage. See, e.g., the tests/perf.html example.
813 Dygraph
.prototype.destroy
= function() {
814 var removeRecursive
= function(node
) {
815 while (node
.hasChildNodes()) {
816 removeRecursive(node
.firstChild
);
817 node
.removeChild(node
.firstChild
);
820 removeRecursive(this.maindiv_
);
822 var nullOut
= function(obj
) {
824 if (typeof(obj
[n
]) === 'object') {
830 // These may not all be necessary, but it can't hurt...
831 nullOut(this.layout_
);
832 nullOut(this.plotter_
);
837 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
838 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
839 * or the zoom rectangles) is done on this.canvas_.
840 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
841 * @return {Object} The newly-created canvas
844 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
845 var h
= Dygraph
.createCanvas();
846 h
.style
.position
= "absolute";
847 // TODO(danvk): h should be offset from canvas. canvas needs to include
848 // some extra area to make it easier to zoom in on the far left and far
849 // right. h needs to be precisely the plot area, so that clipping occurs.
850 h
.style
.top
= canvas
.style
.top
;
851 h
.style
.left
= canvas
.style
.left
;
852 h
.width
= this.width_
;
853 h
.height
= this.height_
;
854 h
.style
.width
= this.width_
+ "px"; // for IE
855 h
.style
.height
= this.height_
+ "px"; // for IE
860 * Generate a set of distinct colors for the data series. This is done with a
861 * color wheel. Saturation/Value are customizable, and the hue is
862 * equally-spaced around the color wheel. If a custom set of colors is
863 * specified, that is used instead.
866 Dygraph
.prototype.setColors_
= function() {
867 var num
= this.attr_("labels").length
- 1;
869 var colors
= this.attr_('colors');
871 var sat
= this.attr_('colorSaturation') || 1.0;
872 var val
= this.attr_('colorValue') || 0.5;
873 var half
= Math
.ceil(num
/ 2);
874 for (var i
= 1; i
<= num
; i
++) {
875 if (!this.visibility()[i
-1]) continue;
876 // alternate colors for high contrast.
877 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
878 var hue
= (1.0 * idx
/ (1 + num
));
879 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
882 for (var i
= 0; i
< num
; i
++) {
883 if (!this.visibility()[i
]) continue;
884 var colorStr
= colors
[i
% colors
.length
];
885 this.colors_
.push(colorStr
);
889 this.plotter_
.setColors(this.colors_
);
893 * Return the list of colors. This is either the list of colors passed in the
894 * attributes or the autogenerated list of rgb(r,g,b) strings.
895 * @return {Array<string>} The list of colors.
897 Dygraph
.prototype.getColors
= function() {
902 * Create the div that contains information on the selected point(s)
903 * This goes in the top right of the canvas, unless an external div has already
907 Dygraph
.prototype.createStatusMessage_
= function() {
908 var userLabelsDiv
= this.user_attrs_
["labelsDiv"];
909 if (userLabelsDiv
&& null != userLabelsDiv
910 && (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
911 this.user_attrs_
["labelsDiv"] = document
.getElementById(userLabelsDiv
);
913 if (!this.attr_("labelsDiv")) {
914 var divWidth
= this.attr_('labelsDivWidth');
916 "position": "absolute",
919 "width": divWidth
+ "px",
921 "left": (this.width_
- divWidth
- 2) + "px",
922 "background": "white",
924 "overflow": "hidden"};
925 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
926 var div
= document
.createElement("div");
927 div
.className
= "dygraph-legend";
928 for (var name
in messagestyle
) {
929 if (messagestyle
.hasOwnProperty(name
)) {
930 div
.style
[name
] = messagestyle
[name
];
933 this.graphDiv
.appendChild(div
);
934 this.attrs_
.labelsDiv
= div
;
939 * Position the labels div so that:
940 * - its right edge is flush with the right edge of the charting area
941 * - its top edge is flush with the top edge of the charting area
944 Dygraph
.prototype.positionLabelsDiv_
= function() {
945 // Don't touch a user-specified labelsDiv.
946 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
948 var area
= this.plotter_
.area
;
949 var div
= this.attr_("labelsDiv");
950 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
951 div
.style
.top
= area
.y
+ "px";
955 * Create the text box to adjust the averaging period
958 Dygraph
.prototype.createRollInterface_
= function() {
959 // Create a roller if one doesn't exist already.
961 this.roller_
= document
.createElement("input");
962 this.roller_
.type
= "text";
963 this.roller_
.style
.display
= "none";
964 this.graphDiv
.appendChild(this.roller_
);
967 var display
= this.attr_('showRoller') ? 'block' : 'none';
969 var area
= this.plotter_
.area
;
970 var textAttr
= { "position": "absolute",
972 "top": (area
.y
+ area
.h
- 25) + "px",
973 "left": (area
.x
+ 1) + "px",
976 this.roller_
.size
= "2";
977 this.roller_
.value
= this.rollPeriod_
;
978 for (var name
in textAttr
) {
979 if (textAttr
.hasOwnProperty(name
)) {
980 this.roller_
.style
[name
] = textAttr
[name
];
985 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
990 * Converts page the x-coordinate of the event to pixel x-coordinates on the
991 * canvas (i.e. DOM Coords).
993 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
994 return Dygraph
.pageX(e
) - context
.px
999 * Converts page the y-coordinate of the event to pixel y-coordinates on the
1000 * canvas (i.e. DOM Coords).
1002 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
1003 return Dygraph
.pageY(e
) - context
.py
1007 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1011 Dygraph
.prototype.createDragInterface_
= function() {
1013 // Tracks whether the mouse is down right now
1015 isPanning
: false, // is this drag part of a pan?
1016 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1017 dragStartX
: null, // pixel coordinates
1018 dragStartY
: null, // pixel coordinates
1019 dragEndX
: null, // pixel coordinates
1020 dragEndY
: null, // pixel coordinates
1021 dragDirection
: null,
1022 prevEndX
: null, // pixel coordinates
1023 prevEndY
: null, // pixel coordinates
1024 prevDragDirection
: null,
1026 // The value on the left side of the graph when a pan operation starts.
1027 initialLeftmostDate
: null,
1029 // The number of units each pixel spans. (This won't be valid for log
1031 xUnitsPerPixel
: null,
1033 // TODO(danvk): update this comment
1034 // The range in second/value units that the viewport encompasses during a
1035 // panning operation.
1038 // Top-left corner of the canvas, in DOM coords
1039 // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1043 // Values for use with panEdgeFraction, which limit how far outside the
1044 // graph's data boundaries it can be panned.
1045 boundedDates
: null, // [minDate, maxDate]
1046 boundedValues
: null, // [[minValue, maxValue] ...]
1048 initializeMouseDown
: function(event
, g
, context
) {
1049 // prevents mouse drags from selecting page text.
1050 if (event
.preventDefault
) {
1051 event
.preventDefault(); // Firefox, Chrome, etc.
1053 event
.returnValue
= false; // IE
1054 event
.cancelBubble
= true;
1057 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1058 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1059 context
.dragStartX
= g
.dragGetX_(event
, context
);
1060 context
.dragStartY
= g
.dragGetY_(event
, context
);
1064 var interactionModel
= this.attr_("interactionModel");
1066 // Self is the graph.
1069 // Function that binds the graph and context to the handler.
1070 var bindHandler
= function(handler
) {
1071 return function(event
) {
1072 handler(event
, self
, context
);
1076 for (var eventName
in interactionModel
) {
1077 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1078 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1079 bindHandler(interactionModel
[eventName
]));
1082 // If the user releases the mouse button during a drag, but not over the
1083 // canvas, then it doesn't count as a zooming action.
1084 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1085 if (context
.isZooming
|| context
.isPanning
) {
1086 context
.isZooming
= false;
1087 context
.dragStartX
= null;
1088 context
.dragStartY
= null;
1091 if (context
.isPanning
) {
1092 context
.isPanning
= false;
1093 context
.draggingDate
= null;
1094 context
.dateRange
= null;
1095 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1096 delete self
.axes_
[i
].draggingValue
;
1097 delete self
.axes_
[i
].dragValueRange
;
1105 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1106 * up any previous zoom rectangles that were drawn. This could be optimized to
1107 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1110 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1111 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1112 * @param {Number} startX The X position where the drag started, in canvas
1114 * @param {Number} endX The current X position of the drag, in canvas coords.
1115 * @param {Number} startY The Y position where the drag started, in canvas
1117 * @param {Number} endY The current Y position of the drag, in canvas coords.
1118 * @param {Number} prevDirection the value of direction on the previous call to
1119 * this function. Used to avoid excess redrawing
1120 * @param {Number} prevEndX The value of endX on the previous call to this
1121 * function. Used to avoid excess redrawing
1122 * @param {Number} prevEndY The value of endY on the previous call to this
1123 * function. Used to avoid excess redrawing
1126 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
,
1127 endY
, prevDirection
, prevEndX
,
1129 var ctx
= this.canvas_ctx_
;
1131 // Clean up from the previous rect if necessary
1132 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1133 ctx
.clearRect(Math
.min(startX
, prevEndX
), 0,
1134 Math
.abs(startX
- prevEndX
), this.height_
);
1135 } else if (prevDirection
== Dygraph
.VERTICAL
){
1136 ctx
.clearRect(0, Math
.min(startY
, prevEndY
),
1137 this.width_
, Math
.abs(startY
- prevEndY
));
1140 // Draw a light-grey rectangle to show the new viewing area
1141 if (direction
== Dygraph
.HORIZONTAL
) {
1142 if (endX
&& startX
) {
1143 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1144 ctx
.fillRect(Math
.min(startX
, endX
), 0,
1145 Math
.abs(endX
- startX
), this.height_
);
1148 if (direction
== Dygraph
.VERTICAL
) {
1149 if (endY
&& startY
) {
1150 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1151 ctx
.fillRect(0, Math
.min(startY
, endY
),
1152 this.width_
, Math
.abs(endY
- startY
));
1158 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1159 * the canvas. The exact zoom window may be slightly larger if there are no data
1160 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1161 * which accepts dates that match the raw data. This function redraws the graph.
1163 * @param {Number} lowX The leftmost pixel value that should be visible.
1164 * @param {Number} highX The rightmost pixel value that should be visible.
1167 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1168 // Find the earliest and latest dates contained in this canvasx range.
1169 // Convert the call to date ranges of the raw data.
1170 var minDate
= this.toDataXCoord(lowX
);
1171 var maxDate
= this.toDataXCoord(highX
);
1172 this.doZoomXDates_(minDate
, maxDate
);
1176 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1177 * method with doZoomX which accepts pixel coordinates. This function redraws
1180 * @param {Number} minDate The minimum date that should be visible.
1181 * @param {Number} maxDate The maximum date that should be visible.
1184 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1185 this.dateWindow_
= [minDate
, maxDate
];
1186 this.zoomed_x_
= true;
1188 if (this.attr_("zoomCallback")) {
1189 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1194 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1195 * the canvas. This function redraws the graph.
1197 * @param {Number} lowY The topmost pixel value that should be visible.
1198 * @param {Number} highY The lowest pixel value that should be visible.
1201 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1202 // Find the highest and lowest values in pixel range for each axis.
1203 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1204 // This is because pixels increase as you go down on the screen, whereas data
1205 // coordinates increase as you go up the screen.
1206 var valueRanges
= [];
1207 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1208 var hi
= this.toDataYCoord(lowY
, i
);
1209 var low
= this.toDataYCoord(highY
, i
);
1210 this.axes_
[i
].valueWindow
= [low
, hi
];
1211 valueRanges
.push([low
, hi
]);
1214 this.zoomed_y_
= true;
1216 if (this.attr_("zoomCallback")) {
1217 var xRange
= this.xAxisRange();
1218 var yRange
= this.yAxisRange();
1219 this.attr_("zoomCallback")(xRange
[0], xRange
[1], this.yAxisRanges());
1224 * Reset the zoom to the original view coordinates. This is the same as
1225 * double-clicking on the graph.
1229 Dygraph
.prototype.doUnzoom_
= function() {
1231 if (this.dateWindow_
!= null) {
1233 this.dateWindow_
= null;
1236 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1237 if (this.axes_
[i
].valueWindow
!= null) {
1239 delete this.axes_
[i
].valueWindow
;
1243 // Clear any selection, since it's likely to be drawn in the wrong place.
1244 this.clearSelection();
1247 // Putting the drawing operation before the callback because it resets
1249 this.zoomed_x_
= false;
1250 this.zoomed_y_
= false;
1252 if (this.attr_("zoomCallback")) {
1253 var minDate
= this.rawData_
[0][0];
1254 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1255 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1261 * When the mouse moves in the canvas, display information about a nearby data
1262 * point and draw dots over those points in the data series. This function
1263 * takes care of cleanup of previously-drawn dots.
1264 * @param {Object} event The mousemove event from the browser.
1267 Dygraph
.prototype.mouseMove_
= function(event
) {
1268 // This prevents JS errors when mousing over the canvas before data loads.
1269 var points
= this.layout_
.points
;
1270 if (points
=== undefined
) return;
1272 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1277 // Loop through all the points and find the date nearest to our current
1279 var minDist
= 1e+100;
1281 for (var i
= 0; i
< points
.length
; i
++) {
1282 var point
= points
[i
];
1283 if (point
== null) continue;
1284 var dist
= Math
.abs(point
.canvasx
- canvasx
);
1285 if (dist
> minDist
) continue;
1289 if (idx
>= 0) lastx
= points
[idx
].xval
;
1291 // Extract the points we've selected
1292 this.selPoints_
= [];
1293 var l
= points
.length
;
1294 if (!this.attr_("stackedGraph")) {
1295 for (var i
= 0; i
< l
; i
++) {
1296 if (points
[i
].xval
== lastx
) {
1297 this.selPoints_
.push(points
[i
]);
1301 // Need to 'unstack' points starting from the bottom
1302 var cumulative_sum
= 0;
1303 for (var i
= l
- 1; i
>= 0; i
--) {
1304 if (points
[i
].xval
== lastx
) {
1305 var p
= {}; // Clone the point since we modify it
1306 for (var k
in points
[i
]) {
1307 p
[k
] = points
[i
][k
];
1309 p
.yval
-= cumulative_sum
;
1310 cumulative_sum
+= p
.yval
;
1311 this.selPoints_
.push(p
);
1314 this.selPoints_
.reverse();
1317 if (this.attr_("highlightCallback")) {
1318 var px
= this.lastx_
;
1319 if (px
!== null && lastx
!= px
) {
1320 // only fire if the selected point has changed.
1321 this.attr_("highlightCallback")(event
, lastx
, this.selPoints_
, this.idxToRow_(idx
));
1325 // Save last x position for callbacks.
1326 this.lastx_
= lastx
;
1328 this.updateSelection_();
1332 * Transforms layout_.points index into data row number.
1333 * @param int layout_.points index
1334 * @return int row number, or -1 if none could be found.
1337 Dygraph
.prototype.idxToRow_
= function(idx
) {
1338 if (idx
< 0) return -1;
1340 for (var i
in this.layout_
.datasets
) {
1341 if (idx
< this.layout_
.datasets
[i
].length
) {
1342 return this.boundaryIds_
[0][0]+idx
;
1344 idx
-= this.layout_
.datasets
[i
].length
;
1351 * Generates HTML for the legend which is displayed when hovering over the
1352 * chart. If no selected points are specified, a default legend is returned
1353 * (this may just be the empty string).
1354 * @param { Number } [x] The x-value of the selected points.
1355 * @param { [Object] } [sel_points] List of selected points for the given
1356 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1358 Dygraph
.prototype.generateLegendHTML_
= function(x
, sel_points
) {
1359 // If no points are selected, we display a default legend. Traditionally,
1360 // this has been blank. But a better default would be a conventional legend,
1361 // which provides essential information for a non-interactive chart.
1362 if (typeof(x
) === 'undefined') {
1363 if (this.attr_('legend') != 'always') return '';
1365 var sepLines
= this.attr_('labelsSeparateLines');
1366 var labels
= this.attr_('labels');
1368 for (var i
= 1; i
< labels
.length
; i
++) {
1369 if (!this.visibility()[i
- 1]) continue;
1370 var c
= this.plotter_
.colors
[labels
[i
]];
1371 if (html
!= '') html
+= (sepLines
? '<br/>' : ' ');
1372 html
+= "<b><span style='color: " + c
+ ";'>—" + labels
[i
] +
1378 var xOptView
= this.optionsViewForAxis_('x');
1379 var xvf
= xOptView('valueFormatter');
1380 var html
= xvf(x
, xOptView
, this.attr_('labels')[0], this) + ":";
1383 var num_axes
= this.numAxes();
1384 for (var i
= 0; i
< num_axes
; i
++) {
1385 yOptViews
[i
] = this.optionsViewForAxis_('y' + (i
? 1 + i
: ''));
1387 var showZeros
= this.attr_("labelsShowZeroValues");
1388 var sepLines
= this.attr_("labelsSeparateLines");
1389 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1390 var pt
= this.selPoints_
[i
];
1391 if (pt
.yval
== 0 && !showZeros
) continue;
1392 if (!Dygraph
.isOK(pt
.canvasy
)) continue;
1393 if (sepLines
) html
+= "<br/>";
1395 var yOptView
= yOptViews
[this.seriesToAxisMap_
[pt
.name
]];
1396 var fmtFunc
= yOptView('valueFormatter');
1397 var c
= this.plotter_
.colors
[pt
.name
];
1398 var yval
= fmtFunc(pt
.yval
, yOptView
, pt
.name
, this);
1400 // TODO(danvk): use a template string here and make it an attribute.
1401 html
+= " <b><span style='color: " + c
+ ";'>"
1402 + pt
.name
+ "</span></b>:"
1410 * Displays information about the selected points in the legend. If there is no
1411 * selection, the legend will be cleared.
1412 * @param { Number } [x] The x-value of the selected points.
1413 * @param { [Object] } [sel_points] List of selected points for the given
1414 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1416 Dygraph
.prototype.setLegendHTML_
= function(x
, sel_points
) {
1417 var html
= this.generateLegendHTML_(x
, sel_points
);
1418 var labelsDiv
= this.attr_("labelsDiv");
1419 if (labelsDiv
!== null) {
1420 labelsDiv
.innerHTML
= html
;
1422 if (typeof(this.shown_legend_error_
) == 'undefined') {
1423 this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
1424 this.shown_legend_error_
= true;
1430 * Draw dots over the selectied points in the data series. This function
1431 * takes care of cleanup of previously-drawn dots.
1434 Dygraph
.prototype.updateSelection_
= function() {
1435 // Clear the previously drawn vertical, if there is one
1436 var ctx
= this.canvas_ctx_
;
1437 if (this.previousVerticalX_
>= 0) {
1438 // Determine the maximum highlight circle size.
1439 var maxCircleSize
= 0;
1440 var labels
= this.attr_('labels');
1441 for (var i
= 1; i
< labels
.length
; i
++) {
1442 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1443 if (r
> maxCircleSize
) maxCircleSize
= r
;
1445 var px
= this.previousVerticalX_
;
1446 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1447 2 * maxCircleSize
+ 2, this.height_
);
1450 if (this.selPoints_
.length
> 0) {
1451 // Set the status message to indicate the selected point(s)
1452 if (this.attr_('showLabelsOnHighlight')) {
1453 this.setLegendHTML_(this.lastx_
, this.selPoints_
);
1456 // Draw colored circles over the center of each selected point
1457 var canvasx
= this.selPoints_
[0].canvasx
;
1459 for (var i
= 0; i
< this.selPoints_
.length
; i
++) {
1460 var pt
= this.selPoints_
[i
];
1461 if (!Dygraph
.isOK(pt
.canvasy
)) continue;
1463 var circleSize
= this.attr_('highlightCircleSize', pt
.name
);
1465 ctx
.fillStyle
= this.plotter_
.colors
[pt
.name
];
1466 ctx
.arc(canvasx
, pt
.canvasy
, circleSize
, 0, 2 * Math
.PI
, false);
1471 this.previousVerticalX_
= canvasx
;
1476 * Manually set the selected points and display information about them in the
1477 * legend. The selection can be cleared using clearSelection() and queried
1478 * using getSelection().
1479 * @param { Integer } row number that should be highlighted (i.e. appear with
1480 * hover dots on the chart). Set to false to clear any selection.
1482 Dygraph
.prototype.setSelection
= function(row
) {
1483 // Extract the points we've selected
1484 this.selPoints_
= [];
1487 if (row
!== false) {
1488 row
= row
-this.boundaryIds_
[0][0];
1491 if (row
!== false && row
>= 0) {
1492 for (var i
in this.layout_
.datasets
) {
1493 if (row
< this.layout_
.datasets
[i
].length
) {
1494 var point
= this.layout_
.points
[pos
+row
];
1496 if (this.attr_("stackedGraph")) {
1497 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1500 this.selPoints_
.push(point
);
1502 pos
+= this.layout_
.datasets
[i
].length
;
1506 if (this.selPoints_
.length
) {
1507 this.lastx_
= this.selPoints_
[0].xval
;
1508 this.updateSelection_();
1510 this.clearSelection();
1516 * The mouse has left the canvas. Clear out whatever artifacts remain
1517 * @param {Object} event the mouseout event from the browser.
1520 Dygraph
.prototype.mouseOut_
= function(event
) {
1521 if (this.attr_("unhighlightCallback")) {
1522 this.attr_("unhighlightCallback")(event
);
1525 if (this.attr_("hideOverlayOnMouseOut")) {
1526 this.clearSelection();
1531 * Clears the current selection (i.e. points that were highlighted by moving
1532 * the mouse over the chart).
1534 Dygraph
.prototype.clearSelection
= function() {
1535 // Get rid of the overlay data
1536 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1537 this.setLegendHTML_();
1538 this.selPoints_
= [];
1543 * Returns the number of the currently selected row. To get data for this row,
1544 * you can use the getValue method.
1545 * @return { Integer } row number, or -1 if nothing is selected
1547 Dygraph
.prototype.getSelection
= function() {
1548 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1552 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
1553 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
1554 return row
+ this.boundaryIds_
[0][0];
1561 * Fires when there's data available to be graphed.
1562 * @param {String} data Raw CSV data to be plotted
1565 Dygraph
.prototype.loadedEvent_
= function(data
) {
1566 this.rawData_
= this.parseCSV_(data
);
1571 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1574 Dygraph
.prototype.addXTicks_
= function() {
1575 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1577 if (this.dateWindow_
) {
1578 range
= [this.dateWindow_
[0], this.dateWindow_
[1]];
1580 range
= [this.rawData_
[0][0], this.rawData_
[this.rawData_
.length
- 1][0]];
1583 var xAxisOptionsView
= this.optionsViewForAxis_('x');
1584 var xTicks
= xAxisOptionsView('ticker')(
1587 this.width_
, // TODO(danvk): should be area.width
1590 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
1591 // console.log(msg);
1592 this.layout_
.setXTicks(xTicks
);
1597 * Computes the range of the data series (including confidence intervals).
1598 * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
1599 * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
1600 * @return [low, high]
1602 Dygraph
.prototype.extremeValues_
= function(series
) {
1603 var minY
= null, maxY
= null;
1605 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1607 // With custom bars, maxY is the max of the high values.
1608 for (var j
= 0; j
< series
.length
; j
++) {
1609 var y
= series
[j
][1][0];
1611 var low
= y
- series
[j
][1][1];
1612 var high
= y
+ series
[j
][1][2];
1613 if (low
> y
) low
= y
; // this can happen with custom bars,
1614 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
1615 if (maxY
== null || high
> maxY
) {
1618 if (minY
== null || low
< minY
) {
1623 for (var j
= 0; j
< series
.length
; j
++) {
1624 var y
= series
[j
][1];
1625 if (y
=== null || isNaN(y
)) continue;
1626 if (maxY
== null || y
> maxY
) {
1629 if (minY
== null || y
< minY
) {
1635 return [minY
, maxY
];
1640 * This function is called once when the chart's data is changed or the options
1641 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1642 * idea is that values derived from the chart's data can be computed here,
1643 * rather than every time the chart is drawn. This includes things like the
1644 * number of axes, rolling averages, etc.
1646 Dygraph
.prototype.predraw_
= function() {
1647 var start
= new Date();
1649 // TODO(danvk): move more computations out of drawGraph_ and into here.
1650 this.computeYAxes_();
1652 // Create a new plotter.
1653 if (this.plotter_
) this.plotter_
.clear();
1654 this.plotter_
= new DygraphCanvasRenderer(this,
1659 // The roller sits in the bottom left corner of the chart. We don't know where
1660 // this will be until the options are available, so it's positioned here.
1661 this.createRollInterface_();
1663 // Same thing applies for the labelsDiv. It's right edge should be flush with
1664 // the right edge of the charting area (which may not be the same as the right
1665 // edge of the div, if we have two y-axes.
1666 this.positionLabelsDiv_();
1668 // If the data or options have changed, then we'd better redraw.
1671 // This is used to determine whether to do various animations.
1672 var end
= new Date();
1673 this.drawingTimeMs_
= (end
- start
);
1677 * Update the graph with new data. This method is called when the viewing area
1678 * has changed. If the underlying data or options have changed, predraw_ will
1679 * be called before drawGraph_ is called.
1681 * clearSelection, when undefined or true, causes this.clearSelection to be
1682 * called at the end of the draw operation. This should rarely be defined,
1683 * and never true (that is it should be undefined most of the time, and
1688 Dygraph
.prototype.drawGraph_
= function(clearSelection
) {
1689 var start
= new Date();
1691 if (typeof(clearSelection
) === 'undefined') {
1692 clearSelection
= true;
1695 var data
= this.rawData_
;
1697 // This is used to set the second parameter to drawCallback, below.
1698 var is_initial_draw
= this.is_initial_draw_
;
1699 this.is_initial_draw_
= false;
1701 var minY
= null, maxY
= null;
1702 this.layout_
.removeAllDatasets();
1704 this.attrs_
['pointSize'] = 0.5 * this.attr_('highlightCircleSize');
1706 // Loop over the fields (series). Go from the last to the first,
1707 // because if they're stacked that's how we accumulate the values.
1709 var cumulative_y
= []; // For stacked series.
1712 var extremes
= {}; // series name -> [low, high]
1714 // Loop over all fields and create datasets
1715 for (var i
= data
[0].length
- 1; i
>= 1; i
--) {
1716 if (!this.visibility()[i
- 1]) continue;
1718 var seriesName
= this.attr_("labels")[i
];
1719 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
1720 var logScale
= this.attr_('logscale', i
);
1723 for (var j
= 0; j
< data
.length
; j
++) {
1724 var date
= data
[j
][0];
1725 var point
= data
[j
][i
];
1727 // On the log scale, points less than zero do not exist.
1728 // This will create a gap in the chart. Note that this ignores
1729 // connectSeparatedPoints.
1733 series
.push([date
, point
]);
1735 if (point
!= null || !connectSeparatedPoints
) {
1736 series
.push([date
, point
]);
1741 // TODO(danvk): move this into predraw_. It's insane to do it here.
1742 series
= this.rollingAverage(series
, this.rollPeriod_
);
1744 // Prune down to the desired range, if necessary (for zooming)
1745 // Because there can be lines going to points outside of the visible area,
1746 // we actually prune to visible points, plus one on either side.
1747 var bars
= this.attr_("errorBars") || this.attr_("customBars");
1748 if (this.dateWindow_
) {
1749 var low
= this.dateWindow_
[0];
1750 var high
= this.dateWindow_
[1];
1752 // TODO(danvk): do binary search instead of linear search.
1753 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
1754 var firstIdx
= null, lastIdx
= null;
1755 for (var k
= 0; k
< series
.length
; k
++) {
1756 if (series
[k
][0] >= low
&& firstIdx
=== null) {
1759 if (series
[k
][0] <= high
) {
1763 if (firstIdx
=== null) firstIdx
= 0;
1764 if (firstIdx
> 0) firstIdx
--;
1765 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
1766 if (lastIdx
< series
.length
- 1) lastIdx
++;
1767 this.boundaryIds_
[i
-1] = [firstIdx
, lastIdx
];
1768 for (var k
= firstIdx
; k
<= lastIdx
; k
++) {
1769 pruned
.push(series
[k
]);
1773 this.boundaryIds_
[i
-1] = [0, series
.length
-1];
1776 var seriesExtremes
= this.extremeValues_(series
);
1779 for (var j
=0; j
<series
.length
; j
++) {
1780 val
= [series
[j
][0], series
[j
][1][0], series
[j
][1][1], series
[j
][1][2]];
1783 } else if (this.attr_("stackedGraph")) {
1784 var l
= series
.length
;
1786 for (var j
= 0; j
< l
; j
++) {
1787 // If one data set has a NaN, let all subsequent stacked
1788 // sets inherit the NaN -- only start at 0 for the first set.
1789 var x
= series
[j
][0];
1790 if (cumulative_y
[x
] === undefined
) {
1791 cumulative_y
[x
] = 0;
1794 actual_y
= series
[j
][1];
1795 cumulative_y
[x
] += actual_y
;
1797 series
[j
] = [x
, cumulative_y
[x
]]
1799 if (cumulative_y
[x
] > seriesExtremes
[1]) {
1800 seriesExtremes
[1] = cumulative_y
[x
];
1802 if (cumulative_y
[x
] < seriesExtremes
[0]) {
1803 seriesExtremes
[0] = cumulative_y
[x
];
1807 extremes
[seriesName
] = seriesExtremes
;
1809 datasets
[i
] = series
;
1812 for (var i
= 1; i
< datasets
.length
; i
++) {
1813 if (!this.visibility()[i
- 1]) continue;
1814 this.layout_
.addDataset(this.attr_("labels")[i
], datasets
[i
]);
1817 this.computeYAxisRanges_(extremes
);
1818 this.layout_
.setYAxes(this.axes_
);
1822 // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
1823 var tmp_zoomed_x
= this.zoomed_x_
;
1824 // Tell PlotKit to use this new data and render itself
1825 this.layout_
.setDateWindow(this.dateWindow_
);
1826 this.zoomed_x_
= tmp_zoomed_x
;
1827 this.layout_
.evaluateWithError();
1828 this.renderGraph_(is_initial_draw
, false);
1830 if (this.attr_("timingName")) {
1831 var end
= new Date();
1833 console
.log(this.attr_("timingName") + " - drawGraph: " + (end
- start
) + "ms")
1838 Dygraph
.prototype.renderGraph_
= function(is_initial_draw
, clearSelection
) {
1839 this.plotter_
.clear();
1840 this.plotter_
.render();
1841 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
1842 this.canvas_
.height
);
1844 if (is_initial_draw
) {
1845 // Generate a static legend before any particular point is selected.
1846 this.setLegendHTML_();
1848 if (clearSelection
) {
1849 if (typeof(this.selPoints_
) !== 'undefined' && this.selPoints_
.length
) {
1850 // We should select the point nearest the page x/y here
, but it
's easier
1851 // to just clear the selection. This prevents erroneous hover dots from
1853 this.clearSelection();
1855 this.clearSelection();
1860 if (this.attr_("drawCallback") !== null) {
1861 this.attr_("drawCallback")(this, is_initial_draw);
1867 * Determine properties of the y-axes which are independent of the data
1868 * currently being displayed. This includes things like the number of axes and
1869 * the style of the axes. It does not include the range of each axis and its
1871 * This fills in this.axes_ and this.seriesToAxisMap_.
1872 * axes_ = [ { options } ]
1873 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
1874 * indices are into the axes_ array.
1876 Dygraph.prototype.computeYAxes_ = function() {
1877 // Preserve valueWindow settings if they exist, and if the user hasn't
1878 // specified a new valueRange.
1880 if (this.axes_
!= undefined
&& this.user_attrs_
.hasOwnProperty("valueRange") == false) {
1882 for (var index
= 0; index
< this.axes_
.length
; index
++) {
1883 valueWindows
.push(this.axes_
[index
].valueWindow
);
1887 this.axes_
= [{ yAxisId
: 0, g
: this }]; // always have at least one y-axis.
1888 this.seriesToAxisMap_
= {};
1890 // Get a list of series names.
1891 var labels
= this.attr_("labels");
1893 for (var i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
1895 // all options which could be applied per-axis:
1903 'axisLabelFontSize',
1908 // Copy global axis options over to the first axis.
1909 for (var i
= 0; i
< axisOptions
.length
; i
++) {
1910 var k
= axisOptions
[i
];
1911 var v
= this.attr_(k
);
1912 if (v
) this.axes_
[0][k
] = v
;
1915 // Go through once and add all the axes.
1916 for (var seriesName
in series
) {
1917 if (!series
.hasOwnProperty(seriesName
)) continue;
1918 var axis
= this.attr_("axis", seriesName
);
1920 this.seriesToAxisMap_
[seriesName
] = 0;
1923 if (typeof(axis
) == 'object') {
1924 // Add a new axis, making a copy of its per-axis options.
1926 Dygraph
.update(opts
, this.axes_
[0]);
1927 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
1928 var yAxisId
= this.axes_
.length
;
1929 opts
.yAxisId
= yAxisId
;
1931 Dygraph
.update(opts
, axis
);
1932 this.axes_
.push(opts
);
1933 this.seriesToAxisMap_
[seriesName
] = yAxisId
;
1937 // Go through one more time and assign series to an axis defined by another
1938 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
1939 for (var seriesName
in series
) {
1940 if (!series
.hasOwnProperty(seriesName
)) continue;
1941 var axis
= this.attr_("axis", seriesName
);
1942 if (typeof(axis
) == 'string') {
1943 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
1944 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
1945 "series " + axis
+ ", which does not define its own axis.");
1948 var idx
= this.seriesToAxisMap_
[axis
];
1949 this.seriesToAxisMap_
[seriesName
] = idx
;
1953 // Now we remove series from seriesToAxisMap_ which are not visible. We do
1954 // this last so that hiding the first series doesn't destroy the axis
1955 // properties of the primary axis.
1956 var seriesToAxisFiltered
= {};
1957 var vis
= this.visibility();
1958 for (var i
= 1; i
< labels
.length
; i
++) {
1960 if (vis
[i
- 1]) seriesToAxisFiltered
[s
] = this.seriesToAxisMap_
[s
];
1962 this.seriesToAxisMap_
= seriesToAxisFiltered
;
1964 if (valueWindows
!= undefined
) {
1965 // Restore valueWindow settings.
1966 for (var index
= 0; index
< valueWindows
.length
; index
++) {
1967 this.axes_
[index
].valueWindow
= valueWindows
[index
];
1973 * Returns the number of y-axes on the chart.
1974 * @return {Number} the number of axes.
1976 Dygraph
.prototype.numAxes
= function() {
1978 for (var series
in this.seriesToAxisMap_
) {
1979 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
1980 var idx
= this.seriesToAxisMap_
[series
];
1981 if (idx
> last_axis
) last_axis
= idx
;
1983 return 1 + last_axis
;
1988 * Returns axis properties for the given series.
1989 * @param { String } setName The name of the series for which to get axis
1990 * properties, e.g. 'Y1'.
1991 * @return { Object } The axis properties.
1993 Dygraph
.prototype.axisPropertiesForSeries
= function(series
) {
1994 // TODO(danvk): handle errors.
1995 return this.axes_
[this.seriesToAxisMap_
[series
]];
2000 * Determine the value range and tick marks for each axis.
2001 * @param {Object} extremes A mapping from seriesName -> [low, high]
2002 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2004 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2005 // Build a map from axis number -> [list of series names]
2006 var seriesForAxis
= [];
2007 for (var series
in this.seriesToAxisMap_
) {
2008 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2009 var idx
= this.seriesToAxisMap_
[series
];
2010 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2011 seriesForAxis
[idx
].push(series
);
2014 // Compute extreme values, a span and tick marks for each axis.
2015 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2016 var axis
= this.axes_
[i
];
2018 if (!seriesForAxis
[i
]) {
2019 // If no series are defined or visible then use a reasonable default
2020 axis
.extremeRange
= [0, 1];
2022 // Calculate the extremes of extremes.
2023 var series
= seriesForAxis
[i
];
2024 var minY
= Infinity
; // extremes[series[0]][0];
2025 var maxY
= -Infinity
; // extremes[series[0]][1];
2026 var extremeMinY
, extremeMaxY
;
2027 for (var j
= 0; j
< series
.length
; j
++) {
2028 // Only use valid extremes to stop null data series' from corrupting the scale.
2029 extremeMinY
= extremes
[series
[j
]][0];
2030 if (extremeMinY
!= null) {
2031 minY
= Math
.min(extremeMinY
, minY
);
2033 extremeMaxY
= extremes
[series
[j
]][1];
2034 if (extremeMaxY
!= null) {
2035 maxY
= Math
.max(extremeMaxY
, maxY
);
2038 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2040 // Ensure we have a valid scale, otherwise defualt to zero for safety.
2041 if (minY
== Infinity
) minY
= 0;
2042 if (maxY
== -Infinity
) maxY
= 0;
2044 // Add some padding and round up to an integer to be human-friendly.
2045 var span
= maxY
- minY
;
2046 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2047 if (span
== 0) { span
= maxY
; }
2051 if (axis
.logscale
) {
2052 var maxAxisY
= maxY
+ 0.1 * span
;
2053 var minAxisY
= minY
;
2055 var maxAxisY
= maxY
+ 0.1 * span
;
2056 var minAxisY
= minY
- 0.1 * span
;
2058 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2059 if (!this.attr_("avoidMinZero")) {
2060 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2061 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2064 if (this.attr_("includeZero")) {
2065 if (maxY
< 0) maxAxisY
= 0;
2066 if (minY
> 0) minAxisY
= 0;
2069 axis
.extremeRange
= [minAxisY
, maxAxisY
];
2071 if (axis
.valueWindow
) {
2072 // This is only set if the user has zoomed on the y-axis. It is never set
2073 // by a user. It takes precedence over axis.valueRange because, if you set
2074 // valueRange, you'd still expect to be able to pan.
2075 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2076 } else if (axis
.valueRange
) {
2077 // This is a user-set value range for this axis.
2078 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2080 axis
.computedValueRange
= axis
.extremeRange
;
2083 // Add ticks. By default, all axes inherit the tick positions of the
2084 // primary axis. However, if an axis is specifically marked as having
2085 // independent ticks, then that is permissible as well.
2086 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2087 var ticker
= opts('ticker');
2088 if (i
== 0 || axis
.independentTicks
) {
2089 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2090 axis
.computedValueRange
[1],
2091 this.height_
, // TODO(danvk): should be area.height
2095 var p_axis
= this.axes_
[0];
2096 var p_ticks
= p_axis
.ticks
;
2097 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2098 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2099 var tick_values
= [];
2100 for (var k
= 0; k
< p_ticks
.length
; k
++) {
2101 var y_frac
= (p_ticks
[k
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2102 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2103 tick_values
.push(y_val
);
2106 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2107 axis
.computedValueRange
[1],
2108 this.height_
, // TODO(danvk): should be area.height
2118 * Calculates the rolling average of a data set.
2119 * If originalData is [label, val], rolls the average of those.
2120 * If originalData is [label, [, it's interpreted as [value, stddev]
2121 * and the roll is returned in the same form, with appropriately reduced
2122 * stddev for each value.
2123 * Note that this is where fractional input (i.e. '5/10') is converted into
2125 * @param {Array} originalData The data in the appropriate format (see above)
2126 * @param {Number} rollPeriod The number of points over which to average the
2129 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2130 if (originalData
.length
< 2)
2131 return originalData
;
2132 var rollPeriod
= Math
.min(rollPeriod
, originalData
.length
);
2133 var rollingData
= [];
2134 var sigma
= this.attr_("sigma");
2136 if (this.fractions_
) {
2138 var den
= 0; // numerator/denominator
2140 for (var i
= 0; i
< originalData
.length
; i
++) {
2141 num
+= originalData
[i
][1][0];
2142 den
+= originalData
[i
][1][1];
2143 if (i
- rollPeriod
>= 0) {
2144 num
-= originalData
[i
- rollPeriod
][1][0];
2145 den
-= originalData
[i
- rollPeriod
][1][1];
2148 var date
= originalData
[i
][0];
2149 var value
= den
? num
/ den
: 0.0;
2150 if (this.attr_("errorBars")) {
2151 if (this.wilsonInterval_
) {
2152 // For more details on this confidence interval, see:
2153 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2155 var p
= value
< 0 ? 0 : value
, n
= den
;
2156 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2157 var denom
= 1 + sigma
* sigma
/ den
;
2158 var low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2159 var high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2160 rollingData
[i
] = [date
,
2161 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2163 rollingData
[i
] = [date
, [0, 0, 0]];
2166 var stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2167 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2170 rollingData
[i
] = [date
, mult
* value
];
2173 } else if (this.attr_("customBars")) {
2178 for (var i
= 0; i
< originalData
.length
; i
++) {
2179 var data
= originalData
[i
][1];
2181 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2183 if (y
!= null && !isNaN(y
)) {
2189 if (i
- rollPeriod
>= 0) {
2190 var prev
= originalData
[i
- rollPeriod
];
2191 if (prev
[1][1] != null && !isNaN(prev
[1][1])) {
2199 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2200 1.0 * (mid
- low
) / count
,
2201 1.0 * (high
- mid
) / count
]];
2203 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2207 // Calculate the rolling average for the first rollPeriod - 1 points where
2208 // there is not enough data to roll over the full number of points
2209 var num_init_points
= Math
.min(rollPeriod
- 1, originalData
.length
- 2);
2210 if (!this.attr_("errorBars")){
2211 if (rollPeriod
== 1) {
2212 return originalData
;
2215 for (var i
= 0; i
< originalData
.length
; i
++) {
2218 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2219 var y
= originalData
[j
][1];
2220 if (y
== null || isNaN(y
)) continue;
2222 sum
+= originalData
[j
][1];
2225 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2227 rollingData
[i
] = [originalData
[i
][0], null];
2232 for (var i
= 0; i
< originalData
.length
; i
++) {
2236 for (var j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2237 var y
= originalData
[j
][1][0];
2238 if (y
== null || isNaN(y
)) continue;
2240 sum
+= originalData
[j
][1][0];
2241 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2244 var stddev
= Math
.sqrt(variance
) / num_ok
;
2245 rollingData
[i
] = [originalData
[i
][0],
2246 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2248 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2258 * Detects the type of the str (date or numeric) and sets the various
2259 * formatting attributes in this.attrs_ based on this type.
2260 * @param {String} str An x value.
2263 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2265 if (str
.indexOf('-') > 0 ||
2266 str
.indexOf('/') >= 0 ||
2267 isNaN(parseFloat(str
))) {
2269 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2270 // TODO(danvk): remove support for this format.
2275 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2276 this.attrs_
.axes
.x
.valueFormatter
= Dygraph
.dateString_
;
2277 this.attrs_
.axes
.x
.ticker
= Dygraph
.dateTicker
;
2278 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2280 /** @private (shut up, jsdoc!) */
2281 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2282 // TODO(danvk): use Dygraph.numberValueFormatter here?
2283 /** @private (shut up, jsdoc!) */
2284 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2285 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2286 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2291 * Parses the value as a floating point number. This is like the parseFloat()
2292 * built-in, but with a few differences:
2293 * - the empty string is parsed as null, rather than NaN.
2294 * - if the string cannot be parsed at all, an error is logged.
2295 * If the string can't be parsed, this method returns null.
2296 * @param {String} x The string to be parsed
2297 * @param {Number} opt_line_no The line number from which the string comes.
2298 * @param {String} opt_line The text of the line from which the string comes.
2302 // Parse the x as a float or return null if it's not a number.
2303 Dygraph
.prototype.parseFloat_
= function(x
, opt_line_no
, opt_line
) {
2304 var val
= parseFloat(x
);
2305 if (!isNaN(val
)) return val
;
2307 // Try to figure out what happeend.
2308 // If the value is the empty string, parse it as null.
2309 if (/^ *$/.test(x
)) return null;
2311 // If it was actually "NaN", return it as NaN.
2312 if (/^ *nan *$/i.test(x
)) return NaN
;
2314 // Looks like a parsing error.
2315 var msg
= "Unable to parse '" + x
+ "' as a number";
2316 if (opt_line
!== null && opt_line_no
!== null) {
2317 msg
+= " on line " + (1+opt_line_no
) + " ('" + opt_line
+ "') of CSV.";
2326 * Parses a string in a special csv format. We expect a csv file where each
2327 * line is a date point, and the first field in each line is the date string.
2328 * We also expect that all remaining fields represent series.
2329 * if the errorBars attribute is set, then interpret the fields as:
2330 * date, series1, stddev1, series2, stddev2, ...
2331 * @param {[Object]} data See above.
2333 * @return [Object] An array with one entry for each row. These entries
2334 * are an array of cells in that row. The first entry is the parsed x-value for
2335 * the row. The second, third, etc. are the y-values. These can take on one of
2336 * three forms, depending on the CSV and constructor parameters:
2338 * 2. [ value, stddev ]
2339 * 3. [ low value, center value, high value ]
2341 Dygraph
.prototype.parseCSV_
= function(data
) {
2343 var lines
= data
.split("\n");
2345 // Use the default delimiter or fall back to a tab if that makes sense.
2346 var delim
= this.attr_('delimiter');
2347 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2352 if (!('labels' in this.user_attrs_
)) {
2353 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2355 this.attrs_
.labels
= lines
[0].split(delim
); // NOTE: _not_ user_attrs_.
2360 var defaultParserSet
= false; // attempt to auto-detect x value type
2361 var expectedCols
= this.attr_("labels").length
;
2362 var outOfOrder
= false;
2363 for (var i
= start
; i
< lines
.length
; i
++) {
2364 var line
= lines
[i
];
2366 if (line
.length
== 0) continue; // skip blank lines
2367 if (line
[0] == '#') continue; // skip comment lines
2368 var inFields
= line
.split(delim
);
2369 if (inFields
.length
< 2) continue;
2372 if (!defaultParserSet
) {
2373 this.detectTypeFromString_(inFields
[0]);
2374 xParser
= this.attr_("xValueParser");
2375 defaultParserSet
= true;
2377 fields
[0] = xParser(inFields
[0], this);
2379 // If fractions are expected, parse the numbers as "A/B
"
2380 if (this.fractions_) {
2381 for (var j = 1; j < inFields.length; j++) {
2382 // TODO(danvk): figure out an appropriate way to flag parse errors.
2383 var vals = inFields[j].split("/");
2384 if (vals.length != 2) {
2385 this.error('Expected fractional "num
/den
" values in CSV data ' +
2386 "but found a value
'" + inFields[j] + "' on line
" +
2387 (1 + i) + " ('" + line + "') which is not of
this form
.");
2390 fields[j] = [this.parseFloat_(vals[0], i, line),
2391 this.parseFloat_(vals[1], i, line)];
2394 } else if (this.attr_("errorBars
")) {
2395 // If there are error bars, values are (value, stddev) pairs
2396 if (inFields.length % 2 != 1) {
2397 this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2398 'but line ' + (1 + i) + ' has an odd number of values (' +
2399 (inFields.length - 1) + "): '" + line + "'");
2401 for (var j = 1; j < inFields.length; j += 2) {
2402 fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
2403 this.parseFloat_(inFields[j + 1], i, line)];
2405 } else if (this.attr_("customBars
")) {
2406 // Bars are a low;center;high tuple
2407 for (var j = 1; j < inFields.length; j++) {
2408 var val = inFields[j];
2409 if (/^ *$/.test(val)) {
2410 fields[j] = [null, null, null];
2412 var vals = val.split(";");
2413 if (vals.length == 3) {
2414 fields[j] = [ this.parseFloat_(vals[0], i, line),
2415 this.parseFloat_(vals[1], i, line),
2416 this.parseFloat_(vals[2], i, line) ];
2418 this.warning('When using customBars, values must be either blank ' +
2419 'or "low
;center
;high
" tuples (got "' + val +
2420 '" on line ' + (1+i));
2425 // Values are just numbers
2426 for (var j = 1; j < inFields.length; j++) {
2427 fields[j] = this.parseFloat_(inFields[j], i, line);
2430 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2434 if (fields.length != expectedCols) {
2435 this.error("Number of columns
in line
" + i + " (" + fields.length +
2436 ") does not agree
with number of
labels (" + expectedCols +
2440 // If the user specified the 'labels' option and none of the cells of the
2441 // first row parsed correctly, then they probably double-specified the
2442 // labels. We go with the values set in the option, discard this row and
2443 // log a warning to the JS console.
2444 if (i == 0 && this.attr_('labels')) {
2445 var all_null = true;
2446 for (var j = 0; all_null && j < fields.length; j++) {
2447 if (fields[j]) all_null = false;
2450 this.warn("The dygraphs
'labels' option is set
, but the first row of
" +
2451 "CSV
data ('" + line + "') appears to also contain labels
. " +
2452 "Will drop the CSV labels and
use the option labels
.");
2460 this.warn("CSV is out of order
; order it correctly to speed loading
.");
2461 ret.sort(function(a,b) { return a[0] - b[0] });
2469 * The user has provided their data as a pre-packaged JS array. If the x values
2470 * are numeric, this is the same as dygraphs' internal format. If the x values
2471 * are dates, we need to convert them from Date objects to ms since epoch.
2472 * @param {[Object]} data
2473 * @return {[Object]} data with numeric x values.
2475 Dygraph.prototype.parseArray_ = function(data) {
2476 // Peek at the first x value to see if it's numeric.
2477 if (data.length == 0) {
2478 this.error("Can
't plot empty data set");
2481 if (data[0].length == 0) {
2482 this.error("Data set cannot contain an empty row");
2486 if (this.attr_("labels") == null) {
2487 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
2488 "in the options parameter");
2489 this.attrs_.labels = [ "X" ];
2490 for (var i = 1; i < data[0].length; i++) {
2491 this.attrs_.labels.push("Y" + i);
2495 if (Dygraph.isDateLike(data[0][0])) {
2496 // Some intelligent defaults for a date x-axis.
2497 this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
2498 this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
2499 this.attrs_.axes.x.ticker = Dygraph.dateTicker;
2501 // Assume they're all dates
.
2502 var parsedData
= Dygraph
.clone(data
);
2503 for (var i
= 0; i
< data
.length
; i
++) {
2504 if (parsedData
[i
].length
== 0) {
2505 this.error("Row " + (1 + i
) + " of data is empty");
2508 if (parsedData
[i
][0] == null
2509 || typeof(parsedData
[i
][0].getTime
) != 'function'
2510 || isNaN(parsedData
[i
][0].getTime())) {
2511 this.error("x value in row " + (1 + i
) + " is not a Date");
2514 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2518 // Some intelligent defaults for a numeric x-axis.
2519 /** @private (shut up, jsdoc!) */
2520 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2521 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.numberAxisLabelFormatter
;
2522 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2528 * Parses a DataTable object from gviz.
2529 * The data is expected to have a first column that is either a date or a
2530 * number. All subsequent columns must be numbers. If there is a clear mismatch
2531 * between this.xValueParser_ and the type of the first column, it will be
2532 * fixed. Fills out rawData_.
2533 * @param {[Object]} data See above.
2536 Dygraph
.prototype.parseDataTable_
= function(data
) {
2537 var cols
= data
.getNumberOfColumns();
2538 var rows
= data
.getNumberOfRows();
2540 var indepType
= data
.getColumnType(0);
2541 if (indepType
== 'date' || indepType
== 'datetime') {
2542 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2543 this.attrs_
.axes
.x
.valueFormatter
= Dygraph
.dateString_
;
2544 this.attrs_
.axes
.x
.ticker
= Dygraph
.dateTicker
;
2545 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2546 } else if (indepType
== 'number') {
2547 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2548 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2549 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2550 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2552 this.error("only 'date', 'datetime' and 'number' types are supported for " +
2553 "column 1 of DataTable input (Got '" + indepType
+ "')");
2557 // Array of the column indices which contain data (and not annotations).
2559 var annotationCols
= {}; // data index -> [annotation cols]
2560 var hasAnnotations
= false;
2561 for (var i
= 1; i
< cols
; i
++) {
2562 var type
= data
.getColumnType(i
);
2563 if (type
== 'number') {
2565 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
2566 // This is OK -- it's an annotation column.
2567 var dataIdx
= colIdx
[colIdx
.length
- 1];
2568 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2569 annotationCols
[dataIdx
] = [i
];
2571 annotationCols
[dataIdx
].push(i
);
2573 hasAnnotations
= true;
2575 this.error("Only 'number' is supported as a dependent type with Gviz." +
2576 " 'string' is only supported if displayAnnotations is true");
2580 // Read column labels
2581 // TODO(danvk): add support back for errorBars
2582 var labels
= [data
.getColumnLabel(0)];
2583 for (var i
= 0; i
< colIdx
.length
; i
++) {
2584 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2585 if (this.attr_("errorBars")) i
+= 1;
2587 this.attrs_
.labels
= labels
;
2588 cols
= labels
.length
;
2591 var outOfOrder
= false;
2592 var annotations
= [];
2593 for (var i
= 0; i
< rows
; i
++) {
2595 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2596 data
.getValue(i
, 0) === null) {
2597 this.warn("Ignoring row " + i
+
2598 " of DataTable because of undefined or null first column.");
2602 if (indepType
== 'date' || indepType
== 'datetime') {
2603 row
.push(data
.getValue(i
, 0).getTime());
2605 row
.push(data
.getValue(i
, 0));
2607 if (!this.attr_("errorBars")) {
2608 for (var j
= 0; j
< colIdx
.length
; j
++) {
2609 var col
= colIdx
[j
];
2610 row
.push(data
.getValue(i
, col
));
2611 if (hasAnnotations
&&
2612 annotationCols
.hasOwnProperty(col
) &&
2613 data
.getValue(i
, annotationCols
[col
][0]) != null) {
2615 ann
.series
= data
.getColumnLabel(col
);
2617 ann
.shortText
= String
.fromCharCode(65 /* A */ + annotations
.length
)
2619 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2620 if (k
) ann
.text
+= "\n";
2621 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2623 annotations
.push(ann
);
2627 // Strip out infinities, which give dygraphs problems later on.
2628 for (var j
= 0; j
< row
.length
; j
++) {
2629 if (!isFinite(row
[j
])) row
[j
] = null;
2632 for (var j
= 0; j
< cols
- 1; j
++) {
2633 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2636 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2643 this.warn("DataTable is out of order; order it correctly to speed loading.");
2644 ret
.sort(function(a
,b
) { return a
[0] - b
[0] });
2646 this.rawData_
= ret
;
2648 if (annotations
.length
> 0) {
2649 this.setAnnotations(annotations
, true);
2654 * Get the CSV data. If it's in a function, call that function. If it's in a
2655 * file, do an XMLHttpRequest to get it.
2658 Dygraph
.prototype.start_
= function() {
2659 if (typeof this.file_
== 'function') {
2660 // CSV string. Pretend we got it via XHR.
2661 this.loadedEvent_(this.file_());
2662 } else if (Dygraph
.isArrayLike(this.file_
)) {
2663 this.rawData_
= this.parseArray_(this.file_
);
2665 } else if (typeof this.file_
== 'object' &&
2666 typeof this.file_
.getColumnRange
== 'function') {
2667 // must be a DataTable from gviz.
2668 this.parseDataTable_(this.file_
);
2670 } else if (typeof this.file_
== 'string') {
2671 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
2672 if (this.file_
.indexOf('\n') >= 0) {
2673 this.loadedEvent_(this.file_
);
2675 var req
= new XMLHttpRequest();
2677 req
.onreadystatechange
= function () {
2678 if (req
.readyState
== 4) {
2679 if (req
.status
== 200 || // Normal http
2680 req
.status
== 0) { // Chrome w/ --allow
-file
-access
-from
-files
2681 caller
.loadedEvent_(req
.responseText
);
2686 req
.open("GET", this.file_
, true);
2690 this.error("Unknown data format: " + (typeof this.file_
));
2695 * Changes various properties of the graph. These can include:
2697 * <li>file: changes the source data for the graph</li>
2698 * <li>errorBars: changes whether the data contains stddev</li>
2701 * There's a huge variety of options that can be passed to this method. For a
2702 * full list, see http://dygraphs.com/options.html.
2704 * @param {Object} attrs The new properties and values
2705 * @param {Boolean} [block_redraw] Usually the chart is redrawn after every
2706 * call to updateOptions(). If you know better, you can pass true to explicitly
2707 * block the redraw. This can be useful for chaining updateOptions() calls,
2708 * avoiding the occasional infinite loop and preventing redraws when it's not
2709 * necessary (e.g. when updating a callback).
2711 Dygraph
.prototype.updateOptions
= function(input_attrs
, block_redraw
) {
2712 if (typeof(block_redraw
) == 'undefined') block_redraw
= false;
2714 // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
2715 var file
= input_attrs
['file'];
2716 var attrs
= Dygraph
.mapLegacyOptions_(input_attrs
);
2718 // TODO(danvk): this is a mess. Move these options into attr_.
2719 if ('rollPeriod' in attrs
) {
2720 this.rollPeriod_
= attrs
.rollPeriod
;
2722 if ('dateWindow' in attrs
) {
2723 this.dateWindow_
= attrs
.dateWindow
;
2724 if (!('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
2725 this.zoomed_x_
= attrs
.dateWindow
!= null;
2728 if ('valueRange' in attrs
&& !('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
2729 this.zoomed_y_
= attrs
.valueRange
!= null;
2732 // TODO(danvk): validate per-series options.
2737 // highlightCircleSize
2739 // Check if this set options will require new points.
2740 var requiresNewPoints
= Dygraph
.isPixelChangingOptionList(this.attr_("labels"), attrs
);
2742 Dygraph
.updateDeep(this.user_attrs_
, attrs
);
2746 if (!block_redraw
) this.start_();
2748 if (!block_redraw
) {
2749 if (requiresNewPoints
) {
2752 this.renderGraph_(false, false);
2759 * Returns a copy of the options with deprecated names converted into current
2760 * names. Also drops the (potentially-large) 'file' attribute. If the caller is
2761 * interested in that, they should save a copy before calling this.
2764 Dygraph
.mapLegacyOptions_
= function(attrs
) {
2766 for (var k
in attrs
) {
2767 if (k
== 'file') continue;
2768 if (attrs
.hasOwnProperty(k
)) my_attrs
[k
] = attrs
[k
];
2771 var set
= function(axis
, opt
, value
) {
2772 if (!my_attrs
.axes
) my_attrs
.axes
= {};
2773 if (!my_attrs
.axes
[axis
]) my_attrs
.axes
[axis
] = {};
2774 my_attrs
.axes
[axis
][opt
] = value
;
2776 var map
= function(opt
, axis
, new_opt
) {
2777 if (typeof(attrs
[opt
]) != 'undefined') {
2778 set(axis
, new_opt
, attrs
[opt
]);
2779 delete my_attrs
[opt
];
2783 // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
2784 map('xValueFormatter', 'x', 'valueFormatter');
2785 map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
2786 map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
2787 map('xTicker', 'x', 'ticker');
2788 map('yValueFormatter', 'y', 'valueFormatter');
2789 map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
2790 map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
2791 map('yTicker', 'y', 'ticker');
2796 * Resizes the dygraph. If no parameters are specified, resizes to fill the
2797 * containing div (which has presumably changed size since the dygraph was
2798 * instantiated. If the width/height are specified, the div will be resized.
2800 * This is far more efficient than destroying and re-instantiating a
2801 * Dygraph, since it doesn't have to reparse the underlying data.
2803 * @param {Number} [width] Width (in pixels)
2804 * @param {Number} [height] Height (in pixels)
2806 Dygraph
.prototype.resize
= function(width
, height
) {
2807 if (this.resize_lock
) {
2810 this.resize_lock
= true;
2812 if ((width
=== null) != (height
=== null)) {
2813 this.warn("Dygraph.resize() should be called with zero parameters or " +
2814 "two non-NULL parameters. Pretending it was zero.");
2815 width
= height
= null;
2818 var old_width
= this.width_
;
2819 var old_height
= this.height_
;
2822 this.maindiv_
.style
.width
= width
+ "px";
2823 this.maindiv_
.style
.height
= height
+ "px";
2824 this.width_
= width
;
2825 this.height_
= height
;
2827 this.width_
= this.maindiv_
.offsetWidth
;
2828 this.height_
= this.maindiv_
.offsetHeight
;
2831 if (old_width
!= this.width_
|| old_height
!= this.height_
) {
2832 // TODO(danvk): there should be a clear() method.
2833 this.maindiv_
.innerHTML
= "";
2834 this.roller_
= null;
2835 this.attrs_
.labelsDiv
= null;
2836 this.createInterface_();
2837 if (this.annotations_
.length
) {
2838 // createInterface_ reset the layout, so we need to do this.
2839 this.layout_
.setAnnotations(this.annotations_
);
2844 this.resize_lock
= false;
2848 * Adjusts the number of points in the rolling average. Updates the graph to
2849 * reflect the new averaging period.
2850 * @param {Number} length Number of points over which to average the data.
2852 Dygraph
.prototype.adjustRoll
= function(length
) {
2853 this.rollPeriod_
= length
;
2858 * Returns a boolean array of visibility statuses.
2860 Dygraph
.prototype.visibility
= function() {
2861 // Do lazy-initialization, so that this happens after we know the number of
2863 if (!this.attr_("visibility")) {
2864 this.attrs_
["visibility"] = [];
2866 while (this.attr_("visibility").length
< this.rawData_
[0].length
- 1) {
2867 this.attr_("visibility").push(true);
2869 return this.attr_("visibility");
2873 * Changes the visiblity of a series.
2875 Dygraph
.prototype.setVisibility
= function(num
, value
) {
2876 var x
= this.visibility();
2877 if (num
< 0 || num
>= x
.length
) {
2878 this.warn("invalid series number in setVisibility: " + num
);
2886 * How large of an area will the dygraph render itself in?
2887 * This is used for testing.
2888 * @return A {width: w, height: h} object.
2891 Dygraph
.prototype.size
= function() {
2892 return { width
: this.width_
, height
: this.height_
};
2896 * Update the list of annotations and redraw the chart.
2898 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
2899 // Only add the annotation CSS rule once we know it will be used.
2900 Dygraph
.addAnnotationRule();
2901 this.annotations_
= ann
;
2902 this.layout_
.setAnnotations(this.annotations_
);
2903 if (!suppressDraw
) {
2909 * Return the list of annotations.
2911 Dygraph
.prototype.annotations
= function() {
2912 return this.annotations_
;
2916 * Get the index of a series (column) given its name. The first column is the
2917 * x-axis, so the data series start with index 1.
2919 Dygraph
.prototype.indexFromSetName
= function(name
) {
2920 var labels
= this.attr_("labels");
2921 for (var i
= 0; i
< labels
.length
; i
++) {
2922 if (labels
[i
] == name
) return i
;
2929 * Adds a default style for the annotation CSS classes to the document. This is
2930 * only executed when annotations are actually used. It is designed to only be
2931 * called once -- all calls after the first will return immediately.
2933 Dygraph
.addAnnotationRule
= function() {
2934 if (Dygraph
.addedAnnotationCSS
) return;
2936 var rule
= "border: 1px solid black; " +
2937 "background-color: white; " +
2938 "text-align: center;";
2940 var styleSheetElement
= document
.createElement("style");
2941 styleSheetElement
.type
= "text/css";
2942 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
2944 // Find the first style sheet that we can access.
2945 // We may not add a rule to a style sheet from another domain for security
2946 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
2947 // adds its own style sheets from google.com.
2948 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
2949 if (document
.styleSheets
[i
].disabled
) continue;
2950 var mysheet
= document
.styleSheets
[i
];
2952 if (mysheet
.insertRule
) { // Firefox
2953 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
2954 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
2955 } else if (mysheet
.addRule
) { // IE
2956 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
2958 Dygraph
.addedAnnotationCSS
= true;
2961 // Was likely a security exception.
2965 this.warn("Unable to add default annotation CSS rule; display may be off.");
2968 // Older pages may still use this name.
2969 DateGraph
= Dygraph
;