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/
45 import DygraphLayout from
'./dygraph-layout';
46 import DygraphCanvasRenderer from
'./dygraph-canvas';
47 import DygraphOptions from
'./dygraph-options';
48 import DygraphInteraction from
'./dygraph-interaction-model';
49 import * as DygraphTickers from
'./dygraph-tickers';
50 import * as utils from
'./dygraph-utils';
51 import DEFAULT_ATTRS from
'./dygraph-default-attrs';
52 import OPTIONS_REFERENCE from
'./dygraph-options-reference';
53 import IFrameTarp from
'./iframe-tarp';
55 import DefaultHandler from
'./datahandler/default';
56 import ErrorBarsHandler from
'./datahandler/bars-error';
57 import CustomBarsHandler from
'./datahandler/bars-custom';
58 import DefaultFractionHandler from
'./datahandler/default-fractions';
59 import FractionsBarsHandler from
'./datahandler/bars-fractions';
60 import BarsHandler from
'./datahandler/bars';
62 import AnnotationsPlugin from
'./plugins/annotations';
63 import AxesPlugin from
'./plugins/axes';
64 import ChartLabelsPlugin from
'./plugins/chart-labels';
65 import GridPlugin from
'./plugins/grid';
66 import LegendPlugin from
'./plugins/legend';
67 import RangeSelectorPlugin from
'./plugins/range-selector';
69 import GVizChart from
'./dygraph-gviz';
74 * Creates an interactive, zoomable chart.
77 * @param {div | String} div A div or the id of a div into which to construct
79 * @param {String | Function} file A file containing CSV data or a function
80 * that returns this data. The most basic expected format for each line is
81 * "YYYY/MM/DD,val1,val2,...". For more information, see
82 * http://dygraphs.com/data.html.
83 * @param {Object} attrs Various other attributes, e.g. errorBars determines
84 * whether the input data contains error ranges. For a complete list of
85 * options, see http://dygraphs.com/options.html.
87 var Dygraph
= function(div
, data
, opts
) {
88 this.__init__(div
, data
, opts
);
91 Dygraph
.NAME
= "Dygraph";
92 Dygraph
.VERSION
= "2.0.0";
94 // Various default values
95 Dygraph
.DEFAULT_ROLL_PERIOD
= 1;
96 Dygraph
.DEFAULT_WIDTH
= 480;
97 Dygraph
.DEFAULT_HEIGHT
= 320;
99 // For max 60 Hz. animation:
100 Dygraph
.ANIMATION_STEPS
= 12;
101 Dygraph
.ANIMATION_DURATION
= 200;
104 * Standard plotters. These may be used by clients.
105 * Available plotters are:
106 * - Dygraph.Plotters.linePlotter: draws central lines (most common)
107 * - Dygraph.Plotters.errorPlotter: draws error bars
108 * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph)
110 * By default, the plotter is [fillPlotter, errorPlotter, linePlotter].
111 * This causes all the lines to be drawn over all the fills/error bars.
113 Dygraph
.Plotters
= DygraphCanvasRenderer
._Plotters
;
116 // Used for initializing annotation CSS rules only once.
117 Dygraph
.addedAnnotationCSS
= false;
120 * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit
121 * and context <canvas> inside of it. See the constructor for details.
123 * @param {Element} div the Element to render the graph into.
124 * @param {string | Function} file Source data
125 * @param {Object} attrs Miscellaneous other options
128 Dygraph
.prototype.__init__
= function(div
, file
, attrs
) {
129 this.is_initial_draw_
= true;
132 // Support two-argument constructor
133 if (attrs
=== null || attrs
=== undefined
) { attrs
= {}; }
135 attrs
= Dygraph
.copyUserAttrs_(attrs
);
137 if (typeof(div
) == 'string') {
138 div
= document
.getElementById(div
);
142 throw new Error('Constructing dygraph with a non-existent div!');
145 // Copy the important bits into the object
146 // TODO(danvk): most of these should just stay in the attrs_ dictionary.
149 this.rollPeriod_
= attrs
.rollPeriod
|| Dygraph
.DEFAULT_ROLL_PERIOD
;
150 this.previousVerticalX_
= -1;
151 this.fractions_
= attrs
.fractions
|| false;
152 this.dateWindow_
= attrs
.dateWindow
|| null;
154 this.annotations_
= [];
156 // Clear the div. This ensure that, if multiple dygraphs are passed the same
157 // div, then only one will be drawn.
160 // For historical reasons, the 'width' and 'height' options trump all CSS
161 // rules _except_ for an explicit 'width' or 'height' on the div.
162 // As an added convenience, if the div has zero height (like <div></div> does
163 // without any styles), then we use a default height/width
.
164 if (div
.style
.width
=== '' && attrs
.width
) {
165 div
.style
.width
= attrs
.width
+ "px";
167 if (div
.style
.height
=== '' && attrs
.height
) {
168 div
.style
.height
= attrs
.height
+ "px";
170 if (div
.style
.height
=== '' && div
.clientHeight
=== 0) {
171 div
.style
.height
= Dygraph
.DEFAULT_HEIGHT
+ "px";
172 if (div
.style
.width
=== '') {
173 div
.style
.width
= Dygraph
.DEFAULT_WIDTH
+ "px";
176 // These will be zero if the dygraph's div is hidden. In that case,
177 // use the user-specified attributes if present. If not, use zero
178 // and assume the user will call resize to fix things later.
179 this.width_
= div
.clientWidth
|| attrs
.width
|| 0;
180 this.height_
= div
.clientHeight
|| attrs
.height
|| 0;
182 // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_.
183 if (attrs
.stackedGraph
) {
184 attrs
.fillGraph
= true;
185 // TODO(nikhilk): Add any other stackedGraph checks here.
188 // DEPRECATION WARNING: All option processing should be moved from
189 // attrs_ and user_attrs_ to options_, which holds all this information.
191 // Dygraphs has many options, some of which interact with one another.
192 // To keep track of everything, we maintain two sets of options:
194 // this.user_attrs_ only options explicitly set by the user.
195 // this.attrs_ defaults, options derived from user_attrs_, data.
197 // Options are then accessed this.attr_('attr'), which first looks at
198 // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent
199 // defaults without overriding behavior that the user specifically asks for.
200 this.user_attrs_
= {};
201 utils
.update(this.user_attrs_
, attrs
);
203 // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified.
205 utils
.updateDeep(this.attrs_
, DEFAULT_ATTRS
);
207 this.boundaryIds_
= [];
208 this.setIndexByName_
= {};
209 this.datasetIndex_
= [];
211 this.registeredEvents_
= [];
212 this.eventListeners_
= {};
214 this.attributes_
= new DygraphOptions(this);
216 // Create the containing DIV and other interactive elements
217 this.createInterface_();
221 var plugins
= Dygraph
.PLUGINS
.concat(this.getOption('plugins'));
222 for (var i
= 0; i
< plugins
.length
; i
++) {
223 // the plugins option may contain either plugin classes or instances.
224 // Plugin instances contain an activate method.
225 var Plugin
= plugins
[i
]; // either a constructor or an instance.
227 if (typeof(Plugin
.activate
) !== 'undefined') {
228 pluginInstance
= Plugin
;
230 pluginInstance
= new Plugin();
234 plugin
: pluginInstance
,
240 var handlers
= pluginInstance
.activate(this);
241 for (var eventName
in handlers
) {
242 if (!handlers
.hasOwnProperty(eventName
)) continue;
243 // TODO(danvk): validate eventName.
244 pluginDict
.events
[eventName
] = handlers
[eventName
];
247 this.plugins_
.push(pluginDict
);
250 // At this point, plugins can no longer register event handlers.
251 // Construct a map from event -> ordered list of [callback, plugin].
252 for (var i
= 0; i
< this.plugins_
.length
; i
++) {
253 var plugin_dict
= this.plugins_
[i
];
254 for (var eventName
in plugin_dict
.events
) {
255 if (!plugin_dict
.events
.hasOwnProperty(eventName
)) continue;
256 var callback
= plugin_dict
.events
[eventName
];
258 var pair
= [plugin_dict
.plugin
, callback
];
259 if (!(eventName
in this.eventListeners_
)) {
260 this.eventListeners_
[eventName
] = [pair
];
262 this.eventListeners_
[eventName
].push(pair
);
267 this.createDragInterface_();
273 * Triggers a cascade of events to the various plugins which are interested in them.
274 * Returns true if the "default behavior" should be prevented, i.e. if one
275 * of the event listeners called event.preventDefault().
278 Dygraph
.prototype.cascadeEvents_
= function(name
, extra_props
) {
279 if (!(name
in this.eventListeners_
)) return false;
281 // QUESTION: can we use objects & prototypes to speed this up?
285 defaultPrevented
: false,
286 preventDefault
: function() {
287 if (!e
.cancelable
) throw "Cannot call preventDefault on non-cancelable event.";
288 e
.defaultPrevented
= true;
290 propagationStopped
: false,
291 stopPropagation
: function() {
292 e
.propagationStopped
= true;
295 utils
.update(e
, extra_props
);
297 var callback_plugin_pairs
= this.eventListeners_
[name
];
298 if (callback_plugin_pairs
) {
299 for (var i
= callback_plugin_pairs
.length
- 1; i
>= 0; i
--) {
300 var plugin
= callback_plugin_pairs
[i
][0];
301 var callback
= callback_plugin_pairs
[i
][1];
302 callback
.call(plugin
, e
);
303 if (e
.propagationStopped
) break;
306 return e
.defaultPrevented
;
310 * Fetch a plugin instance of a particular class. Only for testing.
312 * @param {!Class} type The type of the plugin.
313 * @return {Object} Instance of the plugin, or null if there is none.
315 Dygraph
.prototype.getPluginInstance_
= function(type
) {
316 for (var i
= 0; i
< this.plugins_
.length
; i
++) {
317 var p
= this.plugins_
[i
];
318 if (p
.plugin
instanceof type
) {
326 * Returns the zoomed status of the chart for one or both axes.
328 * Axis is an optional parameter. Can be set to 'x' or 'y'.
330 * The zoomed status for an axis is set whenever a user zooms using the mouse
331 * or when the dateWindow or valueRange are updated. Double-clicking or calling
332 * resetZoom() resets the zoom status for the chart.
334 Dygraph
.prototype.isZoomed
= function(axis
) {
335 const isZoomedX
= !!this.dateWindow_
;
336 if (axis
=== 'x') return isZoomedX
;
338 const isZoomedY
= this.axes_
.map(axis
=> !!axis
.valueRange
).indexOf(true) >= 0;
339 if (axis
=== null || axis
=== undefined
) {
340 return isZoomedX
|| isZoomedY
;
342 if (axis
=== 'y') return isZoomedY
;
344 throw new Error(`axis parameter is
[${axis
}] must be
null, 'x' or
'y'.`);
348 * Returns information about the Dygraph object, including its containing ID.
350 Dygraph
.prototype.toString
= function() {
351 var maindiv
= this.maindiv_
;
352 var id
= (maindiv
&& maindiv
.id
) ? maindiv
.id
: maindiv
;
353 return "[Dygraph " + id
+ "]";
358 * Returns the value of an option. This may be set by the user (either in the
359 * constructor or by calling updateOptions) or by dygraphs, and may be set to a
361 * @param {string} name The name of the option, e.g. 'rollPeriod'.
362 * @param {string} [seriesName] The name of the series to which the option
363 * will be applied. If no per-series value of this option is available, then
364 * the global value is returned. This is optional.
365 * @return { ... } The value of the option.
367 Dygraph
.prototype.attr_
= function(name
, seriesName
) {
368 // For "production" code, this gets removed by uglifyjs.
369 if (typeof(process
) !== 'undefined') {
370 if (process
.env
.NODE_ENV
!= 'production') {
371 if (typeof(OPTIONS_REFERENCE
) === 'undefined') {
372 console
.error('Must include options reference JS for testing');
373 } else if (!OPTIONS_REFERENCE
.hasOwnProperty(name
)) {
374 console
.error('Dygraphs is using property ' + name
+ ', which has no ' +
375 'entry in the Dygraphs.OPTIONS_REFERENCE listing.');
376 // Only log this error once.
377 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 data set.
516 Dygraph
.prototype.xAxisExtremes
= function() {
517 var pad
= this.getNumericOption('xRangePad') / this.plotter_
.area
.w
;
518 if (this.numRows() === 0) {
519 return [0 - pad
, 1 + pad
];
521 var left
= this.rawData_
[0][0];
522 var right
= this.rawData_
[this.rawData_
.length
- 1][0];
524 // Must keep this in sync with dygraph-layout _evaluateLimits()
525 var range
= right
- left
;
527 right
+= range
* pad
;
529 return [left
, right
];
533 * Returns the lower- and upper-bound y-axis values for each axis. These are
534 * the ranges you'll get if you double-click to zoom out or call resetZoom().
535 * The return value is an array of [low, high] tuples, one for each y-axis.
537 Dygraph
.prototype.yAxisExtremes
= function() {
538 // TODO(danvk): this is pretty inefficient
539 const packed
= this.gatherDatasets_(this.rolledSeries_
, null);
540 const { extremes
} = packed
;
541 const saveAxes
= this.axes_
;
542 this.computeYAxisRanges_(extremes
);
543 const newAxes
= this.axes_
;
544 this.axes_
= saveAxes
;
545 return newAxes
.map(axis
=> axis
.extremeRange
);
549 * Returns the currently-visible y-range for an axis. This can be affected by
550 * zooming, panning or a call to updateOptions. Axis indices are zero-based. If
551 * called with no arguments, returns the range of the first axis.
552 * Returns a two-element array: [bottom, top].
554 Dygraph
.prototype.yAxisRange
= function(idx
) {
555 if (typeof(idx
) == "undefined") idx
= 0;
556 if (idx
< 0 || idx
>= this.axes_
.length
) {
559 var axis
= this.axes_
[idx
];
560 return [ axis
.computedValueRange
[0], axis
.computedValueRange
[1] ];
564 * Returns the currently-visible y-ranges for each axis. This can be affected by
565 * zooming, panning, calls to updateOptions, etc.
566 * Returns an array of [bottom, top] pairs, one for each y-axis.
568 Dygraph
.prototype.yAxisRanges
= function() {
570 for (var i
= 0; i
< this.axes_
.length
; i
++) {
571 ret
.push(this.yAxisRange(i
));
576 // TODO(danvk): use these functions throughout dygraphs.
578 * Convert from data coordinates to canvas/div X/Y coordinates.
579 * If specified, do this conversion for the coordinate system of a particular
580 * axis. Uses the first axis by default.
581 * Returns a two-element array: [X, Y]
583 * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord
584 * instead of toDomCoords(null, y, axis).
586 Dygraph
.prototype.toDomCoords
= function(x
, y
, axis
) {
587 return [ this.toDomXCoord(x
), this.toDomYCoord(y
, axis
) ];
591 * Convert from data x coordinates to canvas/div X coordinate.
592 * If specified, do this conversion for the coordinate system of a particular
594 * Returns a single value or null if x is null.
596 Dygraph
.prototype.toDomXCoord
= function(x
) {
601 var area
= this.plotter_
.area
;
602 var xRange
= this.xAxisRange();
603 return area
.x
+ (x
- xRange
[0]) / (xRange
[1] - xRange
[0]) * area
.w
;
607 * Convert from data x coordinates to canvas/div Y coordinate and optional
608 * axis. Uses the first axis by default.
610 * returns a single value or null if y is null.
612 Dygraph
.prototype.toDomYCoord
= function(y
, axis
) {
613 var pct
= this.toPercentYCoord(y
, axis
);
618 var area
= this.plotter_
.area
;
619 return area
.y
+ pct
* area
.h
;
623 * Convert from canvas/div coords to data coordinates.
624 * If specified, do this conversion for the coordinate system of a particular
625 * axis. Uses the first axis by default.
626 * Returns a two-element array: [X, Y].
628 * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord
629 * instead of toDataCoords(null, y, axis).
631 Dygraph
.prototype.toDataCoords
= function(x
, y
, axis
) {
632 return [ this.toDataXCoord(x
), this.toDataYCoord(y
, axis
) ];
636 * Convert from canvas/div x coordinate to data coordinate.
638 * If x is null, this returns null.
640 Dygraph
.prototype.toDataXCoord
= function(x
) {
645 var area
= this.plotter_
.area
;
646 var xRange
= this.xAxisRange();
648 if (!this.attributes_
.getForAxis("logscale", 'x')) {
649 return xRange
[0] + (x
- area
.x
) / area
.w
* (xRange
[1] - xRange
[0]);
651 var pct
= (x
- area
.x
) / area
.w
;
652 return utils
.logRangeFraction(xRange
[0], xRange
[1], pct
);
657 * Convert from canvas/div y coord to value.
659 * If y is null, this returns null.
660 * if axis is null, this uses the first axis.
662 Dygraph
.prototype.toDataYCoord
= function(y
, axis
) {
667 var area
= this.plotter_
.area
;
668 var yRange
= this.yAxisRange(axis
);
670 if (typeof(axis
) == "undefined") axis
= 0;
671 if (!this.attributes_
.getForAxis("logscale", axis
)) {
672 return yRange
[0] + (area
.y
+ area
.h
- y
) / area
.h
* (yRange
[1] - yRange
[0]);
674 // Computing the inverse of toDomCoord.
675 var pct
= (y
- area
.y
) / area
.h
;
676 // Note reversed yRange, y1 is on top with pct==0.
677 return utils
.logRangeFraction(yRange
[1], yRange
[0], pct
);
682 * Converts a y for an axis to a percentage from the top to the
683 * bottom of the drawing area.
685 * If the coordinate represents a value visible on the canvas, then
686 * the value will be between 0 and 1, where 0 is the top of the canvas.
687 * However, this method will return values outside the range, as
688 * values can fall outside the canvas.
690 * If y is null, this returns null.
691 * if axis is null, this uses the first axis.
693 * @param {number} y The data y-coordinate.
694 * @param {number} [axis] The axis number on which the data coordinate lives.
695 * @return {number} A fraction in [0, 1] where 0 = the top edge.
697 Dygraph
.prototype.toPercentYCoord
= function(y
, axis
) {
701 if (typeof(axis
) == "undefined") axis
= 0;
703 var yRange
= this.yAxisRange(axis
);
706 var logscale
= this.attributes_
.getForAxis("logscale", axis
);
708 var logr0
= utils
.log10(yRange
[0]);
709 var logr1
= utils
.log10(yRange
[1]);
710 pct
= (logr1
- utils
.log10(y
)) / (logr1
- logr0
);
712 // yRange[1] - y is unit distance from the bottom.
713 // yRange[1] - yRange[0] is the scale of the range.
714 // (yRange[1] - y) / (yRange
[1] - yRange
[0]) is the
% from the bottom
.
715 pct
= (yRange
[1] - y
) / (yRange
[1] - yRange
[0]);
721 * Converts an x value to a percentage from the left to the right of
724 * If the coordinate represents a value visible on the canvas, then
725 * the value will be between 0 and 1, where 0 is the left of the canvas.
726 * However, this method will return values outside the range, as
727 * values can fall outside the canvas.
729 * If x is null, this returns null.
730 * @param {number} x The data x-coordinate.
731 * @return {number} A fraction in [0, 1] where 0 = the left edge.
733 Dygraph
.prototype.toPercentXCoord
= function(x
) {
738 var xRange
= this.xAxisRange();
740 var logscale
= this.attributes_
.getForAxis("logscale", 'x') ;
741 if (logscale
=== true) { // logscale can be null so we test for true explicitly.
742 var logr0
= utils
.log10(xRange
[0]);
743 var logr1
= utils
.log10(xRange
[1]);
744 pct
= (utils
.log10(x
) - logr0
) / (logr1
- logr0
);
746 // x - xRange[0] is unit distance from the left.
747 // xRange[1] - xRange[0] is the scale of the range.
748 // The full expression below is the % from the left.
749 pct
= (x
- xRange
[0]) / (xRange
[1] - xRange
[0]);
755 * Returns the number of columns (including the independent variable).
756 * @return {number} The number of columns.
758 Dygraph
.prototype.numColumns
= function() {
759 if (!this.rawData_
) return 0;
760 return this.rawData_
[0] ? this.rawData_
[0].length
: this.attr_("labels").length
;
764 * Returns the number of rows (excluding any header/label row).
765 * @return {number} The number of rows, less any header.
767 Dygraph
.prototype.numRows
= function() {
768 if (!this.rawData_
) return 0;
769 return this.rawData_
.length
;
773 * Returns the value in the given row and column. If the row and column exceed
774 * the bounds on the data, returns null. Also returns null if the value is
776 * @param {number} row The row number of the data (0-based). Row 0 is the
777 * first row of data, not a header row.
778 * @param {number} col The column number of the data (0-based)
779 * @return {number} The value in the specified cell or null if the row/col
782 Dygraph
.prototype.getValue
= function(row
, col
) {
783 if (row
< 0 || row
> this.rawData_
.length
) return null;
784 if (col
< 0 || col
> this.rawData_
[row
].length
) return null;
786 return this.rawData_
[row
][col
];
790 * Generates interface elements for the Dygraph: a containing div, a div to
791 * display the current point, and a textbox to adjust the rolling average
792 * period. Also creates the Renderer/Layout elements.
795 Dygraph
.prototype.createInterface_
= function() {
796 // Create the all-enclosing graph div
797 var enclosing
= this.maindiv_
;
799 this.graphDiv
= document
.createElement("div");
801 // TODO(danvk): any other styles that are useful to set here?
802 this.graphDiv
.style
.textAlign
= 'left'; // This is a CSS "reset"
803 this.graphDiv
.style
.position
= 'relative';
804 enclosing
.appendChild(this.graphDiv
);
806 // Create the canvas for interactive parts of the chart.
807 this.canvas_
= utils
.createCanvas();
808 this.canvas_
.style
.position
= "absolute";
810 // ... and for static parts of the chart.
811 this.hidden_
= this.createPlotKitCanvas_(this.canvas_
);
813 this.canvas_ctx_
= utils
.getContext(this.canvas_
);
814 this.hidden_ctx_
= utils
.getContext(this.hidden_
);
816 this.resizeElements_();
818 // The interactive parts of the graph are drawn on top of the chart.
819 this.graphDiv
.appendChild(this.hidden_
);
820 this.graphDiv
.appendChild(this.canvas_
);
821 this.mouseEventElement_
= this.createMouseEventElement_();
823 // Create the grapher
824 this.layout_
= new DygraphLayout(this);
828 this.mouseMoveHandler_
= function(e
) {
829 dygraph
.mouseMove_(e
);
832 this.mouseOutHandler_
= function(e
) {
833 // The mouse has left the chart if:
834 // 1. e.target is inside the chart
835 // 2. e.relatedTarget is outside the chart
836 var target
= e
.target
|| e
.fromElement
;
837 var relatedTarget
= e
.relatedTarget
|| e
.toElement
;
838 if (utils
.isNodeContainedBy(target
, dygraph
.graphDiv
) &&
839 !utils
.isNodeContainedBy(relatedTarget
, dygraph
.graphDiv
)) {
840 dygraph
.mouseOut_(e
);
844 this.addAndTrackEvent(window
, 'mouseout', this.mouseOutHandler_
);
845 this.addAndTrackEvent(this.mouseEventElement_
, 'mousemove', this.mouseMoveHandler_
);
847 // Don't recreate and register the resize handler on subsequent calls.
848 // This happens when the graph is resized.
849 if (!this.resizeHandler_
) {
850 this.resizeHandler_
= function(e
) {
854 // Update when the window is resized.
855 // TODO(danvk): drop frames depending on complexity of the chart.
856 this.addAndTrackEvent(window
, 'resize', this.resizeHandler_
);
860 Dygraph
.prototype.resizeElements_
= function() {
861 this.graphDiv
.style
.width
= this.width_
+ "px";
862 this.graphDiv
.style
.height
= this.height_
+ "px";
864 var canvasScale
= utils
.getContextPixelRatio(this.canvas_ctx_
);
865 this.canvas_
.width
= this.width_
* canvasScale
;
866 this.canvas_
.height
= this.height_
* canvasScale
;
867 this.canvas_
.style
.width
= this.width_
+ "px"; // for IE
868 this.canvas_
.style
.height
= this.height_
+ "px"; // for IE
869 if (canvasScale
!== 1) {
870 this.canvas_ctx_
.scale(canvasScale
, canvasScale
);
873 var hiddenScale
= utils
.getContextPixelRatio(this.hidden_ctx_
);
874 this.hidden_
.width
= this.width_
* hiddenScale
;
875 this.hidden_
.height
= this.height_
* hiddenScale
;
876 this.hidden_
.style
.width
= this.width_
+ "px"; // for IE
877 this.hidden_
.style
.height
= this.height_
+ "px"; // for IE
878 if (hiddenScale
!== 1) {
879 this.hidden_ctx_
.scale(hiddenScale
, hiddenScale
);
884 * Detach DOM elements in the dygraph and null out all data references.
885 * Calling this when you're done with a dygraph can dramatically reduce memory
886 * usage. See, e.g., the tests/perf.html example.
888 Dygraph
.prototype.destroy
= function() {
889 this.canvas_ctx_
.restore();
890 this.hidden_ctx_
.restore();
892 // Destroy any plugins, in the reverse order that they were registered.
893 for (var i
= this.plugins_
.length
- 1; i
>= 0; i
--) {
894 var p
= this.plugins_
.pop();
895 if (p
.plugin
.destroy
) p
.plugin
.destroy();
898 var removeRecursive
= function(node
) {
899 while (node
.hasChildNodes()) {
900 removeRecursive(node
.firstChild
);
901 node
.removeChild(node
.firstChild
);
905 this.removeTrackedEvents_();
907 // remove mouse event handlers (This may not be necessary anymore)
908 utils
.removeEvent(window
, 'mouseout', this.mouseOutHandler_
);
909 utils
.removeEvent(this.mouseEventElement_
, 'mousemove', this.mouseMoveHandler_
);
911 // remove window handlers
912 utils
.removeEvent(window
,'resize', this.resizeHandler_
);
913 this.resizeHandler_
= null;
915 removeRecursive(this.maindiv_
);
917 var nullOut
= function(obj
) {
919 if (typeof(obj
[n
]) === 'object') {
924 // These may not all be necessary, but it can't hurt...
925 nullOut(this.layout_
);
926 nullOut(this.plotter_
);
931 * Creates the canvas on which the chart will be drawn. Only the Renderer ever
932 * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots
933 * or the zoom rectangles) is done on this.canvas_.
934 * @param {Object} canvas The Dygraph canvas over which to overlay the plot
935 * @return {Object} The newly-created canvas
938 Dygraph
.prototype.createPlotKitCanvas_
= function(canvas
) {
939 var h
= utils
.createCanvas();
940 h
.style
.position
= "absolute";
941 // TODO(danvk): h should be offset from canvas. canvas needs to include
942 // some extra area to make it easier to zoom in on the far left and far
943 // right. h needs to be precisely the plot area, so that clipping occurs.
944 h
.style
.top
= canvas
.style
.top
;
945 h
.style
.left
= canvas
.style
.left
;
946 h
.width
= this.width_
;
947 h
.height
= this.height_
;
948 h
.style
.width
= this.width_
+ "px"; // for IE
949 h
.style
.height
= this.height_
+ "px"; // for IE
954 * Creates an overlay element used to handle mouse events.
955 * @return {Object} The mouse event element.
958 Dygraph
.prototype.createMouseEventElement_
= function() {
963 * Generate a set of distinct colors for the data series. This is done with a
964 * color wheel. Saturation/Value are customizable, and the hue is
965 * equally-spaced around the color wheel. If a custom set of colors is
966 * specified, that is used instead.
969 Dygraph
.prototype.setColors_
= function() {
970 var labels
= this.getLabels();
971 var num
= labels
.length
- 1;
973 this.colorsMap_
= {};
975 // These are used for when no custom colors are specified.
976 var sat
= this.getNumericOption('colorSaturation') || 1.0;
977 var val
= this.getNumericOption('colorValue') || 0.5;
978 var half
= Math
.ceil(num
/ 2);
980 var colors
= this.getOption('colors');
981 var visibility
= this.visibility();
982 for (var i
= 0; i
< num
; i
++) {
983 if (!visibility
[i
]) {
986 var label
= labels
[i
+ 1];
987 var colorStr
= this.attributes_
.getForSeries('color', label
);
990 colorStr
= colors
[i
% colors
.length
];
992 // alternate colors for high contrast.
993 var idx
= i
% 2 ? (half
+ (i
+ 1)/ 2) : Math.ceil((i + 1) / 2);
994 var hue
= (1.0 * idx
/ (1 + num
));
995 colorStr
= utils
.hsvToRGB(hue
, sat
, val
);
998 this.colors_
.push(colorStr
);
999 this.colorsMap_
[label
] = colorStr
;
1004 * Return the list of colors. This is either the list of colors passed in the
1005 * attributes or the autogenerated list of rgb(r,g,b) strings.
1006 * This does not return colors for invisible series.
1007 * @return {Array.<string>} The list of colors.
1009 Dygraph
.prototype.getColors
= function() {
1010 return this.colors_
;
1014 * Returns a few attributes of a series, i.e. its color, its visibility, which
1015 * axis it's assigned to, and its column in the original data.
1016 * Returns null if the series does not exist.
1017 * Otherwise, returns an object with column, visibility, color and axis properties.
1018 * The "axis" property will be set to 1 for y1 and 2 for y2.
1019 * The "column" property can be fed back into getValue(row, column) to get
1020 * values for this series.
1022 Dygraph
.prototype.getPropertiesForSeries
= function(series_name
) {
1024 var labels
= this.getLabels();
1025 for (var i
= 1; i
< labels
.length
; i
++) {
1026 if (labels
[i
] == series_name
) {
1031 if (idx
== -1) return null;
1036 visible
: this.visibility()[idx
- 1],
1037 color
: this.colorsMap_
[series_name
],
1038 axis
: 1 + this.attributes_
.axisForSeries(series_name
)
1043 * Create the text box to adjust the averaging period
1046 Dygraph
.prototype.createRollInterface_
= function() {
1047 // Create a roller if one doesn't exist already.
1048 var roller
= this.roller_
;
1050 this.roller_
= roller
= document
.createElement("input");
1051 roller
.type
= "text";
1052 roller
.style
.display
= "none";
1053 roller
.className
= 'dygraph-roller';
1054 this.graphDiv
.appendChild(roller
);
1057 var display
= this.getBooleanOption('showRoller') ? 'block' : 'none';
1059 var area
= this.getArea();
1061 "top": (area
.y
+ area
.h
- 25) + "px",
1062 "left": (area
.x
+ 1) + "px",
1066 roller
.value
= this.rollPeriod_
;
1067 utils
.update(roller
.style
, textAttr
);
1069 roller
.onchange
= () => this.adjustRoll(roller
.value
);
1073 * Set up all the mouse handlers needed to capture dragging behavior for zoom
1077 Dygraph
.prototype.createDragInterface_
= function() {
1079 // Tracks whether the mouse is down right now
1081 isPanning
: false, // is this drag part of a pan?
1082 is2DPan
: false, // if so, is that pan 1- or 2-dimensional?
1083 dragStartX
: null, // pixel coordinates
1084 dragStartY
: null, // pixel coordinates
1085 dragEndX
: null, // pixel coordinates
1086 dragEndY
: null, // pixel coordinates
1087 dragDirection
: null,
1088 prevEndX
: null, // pixel coordinates
1089 prevEndY
: null, // pixel coordinates
1090 prevDragDirection
: null,
1091 cancelNextDblclick
: false, // see comment in dygraph-interaction-model.js
1093 // The value on the left side of the graph when a pan operation starts.
1094 initialLeftmostDate
: null,
1096 // The number of units each pixel spans. (This won't be valid for log
1098 xUnitsPerPixel
: null,
1100 // TODO(danvk): update this comment
1101 // The range in second/value units that the viewport encompasses during a
1102 // panning operation.
1105 // Top-left corner of the canvas, in DOM coords
1106 // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY.
1110 // Values for use with panEdgeFraction, which limit how far outside the
1111 // graph's data boundaries it can be panned.
1112 boundedDates
: null, // [minDate, maxDate]
1113 boundedValues
: null, // [[minValue, maxValue] ...]
1115 // We cover iframes during mouse interactions. See comments in
1116 // dygraph-utils.js for more info on why this is a good idea.
1117 tarp
: new IFrameTarp(),
1119 // contextB is the same thing as this context object but renamed.
1120 initializeMouseDown
: function(event
, g
, contextB
) {
1121 // prevents mouse drags from selecting page text.
1122 if (event
.preventDefault
) {
1123 event
.preventDefault(); // Firefox, Chrome, etc.
1125 event
.returnValue
= false; // IE
1126 event
.cancelBubble
= true;
1129 var canvasPos
= utils
.findPos(g
.canvas_
);
1130 contextB
.px
= canvasPos
.x
;
1131 contextB
.py
= canvasPos
.y
;
1132 contextB
.dragStartX
= utils
.dragGetX_(event
, contextB
);
1133 contextB
.dragStartY
= utils
.dragGetY_(event
, contextB
);
1134 contextB
.cancelNextDblclick
= false;
1135 contextB
.tarp
.cover();
1137 destroy
: function() {
1139 if (context
.isZooming
|| context
.isPanning
) {
1140 context
.isZooming
= false;
1141 context
.dragStartX
= null;
1142 context
.dragStartY
= null;
1145 if (context
.isPanning
) {
1146 context
.isPanning
= false;
1147 context
.draggingDate
= null;
1148 context
.dateRange
= null;
1149 for (var i
= 0; i
< self
.axes_
.length
; i
++) {
1150 delete self
.axes_
[i
].draggingValue
;
1151 delete self
.axes_
[i
].dragValueRange
;
1155 context
.tarp
.uncover();
1159 var interactionModel
= this.getOption("interactionModel");
1161 // Self is the graph.
1164 // Function that binds the graph and context to the handler.
1165 var bindHandler
= function(handler
) {
1166 return function(event
) {
1167 handler(event
, self
, context
);
1171 for (var eventName
in interactionModel
) {
1172 if (!interactionModel
.hasOwnProperty(eventName
)) continue;
1173 this.addAndTrackEvent(this.mouseEventElement_
, eventName
,
1174 bindHandler(interactionModel
[eventName
]));
1177 // If the user releases the mouse button during a drag, but not over the
1178 // canvas, then it doesn't count as a zooming action.
1179 if (!interactionModel
.willDestroyContextMyself
) {
1180 var mouseUpHandler
= function(event
) {
1184 this.addAndTrackEvent(document
, 'mouseup', mouseUpHandler
);
1189 * Draw a gray zoom rectangle over the desired area of the canvas. Also clears
1190 * up any previous zoom rectangles that were drawn. This could be optimized to
1191 * avoid extra redrawing, but it's tricky to avoid interactions with the status
1194 * @param {number} direction the direction of the zoom rectangle. Acceptable
1195 * values are utils.HORIZONTAL and utils.VERTICAL.
1196 * @param {number} startX The X position where the drag started, in canvas
1198 * @param {number} endX The current X position of the drag, in canvas coords.
1199 * @param {number} startY The Y position where the drag started, in canvas
1201 * @param {number} endY The current Y position of the drag, in canvas coords.
1202 * @param {number} prevDirection the value of direction on the previous call to
1203 * this function. Used to avoid excess redrawing
1204 * @param {number} prevEndX The value of endX on the previous call to this
1205 * function. Used to avoid excess redrawing
1206 * @param {number} prevEndY The value of endY on the previous call to this
1207 * function. Used to avoid excess redrawing
1210 Dygraph
.prototype.drawZoomRect_
= function(direction
, startX
, endX
, startY
,
1211 endY
, prevDirection
, prevEndX
,
1213 var ctx
= this.canvas_ctx_
;
1215 // Clean up from the previous rect if necessary
1216 if (prevDirection
== utils
.HORIZONTAL
) {
1217 ctx
.clearRect(Math
.min(startX
, prevEndX
), this.layout_
.getPlotArea().y
,
1218 Math
.abs(startX
- prevEndX
), this.layout_
.getPlotArea().h
);
1219 } else if (prevDirection
== utils
.VERTICAL
) {
1220 ctx
.clearRect(this.layout_
.getPlotArea().x
, Math
.min(startY
, prevEndY
),
1221 this.layout_
.getPlotArea().w
, Math
.abs(startY
- prevEndY
));
1224 // Draw a light-grey rectangle to show the new viewing area
1225 if (direction
== utils
.HORIZONTAL
) {
1226 if (endX
&& startX
) {
1227 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1228 ctx
.fillRect(Math
.min(startX
, endX
), this.layout_
.getPlotArea().y
,
1229 Math
.abs(endX
- startX
), this.layout_
.getPlotArea().h
);
1231 } else if (direction
== utils
.VERTICAL
) {
1232 if (endY
&& startY
) {
1233 ctx
.fillStyle
= "rgba(128,128,128,0.33)";
1234 ctx
.fillRect(this.layout_
.getPlotArea().x
, Math
.min(startY
, endY
),
1235 this.layout_
.getPlotArea().w
, Math
.abs(endY
- startY
));
1241 * Clear the zoom rectangle (and perform no zoom).
1244 Dygraph
.prototype.clearZoomRect_
= function() {
1245 this.currentZoomRectArgs_
= null;
1246 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1250 * Zoom to something containing [lowX, highX]. These are pixel coordinates in
1251 * the canvas. The exact zoom window may be slightly larger if there are no data
1252 * points near lowX or highX. Don't confuse this function with doZoomXDates,
1253 * which accepts dates that match the raw data. This function redraws the graph.
1255 * @param {number} lowX The leftmost pixel value that should be visible.
1256 * @param {number} highX The rightmost pixel value that should be visible.
1259 Dygraph
.prototype.doZoomX_
= function(lowX
, highX
) {
1260 this.currentZoomRectArgs_
= null;
1261 // Find the earliest and latest dates contained in this canvasx range.
1262 // Convert the call to date ranges of the raw data.
1263 var minDate
= this.toDataXCoord(lowX
);
1264 var maxDate
= this.toDataXCoord(highX
);
1265 this.doZoomXDates_(minDate
, maxDate
);
1269 * Zoom to something containing [minDate, maxDate] values. Don't confuse this
1270 * method with doZoomX which accepts pixel coordinates. This function redraws
1273 * @param {number} minDate The minimum date that should be visible.
1274 * @param {number} maxDate The maximum date that should be visible.
1277 Dygraph
.prototype.doZoomXDates_
= function(minDate
, maxDate
) {
1278 // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation
1279 // can produce strange effects. Rather than the x-axis transitioning slowly
1280 // between values, it can jerk around.)
1281 var old_window
= this.xAxisRange();
1282 var new_window
= [minDate
, maxDate
];
1283 const zoomCallback
= this.getFunctionOption('zoomCallback');
1284 this.doAnimatedZoom(old_window
, new_window
, null, null, () => {
1286 zoomCallback
.call(this, minDate
, maxDate
, this.yAxisRanges());
1292 * Zoom to something containing [lowY, highY]. These are pixel coordinates in
1293 * the canvas. This function redraws the graph.
1295 * @param {number} lowY The topmost pixel value that should be visible.
1296 * @param {number} highY The lowest pixel value that should be visible.
1299 Dygraph
.prototype.doZoomY_
= function(lowY
, highY
) {
1300 this.currentZoomRectArgs_
= null;
1301 // Find the highest and lowest values in pixel range for each axis.
1302 // Note that lowY (in pixels) corresponds to the max Value (in data coords).
1303 // This is because pixels increase as you go down on the screen, whereas data
1304 // coordinates increase as you go up the screen.
1305 var oldValueRanges
= this.yAxisRanges();
1306 var newValueRanges
= [];
1307 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1308 var hi
= this.toDataYCoord(lowY
, i
);
1309 var low
= this.toDataYCoord(highY
, i
);
1310 newValueRanges
.push([low
, hi
]);
1313 const zoomCallback
= this.getFunctionOption('zoomCallback');
1314 this.doAnimatedZoom(null, null, oldValueRanges
, newValueRanges
, () => {
1316 const [minX
, maxX
] = this.xAxisRange();
1317 zoomCallback
.call(this, minX
, maxX
, this.yAxisRanges());
1323 * Transition function to use in animations. Returns values between 0.0
1324 * (totally old values) and 1.0 (totally new values) for each frame.
1327 Dygraph
.zoomAnimationFunction
= function(frame
, numFrames
) {
1329 return (1.0 - Math
.pow(k
, -frame
)) / (1.0 - Math
.pow(k
, -numFrames
));
1333 * Reset the zoom to the original view coordinates. This is the same as
1334 * double-clicking on the graph.
1336 Dygraph
.prototype.resetZoom
= function() {
1337 const dirtyX
= this.isZoomed('x');
1338 const dirtyY
= this.isZoomed('y');
1339 const dirty
= dirtyX
|| dirtyY
;
1341 // Clear any selection, since it's likely to be drawn in the wrong place.
1342 this.clearSelection();
1346 // Calculate extremes to avoid lack of padding on reset.
1347 const [minDate
, maxDate
] = this.xAxisExtremes();
1349 const animatedZooms
= this.getBooleanOption('animatedZooms');
1350 const zoomCallback
= this.getFunctionOption('zoomCallback');
1352 // TODO(danvk): merge this block w/ the code below
.
1353 if (!animatedZooms
) {
1354 this.dateWindow_
= null;
1355 this.axes_
.forEach(axis
=> {
1356 if (axis
.valueRange
) delete axis
.valueRange
;
1361 zoomCallback
.call(this, minDate
, maxDate
, this.yAxisRanges());
1366 var oldWindow
=null, newWindow
=null, oldValueRanges
=null, newValueRanges
=null;
1368 oldWindow
= this.xAxisRange();
1369 newWindow
= [minDate
, maxDate
];
1373 oldValueRanges
= this.yAxisRanges();
1374 newValueRanges
= this.yAxisExtremes();
1377 this.doAnimatedZoom(oldWindow
, newWindow
, oldValueRanges
, newValueRanges
,
1379 this.dateWindow_
= null;
1380 this.axes_
.forEach(axis
=> {
1381 if (axis
.valueRange
) delete axis
.valueRange
;
1384 zoomCallback
.call(this, minDate
, maxDate
, this.yAxisRanges());
1390 * Combined animation logic for all zoom functions.
1391 * either the x parameters or y parameters may be null.
1394 Dygraph
.prototype.doAnimatedZoom
= function(oldXRange
, newXRange
, oldYRanges
, newYRanges
, callback
) {
1395 var steps
= this.getBooleanOption("animatedZooms") ?
1396 Dygraph
.ANIMATION_STEPS
: 1;
1399 var valueRanges
= [];
1402 if (oldXRange
!== null && newXRange
!== null) {
1403 for (step
= 1; step
<= steps
; step
++) {
1404 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1405 windows
[step
-1] = [oldXRange
[0]*(1-frac
) + frac
*newXRange
[0],
1406 oldXRange
[1]*(1-frac
) + frac
*newXRange
[1]];
1410 if (oldYRanges
!== null && newYRanges
!== null) {
1411 for (step
= 1; step
<= steps
; step
++) {
1412 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1414 for (var j
= 0; j
< this.axes_
.length
; j
++) {
1415 thisRange
.push([oldYRanges
[j
][0]*(1-frac
) + frac
*newYRanges
[j
][0],
1416 oldYRanges
[j
][1]*(1-frac
) + frac
*newYRanges
[j
][1]]);
1418 valueRanges
[step
-1] = thisRange
;
1422 utils
.repeatAndCleanup(step
=> {
1423 if (valueRanges
.length
) {
1424 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1425 var w
= valueRanges
[step
][i
];
1426 this.axes_
[i
].valueRange
= [w
[0], w
[1]];
1429 if (windows
.length
) {
1430 this.dateWindow_
= windows
[step
];
1433 }, steps
, Dygraph
.ANIMATION_DURATION
/ steps
, callback
);
1437 * Get the current graph's area object.
1439 * Returns: {x, y, w, h}
1441 Dygraph
.prototype.getArea
= function() {
1442 return this.plotter_
.area
;
1446 * Convert a mouse event to DOM coordinates relative to the graph origin.
1448 * Returns a two-element array: [X, Y].
1450 Dygraph
.prototype.eventToDomCoords
= function(event
) {
1451 if (event
.offsetX
&& event
.offsetY
) {
1452 return [ event
.offsetX
, event
.offsetY
];
1454 var eventElementPos
= utils
.findPos(this.mouseEventElement_
);
1455 var canvasx
= utils
.pageX(event
) - eventElementPos
.x
;
1456 var canvasy
= utils
.pageY(event
) - eventElementPos
.y
;
1457 return [canvasx
, canvasy
];
1462 * Given a canvas X coordinate, find the closest row.
1463 * @param {number} domX graph-relative DOM X coordinate
1464 * Returns {number} row number.
1467 Dygraph
.prototype.findClosestRow
= function(domX
) {
1468 var minDistX
= Infinity
;
1469 var closestRow
= -1;
1470 var sets
= this.layout_
.points
;
1471 for (var i
= 0; i
< sets
.length
; i
++) {
1472 var points
= sets
[i
];
1473 var len
= points
.length
;
1474 for (var j
= 0; j
< len
; j
++) {
1475 var point
= points
[j
];
1476 if (!utils
.isValidPoint(point
, true)) continue;
1477 var dist
= Math
.abs(point
.canvasx
- domX
);
1478 if (dist
< minDistX
) {
1480 closestRow
= point
.idx
;
1489 * Given canvas X,Y coordinates, find the closest point.
1491 * This finds the individual data point across all visible series
1492 * that's closest to the supplied DOM coordinates using the standard
1493 * Euclidean X,Y distance.
1495 * @param {number} domX graph-relative DOM X coordinate
1496 * @param {number} domY graph-relative DOM Y coordinate
1497 * Returns: {row, seriesName, point}
1500 Dygraph
.prototype.findClosestPoint
= function(domX
, domY
) {
1501 var minDist
= Infinity
;
1502 var dist
, dx
, dy
, point
, closestPoint
, closestSeries
, closestRow
;
1503 for ( var setIdx
= this.layout_
.points
.length
- 1 ; setIdx
>= 0 ; --setIdx
) {
1504 var points
= this.layout_
.points
[setIdx
];
1505 for (var i
= 0; i
< points
.length
; ++i
) {
1507 if (!utils
.isValidPoint(point
)) continue;
1508 dx
= point
.canvasx
- domX
;
1509 dy
= point
.canvasy
- domY
;
1510 dist
= dx
* dx
+ dy
* dy
;
1511 if (dist
< minDist
) {
1513 closestPoint
= point
;
1514 closestSeries
= setIdx
;
1515 closestRow
= point
.idx
;
1519 var name
= this.layout_
.setNames
[closestSeries
];
1528 * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1530 * This first finds the X data point closest to the supplied DOM X coordinate,
1531 * then finds the series which puts the Y coordinate on top of its filled area,
1532 * using linear interpolation between adjacent point pairs.
1534 * @param {number} domX graph-relative DOM X coordinate
1535 * @param {number} domY graph-relative DOM Y coordinate
1536 * Returns: {row, seriesName, point}
1539 Dygraph
.prototype.findStackedPoint
= function(domX
, domY
) {
1540 var row
= this.findClosestRow(domX
);
1541 var closestPoint
, closestSeries
;
1542 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; ++setIdx
) {
1543 var boundary
= this.getLeftBoundary_(setIdx
);
1544 var rowIdx
= row
- boundary
;
1545 var points
= this.layout_
.points
[setIdx
];
1546 if (rowIdx
>= points
.length
) continue;
1547 var p1
= points
[rowIdx
];
1548 if (!utils
.isValidPoint(p1
)) continue;
1549 var py
= p1
.canvasy
;
1550 if (domX
> p1
.canvasx
&& rowIdx
+ 1 < points
.length
) {
1551 // interpolate series Y value using next point
1552 var p2
= points
[rowIdx
+ 1];
1553 if (utils
.isValidPoint(p2
)) {
1554 var dx
= p2
.canvasx
- p1
.canvasx
;
1556 var r
= (domX
- p1
.canvasx
) / dx
;
1557 py
+= r
* (p2
.canvasy
- p1
.canvasy
);
1560 } else if (domX
< p1
.canvasx
&& rowIdx
> 0) {
1561 // interpolate series Y value using previous point
1562 var p0
= points
[rowIdx
- 1];
1563 if (utils
.isValidPoint(p0
)) {
1564 var dx
= p1
.canvasx
- p0
.canvasx
;
1566 var r
= (p1
.canvasx
- domX
) / dx
;
1567 py
+= r
* (p0
.canvasy
- p1
.canvasy
);
1571 // Stop if the point (domX, py) is above this series' upper edge
1572 if (setIdx
=== 0 || py
< domY
) {
1574 closestSeries
= setIdx
;
1577 var name
= this.layout_
.setNames
[closestSeries
];
1586 * When the mouse moves in the canvas, display information about a nearby data
1587 * point and draw dots over those points in the data series. This function
1588 * takes care of cleanup of previously-drawn dots.
1589 * @param {Object} event The mousemove event from the browser.
1592 Dygraph
.prototype.mouseMove_
= function(event
) {
1593 // This prevents JS errors when mousing over the canvas before data loads.
1594 var points
= this.layout_
.points
;
1595 if (points
=== undefined
|| points
=== null) return;
1597 var canvasCoords
= this.eventToDomCoords(event
);
1598 var canvasx
= canvasCoords
[0];
1599 var canvasy
= canvasCoords
[1];
1601 var highlightSeriesOpts
= this.getOption("highlightSeriesOpts");
1602 var selectionChanged
= false;
1603 if (highlightSeriesOpts
&& !this.isSeriesLocked()) {
1605 if (this.getBooleanOption("stackedGraph")) {
1606 closest
= this.findStackedPoint(canvasx
, canvasy
);
1608 closest
= this.findClosestPoint(canvasx
, canvasy
);
1610 selectionChanged
= this.setSelection(closest
.row
, closest
.seriesName
);
1612 var idx
= this.findClosestRow(canvasx
);
1613 selectionChanged
= this.setSelection(idx
);
1616 var callback
= this.getFunctionOption("highlightCallback");
1617 if (callback
&& selectionChanged
) {
1618 callback
.call(this, event
,
1622 this.highlightSet_
);
1627 * Fetch left offset from the specified set index or if not passed, the
1628 * first defined boundaryIds record (see bug #236).
1631 Dygraph
.prototype.getLeftBoundary_
= function(setIdx
) {
1632 if (this.boundaryIds_
[setIdx
]) {
1633 return this.boundaryIds_
[setIdx
][0];
1635 for (var i
= 0; i
< this.boundaryIds_
.length
; i
++) {
1636 if (this.boundaryIds_
[i
] !== undefined
) {
1637 return this.boundaryIds_
[i
][0];
1644 Dygraph
.prototype.animateSelection_
= function(direction
) {
1645 var totalSteps
= 10;
1647 if (this.fadeLevel
=== undefined
) this.fadeLevel
= 0;
1648 if (this.animateId
=== undefined
) this.animateId
= 0;
1649 var start
= this.fadeLevel
;
1650 var steps
= direction
< 0 ? start
: totalSteps
- start
;
1652 if (this.fadeLevel
) {
1653 this.updateSelection_(1.0);
1658 var thisId
= ++this.animateId
;
1660 var cleanupIfClearing
= function() {
1661 // if we haven't reached fadeLevel 0 in the max frame time,
1662 // ensure that the clear happens and just go to 0
1663 if (that
.fadeLevel
!== 0 && direction
< 0) {
1665 that
.clearSelection();
1668 utils
.repeatAndCleanup(
1670 // ignore simultaneous animations
1671 if (that
.animateId
!= thisId
) return;
1673 that
.fadeLevel
+= direction
;
1674 if (that
.fadeLevel
=== 0) {
1675 that
.clearSelection();
1677 that
.updateSelection_(that
.fadeLevel
/ totalSteps
);
1680 steps
, millis
, cleanupIfClearing
);
1684 * Draw dots over the selectied points in the data series. This function
1685 * takes care of cleanup of previously-drawn dots.
1688 Dygraph
.prototype.updateSelection_
= function(opt_animFraction
) {
1689 /*var defaultPrevented = */
1690 this.cascadeEvents_('select', {
1691 selectedRow
: this.lastRow_
=== -1 ? undefined
: this.lastRow_
,
1692 selectedX
: this.lastx_
=== -1 ? undefined
: this.lastx_
,
1693 selectedPoints
: this.selPoints_
1695 // TODO(danvk): use defaultPrevented here?
1697 // Clear the previously drawn vertical, if there is one
1699 var ctx
= this.canvas_ctx_
;
1700 if (this.getOption('highlightSeriesOpts')) {
1701 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1702 var alpha
= 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
1703 var backgroundColor
= utils
.toRGB_(this.getOption('highlightSeriesBackgroundColor'));
1706 // Activating background fade includes an animation effect for a gradual
1707 // fade. TODO(klausw): make this independently configurable if it causes
1708 // issues? Use a shared preference to control animations?
1709 var animateBackgroundFade
= true;
1710 if (animateBackgroundFade
) {
1711 if (opt_animFraction
=== undefined
) {
1712 // start a new animation
1713 this.animateSelection_(1);
1716 alpha
*= opt_animFraction
;
1718 ctx
.fillStyle
= 'rgba(' + backgroundColor
.r
+ ',' + backgroundColor
.g
+ ',' + backgroundColor
.b
+ ',' + alpha
+ ')';
1719 ctx
.fillRect(0, 0, this.width_
, this.height_
);
1722 // Redraw only the highlighted series in the interactive canvas (not the
1723 // static plot canvas, which is where series are usually drawn).
1724 this.plotter_
._renderLineChart(this.highlightSet_
, ctx
);
1725 } else if (this.previousVerticalX_
>= 0) {
1726 // Determine the maximum highlight circle size.
1727 var maxCircleSize
= 0;
1728 var labels
= this.attr_('labels');
1729 for (i
= 1; i
< labels
.length
; i
++) {
1730 var r
= this.getNumericOption('highlightCircleSize', labels
[i
]);
1731 if (r
> maxCircleSize
) maxCircleSize
= r
;
1733 var px
= this.previousVerticalX_
;
1734 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1735 2 * maxCircleSize
+ 2, this.height_
);
1738 if (this.selPoints_
.length
> 0) {
1739 // Draw colored circles over the center of each selected point
1740 var canvasx
= this.selPoints_
[0].canvasx
;
1742 for (i
= 0; i
< this.selPoints_
.length
; i
++) {
1743 var pt
= this.selPoints_
[i
];
1744 if (isNaN(pt
.canvasy
)) continue;
1746 var circleSize
= this.getNumericOption('highlightCircleSize', pt
.name
);
1747 var callback
= this.getFunctionOption("drawHighlightPointCallback", pt
.name
);
1748 var color
= this.plotter_
.colors
[pt
.name
];
1750 callback
= utils
.Circles
.DEFAULT
;
1752 ctx
.lineWidth
= this.getNumericOption('strokeWidth', pt
.name
);
1753 ctx
.strokeStyle
= color
;
1754 ctx
.fillStyle
= color
;
1755 callback
.call(this, this, pt
.name
, ctx
, canvasx
, pt
.canvasy
,
1756 color
, circleSize
, pt
.idx
);
1760 this.previousVerticalX_
= canvasx
;
1765 * Manually set the selected points and display information about them in the
1766 * legend. The selection can be cleared using clearSelection() and queried
1767 * using getSelection().
1769 * To set a selected series but not a selected point, call setSelection with
1770 * row=false and the selected series name.
1772 * @param {number} row Row number that should be highlighted (i.e. appear with
1773 * hover dots on the chart).
1774 * @param {seriesName} optional series name to highlight that series with the
1775 * the highlightSeriesOpts setting.
1776 * @param { locked } optional If true, keep seriesName selected when mousing
1777 * over the graph, disabling closest-series highlighting. Call clearSelection()
1780 Dygraph
.prototype.setSelection
= function(row
, opt_seriesName
, opt_locked
) {
1781 // Extract the points we've selected
1782 this.selPoints_
= [];
1784 var changed
= false;
1785 if (row
!== false && row
>= 0) {
1786 if (row
!= this.lastRow_
) changed
= true;
1787 this.lastRow_
= row
;
1788 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; ++setIdx
) {
1789 var points
= this.layout_
.points
[setIdx
];
1790 // Check if the point at the appropriate index is the point we're looking
1791 // for. If it is, just use it, otherwise search the array for a point
1792 // in the proper place.
1793 var setRow
= row
- this.getLeftBoundary_(setIdx
);
1794 if (setRow
>= 0 && setRow
< points
.length
&& points
[setRow
].idx
== row
) {
1795 var point
= points
[setRow
];
1796 if (point
.yval
!== null) this.selPoints_
.push(point
);
1798 for (var pointIdx
= 0; pointIdx
< points
.length
; ++pointIdx
) {
1799 var point
= points
[pointIdx
];
1800 if (point
.idx
== row
) {
1801 if (point
.yval
!== null) {
1802 this.selPoints_
.push(point
);
1810 if (this.lastRow_
>= 0) changed
= true;
1814 if (this.selPoints_
.length
) {
1815 this.lastx_
= this.selPoints_
[0].xval
;
1820 if (opt_seriesName
!== undefined
) {
1821 if (this.highlightSet_
!== opt_seriesName
) changed
= true;
1822 this.highlightSet_
= opt_seriesName
;
1825 if (opt_locked
!== undefined
) {
1826 this.lockedSet_
= opt_locked
;
1830 this.updateSelection_(undefined
);
1836 * The mouse has left the canvas. Clear out whatever artifacts remain
1837 * @param {Object} event the mouseout event from the browser.
1840 Dygraph
.prototype.mouseOut_
= function(event
) {
1841 if (this.getFunctionOption("unhighlightCallback")) {
1842 this.getFunctionOption("unhighlightCallback").call(this, event
);
1845 if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_
) {
1846 this.clearSelection();
1851 * Clears the current selection (i.e. points that were highlighted by moving
1852 * the mouse over the chart).
1854 Dygraph
.prototype.clearSelection
= function() {
1855 this.cascadeEvents_('deselect', {});
1857 this.lockedSet_
= false;
1858 // Get rid of the overlay data
1859 if (this.fadeLevel
) {
1860 this.animateSelection_(-1);
1863 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1865 this.selPoints_
= [];
1868 this.highlightSet_
= null;
1872 * Returns the number of the currently selected row. To get data for this row,
1873 * you can use the getValue method.
1874 * @return {number} row number, or -1 if nothing is selected
1876 Dygraph
.prototype.getSelection
= function() {
1877 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1881 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; setIdx
++) {
1882 var points
= this.layout_
.points
[setIdx
];
1883 for (var row
= 0; row
< points
.length
; row
++) {
1884 if (points
[row
].x
== this.selPoints_
[0].x
) {
1885 return points
[row
].idx
;
1893 * Returns the name of the currently-highlighted series.
1894 * Only available when the highlightSeriesOpts option is in use.
1896 Dygraph
.prototype.getHighlightSeries
= function() {
1897 return this.highlightSet_
;
1901 * Returns true if the currently-highlighted series was locked
1902 * via setSelection(..., seriesName, true).
1904 Dygraph
.prototype.isSeriesLocked
= function() {
1905 return this.lockedSet_
;
1909 * Fires when there's data available to be graphed.
1910 * @param {string} data Raw CSV data to be plotted
1913 Dygraph
.prototype.loadedEvent_
= function(data
) {
1914 this.rawData_
= this.parseCSV_(data
);
1915 this.cascadeDataDidUpdateEvent_();
1920 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1923 Dygraph
.prototype.addXTicks_
= function() {
1924 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1926 if (this.dateWindow_
) {
1927 range
= [this.dateWindow_
[0], this.dateWindow_
[1]];
1929 range
= this.xAxisExtremes();
1932 var xAxisOptionsView
= this.optionsViewForAxis_('x');
1933 var xTicks
= xAxisOptionsView('ticker')(
1936 this.plotter_
.area
.w
, // TODO(danvk): should be area.width
1939 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
1940 // console.log(msg);
1941 this.layout_
.setXTicks(xTicks
);
1945 * Returns the correct handler class for the currently set options.
1948 Dygraph
.prototype.getHandlerClass_
= function() {
1950 if (this.attr_('dataHandler')) {
1951 handlerClass
= this.attr_('dataHandler');
1952 } else if (this.fractions_
) {
1953 if (this.getBooleanOption('errorBars')) {
1954 handlerClass
= FractionsBarsHandler
;
1956 handlerClass
= DefaultFractionHandler
;
1958 } else if (this.getBooleanOption('customBars')) {
1959 handlerClass
= CustomBarsHandler
;
1960 } else if (this.getBooleanOption('errorBars')) {
1961 handlerClass
= ErrorBarsHandler
;
1963 handlerClass
= DefaultHandler
;
1965 return handlerClass
;
1970 * This function is called once when the chart's data is changed or the options
1971 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1972 * idea is that values derived from the chart's data can be computed here,
1973 * rather than every time the chart is drawn. This includes things like the
1974 * number of axes, rolling averages, etc.
1976 Dygraph
.prototype.predraw_
= function() {
1977 var start
= new Date();
1979 // Create the correct dataHandler
1980 this.dataHandler_
= new (this.getHandlerClass_())();
1982 this.layout_
.computePlotArea();
1984 // TODO(danvk): move more computations out of drawGraph_ and into here.
1985 this.computeYAxes_();
1987 if (!this.is_initial_draw_
) {
1988 this.canvas_ctx_
.restore();
1989 this.hidden_ctx_
.restore();
1992 this.canvas_ctx_
.save();
1993 this.hidden_ctx_
.save();
1995 // Create a new plotter.
1996 this.plotter_
= new DygraphCanvasRenderer(this,
2001 // The roller sits in the bottom left corner of the chart. We don't know where
2002 // this will be until the options are available, so it's positioned here.
2003 this.createRollInterface_();
2005 this.cascadeEvents_('predraw');
2007 // Convert the raw data (a 2D array) into the internal format and compute
2008 // rolling averages.
2009 this.rolledSeries_
= [null]; // x-axis is the first series and it's special
2010 for (var i
= 1; i
< this.numColumns(); i
++) {
2011 // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too
.
2012 var series
= this.dataHandler_
.extractSeries(this.rawData_
, i
, this.attributes_
);
2013 if (this.rollPeriod_
> 1) {
2014 series
= this.dataHandler_
.rollingAverage(series
, this.rollPeriod_
, this.attributes_
);
2017 this.rolledSeries_
.push(series
);
2020 // If the data or options have changed, then we'd better redraw.
2023 // This is used to determine whether to do various animations.
2024 var end
= new Date();
2025 this.drawingTimeMs_
= (end
- start
);
2031 * xval_* and yval_* are the original unscaled data values,
2032 * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
2033 * yval_stacked is the cumulative Y value used for stacking graphs,
2034 * and bottom/top/minus/plus are used for error bar graphs.
2041 * y_bottom: ?number,
2043 * y_stacked: ?number,
2045 * yval_minus: ?number,
2047 * yval_plus: ?number,
2051 Dygraph
.PointType
= undefined
;
2054 * Calculates point stacking for stackedGraph=true.
2056 * For stacking purposes, interpolate or extend neighboring data across
2057 * NaN values based on stackedGraphNaNFill settings. This is for display
2058 * only, the underlying data value as shown in the legend remains NaN.
2060 * @param {Array.<Dygraph.PointType>} points Point array for a single series.
2061 * Updates each Point's yval_stacked property.
2062 * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
2063 * values for the series seen so far. Index is the row number. Updated
2064 * based on the current series's values.
2065 * @param {Array.<number>} seriesExtremes Min and max values, updated
2066 * to reflect the stacked values.
2067 * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
2071 Dygraph
.stackPoints_
= function(
2072 points
, cumulativeYval
, seriesExtremes
, fillMethod
) {
2073 var lastXval
= null;
2074 var prevPoint
= null;
2075 var nextPoint
= null;
2076 var nextPointIdx
= -1;
2078 // Find the next stackable point starting from the given index.
2079 var updateNextPoint
= function(idx
) {
2080 // If we've previously found a non-NaN point and haven't gone past it yet,
2082 if (nextPointIdx
>= idx
) return;
2084 // We haven't found a non-NaN point yet or have moved past it,
2085 // look towards the right to find a non-NaN point.
2086 for (var j
= idx
; j
< points
.length
; ++j
) {
2087 // Clear out a previously-found point (if any) since it's no longer
2088 // valid, we shouldn't use it for interpolation anymore.
2090 if (!isNaN(points
[j
].yval
) && points
[j
].yval
!== null) {
2092 nextPoint
= points
[j
];
2098 for (var i
= 0; i
< points
.length
; ++i
) {
2099 var point
= points
[i
];
2100 var xval
= point
.xval
;
2101 if (cumulativeYval
[xval
] === undefined
) {
2102 cumulativeYval
[xval
] = 0;
2105 var actualYval
= point
.yval
;
2106 if (isNaN(actualYval
) || actualYval
=== null) {
2107 if(fillMethod
== 'none') {
2110 // Interpolate/extend
for stacking purposes
if possible
.
2112 if (prevPoint
&& nextPoint
&& fillMethod
!= 'none') {
2113 // Use linear interpolation between prevPoint and nextPoint.
2114 actualYval
= prevPoint
.yval
+ (nextPoint
.yval
- prevPoint
.yval
) *
2115 ((xval
- prevPoint
.xval
) / (nextPoint
.xval
- prevPoint
.xval
));
2116 } else if (prevPoint
&& fillMethod
== 'all') {
2117 actualYval
= prevPoint
.yval
;
2118 } else if (nextPoint
&& fillMethod
== 'all') {
2119 actualYval
= nextPoint
.yval
;
2128 var stackedYval
= cumulativeYval
[xval
];
2129 if (lastXval
!= xval
) {
2130 // If an x-value is repeated, we ignore the duplicates.
2131 stackedYval
+= actualYval
;
2132 cumulativeYval
[xval
] = stackedYval
;
2136 point
.yval_stacked
= stackedYval
;
2138 if (stackedYval
> seriesExtremes
[1]) {
2139 seriesExtremes
[1] = stackedYval
;
2141 if (stackedYval
< seriesExtremes
[0]) {
2142 seriesExtremes
[0] = stackedYval
;
2149 * Loop over all fields and create datasets, calculating extreme y-values for
2150 * each series and extreme x-indices as we go.
2152 * dateWindow is passed in as an explicit parameter so that we can compute
2153 * extreme values "speculatively", i.e. without actually setting state on the
2156 * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
2157 * rolledSeries[seriesIndex][row] = raw point, where
2158 * seriesIndex is the column number starting with 1, and
2159 * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
2160 * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
2162 * points: Array.<Array.<Dygraph.PointType>>,
2163 * seriesExtremes: Array.<Array.<number>>,
2164 * boundaryIds: Array.<number>}}
2167 Dygraph
.prototype.gatherDatasets_
= function(rolledSeries
, dateWindow
) {
2168 var boundaryIds
= [];
2170 var cumulativeYval
= []; // For stacked series.
2171 var extremes
= {}; // series name -> [low, high]
2172 var seriesIdx
, sampleIdx
;
2173 var firstIdx
, lastIdx
;
2176 // Loop over the fields (series). Go from the last to the first,
2177 // because if they're stacked that's how we accumulate the values.
2178 var num_series
= rolledSeries
.length
- 1;
2180 for (seriesIdx
= num_series
; seriesIdx
>= 1; seriesIdx
--) {
2181 if (!this.visibility()[seriesIdx
- 1]) continue;
2183 // Prune down to the desired range, if necessary (for zooming)
2184 // Because there can be lines going to points outside of the visible area,
2185 // we actually prune to visible points, plus one on either side.
2187 series
= rolledSeries
[seriesIdx
];
2188 var low
= dateWindow
[0];
2189 var high
= dateWindow
[1];
2191 // TODO(danvk): do binary search instead of linear search.
2192 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2195 for (sampleIdx
= 0; sampleIdx
< series
.length
; sampleIdx
++) {
2196 if (series
[sampleIdx
][0] >= low
&& firstIdx
=== null) {
2197 firstIdx
= sampleIdx
;
2199 if (series
[sampleIdx
][0] <= high
) {
2200 lastIdx
= sampleIdx
;
2204 if (firstIdx
=== null) firstIdx
= 0;
2205 var correctedFirstIdx
= firstIdx
;
2206 var isInvalidValue
= true;
2207 while (isInvalidValue
&& correctedFirstIdx
> 0) {
2208 correctedFirstIdx
--;
2209 // check if the y value is null.
2210 isInvalidValue
= series
[correctedFirstIdx
][1] === null;
2213 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2214 var correctedLastIdx
= lastIdx
;
2215 isInvalidValue
= true;
2216 while (isInvalidValue
&& correctedLastIdx
< series
.length
- 1) {
2218 isInvalidValue
= series
[correctedLastIdx
][1] === null;
2221 if (correctedFirstIdx
!==firstIdx
) {
2222 firstIdx
= correctedFirstIdx
;
2224 if (correctedLastIdx
!== lastIdx
) {
2225 lastIdx
= correctedLastIdx
;
2228 boundaryIds
[seriesIdx
-1] = [firstIdx
, lastIdx
];
2230 // .slice's end is exclusive, we want to include lastIdx.
2231 series
= series
.slice(firstIdx
, lastIdx
+ 1);
2233 series
= rolledSeries
[seriesIdx
];
2234 boundaryIds
[seriesIdx
-1] = [0, series
.length
-1];
2237 var seriesName
= this.attr_("labels")[seriesIdx
];
2238 var seriesExtremes
= this.dataHandler_
.getExtremeYValues(series
,
2239 dateWindow
, this.getBooleanOption("stepPlot",seriesName
));
2241 var seriesPoints
= this.dataHandler_
.seriesToPoints(series
,
2242 seriesName
, boundaryIds
[seriesIdx
-1][0]);
2244 if (this.getBooleanOption("stackedGraph")) {
2245 axisIdx
= this.attributes_
.axisForSeries(seriesName
);
2246 if (cumulativeYval
[axisIdx
] === undefined
) {
2247 cumulativeYval
[axisIdx
] = [];
2249 Dygraph
.stackPoints_(seriesPoints
, cumulativeYval
[axisIdx
], seriesExtremes
,
2250 this.getBooleanOption("stackedGraphNaNFill"));
2253 extremes
[seriesName
] = seriesExtremes
;
2254 points
[seriesIdx
] = seriesPoints
;
2257 return { points
: points
, extremes
: extremes
, boundaryIds
: boundaryIds
};
2261 * Update the graph with new data. This method is called when the viewing area
2262 * has changed. If the underlying data or options have changed, predraw_ will
2263 * be called before drawGraph_ is called.
2267 Dygraph
.prototype.drawGraph_
= function() {
2268 var start
= new Date();
2270 // This is used to set the second parameter to drawCallback, below.
2271 var is_initial_draw
= this.is_initial_draw_
;
2272 this.is_initial_draw_
= false;
2274 this.layout_
.removeAllDatasets();
2276 this.attrs_
.pointSize
= 0.5 * this.getNumericOption('highlightCircleSize');
2278 var packed
= this.gatherDatasets_(this.rolledSeries_
, this.dateWindow_
);
2279 var points
= packed
.points
;
2280 var extremes
= packed
.extremes
;
2281 this.boundaryIds_
= packed
.boundaryIds
;
2283 this.setIndexByName_
= {};
2284 var labels
= this.attr_("labels");
2286 for (var i
= 1; i
< points
.length
; i
++) {
2287 if (!this.visibility()[i
- 1]) continue;
2288 this.layout_
.addDataset(labels
[i
], points
[i
]);
2289 this.datasetIndex_
[i
] = dataIdx
++;
2291 for (var i
= 0; i
< labels
.length
; i
++) {
2292 this.setIndexByName_
[labels
[i
]] = i
;
2295 this.computeYAxisRanges_(extremes
);
2296 this.layout_
.setYAxes(this.axes_
);
2300 // Tell PlotKit to use this new data and render itself
2301 this.layout_
.evaluate();
2302 this.renderGraph_(is_initial_draw
);
2304 if (this.getStringOption("timingName")) {
2305 var end
= new Date();
2306 console
.log(this.getStringOption("timingName") + " - drawGraph: " + (end
- start
) + "ms");
2311 * This does the work of drawing the chart. It assumes that the layout and axis
2312 * scales have already been set (e.g. by predraw_).
2316 Dygraph
.prototype.renderGraph_
= function(is_initial_draw
) {
2317 this.cascadeEvents_('clearChart');
2318 this.plotter_
.clear();
2320 const underlayCallback
= this.getFunctionOption('underlayCallback');
2321 if (underlayCallback
) {
2322 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
2323 // users who expect a deprecated form of this callback.
2324 underlayCallback
.call(this,
2325 this.hidden_ctx_
, this.layout_
.getPlotArea(), this, this);
2329 canvas
: this.hidden_
,
2330 drawingContext
: this.hidden_ctx_
2332 this.cascadeEvents_('willDrawChart', e
);
2333 this.plotter_
.render();
2334 this.cascadeEvents_('didDrawChart', e
);
2335 this.lastRow_
= -1; // because plugins/legend.js clears the legend
2337 // TODO(danvk): is this a performance bottleneck when panning?
2338 // The interaction canvas should already be empty in that situation.
2339 this.canvas_
.getContext('2d').clearRect(0, 0, this.width_
, this.height_
);
2341 const drawCallback
= this.getFunctionOption("drawCallback");
2342 if (drawCallback
!== null) {
2343 drawCallback
.call(this, this, is_initial_draw
);
2345 if (is_initial_draw
) {
2346 this.readyFired_
= true;
2347 while (this.readyFns_
.length
> 0) {
2348 var fn
= this.readyFns_
.pop();
2356 * Determine properties of the y-axes which are independent of the data
2357 * currently being displayed. This includes things like the number of axes and
2358 * the style of the axes. It does not include the range of each axis and its
2360 * This fills in this.axes_.
2361 * axes_ = [ { options } ]
2362 * indices are into the axes_ array.
2364 Dygraph
.prototype.computeYAxes_
= function() {
2365 var axis
, index
, opts
, v
;
2367 // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
2368 // data computation as well as options storage.
2369 // Go through once and add all the axes.
2372 for (axis
= 0; axis
< this.attributes_
.numAxes(); axis
++) {
2373 // Add a new axis, making a copy of its per-axis options.
2374 opts
= { g
: this };
2375 utils
.update(opts
, this.attributes_
.axisOptions(axis
));
2376 this.axes_
[axis
] = opts
;
2379 for (axis
= 0; axis
< this.axes_
.length
; axis
++) {
2381 opts
= this.optionsViewForAxis_('y' + (axis
? '2' : ''));
2382 v
= opts("valueRange");
2383 if (v
) this.axes_
[axis
].valueRange
= v
;
2384 } else { // To keep old behavior
2385 var axes
= this.user_attrs_
.axes
;
2386 if (axes
&& axes
.y2
) {
2387 v
= axes
.y2
.valueRange
;
2388 if (v
) this.axes_
[axis
].valueRange
= v
;
2395 * Returns the number of y-axes on the chart.
2396 * @return {number} the number of axes.
2398 Dygraph
.prototype.numAxes
= function() {
2399 return this.attributes_
.numAxes();
2404 * Returns axis properties for the given series.
2405 * @param {string} setName The name of the series for which to get axis
2406 * properties, e.g. 'Y1'.
2407 * @return {Object} The axis properties.
2409 Dygraph
.prototype.axisPropertiesForSeries
= function(series
) {
2410 // TODO(danvk): handle errors.
2411 return this.axes_
[this.attributes_
.axisForSeries(series
)];
2416 * Determine the value range and tick marks for each axis.
2417 * @param {Object} extremes A mapping from seriesName -> [low, high]
2418 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2420 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2421 var isNullUndefinedOrNaN
= function(num
) {
2422 return isNaN(parseFloat(num
));
2424 var numAxes
= this.attributes_
.numAxes();
2425 var ypadCompat
, span
, series
, ypad
;
2429 // Compute extreme values, a span and tick marks for each axis.
2430 for (var i
= 0; i
< numAxes
; i
++) {
2431 var axis
= this.axes_
[i
];
2432 var logscale
= this.attributes_
.getForAxis("logscale", i
);
2433 var includeZero
= this.attributes_
.getForAxis("includeZero", i
);
2434 var independentTicks
= this.attributes_
.getForAxis("independentTicks", i
);
2435 series
= this.attributes_
.seriesForAxis(i
);
2437 // Add some padding. This supports two Y padding operation modes:
2439 // - backwards compatible (yRangePad not set):
2440 // 10% padding for automatic Y ranges, but not for user-supplied
2441 // ranges, and move a close-to-zero edge to zero, since drawing at the edge
2442 // results in invisible lines. Unfortunately lines drawn at the edge of a
2443 // user-supplied range will still be invisible. If logscale is
2444 // set, add a variable amount of padding at the top but
2445 // none at the bottom.
2447 // - new-style (yRangePad set by the user):
2448 // always add the specified Y padding.
2451 ypad
= 0.1; // add 10%
2452 const yRangePad
= this.getNumericOption('yRangePad');
2453 if (yRangePad
!== null) {
2455 // Convert pixel padding to ratio
2456 ypad
= yRangePad
/ this.plotter_
.area
.h
;
2459 if (series
.length
=== 0) {
2460 // If no series are defined or visible then use a reasonable default
2461 axis
.extremeRange
= [0, 1];
2463 // Calculate the extremes of extremes.
2464 var minY
= Infinity
; // extremes[series[0]][0];
2465 var maxY
= -Infinity
; // extremes[series[0]][1];
2466 var extremeMinY
, extremeMaxY
;
2468 for (var j
= 0; j
< series
.length
; j
++) {
2469 // this skips invisible series
2470 if (!extremes
.hasOwnProperty(series
[j
])) continue;
2472 // Only use valid extremes to stop null data series' from corrupting the scale.
2473 extremeMinY
= extremes
[series
[j
]][0];
2474 if (extremeMinY
!== null) {
2475 minY
= Math
.min(extremeMinY
, minY
);
2477 extremeMaxY
= extremes
[series
[j
]][1];
2478 if (extremeMaxY
!== null) {
2479 maxY
= Math
.max(extremeMaxY
, maxY
);
2483 // Include zero if requested by the user.
2484 if (includeZero
&& !logscale
) {
2485 if (minY
> 0) minY
= 0;
2486 if (maxY
< 0) maxY
= 0;
2489 // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2490 if (minY
== Infinity
) minY
= 0;
2491 if (maxY
== -Infinity
) maxY
= 1;
2494 // special case: if we have no sense of scale, center on the sole value.
2497 span
= Math
.abs(maxY
);
2499 // ... and if the sole value is zero, use range 0-1.
2505 var maxAxisY
= maxY
, minAxisY
= minY
;
2508 maxAxisY
= maxY
+ ypad
* span
;
2511 maxAxisY
= maxY
+ ypad
* span
;
2512 minAxisY
= minY
- ypad
* span
;
2514 // Backwards-compatible behavior: Move the span to start or end at zero if it's
2516 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2517 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2520 axis
.extremeRange
= [minAxisY
, maxAxisY
];
2522 if (axis
.valueRange
) {
2523 // This is a user-set value range for this axis.
2524 var y0
= isNullUndefinedOrNaN(axis
.valueRange
[0]) ? axis
.extremeRange
[0] : axis
.valueRange
[0];
2525 var y1
= isNullUndefinedOrNaN(axis
.valueRange
[1]) ? axis
.extremeRange
[1] : axis
.valueRange
[1];
2526 axis
.computedValueRange
= [y0
, y1
];
2528 axis
.computedValueRange
= axis
.extremeRange
;
2531 // When using yRangePad, adjust the upper/lower bounds to add
2532 // padding unless the user has zoomed/panned the Y axis range
.
2534 y0
= axis
.computedValueRange
[0];
2535 y1
= axis
.computedValueRange
[1];
2536 var y0pct
= ypad
/ (2 * ypad
- 1);
2537 var y1pct
= (ypad
- 1) / (2 * ypad
- 1);
2538 axis
.computedValueRange
[0] = utils
.logRangeFraction(y0
, y1
, y0pct
);
2539 axis
.computedValueRange
[1] = utils
.logRangeFraction(y0
, y1
, y1pct
);
2541 y0
= axis
.computedValueRange
[0];
2542 y1
= axis
.computedValueRange
[1];
2544 axis
.computedValueRange
[0] = y0
- span
* ypad
;
2545 axis
.computedValueRange
[1] = y1
+ span
* ypad
;
2550 if (independentTicks
) {
2551 axis
.independentTicks
= independentTicks
;
2552 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2553 var ticker
= opts('ticker');
2554 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2555 axis
.computedValueRange
[1],
2556 this.plotter_
.area
.h
,
2559 // Define the first independent axis as primary axis.
2560 if (!p_axis
) p_axis
= axis
;
2563 if (p_axis
=== undefined
) {
2564 throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
2566 // Add ticks. By default, all axes inherit the tick positions of the
2567 // primary axis. However, if an axis is specifically marked as having
2568 // independent ticks, then that is permissible as well.
2569 for (var i
= 0; i
< numAxes
; i
++) {
2570 var axis
= this.axes_
[i
];
2572 if (!axis
.independentTicks
) {
2573 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2574 var ticker
= opts('ticker');
2575 var p_ticks
= p_axis
.ticks
;
2576 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2577 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2578 var tick_values
= [];
2579 for (var k
= 0; k
< p_ticks
.length
; k
++) {
2580 var y_frac
= (p_ticks
[k
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2581 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2582 tick_values
.push(y_val
);
2585 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2586 axis
.computedValueRange
[1],
2587 this.plotter_
.area
.h
,
2596 * Detects the type of the str (date or numeric) and sets the various
2597 * formatting attributes in this.attrs_ based on this type.
2598 * @param {string} str An x value.
2601 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2603 var dashPos
= str
.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2
2604 if ((dashPos
> 0 && (str
[dashPos
-1] != 'e' && str
[dashPos
-1] != 'E')) ||
2605 str
.indexOf('/') >= 0 ||
2606 isNaN(parseFloat(str
))) {
2608 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2609 // TODO(danvk): remove support for this format.
2613 this.setXAxisOptions_(isDate
);
2616 Dygraph
.prototype.setXAxisOptions_
= function(isDate
) {
2618 this.attrs_
.xValueParser
= utils
.dateParser
;
2619 this.attrs_
.axes
.x
.valueFormatter
= utils
.dateValueFormatter
;
2620 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.dateTicker
;
2621 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.dateAxisLabelFormatter
;
2623 /** @private (shut up, jsdoc!) */
2624 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2625 // TODO(danvk): use Dygraph.numberValueFormatter here?
2626 /** @private (shut up, jsdoc!) */
2627 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2628 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2629 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2635 * Parses a string in a special csv format. We expect a csv file where each
2636 * line is a date point, and the first field in each line is the date string.
2637 * We also expect that all remaining fields represent series.
2638 * if the errorBars attribute is set, then interpret the fields as:
2639 * date, series1, stddev1, series2, stddev2, ...
2640 * @param {[Object]} data See above.
2642 * @return [Object] An array with one entry for each row. These entries
2643 * are an array of cells in that row. The first entry is the parsed x-value for
2644 * the row. The second, third, etc. are the y-values. These can take on one of
2645 * three forms, depending on the CSV and constructor parameters:
2647 * 2. [ value, stddev ]
2648 * 3. [ low value, center value, high value ]
2650 Dygraph
.prototype.parseCSV_
= function(data
) {
2652 var line_delimiter
= utils
.detectLineDelimiter(data
);
2653 var lines
= data
.split(line_delimiter
|| "\n");
2656 // Use the default delimiter or fall back to a tab if that makes sense.
2657 var delim
= this.getStringOption('delimiter');
2658 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2663 if (!('labels' in this.user_attrs_
)) {
2664 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2666 this.attrs_
.labels
= lines
[0].split(delim
); // NOTE: _not_ user_attrs_.
2667 this.attributes_
.reparseSeries();
2672 var defaultParserSet
= false; // attempt to auto-detect x value type
2673 var expectedCols
= this.attr_("labels").length
;
2674 var outOfOrder
= false;
2675 for (var i
= start
; i
< lines
.length
; i
++) {
2676 var line
= lines
[i
];
2678 if (line
.length
=== 0) continue; // skip blank lines
2679 if (line
[0] == '#') continue; // skip comment lines
2680 var inFields
= line
.split(delim
);
2681 if (inFields
.length
< 2) continue;
2684 if (!defaultParserSet
) {
2685 this.detectTypeFromString_(inFields
[0]);
2686 xParser
= this.getFunctionOption("xValueParser");
2687 defaultParserSet
= true;
2689 fields
[0] = xParser(inFields
[0], this);
2691 // If fractions are expected, parse the numbers as "A/B
"
2692 if (this.fractions_) {
2693 for (j = 1; j < inFields.length; j++) {
2694 // TODO(danvk): figure out an appropriate way to flag parse errors.
2695 vals = inFields[j].split("/");
2696 if (vals.length != 2) {
2697 console.error('Expected fractional "num
/den
" values in CSV data ' +
2698 "but found a value
'" + inFields[j] + "' on line
" +
2699 (1 + i) + " ('" + line + "') which is not of
this form
.");
2702 fields[j] = [utils.parseFloat_(vals[0], i, line),
2703 utils.parseFloat_(vals[1], i, line)];
2706 } else if (this.getBooleanOption("errorBars
")) {
2707 // If there are error bars, values are (value, stddev) pairs
2708 if (inFields.length % 2 != 1) {
2709 console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2710 'but line ' + (1 + i) + ' has an odd number of values (' +
2711 (inFields.length - 1) + "): '" + line + "'");
2713 for (j = 1; j < inFields.length; j += 2) {
2714 fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line),
2715 utils.parseFloat_(inFields[j + 1], i, line)];
2717 } else if (this.getBooleanOption("customBars
")) {
2718 // Bars are a low;center;high tuple
2719 for (j = 1; j < inFields.length; j++) {
2720 var val = inFields[j];
2721 if (/^ *$/.test(val)) {
2722 fields[j] = [null, null, null];
2724 vals = val.split(";");
2725 if (vals.length == 3) {
2726 fields[j] = [ utils.parseFloat_(vals[0], i, line),
2727 utils.parseFloat_(vals[1], i, line),
2728 utils.parseFloat_(vals[2], i, line) ];
2730 console.warn('When using customBars, values must be either blank ' +
2731 'or "low
;center
;high
" tuples (got "' + val +
2732 '" on line ' + (1+i));
2737 // Values are just numbers
2738 for (j = 1; j < inFields.length; j++) {
2739 fields[j] = utils.parseFloat_(inFields[j], i, line);
2742 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2746 if (fields.length != expectedCols) {
2747 console.error("Number of columns
in line
" + i + " (" + fields.length +
2748 ") does not agree
with number of
labels (" + expectedCols +
2752 // If the user specified the 'labels' option and none of the cells of the
2753 // first row parsed correctly, then they probably double-specified the
2754 // labels. We go with the values set in the option, discard this row and
2755 // log a warning to the JS console.
2756 if (i === 0 && this.attr_('labels')) {
2757 var all_null = true;
2758 for (j = 0; all_null && j < fields.length; j++) {
2759 if (fields[j]) all_null = false;
2762 console.warn("The dygraphs
'labels' option is set
, but the first row
" +
2763 "of CSV
data ('" + line + "') appears to also contain
" +
2764 "labels
. Will drop the CSV labels and
use the option
" +
2773 console.warn("CSV is out of order
; order it correctly to speed loading
.");
2774 ret.sort(function(a,b) { return a[0] - b[0]; });
2780 // In native format, all values must be dates or numbers.
2781 // This check isn't perfect but will catch most mistaken uses of strings.
2782 function validateNativeFormat(data) {
2783 const firstRow = data[0];
2784 const firstX = firstRow[0];
2785 if (typeof firstX !== 'number' && !utils.isDateLike(firstX)) {
2786 throw new Error(`Expected number or date but got ${typeof firstX}: ${firstX}.`);
2788 for (let i = 1; i < firstRow.length; i++) {
2789 const val = firstRow[i];
2790 if (val === null || val === undefined) continue;
2791 if (typeof val === 'number') continue;
2792 if (utils.isArrayLike(val)) continue; // e.g. error bars or custom bars.
2793 throw new Error(`Expected number or array but got ${typeof val}: ${val}.`);
2798 * The user has provided their data as a pre-packaged JS array. If the x values
2799 * are numeric, this is the same as dygraphs' internal format. If the x values
2800 * are dates, we need to convert them from Date objects to ms since epoch.
2801 * @param {!Array} data
2802 * @return {Object} data with numeric x values.
2805 Dygraph.prototype.parseArray_ = function(data) {
2806 // Peek at the first x value to see if it's numeric.
2807 if (data.length === 0) {
2808 console.error("Can
't plot empty data set");
2811 if (data[0].length === 0) {
2812 console.error("Data set cannot contain an empty row");
2816 validateNativeFormat(data);
2819 if (this.attr_("labels") === null) {
2820 console.warn("Using default labels. Set labels explicitly via 'labels
' " +
2821 "in the options parameter");
2822 this.attrs_.labels = [ "X" ];
2823 for (i = 1; i < data[0].length; i++) {
2824 this.attrs_.labels.push("Y" + i); // Not user_attrs_.
2826 this.attributes_.reparseSeries();
2828 var num_labels = this.attr_("labels");
2829 if (num_labels.length != data[0].length) {
2830 console.error("Mismatch between number of labels (" + num_labels + ")" +
2831 " and number of columns in array (" + data[0].length + ")");
2836 if (utils.isDateLike(data[0][0])) {
2837 // Some intelligent defaults for a date x-axis.
2838 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2839 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2840 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2842 // Assume they're all dates
.
2843 var parsedData
= utils
.clone(data
);
2844 for (i
= 0; i
< data
.length
; i
++) {
2845 if (parsedData
[i
].length
=== 0) {
2846 console
.error("Row " + (1 + i
) + " of data is empty");
2849 if (parsedData
[i
][0] === null ||
2850 typeof(parsedData
[i
][0].getTime
) != 'function' ||
2851 isNaN(parsedData
[i
][0].getTime())) {
2852 console
.error("x value in row " + (1 + i
) + " is not a Date");
2855 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2859 // Some intelligent defaults for a numeric x-axis.
2860 /** @private (shut up, jsdoc!) */
2861 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2862 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2863 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.numberAxisLabelFormatter
;
2869 * Parses a DataTable object from gviz.
2870 * The data is expected to have a first column that is either a date or a
2871 * number. All subsequent columns must be numbers. If there is a clear mismatch
2872 * between this.xValueParser_ and the type of the first column, it will be
2873 * fixed. Fills out rawData_.
2874 * @param {!google.visualization.DataTable} data See above.
2877 Dygraph
.prototype.parseDataTable_
= function(data
) {
2878 var shortTextForAnnotationNum
= function(num
) {
2879 // converts [0-9]+ [A-Z][a-z]*
2880 // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
2881 // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
2882 var shortText
= String
.fromCharCode(65 /* A */ + num
% 26);
2883 num
= Math
.floor(num
/ 26);
2885 shortText
= String
.fromCharCode(65 /* A */ + (num
- 1) % 26 ) + shortText
.toLowerCase();
2886 num
= Math
.floor((num
- 1) / 26);
2891 var cols
= data
.getNumberOfColumns();
2892 var rows
= data
.getNumberOfRows();
2894 var indepType
= data
.getColumnType(0);
2895 if (indepType
== 'date' || indepType
== 'datetime') {
2896 this.attrs_
.xValueParser
= utils
.dateParser
;
2897 this.attrs_
.axes
.x
.valueFormatter
= utils
.dateValueFormatter
;
2898 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.dateTicker
;
2899 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.dateAxisLabelFormatter
;
2900 } else if (indepType
== 'number') {
2901 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2902 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2903 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2904 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2907 "only 'date', 'datetime' and 'number' types are supported " +
2908 "for column 1 of DataTable input (Got '" + indepType
+ "')");
2911 // Array of the column indices which contain data (and not annotations).
2913 var annotationCols
= {}; // data index -> [annotation cols]
2914 var hasAnnotations
= false;
2916 for (i
= 1; i
< cols
; i
++) {
2917 var type
= data
.getColumnType(i
);
2918 if (type
== 'number') {
2920 } else if (type
== 'string' && this.getBooleanOption('displayAnnotations')) {
2921 // This is OK -- it's an annotation column.
2922 var dataIdx
= colIdx
[colIdx
.length
- 1];
2923 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2924 annotationCols
[dataIdx
] = [i
];
2926 annotationCols
[dataIdx
].push(i
);
2928 hasAnnotations
= true;
2931 "Only 'number' is supported as a dependent type with Gviz." +
2932 " 'string' is only supported if displayAnnotations is true");
2936 // Read column labels
2937 // TODO(danvk): add support back for errorBars
2938 var labels
= [data
.getColumnLabel(0)];
2939 for (i
= 0; i
< colIdx
.length
; i
++) {
2940 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2941 if (this.getBooleanOption("errorBars")) i
+= 1;
2943 this.attrs_
.labels
= labels
;
2944 cols
= labels
.length
;
2947 var outOfOrder
= false;
2948 var annotations
= [];
2949 for (i
= 0; i
< rows
; i
++) {
2951 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2952 data
.getValue(i
, 0) === null) {
2953 console
.warn("Ignoring row " + i
+
2954 " of DataTable because of undefined or null first column.");
2958 if (indepType
== 'date' || indepType
== 'datetime') {
2959 row
.push(data
.getValue(i
, 0).getTime());
2961 row
.push(data
.getValue(i
, 0));
2963 if (!this.getBooleanOption("errorBars")) {
2964 for (j
= 0; j
< colIdx
.length
; j
++) {
2965 var col
= colIdx
[j
];
2966 row
.push(data
.getValue(i
, col
));
2967 if (hasAnnotations
&&
2968 annotationCols
.hasOwnProperty(col
) &&
2969 data
.getValue(i
, annotationCols
[col
][0]) !== null) {
2971 ann
.series
= data
.getColumnLabel(col
);
2973 ann
.shortText
= shortTextForAnnotationNum(annotations
.length
);
2975 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2976 if (k
) ann
.text
+= "\n";
2977 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2979 annotations
.push(ann
);
2983 // Strip out infinities, which give dygraphs problems later on.
2984 for (j
= 0; j
< row
.length
; j
++) {
2985 if (!isFinite(row
[j
])) row
[j
] = null;
2988 for (j
= 0; j
< cols
- 1; j
++) {
2989 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2992 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
2999 console
.warn("DataTable is out of order; order it correctly to speed loading.");
3000 ret
.sort(function(a
,b
) { return a
[0] - b
[0]; });
3002 this.rawData_
= ret
;
3004 if (annotations
.length
> 0) {
3005 this.setAnnotations(annotations
, true);
3007 this.attributes_
.reparseSeries();
3011 * Signals to plugins that the chart data has updated.
3012 * This happens after the data has updated but before the chart has redrawn.
3014 Dygraph
.prototype.cascadeDataDidUpdateEvent_
= function() {
3015 // TODO(danvk): there are some issues checking xAxisRange() and using
3016 // toDomCoords from handlers of this event. The visible range should be set
3017 // when the chart is drawn, not derived from the data.
3018 this.cascadeEvents_('dataDidUpdate', {});
3022 * Get the CSV data. If it's in a function, call that function. If it's in a
3023 * file, do an XMLHttpRequest to get it.
3026 Dygraph
.prototype.start_
= function() {
3027 var data
= this.file_
;
3029 // Functions can return references of all other types.
3030 if (typeof data
== 'function') {
3034 if (utils
.isArrayLike(data
)) {
3035 this.rawData_
= this.parseArray_(data
);
3036 this.cascadeDataDidUpdateEvent_();
3038 } else if (typeof data
== 'object' &&
3039 typeof data
.getColumnRange
== 'function') {
3040 // must be a DataTable from gviz.
3041 this.parseDataTable_(data
);
3042 this.cascadeDataDidUpdateEvent_();
3044 } else if (typeof data
== 'string') {
3045 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3046 var line_delimiter
= utils
.detectLineDelimiter(data
);
3047 if (line_delimiter
) {
3048 this.loadedEvent_(data
);
3052 if (window
.XMLHttpRequest
) {
3053 // Firefox, Opera, IE7, and other browsers will use the native object
3054 req
= new XMLHttpRequest();
3056 // IE 5 and 6 will use the ActiveX control
3057 req
= new ActiveXObject("Microsoft.XMLHTTP");
3061 req
.onreadystatechange
= function () {
3062 if (req
.readyState
== 4) {
3063 if (req
.status
=== 200 || // Normal http
3064 req
.status
=== 0) { // Chrome w/ --allow
-file
-access
-from
-files
3065 caller
.loadedEvent_(req
.responseText
);
3070 req
.open("GET", data
, true);
3074 console
.error("Unknown data format: " + (typeof data
));
3079 * Changes various properties of the graph. These can include:
3081 * <li>file: changes the source data for the graph</li>
3082 * <li>errorBars: changes whether the data contains stddev</li>
3085 * There's a huge variety of options that can be passed to this method. For a
3086 * full list, see http://dygraphs.com/options.html.
3088 * @param {Object} input_attrs The new properties and values
3089 * @param {boolean} block_redraw Usually the chart is redrawn after every
3090 * call to updateOptions(). If you know better, you can pass true to
3091 * explicitly block the redraw. This can be useful for chaining
3092 * updateOptions() calls, avoiding the occasional infinite loop and
3093 * preventing redraws when it's not necessary (e.g. when updating a
3096 Dygraph
.prototype.updateOptions
= function(input_attrs
, block_redraw
) {
3097 if (typeof(block_redraw
) == 'undefined') block_redraw
= false;
3099 // copyUserAttrs_ drops the "file" parameter as a convenience to us.
3100 var file
= input_attrs
.file
;
3101 var attrs
= Dygraph
.copyUserAttrs_(input_attrs
);
3103 // TODO(danvk): this is a mess. Move these options into attr_.
3104 if ('rollPeriod' in attrs
) {
3105 this.rollPeriod_
= attrs
.rollPeriod
;
3107 if ('dateWindow' in attrs
) {
3108 this.dateWindow_
= attrs
.dateWindow
;
3111 // TODO(danvk): validate per-series options.
3116 // highlightCircleSize
3118 // Check if this set options will require new points.
3119 var requiresNewPoints
= utils
.isPixelChangingOptionList(this.attr_("labels"), attrs
);
3121 utils
.updateDeep(this.user_attrs_
, attrs
);
3123 this.attributes_
.reparseSeries();
3126 // This event indicates that the data is about to change, but hasn't yet.
3127 // TODO(danvk): support cancelation of the update via this event.
3128 this.cascadeEvents_('dataWillUpdate', {});
3131 if (!block_redraw
) this.start_();
3133 if (!block_redraw
) {
3134 if (requiresNewPoints
) {
3137 this.renderGraph_(false);
3144 * Make a copy of input attributes, removing file as a convenience.
3146 Dygraph
.copyUserAttrs_
= function(attrs
) {
3148 for (var k
in attrs
) {
3149 if (!attrs
.hasOwnProperty(k
)) continue;
3150 if (k
== 'file') continue;
3151 if (attrs
.hasOwnProperty(k
)) my_attrs
[k
] = attrs
[k
];
3157 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3158 * containing div (which has presumably changed size since the dygraph was
3159 * instantiated. If the width/height are specified, the div will be resized.
3161 * This is far more efficient than destroying and re-instantiating a
3162 * Dygraph, since it doesn't have to reparse the underlying data.
3164 * @param {number} width Width (in pixels)
3165 * @param {number} height Height (in pixels)
3167 Dygraph
.prototype.resize
= function(width
, height
) {
3168 if (this.resize_lock
) {
3171 this.resize_lock
= true;
3173 if ((width
=== null) != (height
=== null)) {
3174 console
.warn("Dygraph.resize() should be called with zero parameters or " +
3175 "two non-NULL parameters. Pretending it was zero.");
3176 width
= height
= null;
3179 var old_width
= this.width_
;
3180 var old_height
= this.height_
;
3183 this.maindiv_
.style
.width
= width
+ "px";
3184 this.maindiv_
.style
.height
= height
+ "px";
3185 this.width_
= width
;
3186 this.height_
= height
;
3188 this.width_
= this.maindiv_
.clientWidth
;
3189 this.height_
= this.maindiv_
.clientHeight
;
3192 if (old_width
!= this.width_
|| old_height
!= this.height_
) {
3193 // Resizing a canvas erases it, even when the size doesn't change, so
3194 // any resize needs to be followed by a redraw.
3195 this.resizeElements_();
3199 this.resize_lock
= false;
3203 * Adjusts the number of points in the rolling average. Updates the graph to
3204 * reflect the new averaging period.
3205 * @param {number} length Number of points over which to average the data.
3207 Dygraph
.prototype.adjustRoll
= function(length
) {
3208 this.rollPeriod_
= length
;
3213 * Returns a boolean array of visibility statuses.
3215 Dygraph
.prototype.visibility
= function() {
3216 // Do lazy-initialization, so that this happens after we know the number of
3218 if (!this.getOption("visibility")) {
3219 this.attrs_
.visibility
= [];
3221 // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs
.
3222 while (this.getOption("visibility").length
< this.numColumns() - 1) {
3223 this.attrs_
.visibility
.push(true);
3225 return this.getOption("visibility");
3229 * Changes the visibility of one or more series.
3231 * @param {number|number[]|object} num the series index or an array of series indices
3232 * or a boolean array of visibility states by index
3233 * or an object mapping series numbers, as keys, to
3234 * visibility state (boolean values)
3235 * @param {boolean} value the visibility state expressed as a boolean
3237 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3238 var x
= this.visibility();
3239 var numIsObject
= false;
3241 if (!Array
.isArray(num
)) {
3242 if (num
!== null && typeof num
=== 'object') {
3250 for (var i
in num
) {
3251 if (num
.hasOwnProperty(i
)) {
3252 if (i
< 0 || i
>= x
.length
) {
3253 console
.warn("Invalid series number in setVisibility: " + i
);
3260 for (var i
= 0; i
< num
.length
; i
++) {
3261 if (typeof num
[i
] === 'boolean') {
3262 if (i
>= x
.length
) {
3263 console
.warn("Invalid series number in setVisibility: " + i
);
3268 if (num
[i
] < 0 || num
[i
] >= x
.length
) {
3269 console
.warn("Invalid series number in setVisibility: " + num
[i
]);
3281 * How large of an area will the dygraph render itself in?
3282 * This is used for testing.
3283 * @return A {width: w, height: h} object.
3286 Dygraph
.prototype.size
= function() {
3287 return { width
: this.width_
, height
: this.height_
};
3291 * Update the list of annotations and redraw the chart.
3292 * See dygraphs.com/annotations.html for more info on how to use annotations.
3293 * @param ann {Array} An array of annotation objects.
3294 * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3296 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3297 // Only add the annotation CSS rule once we know it will be used.
3298 this.annotations_
= ann
;
3299 if (!this.layout_
) {
3300 console
.warn("Tried to setAnnotations before dygraph was ready. " +
3301 "Try setting them in a ready() block. See " +
3302 "dygraphs.com/tests/annotation.html");
3306 this.layout_
.setAnnotations(this.annotations_
);
3307 if (!suppressDraw
) {
3313 * Return the list of annotations.
3315 Dygraph
.prototype.annotations
= function() {
3316 return this.annotations_
;
3320 * Get the list of label names for this graph. The first column is the
3321 * x-axis, so the data series names start at index 1.
3323 * Returns null when labels have not yet been defined.
3325 Dygraph
.prototype.getLabels
= function() {
3326 var labels
= this.attr_("labels");
3327 return labels
? labels
.slice() : null;
3331 * Get the index of a series (column) given its name. The first column is the
3332 * x-axis, so the data series start with index 1.
3334 Dygraph
.prototype.indexFromSetName
= function(name
) {
3335 return this.setIndexByName_
[name
];
3339 * Find the row number corresponding to the given x-value.
3340 * Returns null if there is no such x-value in the data.
3341 * If there are multiple rows with the same x-value, this will return the
3343 * @param {number} xVal The x-value to look for (e.g. millis since epoch).
3344 * @return {?number} The row number, which you can pass to getValue(), or null.
3346 Dygraph
.prototype.getRowForX
= function(xVal
) {
3348 high
= this.numRows() - 1;
3350 while (low
<= high
) {
3351 var idx
= (high
+ low
) >> 1;
3352 var x
= this.getValue(idx
, 0);
3355 } else if (x
> xVal
) {
3357 } else if (low
!= idx
) { // equal, but there may be an earlier match.
3368 * Trigger a callback when the dygraph has drawn itself and is ready to be
3369 * manipulated. This is primarily useful when dygraphs has to do an XHR for the
3370 * data (i.e. a URL is passed as the data source) and the chart is drawn
3371 * asynchronously. If the chart has already drawn, the callback will fire
3374 * This is a good place to call setAnnotation().
3376 * @param {function(!Dygraph)} callback The callback to trigger when the chart
3379 Dygraph
.prototype.ready
= function(callback
) {
3380 if (this.is_initial_draw_
) {
3381 this.readyFns_
.push(callback
);
3383 callback
.call(this, this);
3388 * Add an event handler. This event handler is kept until the graph is
3389 * destroyed with a call to graph.destroy().
3391 * @param {!Node} elem The element to add the event to.
3392 * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
3393 * @param {function(Event):(boolean|undefined)} fn The function to call
3394 * on the event. The function takes one parameter: the event object.
3397 Dygraph
.prototype.addAndTrackEvent
= function(elem
, type
, fn
) {
3398 utils
.addEvent(elem
, type
, fn
);
3399 this.registeredEvents_
.push({elem
, type
, fn
});
3402 Dygraph
.prototype.removeTrackedEvents_
= function() {
3403 if (this.registeredEvents_
) {
3404 for (var idx
= 0; idx
< this.registeredEvents_
.length
; idx
++) {
3405 var reg
= this.registeredEvents_
[idx
];
3406 utils
.removeEvent(reg
.elem
, reg
.type
, reg
.fn
);
3410 this.registeredEvents_
= [];
3414 // Installed plugins, in order of precedence (most-general to most-specific).
3418 RangeSelectorPlugin
, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
3424 // There are many symbols which have historically been available through the
3425 // Dygraph class. These are exported here for backwards compatibility.
3426 Dygraph
.GVizChart
= GVizChart
;
3427 Dygraph
.DASHED_LINE
= utils
.DASHED_LINE
;
3428 Dygraph
.DOT_DASH_LINE
= utils
.DOT_DASH_LINE
;
3429 Dygraph
.dateAxisLabelFormatter
= utils
.dateAxisLabelFormatter
;
3430 Dygraph
.toRGB_
= utils
.toRGB_
;
3431 Dygraph
.findPos
= utils
.findPos
;
3432 Dygraph
.pageX
= utils
.pageX
;
3433 Dygraph
.pageY
= utils
.pageY
;
3434 Dygraph
.dateString_
= utils
.dateString_
;
3435 Dygraph
.defaultInteractionModel
= DygraphInteraction
.defaultModel
;
3436 Dygraph
.nonInteractiveModel
= Dygraph
.nonInteractiveModel_
= DygraphInteraction
.nonInteractiveModel_
;
3437 Dygraph
.Circles
= utils
.Circles
;
3440 Legend
: LegendPlugin
,
3442 Annotations
: AnnotationsPlugin
,
3443 ChartLabels
: ChartLabelsPlugin
,
3445 RangeSelector
: RangeSelectorPlugin
3448 Dygraph
.DataHandlers
= {
3452 DefaultFractionHandler
,
3454 FractionsBarsHandler
3457 Dygraph
.startPan
= DygraphInteraction
.startPan
;
3458 Dygraph
.startZoom
= DygraphInteraction
.startZoom
;
3459 Dygraph
.movePan
= DygraphInteraction
.movePan
;
3460 Dygraph
.moveZoom
= DygraphInteraction
.moveZoom
;
3461 Dygraph
.endPan
= DygraphInteraction
.endPan
;
3462 Dygraph
.endZoom
= DygraphInteraction
.endZoom
;
3464 Dygraph
.numericLinearTicks
= DygraphTickers
.numericLinearTicks
;
3465 Dygraph
.numericTicks
= DygraphTickers
.numericTicks
;
3466 Dygraph
.dateTicker
= DygraphTickers
.dateTicker
;
3467 Dygraph
.Granularity
= DygraphTickers
.Granularity
;
3468 Dygraph
.getDateAxis
= DygraphTickers
.getDateAxis
;
3469 Dygraph
.floatFormat
= utils
.floatFormat
;
3471 export default Dygraph
;