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 // TODO(danvk): factor out a generic, public zoomTo method.
1354 if (!animatedZooms
) {
1355 this.dateWindow_
= null;
1356 this.axes_
.forEach(axis
=> {
1357 if (axis
.valueRange
) delete axis
.valueRange
;
1362 zoomCallback
.call(this, minDate
, maxDate
, this.yAxisRanges());
1367 var oldWindow
=null, newWindow
=null, oldValueRanges
=null, newValueRanges
=null;
1369 oldWindow
= this.xAxisRange();
1370 newWindow
= [minDate
, maxDate
];
1374 oldValueRanges
= this.yAxisRanges();
1375 newValueRanges
= this.yAxisExtremes();
1378 this.doAnimatedZoom(oldWindow
, newWindow
, oldValueRanges
, newValueRanges
,
1380 this.dateWindow_
= null;
1381 this.axes_
.forEach(axis
=> {
1382 if (axis
.valueRange
) delete axis
.valueRange
;
1385 zoomCallback
.call(this, minDate
, maxDate
, this.yAxisRanges());
1391 * Combined animation logic for all zoom functions.
1392 * either the x parameters or y parameters may be null.
1395 Dygraph
.prototype.doAnimatedZoom
= function(oldXRange
, newXRange
, oldYRanges
, newYRanges
, callback
) {
1396 var steps
= this.getBooleanOption("animatedZooms") ?
1397 Dygraph
.ANIMATION_STEPS
: 1;
1400 var valueRanges
= [];
1403 if (oldXRange
!== null && newXRange
!== null) {
1404 for (step
= 1; step
<= steps
; step
++) {
1405 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1406 windows
[step
-1] = [oldXRange
[0]*(1-frac
) + frac
*newXRange
[0],
1407 oldXRange
[1]*(1-frac
) + frac
*newXRange
[1]];
1411 if (oldYRanges
!== null && newYRanges
!== null) {
1412 for (step
= 1; step
<= steps
; step
++) {
1413 frac
= Dygraph
.zoomAnimationFunction(step
, steps
);
1415 for (var j
= 0; j
< this.axes_
.length
; j
++) {
1416 thisRange
.push([oldYRanges
[j
][0]*(1-frac
) + frac
*newYRanges
[j
][0],
1417 oldYRanges
[j
][1]*(1-frac
) + frac
*newYRanges
[j
][1]]);
1419 valueRanges
[step
-1] = thisRange
;
1423 utils
.repeatAndCleanup(step
=> {
1424 if (valueRanges
.length
) {
1425 for (var i
= 0; i
< this.axes_
.length
; i
++) {
1426 var w
= valueRanges
[step
][i
];
1427 this.axes_
[i
].valueRange
= [w
[0], w
[1]];
1430 if (windows
.length
) {
1431 this.dateWindow_
= windows
[step
];
1434 }, steps
, Dygraph
.ANIMATION_DURATION
/ steps
, callback
);
1438 * Get the current graph's area object.
1440 * Returns: {x, y, w, h}
1442 Dygraph
.prototype.getArea
= function() {
1443 return this.plotter_
.area
;
1447 * Convert a mouse event to DOM coordinates relative to the graph origin.
1449 * Returns a two-element array: [X, Y].
1451 Dygraph
.prototype.eventToDomCoords
= function(event
) {
1452 if (event
.offsetX
&& event
.offsetY
) {
1453 return [ event
.offsetX
, event
.offsetY
];
1455 var eventElementPos
= utils
.findPos(this.mouseEventElement_
);
1456 var canvasx
= utils
.pageX(event
) - eventElementPos
.x
;
1457 var canvasy
= utils
.pageY(event
) - eventElementPos
.y
;
1458 return [canvasx
, canvasy
];
1463 * Given a canvas X coordinate, find the closest row.
1464 * @param {number} domX graph-relative DOM X coordinate
1465 * Returns {number} row number.
1468 Dygraph
.prototype.findClosestRow
= function(domX
) {
1469 var minDistX
= Infinity
;
1470 var closestRow
= -1;
1471 var sets
= this.layout_
.points
;
1472 for (var i
= 0; i
< sets
.length
; i
++) {
1473 var points
= sets
[i
];
1474 var len
= points
.length
;
1475 for (var j
= 0; j
< len
; j
++) {
1476 var point
= points
[j
];
1477 if (!utils
.isValidPoint(point
, true)) continue;
1478 var dist
= Math
.abs(point
.canvasx
- domX
);
1479 if (dist
< minDistX
) {
1481 closestRow
= point
.idx
;
1490 * Given canvas X,Y coordinates, find the closest point.
1492 * This finds the individual data point across all visible series
1493 * that's closest to the supplied DOM coordinates using the standard
1494 * Euclidean X,Y distance.
1496 * @param {number} domX graph-relative DOM X coordinate
1497 * @param {number} domY graph-relative DOM Y coordinate
1498 * Returns: {row, seriesName, point}
1501 Dygraph
.prototype.findClosestPoint
= function(domX
, domY
) {
1502 var minDist
= Infinity
;
1503 var dist
, dx
, dy
, point
, closestPoint
, closestSeries
, closestRow
;
1504 for ( var setIdx
= this.layout_
.points
.length
- 1 ; setIdx
>= 0 ; --setIdx
) {
1505 var points
= this.layout_
.points
[setIdx
];
1506 for (var i
= 0; i
< points
.length
; ++i
) {
1508 if (!utils
.isValidPoint(point
)) continue;
1509 dx
= point
.canvasx
- domX
;
1510 dy
= point
.canvasy
- domY
;
1511 dist
= dx
* dx
+ dy
* dy
;
1512 if (dist
< minDist
) {
1514 closestPoint
= point
;
1515 closestSeries
= setIdx
;
1516 closestRow
= point
.idx
;
1520 var name
= this.layout_
.setNames
[closestSeries
];
1529 * Given canvas X,Y coordinates, find the touched area in a stacked graph.
1531 * This first finds the X data point closest to the supplied DOM X coordinate,
1532 * then finds the series which puts the Y coordinate on top of its filled area,
1533 * using linear interpolation between adjacent point pairs.
1535 * @param {number} domX graph-relative DOM X coordinate
1536 * @param {number} domY graph-relative DOM Y coordinate
1537 * Returns: {row, seriesName, point}
1540 Dygraph
.prototype.findStackedPoint
= function(domX
, domY
) {
1541 var row
= this.findClosestRow(domX
);
1542 var closestPoint
, closestSeries
;
1543 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; ++setIdx
) {
1544 var boundary
= this.getLeftBoundary_(setIdx
);
1545 var rowIdx
= row
- boundary
;
1546 var points
= this.layout_
.points
[setIdx
];
1547 if (rowIdx
>= points
.length
) continue;
1548 var p1
= points
[rowIdx
];
1549 if (!utils
.isValidPoint(p1
)) continue;
1550 var py
= p1
.canvasy
;
1551 if (domX
> p1
.canvasx
&& rowIdx
+ 1 < points
.length
) {
1552 // interpolate series Y value using next point
1553 var p2
= points
[rowIdx
+ 1];
1554 if (utils
.isValidPoint(p2
)) {
1555 var dx
= p2
.canvasx
- p1
.canvasx
;
1557 var r
= (domX
- p1
.canvasx
) / dx
;
1558 py
+= r
* (p2
.canvasy
- p1
.canvasy
);
1561 } else if (domX
< p1
.canvasx
&& rowIdx
> 0) {
1562 // interpolate series Y value using previous point
1563 var p0
= points
[rowIdx
- 1];
1564 if (utils
.isValidPoint(p0
)) {
1565 var dx
= p1
.canvasx
- p0
.canvasx
;
1567 var r
= (p1
.canvasx
- domX
) / dx
;
1568 py
+= r
* (p0
.canvasy
- p1
.canvasy
);
1572 // Stop if the point (domX, py) is above this series' upper edge
1573 if (setIdx
=== 0 || py
< domY
) {
1575 closestSeries
= setIdx
;
1578 var name
= this.layout_
.setNames
[closestSeries
];
1587 * When the mouse moves in the canvas, display information about a nearby data
1588 * point and draw dots over those points in the data series. This function
1589 * takes care of cleanup of previously-drawn dots.
1590 * @param {Object} event The mousemove event from the browser.
1593 Dygraph
.prototype.mouseMove_
= function(event
) {
1594 // This prevents JS errors when mousing over the canvas before data loads.
1595 var points
= this.layout_
.points
;
1596 if (points
=== undefined
|| points
=== null) return;
1598 var canvasCoords
= this.eventToDomCoords(event
);
1599 var canvasx
= canvasCoords
[0];
1600 var canvasy
= canvasCoords
[1];
1602 var highlightSeriesOpts
= this.getOption("highlightSeriesOpts");
1603 var selectionChanged
= false;
1604 if (highlightSeriesOpts
&& !this.isSeriesLocked()) {
1606 if (this.getBooleanOption("stackedGraph")) {
1607 closest
= this.findStackedPoint(canvasx
, canvasy
);
1609 closest
= this.findClosestPoint(canvasx
, canvasy
);
1611 selectionChanged
= this.setSelection(closest
.row
, closest
.seriesName
);
1613 var idx
= this.findClosestRow(canvasx
);
1614 selectionChanged
= this.setSelection(idx
);
1617 var callback
= this.getFunctionOption("highlightCallback");
1618 if (callback
&& selectionChanged
) {
1619 callback
.call(this, event
,
1623 this.highlightSet_
);
1628 * Fetch left offset from the specified set index or if not passed, the
1629 * first defined boundaryIds record (see bug #236).
1632 Dygraph
.prototype.getLeftBoundary_
= function(setIdx
) {
1633 if (this.boundaryIds_
[setIdx
]) {
1634 return this.boundaryIds_
[setIdx
][0];
1636 for (var i
= 0; i
< this.boundaryIds_
.length
; i
++) {
1637 if (this.boundaryIds_
[i
] !== undefined
) {
1638 return this.boundaryIds_
[i
][0];
1645 Dygraph
.prototype.animateSelection_
= function(direction
) {
1646 var totalSteps
= 10;
1648 if (this.fadeLevel
=== undefined
) this.fadeLevel
= 0;
1649 if (this.animateId
=== undefined
) this.animateId
= 0;
1650 var start
= this.fadeLevel
;
1651 var steps
= direction
< 0 ? start
: totalSteps
- start
;
1653 if (this.fadeLevel
) {
1654 this.updateSelection_(1.0);
1659 var thisId
= ++this.animateId
;
1661 var cleanupIfClearing
= function() {
1662 // if we haven't reached fadeLevel 0 in the max frame time,
1663 // ensure that the clear happens and just go to 0
1664 if (that
.fadeLevel
!== 0 && direction
< 0) {
1666 that
.clearSelection();
1669 utils
.repeatAndCleanup(
1671 // ignore simultaneous animations
1672 if (that
.animateId
!= thisId
) return;
1674 that
.fadeLevel
+= direction
;
1675 if (that
.fadeLevel
=== 0) {
1676 that
.clearSelection();
1678 that
.updateSelection_(that
.fadeLevel
/ totalSteps
);
1681 steps
, millis
, cleanupIfClearing
);
1685 * Draw dots over the selectied points in the data series. This function
1686 * takes care of cleanup of previously-drawn dots.
1689 Dygraph
.prototype.updateSelection_
= function(opt_animFraction
) {
1690 /*var defaultPrevented = */
1691 this.cascadeEvents_('select', {
1692 selectedRow
: this.lastRow_
=== -1 ? undefined
: this.lastRow_
,
1693 selectedX
: this.lastx_
=== -1 ? undefined
: this.lastx_
,
1694 selectedPoints
: this.selPoints_
1696 // TODO(danvk): use defaultPrevented here?
1698 // Clear the previously drawn vertical, if there is one
1700 var ctx
= this.canvas_ctx_
;
1701 if (this.getOption('highlightSeriesOpts')) {
1702 ctx
.clearRect(0, 0, this.width_
, this.height_
);
1703 var alpha
= 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha');
1704 var backgroundColor
= utils
.toRGB_(this.getOption('highlightSeriesBackgroundColor'));
1707 // Activating background fade includes an animation effect for a gradual
1708 // fade. TODO(klausw): make this independently configurable if it causes
1709 // issues? Use a shared preference to control animations?
1710 var animateBackgroundFade
= true;
1711 if (animateBackgroundFade
) {
1712 if (opt_animFraction
=== undefined
) {
1713 // start a new animation
1714 this.animateSelection_(1);
1717 alpha
*= opt_animFraction
;
1719 ctx
.fillStyle
= 'rgba(' + backgroundColor
.r
+ ',' + backgroundColor
.g
+ ',' + backgroundColor
.b
+ ',' + alpha
+ ')';
1720 ctx
.fillRect(0, 0, this.width_
, this.height_
);
1723 // Redraw only the highlighted series in the interactive canvas (not the
1724 // static plot canvas, which is where series are usually drawn).
1725 this.plotter_
._renderLineChart(this.highlightSet_
, ctx
);
1726 } else if (this.previousVerticalX_
>= 0) {
1727 // Determine the maximum highlight circle size.
1728 var maxCircleSize
= 0;
1729 var labels
= this.attr_('labels');
1730 for (i
= 1; i
< labels
.length
; i
++) {
1731 var r
= this.getNumericOption('highlightCircleSize', labels
[i
]);
1732 if (r
> maxCircleSize
) maxCircleSize
= r
;
1734 var px
= this.previousVerticalX_
;
1735 ctx
.clearRect(px
- maxCircleSize
- 1, 0,
1736 2 * maxCircleSize
+ 2, this.height_
);
1739 if (this.selPoints_
.length
> 0) {
1740 // Draw colored circles over the center of each selected point
1741 var canvasx
= this.selPoints_
[0].canvasx
;
1743 for (i
= 0; i
< this.selPoints_
.length
; i
++) {
1744 var pt
= this.selPoints_
[i
];
1745 if (isNaN(pt
.canvasy
)) continue;
1747 var circleSize
= this.getNumericOption('highlightCircleSize', pt
.name
);
1748 var callback
= this.getFunctionOption("drawHighlightPointCallback", pt
.name
);
1749 var color
= this.plotter_
.colors
[pt
.name
];
1751 callback
= utils
.Circles
.DEFAULT
;
1753 ctx
.lineWidth
= this.getNumericOption('strokeWidth', pt
.name
);
1754 ctx
.strokeStyle
= color
;
1755 ctx
.fillStyle
= color
;
1756 callback
.call(this, this, pt
.name
, ctx
, canvasx
, pt
.canvasy
,
1757 color
, circleSize
, pt
.idx
);
1761 this.previousVerticalX_
= canvasx
;
1766 * Manually set the selected points and display information about them in the
1767 * legend. The selection can be cleared using clearSelection() and queried
1768 * using getSelection().
1770 * To set a selected series but not a selected point, call setSelection with
1771 * row=false and the selected series name.
1773 * @param {number} row Row number that should be highlighted (i.e. appear with
1774 * hover dots on the chart).
1775 * @param {seriesName} optional series name to highlight that series with the
1776 * the highlightSeriesOpts setting.
1777 * @param { locked } optional If true, keep seriesName selected when mousing
1778 * over the graph, disabling closest-series highlighting. Call clearSelection()
1781 Dygraph
.prototype.setSelection
= function(row
, opt_seriesName
, opt_locked
) {
1782 // Extract the points we've selected
1783 this.selPoints_
= [];
1785 var changed
= false;
1786 if (row
!== false && row
>= 0) {
1787 if (row
!= this.lastRow_
) changed
= true;
1788 this.lastRow_
= row
;
1789 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; ++setIdx
) {
1790 var points
= this.layout_
.points
[setIdx
];
1791 // Check if the point at the appropriate index is the point we're looking
1792 // for. If it is, just use it, otherwise search the array for a point
1793 // in the proper place.
1794 var setRow
= row
- this.getLeftBoundary_(setIdx
);
1795 if (setRow
>= 0 && setRow
< points
.length
&& points
[setRow
].idx
== row
) {
1796 var point
= points
[setRow
];
1797 if (point
.yval
!== null) this.selPoints_
.push(point
);
1799 for (var pointIdx
= 0; pointIdx
< points
.length
; ++pointIdx
) {
1800 var point
= points
[pointIdx
];
1801 if (point
.idx
== row
) {
1802 if (point
.yval
!== null) {
1803 this.selPoints_
.push(point
);
1811 if (this.lastRow_
>= 0) changed
= true;
1815 if (this.selPoints_
.length
) {
1816 this.lastx_
= this.selPoints_
[0].xval
;
1821 if (opt_seriesName
!== undefined
) {
1822 if (this.highlightSet_
!== opt_seriesName
) changed
= true;
1823 this.highlightSet_
= opt_seriesName
;
1826 if (opt_locked
!== undefined
) {
1827 this.lockedSet_
= opt_locked
;
1831 this.updateSelection_(undefined
);
1837 * The mouse has left the canvas. Clear out whatever artifacts remain
1838 * @param {Object} event the mouseout event from the browser.
1841 Dygraph
.prototype.mouseOut_
= function(event
) {
1842 if (this.getFunctionOption("unhighlightCallback")) {
1843 this.getFunctionOption("unhighlightCallback").call(this, event
);
1846 if (this.getBooleanOption("hideOverlayOnMouseOut") && !this.lockedSet_
) {
1847 this.clearSelection();
1852 * Clears the current selection (i.e. points that were highlighted by moving
1853 * the mouse over the chart).
1855 Dygraph
.prototype.clearSelection
= function() {
1856 this.cascadeEvents_('deselect', {});
1858 this.lockedSet_
= false;
1859 // Get rid of the overlay data
1860 if (this.fadeLevel
) {
1861 this.animateSelection_(-1);
1864 this.canvas_ctx_
.clearRect(0, 0, this.width_
, this.height_
);
1866 this.selPoints_
= [];
1869 this.highlightSet_
= null;
1873 * Returns the number of the currently selected row. To get data for this row,
1874 * you can use the getValue method.
1875 * @return {number} row number, or -1 if nothing is selected
1877 Dygraph
.prototype.getSelection
= function() {
1878 if (!this.selPoints_
|| this.selPoints_
.length
< 1) {
1882 for (var setIdx
= 0; setIdx
< this.layout_
.points
.length
; setIdx
++) {
1883 var points
= this.layout_
.points
[setIdx
];
1884 for (var row
= 0; row
< points
.length
; row
++) {
1885 if (points
[row
].x
== this.selPoints_
[0].x
) {
1886 return points
[row
].idx
;
1894 * Returns the name of the currently-highlighted series.
1895 * Only available when the highlightSeriesOpts option is in use.
1897 Dygraph
.prototype.getHighlightSeries
= function() {
1898 return this.highlightSet_
;
1902 * Returns true if the currently-highlighted series was locked
1903 * via setSelection(..., seriesName, true).
1905 Dygraph
.prototype.isSeriesLocked
= function() {
1906 return this.lockedSet_
;
1910 * Fires when there's data available to be graphed.
1911 * @param {string} data Raw CSV data to be plotted
1914 Dygraph
.prototype.loadedEvent_
= function(data
) {
1915 this.rawData_
= this.parseCSV_(data
);
1916 this.cascadeDataDidUpdateEvent_();
1921 * Add ticks on the x-axis representing years, months, quarters, weeks, or days
1924 Dygraph
.prototype.addXTicks_
= function() {
1925 // Determine the correct ticks scale on the x-axis: quarterly, monthly, ...
1927 if (this.dateWindow_
) {
1928 range
= [this.dateWindow_
[0], this.dateWindow_
[1]];
1930 range
= this.xAxisExtremes();
1933 var xAxisOptionsView
= this.optionsViewForAxis_('x');
1934 var xTicks
= xAxisOptionsView('ticker')(
1937 this.plotter_
.area
.w
, // TODO(danvk): should be area.width
1940 // var msg = 'ticker(' + range[0] + ', ' + range[1] + ', ' + this.width_ + ', ' + this.attr_('pixelsPerXLabel') + ') -> ' + JSON.stringify(xTicks);
1941 // console.log(msg);
1942 this.layout_
.setXTicks(xTicks
);
1946 * Returns the correct handler class for the currently set options.
1949 Dygraph
.prototype.getHandlerClass_
= function() {
1951 if (this.attr_('dataHandler')) {
1952 handlerClass
= this.attr_('dataHandler');
1953 } else if (this.fractions_
) {
1954 if (this.getBooleanOption('errorBars')) {
1955 handlerClass
= FractionsBarsHandler
;
1957 handlerClass
= DefaultFractionHandler
;
1959 } else if (this.getBooleanOption('customBars')) {
1960 handlerClass
= CustomBarsHandler
;
1961 } else if (this.getBooleanOption('errorBars')) {
1962 handlerClass
= ErrorBarsHandler
;
1964 handlerClass
= DefaultHandler
;
1966 return handlerClass
;
1971 * This function is called once when the chart's data is changed or the options
1972 * dictionary is updated. It is _not_ called when the user pans or zooms. The
1973 * idea is that values derived from the chart's data can be computed here,
1974 * rather than every time the chart is drawn. This includes things like the
1975 * number of axes, rolling averages, etc.
1977 Dygraph
.prototype.predraw_
= function() {
1978 var start
= new Date();
1980 // Create the correct dataHandler
1981 this.dataHandler_
= new (this.getHandlerClass_())();
1983 this.layout_
.computePlotArea();
1985 // TODO(danvk): move more computations out of drawGraph_ and into here.
1986 this.computeYAxes_();
1988 if (!this.is_initial_draw_
) {
1989 this.canvas_ctx_
.restore();
1990 this.hidden_ctx_
.restore();
1993 this.canvas_ctx_
.save();
1994 this.hidden_ctx_
.save();
1996 // Create a new plotter.
1997 this.plotter_
= new DygraphCanvasRenderer(this,
2002 // The roller sits in the bottom left corner of the chart. We don't know where
2003 // this will be until the options are available, so it's positioned here.
2004 this.createRollInterface_();
2006 this.cascadeEvents_('predraw');
2008 // Convert the raw data (a 2D array) into the internal format and compute
2009 // rolling averages.
2010 this.rolledSeries_
= [null]; // x-axis is the first series and it's special
2011 for (var i
= 1; i
< this.numColumns(); i
++) {
2012 // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too
.
2013 var series
= this.dataHandler_
.extractSeries(this.rawData_
, i
, this.attributes_
);
2014 if (this.rollPeriod_
> 1) {
2015 series
= this.dataHandler_
.rollingAverage(series
, this.rollPeriod_
, this.attributes_
);
2018 this.rolledSeries_
.push(series
);
2021 // If the data or options have changed, then we'd better redraw.
2024 // This is used to determine whether to do various animations.
2025 var end
= new Date();
2026 this.drawingTimeMs_
= (end
- start
);
2032 * xval_* and yval_* are the original unscaled data values,
2033 * while x_* and y_* are scaled to the range (0.0-1.0) for plotting.
2034 * yval_stacked is the cumulative Y value used for stacking graphs,
2035 * and bottom/top/minus/plus are used for error bar graphs.
2042 * y_bottom: ?number,
2044 * y_stacked: ?number,
2046 * yval_minus: ?number,
2048 * yval_plus: ?number,
2052 Dygraph
.PointType
= undefined
;
2055 * Calculates point stacking for stackedGraph=true.
2057 * For stacking purposes, interpolate or extend neighboring data across
2058 * NaN values based on stackedGraphNaNFill settings. This is for display
2059 * only, the underlying data value as shown in the legend remains NaN.
2061 * @param {Array.<Dygraph.PointType>} points Point array for a single series.
2062 * Updates each Point's yval_stacked property.
2063 * @param {Array.<number>} cumulativeYval Accumulated top-of-graph stacked Y
2064 * values for the series seen so far. Index is the row number. Updated
2065 * based on the current series's values.
2066 * @param {Array.<number>} seriesExtremes Min and max values, updated
2067 * to reflect the stacked values.
2068 * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or
2072 Dygraph
.stackPoints_
= function(
2073 points
, cumulativeYval
, seriesExtremes
, fillMethod
) {
2074 var lastXval
= null;
2075 var prevPoint
= null;
2076 var nextPoint
= null;
2077 var nextPointIdx
= -1;
2079 // Find the next stackable point starting from the given index.
2080 var updateNextPoint
= function(idx
) {
2081 // If we've previously found a non-NaN point and haven't gone past it yet,
2083 if (nextPointIdx
>= idx
) return;
2085 // We haven't found a non-NaN point yet or have moved past it,
2086 // look towards the right to find a non-NaN point.
2087 for (var j
= idx
; j
< points
.length
; ++j
) {
2088 // Clear out a previously-found point (if any) since it's no longer
2089 // valid, we shouldn't use it for interpolation anymore.
2091 if (!isNaN(points
[j
].yval
) && points
[j
].yval
!== null) {
2093 nextPoint
= points
[j
];
2099 for (var i
= 0; i
< points
.length
; ++i
) {
2100 var point
= points
[i
];
2101 var xval
= point
.xval
;
2102 if (cumulativeYval
[xval
] === undefined
) {
2103 cumulativeYval
[xval
] = 0;
2106 var actualYval
= point
.yval
;
2107 if (isNaN(actualYval
) || actualYval
=== null) {
2108 if(fillMethod
== 'none') {
2111 // Interpolate/extend
for stacking purposes
if possible
.
2113 if (prevPoint
&& nextPoint
&& fillMethod
!= 'none') {
2114 // Use linear interpolation between prevPoint and nextPoint.
2115 actualYval
= prevPoint
.yval
+ (nextPoint
.yval
- prevPoint
.yval
) *
2116 ((xval
- prevPoint
.xval
) / (nextPoint
.xval
- prevPoint
.xval
));
2117 } else if (prevPoint
&& fillMethod
== 'all') {
2118 actualYval
= prevPoint
.yval
;
2119 } else if (nextPoint
&& fillMethod
== 'all') {
2120 actualYval
= nextPoint
.yval
;
2129 var stackedYval
= cumulativeYval
[xval
];
2130 if (lastXval
!= xval
) {
2131 // If an x-value is repeated, we ignore the duplicates.
2132 stackedYval
+= actualYval
;
2133 cumulativeYval
[xval
] = stackedYval
;
2137 point
.yval_stacked
= stackedYval
;
2139 if (stackedYval
> seriesExtremes
[1]) {
2140 seriesExtremes
[1] = stackedYval
;
2142 if (stackedYval
< seriesExtremes
[0]) {
2143 seriesExtremes
[0] = stackedYval
;
2150 * Loop over all fields and create datasets, calculating extreme y-values for
2151 * each series and extreme x-indices as we go.
2153 * dateWindow is passed in as an explicit parameter so that we can compute
2154 * extreme values "speculatively", i.e. without actually setting state on the
2157 * @param {Array.<Array.<Array.<(number|Array<number>)>>} rolledSeries, where
2158 * rolledSeries[seriesIndex][row] = raw point, where
2159 * seriesIndex is the column number starting with 1, and
2160 * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]].
2161 * @param {?Array.<number>} dateWindow [xmin, xmax] pair, or null.
2163 * points: Array.<Array.<Dygraph.PointType>>,
2164 * seriesExtremes: Array.<Array.<number>>,
2165 * boundaryIds: Array.<number>}}
2168 Dygraph
.prototype.gatherDatasets_
= function(rolledSeries
, dateWindow
) {
2169 var boundaryIds
= [];
2171 var cumulativeYval
= []; // For stacked series.
2172 var extremes
= {}; // series name -> [low, high]
2173 var seriesIdx
, sampleIdx
;
2174 var firstIdx
, lastIdx
;
2177 // Loop over the fields (series). Go from the last to the first,
2178 // because if they're stacked that's how we accumulate the values.
2179 var num_series
= rolledSeries
.length
- 1;
2181 for (seriesIdx
= num_series
; seriesIdx
>= 1; seriesIdx
--) {
2182 if (!this.visibility()[seriesIdx
- 1]) continue;
2184 // Prune down to the desired range, if necessary (for zooming)
2185 // Because there can be lines going to points outside of the visible area,
2186 // we actually prune to visible points, plus one on either side.
2188 series
= rolledSeries
[seriesIdx
];
2189 var low
= dateWindow
[0];
2190 var high
= dateWindow
[1];
2192 // TODO(danvk): do binary search instead of linear search.
2193 // TODO(danvk): pass firstIdx and lastIdx directly to the renderer.
2196 for (sampleIdx
= 0; sampleIdx
< series
.length
; sampleIdx
++) {
2197 if (series
[sampleIdx
][0] >= low
&& firstIdx
=== null) {
2198 firstIdx
= sampleIdx
;
2200 if (series
[sampleIdx
][0] <= high
) {
2201 lastIdx
= sampleIdx
;
2205 if (firstIdx
=== null) firstIdx
= 0;
2206 var correctedFirstIdx
= firstIdx
;
2207 var isInvalidValue
= true;
2208 while (isInvalidValue
&& correctedFirstIdx
> 0) {
2209 correctedFirstIdx
--;
2210 // check if the y value is null.
2211 isInvalidValue
= series
[correctedFirstIdx
][1] === null;
2214 if (lastIdx
=== null) lastIdx
= series
.length
- 1;
2215 var correctedLastIdx
= lastIdx
;
2216 isInvalidValue
= true;
2217 while (isInvalidValue
&& correctedLastIdx
< series
.length
- 1) {
2219 isInvalidValue
= series
[correctedLastIdx
][1] === null;
2222 if (correctedFirstIdx
!==firstIdx
) {
2223 firstIdx
= correctedFirstIdx
;
2225 if (correctedLastIdx
!== lastIdx
) {
2226 lastIdx
= correctedLastIdx
;
2229 boundaryIds
[seriesIdx
-1] = [firstIdx
, lastIdx
];
2231 // .slice's end is exclusive, we want to include lastIdx.
2232 series
= series
.slice(firstIdx
, lastIdx
+ 1);
2234 series
= rolledSeries
[seriesIdx
];
2235 boundaryIds
[seriesIdx
-1] = [0, series
.length
-1];
2238 var seriesName
= this.attr_("labels")[seriesIdx
];
2239 var seriesExtremes
= this.dataHandler_
.getExtremeYValues(series
,
2240 dateWindow
, this.getBooleanOption("stepPlot",seriesName
));
2242 var seriesPoints
= this.dataHandler_
.seriesToPoints(series
,
2243 seriesName
, boundaryIds
[seriesIdx
-1][0]);
2245 if (this.getBooleanOption("stackedGraph")) {
2246 axisIdx
= this.attributes_
.axisForSeries(seriesName
);
2247 if (cumulativeYval
[axisIdx
] === undefined
) {
2248 cumulativeYval
[axisIdx
] = [];
2250 Dygraph
.stackPoints_(seriesPoints
, cumulativeYval
[axisIdx
], seriesExtremes
,
2251 this.getBooleanOption("stackedGraphNaNFill"));
2254 extremes
[seriesName
] = seriesExtremes
;
2255 points
[seriesIdx
] = seriesPoints
;
2258 return { points
: points
, extremes
: extremes
, boundaryIds
: boundaryIds
};
2262 * Update the graph with new data. This method is called when the viewing area
2263 * has changed. If the underlying data or options have changed, predraw_ will
2264 * be called before drawGraph_ is called.
2268 Dygraph
.prototype.drawGraph_
= function() {
2269 var start
= new Date();
2271 // This is used to set the second parameter to drawCallback, below.
2272 var is_initial_draw
= this.is_initial_draw_
;
2273 this.is_initial_draw_
= false;
2275 this.layout_
.removeAllDatasets();
2277 this.attrs_
.pointSize
= 0.5 * this.getNumericOption('highlightCircleSize');
2279 var packed
= this.gatherDatasets_(this.rolledSeries_
, this.dateWindow_
);
2280 var points
= packed
.points
;
2281 var extremes
= packed
.extremes
;
2282 this.boundaryIds_
= packed
.boundaryIds
;
2284 this.setIndexByName_
= {};
2285 var labels
= this.attr_("labels");
2287 for (var i
= 1; i
< points
.length
; i
++) {
2288 if (!this.visibility()[i
- 1]) continue;
2289 this.layout_
.addDataset(labels
[i
], points
[i
]);
2290 this.datasetIndex_
[i
] = dataIdx
++;
2292 for (var i
= 0; i
< labels
.length
; i
++) {
2293 this.setIndexByName_
[labels
[i
]] = i
;
2296 this.computeYAxisRanges_(extremes
);
2297 this.layout_
.setYAxes(this.axes_
);
2301 // Tell PlotKit to use this new data and render itself
2302 this.layout_
.evaluate();
2303 this.renderGraph_(is_initial_draw
);
2305 if (this.getStringOption("timingName")) {
2306 var end
= new Date();
2307 console
.log(this.getStringOption("timingName") + " - drawGraph: " + (end
- start
) + "ms");
2312 * This does the work of drawing the chart. It assumes that the layout and axis
2313 * scales have already been set (e.g. by predraw_).
2317 Dygraph
.prototype.renderGraph_
= function(is_initial_draw
) {
2318 this.cascadeEvents_('clearChart');
2319 this.plotter_
.clear();
2321 const underlayCallback
= this.getFunctionOption('underlayCallback');
2322 if (underlayCallback
) {
2323 // NOTE: we pass the dygraph object to this callback twice to avoid breaking
2324 // users who expect a deprecated form of this callback.
2325 underlayCallback
.call(this,
2326 this.hidden_ctx_
, this.layout_
.getPlotArea(), this, this);
2330 canvas
: this.hidden_
,
2331 drawingContext
: this.hidden_ctx_
2333 this.cascadeEvents_('willDrawChart', e
);
2334 this.plotter_
.render();
2335 this.cascadeEvents_('didDrawChart', e
);
2336 this.lastRow_
= -1; // because plugins/legend.js clears the legend
2338 // TODO(danvk): is this a performance bottleneck when panning?
2339 // The interaction canvas should already be empty in that situation.
2340 this.canvas_
.getContext('2d').clearRect(0, 0, this.width_
, this.height_
);
2342 const drawCallback
= this.getFunctionOption("drawCallback");
2343 if (drawCallback
!== null) {
2344 drawCallback
.call(this, this, is_initial_draw
);
2346 if (is_initial_draw
) {
2347 this.readyFired_
= true;
2348 while (this.readyFns_
.length
> 0) {
2349 var fn
= this.readyFns_
.pop();
2357 * Determine properties of the y-axes which are independent of the data
2358 * currently being displayed. This includes things like the number of axes and
2359 * the style of the axes. It does not include the range of each axis and its
2361 * This fills in this.axes_.
2362 * axes_ = [ { options } ]
2363 * indices are into the axes_ array.
2365 Dygraph
.prototype.computeYAxes_
= function() {
2366 var axis
, index
, opts
, v
;
2368 // this.axes_ doesn't match this.attributes_.axes_.options. It's used for
2369 // data computation as well as options storage.
2370 // Go through once and add all the axes.
2373 for (axis
= 0; axis
< this.attributes_
.numAxes(); axis
++) {
2374 // Add a new axis, making a copy of its per-axis options.
2375 opts
= { g
: this };
2376 utils
.update(opts
, this.attributes_
.axisOptions(axis
));
2377 this.axes_
[axis
] = opts
;
2380 for (axis
= 0; axis
< this.axes_
.length
; axis
++) {
2382 opts
= this.optionsViewForAxis_('y' + (axis
? '2' : ''));
2383 v
= opts("valueRange");
2384 if (v
) this.axes_
[axis
].valueRange
= v
;
2385 } else { // To keep old behavior
2386 var axes
= this.user_attrs_
.axes
;
2387 if (axes
&& axes
.y2
) {
2388 v
= axes
.y2
.valueRange
;
2389 if (v
) this.axes_
[axis
].valueRange
= v
;
2396 * Returns the number of y-axes on the chart.
2397 * @return {number} the number of axes.
2399 Dygraph
.prototype.numAxes
= function() {
2400 return this.attributes_
.numAxes();
2405 * Returns axis properties for the given series.
2406 * @param {string} setName The name of the series for which to get axis
2407 * properties, e.g. 'Y1'.
2408 * @return {Object} The axis properties.
2410 Dygraph
.prototype.axisPropertiesForSeries
= function(series
) {
2411 // TODO(danvk): handle errors.
2412 return this.axes_
[this.attributes_
.axisForSeries(series
)];
2417 * Determine the value range and tick marks for each axis.
2418 * @param {Object} extremes A mapping from seriesName -> [low, high]
2419 * This fills in the valueRange and ticks fields in each entry of this.axes_.
2421 Dygraph
.prototype.computeYAxisRanges_
= function(extremes
) {
2422 var isNullUndefinedOrNaN
= function(num
) {
2423 return isNaN(parseFloat(num
));
2425 var numAxes
= this.attributes_
.numAxes();
2426 var ypadCompat
, span
, series
, ypad
;
2430 // Compute extreme values, a span and tick marks for each axis.
2431 for (var i
= 0; i
< numAxes
; i
++) {
2432 var axis
= this.axes_
[i
];
2433 var logscale
= this.attributes_
.getForAxis("logscale", i
);
2434 var includeZero
= this.attributes_
.getForAxis("includeZero", i
);
2435 var independentTicks
= this.attributes_
.getForAxis("independentTicks", i
);
2436 series
= this.attributes_
.seriesForAxis(i
);
2438 // Add some padding. This supports two Y padding operation modes:
2440 // - backwards compatible (yRangePad not set):
2441 // 10% padding for automatic Y ranges, but not for user-supplied
2442 // ranges, and move a close-to-zero edge to zero, since drawing at the edge
2443 // results in invisible lines. Unfortunately lines drawn at the edge of a
2444 // user-supplied range will still be invisible. If logscale is
2445 // set, add a variable amount of padding at the top but
2446 // none at the bottom.
2448 // - new-style (yRangePad set by the user):
2449 // always add the specified Y padding.
2452 ypad
= 0.1; // add 10%
2453 const yRangePad
= this.getNumericOption('yRangePad');
2454 if (yRangePad
!== null) {
2456 // Convert pixel padding to ratio
2457 ypad
= yRangePad
/ this.plotter_
.area
.h
;
2460 if (series
.length
=== 0) {
2461 // If no series are defined or visible then use a reasonable default
2462 axis
.extremeRange
= [0, 1];
2464 // Calculate the extremes of extremes.
2465 var minY
= Infinity
; // extremes[series[0]][0];
2466 var maxY
= -Infinity
; // extremes[series[0]][1];
2467 var extremeMinY
, extremeMaxY
;
2469 for (var j
= 0; j
< series
.length
; j
++) {
2470 // this skips invisible series
2471 if (!extremes
.hasOwnProperty(series
[j
])) continue;
2473 // Only use valid extremes to stop null data series' from corrupting the scale.
2474 extremeMinY
= extremes
[series
[j
]][0];
2475 if (extremeMinY
!== null) {
2476 minY
= Math
.min(extremeMinY
, minY
);
2478 extremeMaxY
= extremes
[series
[j
]][1];
2479 if (extremeMaxY
!== null) {
2480 maxY
= Math
.max(extremeMaxY
, maxY
);
2484 // Include zero if requested by the user.
2485 if (includeZero
&& !logscale
) {
2486 if (minY
> 0) minY
= 0;
2487 if (maxY
< 0) maxY
= 0;
2490 // Ensure we have a valid scale, otherwise default to [0, 1] for safety.
2491 if (minY
== Infinity
) minY
= 0;
2492 if (maxY
== -Infinity
) maxY
= 1;
2495 // special case: if we have no sense of scale, center on the sole value.
2498 span
= Math
.abs(maxY
);
2500 // ... and if the sole value is zero, use range 0-1.
2506 var maxAxisY
= maxY
, minAxisY
= minY
;
2509 maxAxisY
= maxY
+ ypad
* span
;
2512 maxAxisY
= maxY
+ ypad
* span
;
2513 minAxisY
= minY
- ypad
* span
;
2515 // Backwards-compatible behavior: Move the span to start or end at zero if it's
2517 if (minAxisY
< 0 && minY
>= 0) minAxisY
= 0;
2518 if (maxAxisY
> 0 && maxY
<= 0) maxAxisY
= 0;
2521 axis
.extremeRange
= [minAxisY
, maxAxisY
];
2523 if (axis
.valueRange
) {
2524 // This is a user-set value range for this axis.
2525 var y0
= isNullUndefinedOrNaN(axis
.valueRange
[0]) ? axis
.extremeRange
[0] : axis
.valueRange
[0];
2526 var y1
= isNullUndefinedOrNaN(axis
.valueRange
[1]) ? axis
.extremeRange
[1] : axis
.valueRange
[1];
2527 axis
.computedValueRange
= [y0
, y1
];
2529 axis
.computedValueRange
= axis
.extremeRange
;
2532 // When using yRangePad, adjust the upper/lower bounds to add
2533 // padding unless the user has zoomed/panned the Y axis range
.
2535 y0
= axis
.computedValueRange
[0];
2536 y1
= axis
.computedValueRange
[1];
2537 var y0pct
= ypad
/ (2 * ypad
- 1);
2538 var y1pct
= (ypad
- 1) / (2 * ypad
- 1);
2539 axis
.computedValueRange
[0] = utils
.logRangeFraction(y0
, y1
, y0pct
);
2540 axis
.computedValueRange
[1] = utils
.logRangeFraction(y0
, y1
, y1pct
);
2542 y0
= axis
.computedValueRange
[0];
2543 y1
= axis
.computedValueRange
[1];
2545 axis
.computedValueRange
[0] = y0
- span
* ypad
;
2546 axis
.computedValueRange
[1] = y1
+ span
* ypad
;
2551 if (independentTicks
) {
2552 axis
.independentTicks
= independentTicks
;
2553 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2554 var ticker
= opts('ticker');
2555 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2556 axis
.computedValueRange
[1],
2557 this.plotter_
.area
.h
,
2560 // Define the first independent axis as primary axis.
2561 if (!p_axis
) p_axis
= axis
;
2564 if (p_axis
=== undefined
) {
2565 throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated.");
2567 // Add ticks. By default, all axes inherit the tick positions of the
2568 // primary axis. However, if an axis is specifically marked as having
2569 // independent ticks, then that is permissible as well.
2570 for (var i
= 0; i
< numAxes
; i
++) {
2571 var axis
= this.axes_
[i
];
2573 if (!axis
.independentTicks
) {
2574 var opts
= this.optionsViewForAxis_('y' + (i
? '2' : ''));
2575 var ticker
= opts('ticker');
2576 var p_ticks
= p_axis
.ticks
;
2577 var p_scale
= p_axis
.computedValueRange
[1] - p_axis
.computedValueRange
[0];
2578 var scale
= axis
.computedValueRange
[1] - axis
.computedValueRange
[0];
2579 var tick_values
= [];
2580 for (var k
= 0; k
< p_ticks
.length
; k
++) {
2581 var y_frac
= (p_ticks
[k
].v
- p_axis
.computedValueRange
[0]) / p_scale
;
2582 var y_val
= axis
.computedValueRange
[0] + y_frac
* scale
;
2583 tick_values
.push(y_val
);
2586 axis
.ticks
= ticker(axis
.computedValueRange
[0],
2587 axis
.computedValueRange
[1],
2588 this.plotter_
.area
.h
,
2597 * Detects the type of the str (date or numeric) and sets the various
2598 * formatting attributes in this.attrs_ based on this type.
2599 * @param {string} str An x value.
2602 Dygraph
.prototype.detectTypeFromString_
= function(str
) {
2604 var dashPos
= str
.indexOf('-'); // could be 2006-01-01 _or_ 1.0e-2
2605 if ((dashPos
> 0 && (str
[dashPos
-1] != 'e' && str
[dashPos
-1] != 'E')) ||
2606 str
.indexOf('/') >= 0 ||
2607 isNaN(parseFloat(str
))) {
2609 } else if (str
.length
== 8 && str
> '19700101' && str
< '20371231') {
2610 // TODO(danvk): remove support for this format.
2614 this.setXAxisOptions_(isDate
);
2617 Dygraph
.prototype.setXAxisOptions_
= function(isDate
) {
2619 this.attrs_
.xValueParser
= utils
.dateParser
;
2620 this.attrs_
.axes
.x
.valueFormatter
= utils
.dateValueFormatter
;
2621 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.dateTicker
;
2622 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.dateAxisLabelFormatter
;
2624 /** @private (shut up, jsdoc!) */
2625 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2626 // TODO(danvk): use Dygraph.numberValueFormatter here?
2627 /** @private (shut up, jsdoc!) */
2628 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2629 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2630 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2636 * Parses a string in a special csv format. We expect a csv file where each
2637 * line is a date point, and the first field in each line is the date string.
2638 * We also expect that all remaining fields represent series.
2639 * if the errorBars attribute is set, then interpret the fields as:
2640 * date, series1, stddev1, series2, stddev2, ...
2641 * @param {[Object]} data See above.
2643 * @return [Object] An array with one entry for each row. These entries
2644 * are an array of cells in that row. The first entry is the parsed x-value for
2645 * the row. The second, third, etc. are the y-values. These can take on one of
2646 * three forms, depending on the CSV and constructor parameters:
2648 * 2. [ value, stddev ]
2649 * 3. [ low value, center value, high value ]
2651 Dygraph
.prototype.parseCSV_
= function(data
) {
2653 var line_delimiter
= utils
.detectLineDelimiter(data
);
2654 var lines
= data
.split(line_delimiter
|| "\n");
2657 // Use the default delimiter or fall back to a tab if that makes sense.
2658 var delim
= this.getStringOption('delimiter');
2659 if (lines
[0].indexOf(delim
) == -1 && lines
[0].indexOf('\t') >= 0) {
2664 if (!('labels' in this.user_attrs_
)) {
2665 // User hasn't explicitly set labels, so they're (presumably) in the CSV.
2667 this.attrs_
.labels
= lines
[0].split(delim
); // NOTE: _not_ user_attrs_.
2668 this.attributes_
.reparseSeries();
2673 var defaultParserSet
= false; // attempt to auto-detect x value type
2674 var expectedCols
= this.attr_("labels").length
;
2675 var outOfOrder
= false;
2676 for (var i
= start
; i
< lines
.length
; i
++) {
2677 var line
= lines
[i
];
2679 if (line
.length
=== 0) continue; // skip blank lines
2680 if (line
[0] == '#') continue; // skip comment lines
2681 var inFields
= line
.split(delim
);
2682 if (inFields
.length
< 2) continue;
2685 if (!defaultParserSet
) {
2686 this.detectTypeFromString_(inFields
[0]);
2687 xParser
= this.getFunctionOption("xValueParser");
2688 defaultParserSet
= true;
2690 fields
[0] = xParser(inFields
[0], this);
2692 // If fractions are expected, parse the numbers as "A/B
"
2693 if (this.fractions_) {
2694 for (j = 1; j < inFields.length; j++) {
2695 // TODO(danvk): figure out an appropriate way to flag parse errors.
2696 vals = inFields[j].split("/");
2697 if (vals.length != 2) {
2698 console.error('Expected fractional "num
/den
" values in CSV data ' +
2699 "but found a value
'" + inFields[j] + "' on line
" +
2700 (1 + i) + " ('" + line + "') which is not of
this form
.");
2703 fields[j] = [utils.parseFloat_(vals[0], i, line),
2704 utils.parseFloat_(vals[1], i, line)];
2707 } else if (this.getBooleanOption("errorBars
")) {
2708 // If there are error bars, values are (value, stddev) pairs
2709 if (inFields.length % 2 != 1) {
2710 console.error('Expected alternating (value, stdev.) pairs in CSV data ' +
2711 'but line ' + (1 + i) + ' has an odd number of values (' +
2712 (inFields.length - 1) + "): '" + line + "'");
2714 for (j = 1; j < inFields.length; j += 2) {
2715 fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line),
2716 utils.parseFloat_(inFields[j + 1], i, line)];
2718 } else if (this.getBooleanOption("customBars
")) {
2719 // Bars are a low;center;high tuple
2720 for (j = 1; j < inFields.length; j++) {
2721 var val = inFields[j];
2722 if (/^ *$/.test(val)) {
2723 fields[j] = [null, null, null];
2725 vals = val.split(";");
2726 if (vals.length == 3) {
2727 fields[j] = [ utils.parseFloat_(vals[0], i, line),
2728 utils.parseFloat_(vals[1], i, line),
2729 utils.parseFloat_(vals[2], i, line) ];
2731 console.warn('When using customBars, values must be either blank ' +
2732 'or "low
;center
;high
" tuples (got "' + val +
2733 '" on line ' + (1+i));
2738 // Values are just numbers
2739 for (j = 1; j < inFields.length; j++) {
2740 fields[j] = utils.parseFloat_(inFields[j], i, line);
2743 if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) {
2747 if (fields.length != expectedCols) {
2748 console.error("Number of columns
in line
" + i + " (" + fields.length +
2749 ") does not agree
with number of
labels (" + expectedCols +
2753 // If the user specified the 'labels' option and none of the cells of the
2754 // first row parsed correctly, then they probably double-specified the
2755 // labels. We go with the values set in the option, discard this row and
2756 // log a warning to the JS console.
2757 if (i === 0 && this.attr_('labels')) {
2758 var all_null = true;
2759 for (j = 0; all_null && j < fields.length; j++) {
2760 if (fields[j]) all_null = false;
2763 console.warn("The dygraphs
'labels' option is set
, but the first row
" +
2764 "of CSV
data ('" + line + "') appears to also contain
" +
2765 "labels
. Will drop the CSV labels and
use the option
" +
2774 console.warn("CSV is out of order
; order it correctly to speed loading
.");
2775 ret.sort(function(a,b) { return a[0] - b[0]; });
2781 // In native format, all values must be dates or numbers.
2782 // This check isn't perfect but will catch most mistaken uses of strings.
2783 function validateNativeFormat(data) {
2784 const firstRow = data[0];
2785 const firstX = firstRow[0];
2786 if (typeof firstX !== 'number' && !utils.isDateLike(firstX)) {
2787 throw new Error(`Expected number or date but got ${typeof firstX}: ${firstX}.`);
2789 for (let i = 1; i < firstRow.length; i++) {
2790 const val = firstRow[i];
2791 if (val === null || val === undefined) continue;
2792 if (typeof val === 'number') continue;
2793 if (utils.isArrayLike(val)) continue; // e.g. error bars or custom bars.
2794 throw new Error(`Expected number or array but got ${typeof val}: ${val}.`);
2799 * The user has provided their data as a pre-packaged JS array. If the x values
2800 * are numeric, this is the same as dygraphs' internal format. If the x values
2801 * are dates, we need to convert them from Date objects to ms since epoch.
2802 * @param {!Array} data
2803 * @return {Object} data with numeric x values.
2806 Dygraph.prototype.parseArray_ = function(data) {
2807 // Peek at the first x value to see if it's numeric.
2808 if (data.length === 0) {
2809 console.error("Can
't plot empty data set");
2812 if (data[0].length === 0) {
2813 console.error("Data set cannot contain an empty row");
2817 validateNativeFormat(data);
2820 if (this.attr_("labels") === null) {
2821 console.warn("Using default labels. Set labels explicitly via 'labels
' " +
2822 "in the options parameter");
2823 this.attrs_.labels = [ "X" ];
2824 for (i = 1; i < data[0].length; i++) {
2825 this.attrs_.labels.push("Y" + i); // Not user_attrs_.
2827 this.attributes_.reparseSeries();
2829 var num_labels = this.attr_("labels");
2830 if (num_labels.length != data[0].length) {
2831 console.error("Mismatch between number of labels (" + num_labels + ")" +
2832 " and number of columns in array (" + data[0].length + ")");
2837 if (utils.isDateLike(data[0][0])) {
2838 // Some intelligent defaults for a date x-axis.
2839 this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter;
2840 this.attrs_.axes.x.ticker = DygraphTickers.dateTicker;
2841 this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter;
2843 // Assume they're all dates
.
2844 var parsedData
= utils
.clone(data
);
2845 for (i
= 0; i
< data
.length
; i
++) {
2846 if (parsedData
[i
].length
=== 0) {
2847 console
.error("Row " + (1 + i
) + " of data is empty");
2850 if (parsedData
[i
][0] === null ||
2851 typeof(parsedData
[i
][0].getTime
) != 'function' ||
2852 isNaN(parsedData
[i
][0].getTime())) {
2853 console
.error("x value in row " + (1 + i
) + " is not a Date");
2856 parsedData
[i
][0] = parsedData
[i
][0].getTime();
2860 // Some intelligent defaults for a numeric x-axis.
2861 /** @private (shut up, jsdoc!) */
2862 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2863 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2864 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.numberAxisLabelFormatter
;
2870 * Parses a DataTable object from gviz.
2871 * The data is expected to have a first column that is either a date or a
2872 * number. All subsequent columns must be numbers. If there is a clear mismatch
2873 * between this.xValueParser_ and the type of the first column, it will be
2874 * fixed. Fills out rawData_.
2875 * @param {!google.visualization.DataTable} data See above.
2878 Dygraph
.prototype.parseDataTable_
= function(data
) {
2879 var shortTextForAnnotationNum
= function(num
) {
2880 // converts [0-9]+ [A-Z][a-z]*
2881 // example: 0=A, 1=B, 25=Z, 26=Aa, 27=Ab
2882 // and continues like.. Ba Bb .. Za .. Zz..Aaa...Zzz Aaaa Zzzz
2883 var shortText
= String
.fromCharCode(65 /* A */ + num
% 26);
2884 num
= Math
.floor(num
/ 26);
2886 shortText
= String
.fromCharCode(65 /* A */ + (num
- 1) % 26 ) + shortText
.toLowerCase();
2887 num
= Math
.floor((num
- 1) / 26);
2892 var cols
= data
.getNumberOfColumns();
2893 var rows
= data
.getNumberOfRows();
2895 var indepType
= data
.getColumnType(0);
2896 if (indepType
== 'date' || indepType
== 'datetime') {
2897 this.attrs_
.xValueParser
= utils
.dateParser
;
2898 this.attrs_
.axes
.x
.valueFormatter
= utils
.dateValueFormatter
;
2899 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.dateTicker
;
2900 this.attrs_
.axes
.x
.axisLabelFormatter
= utils
.dateAxisLabelFormatter
;
2901 } else if (indepType
== 'number') {
2902 this.attrs_
.xValueParser
= function(x
) { return parseFloat(x
); };
2903 this.attrs_
.axes
.x
.valueFormatter
= function(x
) { return x
; };
2904 this.attrs_
.axes
.x
.ticker
= DygraphTickers
.numericTicks
;
2905 this.attrs_
.axes
.x
.axisLabelFormatter
= this.attrs_
.axes
.x
.valueFormatter
;
2908 "only 'date', 'datetime' and 'number' types are supported " +
2909 "for column 1 of DataTable input (Got '" + indepType
+ "')");
2912 // Array of the column indices which contain data (and not annotations).
2914 var annotationCols
= {}; // data index -> [annotation cols]
2915 var hasAnnotations
= false;
2917 for (i
= 1; i
< cols
; i
++) {
2918 var type
= data
.getColumnType(i
);
2919 if (type
== 'number') {
2921 } else if (type
== 'string' && this.getBooleanOption('displayAnnotations')) {
2922 // This is OK -- it's an annotation column.
2923 var dataIdx
= colIdx
[colIdx
.length
- 1];
2924 if (!annotationCols
.hasOwnProperty(dataIdx
)) {
2925 annotationCols
[dataIdx
] = [i
];
2927 annotationCols
[dataIdx
].push(i
);
2929 hasAnnotations
= true;
2932 "Only 'number' is supported as a dependent type with Gviz." +
2933 " 'string' is only supported if displayAnnotations is true");
2937 // Read column labels
2938 // TODO(danvk): add support back for errorBars
2939 var labels
= [data
.getColumnLabel(0)];
2940 for (i
= 0; i
< colIdx
.length
; i
++) {
2941 labels
.push(data
.getColumnLabel(colIdx
[i
]));
2942 if (this.getBooleanOption("errorBars")) i
+= 1;
2944 this.attrs_
.labels
= labels
;
2945 cols
= labels
.length
;
2948 var outOfOrder
= false;
2949 var annotations
= [];
2950 for (i
= 0; i
< rows
; i
++) {
2952 if (typeof(data
.getValue(i
, 0)) === 'undefined' ||
2953 data
.getValue(i
, 0) === null) {
2954 console
.warn("Ignoring row " + i
+
2955 " of DataTable because of undefined or null first column.");
2959 if (indepType
== 'date' || indepType
== 'datetime') {
2960 row
.push(data
.getValue(i
, 0).getTime());
2962 row
.push(data
.getValue(i
, 0));
2964 if (!this.getBooleanOption("errorBars")) {
2965 for (j
= 0; j
< colIdx
.length
; j
++) {
2966 var col
= colIdx
[j
];
2967 row
.push(data
.getValue(i
, col
));
2968 if (hasAnnotations
&&
2969 annotationCols
.hasOwnProperty(col
) &&
2970 data
.getValue(i
, annotationCols
[col
][0]) !== null) {
2972 ann
.series
= data
.getColumnLabel(col
);
2974 ann
.shortText
= shortTextForAnnotationNum(annotations
.length
);
2976 for (var k
= 0; k
< annotationCols
[col
].length
; k
++) {
2977 if (k
) ann
.text
+= "\n";
2978 ann
.text
+= data
.getValue(i
, annotationCols
[col
][k
]);
2980 annotations
.push(ann
);
2984 // Strip out infinities, which give dygraphs problems later on.
2985 for (j
= 0; j
< row
.length
; j
++) {
2986 if (!isFinite(row
[j
])) row
[j
] = null;
2989 for (j
= 0; j
< cols
- 1; j
++) {
2990 row
.push([ data
.getValue(i
, 1 + 2 * j
), data
.getValue(i
, 2 + 2 * j
) ]);
2993 if (ret
.length
> 0 && row
[0] < ret
[ret
.length
- 1][0]) {
3000 console
.warn("DataTable is out of order; order it correctly to speed loading.");
3001 ret
.sort(function(a
,b
) { return a
[0] - b
[0]; });
3003 this.rawData_
= ret
;
3005 if (annotations
.length
> 0) {
3006 this.setAnnotations(annotations
, true);
3008 this.attributes_
.reparseSeries();
3012 * Signals to plugins that the chart data has updated.
3013 * This happens after the data has updated but before the chart has redrawn.
3016 Dygraph
.prototype.cascadeDataDidUpdateEvent_
= function() {
3017 // TODO(danvk): there are some issues checking xAxisRange() and using
3018 // toDomCoords from handlers of this event. The visible range should be set
3019 // when the chart is drawn, not derived from the data.
3020 this.cascadeEvents_('dataDidUpdate', {});
3024 * Get the CSV data. If it's in a function, call that function. If it's in a
3025 * file, do an XMLHttpRequest to get it.
3028 Dygraph
.prototype.start_
= function() {
3029 var data
= this.file_
;
3031 // Functions can return references of all other types.
3032 if (typeof data
== 'function') {
3036 if (utils
.isArrayLike(data
)) {
3037 this.rawData_
= this.parseArray_(data
);
3038 this.cascadeDataDidUpdateEvent_();
3040 } else if (typeof data
== 'object' &&
3041 typeof data
.getColumnRange
== 'function') {
3042 // must be a DataTable from gviz.
3043 this.parseDataTable_(data
);
3044 this.cascadeDataDidUpdateEvent_();
3046 } else if (typeof data
== 'string') {
3047 // Heuristic: a newline means it's CSV data. Otherwise it's an URL.
3048 var line_delimiter
= utils
.detectLineDelimiter(data
);
3049 if (line_delimiter
) {
3050 this.loadedEvent_(data
);
3054 if (window
.XMLHttpRequest
) {
3055 // Firefox, Opera, IE7, and other browsers will use the native object
3056 req
= new XMLHttpRequest();
3058 // IE 5 and 6 will use the ActiveX control
3059 req
= new ActiveXObject("Microsoft.XMLHTTP");
3063 req
.onreadystatechange
= function () {
3064 if (req
.readyState
== 4) {
3065 if (req
.status
=== 200 || // Normal http
3066 req
.status
=== 0) { // Chrome w/ --allow
-file
-access
-from
-files
3067 caller
.loadedEvent_(req
.responseText
);
3072 req
.open("GET", data
, true);
3076 console
.error("Unknown data format: " + (typeof data
));
3081 * Changes various properties of the graph. These can include:
3083 * <li>file: changes the source data for the graph</li>
3084 * <li>errorBars: changes whether the data contains stddev</li>
3087 * There's a huge variety of options that can be passed to this method. For a
3088 * full list, see http://dygraphs.com/options.html.
3090 * @param {Object} input_attrs The new properties and values
3091 * @param {boolean} block_redraw Usually the chart is redrawn after every
3092 * call to updateOptions(). If you know better, you can pass true to
3093 * explicitly block the redraw. This can be useful for chaining
3094 * updateOptions() calls, avoiding the occasional infinite loop and
3095 * preventing redraws when it's not necessary (e.g. when updating a
3098 Dygraph
.prototype.updateOptions
= function(input_attrs
, block_redraw
) {
3099 if (typeof(block_redraw
) == 'undefined') block_redraw
= false;
3101 // copyUserAttrs_ drops the "file" parameter as a convenience to us.
3102 var file
= input_attrs
.file
;
3103 var attrs
= Dygraph
.copyUserAttrs_(input_attrs
);
3105 // TODO(danvk): this is a mess. Move these options into attr_.
3106 if ('rollPeriod' in attrs
) {
3107 this.rollPeriod_
= attrs
.rollPeriod
;
3109 if ('dateWindow' in attrs
) {
3110 this.dateWindow_
= attrs
.dateWindow
;
3113 // TODO(danvk): validate per-series options.
3118 // highlightCircleSize
3120 // Check if this set options will require new points.
3121 var requiresNewPoints
= utils
.isPixelChangingOptionList(this.attr_("labels"), attrs
);
3123 utils
.updateDeep(this.user_attrs_
, attrs
);
3125 this.attributes_
.reparseSeries();
3128 // This event indicates that the data is about to change, but hasn't yet.
3129 // TODO(danvk): support cancellation of the update via this event.
3130 this.cascadeEvents_('dataWillUpdate', {});
3133 if (!block_redraw
) this.start_();
3135 if (!block_redraw
) {
3136 if (requiresNewPoints
) {
3139 this.renderGraph_(false);
3146 * Make a copy of input attributes, removing file as a convenience.
3149 Dygraph
.copyUserAttrs_
= function(attrs
) {
3151 for (var k
in attrs
) {
3152 if (!attrs
.hasOwnProperty(k
)) continue;
3153 if (k
== 'file') continue;
3154 if (attrs
.hasOwnProperty(k
)) my_attrs
[k
] = attrs
[k
];
3160 * Resizes the dygraph. If no parameters are specified, resizes to fill the
3161 * containing div (which has presumably changed size since the dygraph was
3162 * instantiated. If the width/height are specified, the div will be resized.
3164 * This is far more efficient than destroying and re-instantiating a
3165 * Dygraph, since it doesn't have to reparse the underlying data.
3167 * @param {number} width Width (in pixels)
3168 * @param {number} height Height (in pixels)
3170 Dygraph
.prototype.resize
= function(width
, height
) {
3171 if (this.resize_lock
) {
3174 this.resize_lock
= true;
3176 if ((width
=== null) != (height
=== null)) {
3177 console
.warn("Dygraph.resize() should be called with zero parameters or " +
3178 "two non-NULL parameters. Pretending it was zero.");
3179 width
= height
= null;
3182 var old_width
= this.width_
;
3183 var old_height
= this.height_
;
3186 this.maindiv_
.style
.width
= width
+ "px";
3187 this.maindiv_
.style
.height
= height
+ "px";
3188 this.width_
= width
;
3189 this.height_
= height
;
3191 this.width_
= this.maindiv_
.clientWidth
;
3192 this.height_
= this.maindiv_
.clientHeight
;
3195 if (old_width
!= this.width_
|| old_height
!= this.height_
) {
3196 // Resizing a canvas erases it, even when the size doesn't change, so
3197 // any resize needs to be followed by a redraw.
3198 this.resizeElements_();
3202 this.resize_lock
= false;
3206 * Adjusts the number of points in the rolling average. Updates the graph to
3207 * reflect the new averaging period.
3208 * @param {number} length Number of points over which to average the data.
3210 Dygraph
.prototype.adjustRoll
= function(length
) {
3211 this.rollPeriod_
= length
;
3216 * Returns a boolean array of visibility statuses.
3218 Dygraph
.prototype.visibility
= function() {
3219 // Do lazy-initialization, so that this happens after we know the number of
3221 if (!this.getOption("visibility")) {
3222 this.attrs_
.visibility
= [];
3224 // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs
.
3225 while (this.getOption("visibility").length
< this.numColumns() - 1) {
3226 this.attrs_
.visibility
.push(true);
3228 return this.getOption("visibility");
3232 * Changes the visibility of one or more series.
3234 * @param {number|number[]|object} num the series index or an array of series indices
3235 * or a boolean array of visibility states by index
3236 * or an object mapping series numbers, as keys, to
3237 * visibility state (boolean values)
3238 * @param {boolean} value the visibility state expressed as a boolean
3240 Dygraph
.prototype.setVisibility
= function(num
, value
) {
3241 var x
= this.visibility();
3242 var numIsObject
= false;
3244 if (!Array
.isArray(num
)) {
3245 if (num
!== null && typeof num
=== 'object') {
3253 for (var i
in num
) {
3254 if (num
.hasOwnProperty(i
)) {
3255 if (i
< 0 || i
>= x
.length
) {
3256 console
.warn("Invalid series number in setVisibility: " + i
);
3263 for (var i
= 0; i
< num
.length
; i
++) {
3264 if (typeof num
[i
] === 'boolean') {
3265 if (i
>= x
.length
) {
3266 console
.warn("Invalid series number in setVisibility: " + i
);
3271 if (num
[i
] < 0 || num
[i
] >= x
.length
) {
3272 console
.warn("Invalid series number in setVisibility: " + num
[i
]);
3284 * How large of an area will the dygraph render itself in?
3285 * This is used for testing.
3286 * @return A {width: w, height: h} object.
3289 Dygraph
.prototype.size
= function() {
3290 return { width
: this.width_
, height
: this.height_
};
3294 * Update the list of annotations and redraw the chart.
3295 * See dygraphs.com/annotations.html for more info on how to use annotations.
3296 * @param ann {Array} An array of annotation objects.
3297 * @param suppressDraw {Boolean} Set to "true" to block chart redraw (optional).
3299 Dygraph
.prototype.setAnnotations
= function(ann
, suppressDraw
) {
3300 // Only add the annotation CSS rule once we know it will be used.
3301 this.annotations_
= ann
;
3302 if (!this.layout_
) {
3303 console
.warn("Tried to setAnnotations before dygraph was ready. " +
3304 "Try setting them in a ready() block. See " +
3305 "dygraphs.com/tests/annotation.html");
3309 this.layout_
.setAnnotations(this.annotations_
);
3310 if (!suppressDraw
) {
3316 * Return the list of annotations.
3318 Dygraph
.prototype.annotations
= function() {
3319 return this.annotations_
;
3323 * Get the list of label names for this graph. The first column is the
3324 * x-axis, so the data series names start at index 1.
3326 * Returns null when labels have not yet been defined.
3328 Dygraph
.prototype.getLabels
= function() {
3329 var labels
= this.attr_("labels");
3330 return labels
? labels
.slice() : null;
3334 * Get the index of a series (column) given its name. The first column is the
3335 * x-axis, so the data series start with index 1.
3337 Dygraph
.prototype.indexFromSetName
= function(name
) {
3338 return this.setIndexByName_
[name
];
3342 * Find the row number corresponding to the given x-value.
3343 * Returns null if there is no such x-value in the data.
3344 * If there are multiple rows with the same x-value, this will return the
3346 * @param {number} xVal The x-value to look for (e.g. millis since epoch).
3347 * @return {?number} The row number, which you can pass to getValue(), or null.
3349 Dygraph
.prototype.getRowForX
= function(xVal
) {
3351 high
= this.numRows() - 1;
3353 while (low
<= high
) {
3354 var idx
= (high
+ low
) >> 1;
3355 var x
= this.getValue(idx
, 0);
3358 } else if (x
> xVal
) {
3360 } else if (low
!= idx
) { // equal, but there may be an earlier match.
3371 * Trigger a callback when the dygraph has drawn itself and is ready to be
3372 * manipulated. This is primarily useful when dygraphs has to do an XHR for the
3373 * data (i.e. a URL is passed as the data source) and the chart is drawn
3374 * asynchronously. If the chart has already drawn, the callback will fire
3377 * This is a good place to call setAnnotation().
3379 * @param {function(!Dygraph)} callback The callback to trigger when the chart
3382 Dygraph
.prototype.ready
= function(callback
) {
3383 if (this.is_initial_draw_
) {
3384 this.readyFns_
.push(callback
);
3386 callback
.call(this, this);
3391 * Add an event handler. This event handler is kept until the graph is
3392 * destroyed with a call to graph.destroy().
3394 * @param {!Node} elem The element to add the event to.
3395 * @param {string} type The type of the event, e.g. 'click' or 'mousemove'.
3396 * @param {function(Event):(boolean|undefined)} fn The function to call
3397 * on the event. The function takes one parameter: the event object.
3400 Dygraph
.prototype.addAndTrackEvent
= function(elem
, type
, fn
) {
3401 utils
.addEvent(elem
, type
, fn
);
3402 this.registeredEvents_
.push({elem
, type
, fn
});
3405 Dygraph
.prototype.removeTrackedEvents_
= function() {
3406 if (this.registeredEvents_
) {
3407 for (var idx
= 0; idx
< this.registeredEvents_
.length
; idx
++) {
3408 var reg
= this.registeredEvents_
[idx
];
3409 utils
.removeEvent(reg
.elem
, reg
.type
, reg
.fn
);
3413 this.registeredEvents_
= [];
3417 // Installed plugins, in order of precedence (most-general to most-specific).
3421 RangeSelectorPlugin
, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks.
3427 // There are many symbols which have historically been available through the
3428 // Dygraph class. These are exported here for backwards compatibility.
3429 Dygraph
.GVizChart
= GVizChart
;
3430 Dygraph
.DASHED_LINE
= utils
.DASHED_LINE
;
3431 Dygraph
.DOT_DASH_LINE
= utils
.DOT_DASH_LINE
;
3432 Dygraph
.dateAxisLabelFormatter
= utils
.dateAxisLabelFormatter
;
3433 Dygraph
.toRGB_
= utils
.toRGB_
;
3434 Dygraph
.findPos
= utils
.findPos
;
3435 Dygraph
.pageX
= utils
.pageX
;
3436 Dygraph
.pageY
= utils
.pageY
;
3437 Dygraph
.dateString_
= utils
.dateString_
;
3438 Dygraph
.defaultInteractionModel
= DygraphInteraction
.defaultModel
;
3439 Dygraph
.nonInteractiveModel
= Dygraph
.nonInteractiveModel_
= DygraphInteraction
.nonInteractiveModel_
;
3440 Dygraph
.Circles
= utils
.Circles
;
3443 Legend
: LegendPlugin
,
3445 Annotations
: AnnotationsPlugin
,
3446 ChartLabels
: ChartLabelsPlugin
,
3448 RangeSelector
: RangeSelectorPlugin
3451 Dygraph
.DataHandlers
= {
3455 DefaultFractionHandler
,
3457 FractionsBarsHandler
3460 Dygraph
.startPan
= DygraphInteraction
.startPan
;
3461 Dygraph
.startZoom
= DygraphInteraction
.startZoom
;
3462 Dygraph
.movePan
= DygraphInteraction
.movePan
;
3463 Dygraph
.moveZoom
= DygraphInteraction
.moveZoom
;
3464 Dygraph
.endPan
= DygraphInteraction
.endPan
;
3465 Dygraph
.endZoom
= DygraphInteraction
.endZoom
;
3467 Dygraph
.numericLinearTicks
= DygraphTickers
.numericLinearTicks
;
3468 Dygraph
.numericTicks
= DygraphTickers
.numericTicks
;
3469 Dygraph
.dateTicker
= DygraphTickers
.dateTicker
;
3470 Dygraph
.Granularity
= DygraphTickers
.Granularity
;
3471 Dygraph
.getDateAxis
= DygraphTickers
.getDateAxis
;
3472 Dygraph
.floatFormat
= utils
.floatFormat
;
3474 export default Dygraph
;