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 // For "production" code, this gets set to false by uglifyjs.
47 // if (typeof(DEBUG) === 'undefined') DEBUG=true;
50 import DygraphLayout from
'./dygraph-layout';
51 import DygraphCanvasRenderer from
'./dygraph-canvas';
52 import DygraphOptions from
'./dygraph-options';
53 import DygraphInteraction from
'./dygraph-interaction-model';
54 import * as DygraphTickers from
'./dygraph-tickers';
55 import * as utils from
'./dygraph-utils';
56 import DEFAULT_ATTRS from
'./dygraph-default-attrs';
58 import DefaultHandler from
'./datahandler/default';
59 import ErrorBarsHandler from
'./datahandler/bars-error';
60 import CustomBarsHandler from
'./datahandler/bars-custom';
61 import DefaultFractionHandler from
'./datahandler/default-fractions';
62 import FractionsBarsHandler from
'./datahandler/bars-fractions';
64 import AnnotationsPlugin from
'./plugins/annotations';
65 import AxesPlugin from
'./plugins/axes';
66 import ChartLabelsPlugin from
'./plugins/chart-labels';
67 import GridPlugin from
'./plugins/grid';
68 import LegendPlugin from
'./plugins/legend';
69 import RangeSelectorPlugin from
'./plugins/range-selector';
71 import GVizChart from
'./dygraph-gviz';
73 /*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */
77 * Creates an interactive, zoomable chart.
80 * @param {div | String} div A div or the id of a div into which to construct
82 * @param {String | Function} file A file containing CSV data or a function
83 * that returns this data. The most basic expected format for each line is
84 * "YYYY/MM/DD,val1,val2,...". For more information, see
85 * http://dygraphs.com/data.html.
86 * @param {Object} attrs Various other attributes, e.g. errorBars determines
87 * whether the input data contains error ranges. For a complete list of
88 * options, see http://dygraphs.com/options.html.
90 var Dygraph
= function(div
, data
, opts
) {
91 this.__init__(div
, data
, opts
);
94 Dygraph
.NAME
= "Dygraph";
95 Dygraph
.VERSION
= "1.1.0";
97 // Various default values
98 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
99 Dygraph
.DEFAULT_WIDTH
= 480;
100 Dygraph
.DEFAULT_HEIGHT
= 320;
102 // For max 60 Hz. animation:
103 Dygraph
.ANIMATION_STEPS
= 12;
104 Dygraph
.ANIMATION_DURATION
= 200;
107 * Standard plotters. These may be used by clients.
108 * Available plotters are:
109 * - Dygraph.Plotters.linePlotter: draws central lines (most common)
110 * - Dygraph.Plotters.errorPlotter: draws error bars
111 * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
113 * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
114 * This causes all the lines to be drawn over all the fills/error bars.
116 Dygraph
.Plotters
= DygraphCanvasRenderer
._Plotters
;
119 // Used for initializing annotation CSS rules only once.
120 Dygraph
.addedAnnotationCSS
= false;
123 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
124 * and context <canvas> inside of it. See the constructor for details.
126 * @param {Element} div the Element to render the graph into.
127 * @param {string | Function} file Source data
128 * @param {Object} attrs Miscellaneous other options
131 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
132 this.is_initial_draw_
= true;
135 // Support two-argument constructor
136 if (attrs
=== null || attrs
=== undefined
) { attrs
= {}; }
138 attrs
= Dygraph
.copyUserAttrs_(attrs
);
140 if (typeof(div
) == 'string') {
141 div
= document
.getElementById(div
);
145 throw new Error('Constructing dygraph with a non-existent div!');
148 // Copy the important bits into the object
149 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
152 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
153 this.previousVerticalX_
= -1;
154 this.fractions_
= attrs
.fractions
|| false;
155 this.dateWindow_
= attrs
.dateWindow
|| null;
157 this.annotations_
= [];
159 // Zoomed indicators - These indicate when the graph has been zoomed and on what axis.
160 this.zoomed_x_
= false;
161 this.zoomed_y_
= false;
163 // Clear the div. This ensure that, if multiple dygraphs are passed the same
164 // div, then only one will be drawn.
167 // For historical reasons, the 'width' and 'height' options trump all CSS
168 // rules _except_ for an explicit 'width' or 'height' on the div.
169 // As an added convenience, if the div has zero height (like <div></div> does
170 // without any styles), then we use a default height/width
.
171 if (div
.style
.width
=== '' && attrs
.width
) {
172 div
.style
.width
= attrs
.width
+ "px";
174 if (div
.style
.height
=== '' && attrs
.height
) {
175 div
.style
.height
= attrs
.height
+ "px";
177 if (div
.style
.height
=== '' && div
.clientHeight
=== 0) {
178 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
179 if (div
.style
.width
=== '') {
180 div
.style
.width
= Dygraph
.DEFAULT_WIDTH
+ "px";
183 // These will be zero if the dygraph's div is hidden. In that case,
184 // use the user-specified attributes if present. If not, use zero
185 // and assume the user will call resize to fix things later.
186 this.width_
= div
.clientWidth
|| attrs
.width
|| 0;
187 this.height_
= div
.clientHeight
|| attrs
.height
|| 0;
189 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
190 if (attrs
.stackedGraph
) {
191 attrs
.fillGraph
= true;
192 // TODO(nikhilk): Add any other stackedGraph checks here.
195 // DEPRECATION WARNING: All option processing should be moved from
196 // attrs_ and user_attrs_ to options_, which holds all this information.
198 // Dygraphs has many options, some of which interact with one another.
199 // To keep track of everything, we maintain two sets of options:
201 // this.user_attrs_ only options explicitly set by the user.
202 // this.attrs_ defaults, options derived from user_attrs_, data.
204 // Options are then accessed this.attr_('attr'), which first looks at
205 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
206 // defaults without overriding behavior that the user specifically asks for.
207 this.user_attrs_
= {};
208 utils
.update(this.user_attrs_
, attrs
);
210 // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
212 utils
.updateDeep(this.attrs_
, DEFAULT_ATTRS
);
214 this.boundaryIds_
= [];
215 this.setIndexByName_
= {};
216 this.datasetIndex_
= [];
218 this.registeredEvents_
= [];
219 this.eventListeners_
= {};
221 this.attributes_
= new DygraphOptions(this);
223 // Create the containing DIV and other interactive elements
224 this.createInterface_();
228 var plugins
= Dygraph
.PLUGINS
.concat(this.getOption('plugins'));
229 for (var i
= 0; i
< plugins
.length
; i
++) {
230 // the plugins option may contain either plugin classes or instances.
231 // Plugin instances contain an activate method.
232 var Plugin
= plugins
[i
]; // either a constructor or an instance.
234 if (typeof(Plugin
.activate
) !== 'undefined') {
235 pluginInstance
= Plugin
;
237 pluginInstance
= new Plugin();
241 plugin
: pluginInstance
,
247 var handlers
= pluginInstance
.activate(this);
248 for (var eventName
in handlers
) {
249 if (!handlers
.hasOwnProperty(eventName
)) continue;
250 // TODO(danvk): validate eventName.
251 pluginDict
.events
[eventName
] = handlers
[eventName
];
254 this.plugins_
.push(pluginDict
);
257 // At this point, plugins can no longer register event handlers.
258 // Construct a map from event -> ordered list of [callback, plugin].
259 for (var i
= 0; i
< this.plugins_
.length
; i
++) {
260 var plugin_dict
= this.plugins_
[i
];
261 for (var eventName
in plugin_dict
.events
) {
262 if (!plugin_dict
.events
.hasOwnProperty(eventName
)) continue;
263 var callback
= plugin_dict
.events
[eventName
];
265 var pair
= [plugin_dict
.plugin
, callback
];
266 if (!(eventName
in this.eventListeners_
)) {
267 this.eventListeners_
[eventName
] = [pair
];
269 this.eventListeners_
[eventName
].push(pair
);
274 this.createDragInterface_();
280 * Triggers a cascade of events to the various plugins which are interested in them.
281 * Returns true if the "default behavior" should be prevented, i.e. if one
282 * of the event listeners called event.preventDefault().
285 Dygraph
.prototype.cascadeEvents_
= function(name
, extra_props
) {
286 if (!(name
in this.eventListeners_
)) return false;
288 // QUESTION: can we use objects & prototypes to speed this up?
292 defaultPrevented
: false,
293 preventDefault
: function() {
294 if (!e
.cancelable
) throw "Cannot call preventDefault on non-cancelable event.";
295 e
.defaultPrevented
= true;
297 propagationStopped
: false,
298 stopPropagation
: function() {
299 e
.propagationStopped
= true;
302 utils
.update(e
, extra_props
);
304 var callback_plugin_pairs
= this.eventListeners_
[name
];
305 if (callback_plugin_pairs
) {
306 for (var i
= callback_plugin_pairs
.length
- 1; i
>= 0; i
--) {
307 var plugin
= callback_plugin_pairs
[i
][0];
308 var callback
= callback_plugin_pairs
[i
][1];
309 callback
.call(plugin
, e
);
310 if (e
.propagationStopped
) break;
313 return e
.defaultPrevented
;
317 * Fetch a plugin instance of a particular class. Only for testing.
319 * @param {!Class} type The type of the plugin.
320 * @return {Object} Instance of the plugin, or null if there is none.
322 Dygraph
.prototype.getPluginInstance_
= function(type
) {
323 for (var i
= 0; i
< this.plugins_
.length
; i
++) {
324 var p
= this.plugins_
[i
];
325 if (p
.plugin
instanceof type
) {
333 * Returns the zoomed status of the chart for one or both axes.
335 * Axis is an optional parameter. Can be set to 'x' or 'y'.
337 * The zoomed status for an axis is set whenever a user zooms using the mouse
338 * or when the dateWindow or valueRange are updated (unless the
339 * isZoomedIgnoreProgrammaticZoom option is also specified).
341 Dygraph
.prototype.isZoomed
= function(axis
) {
342 if (axis
=== null || axis
=== undefined
) {
343 return this.zoomed_x_
|| this.zoomed_y_
;
345 if (axis
=== 'x') return this.zoomed_x_
;
346 if (axis
=== 'y') return this.zoomed_y_
;
347 throw "axis parameter is [" + axis
+ "] must be null, 'x' or 'y'.";
351 * Returns information about the Dygraph object, including its containing ID.
353 Dygraph
.prototype.toString
= function() {
354 var maindiv
= this.maindiv_
;
355 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
;
356 return "[Dygraph " + id
+ "]";
361 * Returns the value of an option. This may be set by the user (either in the
362 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
364 * @param {string} name The name of the option, e.g. 'rollPeriod'.
365 * @param {string} [seriesName] The name of the series to which the option
366 * will be applied. If no per-series value of this option is available, then
367 * the global value is returned. This is optional.
368 * @return { ... } The value of the option.
370 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
372 // if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') {
373 // console.error('Must include options reference JS for testing');
374 // } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) {
375 // console.error('Dygraphs is using property ' + name + ', which has no ' +
376 // 'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
377 // // Only log
this error once
.
378 // Dygraph.OPTIONS_REFERENCE[name] = true;
381 return seriesName
? this.attributes_
.getForSeries(name
, seriesName
) : this.attributes_
.get(name
);
385 * Returns the current value for an option, as set in the constructor or via
386 * updateOptions. You may pass in an (optional) series name to get per-series
387 * values for the option.
389 * All values returned by this method should be considered immutable. If you
390 * modify them, there is no guarantee that the changes will be honored or that
391 * dygraphs will remain in a consistent state. If you want to modify an option,
392 * use updateOptions() instead.
394 * @param {string} name The name of the option (e.g. 'strokeWidth')
395 * @param {string=} opt_seriesName Series name to get per-series values.
396 * @return {*} The value of the option.
398 Dygraph
.prototype.getOption
= function(name
, opt_seriesName
) {
399 return this.attr_(name
, opt_seriesName
);
403 * Like getOption(), but specifically returns a number.
404 * This is a convenience function for working with the Closure Compiler.
405 * @param {string} name The name of the option (e.g. 'strokeWidth')
406 * @param {string=} opt_seriesName Series name to get per-series values.
407 * @return {number} The value of the option.
410 Dygraph
.prototype.getNumericOption
= function(name
, opt_seriesName
) {
411 return /** @type{number} */(this.getOption(name
, opt_seriesName
));
415 * Like getOption(), but specifically returns a string.
416 * This is a convenience function for working with the Closure Compiler.
417 * @param {string} name The name of the option (e.g. 'strokeWidth')
418 * @param {string=} opt_seriesName Series name to get per-series values.
419 * @return {string} The value of the option.
422 Dygraph
.prototype.getStringOption
= function(name
, opt_seriesName
) {
423 return /** @type{string} */(this.getOption(name
, opt_seriesName
));
427 * Like getOption(), but specifically returns a boolean.
428 * This is a convenience function for working with the Closure Compiler.
429 * @param {string} name The name of the option (e.g. 'strokeWidth')
430 * @param {string=} opt_seriesName Series name to get per-series values.
431 * @return {boolean} The value of the option.
434 Dygraph
.prototype.getBooleanOption
= function(name
, opt_seriesName
) {
435 return /** @type{boolean} */(this.getOption(name
, opt_seriesName
));
439 * Like getOption(), but specifically returns a function.
440 * This is a convenience function for working with the Closure Compiler.
441 * @param {string} name The name of the option (e.g. 'strokeWidth')
442 * @param {string=} opt_seriesName Series name to get per-series values.
443 * @return {function(...)} The value of the option.
446 Dygraph
.prototype.getFunctionOption
= function(name
, opt_seriesName
) {
447 return /** @type{function(...)} */(this.getOption(name
, opt_seriesName
));
450 Dygraph
.prototype.getOptionForAxis
= function(name
, axis
) {
451 return this.attributes_
.getForAxis(name
, axis
);
456 * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2')
457 * @return { ... } A function mapping string -> option value
459 Dygraph
.prototype.optionsViewForAxis_
= function(axis
) {
461 return function(opt
) {
462 var axis_opts
= self
.user_attrs_
.axes
;
463 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
].hasOwnProperty(opt
)) {
464 return axis_opts
[axis
][opt
];
467 // I don't like that this is in a second spot.
468 if (axis
=== 'x' && opt
=== 'logscale') {
469 // return the default value.
470 // TODO(konigsberg): pull the default from a global default.
474 // user-specified attributes always trump defaults, even if they're less
476 if (typeof(self
.user_attrs_
[opt
]) != 'undefined') {
477 return self
.user_attrs_
[opt
];
480 axis_opts
= self
.attrs_
.axes
;
481 if (axis_opts
&& axis_opts
[axis
] && axis_opts
[axis
].hasOwnProperty(opt
)) {
482 return axis_opts
[axis
][opt
];
484 // check old-style axis options
485 // TODO(danvk): add a deprecation warning if either of these match.
486 if (axis
== 'y' && self
.axes_
[0].hasOwnProperty(opt
)) {
487 return self
.axes_
[0][opt
];
488 } else if (axis
== 'y2' && self
.axes_
[1].hasOwnProperty(opt
)) {
489 return self
.axes_
[1][opt
];
491 return self
.attr_(opt
);
496 * Returns the current rolling period, as set by the user or an option.
497 * @return {number} The number of points in the rolling window
499 Dygraph
.prototype.rollPeriod
= function() {
500 return this.rollPeriod_
;
504 * Returns the currently-visible x-range. This can be affected by zooming,
505 * panning or a call to updateOptions.
506 * Returns a two-element array: [left, right].
507 * If the Dygraph has dates on the x-axis, these will be millis since epoch.
509 Dygraph
.prototype.xAxisRange
= function() {
510 return this.dateWindow_
? this.dateWindow_
: this.xAxisExtremes();
514 * Returns the lower- and upper-bound x-axis values of the
517 Dygraph
.prototype.xAxisExtremes
= function() {
518 var pad
= this.getNumericOption('xRangePad') / this.plotter_
.area
.w
;
519 if (this.numRows() === 0) {
520 return [0 - pad
, 1 + pad
];
522 var left
= this.rawData_
[0][0];
523 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
525 // Must keep this in sync with dygraph-layout _evaluateLimits()
526 var range
= right
- left
;
528 right
+= range
* pad
;
530 return [left
, right
];
534 * Returns the currently-visible y-range for an axis. This can be affected by
535 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
536 * called with no arguments, returns the range of the first axis.
537 * Returns a two-element array: [bottom, top].
539 Dygraph
.prototype.yAxisRange
= function(idx
) {
540 if (typeof(idx
) == "undefined") idx
= 0;
541 if (idx
< 0 || idx
>= this.axes_
.length
) {
544 var axis
= this.axes_
[idx
];
545 return [ axis
.computedValueRange
[0], axis
.computedValueRange
[1] ];
549 * Returns the currently-visible y-ranges for each axis. This can be affected by
550 * zooming, panning, calls to updateOptions, etc.
551 * Returns an array of [bottom, top] pairs, one for each y-axis.
553 Dygraph
.prototype.yAxisRanges
= function() {
555 for (var i
= 0; i
< this.axes_
.length
; i
++) {
556 ret
.push(this.yAxisRange(i
));
561 // TODO(danvk): use these functions throughout dygraphs.
563 * Convert from data coordinates to canvas/div X/Y coordinates.
564 * If specified, do this conversion for the coordinate system of a particular
565 * axis. Uses the first axis by default.
566 * Returns a two-element array: [X, Y]
568 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
569 * instead of toDomCoords(null, y, axis).
571 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
572 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
576 * Convert from data x coordinates to canvas/div X coordinate.
577 * If specified, do this conversion for the coordinate system of a particular
579 * Returns a single value or null if x is null.
581 Dygraph
.prototype.toDomXCoord
= function(x
) {
586 var area
= this.plotter_
.area
;
587 var xRange
= this.xAxisRange();
588 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
592 * Convert from data x coordinates to canvas/div Y coordinate and optional
593 * axis. Uses the first axis by default.
595 * returns a single value or null if y is null.
597 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
598 var pct
= this.toPercentYCoord(y
, axis
);
603 var area
= this.plotter_
.area
;
604 return area
.y
+ pct
* area
.h
;
608 * Convert from canvas/div coords to data coordinates.
609 * If specified, do this conversion for the coordinate system of a particular
610 * axis. Uses the first axis by default.
611 * Returns a two-element array: [X, Y].
613 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
614 * instead of toDataCoords(null, y, axis).
616 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
617 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
621 * Convert from canvas/div x coordinate to data coordinate.
623 * If x is null, this returns null.
625 Dygraph
.prototype.toDataXCoord
= function(x
) {
630 var area
= this.plotter_
.area
;
631 var xRange
= this.xAxisRange();
633 if (!this.attributes_
.getForAxis("logscale", 'x')) {
634 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
636 // TODO: remove duplicate code?
637 // Computing the inverse of toDomCoord.
638 var pct
= (x
- area
.x
) / area
.w
;
640 // Computing the inverse of toPercentXCoord. The function was arrived at with
641 // the following steps:
643 // Original calcuation:
644 // pct = (log(x) - log(xRange[0])) / (log(xRange
[1]) - log(xRange
[0])));
646 // Multiply both sides by the right-side demoninator.
647 // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0])
649 // add log(xRange[0]) to both sides
650 // log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) = log(x);
652 // Swap both sides of the equation,
653 // log(x) = log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0]))
655 // Use both sides as the exponent in 10^exp and we're done.
656 // x = 10 ^ (log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])))
657 var logr0
= utils
.log10(xRange
[0]);
658 var logr1
= utils
.log10(xRange
[1]);
659 var exponent
= logr0
+ (pct
* (logr1
- logr0
));
660 var value
= Math
.pow(utils
.LOG_SCALE
, exponent
);
666 * Convert from canvas/div y coord to value.
668 * If y is null, this returns null.
669 * if axis is null, this uses the first axis.
671 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
676 var area
= this.plotter_
.area
;
677 var yRange
= this.yAxisRange(axis
);
679 if (typeof(axis
) == "undefined") axis
= 0;
680 if (!this.attributes_
.getForAxis("logscale", axis
)) {
681 return yRange
[0] + (area
.y
+ area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
683 // Computing the inverse of toDomCoord.
684 var pct
= (y
- area
.y
) / area
.h
;
686 // Computing the inverse of toPercentYCoord. The function was arrived at with
687 // the following steps:
689 // Original calcuation:
690 // pct = (log(yRange[1]) - log(y)) / (log(yRange
[1]) - log(yRange
[0]));
692 // Multiply both sides by the right-side demoninator.
693 // pct * (log(yRange[1]) - log(yRange[0])) = log(yRange[1]) - log(y);
695 // subtract log(yRange[1]) from both sides.
696 // (pct * (log(yRange[1]) - log(yRange[0]))) - log(yRange[1]) = -log(y);
698 // and multiply both sides by -1.
699 // log(yRange[1]) - (pct * (logr1 - log(yRange[0])) = log(y);
701 // Swap both sides of the equation,
702 // log(y) = log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0])));
704 // Use both sides as the exponent in 10^exp and we're done.
705 // y = 10 ^ (log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0]))));
706 var logr0
= utils
.log10(yRange
[0]);
707 var logr1
= utils
.log10(yRange
[1]);
708 var exponent
= logr1
- (pct
* (logr1
- logr0
));
709 var value
= Math
.pow(utils
.LOG_SCALE
, exponent
);
715 * Converts a y for an axis to a percentage from the top to the
716 * bottom of the drawing area.
718 * If the coordinate represents a value visible on the canvas, then
719 * the value will be between 0 and 1, where 0 is the top of the canvas.
720 * However, this method will return values outside the range, as
721 * values can fall outside the canvas.
723 * If y is null, this returns null.
724 * if axis is null, this uses the first axis.
726 * @param {number} y The data y-coordinate.
727 * @param {number} [axis] The axis number on which the data coordinate lives.
728 * @return {number} A fraction in [0, 1] where 0 = the top edge.
730 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
734 if (typeof(axis
) == "undefined") axis
= 0;
736 var yRange
= this.yAxisRange(axis
);
739 var logscale
= this.attributes_
.getForAxis("logscale", axis
);
741 var logr0
= utils
.log10(yRange
[0]);
742 var logr1
= utils
.log10(yRange
[1]);
743 pct
= (logr1
- utils
.log10(y
)) / (logr1
- logr0
);
745 // yRange[1] - y is unit distance from the bottom.
746 // yRange[1] - yRange[0] is the scale of the range.
747 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
748 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
754 * Converts an x value to a percentage from the left to the right of
757 * If the coordinate represents a value visible on the canvas, then
758 * the value will be between 0 and 1, where 0 is the left of the canvas.
759 * However, this method will return values outside the range, as
760 * values can fall outside the canvas.
762 * If x is null, this returns null.
763 * @param {number} x The data x-coordinate.
764 * @return {number} A fraction in [0, 1] where 0 = the left edge.
766 Dygraph
.prototype.toPercentXCoord
= function(x
) {
771 var xRange
= this.xAxisRange();
773 var logscale
= this.attributes_
.getForAxis("logscale", 'x') ;
774 if (logscale
=== true) { // logscale can be null so we test for true explicitly.
775 var logr0
= utils
.log10(xRange
[0]);
776 var logr1
= utils
.log10(xRange
[1]);
777 pct
= (utils
.log10(x
) - logr0
) / (logr1
- logr0
);
779 // x - xRange[0] is unit distance from the left.
780 // xRange[1] - xRange[0] is the scale of the range.
781 // The full expression below is the % from the left.
782 pct
= (x
- xRange
[0]) / (xRange
[1] - xRange
[0]);
788 * Returns the number of columns (including the independent variable).
789 * @return {number} The number of columns.
791 Dygraph
.prototype.numColumns
= function() {
792 if (!this.rawData_
) return 0;
793 return this.rawData_
[0] ? this.rawData_
[0].length
: this.attr_("labels").length
;
797 * Returns the number of rows (excluding any header/label row).
798 * @return {number} The number of rows, less any header.
800 Dygraph
.prototype.numRows
= function() {
801 if (!this.rawData_
) return 0;
802 return this.rawData_
.length
;
806 * Returns the value in the given row and column. If the row and column exceed
807 * the bounds on the data, returns null. Also returns null if the value is
809 * @param {number} row The row number of the data (0-based). Row 0 is the
810 * first row of data, not a header row.
811 * @param {number} col The column number of the data (0-based)
812 * @return {number} The value in the specified cell or null if the row/col
815 Dygraph
.prototype.getValue
= function(row
, col
) {
816 if (row
< 0 || row
> this.rawData_
.length
) return null;
817 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
819 return this.rawData_
[row
][col
];
823 * Generates interface elements for the Dygraph: a containing div, a div to
824 * display the current point, and a textbox to adjust the rolling average
825 * period. Also creates the Renderer/Layout elements.
828 Dygraph
.prototype.createInterface_
= function() {
829 // Create the all-enclosing graph div
830 var enclosing
= this.maindiv_
;
832 this.graphDiv
= document
.createElement("div");
834 // TODO(danvk): any other styles that are useful to set here?
835 this.graphDiv
.style
.textAlign
= 'left'; // This is a CSS "reset"
836 this.graphDiv
.style
.position
= 'relative';
837 enclosing
.appendChild(this.graphDiv
);
839 // Create the canvas for interactive parts of the chart.
840 this.canvas_
= utils
.createCanvas();
841 this.canvas_
.style
.position
= "absolute";
843 // ... and for static parts of the chart.
844 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
846 this.canvas_ctx_
= utils
.getContext(this.canvas_
);
847 this.hidden_ctx_
= utils
.getContext(this.hidden_
);
849 this.resizeElements_();
851 // The interactive parts of the graph are drawn on top of the chart.
852 this.graphDiv
.appendChild(this.hidden_
);
853 this.graphDiv
.appendChild(this.canvas_
);
854 this.mouseEventElement_
= this.createMouseEventElement_();
856 // Create the grapher
857 this.layout_
= new DygraphLayout(this);
861 this.mouseMoveHandler_
= function(e
) {
862 dygraph
.mouseMove_(e
);
865 this.mouseOutHandler_
= function(e
) {
866 // The mouse has left the chart if:
867 // 1. e.target is inside the chart
868 // 2. e.relatedTarget is outside the chart
869 var target
= e
.target
|| e
.fromElement
;
870 var relatedTarget
= e
.relatedTarget
|| e
.toElement
;
871 if (utils
.isNodeContainedBy(target
, dygraph
.graphDiv
) &&
872 !utils
.isNodeContainedBy(relatedTarget
, dygraph
.graphDiv
)) {
873 dygraph
.mouseOut_(e
);
877 this.addAndTrackEvent(window
, 'mouseout', this.mouseOutHandler_
);
878 this.addAndTrackEvent(this.mouseEventElement_
, 'mousemove', this.mouseMoveHandler_
);
880 // Don't recreate and register the resize handler on subsequent calls.
881 // This happens when the graph is resized.
882 if (!this.resizeHandler_
) {
883 this.resizeHandler_
= function(e
) {
887 // Update when the window is resized.
888 // TODO(danvk): drop frames depending on complexity of the chart.
889 this.addAndTrackEvent(window
, 'resize', this.resizeHandler_
);
893 Dygraph
.prototype.resizeElements_
= function() {
894 this.graphDiv
.style
.width
= this.width_
+ "px";
895 this.graphDiv
.style
.height
= this.height_
+ "px";
897 var canvasScale
= utils
.getContextPixelRatio(this.canvas_ctx_
);
898 this.canvas_
.width
= this.width_
* canvasScale
;
899 this.canvas_
.height
= this.height_
* canvasScale
;
900 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
901 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
902 if (canvasScale
!== 1) {
903 this.canvas_ctx_
.scale(canvasScale
, canvasScale
);
906 var hiddenScale
= utils
.getContextPixelRatio(this.hidden_ctx_
);
907 this.hidden_
.width
= this.width_
* hiddenScale
;
908 this.hidden_
.height
= this.height_
* hiddenScale
;
909 this.hidden_
.style
.width
= this.width_
+ "px"; // for IE
910 this.hidden_
.style
.height
= this.height_
+ "px"; // for IE
911 if (hiddenScale
!== 1) {
912 this.hidden_ctx_
.scale(hiddenScale
, hiddenScale
);
917 * Detach DOM elements in the dygraph and null out all data references.
918 * Calling this when you're done with a dygraph can dramatically reduce memory
919 * usage. See, e.g., the tests/perf.html example.
921 Dygraph
.prototype.destroy
= function() {
922 this.canvas_ctx_
.restore();
923 this.hidden_ctx_
.restore();
925 // Destroy any plugins, in the reverse order that they were registered.
926 for (var i
= this.plugins_
.length
- 1; i
>= 0; i
--) {
927 var p
= this.plugins_
.pop();
928 if (p
.plugin
.destroy
) p
.plugin
.destroy();
931 var removeRecursive
= function(node
) {
932 while (node
.hasChildNodes()) {
933 removeRecursive(node
.firstChild
);
934 node
.removeChild(node
.firstChild
);
938 this.removeTrackedEvents_();
940 // remove mouse event handlers (This may not be necessary anymore)
941 utils
.removeEvent(window
, 'mouseout', this.mouseOutHandler_
);
942 utils
.removeEvent(this.mouseEventElement_
, 'mousemove', this.mouseMoveHandler_
);
944 // remove window handlers
945 utils
.removeEvent(window
,'resize', this.resizeHandler_
);
946 this.resizeHandler_
= null;
948 removeRecursive(this.maindiv_
);
950 var nullOut
= function(obj
) {
952 if (typeof(obj
[n
]) === 'object') {
957 // These may not all be necessary, but it can't hurt...
958 nullOut(this.layout_
);
959 nullOut(this.plotter_
);
964 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
965 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
966 * or the zoom rectangles) is done on this.canvas_.
967 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
968 * @return {Object} The newly-created canvas
971 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
972 var h
= utils
.createCanvas();
973 h
.style
.position
= "absolute";
974 // TODO(danvk): h should be offset from canvas. canvas needs to include
975 // some extra area to make it easier to zoom in on the far left and far
976 // right. h needs to be precisely the plot area, so that clipping occurs.
977 h
.style
.top
= canvas
.style
.top
;
978 h
.style
.left
= canvas
.style
.left
;
979 h
.width
= this.width_
;
980 h
.height
= this.height_
;
981 h
.style
.width
= this.width_
+ "px"; // for IE
982 h
.style
.height
= this.height_
+ "px"; // for IE
987 * Creates an overlay element used to handle mouse events.
988 * @return {Object} The mouse event element.
991 Dygraph
.prototype.createMouseEventElement_
= function() {
996 * Generate a set of distinct colors for the data series. This is done with a
997 * color wheel. Saturation/Value are customizable, and the hue is
998 * equally-spaced around the color wheel. If a custom set of colors is
999 * specified, that is used instead.
1002 Dygraph
.prototype.setColors_
= function() {
1003 var labels
= this.getLabels();
1004 var num
= labels
.length
- 1;
1006 this.colorsMap_
= {};
1008 // These are used for when no custom colors are specified.
1009 var sat
= this.getNumericOption('colorSaturation') || 1.0;
1010 var val
= this.getNumericOption('colorValue') || 0.5;
1011 var half
= Math
.ceil(num
/ 2);
1013 var colors
= this.getOption('colors');
1014 var visibility
= this.visibility();
1015 for (var i
= 0; i
< num
; i
++) {
1016 if (!visibility
[i
]) {
1019 var label
= labels
[i
+ 1];
1020 var colorStr
= this.attributes_
.getForSeries('color', label
);
1023 colorStr
= colors
[i
% colors
.length
];
1025 // alternate colors for high contrast.
1026 var idx
= i
% 2 ? (half
+ (i
+ 1)/ 2) : Math.ceil((i + 1) / 2);
1027 var hue
= (1.0 * idx
/ (1 + num
));
1028 colorStr
= utils
.hsvToRGB(hue
, sat
, val
);
1031 this.colors_
.push(colorStr
);
1032 this.colorsMap_
[label
] = colorStr
;
1037 * Return the list of colors. This is either the list of colors passed in the
1038 * attributes or the autogenerated list of rgb(r,g,b) strings.
1039 * This does not return colors for invisible series.
1040 * @return {Array.<string>} The list of colors.
1042 Dygraph
.prototype.getColors
= function() {
1043 return this.colors_
;
1047 * Returns a few attributes of a series, i.e. its color, its visibility, which
1048 * axis it's assigned to, and its column in the original data.
1049 * Returns null if the series does not exist.
1050 * Otherwise, returns an object with column, visibility, color and axis properties.
1051 * The "axis" property will be set to 1 for y1 and 2 for y2.
1052 * The "column" property can be fed back into getValue(row, column) to get
1053 * values for this series.
1055 Dygraph
.prototype.getPropertiesForSeries
= function(series_name
) {
1057 var labels
= this.getLabels();
1058 for (var i
= 1; i
< labels
.length
; i
++) {
1059 if (labels
[i
] == series_name
) {
1064 if (idx
== -1) return null;
1069 visible
: this.visibility()[idx
- 1],
1070 color
: this.colorsMap_
[series_name
],
1071 axis
: 1 + this.attributes_
.axisForSeries(series_name
)
1076 * Create the text box to adjust the averaging period
1079 Dygraph
.prototype.createRollInterface_
= function() {
1080 // Create a roller if one doesn't exist already.
1081 if (!this.roller_
) {
1082 this.roller_
= document
.createElement("input");
1083 this.roller_
.type
= "text";
1084 this.roller_
.style
.display
= "none";
1085 this.graphDiv
.appendChild(this.roller_
);
1088 var display
= this.getBooleanOption('showRoller') ? 'block' : 'none';
1090 var area
= this.plotter_
.area
;
1091 var textAttr
= { "position": "absolute",
1093 "top": (area
.y
+ area
.h
- 25) + "px",
1094 "left": (area
.x
+ 1) + "px",
1097 this.roller_
.size
= "2";
1098 this.roller_
.value
= this.rollPeriod_
;
1099 for (var name
in textAttr
) {
1100 if (textAttr
.hasOwnProperty(name
)) {
1101 this.roller_
.style
[name
] = textAttr
[name
];
1106 this.roller_
.onchange
= function() { dygraph
.adjustRoll(dygraph
.roller_
.value
); };
1110 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1114 Dygraph
.prototype.createDragInterface_
= function() {
1116 // Tracks whether the mouse is down right now
1118 isPanning
: false, // is this drag part of a pan?
1119 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1120 dragStartX
: null, // pixel coordinates
1121 dragStartY
: null, // pixel coordinates
1122 dragEndX
: null, // pixel coordinates
1123 dragEndY
: null, // pixel coordinates
1124 dragDirection
: null,
1125 prevEndX
: null, // pixel coordinates
1126 prevEndY
: null, // pixel coordinates
1127 prevDragDirection
: null,
1128 cancelNextDblclick
: false, // see comment in dygraph-interaction-model.js
1130 // The value on the left side of the graph when a pan operation starts.
1131 initialLeftmostDate
: null,
1133 // The number of units each pixel spans. (This won't be valid for log
1135 xUnitsPerPixel
: null,
1137 // TODO(danvk): update this comment
1138 // The range in second/value units that the viewport encompasses during a
1139 // panning operation.
1142 // Top-left corner of the canvas, in DOM coords
1143 // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1147 // Values for use with panEdgeFraction, which limit how far outside the
1148 // graph's data boundaries it can be panned.
1149 boundedDates
: null, // [minDate, maxDate]
1150 boundedValues
: null, // [[minValue, maxValue] ...]
1152 // We cover iframes during mouse interactions. See comments in
1153 // dygraph-utils.js for more info on why this is a good idea.
1154 tarp
: new utils
.IFrameTarp(),
1156 // contextB is the same thing as this context object but renamed.
1157 initializeMouseDown
: function(event
, g
, contextB
) {
1158 // prevents mouse drags from selecting page text.
1159 if (event
.preventDefault
) {
1160 event
.preventDefault(); // Firefox, Chrome, etc.
1162 event
.returnValue
= false; // IE
1163 event
.cancelBubble
= true;
1166 var canvasPos
= utils
.findPos(g
.canvas_
);
1167 contextB
.px
= canvasPos
.x
;
1168 contextB
.py
= canvasPos
.y
;
1169 contextB
.dragStartX
= utils
.dragGetX_(event
, contextB
);
1170 contextB
.dragStartY
= utils
.dragGetY_(event
, contextB
);
1171 contextB
.cancelNextDblclick
= false;
1172 contextB
.tarp
.cover();
1174 destroy
: function() {
1176 if (context
.isZooming
|| context
.isPanning
) {
1177 context
.isZooming
= false;
1178 context
.dragStartX
= null;
1179 context
.dragStartY
= null;
1182 if (context
.isPanning
) {
1183 context
.isPanning
= false;
1184 context
.draggingDate
= null;
1185 context
.dateRange
= null;
1186 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1187 delete self
.axes_
[i
].draggingValue
;
1188 delete self
.axes_
[i
].dragValueRange
;
1192 context
.tarp
.uncover();
1196 var interactionModel
= this.getOption("interactionModel");
1198 // Self is the graph.
1201 // Function that binds the graph and context to the handler.
1202 var bindHandler
= function(handler
) {
1203 return function(event
) {
1204 handler(event
, self
, context
);
1208 for (var eventName
in interactionModel
) {
1209 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1210 this.addAndTrackEvent(this.mouseEventElement_
, eventName
,
1211 bindHandler(interactionModel
[eventName
]));
1214 // If the user releases the mouse button during a drag, but not over the
1215 // canvas, then it doesn't count as a zooming action.
1216 if (!interactionModel
.willDestroyContextMyself
) {
1217 var mouseUpHandler
= function(event
) {
1221 this.addAndTrackEvent(document
, 'mouseup', mouseUpHandler
);
1226 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1227 * up any previous zoom rectangles that were drawn. This could be optimized to
1228 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1231 * @param {number} direction the direction of the zoom rectangle. Acceptable
1232 * values are utils.HORIZONTAL and utils.VERTICAL.
1233 * @param {number} startX The X position where the drag started, in canvas
1235 * @param {number} endX The current X position of the drag, in canvas coords.
1236 * @param {number} startY The Y position where the drag started, in canvas
1238 * @param {number} endY The current Y position of the drag, in canvas coords.
1239 * @param {number} prevDirection the value of direction on the previous call to
1240 * this function. Used to avoid excess redrawing
1241 * @param {number} prevEndX The value of endX on the previous call to this
1242 * function. Used to avoid excess redrawing
1243 * @param {number} prevEndY The value of endY on the previous call to this
1244 * function. Used to avoid excess redrawing
1247 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
,
1248 endY
, prevDirection
, prevEndX
,
1250 var ctx
= this.canvas_ctx_
;
1252 // Clean up from the previous rect if necessary
1253 if (prevDirection
== utils
.HORIZONTAL
) {
1254 ctx
.clearRect(Math
.min(startX
, prevEndX
), this.layout_
.getPlotArea().y
,
1255 Math
.abs(startX
- prevEndX
), this.layout_
.getPlotArea().h
);
1256 } else if (prevDirection
== utils
.VERTICAL
) {
1257 ctx
.clearRect(this.layout_
.getPlotArea().x
, Math
.min(startY
, prevEndY
),
1258 this.layout_
.getPlotArea().w
, Math
.abs(startY
- prevEndY
));
1261 // Draw a light-grey rectangle to show the new viewing area
1262 if (direction
== utils
.HORIZONTAL
) {
1263 if (endX
&& startX
) {
1264 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1265 ctx
.fillRect(Math
.min(startX
, endX
), this.layout_
.getPlotArea().y
,
1266 Math
.abs(endX
- startX
), this.layout_
.getPlotArea().h
);
1268 } else if (direction
== utils
.VERTICAL
) {
1269 if (endY
&& startY
) {
1270 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1271 ctx
.fillRect(this.layout_
.getPlotArea().x
, Math
.min(startY
, endY
),
1272 this.layout_
.getPlotArea().w
, Math
.abs(endY
- startY
));
1278 * Clear the zoom rectangle (and perform no zoom).
1281 Dygraph
.prototype.clearZoomRect_
= function() {
1282 this.currentZoomRectArgs_
= null;
1283 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1287 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1288 * the canvas. The exact zoom window may be slightly larger if there are no data
1289 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1290 * which accepts dates that match the raw data. This function redraws the graph.
1292 * @param {number} lowX The leftmost pixel value that should be visible.
1293 * @param {number} highX The rightmost pixel value that should be visible.
1296 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1297 this.currentZoomRectArgs_
= null;
1298 // Find the earliest and latest dates contained in this canvasx range.
1299 // Convert the call to date ranges of the raw data.
1300 var minDate
= this.toDataXCoord(lowX
);
1301 var maxDate
= this.toDataXCoord(highX
);
1302 this.doZoomXDates_(minDate
, maxDate
);
1306 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1307 * method with doZoomX which accepts pixel coordinates. This function redraws
1310 * @param {number} minDate The minimum date that should be visible.
1311 * @param {number} maxDate The maximum date that should be visible.
1314 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1315 // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation
1316 // can produce strange effects. Rather than the x-axis transitioning slowly
1317 // between values, it can jerk around.)
1318 var old_window
= this.xAxisRange();
1319 var new_window
= [minDate
, maxDate
];
1320 this.zoomed_x_
= true;
1322 this.doAnimatedZoom(old_window
, new_window
, null, null, function() {
1323 if (that
.getFunctionOption("zoomCallback")) {
1324 that
.getFunctionOption("zoomCallback").call(that
,
1325 minDate
, maxDate
, that
.yAxisRanges());
1331 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1332 * the canvas. This function redraws the graph.
1334 * @param {number} lowY The topmost pixel value that should be visible.
1335 * @param {number} highY The lowest pixel value that should be visible.
1338 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1339 this.currentZoomRectArgs_
= null;
1340 // Find the highest and lowest values in pixel range for each axis.
1341 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1342 // This is because pixels increase as you go down on the screen, whereas data
1343 // coordinates increase as you go up the screen.
1344 var oldValueRanges
= this.yAxisRanges();
1345 var newValueRanges
= [];
1346 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1347 var hi
= this.toDataYCoord(lowY
, i
);
1348 var low
= this.toDataYCoord(highY
, i
);
1349 newValueRanges
.push([low
, hi
]);
1352 this.zoomed_y_
= true;
1354 this.doAnimatedZoom(null, null, oldValueRanges
, newValueRanges
, function() {
1355 if (that
.getFunctionOption("zoomCallback")) {
1356 var xRange
= that
.xAxisRange();
1357 that
.getFunctionOption("zoomCallback").call(that
,
1358 xRange
[0], xRange
[1], that
.yAxisRanges());
1364 * Transition function to use in animations. Returns values between 0.0
1365 * (totally old values) and 1.0 (totally new values) for each frame.
1368 Dygraph
.zoomAnimationFunction
= function(frame
, numFrames
) {
1370 return (1.0 - Math
.pow(k
, -frame
)) / (1.0 - Math
.pow(k
, -numFrames
));
1374 * Reset the zoom to the original view coordinates. This is the same as
1375 * double-clicking on the graph.
1377 Dygraph
.prototype.resetZoom
= function() {
1378 var dirty
= false, dirtyX
= false, dirtyY
= false;
1379 if (this.dateWindow_
!== null) {
1384 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1385 if (typeof(this.axes_
[i
].valueWindow
) !== 'undefined' && this.axes_
[i
].valueWindow
!== null) {
1391 // Clear any selection, since it's likely to be drawn in the wrong place.
1392 this.clearSelection();
1395 this.zoomed_x_
= false;
1396 this.zoomed_y_
= false;
1398 //calculate extremes to avoid lack of padding on reset.
1399 var extremes
= this.xAxisExtremes();
1400 var minDate
= extremes
[0],
1401 maxDate
= extremes
[1];
1403 // TODO(danvk): merge this block w/ the code below
.
1404 if (!this.getBooleanOption("animatedZooms")) {
1405 this.dateWindow_
= null;
1406 for (i
= 0; i
< this.axes_
.length
; i
++) {
1407 if (this.axes_
[i
].valueWindow
!== null) {
1408 delete this.axes_
[i
].valueWindow
;
1412 if (this.getFunctionOption("zoomCallback")) {
1413 this.getFunctionOption("zoomCallback").call(this,
1414 minDate
, maxDate
, this.yAxisRanges());
1419 var oldWindow
=null, newWindow
=null, oldValueRanges
=null, newValueRanges
=null;
1421 oldWindow
= this.xAxisRange();
1422 newWindow
= [minDate
, maxDate
];
1426 oldValueRanges
= this.yAxisRanges();
1427 // TODO(danvk): this is pretty inefficient
1428 var packed
= this.gatherDatasets_(this.rolledSeries_
, null);
1429 var extremes
= packed
.extremes
;
1431 // this has the side-effect of modifying this.axes_.
1432 // this doesn't make much sense in this context, but it's convenient (we
1433 // need this.axes_[*].extremeValues) and not harmful since we'll be
1434 // calling drawGraph_ shortly, which clobbers these values.
1435 this.computeYAxisRanges_(extremes
);
1437 newValueRanges
= [];
1438 for (i
= 0; i
< this.axes_
.length
; i
++) {
1439 var axis
= this.axes_
[i
];
1440 newValueRanges
.push((axis
.valueRange
!== null &&
1441 axis
.valueRange
!== undefined
) ?
1442 axis
.valueRange
: axis
.extremeRange
);
1447 this.doAnimatedZoom(oldWindow
, newWindow
, oldValueRanges
, newValueRanges
,
1449 that
.dateWindow_
= null;
1450 for (var i
= 0; i
< that
.axes_
.length
; i
++) {
1451 if (that
.axes_
[i
].valueWindow
!== null) {
1452 delete that
.axes_
[i
].valueWindow
;
1455 if (that
.getFunctionOption("zoomCallback")) {
1456 that
.getFunctionOption("zoomCallback").call(that
,
1457 minDate
, maxDate
, that
.yAxisRanges());
1464 * Combined animation logic for all zoom functions.
1465 * either the x parameters or y parameters may be null.
1468 Dygraph
.prototype.doAnimatedZoom
= function(oldXRange
, newXRange
, oldYRanges
, newYRanges
, callback
) {
1469 var steps
= this.getBooleanOption("animatedZooms") ?
1470 Dygraph
.ANIMATION_STEPS
: 1;
1473 var valueRanges
= [];
1476 if (oldXRange
!== null && newXRange
!== null) {
1477 for (step
= 1; step
<= steps
; step
++) {
1478 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1479 windows
[step
-1] = [oldXRange
[0]*(1-frac
) + frac
*newXRange
[0],
1480 oldXRange
[1]*(1-frac
) + frac
*newXRange
[1]];
1484 if (oldYRanges
!== null && newYRanges
!== null) {
1485 for (step
= 1; step
<= steps
; step
++) {
1486 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1488 for (var j
= 0; j
< this.axes_
.length
; j
++) {
1489 thisRange
.push([oldYRanges
[j
][0]*(1-frac
) + frac
*newYRanges
[j
][0],
1490 oldYRanges
[j
][1]*(1-frac
) + frac
*newYRanges
[j
][1]]);
1492 valueRanges
[step
-1] = thisRange
;
1497 utils
.repeatAndCleanup(function(step
) {
1498 if (valueRanges
.length
) {
1499 for (var i
= 0; i
< that
.axes_
.length
; i
++) {
1500 var w
= valueRanges
[step
][i
];
1501 that
.axes_
[i
].valueWindow
= [w
[0], w
[1]];
1504 if (windows
.length
) {
1505 that
.dateWindow_
= windows
[step
];
1508 }, steps
, Dygraph
.ANIMATION_DURATION
/ steps
, callback
);
1512 * Get the current graph's area object.
1514 * Returns: {x, y, w, h}
1516 Dygraph
.prototype.getArea
= function() {
1517 return this.plotter_
.area
;
1521 * Convert a mouse event to DOM coordinates relative to the graph origin.
1523 * Returns a two-element array: [X, Y].
1525 Dygraph
.prototype.eventToDomCoords
= function(event
) {
1526 if (event
.offsetX
&& event
.offsetY
) {
1527 return [ event
.offsetX
, event
.offsetY
];
1529 var eventElementPos
= utils
.findPos(this.mouseEventElement_
);
1530 var canvasx
= utils
.pageX(event
) - eventElementPos
.x
;
1531 var canvasy
= utils
.pageY(event
) - eventElementPos
.y
;
1532 return [canvasx
, canvasy
];
1537 * Given a canvas X coordinate, find the closest row.
1538 * @param {number} domX graph-relative DOM X coordinate
1539 * Returns {number} row number.
1542 Dygraph
.prototype.findClosestRow
= function(domX
) {
1543 var minDistX
= Infinity
;
1544 var closestRow
= -1;
1545 var sets
= this.layout_
.points
;
1546 for (var i
= 0; i
< sets
.length
; i
++) {
1547 var points
= sets
[i
];
1548 var len
= points
.length
;
1549 for (var j
= 0; j
< len
; j
++) {
1550 var point
= points
[j
];
1551 if (!utils
.isValidPoint(point
, true)) continue;
1552 var dist
= Math
.abs(point
.canvasx
- domX
);
1553 if (dist
< minDistX
) {
1555 closestRow
= point
.idx
;
1564 * Given canvas X,Y coordinates, find the closest point.
1566 * This finds the individual data point across all visible series
1567 * that's closest to the supplied DOM coordinates using the standard
1568 * Euclidean X,Y distance.
1570 * @param {number} domX graph-relative DOM X coordinate
1571 * @param {number} domY graph-relative DOM Y coordinate
1572 * Returns: {row, seriesName, point}
1575 Dygraph
.prototype.findClosestPoint
= function(domX
, domY
) {
1576 var minDist
= Infinity
;
1577 var dist
, dx
, dy
, point
, closestPoint
, closestSeries
, closestRow
;
1578 for ( var setIdx
= this.layout_
.points
.length
- 1 ; setIdx
>= 0 ; --setIdx
) {
1579 var points
= this.layout_
.points
[setIdx
];
1580 for (var i
= 0; i
< points
.length
; ++i
) {
1582 if (!utils
.isValidPoint(point
)) continue;
1583 dx
= point
.canvasx
- domX
;
1584 dy
= point
.canvasy
- domY
;
1585 dist
= dx
* dx
+ dy
* dy
;
1586 if (dist
< minDist
) {
1588 closestPoint
= point
;
1589 closestSeries
= setIdx
;
1590 closestRow
= point
.idx
;
1594 var name
= this.layout_
.setNames
[closestSeries
];
1603 * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1605 * This first finds the X data point closest to the supplied DOM X coordinate,
1606 * then finds the series which puts the Y coordinate on top of its filled area,
1607 * using linear interpolation between adjacent point pairs.
1609 * @param {number} domX graph-relative DOM X coordinate
1610 * @param {number} domY graph-relative DOM Y coordinate
1611 * Returns: {row, seriesName, point}
1614 Dygraph
.prototype.findStackedPoint
= function(domX
, domY
) {
1615 var row
= this.findClosestRow(domX
);
1616 var closestPoint
, closestSeries
;
1617 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; ++setIdx
) {
1618 var boundary
= this.getLeftBoundary_(setIdx
);
1619 var rowIdx
= row
- boundary
;
1620 var points
= this.layout_
.points
[setIdx
];
1621 if (rowIdx
>= points
.length
) continue;
1622 var p1
= points
[rowIdx
];
1623 if (!utils
.isValidPoint(p1
)) continue;
1624 var py
= p1
.canvasy
;
1625 if (domX
> p1
.canvasx
&& rowIdx
+ 1 < points
.length
) {
1626 // interpolate series Y value using next point
1627 var p2
= points
[rowIdx
+ 1];
1628 if (utils
.isValidPoint(p2
)) {
1629 var dx
= p2
.canvasx
- p1
.canvasx
;
1631 var r
= (domX
- p1
.canvasx
) / dx
;
1632 py
+= r
* (p2
.canvasy
- p1
.canvasy
);
1635 } else if (domX
< p1
.canvasx
&& rowIdx
> 0) {
1636 // interpolate series Y value using previous point
1637 var p0
= points
[rowIdx
- 1];
1638 if (utils
.isValidPoint(p0
)) {
1639 var dx
= p1
.canvasx
- p0
.canvasx
;
1641 var r
= (p1
.canvasx
- domX
) / dx
;
1642 py
+= r
* (p0
.canvasy
- p1
.canvasy
);
1646 // Stop if the point (domX, py) is above this series' upper edge
1647 if (setIdx
=== 0 || py
< domY
) {
1649 closestSeries
= setIdx
;
1652 var name
= this.layout_
.setNames
[closestSeries
];
1661 * When the mouse moves in the canvas, display information about a nearby data
1662 * point and draw dots over those points in the data series. This function
1663 * takes care of cleanup of previously-drawn dots.
1664 * @param {Object} event The mousemove event from the browser.
1667 Dygraph
.prototype.mouseMove_
= function(event
) {
1668 // This prevents JS errors when mousing over the canvas before data loads.
1669 var points
= this.layout_
.points
;
1670 if (points
=== undefined
|| points
=== null) return;
1672 var canvasCoords
= this.eventToDomCoords(event
);
1673 var canvasx
= canvasCoords
[0];
1674 var canvasy
= canvasCoords
[1];
1676 var highlightSeriesOpts
= this.getOption("highlightSeriesOpts");
1677 var selectionChanged
= false;
1678 if (highlightSeriesOpts
&& !this.isSeriesLocked()) {
1680 if (this.getBooleanOption("stackedGraph")) {
1681 closest
= this.findStackedPoint(canvasx
, canvasy
);
1683 closest
= this.findClosestPoint(canvasx
, canvasy
);
1685 selectionChanged
= this.setSelection(closest
.row
, closest
.seriesName
);
1687 var idx
= this.findClosestRow(canvasx
);
1688 selectionChanged
= this.setSelection(idx
);
1691 var callback
= this.getFunctionOption("highlightCallback");
1692 if (callback
&& selectionChanged
) {
1693 callback
.call(this, event
,
1697 this.highlightSet_
);
1702 * Fetch left offset from the specified set index or if not passed, the
1703 * first defined boundaryIds record (see bug #236).
1706 Dygraph
.prototype.getLeftBoundary_
= function(setIdx
) {
1707 if (this.boundaryIds_
[setIdx
]) {
1708 return this.boundaryIds_
[setIdx
][0];
1710 for (var i
= 0; i
< this.boundaryIds_
.length
; i
++) {
1711 if (this.boundaryIds_
[i
] !== undefined
) {
1712 return this.boundaryIds_
[i
][0];
1719 Dygraph
.prototype.animateSelection_
= function(direction
) {
1720 var totalSteps
= 10;
1722 if (this.fadeLevel
=== undefined
) this.fadeLevel
= 0;
1723 if (this.animateId
=== undefined
) this.animateId
= 0;
1724 var start
= this.fadeLevel
;
1725 var steps
= direction
< 0 ? start
: totalSteps
- start
;
1727 if (this.fadeLevel
) {
1728 this.updateSelection_(1.0);
1733 var thisId
= ++this.animateId
;
1735 var cleanupIfClearing
= function() {
1736 // if we haven't reached fadeLevel 0 in the max frame time,
1737 // ensure that the clear happens and just go to 0
1738 if (that
.fadeLevel
!== 0 && direction
< 0) {
1740 that
.clearSelection();
1743 utils
.repeatAndCleanup(
1745 // ignore simultaneous animations
1746 if (that
.animateId
!= thisId
) return;
1748 that
.fadeLevel
+= direction
;
1749 if (that
.fadeLevel
=== 0) {
1750 that
.clearSelection();
1752 that
.updateSelection_(that
.fadeLevel
/ totalSteps
);
1755 steps
, millis
, cleanupIfClearing
);
1759 * Draw dots over the selectied points in the data series. This function
1760 * takes care of cleanup of previously-drawn dots.
1763 Dygraph
.prototype.updateSelection_
= function(opt_animFraction
) {
1764 /*var defaultPrevented = */
1765 this.cascadeEvents_('select', {
1766 selectedRow
: this.lastRow_
,
1767 selectedX
: this.lastx_
,
1768 selectedPoints
: this.selPoints_
1770 // TODO(danvk): use defaultPrevented here?
1772 // Clear the previously drawn vertical, if there is one
1774 var ctx
= this.canvas_ctx_
;
1775 if (this.getOption('highlightSeriesOpts')) {
1776 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1777 var alpha
= 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
1779 // Activating background fade includes an animation effect for a gradual
1780 // fade. TODO(klausw): make this independently configurable if it causes
1781 // issues? Use a shared preference to control animations?
1782 var animateBackgroundFade
= true;
1783 if (animateBackgroundFade
) {
1784 if (opt_animFraction
=== undefined
) {
1785 // start a new animation
1786 this.animateSelection_(1);
1789 alpha
*= opt_animFraction
;
1791 ctx
.fillStyle
= 'rgba(255,255,255,' + alpha
+ ')';
1792 ctx
.fillRect(0, 0, this.width_
, this.height_
);
1795 // Redraw only the highlighted series in the interactive canvas (not the
1796 // static plot canvas, which is where series are usually drawn).
1797 this.plotter_
._renderLineChart(this.highlightSet_
, ctx
);
1798 } else if (this.previousVerticalX_
>= 0) {
1799 // Determine the maximum highlight circle size.
1800 var maxCircleSize
= 0;
1801 var labels
= this.attr_('labels');
1802 for (i
= 1; i
< labels
.length
; i
++) {
1803 var r
= this.getNumericOption('highlightCircleSize', labels
[i
]);
1804 if (r
> maxCircleSize
) maxCircleSize
= r
;
1806 var px
= this.previousVerticalX_
;
1807 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1808 2 * maxCircleSize
+ 2, this.height_
);
1811 if (this.selPoints_
.length
> 0) {
1812 // Draw colored circles over the center of each selected point
1813 var canvasx
= this.selPoints_
[0].canvasx
;
1815 for (i
= 0; i
< this.selPoints_
.length
; i
++) {
1816 var pt
= this.selPoints_
[i
];
1817 if (!utils
.isOK(pt
.canvasy
)) continue;
1819 var circleSize
= this.getNumericOption('highlightCircleSize', pt
.name
);
1820 var callback
= this.getFunctionOption("drawHighlightPointCallback", pt
.name
);
1821 var color
= this.plotter_
.colors
[pt
.name
];
1823 callback
= utils
.Circles
.DEFAULT
;
1825 ctx
.lineWidth
= this.getNumericOption('strokeWidth', pt
.name
);
1826 ctx
.strokeStyle
= color
;
1827 ctx
.fillStyle
= color
;
1828 callback
.call(this, this, pt
.name
, ctx
, canvasx
, pt
.canvasy
,
1829 color
, circleSize
, pt
.idx
);
1833 this.previousVerticalX_
= canvasx
;
1838 * Manually set the selected points and display information about them in the
1839 * legend. The selection can be cleared using clearSelection() and queried
1840 * using getSelection().
1841 * @param {number} row Row number that should be highlighted (i.e. appear with
1842 * hover dots on the chart).
1843 * @param {seriesName} optional series name to highlight that series with the
1844 * the highlightSeriesOpts setting.
1845 * @param { locked } optional If true, keep seriesName selected when mousing
1846 * over the graph, disabling closest-series highlighting. Call clearSelection()
1849 Dygraph
.prototype.setSelection
= function(row
, opt_seriesName
, opt_locked
) {
1850 // Extract the points we've selected
1851 this.selPoints_
= [];
1853 var changed
= false;
1854 if (row
!== false && row
>= 0) {
1855 if (row
!= this.lastRow_
) changed
= true;
1856 this.lastRow_
= row
;
1857 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; ++setIdx
) {
1858 var points
= this.layout_
.points
[setIdx
];
1859 // Check if the point at the appropriate index is the point we're looking
1860 // for. If it is, just use it, otherwise search the array for a point
1861 // in the proper place.
1862 var setRow
= row
- this.getLeftBoundary_(setIdx
);
1863 if (setRow
< points
.length
&& points
[setRow
].idx
== row
) {
1864 var point
= points
[setRow
];
1865 if (point
.yval
!== null) this.selPoints_
.push(point
);
1867 for (var pointIdx
= 0; pointIdx
< points
.length
; ++pointIdx
) {
1868 var point
= points
[pointIdx
];
1869 if (point
.idx
== row
) {
1870 if (point
.yval
!== null) {
1871 this.selPoints_
.push(point
);
1879 if (this.lastRow_
>= 0) changed
= true;
1883 if (this.selPoints_
.length
) {
1884 this.lastx_
= this.selPoints_
[0].xval
;
1889 if (opt_seriesName
!== undefined
) {
1890 if (this.highlightSet_
!== opt_seriesName
) changed
= true;
1891 this.highlightSet_
= opt_seriesName
;
1894 if (opt_locked
!== undefined
) {
1895 this.lockedSet_
= opt_locked
;
1899 this.updateSelection_(undefined
);
1905 * The mouse has left the canvas. Clear out whatever artifacts remain
1906 * @param {Object} event the mouseout event from the browser.
1909 Dygraph
.prototype.mouseOut_
= function(event
) {
1910 if (this.getFunctionOption("unhighlightCallback")) {
1911 this.getFunctionOption("unhighlightCallback").call(this, event
);
1914 if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_
) {
1915 this.clearSelection();
1920 * Clears the current selection (i.e. points that were highlighted by moving
1921 * the mouse over the chart).
1923 Dygraph
.prototype.clearSelection
= function() {
1924 this.cascadeEvents_('deselect', {});
1926 this.lockedSet_
= false;
1927 // Get rid of the overlay data
1928 if (this.fadeLevel
) {
1929 this.animateSelection_(-1);
1932 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1934 this.selPoints_
= [];
1937 this.highlightSet_
= null;
1941 * Returns the number of the currently selected row. To get data for this row,
1942 * you can use the getValue method.
1943 * @return {number} row number, or -1 if nothing is selected
1945 Dygraph
.prototype.getSelection
= function() {
1946 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1950 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; setIdx
++) {
1951 var points
= this.layout_
.points
[setIdx
];
1952 for (var row
= 0; row
< points
.length
; row
++) {
1953 if (points
[row
].x
== this.selPoints_
[0].x
) {
1954 return points
[row
].idx
;
1962 * Returns the name of the currently-highlighted series.
1963 * Only available when the highlightSeriesOpts option is in use.
1965 Dygraph
.prototype.getHighlightSeries
= function() {
1966 return this.highlightSet_
;
1970 * Returns true if the currently-highlighted series was locked
1971 * via setSelection(..., seriesName, true).
1973 Dygraph
.prototype.isSeriesLocked
= function() {
1974 return this.lockedSet_
;
1978 * Fires when there's data available to be graphed.
1979 * @param {string} data Raw CSV data to be plotted
1982 Dygraph
.prototype.loadedEvent_
= function(data
) {
1983 this.rawData_
= this.parseCSV_(data
);
1984 this.cascadeDataDidUpdateEvent_();
1989 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1992 Dygraph
.prototype.addXTicks_
= function() {
1993 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1995 if (this.dateWindow_
) {
1996 range
= [this.dateWindow_
[0], this.dateWindow_
[1]];
1998 range
= this.xAxisExtremes();
2001 var xAxisOptionsView
= this.optionsViewForAxis_('x');
2002 var xTicks
= xAxisOptionsView('ticker')(
2005 this.plotter_
.area
.w
, // TODO(danvk): should be area.width
2008 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
2009 // console.log(msg);
2010 this.layout_
.setXTicks(xTicks
);
2014 * Returns the correct handler class for the currently set options.
2017 Dygraph
.prototype.getHandlerClass_
= function() {
2019 if (this.attr_('dataHandler')) {
2020 handlerClass
= this.attr_('dataHandler');
2021 } else if (this.fractions_
) {
2022 if (this.getBooleanOption('errorBars')) {
2023 handlerClass
= FractionsBarsHandler
;
2025 handlerClass
= DefaultFractionHandler
;
2027 } else if (this.getBooleanOption('customBars')) {
2028 handlerClass
= CustomBarsHandler
;
2029 } else if (this.getBooleanOption('errorBars')) {
2030 handlerClass
= ErrorBarsHandler
;
2032 handlerClass
= DefaultHandler
;
2034 return handlerClass
;
2039 * This function is called once when the chart's data is changed or the options
2040 * dictionary is updated. It is _not_ called when the user pans or zooms. The
2041 * idea is that values derived from the chart's data can be computed here,
2042 * rather than every time the chart is drawn. This includes things like the
2043 * number of axes, rolling averages, etc.
2045 Dygraph
.prototype.predraw_
= function() {
2046 var start
= new Date();
2048 // Create the correct dataHandler
2049 this.dataHandler_
= new (this.getHandlerClass_())();
2051 this.layout_
.computePlotArea();
2053 // TODO(danvk): move more computations out of drawGraph_ and into here.
2054 this.computeYAxes_();
2056 if (!this.is_initial_draw_
) {
2057 this.canvas_ctx_
.restore();
2058 this.hidden_ctx_
.restore();
2061 this.canvas_ctx_
.save();
2062 this.hidden_ctx_
.save();
2064 // Create a new plotter.
2065 this.plotter_
= new DygraphCanvasRenderer(this,
2070 // The roller sits in the bottom left corner of the chart. We don't know where
2071 // this will be until the options are available, so it's positioned here.
2072 this.createRollInterface_();
2074 this.cascadeEvents_('predraw');
2076 // Convert the raw data (a 2D array) into the internal format and compute
2077 // rolling averages.
2078 this.rolledSeries_
= [null]; // x-axis is the first series and it's special
2079 for (var i
= 1; i
< this.numColumns(); i
++) {
2080 // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too
.
2081 var series
= this.dataHandler_
.extractSeries(this.rawData_
, i
, this.attributes_
);
2082 if (this.rollPeriod_
> 1) {
2083 series
= this.dataHandler_
.rollingAverage(series
, this.rollPeriod_
, this.attributes_
);
2086 this.rolledSeries_
.push(series
);
2089 // If the data or options have changed, then we'd better redraw.
2092 // This is used to determine whether to do various animations.
2093 var end
= new Date();
2094 this.drawingTimeMs_
= (end
- start
);
2100 * xval_* and yval_* are the original unscaled data values,
2101 * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
2102 * yval_stacked is the cumulative Y value used for stacking graphs,
2103 * and bottom/top/minus/plus are used for error bar graphs.
2110 * y_bottom: ?number,
2112 * y_stacked: ?number,
2114 * yval_minus: ?number,
2116 * yval_plus: ?number,
2120 Dygraph
.PointType
= undefined
;
2123 * Calculates point stacking for stackedGraph=true.
2125 * For stacking purposes, interpolate or extend neighboring data across
2126 * NaN values based on stackedGraphNaNFill settings. This is for display
2127 * only, the underlying data value as shown in the legend remains NaN.
2129 * @param {Array.<Dygraph.PointType>} points Point array for a single series.
2130 * Updates each Point's yval_stacked property.
2131 * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
2132 * values for the series seen so far. Index is the row number. Updated
2133 * based on the current series's values.
2134 * @param {Array.<number>} seriesExtremes Min and max values, updated
2135 * to reflect the stacked values.
2136 * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
2140 Dygraph
.stackPoints_
= function(
2141 points
, cumulativeYval
, seriesExtremes
, fillMethod
) {
2142 var lastXval
= null;
2143 var prevPoint
= null;
2144 var nextPoint
= null;
2145 var nextPointIdx
= -1;
2147 // Find the next stackable point starting from the given index.
2148 var updateNextPoint
= function(idx
) {
2149 // If we've previously found a non-NaN point and haven't gone past it yet,
2151 if (nextPointIdx
>= idx
) return;
2153 // We haven't found a non-NaN point yet or have moved past it,
2154 // look towards the right to find a non-NaN point.
2155 for (var j
= idx
; j
< points
.length
; ++j
) {
2156 // Clear out a previously-found point (if any) since it's no longer
2157 // valid, we shouldn't use it for interpolation anymore.
2159 if (!isNaN(points
[j
].yval
) && points
[j
].yval
!== null) {
2161 nextPoint
= points
[j
];
2167 for (var i
= 0; i
< points
.length
; ++i
) {
2168 var point
= points
[i
];
2169 var xval
= point
.xval
;
2170 if (cumulativeYval
[xval
] === undefined
) {
2171 cumulativeYval
[xval
] = 0;
2174 var actualYval
= point
.yval
;
2175 if (isNaN(actualYval
) || actualYval
=== null) {
2176 if(fillMethod
== 'none') {
2179 // Interpolate/extend
for stacking purposes
if possible
.
2181 if (prevPoint
&& nextPoint
&& fillMethod
!= 'none') {
2182 // Use linear interpolation between prevPoint and nextPoint.
2183 actualYval
= prevPoint
.yval
+ (nextPoint
.yval
- prevPoint
.yval
) *
2184 ((xval
- prevPoint
.xval
) / (nextPoint
.xval
- prevPoint
.xval
));
2185 } else if (prevPoint
&& fillMethod
== 'all') {
2186 actualYval
= prevPoint
.yval
;
2187 } else if (nextPoint
&& fillMethod
== 'all') {
2188 actualYval
= nextPoint
.yval
;
2197 var stackedYval
= cumulativeYval
[xval
];
2198 if (lastXval
!= xval
) {
2199 // If an x-value is repeated, we ignore the duplicates.
2200 stackedYval
+= actualYval
;
2201 cumulativeYval
[xval
] = stackedYval
;
2205 point
.yval_stacked
= stackedYval
;
2207 if (stackedYval
> seriesExtremes
[1]) {
2208 seriesExtremes
[1] = stackedYval
;
2210 if (stackedYval
< seriesExtremes
[0]) {
2211 seriesExtremes
[0] = stackedYval
;
2218 * Loop over all fields and create datasets, calculating extreme y-values for
2219 * each series and extreme x-indices as we go.
2221 * dateWindow is passed in as an explicit parameter so that we can compute
2222 * extreme values "speculatively", i.e. without actually setting state on the
2225 * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
2226 * rolledSeries[seriesIndex][row] = raw point, where
2227 * seriesIndex is the column number starting with 1, and
2228 * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
2229 * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
2231 * points: Array.<Array.<Dygraph.PointType>>,
2232 * seriesExtremes: Array.<Array.<number>>,
2233 * boundaryIds: Array.<number>}}
2236 Dygraph
.prototype.gatherDatasets_
= function(rolledSeries
, dateWindow
) {
2237 var boundaryIds
= [];
2239 var cumulativeYval
= []; // For stacked series.
2240 var extremes
= {}; // series name -> [low, high]
2241 var seriesIdx
, sampleIdx
;
2242 var firstIdx
, lastIdx
;
2245 // Loop over the fields (series). Go from the last to the first,
2246 // because if they're stacked that's how we accumulate the values.
2247 var num_series
= rolledSeries
.length
- 1;
2249 for (seriesIdx
= num_series
; seriesIdx
>= 1; seriesIdx
--) {
2250 if (!this.visibility()[seriesIdx
- 1]) continue;
2252 // Prune down to the desired range, if necessary (for zooming)
2253 // Because there can be lines going to points outside of the visible area,
2254 // we actually prune to visible points, plus one on either side.
2256 series
= rolledSeries
[seriesIdx
];
2257 var low
= dateWindow
[0];
2258 var high
= dateWindow
[1];
2260 // TODO(danvk): do binary search instead of linear search.
2261 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2264 for (sampleIdx
= 0; sampleIdx
< series
.length
; sampleIdx
++) {
2265 if (series
[sampleIdx
][0] >= low
&& firstIdx
=== null) {
2266 firstIdx
= sampleIdx
;
2268 if (series
[sampleIdx
][0] <= high
) {
2269 lastIdx
= sampleIdx
;
2273 if (firstIdx
=== null) firstIdx
= 0;
2274 var correctedFirstIdx
= firstIdx
;
2275 var isInvalidValue
= true;
2276 while (isInvalidValue
&& correctedFirstIdx
> 0) {
2277 correctedFirstIdx
--;
2278 // check if the y value is null.
2279 isInvalidValue
= series
[correctedFirstIdx
][1] === null;
2282 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2283 var correctedLastIdx
= lastIdx
;
2284 isInvalidValue
= true;
2285 while (isInvalidValue
&& correctedLastIdx
< series
.length
- 1) {
2287 isInvalidValue
= series
[correctedLastIdx
][1] === null;
2290 if (correctedFirstIdx
!==firstIdx
) {
2291 firstIdx
= correctedFirstIdx
;
2293 if (correctedLastIdx
!== lastIdx
) {
2294 lastIdx
= correctedLastIdx
;
2297 boundaryIds
[seriesIdx
-1] = [firstIdx
, lastIdx
];
2299 // .slice's end is exclusive, we want to include lastIdx.
2300 series
= series
.slice(firstIdx
, lastIdx
+ 1);
2302 series
= rolledSeries
[seriesIdx
];
2303 boundaryIds
[seriesIdx
-1] = [0, series
.length
-1];
2306 var seriesName
= this.attr_("labels")[seriesIdx
];
2307 var seriesExtremes
= this.dataHandler_
.getExtremeYValues(series
,
2308 dateWindow
, this.getBooleanOption("stepPlot",seriesName
));
2310 var seriesPoints
= this.dataHandler_
.seriesToPoints(series
,
2311 seriesName
, boundaryIds
[seriesIdx
-1][0]);
2313 if (this.getBooleanOption("stackedGraph")) {
2314 axisIdx
= this.attributes_
.axisForSeries(seriesName
);
2315 if (cumulativeYval
[axisIdx
] === undefined
) {
2316 cumulativeYval
[axisIdx
] = [];
2318 Dygraph
.stackPoints_(seriesPoints
, cumulativeYval
[axisIdx
], seriesExtremes
,
2319 this.getBooleanOption("stackedGraphNaNFill"));
2322 extremes
[seriesName
] = seriesExtremes
;
2323 points
[seriesIdx
] = seriesPoints
;
2326 return { points
: points
, extremes
: extremes
, boundaryIds
: boundaryIds
};
2330 * Update the graph with new data. This method is called when the viewing area
2331 * has changed. If the underlying data or options have changed, predraw_ will
2332 * be called before drawGraph_ is called.
2336 Dygraph
.prototype.drawGraph_
= function() {
2337 var start
= new Date();
2339 // This is used to set the second parameter to drawCallback, below.
2340 var is_initial_draw
= this.is_initial_draw_
;
2341 this.is_initial_draw_
= false;
2343 this.layout_
.removeAllDatasets();
2345 this.attrs_
.pointSize
= 0.5 * this.getNumericOption('highlightCircleSize');
2347 var packed
= this.gatherDatasets_(this.rolledSeries_
, this.dateWindow_
);
2348 var points
= packed
.points
;
2349 var extremes
= packed
.extremes
;
2350 this.boundaryIds_
= packed
.boundaryIds
;
2352 this.setIndexByName_
= {};
2353 var labels
= this.attr_("labels");
2354 if (labels
.length
> 0) {
2355 this.setIndexByName_
[labels
[0]] = 0;
2358 for (var i
= 1; i
< points
.length
; i
++) {
2359 this.setIndexByName_
[labels
[i
]] = i
;
2360 if (!this.visibility()[i
- 1]) continue;
2361 this.layout_
.addDataset(labels
[i
], points
[i
]);
2362 this.datasetIndex_
[i
] = dataIdx
++;
2365 this.computeYAxisRanges_(extremes
);
2366 this.layout_
.setYAxes(this.axes_
);
2370 // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously
2371 var tmp_zoomed_x
= this.zoomed_x_
;
2372 // Tell PlotKit to use this new data and render itself
2373 this.zoomed_x_
= tmp_zoomed_x
;
2374 this.layout_
.evaluate();
2375 this.renderGraph_(is_initial_draw
);
2377 if (this.getStringOption("timingName")) {
2378 var end
= new Date();
2379 console
.log(this.getStringOption("timingName") + " - drawGraph: " + (end
- start
) + "ms");
2384 * This does the work of drawing the chart. It assumes that the layout and axis
2385 * scales have already been set (e.g. by predraw_).
2389 Dygraph
.prototype.renderGraph_
= function(is_initial_draw
) {
2390 this.cascadeEvents_('clearChart');
2391 this.plotter_
.clear();
2393 if (this.getFunctionOption('underlayCallback')) {
2394 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
2395 // users who expect a deprecated form of this callback.
2396 this.getFunctionOption('underlayCallback').call(this,
2397 this.hidden_ctx_
, this.layout_
.getPlotArea(), this, this);
2401 canvas
: this.hidden_
,
2402 drawingContext
: this.hidden_ctx_
2404 this.cascadeEvents_('willDrawChart', e
);
2405 this.plotter_
.render();
2406 this.cascadeEvents_('didDrawChart', e
);
2407 this.lastRow_
= -1; // because plugins/legend.js clears the legend
2409 // TODO(danvk): is this a performance bottleneck when panning?
2410 // The interaction canvas should already be empty in that situation.
2411 this.canvas_
.getContext('2d').clearRect(0, 0, this.width_
, this.height_
);
2413 if (this.getFunctionOption("drawCallback") !== null) {
2414 this.getFunctionOption("drawCallback").call(this, this, is_initial_draw
);
2416 if (is_initial_draw
) {
2417 this.readyFired_
= true;
2418 while (this.readyFns_
.length
> 0) {
2419 var fn
= this.readyFns_
.pop();
2427 * Determine properties of the y-axes which are independent of the data
2428 * currently being displayed. This includes things like the number of axes and
2429 * the style of the axes. It does not include the range of each axis and its
2431 * This fills in this.axes_.
2432 * axes_ = [ { options } ]
2433 * indices are into the axes_ array.
2435 Dygraph
.prototype.computeYAxes_
= function() {
2436 // Preserve valueWindow settings if they exist, and if the user hasn't
2437 // specified a new valueRange.
2438 var valueWindows
, axis
, index
, opts
, v
;
2439 if (this.axes_
!== undefined
&& this.user_attrs_
.hasOwnProperty("valueRange") === false) {
2441 for (index
= 0; index
< this.axes_
.length
; index
++) {
2442 valueWindows
.push(this.axes_
[index
].valueWindow
);
2446 // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
2447 // data computation as well as options storage.
2448 // Go through once and add all the axes.
2451 for (axis
= 0; axis
< this.attributes_
.numAxes(); axis
++) {
2452 // Add a new axis, making a copy of its per-axis options.
2453 opts
= { g
: this };
2454 utils
.update(opts
, this.attributes_
.axisOptions(axis
));
2455 this.axes_
[axis
] = opts
;
2459 // Copy global valueRange option over to the first axis.
2460 // NOTE(konigsberg): Are these two statements necessary?
2461 // I tried removing it. The automated tests pass, and manually
2462 // messing with tests/zoom
.html showed no trouble
.
2463 v
= this.attr_('valueRange');
2464 if (v
) this.axes_
[0].valueRange
= v
;
2466 if (valueWindows
!== undefined
) {
2467 // Restore valueWindow settings.
2469 // When going from two axes back to one, we only restore
2471 var idxCount
= Math
.min(valueWindows
.length
, this.axes_
.length
);
2473 for (index
= 0; index
< idxCount
; index
++) {
2474 this.axes_
[index
].valueWindow
= valueWindows
[index
];
2478 for (axis
= 0; axis
< this.axes_
.length
; axis
++) {
2480 opts
= this.optionsViewForAxis_('y' + (axis
? '2' : ''));
2481 v
= opts("valueRange");
2482 if (v
) this.axes_
[axis
].valueRange
= v
;
2483 } else { // To keep old behavior
2484 var axes
= this.user_attrs_
.axes
;
2485 if (axes
&& axes
.y2
) {
2486 v
= axes
.y2
.valueRange
;
2487 if (v
) this.axes_
[axis
].valueRange
= v
;
2494 * Returns the number of y-axes on the chart.
2495 * @return {number} the number of axes.
2497 Dygraph
.prototype.numAxes
= function() {
2498 return this.attributes_
.numAxes();
2503 * Returns axis properties for the given series.
2504 * @param {string} setName The name of the series for which to get axis
2505 * properties, e.g. 'Y1'.
2506 * @return {Object} The axis properties.
2508 Dygraph
.prototype.axisPropertiesForSeries
= function(series
) {
2509 // TODO(danvk): handle errors.
2510 return this.axes_
[this.attributes_
.axisForSeries(series
)];
2515 * Determine the value range and tick marks for each axis.
2516 * @param {Object} extremes A mapping from seriesName -> [low, high]
2517 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2519 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2520 var isNullUndefinedOrNaN
= function(num
) {
2521 return isNaN(parseFloat(num
));
2523 var numAxes
= this.attributes_
.numAxes();
2524 var ypadCompat
, span
, series
, ypad
;
2528 // Compute extreme values, a span and tick marks for each axis.
2529 for (var i
= 0; i
< numAxes
; i
++) {
2530 var axis
= this.axes_
[i
];
2531 var logscale
= this.attributes_
.getForAxis("logscale", i
);
2532 var includeZero
= this.attributes_
.getForAxis("includeZero", i
);
2533 var independentTicks
= this.attributes_
.getForAxis("independentTicks", i
);
2534 series
= this.attributes_
.seriesForAxis(i
);
2536 // Add some padding. This supports two Y padding operation modes:
2538 // - backwards compatible (yRangePad not set):
2539 // 10% padding for automatic Y ranges, but not for user-supplied
2540 // ranges, and move a close-to-zero edge to zero except if
2541 // avoidMinZero is set, since drawing at the edge results in
2542 // invisible lines. Unfortunately lines drawn at the edge of a
2543 // user-supplied range will still be invisible. If logscale is
2544 // set, add a variable amount of padding at the top but
2545 // none at the bottom.
2547 // - new-style (yRangePad set by the user):
2548 // always add the specified Y padding.
2551 ypad
= 0.1; // add 10%
2552 if (this.getNumericOption('yRangePad') !== null) {
2554 // Convert pixel padding to ratio
2555 ypad
= this.getNumericOption('yRangePad') / this.plotter_
.area
.h
;
2558 if (series
.length
=== 0) {
2559 // If no series are defined or visible then use a reasonable default
2560 axis
.extremeRange
= [0, 1];
2562 // Calculate the extremes of extremes.
2563 var minY
= Infinity
; // extremes[series[0]][0];
2564 var maxY
= -Infinity
; // extremes[series[0]][1];
2565 var extremeMinY
, extremeMaxY
;
2567 for (var j
= 0; j
< series
.length
; j
++) {
2568 // this skips invisible series
2569 if (!extremes
.hasOwnProperty(series
[j
])) continue;
2571 // Only use valid extremes to stop null data series' from corrupting the scale.
2572 extremeMinY
= extremes
[series
[j
]][0];
2573 if (extremeMinY
!== null) {
2574 minY
= Math
.min(extremeMinY
, minY
);
2576 extremeMaxY
= extremes
[series
[j
]][1];
2577 if (extremeMaxY
!== null) {
2578 maxY
= Math
.max(extremeMaxY
, maxY
);
2582 // Include zero if requested by the user.
2583 if (includeZero
&& !logscale
) {
2584 if (minY
> 0) minY
= 0;
2585 if (maxY
< 0) maxY
= 0;
2588 // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2589 if (minY
== Infinity
) minY
= 0;
2590 if (maxY
== -Infinity
) maxY
= 1;
2593 // special case: if we have no sense of scale, center on the sole value.
2596 span
= Math
.abs(maxY
);
2598 // ... and if the sole value is zero, use range 0-1.
2604 var maxAxisY
, minAxisY
;
2607 maxAxisY
= maxY
+ ypad
* span
;
2610 var logpad
= Math
.exp(Math
.log(span
) * ypad
);
2611 maxAxisY
= maxY
* logpad
;
2612 minAxisY
= minY
/ logpad
;
2615 maxAxisY
= maxY
+ ypad
* span
;
2616 minAxisY
= minY
- ypad
* span
;
2618 // Backwards-compatible behavior: Move the span to start or end at zero if it's
2619 // close to zero, but not if avoidMinZero is set.
2620 if (ypadCompat
&& !this.getBooleanOption("avoidMinZero")) {
2621 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2622 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2625 axis
.extremeRange
= [minAxisY
, maxAxisY
];
2627 if (axis
.valueWindow
) {
2628 // This is only set if the user has zoomed on the y-axis. It is never set
2629 // by a user. It takes precedence over axis.valueRange because, if you set
2630 // valueRange, you'd still expect to be able to pan.
2631 axis
.computedValueRange
= [axis
.valueWindow
[0], axis
.valueWindow
[1]];
2632 } else if (axis
.valueRange
) {
2633 // This is a user-set value range for this axis.
2634 var y0
= isNullUndefinedOrNaN(axis
.valueRange
[0]) ? axis
.extremeRange
[0] : axis
.valueRange
[0];
2635 var y1
= isNullUndefinedOrNaN(axis
.valueRange
[1]) ? axis
.extremeRange
[1] : axis
.valueRange
[1];
2637 if (axis
.logscale
) {
2638 var logpad
= Math
.exp(Math
.log(span
) * ypad
);
2647 axis
.computedValueRange
= [y0
, y1
];
2649 axis
.computedValueRange
= axis
.extremeRange
;
2653 if (independentTicks
) {
2654 axis
.independentTicks
= independentTicks
;
2655 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2656 var ticker
= opts('ticker');
2657 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2658 axis
.computedValueRange
[1],
2659 this.plotter_
.area
.h
,
2662 // Define the first independent axis as primary axis.
2663 if (!p_axis
) p_axis
= axis
;
2666 if (p_axis
=== undefined
) {
2667 throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
2669 // Add ticks. By default, all axes inherit the tick positions of the
2670 // primary axis. However, if an axis is specifically marked as having
2671 // independent ticks, then that is permissible as well.
2672 for (var i
= 0; i
< numAxes
; i
++) {
2673 var axis
= this.axes_
[i
];
2675 if (!axis
.independentTicks
) {
2676 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2677 var ticker
= opts('ticker');
2678 var p_ticks
= p_axis
.ticks
;
2679 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2680 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2681 var tick_values
= [];
2682 for (var k
= 0; k
< p_ticks
.length
; k
++) {
2683 var y_frac
= (p_ticks
[k
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2684 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2685 tick_values
.push(y_val
);
2688 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2689 axis
.computedValueRange
[1],
2690 this.plotter_
.area
.h
,
2699 * Detects the type of the str (date or numeric) and sets the various
2700 * formatting attributes in this.attrs_ based on this type.
2701 * @param {string} str An x value.
2704 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2706 var dashPos
= str
.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2
2707 if ((dashPos
> 0 && (str
[dashPos
-1] != 'e' && str
[dashPos
-1] != 'E')) ||
2708 str
.indexOf('/') >= 0 ||
2709 isNaN(parseFloat(str
))) {
2711 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2712 // TODO(danvk): remove support for this format.
2716 this.setXAxisOptions_(isDate
);
2719 Dygraph
.prototype.setXAxisOptions_
= function(isDate
) {
2721 this.attrs_
.xValueParser
= utils
.dateParser
;
2722 this.attrs_
.axes
.x
.valueFormatter
= utils
.dateValueFormatter
;
2723 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.dateTicker
;
2724 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.dateAxisLabelFormatter
;
2726 /** @private (shut up, jsdoc!) */
2727 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2728 // TODO(danvk): use Dygraph.numberValueFormatter here?
2729 /** @private (shut up, jsdoc!) */
2730 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2731 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2732 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2738 * Parses a string in a special csv format. We expect a csv file where each
2739 * line is a date point, and the first field in each line is the date string.
2740 * We also expect that all remaining fields represent series.
2741 * if the errorBars attribute is set, then interpret the fields as:
2742 * date, series1, stddev1, series2, stddev2, ...
2743 * @param {[Object]} data See above.
2745 * @return [Object] An array with one entry for each row. These entries
2746 * are an array of cells in that row. The first entry is the parsed x-value for
2747 * the row. The second, third, etc. are the y-values. These can take on one of
2748 * three forms, depending on the CSV and constructor parameters:
2750 * 2. [ value, stddev ]
2751 * 3. [ low value, center value, high value ]
2753 Dygraph
.prototype.parseCSV_
= function(data
) {
2755 var line_delimiter
= utils
.detectLineDelimiter(data
);
2756 var lines
= data
.split(line_delimiter
|| "\n");
2759 // Use the default delimiter or fall back to a tab if that makes sense.
2760 var delim
= this.getStringOption('delimiter');
2761 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2766 if (!('labels' in this.user_attrs_
)) {
2767 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2769 this.attrs_
.labels
= lines
[0].split(delim
); // NOTE: _not_ user_attrs_.
2770 this.attributes_
.reparseSeries();
2775 var defaultParserSet
= false; // attempt to auto-detect x value type
2776 var expectedCols
= this.attr_("labels").length
;
2777 var outOfOrder
= false;
2778 for (var i
= start
; i
< lines
.length
; i
++) {
2779 var line
= lines
[i
];
2781 if (line
.length
=== 0) continue; // skip blank lines
2782 if (line
[0] == '#') continue; // skip comment lines
2783 var inFields
= line
.split(delim
);
2784 if (inFields
.length
< 2) continue;
2787 if (!defaultParserSet
) {
2788 this.detectTypeFromString_(inFields
[0]);
2789 xParser
= this.getFunctionOption("xValueParser");
2790 defaultParserSet
= true;
2792 fields
[0] = xParser(inFields
[0], this);
2794 // If fractions are expected, parse the numbers as "A/B
"
2795 if (this.fractions_) {
2796 for (j = 1; j < inFields.length; j++) {
2797 // TODO(danvk): figure out an appropriate way to flag parse errors.
2798 vals = inFields[j].split("/");
2799 if (vals.length != 2) {
2800 console.error('Expected fractional "num
/den
" values in CSV data ' +
2801 "but found a value
'" + inFields[j] + "' on line
" +
2802 (1 + i) + " ('" + line + "') which is not of
this form
.");
2805 fields[j] = [utils.parseFloat_(vals[0], i, line),
2806 utils.parseFloat_(vals[1], i, line)];
2809 } else if (this.getBooleanOption("errorBars
")) {
2810 // If there are error bars, values are (value, stddev) pairs
2811 if (inFields.length % 2 != 1) {
2812 console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2813 'but line ' + (1 + i) + ' has an odd number of values (' +
2814 (inFields.length - 1) + "): '" + line + "'");
2816 for (j = 1; j < inFields.length; j += 2) {
2817 fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line),
2818 utils.parseFloat_(inFields[j + 1], i, line)];
2820 } else if (this.getBooleanOption("customBars
")) {
2821 // Bars are a low;center;high tuple
2822 for (j = 1; j < inFields.length; j++) {
2823 var val = inFields[j];
2824 if (/^ *$/.test(val)) {
2825 fields[j] = [null, null, null];
2827 vals = val.split(";");
2828 if (vals.length == 3) {
2829 fields[j] = [ utils.parseFloat_(vals[0], i, line),
2830 utils.parseFloat_(vals[1], i, line),
2831 utils.parseFloat_(vals[2], i, line) ];
2833 console.warn('When using customBars, values must be either blank ' +
2834 'or "low
;center
;high
" tuples (got "' + val +
2835 '" on line ' + (1+i));
2840 // Values are just numbers
2841 for (j = 1; j < inFields.length; j++) {
2842 fields[j] = utils.parseFloat_(inFields[j], i, line);
2845 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2849 if (fields.length != expectedCols) {
2850 console.error("Number of columns
in line
" + i + " (" + fields.length +
2851 ") does not agree
with number of
labels (" + expectedCols +
2855 // If the user specified the 'labels' option and none of the cells of the
2856 // first row parsed correctly, then they probably double-specified the
2857 // labels. We go with the values set in the option, discard this row and
2858 // log a warning to the JS console.
2859 if (i === 0 && this.attr_('labels')) {
2860 var all_null = true;
2861 for (j = 0; all_null && j < fields.length; j++) {
2862 if (fields[j]) all_null = false;
2865 console.warn("The dygraphs
'labels' option is set
, but the first row
" +
2866 "of CSV
data ('" + line + "') appears to also contain
" +
2867 "labels
. Will drop the CSV labels and
use the option
" +
2876 console.warn("CSV is out of order
; order it correctly to speed loading
.");
2877 ret.sort(function(a,b) { return a[0] - b[0]; });
2884 * The user has provided their data as a pre-packaged JS array. If the x values
2885 * are numeric, this is the same as dygraphs' internal format. If the x values
2886 * are dates, we need to convert them from Date objects to ms since epoch.
2887 * @param {!Array} data
2888 * @return {Object} data with numeric x values.
2891 Dygraph.prototype.parseArray_ = function(data) {
2892 // Peek at the first x value to see if it's numeric.
2893 if (data.length === 0) {
2894 console.error("Can
't plot empty data set");
2897 if (data[0].length === 0) {
2898 console.error("Data set cannot contain an empty row");
2903 if (this.attr_("labels") === null) {
2904 console.warn("Using default labels. Set labels explicitly via 'labels
' " +
2905 "in the options parameter");
2906 this.attrs_.labels = [ "X" ];
2907 for (i = 1; i < data[0].length; i++) {
2908 this.attrs_.labels.push("Y" + i); // Not user_attrs_.
2910 this.attributes_.reparseSeries();
2912 var num_labels = this.attr_("labels");
2913 if (num_labels.length != data[0].length) {
2914 console.error("Mismatch between number of labels (" + num_labels + ")" +
2915 " and number of columns in array (" + data[0].length + ")");
2920 if (utils.isDateLike(data[0][0])) {
2921 // Some intelligent defaults for a date x-axis.
2922 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2923 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2924 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2926 // Assume they're all dates
.
2927 var parsedData
= utils
.clone(data
);
2928 for (i
= 0; i
< data
.length
; i
++) {
2929 if (parsedData
[i
].length
=== 0) {
2930 console
.error("Row " + (1 + i
) + " of data is empty");
2933 if (parsedData
[i
][0] === null ||
2934 typeof(parsedData
[i
][0].getTime
) != 'function' ||
2935 isNaN(parsedData
[i
][0].getTime())) {
2936 console
.error("x value in row " + (1 + i
) + " is not a Date");
2939 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2943 // Some intelligent defaults for a numeric x-axis.
2944 /** @private (shut up, jsdoc!) */
2945 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2946 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2947 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.numberAxisLabelFormatter
;
2953 * Parses a DataTable object from gviz.
2954 * The data is expected to have a first column that is either a date or a
2955 * number. All subsequent columns must be numbers. If there is a clear mismatch
2956 * between this.xValueParser_ and the type of the first column, it will be
2957 * fixed. Fills out rawData_.
2958 * @param {!google.visualization.DataTable} data See above.
2961 Dygraph
.prototype.parseDataTable_
= function(data
) {
2962 var shortTextForAnnotationNum
= function(num
) {
2963 // converts [0-9]+ [A-Z][a-z]*
2964 // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
2965 // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
2966 var shortText
= String
.fromCharCode(65 /* A */ + num
% 26);
2967 num
= Math
.floor(num
/ 26);
2969 shortText
= String
.fromCharCode(65 /* A */ + (num
- 1) % 26 ) + shortText
.toLowerCase();
2970 num
= Math
.floor((num
- 1) / 26);
2975 var cols
= data
.getNumberOfColumns();
2976 var rows
= data
.getNumberOfRows();
2978 var indepType
= data
.getColumnType(0);
2979 if (indepType
== 'date' || indepType
== 'datetime') {
2980 this.attrs_
.xValueParser
= utils
.dateParser
;
2981 this.attrs_
.axes
.x
.valueFormatter
= utils
.dateValueFormatter
;
2982 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.dateTicker
;
2983 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.dateAxisLabelFormatter
;
2984 } else if (indepType
== 'number') {
2985 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2986 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2987 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2988 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2991 "only 'date', 'datetime' and 'number' types are supported " +
2992 "for column 1 of DataTable input (Got '" + indepType
+ "')");
2995 // Array of the column indices which contain data (and not annotations).
2997 var annotationCols
= {}; // data index -> [annotation cols]
2998 var hasAnnotations
= false;
3000 for (i
= 1; i
< cols
; i
++) {
3001 var type
= data
.getColumnType(i
);
3002 if (type
== 'number') {
3004 } else if (type
== 'string' && this.getBooleanOption('displayAnnotations')) {
3005 // This is OK -- it's an annotation column.
3006 var dataIdx
= colIdx
[colIdx
.length
- 1];
3007 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
3008 annotationCols
[dataIdx
] = [i
];
3010 annotationCols
[dataIdx
].push(i
);
3012 hasAnnotations
= true;
3015 "Only 'number' is supported as a dependent type with Gviz." +
3016 " 'string' is only supported if displayAnnotations is true");
3020 // Read column labels
3021 // TODO(danvk): add support back for errorBars
3022 var labels
= [data
.getColumnLabel(0)];
3023 for (i
= 0; i
< colIdx
.length
; i
++) {
3024 labels
.push(data
.getColumnLabel(colIdx
[i
]));
3025 if (this.getBooleanOption("errorBars")) i
+= 1;
3027 this.attrs_
.labels
= labels
;
3028 cols
= labels
.length
;
3031 var outOfOrder
= false;
3032 var annotations
= [];
3033 for (i
= 0; i
< rows
; i
++) {
3035 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
3036 data
.getValue(i
, 0) === null) {
3037 console
.warn("Ignoring row " + i
+
3038 " of DataTable because of undefined or null first column.");
3042 if (indepType
== 'date' || indepType
== 'datetime') {
3043 row
.push(data
.getValue(i
, 0).getTime());
3045 row
.push(data
.getValue(i
, 0));
3047 if (!this.getBooleanOption("errorBars")) {
3048 for (j
= 0; j
< colIdx
.length
; j
++) {
3049 var col
= colIdx
[j
];
3050 row
.push(data
.getValue(i
, col
));
3051 if (hasAnnotations
&&
3052 annotationCols
.hasOwnProperty(col
) &&
3053 data
.getValue(i
, annotationCols
[col
][0]) !== null) {
3055 ann
.series
= data
.getColumnLabel(col
);
3057 ann
.shortText
= shortTextForAnnotationNum(annotations
.length
);
3059 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
3060 if (k
) ann
.text
+= "\n";
3061 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
3063 annotations
.push(ann
);
3067 // Strip out infinities, which give dygraphs problems later on.
3068 for (j
= 0; j
< row
.length
; j
++) {
3069 if (!isFinite(row
[j
])) row
[j
] = null;
3072 for (j
= 0; j
< cols
- 1; j
++) {
3073 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
3076 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
3083 console
.warn("DataTable is out of order; order it correctly to speed loading.");
3084 ret
.sort(function(a
,b
) { return a
[0] - b
[0]; });
3086 this.rawData_
= ret
;
3088 if (annotations
.length
> 0) {
3089 this.setAnnotations(annotations
, true);
3091 this.attributes_
.reparseSeries();
3095 * Signals to plugins that the chart data has updated.
3096 * This happens after the data has updated but before the chart has redrawn.
3098 Dygraph
.prototype.cascadeDataDidUpdateEvent_
= function() {
3099 // TODO(danvk): there are some issues checking xAxisRange() and using
3100 // toDomCoords from handlers of this event. The visible range should be set
3101 // when the chart is drawn, not derived from the data.
3102 this.cascadeEvents_('dataDidUpdate', {});
3106 * Get the CSV data. If it's in a function, call that function. If it's in a
3107 * file, do an XMLHttpRequest to get it.
3110 Dygraph
.prototype.start_
= function() {
3111 var data
= this.file_
;
3113 // Functions can return references of all other types.
3114 if (typeof data
== 'function') {
3118 if (utils
.isArrayLike(data
)) {
3119 this.rawData_
= this.parseArray_(data
);
3120 this.cascadeDataDidUpdateEvent_();
3122 } else if (typeof data
== 'object' &&
3123 typeof data
.getColumnRange
== 'function') {
3124 // must be a DataTable from gviz.
3125 this.parseDataTable_(data
);
3126 this.cascadeDataDidUpdateEvent_();
3128 } else if (typeof data
== 'string') {
3129 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3130 var line_delimiter
= utils
.detectLineDelimiter(data
);
3131 if (line_delimiter
) {
3132 this.loadedEvent_(data
);
3136 if (window
.XMLHttpRequest
) {
3137 // Firefox, Opera, IE7, and other browsers will use the native object
3138 req
= new XMLHttpRequest();
3140 // IE 5 and 6 will use the ActiveX control
3141 req
= new ActiveXObject("Microsoft.XMLHTTP");
3145 req
.onreadystatechange
= function () {
3146 if (req
.readyState
== 4) {
3147 if (req
.status
=== 200 || // Normal http
3148 req
.status
=== 0) { // Chrome w/ --allow
-file
-access
-from
-files
3149 caller
.loadedEvent_(req
.responseText
);
3154 req
.open("GET", data
, true);
3158 console
.error("Unknown data format: " + (typeof data
));
3163 * Changes various properties of the graph. These can include:
3165 * <li>file: changes the source data for the graph</li>
3166 * <li>errorBars: changes whether the data contains stddev</li>
3169 * There's a huge variety of options that can be passed to this method. For a
3170 * full list, see http://dygraphs.com/options.html.
3172 * @param {Object} input_attrs The new properties and values
3173 * @param {boolean} block_redraw Usually the chart is redrawn after every
3174 * call to updateOptions(). If you know better, you can pass true to
3175 * explicitly block the redraw. This can be useful for chaining
3176 * updateOptions() calls, avoiding the occasional infinite loop and
3177 * preventing redraws when it's not necessary (e.g. when updating a
3180 Dygraph
.prototype.updateOptions
= function(input_attrs
, block_redraw
) {
3181 if (typeof(block_redraw
) == 'undefined') block_redraw
= false;
3183 // copyUserAttrs_ drops the "file" parameter as a convenience to us.
3184 var file
= input_attrs
.file
;
3185 var attrs
= Dygraph
.copyUserAttrs_(input_attrs
);
3187 // TODO(danvk): this is a mess. Move these options into attr_.
3188 if ('rollPeriod' in attrs
) {
3189 this.rollPeriod_
= attrs
.rollPeriod
;
3191 if ('dateWindow' in attrs
) {
3192 this.dateWindow_
= attrs
.dateWindow
;
3193 if (!('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
3194 this.zoomed_x_
= (attrs
.dateWindow
!== null);
3197 if ('valueRange' in attrs
&& !('isZoomedIgnoreProgrammaticZoom' in attrs
)) {
3198 this.zoomed_y_
= (attrs
.valueRange
!== null);
3201 // TODO(danvk): validate per-series options.
3206 // highlightCircleSize
3208 // Check if this set options will require new points.
3209 var requiresNewPoints
= utils
.isPixelChangingOptionList(this.attr_("labels"), attrs
);
3211 utils
.updateDeep(this.user_attrs_
, attrs
);
3213 this.attributes_
.reparseSeries();
3216 // This event indicates that the data is about to change, but hasn't yet.
3217 // TODO(danvk): support cancelation of the update via this event.
3218 this.cascadeEvents_('dataWillUpdate', {});
3221 if (!block_redraw
) this.start_();
3223 if (!block_redraw
) {
3224 if (requiresNewPoints
) {
3227 this.renderGraph_(false);
3234 * Make a copy of input attributes, removing file as a convenience.
3236 Dygraph
.copyUserAttrs_
= function(attrs
) {
3238 for (var k
in attrs
) {
3239 if (!attrs
.hasOwnProperty(k
)) continue;
3240 if (k
== 'file') continue;
3241 if (attrs
.hasOwnProperty(k
)) my_attrs
[k
] = attrs
[k
];
3247 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3248 * containing div (which has presumably changed size since the dygraph was
3249 * instantiated. If the width/height are specified, the div will be resized.
3251 * This is far more efficient than destroying and re-instantiating a
3252 * Dygraph, since it doesn't have to reparse the underlying data.
3254 * @param {number} width Width (in pixels)
3255 * @param {number} height Height (in pixels)
3257 Dygraph
.prototype.resize
= function(width
, height
) {
3258 if (this.resize_lock
) {
3261 this.resize_lock
= true;
3263 if ((width
=== null) != (height
=== null)) {
3264 console
.warn("Dygraph.resize() should be called with zero parameters or " +
3265 "two non-NULL parameters. Pretending it was zero.");
3266 width
= height
= null;
3269 var old_width
= this.width_
;
3270 var old_height
= this.height_
;
3273 this.maindiv_
.style
.width
= width
+ "px";
3274 this.maindiv_
.style
.height
= height
+ "px";
3275 this.width_
= width
;
3276 this.height_
= height
;
3278 this.width_
= this.maindiv_
.clientWidth
;
3279 this.height_
= this.maindiv_
.clientHeight
;
3282 if (old_width
!= this.width_
|| old_height
!= this.height_
) {
3283 // Resizing a canvas erases it, even when the size doesn't change, so
3284 // any resize needs to be followed by a redraw.
3285 this.resizeElements_();
3289 this.resize_lock
= false;
3293 * Adjusts the number of points in the rolling average. Updates the graph to
3294 * reflect the new averaging period.
3295 * @param {number} length Number of points over which to average the data.
3297 Dygraph
.prototype.adjustRoll
= function(length
) {
3298 this.rollPeriod_
= length
;
3303 * Returns a boolean array of visibility statuses.
3305 Dygraph
.prototype.visibility
= function() {
3306 // Do lazy-initialization, so that this happens after we know the number of
3308 if (!this.getOption("visibility")) {
3309 this.attrs_
.visibility
= [];
3311 // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs
.
3312 while (this.getOption("visibility").length
< this.numColumns() - 1) {
3313 this.attrs_
.visibility
.push(true);
3315 return this.getOption("visibility");
3319 * Changes the visibility of one or more series.
3321 * @param {number|number[]|object} num the series index or an array of series indices
3322 * or a boolean array of visibility states by index
3323 * or an object mapping series numbers, as keys, to
3324 * visibility state (boolean values)
3325 * @param {boolean} value the visibility state expressed as a boolean
3327 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3328 var x
= this.visibility();
3329 var numIsObject
= false;
3331 if (!Array
.isArray(num
)) {
3332 if (num
!== null && typeof num
=== 'object') {
3340 for (var i
in num
) {
3341 if (num
.hasOwnProperty(i
)) {
3342 if (i
< 0 || i
>= x
.length
) {
3343 console
.warn("Invalid series number in setVisibility: " + i
);
3350 for (var i
= 0; i
< num
.length
; i
++) {
3351 if (typeof num
[i
] === 'boolean') {
3352 if (i
>= x
.length
) {
3353 console
.warn("Invalid series number in setVisibility: " + i
);
3358 if (num
[i
] < 0 || num
[i
] >= x
.length
) {
3359 console
.warn("Invalid series number in setVisibility: " + num
[i
]);
3371 * How large of an area will the dygraph render itself in?
3372 * This is used for testing.
3373 * @return A {width: w, height: h} object.
3376 Dygraph
.prototype.size
= function() {
3377 return { width
: this.width_
, height
: this.height_
};
3381 * Update the list of annotations and redraw the chart.
3382 * See dygraphs.com/annotations.html for more info on how to use annotations.
3383 * @param ann {Array} An array of annotation objects.
3384 * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3386 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3387 // Only add the annotation CSS rule once we know it will be used.
3388 Dygraph
.addAnnotationRule();
3389 this.annotations_
= ann
;
3390 if (!this.layout_
) {
3391 console
.warn("Tried to setAnnotations before dygraph was ready. " +
3392 "Try setting them in a ready() block. See " +
3393 "dygraphs.com/tests/annotation.html");
3397 this.layout_
.setAnnotations(this.annotations_
);
3398 if (!suppressDraw
) {
3404 * Return the list of annotations.
3406 Dygraph
.prototype.annotations
= function() {
3407 return this.annotations_
;
3411 * Get the list of label names for this graph. The first column is the
3412 * x-axis, so the data series names start at index 1.
3414 * Returns null when labels have not yet been defined.
3416 Dygraph
.prototype.getLabels
= function() {
3417 var labels
= this.attr_("labels");
3418 return labels
? labels
.slice() : null;
3422 * Get the index of a series (column) given its name. The first column is the
3423 * x-axis, so the data series start with index 1.
3425 Dygraph
.prototype.indexFromSetName
= function(name
) {
3426 return this.setIndexByName_
[name
];
3430 * Find the row number corresponding to the given x-value.
3431 * Returns null if there is no such x-value in the data.
3432 * If there are multiple rows with the same x-value, this will return the
3434 * @param {number} xVal The x-value to look for (e.g. millis since epoch).
3435 * @return {?number} The row number, which you can pass to getValue(), or null.
3437 Dygraph
.prototype.getRowForX
= function(xVal
) {
3439 high
= this.numRows() - 1;
3441 while (low
<= high
) {
3442 var idx
= (high
+ low
) >> 1;
3443 var x
= this.getValue(idx
, 0);
3446 } else if (x
> xVal
) {
3448 } else if (low
!= idx
) { // equal, but there may be an earlier match.
3459 * Trigger a callback when the dygraph has drawn itself and is ready to be
3460 * manipulated. This is primarily useful when dygraphs has to do an XHR for the
3461 * data (i.e. a URL is passed as the data source) and the chart is drawn
3462 * asynchronously. If the chart has already drawn, the callback will fire
3465 * This is a good place to call setAnnotation().
3467 * @param {function(!Dygraph)} callback The callback to trigger when the chart
3470 Dygraph
.prototype.ready
= function(callback
) {
3471 if (this.is_initial_draw_
) {
3472 this.readyFns_
.push(callback
);
3474 callback
.call(this, this);
3480 * Adds a default style for the annotation CSS classes to the document. This is
3481 * only executed when annotations are actually used. It is designed to only be
3482 * called once -- all calls after the first will return immediately.
3484 Dygraph
.addAnnotationRule
= function() {
3485 // TODO(danvk): move this function into plugins/annotations.js
?
3486 if (Dygraph
.addedAnnotationCSS
) return;
3488 var rule
= "border: 1px solid black; " +
3489 "background-color: white; " +
3490 "text-align: center;";
3492 var styleSheetElement
= document
.createElement("style");
3493 styleSheetElement
.type
= "text/css";
3494 document
.getElementsByTagName("head")[0].appendChild(styleSheetElement
);
3496 // Find the first style sheet that we can access.
3497 // We may not add a rule to a style sheet from another domain for security
3498 // reasons. This sometimes comes up when using gviz, since the Google gviz JS
3499 // adds its own style sheets from google.com.
3500 for (var i
= 0; i
< document
.styleSheets
.length
; i
++) {
3501 if (document
.styleSheets
[i
].disabled
) continue;
3502 var mysheet
= document
.styleSheets
[i
];
3504 if (mysheet
.insertRule
) { // Firefox
3505 var idx
= mysheet
.cssRules
? mysheet
.cssRules
.length
: 0;
3506 mysheet
.insertRule(".dygraphDefaultAnnotation { " + rule
+ " }", idx
);
3507 } else if (mysheet
.addRule
) { // IE
3508 mysheet
.addRule(".dygraphDefaultAnnotation", rule
);
3510 Dygraph
.addedAnnotationCSS
= true;
3513 // Was likely a security exception.
3517 console
.warn("Unable to add default annotation CSS rule; display may be off.");
3521 * Add an event handler. This event handler is kept until the graph is
3522 * destroyed with a call to graph.destroy().
3524 * @param {!Node} elem The element to add the event to.
3525 * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
3526 * @param {function(Event):(boolean|undefined)} fn The function to call
3527 * on the event. The function takes one parameter: the event object.
3530 Dygraph
.prototype.addAndTrackEvent
= function(elem
, type
, fn
) {
3531 utils
.addEvent(elem
, type
, fn
);
3532 this.registeredEvents_
.push({elem
, type
, fn
});
3535 Dygraph
.prototype.removeTrackedEvents_
= function() {
3536 if (this.registeredEvents_
) {
3537 for (var idx
= 0; idx
< this.registeredEvents_
.length
; idx
++) {
3538 var reg
= this.registeredEvents_
[idx
];
3539 utils
.removeEvent(reg
.elem
, reg
.type
, reg
.fn
);
3543 this.registeredEvents_
= [];
3547 // Installed plugins, in order of precedence (most-general to most-specific).
3551 RangeSelectorPlugin
, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
3557 Dygraph
.GVizChart
= GVizChart
;
3559 export default Dygraph
;