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/
46 /*jshint globalstrict: true */
47 /*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */
51 * Creates an interactive, zoomable chart.
54 * @param {div | String} div A div or the id of a div into which to construct
56 * @param {String | Function} file A file containing CSV data or a function
57 * that returns this data. The most basic expected format for each line is
58 * "YYYY/MM/DD,val1,val2,...". For more information, see
59 * http://dygraphs.com/data.html.
60 * @param {Object} attrs Various other attributes, e.g. errorBars determines
61 * whether the input data contains error ranges. For a complete list of
62 * options, see http://dygraphs.com/options.html.
64 var Dygraph
= function(div
, data
, opts
) {
65 if (arguments
.length
> 0) {
66 if (arguments
.length
== 4) {
67 // Old versions of dygraphs took in the series labels as a constructor
68 // parameter. This doesn't make sense anymore, but it's easy to continue
69 // to support this usage.
70 this.warn("Using deprecated four-argument dygraph constructor");
71 this.__old_init__(div
, data
, arguments
[2], arguments
[3]);
73 this.__init__(div
, data
, opts
);
78 Dygraph
.NAME
= "Dygraph";
79 Dygraph
.VERSION
= "1.2";
80 Dygraph
.__repr__
= function() {
81 return "[" + this.NAME
+ " " + this.VERSION
+ "]";
85 * Returns information about the Dygraph class.
87 Dygraph
.toString
= function() {
88 return this.__repr__();
91 // Various default values
92 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
93 Dygraph
.DEFAULT_WIDTH
= 480;
94 Dygraph
.DEFAULT_HEIGHT
= 320;
96 Dygraph
.ANIMATION_STEPS
= 10;
97 Dygraph
.ANIMATION_DURATION
= 200;
99 // These are defined before DEFAULT_ATTRS so that it can refer to them.
102 * Return a string version of a number. This respects the digitsAfterDecimal
103 * and maxNumberWidth options.
104 * @param {Number} x The number to be formatted
105 * @param {Dygraph} opts An options view
106 * @param {String} name The name of the point's data series
107 * @param {Dygraph} g The dygraph object
109 Dygraph
.numberValueFormatter
= function(x
, opts
, pt
, g
) {
110 var sigFigs
= opts('sigFigs');
112 if (sigFigs
!== null) {
113 // User has opted for a fixed number of significant figures.
114 return Dygraph
.floatFormat(x
, sigFigs
);
117 var digits
= opts('digitsAfterDecimal');
118 var maxNumberWidth
= opts('maxNumberWidth');
120 // switch to scientific notation if we underflow or overflow fixed display.
122 (Math
.abs(x
) >= Math
.pow(10, maxNumberWidth
) ||
123 Math
.abs(x
) < Math
.pow(10, -digits
))) {
124 return x
.toExponential(digits
);
126 return '' + Dygraph
.round_(x
, digits
);
131 * variant for use as an axisLabelFormatter.
134 Dygraph
.numberAxisLabelFormatter
= function(x
, granularity
, opts
, g
) {
135 return Dygraph
.numberValueFormatter(x
, opts
, g
);
139 * Convert a JS date (millis since epoch) to YYYY/MM/DD
140 * @param {Number} date The JavaScript date (ms since epoch)
141 * @return {String} A date of the form "YYYY/MM/DD"
144 Dygraph
.dateString_
= function(date
) {
145 var zeropad
= Dygraph
.zeropad
;
146 var d
= new Date(date
);
149 var year
= "" + d
.getFullYear();
150 // Get a 0 padded month string
151 var month
= zeropad(d
.getMonth() + 1); //months are 0-offset, sigh
152 // Get a 0 padded day string
153 var day
= zeropad(d
.getDate());
156 var frac
= d
.getHours() * 3600 + d
.getMinutes() * 60 + d
.getSeconds();
157 if (frac
) ret
= " " + Dygraph
.hmsString_(date
);
159 return year
+ "/" + month + "/" + day
+ ret
;
163 * Convert a JS date to a string appropriate to display on an axis that
164 * is displaying values at the stated granularity.
165 * @param {Date} date The date to format
166 * @param {Number} granularity One of the Dygraph granularity constants
167 * @return {String} The formatted date
170 Dygraph
.dateAxisFormatter
= function(date
, granularity
) {
171 if (granularity
>= Dygraph
.DECADAL
) {
172 return date
.strftime('%Y');
173 } else if (granularity
>= Dygraph
.MONTHLY
) {
174 return date
.strftime('%b %y');
176 var frac
= date
.getHours() * 3600 + date
.getMinutes() * 60 + date
.getSeconds() + date
.getMilliseconds();
177 if (frac
=== 0 || granularity
>= Dygraph
.DAILY
) {
178 return new Date(date
.getTime() + 3600*1000).strftime('%d%b');
180 return Dygraph
.hmsString_(date
.getTime());
186 // Default attribute values.
187 Dygraph
.DEFAULT_ATTRS
= {
188 highlightCircleSize
: 3,
189 highlightSeriesOpts
: null,
190 highlightSeriesBackgroundFade
: 0,
191 highlightSeriesAnimated
: false,
195 // TODO(danvk): move defaults from createStatusMessage_ here.
197 labelsSeparateLines
: false,
198 labelsShowZeroValues
: true,
201 showLabelsOnHighlight
: true,
203 digitsAfterDecimal
: 2,
208 strokeBorderWidth
: 0,
209 strokeBorderColor
: "white",
212 axisLabelFontSize
: 14,
218 xValueParser
: Dygraph
.dateParser
,
225 wilsonInterval
: true, // only relevant if fractions is true
229 connectSeparatedPoints
: false,
232 hideOverlayOnMouseOut
: true,
234 // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms.
235 legend
: 'onmouseover', // the only relevant value at the moment is 'always'.
240 // Sizes of the various chart labels.
247 axisLineColor
: "black",
250 axisLabelColor
: "black",
251 axisLabelFont
: "Arial", // TODO(danvk): is this implemented?
255 gridLineColor
: "rgb(128,128,128)",
257 interactionModel
: null, // will be set to Dygraph.Interaction.defaultModel
258 animatedZooms
: false, // (for now)
260 // Range selector options
261 showRangeSelector
: false,
262 rangeSelectorHeight
: 40,
263 rangeSelectorPlotStrokeColor
: "#808FAB",
264 rangeSelectorPlotFillColor
: "#A7B1C4",
270 axisLabelFormatter
: Dygraph
.dateAxisFormatter
,
271 valueFormatter
: Dygraph
.dateString_
,
272 ticker
: null // will be set in dygraph-tickers.js
276 valueFormatter
: Dygraph
.numberValueFormatter
,
277 axisLabelFormatter
: Dygraph
.numberAxisLabelFormatter
,
278 ticker
: null // will be set in dygraph-tickers.js
282 valueFormatter
: Dygraph
.numberValueFormatter
,
283 axisLabelFormatter
: Dygraph
.numberAxisLabelFormatter
,
284 ticker
: null // will be set in dygraph-tickers.js
289 // Directions for panning and zooming. Use bit operations when combined
290 // values are possible.
291 Dygraph
.HORIZONTAL
= 1;
292 Dygraph
.VERTICAL
= 2;
294 // Used for initializing annotation CSS rules only once.
295 Dygraph
.addedAnnotationCSS
= false;
297 Dygraph
.prototype.__old_init__
= function(div
, file
, labels
, attrs
) {
298 // Labels is no longer a constructor parameter, since it's typically set
299 // directly from the data source. It also conains a name for the x-axis,
300 // which the previous constructor form did not.
301 if (labels
!== null) {
302 var new_labels
= ["Date"];
303 for (var i
= 0; i
< labels
.length
; i
++) new_labels
.push(labels
[i
]);
304 Dygraph
.update(attrs
, { 'labels': new_labels
});
306 this.__init__(div
, file
, attrs
);
310 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
311 * and context <canvas> inside of it. See the constructor for details.
313 * @param {Element} div the Element to render the graph into.
314 * @param {String | Function} file Source data
315 * @param {Object} attrs Miscellaneous other options
318 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
319 // Hack for IE: if we're using excanvas and the document hasn't finished
320 // loading yet (and hence may not have initialized whatever it needs to
321 // initialize), then keep calling this routine periodically until it has.
322 if (/MSIE/.test(navigator
.userAgent
) && !window
.opera
&&
323 typeof(G_vmlCanvasManager
) != 'undefined' &&
324 document
.readyState
!= 'complete') {
326 setTimeout(function() { self
.__init__(div
, file
, attrs
); }, 100);
330 // Support two-argument constructor
331 if (attrs
=== null || attrs
=== undefined
) { attrs
= {}; }
333 attrs
= Dygraph
.mapLegacyOptions_(attrs
);
336 Dygraph
.error("Constructing dygraph with a non-existent div!");
340 this.isUsingExcanvas_
= typeof(G_vmlCanvasManager
) != 'undefined';
342 // Copy the important bits into the object
343 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
346 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
347 this.previousVerticalX_
= -1;
348 this.fractions_
= attrs
.fractions
|| false;
349 this.dateWindow_
= attrs
.dateWindow
|| null;
351 this.is_initial_draw_
= true;
352 this.annotations_
= [];
354 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
355 this.zoomed_x_
= false;
356 this.zoomed_y_
= false;
358 // Clear the div. This ensure that, if multiple dygraphs are passed the same
359 // div, then only one will be drawn.
362 // For historical reasons, the 'width' and 'height' options trump all CSS
363 // rules _except_ for an explicit 'width' or 'height' on the div.
364 // As an added convenience, if the div has zero height (like <div></div> does
365 // without any styles), then we use a default height/width
.
366 if (div
.style
.width
=== '' && attrs
.width
) {
367 div
.style
.width
= attrs
.width
+ "px";
369 if (div
.style
.height
=== '' && attrs
.height
) {
370 div
.style
.height
= attrs
.height
+ "px";
372 if (div
.style
.height
=== '' && div
.clientHeight
=== 0) {
373 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
374 if (div
.style
.width
=== '') {
375 div
.style
.width
= Dygraph
.DEFAULT_WIDTH
+ "px";
378 // these will be zero if the dygraph's div is hidden.
379 this.width_
= div
.clientWidth
;
380 this.height_
= div
.clientHeight
;
382 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
383 if (attrs
.stackedGraph
) {
384 attrs
.fillGraph
= true;
385 // TODO(nikhilk): Add any other stackedGraph checks here.
388 // Dygraphs has many options, some of which interact with one another.
389 // To keep track of everything, we maintain two sets of options:
391 // this.user_attrs_ only options explicitly set by the user.
392 // this.attrs_ defaults, options derived from user_attrs_, data.
394 // Options are then accessed this.attr_('attr'), which first looks at
395 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
396 // defaults without overriding behavior that the user specifically asks for.
397 this.user_attrs_
= {};
398 Dygraph
.update(this.user_attrs_
, attrs
);
400 // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
402 Dygraph
.updateDeep(this.attrs_
, Dygraph
.DEFAULT_ATTRS
);
404 this.boundaryIds_
= [];
405 this.setIndexByName_
= {};
406 this.datasetIndex_
= [];
408 // Create the containing DIV and other interactive elements
409 this.createInterface_();
415 * Returns the zoomed status of the chart for one or both axes.
417 * Axis is an optional parameter. Can be set to 'x' or 'y'.
419 * The zoomed status for an axis is set whenever a user zooms using the mouse
420 * or when the dateWindow or valueRange are updated (unless the isZoomedIgnoreProgrammaticZoom
421 * option is also specified).
423 Dygraph
.prototype.isZoomed
= function(axis
) {
424 if (axis
== null) return this.zoomed_x_
|| this.zoomed_y_
;
425 if (axis
=== 'x') return this.zoomed_x_
;
426 if (axis
=== 'y') return this.zoomed_y_
;
427 throw "axis parameter is [" + axis
+ "] must be null, 'x' or 'y'.";
431 * Returns information about the Dygraph object, including its containing ID.
433 Dygraph
.prototype.toString
= function() {
434 var maindiv
= this.maindiv_
;
435 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
;
436 return "[Dygraph " + id
+ "]";
441 * Returns the value of an option. This may be set by the user (either in the
442 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
444 * @param { String } name The name of the option, e.g. 'rollPeriod'.
445 * @param { String } [seriesName] The name of the series to which the option
446 * will be applied. If no per-series value of this option is available, then
447 * the global value is returned. This is optional.
448 * @return { ... } The value of the option.
450 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
451 // <REMOVE_FOR_COMBINED>
452 if (typeof(Dygraph
.OPTIONS_REFERENCE
) === 'undefined') {
453 this.error('Must include options reference JS for testing');
454 } else if (!Dygraph
.OPTIONS_REFERENCE
.hasOwnProperty(name
)) {
455 this.error('Dygraphs is using property ' + name
+ ', which has no entry ' +
456 'in the Dygraphs.OPTIONS_REFERENCE listing.');
457 // Only log this error once.
458 Dygraph
.OPTIONS_REFERENCE
[name
] = true;
460 // </REMOVE_FOR_COMBINED
>
463 sources
.push(this.attrs_
);
464 if (this.user_attrs_
) sources
.push(this.user_attrs_
);
465 if (this.user_attrs_
&& seriesName
) {
466 if (this.user_attrs_
.hasOwnProperty(seriesName
)) {
467 sources
.push(this.user_attrs_
[seriesName
]);
469 if (seriesName
=== this.highlightSet_
&&
470 this.user_attrs_
.hasOwnProperty('highlightSeriesOpts')) {
471 sources
.push(this.user_attrs_
['highlightSeriesOpts']);
476 for (var i
= sources
.length
- 1; i
>= 0; --i
) {
477 var source
= sources
[i
];
478 if (source
.hasOwnProperty(name
)) {
488 * @param String} axis The name of the axis (i.e. 'x', 'y' or 'y2')
489 * @return { ... } A function mapping string -> option value
491 Dygraph
.prototype.optionsViewForAxis_
= function(axis
) {
493 return function(opt
) {
494 var axis_opts
= self
.user_attrs_
.axes
;
495 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
][opt
]) {
496 return axis_opts
[axis
][opt
];
498 // user-specified attributes always trump defaults, even if they're less
500 if (typeof(self
.user_attrs_
[opt
]) != 'undefined') {
501 return self
.user_attrs_
[opt
];
504 axis_opts
= self
.attrs_
.axes
;
505 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
][opt
]) {
506 return axis_opts
[axis
][opt
];
508 // check old-style axis options
509 // TODO(danvk): add a deprecation warning if either of these match.
510 if (axis
== 'y' && self
.axes_
[0].hasOwnProperty(opt
)) {
511 return self
.axes_
[0][opt
];
512 } else if (axis
== 'y2' && self
.axes_
[1].hasOwnProperty(opt
)) {
513 return self
.axes_
[1][opt
];
515 return self
.attr_(opt
);
520 * Returns the current rolling period, as set by the user or an option.
521 * @return {Number} The number of points in the rolling window
523 Dygraph
.prototype.rollPeriod
= function() {
524 return this.rollPeriod_
;
528 * Returns the currently-visible x-range. This can be affected by zooming,
529 * panning or a call to updateOptions.
530 * Returns a two-element array: [left, right].
531 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
533 Dygraph
.prototype.xAxisRange
= function() {
534 return this.dateWindow_
? this.dateWindow_
: this.xAxisExtremes();
538 * Returns the lower- and upper-bound x-axis values of the
541 Dygraph
.prototype.xAxisExtremes
= function() {
542 var left
= this.rawData_
[0][0];
543 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
544 return [left
, right
];
548 * Returns the currently-visible y-range for an axis. This can be affected by
549 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
550 * called with no arguments, returns the range of the first axis.
551 * Returns a two-element array: [bottom, top].
553 Dygraph
.prototype.yAxisRange
= function(idx
) {
554 if (typeof(idx
) == "undefined") idx
= 0;
555 if (idx
< 0 || idx
>= this.axes_
.length
) {
558 var axis
= this.axes_
[idx
];
559 return [ axis
.computedValueRange
[0], axis
.computedValueRange
[1] ];
563 * Returns the currently-visible y-ranges for each axis. This can be affected by
564 * zooming, panning, calls to updateOptions, etc.
565 * Returns an array of [bottom, top] pairs, one for each y-axis.
567 Dygraph
.prototype.yAxisRanges
= function() {
569 for (var i
= 0; i
< this.axes_
.length
; i
++) {
570 ret
.push(this.yAxisRange(i
));
575 // TODO(danvk): use these functions throughout dygraphs.
577 * Convert from data coordinates to canvas/div X/Y coordinates.
578 * If specified, do this conversion for the coordinate system of a particular
579 * axis. Uses the first axis by default.
580 * Returns a two-element array: [X, Y]
582 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
583 * instead of toDomCoords(null, y, axis).
585 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
586 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
590 * Convert from data x coordinates to canvas/div X coordinate.
591 * If specified, do this conversion for the coordinate system of a particular
593 * Returns a single value or null if x is null.
595 Dygraph
.prototype.toDomXCoord
= function(x
) {
600 var area
= this.plotter_
.area
;
601 var xRange
= this.xAxisRange();
602 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
606 * Convert from data x coordinates to canvas/div Y coordinate and optional
607 * axis. Uses the first axis by default.
609 * returns a single value or null if y is null.
611 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
612 var pct
= this.toPercentYCoord(y
, axis
);
617 var area
= this.plotter_
.area
;
618 return area
.y
+ pct
* area
.h
;
622 * Convert from canvas/div coords to data coordinates.
623 * If specified, do this conversion for the coordinate system of a particular
624 * axis. Uses the first axis by default.
625 * Returns a two-element array: [X, Y].
627 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
628 * instead of toDataCoords(null, y, axis).
630 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
631 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
635 * Convert from canvas/div x coordinate to data coordinate.
637 * If x is null, this returns null.
639 Dygraph
.prototype.toDataXCoord
= function(x
) {
644 var area
= this.plotter_
.area
;
645 var xRange
= this.xAxisRange();
646 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
650 * Convert from canvas/div y coord to value.
652 * If y is null, this returns null.
653 * if axis is null, this uses the first axis.
655 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
660 var area
= this.plotter_
.area
;
661 var yRange
= this.yAxisRange(axis
);
663 if (typeof(axis
) == "undefined") axis
= 0;
664 if (!this.axes_
[axis
].logscale
) {
665 return yRange
[0] + (area
.y
+ area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
667 // Computing the inverse of toDomCoord.
668 var pct
= (y
- area
.y
) / area
.h
;
670 // Computing the inverse of toPercentYCoord. The function was arrived at with
671 // the following steps:
673 // Original calcuation:
674 // pct = (logr1 - Dygraph.log10(y)) / (logr1
- Dygraph
.log10(yRange
[0]));
676 // Move denominator to both sides:
677 // pct * (logr1 - Dygraph.log10(yRange[0])) = logr1 - Dygraph.log10(y);
679 // subtract logr1, and take the negative value.
680 // logr1 - (pct * (logr1 - Dygraph.log10(yRange[0]))) = Dygraph.log10(y);
682 // Swap both sides of the equation, and we can compute the log of the
683 // return value. Which means we just need to use that as the exponent in
685 // Dygraph.log10(y) = logr1 - (pct * (logr1 - Dygraph.log10(yRange[0])));
687 var logr1
= Dygraph
.log10(yRange
[1]);
688 var exponent
= logr1
- (pct
* (logr1
- Dygraph
.log10(yRange
[0])));
689 var value
= Math
.pow(Dygraph
.LOG_SCALE
, exponent
);
695 * Converts a y for an axis to a percentage from the top to the
696 * bottom of the drawing area.
698 * If the coordinate represents a value visible on the canvas, then
699 * the value will be between 0 and 1, where 0 is the top of the canvas.
700 * However, this method will return values outside the range, as
701 * values can fall outside the canvas.
703 * If y is null, this returns null.
704 * if axis is null, this uses the first axis.
706 * @param { Number } y The data y-coordinate.
707 * @param { Number } [axis] The axis number on which the data coordinate lives.
708 * @return { Number } A fraction in [0, 1] where 0 = the top edge.
710 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
714 if (typeof(axis
) == "undefined") axis
= 0;
716 var yRange
= this.yAxisRange(axis
);
719 if (!this.axes_
[axis
].logscale
) {
720 // yRange[1] - y is unit distance from the bottom.
721 // yRange[1] - yRange[0] is the scale of the range.
722 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
723 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
725 var logr1
= Dygraph
.log10(yRange
[1]);
726 pct
= (logr1
- Dygraph
.log10(y
)) / (logr1
- Dygraph
.log10(yRange
[0]));
732 * Converts an x value to a percentage from the left to the right of
735 * If the coordinate represents a value visible on the canvas, then
736 * the value will be between 0 and 1, where 0 is the left of the canvas.
737 * However, this method will return values outside the range, as
738 * values can fall outside the canvas.
740 * If x is null, this returns null.
741 * @param { Number } x The data x-coordinate.
742 * @return { Number } A fraction in [0, 1] where 0 = the left edge.
744 Dygraph
.prototype.toPercentXCoord
= function(x
) {
749 var xRange
= this.xAxisRange();
750 return (x
- xRange
[0]) / (xRange
[1] - xRange
[0]);
754 * Returns the number of columns (including the independent variable).
755 * @return { Integer } The number of columns.
757 Dygraph
.prototype.numColumns
= function() {
758 return this.rawData_
[0] ? this.rawData_
[0].length
: this.attr_("labels").length
;
762 * Returns the number of rows (excluding any header/label row).
763 * @return { Integer } The number of rows, less any header.
765 Dygraph
.prototype.numRows
= function() {
766 return this.rawData_
.length
;
770 * Returns the full range of the x-axis, as determined by the most extreme
771 * values in the data set. Not affected by zooming, visibility, etc.
772 * TODO(danvk): merge w/ xAxisExtremes
773 * @return { Array<Number> } A [low, high] pair
776 Dygraph
.prototype.fullXRange_
= function() {
777 if (this.numRows() > 0) {
778 return [this.rawData_
[0][0], this.rawData_
[this.numRows() - 1][0]];
785 * Returns the value in the given row and column. If the row and column exceed
786 * the bounds on the data, returns null. Also returns null if the value is
788 * @param { Number} row The row number of the data (0-based). Row 0 is the
789 * first row of data, not a header row.
790 * @param { Number} col The column number of the data (0-based)
791 * @return { Number } The value in the specified cell or null if the row/col
794 Dygraph
.prototype.getValue
= function(row
, col
) {
795 if (row
< 0 || row
> this.rawData_
.length
) return null;
796 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
798 return this.rawData_
[row
][col
];
802 * Generates interface elements for the Dygraph: a containing div, a div to
803 * display the current point, and a textbox to adjust the rolling average
804 * period. Also creates the Renderer/Layout elements.
807 Dygraph
.prototype.createInterface_
= function() {
808 // Create the all-enclosing graph div
809 var enclosing
= this.maindiv_
;
811 this.graphDiv
= document
.createElement("div");
812 this.graphDiv
.style
.width
= this.width_
+ "px";
813 this.graphDiv
.style
.height
= this.height_
+ "px";
814 enclosing
.appendChild(this.graphDiv
);
816 // Create the canvas for interactive parts of the chart.
817 this.canvas_
= Dygraph
.createCanvas();
818 this.canvas_
.style
.position
= "absolute";
819 this.canvas_
.width
= this.width_
;
820 this.canvas_
.height
= this.height_
;
821 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
822 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
824 this.canvas_ctx_
= Dygraph
.getContext(this.canvas_
);
826 // ... and for static parts of the chart.
827 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
828 this.hidden_ctx_
= Dygraph
.getContext(this.hidden_
);
830 if (this.attr_('showRangeSelector')) {
831 // The range selector must be created here so that its canvases and contexts get created here.
832 // For some reason, if the canvases and contexts don't get created here, things don't work in IE.
833 // The range selector also sets xAxisHeight in order to reserve space.
834 this.rangeSelector_
= new DygraphRangeSelector(this);
837 // The interactive parts of the graph are drawn on top of the chart.
838 this.graphDiv
.appendChild(this.hidden_
);
839 this.graphDiv
.appendChild(this.canvas_
);
840 this.mouseEventElement_
= this.createMouseEventElement_();
842 // Create the grapher
843 this.layout_
= new DygraphLayout(this);
845 if (this.rangeSelector_
) {
846 // This needs to happen after the graph canvases are added to the div and the layout object is created.
847 this.rangeSelector_
.addToGraph(this.graphDiv
, this.layout_
);
852 this.mouseMoveHandler
= function(e
) {
853 dygraph
.mouseMove_(e
);
855 Dygraph
.addEvent(this.mouseEventElement_
, 'mousemove', this.mouseMoveHandler
);
857 this.mouseOutHandler
= function(e
) {
858 dygraph
.mouseOut_(e
);
860 Dygraph
.addEvent(this.mouseEventElement_
, 'mouseout', this.mouseOutHandler
);
862 this.createStatusMessage_();
863 this.createDragInterface_();
865 this.resizeHandler
= function(e
) {
869 // Update when the window is resized.
870 // TODO(danvk): drop frames depending on complexity of the chart.
871 Dygraph
.addEvent(window
, 'resize', this.resizeHandler
);
875 * Detach DOM elements in the dygraph and null out all data references.
876 * Calling this when you're done with a dygraph can dramatically reduce memory
877 * usage. See, e.g., the tests/perf.html example.
879 Dygraph
.prototype.destroy
= function() {
880 var removeRecursive
= function(node
) {
881 while (node
.hasChildNodes()) {
882 removeRecursive(node
.firstChild
);
883 node
.removeChild(node
.firstChild
);
887 // remove mouse event handlers
888 Dygraph
.removeEvent(this.mouseEventElement_
, 'mouseout', this.mouseOutHandler
);
889 Dygraph
.removeEvent(this.mouseEventElement_
, 'mousemove', this.mouseMoveHandler
);
890 removeRecursive(this.maindiv_
);
892 var nullOut
= function(obj
) {
894 if (typeof(obj
[n
]) === 'object') {
899 // remove event handlers
900 Dygraph
.removeEvent(window
,'resize',this.resizeHandler
);
901 this.resizeHandler
= null;
902 // These may not all be necessary, but it can't hurt...
903 nullOut(this.layout_
);
904 nullOut(this.plotter_
);
909 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
910 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
911 * or the zoom rectangles) is done on this.canvas_.
912 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
913 * @return {Object} The newly-created canvas
916 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
917 var h
= Dygraph
.createCanvas();
918 h
.style
.position
= "absolute";
919 // TODO(danvk): h should be offset from canvas. canvas needs to include
920 // some extra area to make it easier to zoom in on the far left and far
921 // right. h needs to be precisely the plot area, so that clipping occurs.
922 h
.style
.top
= canvas
.style
.top
;
923 h
.style
.left
= canvas
.style
.left
;
924 h
.width
= this.width_
;
925 h
.height
= this.height_
;
926 h
.style
.width
= this.width_
+ "px"; // for IE
927 h
.style
.height
= this.height_
+ "px"; // for IE
932 * Creates an overlay element used to handle mouse events.
933 * @return {Object} The mouse event element.
936 Dygraph
.prototype.createMouseEventElement_
= function() {
937 if (this.isUsingExcanvas_
) {
938 var elem
= document
.createElement("div");
939 elem
.style
.position
= 'absolute';
940 elem
.style
.backgroundColor
= 'white';
941 elem
.style
.filter
= 'alpha(opacity=0)';
942 elem
.style
.width
= this.width_
+ "px";
943 elem
.style
.height
= this.height_
+ "px";
944 this.graphDiv
.appendChild(elem
);
952 * Generate a set of distinct colors for the data series. This is done with a
953 * color wheel. Saturation/Value are customizable, and the hue is
954 * equally-spaced around the color wheel. If a custom set of colors is
955 * specified, that is used instead.
958 Dygraph
.prototype.setColors_
= function() {
959 var num
= this.attr_("labels").length
- 1;
961 var colors
= this.attr_('colors');
964 var sat
= this.attr_('colorSaturation') || 1.0;
965 var val
= this.attr_('colorValue') || 0.5;
966 var half
= Math
.ceil(num
/ 2);
967 for (i
= 1; i
<= num
; i
++) {
968 if (!this.visibility()[i
-1]) continue;
969 // alternate colors for high contrast.
970 var idx
= i
% 2 ? Math
.ceil(i
/ 2) : (half + i / 2);
971 var hue
= (1.0 * idx
/ (1 + num
));
972 this.colors_
.push(Dygraph
.hsvToRGB(hue
, sat
, val
));
975 for (i
= 0; i
< num
; i
++) {
976 if (!this.visibility()[i
]) continue;
977 var colorStr
= colors
[i
% colors
.length
];
978 this.colors_
.push(colorStr
);
982 this.plotter_
.setColors(this.colors_
);
986 * Return the list of colors. This is either the list of colors passed in the
987 * attributes or the autogenerated list of rgb(r,g,b) strings.
988 * @return {Array<string>} The list of colors.
990 Dygraph
.prototype.getColors
= function() {
995 * Create the div that contains information on the selected point(s)
996 * This goes in the top right of the canvas, unless an external div has already
1000 Dygraph
.prototype.createStatusMessage_
= function() {
1001 var userLabelsDiv
= this.user_attrs_
.labelsDiv
;
1002 if (userLabelsDiv
&& null !== userLabelsDiv
&&
1003 (typeof(userLabelsDiv
) == "string" || userLabelsDiv
instanceof String
)) {
1004 this.user_attrs_
.labelsDiv
= document
.getElementById(userLabelsDiv
);
1006 if (!this.attr_("labelsDiv")) {
1007 var divWidth
= this.attr_('labelsDivWidth');
1008 var messagestyle
= {
1009 "position": "absolute",
1012 "width": divWidth
+ "px",
1014 "left": (this.width_
- divWidth
- 2) + "px",
1015 "background": "white",
1016 "textAlign": "left",
1017 "overflow": "hidden"};
1018 Dygraph
.update(messagestyle
, this.attr_('labelsDivStyles'));
1019 var div
= document
.createElement("div");
1020 div
.className
= "dygraph-legend";
1021 for (var name
in messagestyle
) {
1022 if (messagestyle
.hasOwnProperty(name
)) {
1023 div
.style
[name
] = messagestyle
[name
];
1026 this.graphDiv
.appendChild(div
);
1027 this.attrs_
.labelsDiv
= div
;
1032 * Position the labels div so that:
1033 * - its right edge is flush with the right edge of the charting area
1034 * - its top edge is flush with the top edge of the charting area
1037 Dygraph
.prototype.positionLabelsDiv_
= function() {
1038 // Don't touch a user-specified labelsDiv.
1039 if (this.user_attrs_
.hasOwnProperty("labelsDiv")) return;
1041 var area
= this.plotter_
.area
;
1042 var div
= this.attr_("labelsDiv");
1043 div
.style
.left
= area
.x
+ area
.w
- this.attr_("labelsDivWidth") - 1 + "px";
1044 div
.style
.top
= area
.y
+ "px";
1048 * Create the text box to adjust the averaging period
1051 Dygraph
.prototype.createRollInterface_
= function() {
1052 // Create a roller if one doesn't exist already.
1053 if (!this.roller_
) {
1054 this.roller_
= document
.createElement("input");
1055 this.roller_
.type
= "text";
1056 this.roller_
.style
.display
= "none";
1057 this.graphDiv
.appendChild(this.roller_
);
1060 var display
= this.attr_('showRoller') ? 'block' : 'none';
1062 var area
= this.plotter_
.area
;
1063 var textAttr
= { "position": "absolute",
1065 "top": (area
.y
+ area
.h
- 25) + "px",
1066 "left": (area
.x
+ 1) + "px",
1069 this.roller_
.size
= "2";
1070 this.roller_
.value
= this.rollPeriod_
;
1071 for (var name
in textAttr
) {
1072 if (textAttr
.hasOwnProperty(name
)) {
1073 this.roller_
.style
[name
] = textAttr
[name
];
1078 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
1083 * Converts page the x-coordinate of the event to pixel x-coordinates on the
1084 * canvas (i.e. DOM Coords).
1086 Dygraph
.prototype.dragGetX_
= function(e
, context
) {
1087 return Dygraph
.pageX(e
) - context
.px
;
1092 * Converts page the y-coordinate of the event to pixel y-coordinates on the
1093 * canvas (i.e. DOM Coords).
1095 Dygraph
.prototype.dragGetY_
= function(e
, context
) {
1096 return Dygraph
.pageY(e
) - context
.py
;
1100 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1104 Dygraph
.prototype.createDragInterface_
= function() {
1106 // Tracks whether the mouse is down right now
1108 isPanning
: false, // is this drag part of a pan?
1109 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1110 dragStartX
: null, // pixel coordinates
1111 dragStartY
: null, // pixel coordinates
1112 dragEndX
: null, // pixel coordinates
1113 dragEndY
: null, // pixel coordinates
1114 dragDirection
: null,
1115 prevEndX
: null, // pixel coordinates
1116 prevEndY
: null, // pixel coordinates
1117 prevDragDirection
: null,
1119 // The value on the left side of the graph when a pan operation starts.
1120 initialLeftmostDate
: null,
1122 // The number of units each pixel spans. (This won't be valid for log
1124 xUnitsPerPixel
: null,
1126 // TODO(danvk): update this comment
1127 // The range in second/value units that the viewport encompasses during a
1128 // panning operation.
1131 // Top-left corner of the canvas, in DOM coords
1132 // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1136 // Values for use with panEdgeFraction, which limit how far outside the
1137 // graph's data boundaries it can be panned.
1138 boundedDates
: null, // [minDate, maxDate]
1139 boundedValues
: null, // [[minValue, maxValue] ...]
1141 initializeMouseDown
: function(event
, g
, context
) {
1142 // prevents mouse drags from selecting page text.
1143 if (event
.preventDefault
) {
1144 event
.preventDefault(); // Firefox, Chrome, etc.
1146 event
.returnValue
= false; // IE
1147 event
.cancelBubble
= true;
1150 context
.px
= Dygraph
.findPosX(g
.canvas_
);
1151 context
.py
= Dygraph
.findPosY(g
.canvas_
);
1152 context
.dragStartX
= g
.dragGetX_(event
, context
);
1153 context
.dragStartY
= g
.dragGetY_(event
, context
);
1157 var interactionModel
= this.attr_("interactionModel");
1159 // Self is the graph.
1162 // Function that binds the graph and context to the handler.
1163 var bindHandler
= function(handler
) {
1164 return function(event
) {
1165 handler(event
, self
, context
);
1169 for (var eventName
in interactionModel
) {
1170 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1171 Dygraph
.addEvent(this.mouseEventElement_
, eventName
,
1172 bindHandler(interactionModel
[eventName
]));
1175 // If the user releases the mouse button during a drag, but not over the
1176 // canvas, then it doesn't count as a zooming action.
1177 Dygraph
.addEvent(document
, 'mouseup', function(event
) {
1178 if (context
.isZooming
|| context
.isPanning
) {
1179 context
.isZooming
= false;
1180 context
.dragStartX
= null;
1181 context
.dragStartY
= null;
1184 if (context
.isPanning
) {
1185 context
.isPanning
= false;
1186 context
.draggingDate
= null;
1187 context
.dateRange
= null;
1188 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1189 delete self
.axes_
[i
].draggingValue
;
1190 delete self
.axes_
[i
].dragValueRange
;
1197 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1198 * up any previous zoom rectangles that were drawn. This could be optimized to
1199 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1202 * @param {Number} direction the direction of the zoom rectangle. Acceptable
1203 * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL.
1204 * @param {Number} startX The X position where the drag started, in canvas
1206 * @param {Number} endX The current X position of the drag, in canvas coords.
1207 * @param {Number} startY The Y position where the drag started, in canvas
1209 * @param {Number} endY The current Y position of the drag, in canvas coords.
1210 * @param {Number} prevDirection the value of direction on the previous call to
1211 * this function. Used to avoid excess redrawing
1212 * @param {Number} prevEndX The value of endX on the previous call to this
1213 * function. Used to avoid excess redrawing
1214 * @param {Number} prevEndY The value of endY on the previous call to this
1215 * function. Used to avoid excess redrawing
1218 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
,
1219 endY
, prevDirection
, prevEndX
,
1221 var ctx
= this.canvas_ctx_
;
1223 // Clean up from the previous rect if necessary
1224 if (prevDirection
== Dygraph
.HORIZONTAL
) {
1225 ctx
.clearRect(Math
.min(startX
, prevEndX
), this.layout_
.getPlotArea().y
,
1226 Math
.abs(startX
- prevEndX
), this.layout_
.getPlotArea().h
);
1227 } else if (prevDirection
== Dygraph
.VERTICAL
){
1228 ctx
.clearRect(this.layout_
.getPlotArea().x
, Math
.min(startY
, prevEndY
),
1229 this.layout_
.getPlotArea().w
, Math
.abs(startY
- prevEndY
));
1232 // Draw a light-grey rectangle to show the new viewing area
1233 if (direction
== Dygraph
.HORIZONTAL
) {
1234 if (endX
&& startX
) {
1235 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1236 ctx
.fillRect(Math
.min(startX
, endX
), this.layout_
.getPlotArea().y
,
1237 Math
.abs(endX
- startX
), this.layout_
.getPlotArea().h
);
1239 } else if (direction
== Dygraph
.VERTICAL
) {
1240 if (endY
&& startY
) {
1241 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1242 ctx
.fillRect(this.layout_
.getPlotArea().x
, Math
.min(startY
, endY
),
1243 this.layout_
.getPlotArea().w
, Math
.abs(endY
- startY
));
1247 if (this.isUsingExcanvas_
) {
1248 this.currentZoomRectArgs_
= [direction
, startX
, endX
, startY
, endY
, 0, 0, 0];
1253 * Clear the zoom rectangle (and perform no zoom).
1256 Dygraph
.prototype.clearZoomRect_
= function() {
1257 this.currentZoomRectArgs_
= null;
1258 this.canvas_ctx_
.clearRect(0, 0, this.canvas_
.width
, this.canvas_
.height
);
1262 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1263 * the canvas. The exact zoom window may be slightly larger if there are no data
1264 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1265 * which accepts dates that match the raw data. This function redraws the graph.
1267 * @param {Number} lowX The leftmost pixel value that should be visible.
1268 * @param {Number} highX The rightmost pixel value that should be visible.
1271 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1272 this.currentZoomRectArgs_
= null;
1273 // Find the earliest and latest dates contained in this canvasx range.
1274 // Convert the call to date ranges of the raw data.
1275 var minDate
= this.toDataXCoord(lowX
);
1276 var maxDate
= this.toDataXCoord(highX
);
1277 this.doZoomXDates_(minDate
, maxDate
);
1281 * Transition function to use in animations. Returns values between 0.0
1282 * (totally old values) and 1.0 (totally new values) for each frame.
1285 Dygraph
.zoomAnimationFunction
= function(frame
, numFrames
) {
1287 return (1.0 - Math
.pow(k
, -frame
)) / (1.0 - Math
.pow(k
, -numFrames
));
1291 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1292 * method with doZoomX which accepts pixel coordinates. This function redraws
1295 * @param {Number} minDate The minimum date that should be visible.
1296 * @param {Number} maxDate The maximum date that should be visible.
1299 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1300 // TODO(danvk): when yAxisRange is null (i.e. "fit to data", the animation
1301 // can produce strange effects. Rather than the y-axis transitioning slowly
1302 // between values, it can jerk around.)
1303 var old_window
= this.xAxisRange();
1304 var new_window
= [minDate
, maxDate
];
1305 this.zoomed_x_
= true;
1307 this.doAnimatedZoom(old_window
, new_window
, null, null, function() {
1308 if (that
.attr_("zoomCallback")) {
1309 that
.attr_("zoomCallback")(minDate
, maxDate
, that
.yAxisRanges());
1315 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1316 * the canvas. This function redraws the graph.
1318 * @param {Number} lowY The topmost pixel value that should be visible.
1319 * @param {Number} highY The lowest pixel value that should be visible.
1322 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1323 this.currentZoomRectArgs_
= null;
1324 // Find the highest and lowest values in pixel range for each axis.
1325 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1326 // This is because pixels increase as you go down on the screen, whereas data
1327 // coordinates increase as you go up the screen.
1328 var oldValueRanges
= this.yAxisRanges();
1329 var newValueRanges
= [];
1330 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1331 var hi
= this.toDataYCoord(lowY
, i
);
1332 var low
= this.toDataYCoord(highY
, i
);
1333 newValueRanges
.push([low
, hi
]);
1336 this.zoomed_y_
= true;
1338 this.doAnimatedZoom(null, null, oldValueRanges
, newValueRanges
, function() {
1339 if (that
.attr_("zoomCallback")) {
1340 var xRange
= that
.xAxisRange();
1341 that
.attr_("zoomCallback")(xRange
[0], xRange
[1], that
.yAxisRanges());
1347 * Reset the zoom to the original view coordinates. This is the same as
1348 * double-clicking on the graph.
1352 Dygraph
.prototype.doUnzoom_
= function() {
1353 var dirty
= false, dirtyX
= false, dirtyY
= false;
1354 if (this.dateWindow_
!== null) {
1359 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1360 if (typeof(this.axes_
[i
].valueWindow
) !== 'undefined' && this.axes_
[i
].valueWindow
!== null) {
1366 // Clear any selection, since it's likely to be drawn in the wrong place.
1367 this.clearSelection();
1370 this.zoomed_x_
= false;
1371 this.zoomed_y_
= false;
1373 var minDate
= this.rawData_
[0][0];
1374 var maxDate
= this.rawData_
[this.rawData_
.length
- 1][0];
1376 // With only one frame, don't bother calculating extreme ranges.
1377 // TODO(danvk): merge this block w/ the code below
.
1378 if (!this.attr_("animatedZooms")) {
1379 this.dateWindow_
= null;
1380 for (i
= 0; i
< this.axes_
.length
; i
++) {
1381 if (this.axes_
[i
].valueWindow
!== null) {
1382 delete this.axes_
[i
].valueWindow
;
1386 if (this.attr_("zoomCallback")) {
1387 this.attr_("zoomCallback")(minDate
, maxDate
, this.yAxisRanges());
1392 var oldWindow
=null, newWindow
=null, oldValueRanges
=null, newValueRanges
=null;
1394 oldWindow
= this.xAxisRange();
1395 newWindow
= [minDate
, maxDate
];
1399 oldValueRanges
= this.yAxisRanges();
1400 // TODO(danvk): this is pretty inefficient
1401 var packed
= this.gatherDatasets_(this.rolledSeries_
, null);
1402 var extremes
= packed
[1];
1404 // this has the side-effect of modifying this.axes_.
1405 // this doesn't make much sense in this context, but it's convenient (we
1406 // need this.axes_[*].extremeValues) and not harmful since we'll be
1407 // calling drawGraph_ shortly, which clobbers these values.
1408 this.computeYAxisRanges_(extremes
);
1410 newValueRanges
= [];
1411 for (i
= 0; i
< this.axes_
.length
; i
++) {
1412 var axis
= this.axes_
[i
];
1413 newValueRanges
.push(axis
.valueRange
!= null ? axis
.valueRange
: axis
.extremeRange
);
1418 this.doAnimatedZoom(oldWindow
, newWindow
, oldValueRanges
, newValueRanges
,
1420 that
.dateWindow_
= null;
1421 for (var i
= 0; i
< that
.axes_
.length
; i
++) {
1422 if (that
.axes_
[i
].valueWindow
!== null) {
1423 delete that
.axes_
[i
].valueWindow
;
1426 if (that
.attr_("zoomCallback")) {
1427 that
.attr_("zoomCallback")(minDate
, maxDate
, that
.yAxisRanges());
1434 * Combined animation logic for all zoom functions.
1435 * either the x parameters or y parameters may be null.
1438 Dygraph
.prototype.doAnimatedZoom
= function(oldXRange
, newXRange
, oldYRanges
, newYRanges
, callback
) {
1439 var steps
= this.attr_("animatedZooms") ? Dygraph
.ANIMATION_STEPS
: 1;
1442 var valueRanges
= [];
1445 if (oldXRange
!== null && newXRange
!== null) {
1446 for (step
= 1; step
<= steps
; step
++) {
1447 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1448 windows
[step
-1] = [oldXRange
[0]*(1-frac
) + frac
*newXRange
[0],
1449 oldXRange
[1]*(1-frac
) + frac
*newXRange
[1]];
1453 if (oldYRanges
!== null && newYRanges
!== null) {
1454 for (step
= 1; step
<= steps
; step
++) {
1455 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1457 for (var j
= 0; j
< this.axes_
.length
; j
++) {
1458 thisRange
.push([oldYRanges
[j
][0]*(1-frac
) + frac
*newYRanges
[j
][0],
1459 oldYRanges
[j
][1]*(1-frac
) + frac
*newYRanges
[j
][1]]);
1461 valueRanges
[step
-1] = thisRange
;
1466 Dygraph
.repeatAndCleanup(function(step
) {
1467 if (valueRanges
.length
) {
1468 for (var i
= 0; i
< that
.axes_
.length
; i
++) {
1469 var w
= valueRanges
[step
][i
];
1470 that
.axes_
[i
].valueWindow
= [w
[0], w
[1]];
1473 if (windows
.length
) {
1474 that
.dateWindow_
= windows
[step
];
1477 }, steps
, Dygraph
.ANIMATION_DURATION
/ steps
, callback
);
1481 * Get the current graph's area object.
1483 * Returns: {x, y, w, h}
1485 Dygraph
.prototype.getArea
= function() {
1486 return this.plotter_
.area
;
1490 * Convert a mouse event to DOM coordinates relative to the graph origin.
1492 * Returns a two-element array: [X, Y].
1494 Dygraph
.prototype.eventToDomCoords
= function(event
) {
1495 var canvasx
= Dygraph
.pageX(event
) - Dygraph
.findPosX(this.mouseEventElement_
);
1496 var canvasy
= Dygraph
.pageY(event
) - Dygraph
.findPosY(this.mouseEventElement_
);
1497 return [canvasx
, canvasy
];
1501 * Given a canvas X coordinate, find the closest row.
1502 * @param {Number} domX graph-relative DOM X coordinate
1503 * Returns: row number, integer
1506 Dygraph
.prototype.findClosestRow
= function(domX
) {
1507 var minDistX
= null;
1509 var points
= this.layout_
.points
;
1510 var l
= points
.length
;
1511 for (var i
= 0; i
< l
; i
++) {
1512 var point
= points
[i
];
1513 if (point
=== null) continue;
1514 var dist
= Math
.abs(point
.canvasx
- domX
);
1515 if (minDistX
!== null && dist
>= minDistX
) continue;
1519 return this.idxToRow_(idx
);
1523 * Given canvas X,Y coordinates, find the closest point
1524 * @param {Number} domX graph-relative DOM X coordinate
1525 * @param {Number} domY graph-relative DOM Y coordinate
1526 * Returns: {row, seriesName, point}
1529 Dygraph
.prototype.findClosestPoint
= function(domX
, domY
) {
1532 var points
= this.layout_
.points
;
1533 var dist
, dx
, dy
, point
, closestPoint
, closestSeries
;
1534 for (var setIdx
= 0; setIdx
< this.layout_
.datasets
.length
; ++setIdx
) {
1535 var first
= this.layout_
.setPointsOffsets
[setIdx
];
1536 var len
= this.layout_
.setPointsLengths
[setIdx
];
1537 for (var i
= 0; i
< len
; ++i
) {
1538 var point
= points
[first
+ i
];
1539 if (point
=== null) continue;
1540 dx
= point
.canvasx
- domX
;
1541 dy
= point
.canvasy
- domY
;
1542 dist
= dx
* dx
+ dy
* dy
;
1543 if (minDist
!== null && dist
>= minDist
) continue;
1545 closestPoint
= point
;
1546 closestSeries
= setIdx
;
1550 var name
= this.layout_
.setNames
[closestSeries
];
1559 * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1560 * @param {Number} domX graph-relative DOM X coordinate
1561 * @param {Number} domY graph-relative DOM Y coordinate
1562 * Returns: {row, seriesName, point}
1565 Dygraph
.prototype.findStackedPoint
= function(domX
, domY
) {
1566 var row
= this.findClosestRow(domX
);
1567 var points
= this.layout_
.points
;
1568 var closestPoint
, closestSeries
;
1569 for (var setIdx
= 0; setIdx
< this.layout_
.datasets
.length
; ++setIdx
) {
1570 var first
= this.layout_
.setPointsOffsets
[setIdx
];
1571 var len
= this.layout_
.setPointsLengths
[setIdx
];
1572 if (row
>= len
) continue;
1573 var p1
= points
[first
+ row
];
1574 var py
= p1
.canvasy
;
1575 if (domX
> p1
.canvasx
&& row
+ 1 < len
) {
1576 // interpolate series Y value using next point
1577 var p2
= points
[first
+ row
+ 1];
1578 var dx
= p2
.canvasx
- p1
.canvasx
;
1580 var r
= (domX
- p1
.canvasx
) / dx
;
1581 py
+= r
* (p2
.canvasy
- p1
.canvasy
);
1583 } else if (domX
< p1
.canvasx
&& row
> 0) {
1584 // interpolate series Y value using previous point
1585 var p0
= points
[first
+ row
- 1];
1586 var dx
= p1
.canvasx
- p0
.canvasx
;
1588 var r
= (p1
.canvasx
- domX
) / dx
;
1589 py
+= r
* (p0
.canvasy
- p1
.canvasy
);
1592 // Stop if the point (domX, py) is above this series' upper edge
1593 if (setIdx
> 0 && py
>= domY
) break;
1595 closestSeries
= setIdx
;
1597 var name
= this.layout_
.setNames
[closestSeries
];
1606 * When the mouse moves in the canvas, display information about a nearby data
1607 * point and draw dots over those points in the data series. This function
1608 * takes care of cleanup of previously-drawn dots.
1609 * @param {Object} event The mousemove event from the browser.
1612 Dygraph
.prototype.mouseMove_
= function(event
) {
1613 // This prevents JS errors when mousing over the canvas before data loads.
1614 var points
= this.layout_
.points
;
1615 if (points
=== undefined
) return;
1617 var canvasCoords
= this.eventToDomCoords(event
);
1618 var canvasx
= canvasCoords
[0];
1619 var canvasy
= canvasCoords
[1];
1621 var mouseoverCallback
= this.attr_("mouseoverCallback");
1622 if (mouseoverCallback
) {
1623 var highlightRow
= this.idxToRow_(idx
);
1624 var ret
= mouseoverCallback(this, event
);
1628 var highlightSeriesOpts
= this.attr_("highlightSeriesOpts");
1629 var selectionChanged
= false;
1630 if (highlightSeriesOpts
) {
1632 if (this.attr_("stackedGraph")) {
1633 closest
= this.findStackedPoint(canvasx
, canvasy
);
1635 closest
= this.findClosestPoint(canvasx
, canvasy
);
1637 selectionChanged
= this.setSelection(closest
.row
, closest
.seriesName
);
1639 var idx
= this.findClosestRow(canvasx
);
1640 selectionChanged
= this.setSelection(idx
);
1643 var callback
= this.attr_("highlightCallback");
1644 if (callback
&& selectionChanged
) {
1645 callback(event
, this.lastx_
, this.selPoints_
, this.lastRow_
, this.highlightSet_
);
1650 * Transforms layout_.points index into data row number.
1651 * @param int layout_.points index
1652 * @return int row number, or -1 if none could be found.
1655 Dygraph
.prototype.idxToRow_
= function(idx
) {
1656 if (idx
< 0) return -1;
1658 // make sure that you get the boundaryIds record which is also defined (see bug #236)
1659 var boundaryIdx
= -1;
1660 for (var i
= 0; i
< this.boundaryIds_
.length
; i
++) {
1661 if (this.boundaryIds_
[i
] !== undefined
) {
1666 if (boundaryIdx
< 0) return -1;
1667 for (var setIdx
= 0; setIdx
< this.layout_
.datasets
.length
; ++setIdx
) {
1668 var set
= this.layout_
.datasets
[setIdx
];
1669 if (idx
< set
.length
) {
1670 return this.boundaryIds_
[boundaryIdx
][0] + idx
;
1679 * Generates legend html dash for any stroke pattern. It will try to scale the
1680 * pattern to fit in 1em width. Or if small enough repeat the partern for 1em
1682 * @param strokePattern The pattern
1683 * @param color The color of the series.
1684 * @param oneEmWidth The width in pixels of 1em in the legend.
1686 Dygraph
.prototype.generateLegendDashHTML_
= function(strokePattern
, color
, oneEmWidth
) {
1688 var i
, j
, paddingLeft
, marginRight
;
1689 var strokePixelLength
= 0, segmentLoop
= 0;
1690 var normalizedPattern
= [];
1692 // IE 7,8 fail at these divs, so they get boring legend, have not tested 9.
1693 var isIE
= (/MSIE/.test(navigator
.userAgent
) && !window
.opera
);
1697 if (!strokePattern
|| strokePattern
.length
<= 1) {
1699 dash
= "<div style=\"display: inline-block; position: relative; " +
1700 "bottom: .5ex; padding-left: 1em; height: 1px; " +
1701 "border-bottom: 2px solid " + color
+ ";\"></div>";
1703 // Compute the length of the pixels including the first segment twice,
1704 // since we repeat it.
1705 for (i
= 0; i
<= strokePattern
.length
; i
++) {
1706 strokePixelLength
+= strokePattern
[i
%strokePattern
.length
];
1709 // See if we can loop the pattern by itself at least twice.
1710 loop
= Math
.floor(oneEmWidth
/(strokePixelLength
-strokePattern
[0]));
1712 // This pattern fits at least two times, no scaling just convert to em;
1713 for (i
= 0; i
< strokePattern
.length
; i
++) {
1714 normalizedPattern
[i
] = strokePattern
[i
]/oneEmWidth
;
1716 // Since we are repeating the pattern, we don't worry about repeating the
1717 // first segment in one draw.
1718 segmentLoop
= normalizedPattern
.length
;
1720 // If the pattern doesn't fit in the legend we scale it to fit.
1722 for (i
= 0; i
< strokePattern
.length
; i
++) {
1723 normalizedPattern
[i
] = strokePattern
[i
]/strokePixelLength
;
1725 // For the scaled patterns we do redraw the first segment.
1726 segmentLoop
= normalizedPattern
.length
+1;
1728 // Now make the pattern.
1729 for (j
= 0; j
< loop
; j
++) {
1730 for (i
= 0; i
< segmentLoop
; i
+=2) {
1731 // The padding is the drawn segment.
1732 paddingLeft
= normalizedPattern
[i
%normalizedPattern
.length
];
1733 if (i
< strokePattern
.length
) {
1734 // The margin is the space segment.
1735 marginRight
= normalizedPattern
[(i
+1)%normalizedPattern
.length
];
1737 // The repeated first segment has no right margin.
1740 dash
+= "<div style=\"display: inline-block; position: relative; " +
1741 "bottom: .5ex; margin-right: " + marginRight
+ "em; padding-left: " +
1742 paddingLeft
+ "em; height: 1px; border-bottom: 2px solid " + color
+
1752 * Generates HTML for the legend which is displayed when hovering over the
1753 * chart. If no selected points are specified, a default legend is returned
1754 * (this may just be the empty string).
1755 * @param { Number } [x] The x-value of the selected points.
1756 * @param { [Object] } [sel_points] List of selected points for the given
1757 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1758 * @param { Number } [oneEmWidth] The pixel width for 1em in the legend.
1760 Dygraph
.prototype.generateLegendHTML_
= function(x
, sel_points
, oneEmWidth
) {
1761 // If no points are selected, we display a default legend. Traditionally,
1762 // this has been blank. But a better default would be a conventional legend,
1763 // which provides essential information for a non-interactive chart.
1764 var html
, sepLines
, i
, c
, dash
, strokePattern
;
1765 if (typeof(x
) === 'undefined') {
1766 if (this.attr_('legend') != 'always') return '';
1768 sepLines
= this.attr_('labelsSeparateLines');
1769 var labels
= this.attr_('labels');
1771 for (i
= 1; i
< labels
.length
; i
++) {
1772 if (!this.visibility()[i
- 1]) continue;
1773 c
= this.plotter_
.colors
[labels
[i
]];
1774 if (html
!== '') html
+= (sepLines
? '<br/>' : ' ');
1775 strokePattern
= this.attr_("strokePattern", labels
[i
]);
1776 dash
= this.generateLegendDashHTML_(strokePattern
, c
, oneEmWidth
);
1777 html
+= "<span style='font-weight: bold; color: " + c
+ ";'>" + dash
+
1778 " " + labels
[i
] + "</span>";
1783 var xOptView
= this.optionsViewForAxis_('x');
1784 var xvf
= xOptView('valueFormatter');
1785 html
= xvf(x
, xOptView
, this.attr_('labels')[0], this) + ":";
1788 var num_axes
= this.numAxes();
1789 for (i
= 0; i
< num_axes
; i
++) {
1790 yOptViews
[i
] = this.optionsViewForAxis_('y' + (i
? 1 + i
: ''));
1792 var showZeros
= this.attr_("labelsShowZeroValues");
1793 sepLines
= this.attr_("labelsSeparateLines");
1794 for (i
= 0; i
< this.selPoints_
.length
; i
++) {
1795 var pt
= this.selPoints_
[i
];
1796 if (pt
.yval
=== 0 && !showZeros
) continue;
1797 if (!Dygraph
.isOK(pt
.canvasy
)) continue;
1798 if (sepLines
) html
+= "<br/>";
1800 var yOptView
= yOptViews
[this.seriesToAxisMap_
[pt
.name
]];
1801 var fmtFunc
= yOptView('valueFormatter');
1802 c
= this.plotter_
.colors
[pt
.name
];
1803 var yval
= fmtFunc(pt
.yval
, yOptView
, pt
.name
, this);
1805 var cls
= (pt
.name
== this.highlightSet_
) ? " class='highlight'" : "";
1806 // TODO(danvk): use a template string here and make it an attribute.
1807 html
+= "<span" + cls
+ ">" + " <b><span style='color: " + c
+ ";'>" + pt
.name
+
1808 "</span></b>:" + yval
+ "</span>";
1815 * Displays information about the selected points in the legend. If there is no
1816 * selection, the legend will be cleared.
1817 * @param { Number } [x] The x-value of the selected points.
1818 * @param { [Object] } [sel_points] List of selected points for the given
1819 * x-value. Should have properties like 'name', 'yval' and 'canvasy'.
1821 Dygraph
.prototype.setLegendHTML_
= function(x
, sel_points
) {
1822 var labelsDiv
= this.attr_("labelsDiv");
1823 var sizeSpan
= document
.createElement('span');
1824 // Calculates the width of 1em in pixels for the legend.
1825 sizeSpan
.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;');
1826 labelsDiv
.appendChild(sizeSpan
);
1827 var oneEmWidth
=sizeSpan
.offsetWidth
;
1829 var html
= this.generateLegendHTML_(x
, sel_points
, oneEmWidth
);
1830 if (labelsDiv
!== null) {
1831 labelsDiv
.innerHTML
= html
;
1833 if (typeof(this.shown_legend_error_
) == 'undefined') {
1834 this.error('labelsDiv is set to something nonexistent; legend will not be shown.');
1835 this.shown_legend_error_
= true;
1840 Dygraph
.prototype.animateSelection_
= function(direction
) {
1841 var totalSteps
= 10;
1843 if (this.fadeLevel
=== undefined
) {
1847 var start
= this.fadeLevel
;
1848 var steps
= direction
< 0 ? start
: totalSteps
- start
;
1850 if (this.fadeLevel
) {
1851 this.updateSelection_(1.0);
1856 var thisId
= ++this.animateId
;
1858 Dygraph
.repeatAndCleanup(function(n
) {
1859 // ignore simultaneous animations
1860 if (that
.animateId
!= thisId
) return;
1862 that
.fadeLevel
+= direction
;
1863 if (that
.fadeLevel
=== 0) {
1864 that
.clearSelection();
1866 that
.updateSelection_(that
.fadeLevel
/ totalSteps
);
1869 steps
, millis
, function() {});
1873 * Draw dots over the selectied points in the data series. This function
1874 * takes care of cleanup of previously-drawn dots.
1877 Dygraph
.prototype.updateSelection_
= function(opt_animFraction
) {
1878 // Clear the previously drawn vertical, if there is one
1880 var ctx
= this.canvas_ctx_
;
1881 if (this.attr_('highlightSeriesOpts')) {
1882 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1883 var alpha
= this.attr_('highlightSeriesBackgroundFade');
1885 if (this.attr_('highlightSeriesAnimate')) {
1886 if (opt_animFraction
=== undefined
) {
1887 // start a new animation
1888 this.animateSelection_(1);
1891 alpha
*= opt_animFraction
;
1893 ctx
.fillStyle
= 'rgba(255,255,255,' + alpha
+ ')';
1894 ctx
.fillRect(0, 0, this.width_
, this.height_
);
1896 var setIdx
= this.datasetIndexFromSetName_(this.highlightSet_
);
1897 var underlay
= this.attr_('highlightUnderlay');
1898 if (underlay
) underlay(this, ctx
, setIdx
);
1899 this.plotter_
._drawLine(ctx
, setIdx
);
1900 } else if (this.previousVerticalX_
>= 0) {
1901 // Determine the maximum highlight circle size.
1902 var maxCircleSize
= 0;
1903 var labels
= this.attr_('labels');
1904 for (i
= 1; i
< labels
.length
; i
++) {
1905 var r
= this.attr_('highlightCircleSize', labels
[i
]);
1906 if (r
> maxCircleSize
) maxCircleSize
= r
;
1908 var px
= this.previousVerticalX_
;
1909 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1910 2 * maxCircleSize
+ 2, this.height_
);
1913 if (this.isUsingExcanvas_
&& this.currentZoomRectArgs_
) {
1914 Dygraph
.prototype.drawZoomRect_
.apply(this, this.currentZoomRectArgs_
);
1917 if (this.selPoints_
.length
> 0) {
1918 // Set the status message to indicate the selected point(s)
1919 if (this.attr_('showLabelsOnHighlight')) {
1920 this.setLegendHTML_(this.lastx_
, this.selPoints_
);
1923 // Draw colored circles over the center of each selected point
1924 var canvasx
= this.selPoints_
[0].canvasx
;
1926 for (i
= 0; i
< this.selPoints_
.length
; i
++) {
1927 var pt
= this.selPoints_
[i
];
1928 if (!Dygraph
.isOK(pt
.canvasy
)) continue;
1930 var circleSize
= this.attr_('highlightCircleSize', pt
.name
);
1932 ctx
.fillStyle
= this.plotter_
.colors
[pt
.name
];
1933 ctx
.arc(canvasx
, pt
.canvasy
, circleSize
, 0, 2 * Math
.PI
, false);
1938 this.previousVerticalX_
= canvasx
;
1943 * Manually set the selected points and display information about them in the
1944 * legend. The selection can be cleared using clearSelection() and queried
1945 * using getSelection().
1946 * @param { Integer } row number that should be highlighted (i.e. appear with
1947 * hover dots on the chart). Set to false to clear any selection.
1948 * @param { seriesName } optional series name to highlight that series with the
1949 * the highlightSeriesOpts setting.
1951 Dygraph
.prototype.setSelection
= function(row
, opt_seriesName
) {
1952 // Extract the points we've selected
1953 this.selPoints_
= [];
1956 if (row
!== false) {
1957 for (var i
= 0; i
< this.boundaryIds_
.length
; i
++) {
1958 if (this.boundaryIds_
[i
] !== undefined
) {
1959 row
-= this.boundaryIds_
[i
][0];
1965 var changed
= false;
1966 if (row
!== false && row
>= 0) {
1967 if (row
!= this.lastRow_
) changed
= true;
1968 this.lastRow_
= row
;
1969 for (var setIdx
= 0; setIdx
< this.layout_
.datasets
.length
; ++setIdx
) {
1970 var set
= this.layout_
.datasets
[setIdx
];
1971 if (row
< set
.length
) {
1972 var point
= this.layout_
.points
[pos
+row
];
1974 if (this.attr_("stackedGraph")) {
1975 point
= this.layout_
.unstackPointAtIndex(pos
+row
);
1978 this.selPoints_
.push(point
);
1983 if (this.lastRow_
>= 0) changed
= true;
1987 if (this.selPoints_
.length
) {
1988 this.lastx_
= this.selPoints_
[0].xval
;
1993 if (opt_seriesName
!== undefined
) {
1994 if (this.highlightSet_
!== opt_seriesName
) changed
= true;
1995 this.highlightSet_
= opt_seriesName
;
1999 this.updateSelection_(undefined
);
2005 * The mouse has left the canvas. Clear out whatever artifacts remain
2006 * @param {Object} event the mouseout event from the browser.
2009 Dygraph
.prototype.mouseOut_
= function(event
) {
2010 if (this.attr_("unhighlightCallback")) {
2011 this.attr_("unhighlightCallback")(event
);
2014 if (this.attr_("hideOverlayOnMouseOut")) {
2015 this.clearSelection();
2020 * Clears the current selection (i.e. points that were highlighted by moving
2021 * the mouse over the chart).
2023 Dygraph
.prototype.clearSelection
= function() {
2024 // Get rid of the overlay data
2025 if (this.fadeLevel
) {
2026 this.animateSelection_(-1);
2029 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
2031 this.setLegendHTML_();
2032 this.selPoints_
= [];
2035 this.highlightSet_
= null;
2039 * Returns the number of the currently selected row. To get data for this row,
2040 * you can use the getValue method.
2041 * @return { Integer } row number, or -1 if nothing is selected
2043 Dygraph
.prototype.getSelection
= function() {
2044 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
2048 for (var row
=0; row
<this.layout_
.points
.length
; row
++ ) {
2049 if (this.layout_
.points
[row
].x
== this.selPoints_
[0].x
) {
2050 return row
+ this.boundaryIds_
[0][0];
2056 Dygraph
.prototype.getHighlightSeries
= function() {
2057 return this.highlightSet_
;
2061 * Fires when there's data available to be graphed.
2062 * @param {String} data Raw CSV data to be plotted
2065 Dygraph
.prototype.loadedEvent_
= function(data
) {
2066 this.rawData_
= this.parseCSV_(data
);
2071 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
2074 Dygraph
.prototype.addXTicks_
= function() {
2075 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
2077 if (this.dateWindow_
) {
2078 range
= [this.dateWindow_
[0], this.dateWindow_
[1]];
2080 range
= this.fullXRange_();
2083 var xAxisOptionsView
= this.optionsViewForAxis_('x');
2084 var xTicks
= xAxisOptionsView('ticker')(
2087 this.width_
, // TODO(danvk): should be area.width
2090 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
2091 // console.log(msg);
2092 this.layout_
.setXTicks(xTicks
);
2097 * Computes the range of the data series (including confidence intervals).
2098 * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or
2099 * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ...
2100 * @return [low, high]
2102 Dygraph
.prototype.extremeValues_
= function(series
) {
2103 var minY
= null, maxY
= null, j
, y
;
2105 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2107 // With custom bars, maxY is the max of the high values.
2108 for (j
= 0; j
< series
.length
; j
++) {
2109 y
= series
[j
][1][0];
2111 var low
= y
- series
[j
][1][1];
2112 var high
= y
+ series
[j
][1][2];
2113 if (low
> y
) low
= y
; // this can happen with custom bars,
2114 if (high
< y
) high
= y
; // e.g. in tests/custom-bars
.html
2115 if (maxY
=== null || high
> maxY
) {
2118 if (minY
=== null || low
< minY
) {
2123 for (j
= 0; j
< series
.length
; j
++) {
2125 if (y
=== null || isNaN(y
)) continue;
2126 if (maxY
=== null || y
> maxY
) {
2129 if (minY
=== null || y
< minY
) {
2135 return [minY
, maxY
];
2140 * This function is called once when the chart's data is changed or the options
2141 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2142 * idea is that values derived from the chart's data can be computed here,
2143 * rather than every time the chart is drawn. This includes things like the
2144 * number of axes, rolling averages, etc.
2146 Dygraph
.prototype.predraw_
= function() {
2147 var start
= new Date();
2149 // TODO(danvk): move more computations out of drawGraph_ and into here.
2150 this.computeYAxes_();
2152 // Create a new plotter.
2153 if (this.plotter_
) this.plotter_
.clear();
2154 this.plotter_
= new DygraphCanvasRenderer(this,
2159 // The roller sits in the bottom left corner of the chart. We don't know where
2160 // this will be until the options are available, so it's positioned here.
2161 this.createRollInterface_();
2163 // Same thing applies for the labelsDiv. It's right edge should be flush with
2164 // the right edge of the charting area (which may not be the same as the right
2165 // edge of the div, if we have two y-axes.
2166 this.positionLabelsDiv_();
2168 if (this.rangeSelector_
) {
2169 this.rangeSelector_
.renderStaticLayer();
2172 // Convert the raw data (a 2D array) into the internal format and compute
2173 // rolling averages.
2174 this.rolledSeries_
= [null]; // x-axis is the first series and it's special
2175 for (var i
= 1; i
< this.numColumns(); i
++) {
2176 var connectSeparatedPoints
= this.attr_('connectSeparatedPoints', i
);
2177 var logScale
= this.attr_('logscale', i
);
2178 var series
= this.extractSeries_(this.rawData_
, i
, logScale
, connectSeparatedPoints
);
2179 series
= this.rollingAverage(series
, this.rollPeriod_
);
2180 this.rolledSeries_
.push(series
);
2183 // If the data or options have changed, then we'd better redraw.
2186 // This is used to determine whether to do various animations.
2187 var end
= new Date();
2188 this.drawingTimeMs_
= (end
- start
);
2192 * Loop over all fields and create datasets, calculating extreme y-values for
2193 * each series and extreme x-indices as we go.
2195 * dateWindow is passed in as an explicit parameter so that we can compute
2196 * extreme values "speculatively", i.e. without actually setting state on the
2199 * TODO(danvk): make this more of a true function
2200 * @return [ datasets, seriesExtremes, boundaryIds ]
2203 Dygraph
.prototype.gatherDatasets_
= function(rolledSeries
, dateWindow
) {
2204 var boundaryIds
= [];
2205 var cumulative_y
= []; // For stacked series.
2207 var extremes
= {}; // series name -> [low, high]
2210 // Loop over the fields (series). Go from the last to the first,
2211 // because if they're stacked that's how we accumulate the values.
2212 var num_series
= rolledSeries
.length
- 1;
2213 for (i
= num_series
; i
>= 1; i
--) {
2214 if (!this.visibility()[i
- 1]) continue;
2216 // TODO(danvk): is this copy really necessary?
2218 for (j
= 0; j
< rolledSeries
[i
].length
; j
++) {
2219 series
.push(rolledSeries
[i
][j
]);
2222 // Prune down to the desired range, if necessary (for zooming)
2223 // Because there can be lines going to points outside of the visible area,
2224 // we actually prune to visible points, plus one on either side.
2225 var bars
= this.attr_("errorBars") || this.attr_("customBars");
2227 var low
= dateWindow
[0];
2228 var high
= dateWindow
[1];
2230 // TODO(danvk): do binary search instead of linear search.
2231 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2232 var firstIdx
= null, lastIdx
= null;
2233 for (k
= 0; k
< series
.length
; k
++) {
2234 if (series
[k
][0] >= low
&& firstIdx
=== null) {
2237 if (series
[k
][0] <= high
) {
2241 if (firstIdx
=== null) firstIdx
= 0;
2242 if (firstIdx
> 0) firstIdx
--;
2243 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2244 if (lastIdx
< series
.length
- 1) lastIdx
++;
2245 boundaryIds
[i
-1] = [firstIdx
, lastIdx
];
2246 for (k
= firstIdx
; k
<= lastIdx
; k
++) {
2247 pruned
.push(series
[k
]);
2251 boundaryIds
[i
-1] = [0, series
.length
-1];
2254 var seriesExtremes
= this.extremeValues_(series
);
2257 for (j
=0; j
<series
.length
; j
++) {
2258 series
[j
] = [series
[j
][0],
2263 } else if (this.attr_("stackedGraph")) {
2264 var l
= series
.length
;
2266 for (j
= 0; j
< l
; j
++) {
2267 // If one data set has a NaN, let all subsequent stacked
2268 // sets inherit the NaN -- only start at 0 for the first set.
2269 var x
= series
[j
][0];
2270 if (cumulative_y
[x
] === undefined
) {
2271 cumulative_y
[x
] = 0;
2274 actual_y
= series
[j
][1];
2275 cumulative_y
[x
] += actual_y
;
2277 series
[j
] = [x
, cumulative_y
[x
]];
2279 if (cumulative_y
[x
] > seriesExtremes
[1]) {
2280 seriesExtremes
[1] = cumulative_y
[x
];
2282 if (cumulative_y
[x
] < seriesExtremes
[0]) {
2283 seriesExtremes
[0] = cumulative_y
[x
];
2288 var seriesName
= this.attr_("labels")[i
];
2289 extremes
[seriesName
] = seriesExtremes
;
2290 datasets
[i
] = series
;
2293 return [ datasets
, extremes
, boundaryIds
];
2297 * Update the graph with new data. This method is called when the viewing area
2298 * has changed. If the underlying data or options have changed, predraw_ will
2299 * be called before drawGraph_ is called.
2301 * clearSelection, when undefined or true, causes this.clearSelection to be
2302 * called at the end of the draw operation. This should rarely be defined,
2303 * and never true (that is it should be undefined most of the time, and
2308 Dygraph
.prototype.drawGraph_
= function(clearSelection
) {
2309 var start
= new Date();
2311 if (typeof(clearSelection
) === 'undefined') {
2312 clearSelection
= true;
2315 // This is used to set the second parameter to drawCallback, below.
2316 var is_initial_draw
= this.is_initial_draw_
;
2317 this.is_initial_draw_
= false;
2319 this.layout_
.removeAllDatasets();
2321 this.attrs_
.pointSize
= 0.5 * this.attr_('highlightCircleSize');
2323 var packed
= this.gatherDatasets_(this.rolledSeries_
, this.dateWindow_
);
2324 var datasets
= packed
[0];
2325 var extremes
= packed
[1];
2326 this.boundaryIds_
= packed
[2];
2328 this.setIndexByName_
= {};
2329 var labels
= this.attr_("labels");
2330 if (labels
.length
> 0) {
2331 this.setIndexByName_
[labels
[0]] = 0;
2334 for (var i
= 1; i
< datasets
.length
; i
++) {
2335 this.setIndexByName_
[labels
[i
]] = i
;
2336 if (!this.visibility()[i
- 1]) continue;
2337 this.layout_
.addDataset(labels
[i
], datasets
[i
]);
2338 this.datasetIndex_
[i
] = dataIdx
++;
2341 this.computeYAxisRanges_(extremes
);
2342 this.layout_
.setYAxes(this.axes_
);
2346 // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
2347 var tmp_zoomed_x
= this.zoomed_x_
;
2348 // Tell PlotKit to use this new data and render itself
2349 this.layout_
.setDateWindow(this.dateWindow_
);
2350 this.zoomed_x_
= tmp_zoomed_x
;
2351 this.layout_
.evaluateWithError();
2352 this.renderGraph_(is_initial_draw
, false);
2354 if (this.attr_("timingName")) {
2355 var end
= new Date();
2357 console
.log(this.attr_("timingName") + " - drawGraph: " + (end
- start
) + "ms");
2362 Dygraph
.prototype.renderGraph_
= function(is_initial_draw
, clearSelection
) {
2363 this.plotter_
.clear();
2364 this.plotter_
.render();
2365 this.canvas_
.getContext('2d').clearRect(0, 0, this.canvas_
.width
,
2366 this.canvas_
.height
);
2368 // Generate a static legend before any particular point is selected.
2369 this.setLegendHTML_();
2371 if (!is_initial_draw
) {
2372 if (clearSelection
) {
2373 if (typeof(this.selPoints_
) !== 'undefined' && this.selPoints_
.length
) {
2374 // We should select the point nearest the page x/y here
, but it
's easier
2375 // to just clear the selection. This prevents erroneous hover dots from
2377 this.clearSelection();
2379 this.clearSelection();
2384 if (this.rangeSelector_) {
2385 this.rangeSelector_.renderInteractiveLayer();
2388 if (this.attr_("drawCallback") !== null) {
2389 this.attr_("drawCallback")(this, is_initial_draw);
2395 * Determine properties of the y-axes which are independent of the data
2396 * currently being displayed. This includes things like the number of axes and
2397 * the style of the axes. It does not include the range of each axis and its
2399 * This fills in this.axes_ and this.seriesToAxisMap_.
2400 * axes_ = [ { options } ]
2401 * seriesToAxisMap_ = { seriesName: 0, seriesName2: 1, ... }
2402 * indices are into the axes_ array.
2404 Dygraph.prototype.computeYAxes_ = function() {
2405 // Preserve valueWindow settings if they exist, and if the user hasn't
2406 // specified a new valueRange.
2407 var i
, valueWindows
, seriesName
, axis
, index
, opts
, v
;
2408 if (this.axes_
!== undefined
&& this.user_attrs_
.hasOwnProperty("valueRange") === false) {
2410 for (index
= 0; index
< this.axes_
.length
; index
++) {
2411 valueWindows
.push(this.axes_
[index
].valueWindow
);
2415 this.axes_
= [{ yAxisId
: 0, g
: this }]; // always have at least one y-axis.
2416 this.seriesToAxisMap_
= {};
2418 // Get a list of series names.
2419 var labels
= this.attr_("labels");
2421 for (i
= 1; i
< labels
.length
; i
++) series
[labels
[i
]] = (i
- 1);
2423 // all options which could be applied per-axis:
2431 'axisLabelFontSize',
2436 // Copy global axis options over to the first axis.
2437 for (i
= 0; i
< axisOptions
.length
; i
++) {
2438 var k
= axisOptions
[i
];
2440 if (v
) this.axes_
[0][k
] = v
;
2443 // Go through once and add all the axes.
2444 for (seriesName
in series
) {
2445 if (!series
.hasOwnProperty(seriesName
)) continue;
2446 axis
= this.attr_("axis", seriesName
);
2447 if (axis
=== null) {
2448 this.seriesToAxisMap_
[seriesName
] = 0;
2451 if (typeof(axis
) == 'object') {
2452 // Add a new axis, making a copy of its per-axis options.
2454 Dygraph
.update(opts
, this.axes_
[0]);
2455 Dygraph
.update(opts
, { valueRange
: null }); // shouldn't inherit this.
2456 var yAxisId
= this.axes_
.length
;
2457 opts
.yAxisId
= yAxisId
;
2459 Dygraph
.update(opts
, axis
);
2460 this.axes_
.push(opts
);
2461 this.seriesToAxisMap_
[seriesName
] = yAxisId
;
2465 // Go through one more time and assign series to an axis defined by another
2466 // series, e.g. { 'Y1: { axis: {} }, 'Y2': { axis: 'Y1' } }
2467 for (seriesName
in series
) {
2468 if (!series
.hasOwnProperty(seriesName
)) continue;
2469 axis
= this.attr_("axis", seriesName
);
2470 if (typeof(axis
) == 'string') {
2471 if (!this.seriesToAxisMap_
.hasOwnProperty(axis
)) {
2472 this.error("Series " + seriesName
+ " wants to share a y-axis with " +
2473 "series " + axis
+ ", which does not define its own axis.");
2476 var idx
= this.seriesToAxisMap_
[axis
];
2477 this.seriesToAxisMap_
[seriesName
] = idx
;
2481 if (valueWindows
!== undefined
) {
2482 // Restore valueWindow settings.
2483 for (index
= 0; index
< valueWindows
.length
; index
++) {
2484 this.axes_
[index
].valueWindow
= valueWindows
[index
];
2489 for (axis
= 0; axis
< this.axes_
.length
; axis
++) {
2491 opts
= this.optionsViewForAxis_('y' + (axis
? '2' : ''));
2492 v
= opts("valueRange");
2493 if (v
) this.axes_
[axis
].valueRange
= v
;
2494 } else { // To keep old behavior
2495 var axes
= this.user_attrs_
.axes
;
2496 if (axes
&& axes
.y2
) {
2497 v
= axes
.y2
.valueRange
;
2498 if (v
) this.axes_
[axis
].valueRange
= v
;
2506 * Returns the number of y-axes on the chart.
2507 * @return {Number} the number of axes.
2509 Dygraph
.prototype.numAxes
= function() {
2511 for (var series
in this.seriesToAxisMap_
) {
2512 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2513 var idx
= this.seriesToAxisMap_
[series
];
2514 if (idx
> last_axis
) last_axis
= idx
;
2516 return 1 + last_axis
;
2521 * Returns axis properties for the given series.
2522 * @param { String } setName The name of the series for which to get axis
2523 * properties, e.g. 'Y1'.
2524 * @return { Object } The axis properties.
2526 Dygraph
.prototype.axisPropertiesForSeries
= function(series
) {
2527 // TODO(danvk): handle errors.
2528 return this.axes_
[this.seriesToAxisMap_
[series
]];
2533 * Determine the value range and tick marks for each axis.
2534 * @param {Object} extremes A mapping from seriesName -> [low, high]
2535 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2537 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2538 // Build a map from axis number -> [list of series names]
2539 var seriesForAxis
= [], series
;
2540 for (series
in this.seriesToAxisMap_
) {
2541 if (!this.seriesToAxisMap_
.hasOwnProperty(series
)) continue;
2542 var idx
= this.seriesToAxisMap_
[series
];
2543 while (seriesForAxis
.length
<= idx
) seriesForAxis
.push([]);
2544 seriesForAxis
[idx
].push(series
);
2547 // Compute extreme values, a span and tick marks for each axis.
2548 for (var i
= 0; i
< this.axes_
.length
; i
++) {
2549 var axis
= this.axes_
[i
];
2551 if (!seriesForAxis
[i
]) {
2552 // If no series are defined or visible then use a reasonable default
2553 axis
.extremeRange
= [0, 1];
2555 // Calculate the extremes of extremes.
2556 series
= seriesForAxis
[i
];
2557 var minY
= Infinity
; // extremes[series[0]][0];
2558 var maxY
= -Infinity
; // extremes[series[0]][1];
2559 var extremeMinY
, extremeMaxY
;
2561 for (var j
= 0; j
< series
.length
; j
++) {
2562 // this skips invisible series
2563 if (!extremes
.hasOwnProperty(series
[j
])) continue;
2565 // Only use valid extremes to stop null data series' from corrupting the scale.
2566 extremeMinY
= extremes
[series
[j
]][0];
2567 if (extremeMinY
!== null) {
2568 minY
= Math
.min(extremeMinY
, minY
);
2570 extremeMaxY
= extremes
[series
[j
]][1];
2571 if (extremeMaxY
!== null) {
2572 maxY
= Math
.max(extremeMaxY
, maxY
);
2575 if (axis
.includeZero
&& minY
> 0) minY
= 0;
2577 // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2578 if (minY
== Infinity
) minY
= 0;
2579 if (maxY
== -Infinity
) maxY
= 1;
2581 // Add some padding and round up to an integer to be human-friendly.
2582 var span
= maxY
- minY
;
2583 // special case: if we have no sense of scale, use +/-10% of the sole value
.
2584 if (span
=== 0) { span
= maxY
; }
2586 var maxAxisY
, minAxisY
;
2587 if (axis
.logscale
) {
2588 maxAxisY
= maxY
+ 0.1 * span
;
2591 maxAxisY
= maxY
+ 0.1 * span
;
2592 minAxisY
= minY
- 0.1 * span
;
2594 // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense.
2595 if (!this.attr_("avoidMinZero")) {
2596 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2597 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2600 if (this.attr_("includeZero")) {
2601 if (maxY
< 0) maxAxisY
= 0;
2602 if (minY
> 0) minAxisY
= 0;
2605 axis
.extremeRange
= [minAxisY
, maxAxisY
];
2607 if (axis
.valueWindow
) {
2608 // This is only set if the user has zoomed on the y-axis. It is never set
2609 // by a user. It takes precedence over axis.valueRange because, if you set
2610 // valueRange, you'd still expect to be able to pan.
2611 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2612 } else if (axis
.valueRange
) {
2613 // This is a user-set value range for this axis.
2614 axis
.computedValueRange
= [axis
.valueRange
[0], axis
.valueRange
[1]];
2616 axis
.computedValueRange
= axis
.extremeRange
;
2619 // Add ticks. By default, all axes inherit the tick positions of the
2620 // primary axis. However, if an axis is specifically marked as having
2621 // independent ticks, then that is permissible as well.
2622 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2623 var ticker
= opts('ticker');
2624 if (i
=== 0 || axis
.independentTicks
) {
2625 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2626 axis
.computedValueRange
[1],
2627 this.height_
, // TODO(danvk): should be area.height
2631 var p_axis
= this.axes_
[0];
2632 var p_ticks
= p_axis
.ticks
;
2633 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2634 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2635 var tick_values
= [];
2636 for (var k
= 0; k
< p_ticks
.length
; k
++) {
2637 var y_frac
= (p_ticks
[k
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2638 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2639 tick_values
.push(y_val
);
2642 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2643 axis
.computedValueRange
[1],
2644 this.height_
, // TODO(danvk): should be area.height
2653 * Extracts one series from the raw data (a 2D array) into an array of (date,
2656 * This is where undesirable points (i.e. negative values on log scales and
2657 * missing values through which we wish to connect lines) are dropped.
2661 Dygraph
.prototype.extractSeries_
= function(rawData
, i
, logScale
, connectSeparatedPoints
) {
2663 for (var j
= 0; j
< rawData
.length
; j
++) {
2664 var x
= rawData
[j
][0];
2665 var point
= rawData
[j
][i
];
2667 // On the log scale, points less than zero do not exist.
2668 // This will create a gap in the chart. Note that this ignores
2669 // connectSeparatedPoints.
2673 series
.push([x
, point
]);
2675 if (point
!== null || !connectSeparatedPoints
) {
2676 series
.push([x
, point
]);
2685 * Calculates the rolling average of a data set.
2686 * If originalData is [label, val], rolls the average of those.
2687 * If originalData is [label, [, it's interpreted as [value, stddev]
2688 * and the roll is returned in the same form, with appropriately reduced
2689 * stddev for each value.
2690 * Note that this is where fractional input (i.e. '5/10') is converted into
2692 * @param {Array} originalData The data in the appropriate format (see above)
2693 * @param {Number} rollPeriod The number of points over which to average the
2696 Dygraph
.prototype.rollingAverage
= function(originalData
, rollPeriod
) {
2697 if (originalData
.length
< 2)
2698 return originalData
;
2699 rollPeriod
= Math
.min(rollPeriod
, originalData
.length
);
2700 var rollingData
= [];
2701 var sigma
= this.attr_("sigma");
2703 var low
, high
, i
, j
, y
, sum
, num_ok
, stddev
;
2704 if (this.fractions_
) {
2706 var den
= 0; // numerator/denominator
2708 for (i
= 0; i
< originalData
.length
; i
++) {
2709 num
+= originalData
[i
][1][0];
2710 den
+= originalData
[i
][1][1];
2711 if (i
- rollPeriod
>= 0) {
2712 num
-= originalData
[i
- rollPeriod
][1][0];
2713 den
-= originalData
[i
- rollPeriod
][1][1];
2716 var date
= originalData
[i
][0];
2717 var value
= den
? num
/ den
: 0.0;
2718 if (this.attr_("errorBars")) {
2719 if (this.attr_("wilsonInterval")) {
2720 // For more details on this confidence interval, see:
2721 // http://en.wikipedia.org/wiki
/Binomial_confidence_interval
2723 var p
= value
< 0 ? 0 : value
, n
= den
;
2724 var pm
= sigma
* Math
.sqrt(p
*(1-p
)/n + sigma*sigma/(4*n
*n
));
2725 var denom
= 1 + sigma
* sigma
/ den
;
2726 low
= (p
+ sigma
* sigma
/ (2 * den) - pm) / denom
;
2727 high
= (p
+ sigma
* sigma
/ (2 * den) + pm) / denom
;
2728 rollingData
[i
] = [date
,
2729 [p
* mult
, (p
- low
) * mult
, (high
- p
) * mult
]];
2731 rollingData
[i
] = [date
, [0, 0, 0]];
2734 stddev
= den
? sigma
* Math
.sqrt(value
* (1 - value
) / den
) : 1.0;
2735 rollingData
[i
] = [date
, [mult
* value
, mult
* stddev
, mult
* stddev
]];
2738 rollingData
[i
] = [date
, mult
* value
];
2741 } else if (this.attr_("customBars")) {
2746 for (i
= 0; i
< originalData
.length
; i
++) {
2747 var data
= originalData
[i
][1];
2749 rollingData
[i
] = [originalData
[i
][0], [y
, y
- data
[0], data
[2] - y
]];
2751 if (y
!== null && !isNaN(y
)) {
2757 if (i
- rollPeriod
>= 0) {
2758 var prev
= originalData
[i
- rollPeriod
];
2759 if (prev
[1][1] !== null && !isNaN(prev
[1][1])) {
2767 rollingData
[i
] = [originalData
[i
][0], [ 1.0 * mid
/ count
,
2768 1.0 * (mid
- low
) / count
,
2769 1.0 * (high
- mid
) / count
]];
2771 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2775 // Calculate the rolling average for the first rollPeriod - 1 points where
2776 // there is not enough data to roll over the full number of points
2777 if (!this.attr_("errorBars")){
2778 if (rollPeriod
== 1) {
2779 return originalData
;
2782 for (i
= 0; i
< originalData
.length
; i
++) {
2785 for (j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2786 y
= originalData
[j
][1];
2787 if (y
=== null || isNaN(y
)) continue;
2789 sum
+= originalData
[j
][1];
2792 rollingData
[i
] = [originalData
[i
][0], sum
/ num_ok
];
2794 rollingData
[i
] = [originalData
[i
][0], null];
2799 for (i
= 0; i
< originalData
.length
; i
++) {
2803 for (j
= Math
.max(0, i
- rollPeriod
+ 1); j
< i
+ 1; j
++) {
2804 y
= originalData
[j
][1][0];
2805 if (y
=== null || isNaN(y
)) continue;
2807 sum
+= originalData
[j
][1][0];
2808 variance
+= Math
.pow(originalData
[j
][1][1], 2);
2811 stddev
= Math
.sqrt(variance
) / num_ok
;
2812 rollingData
[i
] = [originalData
[i
][0],
2813 [sum
/ num_ok
, sigma
* stddev
, sigma
* stddev
]];
2815 rollingData
[i
] = [originalData
[i
][0], [null, null, null]];
2825 * Detects the type of the str (date or numeric) and sets the various
2826 * formatting attributes in this.attrs_ based on this type.
2827 * @param {String} str An x value.
2830 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2832 var dashPos
= str
.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2
2833 if ((dashPos
> 0 && (str
[dashPos
-1] != 'e' && str
[dashPos
-1] != 'E')) ||
2834 str
.indexOf('/') >= 0 ||
2835 isNaN(parseFloat(str
))) {
2837 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2838 // TODO(danvk): remove support for this format.
2843 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
2844 this.attrs_
.axes
.x
.valueFormatter
= Dygraph
.dateString_
;
2845 this.attrs_
.axes
.x
.ticker
= Dygraph
.dateTicker
;
2846 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.dateAxisFormatter
;
2848 /** @private (shut up, jsdoc!) */
2849 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2850 // TODO(danvk): use Dygraph.numberValueFormatter here?
2851 /** @private (shut up, jsdoc!) */
2852 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2853 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
2854 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2859 * Parses the value as a floating point number. This is like the parseFloat()
2860 * built-in, but with a few differences:
2861 * - the empty string is parsed as null, rather than NaN.
2862 * - if the string cannot be parsed at all, an error is logged.
2863 * If the string can't be parsed, this method returns null.
2864 * @param {String} x The string to be parsed
2865 * @param {Number} opt_line_no The line number from which the string comes.
2866 * @param {String} opt_line The text of the line from which the string comes.
2870 // Parse the x as a float or return null if it's not a number.
2871 Dygraph
.prototype.parseFloat_
= function(x
, opt_line_no
, opt_line
) {
2872 var val
= parseFloat(x
);
2873 if (!isNaN(val
)) return val
;
2875 // Try to figure out what happeend.
2876 // If the value is the empty string, parse it as null.
2877 if (/^ *$/.test(x
)) return null;
2879 // If it was actually "NaN", return it as NaN.
2880 if (/^ *nan *$/i.test(x
)) return NaN
;
2882 // Looks like a parsing error.
2883 var msg
= "Unable to parse '" + x
+ "' as a number";
2884 if (opt_line
!== null && opt_line_no
!== null) {
2885 msg
+= " on line " + (1+opt_line_no
) + " ('" + opt_line
+ "') of CSV.";
2894 * Parses a string in a special csv format. We expect a csv file where each
2895 * line is a date point, and the first field in each line is the date string.
2896 * We also expect that all remaining fields represent series.
2897 * if the errorBars attribute is set, then interpret the fields as:
2898 * date, series1, stddev1, series2, stddev2, ...
2899 * @param {[Object]} data See above.
2901 * @return [Object] An array with one entry for each row. These entries
2902 * are an array of cells in that row. The first entry is the parsed x-value for
2903 * the row. The second, third, etc. are the y-values. These can take on one of
2904 * three forms, depending on the CSV and constructor parameters:
2906 * 2. [ value, stddev ]
2907 * 3. [ low value, center value, high value ]
2909 Dygraph
.prototype.parseCSV_
= function(data
) {
2911 var lines
= data
.split("\n");
2914 // Use the default delimiter or fall back to a tab if that makes sense.
2915 var delim
= this.attr_('delimiter');
2916 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2921 if (!('labels' in this.user_attrs_
)) {
2922 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2924 this.attrs_
.labels
= lines
[0].split(delim
); // NOTE: _not_ user_attrs_.
2929 var defaultParserSet
= false; // attempt to auto-detect x value type
2930 var expectedCols
= this.attr_("labels").length
;
2931 var outOfOrder
= false;
2932 for (var i
= start
; i
< lines
.length
; i
++) {
2933 var line
= lines
[i
];
2935 if (line
.length
=== 0) continue; // skip blank lines
2936 if (line
[0] == '#') continue; // skip comment lines
2937 var inFields
= line
.split(delim
);
2938 if (inFields
.length
< 2) continue;
2941 if (!defaultParserSet
) {
2942 this.detectTypeFromString_(inFields
[0]);
2943 xParser
= this.attr_("xValueParser");
2944 defaultParserSet
= true;
2946 fields
[0] = xParser(inFields
[0], this);
2948 // If fractions are expected, parse the numbers as "A/B
"
2949 if (this.fractions_) {
2950 for (j = 1; j < inFields.length; j++) {
2951 // TODO(danvk): figure out an appropriate way to flag parse errors.
2952 vals = inFields[j].split("/");
2953 if (vals.length != 2) {
2954 this.error('Expected fractional "num
/den
" values in CSV data ' +
2955 "but found a value
'" + inFields[j] + "' on line
" +
2956 (1 + i) + " ('" + line + "') which is not of
this form
.");
2959 fields[j] = [this.parseFloat_(vals[0], i, line),
2960 this.parseFloat_(vals[1], i, line)];
2963 } else if (this.attr_("errorBars
")) {
2964 // If there are error bars, values are (value, stddev) pairs
2965 if (inFields.length % 2 != 1) {
2966 this.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2967 'but line ' + (1 + i) + ' has an odd number of values (' +
2968 (inFields.length - 1) + "): '" + line + "'");
2970 for (j = 1; j < inFields.length; j += 2) {
2971 fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line),
2972 this.parseFloat_(inFields[j + 1], i, line)];
2974 } else if (this.attr_("customBars
")) {
2975 // Bars are a low;center;high tuple
2976 for (j = 1; j < inFields.length; j++) {
2977 var val = inFields[j];
2978 if (/^ *$/.test(val)) {
2979 fields[j] = [null, null, null];
2981 vals = val.split(";");
2982 if (vals.length == 3) {
2983 fields[j] = [ this.parseFloat_(vals[0], i, line),
2984 this.parseFloat_(vals[1], i, line),
2985 this.parseFloat_(vals[2], i, line) ];
2987 this.warn('When using customBars, values must be either blank ' +
2988 'or "low
;center
;high
" tuples (got "' + val +
2989 '" on line ' + (1+i));
2994 // Values are just numbers
2995 for (j = 1; j < inFields.length; j++) {
2996 fields[j] = this.parseFloat_(inFields[j], i, line);
2999 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
3003 if (fields.length != expectedCols) {
3004 this.error("Number of columns
in line
" + i + " (" + fields.length +
3005 ") does not agree
with number of
labels (" + expectedCols +
3009 // If the user specified the 'labels' option and none of the cells of the
3010 // first row parsed correctly, then they probably double-specified the
3011 // labels. We go with the values set in the option, discard this row and
3012 // log a warning to the JS console.
3013 if (i === 0 && this.attr_('labels')) {
3014 var all_null = true;
3015 for (j = 0; all_null && j < fields.length; j++) {
3016 if (fields[j]) all_null = false;
3019 this.warn("The dygraphs
'labels' option is set
, but the first row of
" +
3020 "CSV
data ('" + line + "') appears to also contain labels
. " +
3021 "Will drop the CSV labels and
use the option labels
.");
3029 this.warn("CSV is out of order
; order it correctly to speed loading
.");
3030 ret.sort(function(a,b) { return a[0] - b[0]; });
3038 * The user has provided their data as a pre-packaged JS array. If the x values
3039 * are numeric, this is the same as dygraphs' internal format. If the x values
3040 * are dates, we need to convert them from Date objects to ms since epoch.
3041 * @param {[Object]} data
3042 * @return {[Object]} data with numeric x values.
3044 Dygraph.prototype.parseArray_ = function(data) {
3045 // Peek at the first x value to see if it's numeric.
3046 if (data.length === 0) {
3047 this.error("Can
't plot empty data set");
3050 if (data[0].length === 0) {
3051 this.error("Data set cannot contain an empty row");
3056 if (this.attr_("labels") === null) {
3057 this.warn("Using default labels. Set labels explicitly via 'labels
' " +
3058 "in the options parameter");
3059 this.attrs_.labels = [ "X" ];
3060 for (i = 1; i < data[0].length; i++) {
3061 this.attrs_.labels.push("Y" + i);
3065 if (Dygraph.isDateLike(data[0][0])) {
3066 // Some intelligent defaults for a date x-axis.
3067 this.attrs_.axes.x.valueFormatter = Dygraph.dateString_;
3068 this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisFormatter;
3069 this.attrs_.axes.x.ticker = Dygraph.dateTicker;
3071 // Assume they're all dates
.
3072 var parsedData
= Dygraph
.clone(data
);
3073 for (i
= 0; i
< data
.length
; i
++) {
3074 if (parsedData
[i
].length
=== 0) {
3075 this.error("Row " + (1 + i
) + " of data is empty");
3078 if (parsedData
[i
][0] === null ||
3079 typeof(parsedData
[i
][0].getTime
) != 'function' ||
3080 isNaN(parsedData
[i
][0].getTime())) {
3081 this.error("x value in row " + (1 + i
) + " is not a Date");
3084 parsedData
[i
][0] = parsedData
[i
][0].getTime();
3088 // Some intelligent defaults for a numeric x-axis.
3089 /** @private (shut up, jsdoc!) */
3090 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
3091 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.numberAxisLabelFormatter
;
3092 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
3098 * Parses a DataTable object from gviz.
3099 * The data is expected to have a first column that is either a date or a
3100 * number. All subsequent columns must be numbers. If there is a clear mismatch
3101 * between this.xValueParser_ and the type of the first column, it will be
3102 * fixed. Fills out rawData_.
3103 * @param {[Object]} data See above.
3106 Dygraph
.prototype.parseDataTable_
= function(data
) {
3107 var shortTextForAnnotationNum
= function(num
) {
3108 // converts [0-9]+ [A-Z][a-z]*
3109 // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
3110 // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
3111 var shortText
= String
.fromCharCode(65 /* A */ + num
% 26);
3112 num
= Math
.floor(num
/ 26);
3114 shortText
= String
.fromCharCode(65 /* A */ + (num
- 1) % 26 ) + shortText
.toLowerCase();
3115 num
= Math
.floor((num
- 1) / 26);
3120 var cols
= data
.getNumberOfColumns();
3121 var rows
= data
.getNumberOfRows();
3123 var indepType
= data
.getColumnType(0);
3124 if (indepType
== 'date' || indepType
== 'datetime') {
3125 this.attrs_
.xValueParser
= Dygraph
.dateParser
;
3126 this.attrs_
.axes
.x
.valueFormatter
= Dygraph
.dateString_
;
3127 this.attrs_
.axes
.x
.ticker
= Dygraph
.dateTicker
;
3128 this.attrs_
.axes
.x
.axisLabelFormatter
= Dygraph
.dateAxisFormatter
;
3129 } else if (indepType
== 'number') {
3130 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
3131 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
3132 this.attrs_
.axes
.x
.ticker
= Dygraph
.numericTicks
;
3133 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
3135 this.error("only 'date', 'datetime' and 'number' types are supported for " +
3136 "column 1 of DataTable input (Got '" + indepType
+ "')");
3140 // Array of the column indices which contain data (and not annotations).
3142 var annotationCols
= {}; // data index -> [annotation cols]
3143 var hasAnnotations
= false;
3145 for (i
= 1; i
< cols
; i
++) {
3146 var type
= data
.getColumnType(i
);
3147 if (type
== 'number') {
3149 } else if (type
== 'string' && this.attr_('displayAnnotations')) {
3150 // This is OK -- it's an annotation column.
3151 var dataIdx
= colIdx
[colIdx
.length
- 1];
3152 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
3153 annotationCols
[dataIdx
] = [i
];
3155 annotationCols
[dataIdx
].push(i
);
3157 hasAnnotations
= true;
3159 this.error("Only 'number' is supported as a dependent type with Gviz." +
3160 " 'string' is only supported if displayAnnotations is true");
3164 // Read column labels
3165 // TODO(danvk): add support back for errorBars
3166 var labels
= [data
.getColumnLabel(0)];
3167 for (i
= 0; i
< colIdx
.length
; i
++) {
3168 labels
.push(data
.getColumnLabel(colIdx
[i
]));
3169 if (this.attr_("errorBars")) i
+= 1;
3171 this.attrs_
.labels
= labels
;
3172 cols
= labels
.length
;
3175 var outOfOrder
= false;
3176 var annotations
= [];
3177 for (i
= 0; i
< rows
; i
++) {
3179 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
3180 data
.getValue(i
, 0) === null) {
3181 this.warn("Ignoring row " + i
+
3182 " of DataTable because of undefined or null first column.");
3186 if (indepType
== 'date' || indepType
== 'datetime') {
3187 row
.push(data
.getValue(i
, 0).getTime());
3189 row
.push(data
.getValue(i
, 0));
3191 if (!this.attr_("errorBars")) {
3192 for (j
= 0; j
< colIdx
.length
; j
++) {
3193 var col
= colIdx
[j
];
3194 row
.push(data
.getValue(i
, col
));
3195 if (hasAnnotations
&&
3196 annotationCols
.hasOwnProperty(col
) &&
3197 data
.getValue(i
, annotationCols
[col
][0]) !== null) {
3199 ann
.series
= data
.getColumnLabel(col
);
3201 ann
.shortText
= shortTextForAnnotationNum(annotations
.length
);
3203 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
3204 if (k
) ann
.text
+= "\n";
3205 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
3207 annotations
.push(ann
);
3211 // Strip out infinities, which give dygraphs problems later on.
3212 for (j
= 0; j
< row
.length
; j
++) {
3213 if (!isFinite(row
[j
])) row
[j
] = null;
3216 for (j
= 0; j
< cols
- 1; j
++) {
3217 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
3220 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
3227 this.warn("DataTable is out of order; order it correctly to speed loading.");
3228 ret
.sort(function(a
,b
) { return a
[0] - b
[0]; });
3230 this.rawData_
= ret
;
3232 if (annotations
.length
> 0) {
3233 this.setAnnotations(annotations
, true);
3238 * Get the CSV data. If it's in a function, call that function. If it's in a
3239 * file, do an XMLHttpRequest to get it.
3242 Dygraph
.prototype.start_
= function() {
3243 var data
= this.file_
;
3245 // Functions can return references of all other types.
3246 if (typeof data
== 'function') {
3250 if (Dygraph
.isArrayLike(data
)) {
3251 this.rawData_
= this.parseArray_(data
);
3253 } else if (typeof data
== 'object' &&
3254 typeof data
.getColumnRange
== 'function') {
3255 // must be a DataTable from gviz.
3256 this.parseDataTable_(data
);
3258 } else if (typeof data
== 'string') {
3259 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3260 if (data
.indexOf('\n') >= 0) {
3261 this.loadedEvent_(data
);
3263 var req
= new XMLHttpRequest();
3265 req
.onreadystatechange
= function () {
3266 if (req
.readyState
== 4) {
3267 if (req
.status
=== 200 || // Normal http
3268 req
.status
=== 0) { // Chrome w/ --allow
-file
-access
-from
-files
3269 caller
.loadedEvent_(req
.responseText
);
3274 req
.open("GET", data
, true);
3278 this.error("Unknown data format: " + (typeof data
));
3283 * Changes various properties of the graph. These can include:
3285 * <li>file: changes the source data for the graph</li>
3286 * <li>errorBars: changes whether the data contains stddev</li>
3289 * There's a huge variety of options that can be passed to this method. For a
3290 * full list, see http://dygraphs.com/options.html.
3292 * @param {Object} attrs The new properties and values
3293 * @param {Boolean} [block_redraw] Usually the chart is redrawn after every
3294 * call to updateOptions(). If you know better, you can pass true to explicitly
3295 * block the redraw. This can be useful for chaining updateOptions() calls,
3296 * avoiding the occasional infinite loop and preventing redraws when it's not
3297 * necessary (e.g. when updating a callback).
3299 Dygraph
.prototype.updateOptions
= function(input_attrs
, block_redraw
) {
3300 if (typeof(block_redraw
) == 'undefined') block_redraw
= false;
3302 // mapLegacyOptions_ drops the "file" parameter as a convenience to us.
3303 var file
= input_attrs
.file
;
3304 var attrs
= Dygraph
.mapLegacyOptions_(input_attrs
);
3306 // TODO(danvk): this is a mess. Move these options into attr_.
3307 if ('rollPeriod' in attrs
) {
3308 this.rollPeriod_
= attrs
.rollPeriod
;
3310 if ('dateWindow' in attrs
) {
3311 this.dateWindow_
= attrs
.dateWindow
;
3312 if (!('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
3313 this.zoomed_x_
= (attrs
.dateWindow
!== null);
3316 if ('valueRange' in attrs
&& !('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
3317 this.zoomed_y_
= (attrs
.valueRange
!== null);
3320 // TODO(danvk): validate per-series options.
3325 // highlightCircleSize
3327 // Check if this set options will require new points.
3328 var requiresNewPoints
= Dygraph
.isPixelChangingOptionList(this.attr_("labels"), attrs
);
3330 Dygraph
.updateDeep(this.user_attrs_
, attrs
);
3334 if (!block_redraw
) this.start_();
3336 if (!block_redraw
) {
3337 if (requiresNewPoints
) {
3340 this.renderGraph_(false, false);
3347 * Returns a copy of the options with deprecated names converted into current
3348 * names. Also drops the (potentially-large) 'file' attribute. If the caller is
3349 * interested in that, they should save a copy before calling this.
3352 Dygraph
.mapLegacyOptions_
= function(attrs
) {
3354 for (var k
in attrs
) {
3355 if (k
== 'file') continue;
3356 if (attrs
.hasOwnProperty(k
)) my_attrs
[k
] = attrs
[k
];
3359 var set
= function(axis
, opt
, value
) {
3360 if (!my_attrs
.axes
) my_attrs
.axes
= {};
3361 if (!my_attrs
.axes
[axis
]) my_attrs
.axes
[axis
] = {};
3362 my_attrs
.axes
[axis
][opt
] = value
;
3364 var map
= function(opt
, axis
, new_opt
) {
3365 if (typeof(attrs
[opt
]) != 'undefined') {
3366 set(axis
, new_opt
, attrs
[opt
]);
3367 delete my_attrs
[opt
];
3371 // This maps, e.g., xValueFormater -> axes: { x: { valueFormatter: ... } }
3372 map('xValueFormatter', 'x', 'valueFormatter');
3373 map('pixelsPerXLabel', 'x', 'pixelsPerLabel');
3374 map('xAxisLabelFormatter', 'x', 'axisLabelFormatter');
3375 map('xTicker', 'x', 'ticker');
3376 map('yValueFormatter', 'y', 'valueFormatter');
3377 map('pixelsPerYLabel', 'y', 'pixelsPerLabel');
3378 map('yAxisLabelFormatter', 'y', 'axisLabelFormatter');
3379 map('yTicker', 'y', 'ticker');
3384 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3385 * containing div (which has presumably changed size since the dygraph was
3386 * instantiated. If the width/height are specified, the div will be resized.
3388 * This is far more efficient than destroying and re-instantiating a
3389 * Dygraph, since it doesn't have to reparse the underlying data.
3391 * @param {Number} [width] Width (in pixels)
3392 * @param {Number} [height] Height (in pixels)
3394 Dygraph
.prototype.resize
= function(width
, height
) {
3395 if (this.resize_lock
) {
3398 this.resize_lock
= true;
3400 if ((width
=== null) != (height
=== null)) {
3401 this.warn("Dygraph.resize() should be called with zero parameters or " +
3402 "two non-NULL parameters. Pretending it was zero.");
3403 width
= height
= null;
3406 var old_width
= this.width_
;
3407 var old_height
= this.height_
;
3410 this.maindiv_
.style
.width
= width
+ "px";
3411 this.maindiv_
.style
.height
= height
+ "px";
3412 this.width_
= width
;
3413 this.height_
= height
;
3415 this.width_
= this.maindiv_
.clientWidth
;
3416 this.height_
= this.maindiv_
.clientHeight
;
3419 if (old_width
!= this.width_
|| old_height
!= this.height_
) {
3420 // TODO(danvk): there should be a clear() method.
3421 this.maindiv_
.innerHTML
= "";
3422 this.roller_
= null;
3423 this.attrs_
.labelsDiv
= null;
3424 this.createInterface_();
3425 if (this.annotations_
.length
) {
3426 // createInterface_ reset the layout, so we need to do this.
3427 this.layout_
.setAnnotations(this.annotations_
);
3432 this.resize_lock
= false;
3436 * Adjusts the number of points in the rolling average. Updates the graph to
3437 * reflect the new averaging period.
3438 * @param {Number} length Number of points over which to average the data.
3440 Dygraph
.prototype.adjustRoll
= function(length
) {
3441 this.rollPeriod_
= length
;
3446 * Returns a boolean array of visibility statuses.
3448 Dygraph
.prototype.visibility
= function() {
3449 // Do lazy-initialization, so that this happens after we know the number of
3451 if (!this.attr_("visibility")) {
3452 this.attrs_
.visibility
= [];
3454 // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs
.
3455 while (this.attr_("visibility").length
< this.numColumns() - 1) {
3456 this.attrs_
.visibility
.push(true);
3458 return this.attr_("visibility");
3462 * Changes the visiblity of a series.
3464 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3465 var x
= this.visibility();
3466 if (num
< 0 || num
>= x
.length
) {
3467 this.warn("invalid series number in setVisibility: " + num
);
3475 * How large of an area will the dygraph render itself in?
3476 * This is used for testing.
3477 * @return A {width: w, height: h} object.
3480 Dygraph
.prototype.size
= function() {
3481 return { width
: this.width_
, height
: this.height_
};
3485 * Update the list of annotations and redraw the chart.
3486 * See dygraphs.com/annotations.html for more info on how to use annotations.
3487 * @param ann {Array} An array of annotation objects.
3488 * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3490 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3491 // Only add the annotation CSS rule once we know it will be used.
3492 Dygraph
.addAnnotationRule();
3493 this.annotations_
= ann
;
3494 this.layout_
.setAnnotations(this.annotations_
);
3495 if (!suppressDraw
) {
3501 * Return the list of annotations.
3503 Dygraph
.prototype.annotations
= function() {
3504 return this.annotations_
;
3508 * Get the list of label names for this graph. The first column is the
3509 * x-axis, so the data series names start at index 1.
3511 Dygraph
.prototype.getLabels
= function(name
) {
3512 return this.attr_("labels").slice();
3516 * Get the index of a series (column) given its name. The first column is the
3517 * x-axis, so the data series start with index 1.
3519 Dygraph
.prototype.indexFromSetName
= function(name
) {
3520 return this.setIndexByName_
[name
];
3524 * Get the internal dataset index given its name. These are numbered starting from 0,
3525 * and only count visible sets.
3528 Dygraph
.prototype.datasetIndexFromSetName_
= function(name
) {
3529 return this.datasetIndex_
[this.indexFromSetName(name
)];
3534 * Adds a default style for the annotation CSS classes to the document. This is
3535 * only executed when annotations are actually used. It is designed to only be
3536 * called once -- all calls after the first will return immediately.
3538 Dygraph
.addAnnotationRule
= function() {
3539 if (Dygraph
.addedAnnotationCSS
) return;
3541 var rule
= "border: 1px solid black; " +
3542 "background-color: white; " +
3543 "text-align: center;";
3545 var styleSheetElement
= document
.createElement("style");
3546 styleSheetElement
.type
= "text/css";
3547 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3549 // Find the first style sheet that we can access.
3550 // We may not add a rule to a style sheet from another domain for security
3551 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3552 // adds its own style sheets from google.com.
3553 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3554 if (document
.styleSheets
[i
].disabled
) continue;
3555 var mysheet
= document
.styleSheets
[i
];
3557 if (mysheet
.insertRule
) { // Firefox
3558 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3559 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3560 } else if (mysheet
.addRule
) { // IE
3561 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3563 Dygraph
.addedAnnotationCSS
= true;
3566 // Was likely a security exception.
3570 this.warn("Unable to add default annotation CSS rule; display may be off.");
3573 // Older pages may still use this name.
3574 var DateGraph
= Dygraph
;