UNPKG

dcos-dygraphs

Version:

dygraphs is a fast, flexible open source JavaScript charting library.

1,480 lines (1,308 loc) 120 kB
/** * @license * Copyright 2006 Dan Vanderkam (danvdk@gmail.com) * MIT-licensed (http://opensource.org/licenses/MIT) */ /** * @fileoverview Creates an interactive, zoomable graph based on a CSV file or * string. Dygraph can handle multiple series with or without error bars. The * date/value ranges will be automatically set. Dygraph uses the * &lt;canvas&gt; tag, so it only works in FF1.5+. * @author danvdk@gmail.com (Dan Vanderkam) Usage: <div id="graphdiv" style="width:800px; height:500px;"></div> <script type="text/javascript"> new Dygraph(document.getElementById("graphdiv"), "datafile.csv", // CSV file with headers { }); // options </script> The CSV file is of the form Date,SeriesA,SeriesB,SeriesC YYYYMMDD,A1,B1,C1 YYYYMMDD,A2,B2,C2 If the 'errorBars' option is set in the constructor, the input should be of the form Date,SeriesA,SeriesB,... YYYYMMDD,A1,sigmaA1,B1,sigmaB1,... YYYYMMDD,A2,sigmaA2,B2,sigmaB2,... If the 'fractions' option is set, the input should be of the form: Date,SeriesA,SeriesB,... YYYYMMDD,A1/B1,A2/B2,... YYYYMMDD,A1/B1,A2/B2,... And error bars will be calculated automatically using a binomial distribution. For further documentation and examples, see http://dygraphs.com/ */ import DygraphLayout from './dygraph-layout'; import DygraphCanvasRenderer from './dygraph-canvas'; import DygraphOptions from './dygraph-options'; import DygraphInteraction from './dygraph-interaction-model'; import * as DygraphTickers from './dygraph-tickers'; import * as utils from './dygraph-utils'; import DEFAULT_ATTRS from './dygraph-default-attrs'; import OPTIONS_REFERENCE from './dygraph-options-reference'; import IFrameTarp from './iframe-tarp'; import DefaultHandler from './datahandler/default'; import ErrorBarsHandler from './datahandler/bars-error'; import CustomBarsHandler from './datahandler/bars-custom'; import DefaultFractionHandler from './datahandler/default-fractions'; import FractionsBarsHandler from './datahandler/bars-fractions'; import BarsHandler from './datahandler/bars'; import AnnotationsPlugin from './plugins/annotations'; import AxesPlugin from './plugins/axes'; import ChartLabelsPlugin from './plugins/chart-labels'; import GridPlugin from './plugins/grid'; import LegendPlugin from './plugins/legend'; import RangeSelectorPlugin from './plugins/range-selector'; import GVizChart from './dygraph-gviz'; "use strict"; /** * Creates an interactive, zoomable chart. * * @constructor * @param {div | String} div A div or the id of a div into which to construct * the chart. * @param {String | Function} file A file containing CSV data or a function * that returns this data. The most basic expected format for each line is * "YYYY/MM/DD,val1,val2,...". For more information, see * http://dygraphs.com/data.html. * @param {Object} attrs Various other attributes, e.g. errorBars determines * whether the input data contains error ranges. For a complete list of * options, see http://dygraphs.com/options.html. */ var Dygraph = function(div, data, opts) { this.__init__(div, data, opts); }; Dygraph.NAME = "Dygraph"; Dygraph.VERSION = "1.1.0"; // Various default values Dygraph.DEFAULT_ROLL_PERIOD = 1; Dygraph.DEFAULT_WIDTH = 480; Dygraph.DEFAULT_HEIGHT = 320; // For max 60 Hz. animation: Dygraph.ANIMATION_STEPS = 12; Dygraph.ANIMATION_DURATION = 200; /** * Standard plotters. These may be used by clients. * Available plotters are: * - Dygraph.Plotters.linePlotter: draws central lines (most common) * - Dygraph.Plotters.errorPlotter: draws error bars * - Dygraph.Plotters.fillPlotter: draws fills under lines (used with fillGraph) * * By default, the plotter is [fillPlotter, errorPlotter, linePlotter]. * This causes all the lines to be drawn over all the fills/error bars. */ Dygraph.Plotters = DygraphCanvasRenderer._Plotters; // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; /** * Initializes the Dygraph. This creates a new DIV and constructs the PlotKit * and context &lt;canvas&gt; inside of it. See the constructor for details. * on the parameters. * @param {Element} div the Element to render the graph into. * @param {string | Function} file Source data * @param {Object} attrs Miscellaneous other options * @private */ Dygraph.prototype.__init__ = function(div, file, attrs) { this.is_initial_draw_ = true; this.readyFns_ = []; // Support two-argument constructor if (attrs === null || attrs === undefined) { attrs = {}; } attrs = Dygraph.copyUserAttrs_(attrs); if (typeof(div) == 'string') { div = document.getElementById(div); } if (!div) { throw new Error('Constructing dygraph with a non-existent div!'); } // Copy the important bits into the object // TODO(danvk): most of these should just stay in the attrs_ dictionary. this.maindiv_ = div; this.file_ = file; this.rollPeriod_ = attrs.rollPeriod || Dygraph.DEFAULT_ROLL_PERIOD; this.previousVerticalX_ = -1; this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; this.annotations_ = []; // Zoomed indicators - These indicate when the graph has been zoomed and on what axis. this.zoomed_x_ = false; this.zoomed_y_ = false; // Clear the div. This ensure that, if multiple dygraphs are passed the same // div, then only one will be drawn. div.innerHTML = ""; // For historical reasons, the 'width' and 'height' options trump all CSS // rules _except_ for an explicit 'width' or 'height' on the div. // As an added convenience, if the div has zero height (like <div></div> does // without any styles), then we use a default height/width. if (div.style.width === '' && attrs.width) { div.style.width = attrs.width + "px"; } if (div.style.height === '' && attrs.height) { div.style.height = attrs.height + "px"; } if (div.style.height === '' && div.clientHeight === 0) { div.style.height = Dygraph.DEFAULT_HEIGHT + "px"; if (div.style.width === '') { div.style.width = Dygraph.DEFAULT_WIDTH + "px"; } } // These will be zero if the dygraph's div is hidden. In that case, // use the user-specified attributes if present. If not, use zero // and assume the user will call resize to fix things later. this.width_ = div.clientWidth || attrs.width || 0; this.height_ = div.clientHeight || attrs.height || 0; // TODO(danvk): set fillGraph to be part of attrs_ here, not user_attrs_. if (attrs.stackedGraph) { attrs.fillGraph = true; // TODO(nikhilk): Add any other stackedGraph checks here. } // DEPRECATION WARNING: All option processing should be moved from // attrs_ and user_attrs_ to options_, which holds all this information. // // Dygraphs has many options, some of which interact with one another. // To keep track of everything, we maintain two sets of options: // // this.user_attrs_ only options explicitly set by the user. // this.attrs_ defaults, options derived from user_attrs_, data. // // Options are then accessed this.attr_('attr'), which first looks at // user_attrs_ and then computed attrs_. This way Dygraphs can set intelligent // defaults without overriding behavior that the user specifically asks for. this.user_attrs_ = {}; utils.update(this.user_attrs_, attrs); // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified. this.attrs_ = {}; utils.updateDeep(this.attrs_, DEFAULT_ATTRS); this.boundaryIds_ = []; this.setIndexByName_ = {}; this.datasetIndex_ = []; this.registeredEvents_ = []; this.eventListeners_ = {}; this.attributes_ = new DygraphOptions(this); // Create the containing DIV and other interactive elements this.createInterface_(); // Activate plugins. this.plugins_ = []; var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins')); for (var i = 0; i < plugins.length; i++) { // the plugins option may contain either plugin classes or instances. // Plugin instances contain an activate method. var Plugin = plugins[i]; // either a constructor or an instance. var pluginInstance; if (typeof(Plugin.activate) !== 'undefined') { pluginInstance = Plugin; } else { pluginInstance = new Plugin(); } var pluginDict = { plugin: pluginInstance, events: {}, options: {}, pluginOptions: {} }; var handlers = pluginInstance.activate(this); for (var eventName in handlers) { if (!handlers.hasOwnProperty(eventName)) continue; // TODO(danvk): validate eventName. pluginDict.events[eventName] = handlers[eventName]; } this.plugins_.push(pluginDict); } // At this point, plugins can no longer register event handlers. // Construct a map from event -> ordered list of [callback, plugin]. for (var i = 0; i < this.plugins_.length; i++) { var plugin_dict = this.plugins_[i]; for (var eventName in plugin_dict.events) { if (!plugin_dict.events.hasOwnProperty(eventName)) continue; var callback = plugin_dict.events[eventName]; var pair = [plugin_dict.plugin, callback]; if (!(eventName in this.eventListeners_)) { this.eventListeners_[eventName] = [pair]; } else { this.eventListeners_[eventName].push(pair); } } } this.createDragInterface_(); this.start_(); }; /** * Triggers a cascade of events to the various plugins which are interested in them. * Returns true if the "default behavior" should be prevented, i.e. if one * of the event listeners called event.preventDefault(). * @private */ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { if (!(name in this.eventListeners_)) return false; // QUESTION: can we use objects & prototypes to speed this up? var e = { dygraph: this, cancelable: false, defaultPrevented: false, preventDefault: function() { if (!e.cancelable) throw "Cannot call preventDefault on non-cancelable event."; e.defaultPrevented = true; }, propagationStopped: false, stopPropagation: function() { e.propagationStopped = true; } }; utils.update(e, extra_props); var callback_plugin_pairs = this.eventListeners_[name]; if (callback_plugin_pairs) { for (var i = callback_plugin_pairs.length - 1; i >= 0; i--) { var plugin = callback_plugin_pairs[i][0]; var callback = callback_plugin_pairs[i][1]; callback.call(plugin, e); if (e.propagationStopped) break; } } return e.defaultPrevented; }; /** * Fetch a plugin instance of a particular class. Only for testing. * @private * @param {!Class} type The type of the plugin. * @return {Object} Instance of the plugin, or null if there is none. */ Dygraph.prototype.getPluginInstance_ = function(type) { for (var i = 0; i < this.plugins_.length; i++) { var p = this.plugins_[i]; if (p.plugin instanceof type) { return p.plugin; } } return null; }; /** * Returns the zoomed status of the chart for one or both axes. * * Axis is an optional parameter. Can be set to 'x' or 'y'. * * The zoomed status for an axis is set whenever a user zooms using the mouse * or when the dateWindow or valueRange are updated (unless the * isZoomedIgnoreProgrammaticZoom option is also specified). */ Dygraph.prototype.isZoomed = function(axis) { if (axis === null || axis === undefined) { return this.zoomed_x_ || this.zoomed_y_; } if (axis === 'x') return this.zoomed_x_; if (axis === 'y') return this.zoomed_y_; throw "axis parameter is [" + axis + "] must be null, 'x' or 'y'."; }; /** * Returns information about the Dygraph object, including its containing ID. */ Dygraph.prototype.toString = function() { var maindiv = this.maindiv_; var id = (maindiv && maindiv.id) ? maindiv.id : maindiv; return "[Dygraph " + id + "]"; }; /** * @private * Returns the value of an option. This may be set by the user (either in the * constructor or by calling updateOptions) or by dygraphs, and may be set to a * per-series value. * @param {string} name The name of the option, e.g. 'rollPeriod'. * @param {string} [seriesName] The name of the series to which the option * will be applied. If no per-series value of this option is available, then * the global value is returned. This is optional. * @return { ... } The value of the option. */ Dygraph.prototype.attr_ = function(name, seriesName) { // For "production" code, this gets removed by uglifyjs. if (process.env.NODE_ENV != 'production') { if (typeof(OPTIONS_REFERENCE) === 'undefined') { console.error('Must include options reference JS for testing'); } else if (!OPTIONS_REFERENCE.hasOwnProperty(name)) { console.error('Dygraphs is using property ' + name + ', which has no ' + 'entry in the Dygraphs.OPTIONS_REFERENCE listing.'); // Only log this error once. OPTIONS_REFERENCE[name] = true; } } return seriesName ? this.attributes_.getForSeries(name, seriesName) : this.attributes_.get(name); }; /** * Returns the current value for an option, as set in the constructor or via * updateOptions. You may pass in an (optional) series name to get per-series * values for the option. * * All values returned by this method should be considered immutable. If you * modify them, there is no guarantee that the changes will be honored or that * dygraphs will remain in a consistent state. If you want to modify an option, * use updateOptions() instead. * * @param {string} name The name of the option (e.g. 'strokeWidth') * @param {string=} opt_seriesName Series name to get per-series values. * @return {*} The value of the option. */ Dygraph.prototype.getOption = function(name, opt_seriesName) { return this.attr_(name, opt_seriesName); }; /** * Like getOption(), but specifically returns a number. * This is a convenience function for working with the Closure Compiler. * @param {string} name The name of the option (e.g. 'strokeWidth') * @param {string=} opt_seriesName Series name to get per-series values. * @return {number} The value of the option. * @private */ Dygraph.prototype.getNumericOption = function(name, opt_seriesName) { return /** @type{number} */(this.getOption(name, opt_seriesName)); }; /** * Like getOption(), but specifically returns a string. * This is a convenience function for working with the Closure Compiler. * @param {string} name The name of the option (e.g. 'strokeWidth') * @param {string=} opt_seriesName Series name to get per-series values. * @return {string} The value of the option. * @private */ Dygraph.prototype.getStringOption = function(name, opt_seriesName) { return /** @type{string} */(this.getOption(name, opt_seriesName)); }; /** * Like getOption(), but specifically returns a boolean. * This is a convenience function for working with the Closure Compiler. * @param {string} name The name of the option (e.g. 'strokeWidth') * @param {string=} opt_seriesName Series name to get per-series values. * @return {boolean} The value of the option. * @private */ Dygraph.prototype.getBooleanOption = function(name, opt_seriesName) { return /** @type{boolean} */(this.getOption(name, opt_seriesName)); }; /** * Like getOption(), but specifically returns a function. * This is a convenience function for working with the Closure Compiler. * @param {string} name The name of the option (e.g. 'strokeWidth') * @param {string=} opt_seriesName Series name to get per-series values. * @return {function(...)} The value of the option. * @private */ Dygraph.prototype.getFunctionOption = function(name, opt_seriesName) { return /** @type{function(...)} */(this.getOption(name, opt_seriesName)); }; Dygraph.prototype.getOptionForAxis = function(name, axis) { return this.attributes_.getForAxis(name, axis); }; /** * @private * @param {string} axis The name of the axis (i.e. 'x', 'y' or 'y2') * @return { ... } A function mapping string -> option value */ Dygraph.prototype.optionsViewForAxis_ = function(axis) { var self = this; return function(opt) { var axis_opts = self.user_attrs_.axes; if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { return axis_opts[axis][opt]; } // I don't like that this is in a second spot. if (axis === 'x' && opt === 'logscale') { // return the default value. // TODO(konigsberg): pull the default from a global default. return false; } // user-specified attributes always trump defaults, even if they're less // specific. if (typeof(self.user_attrs_[opt]) != 'undefined') { return self.user_attrs_[opt]; } axis_opts = self.attrs_.axes; if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { return axis_opts[axis][opt]; } // check old-style axis options // TODO(danvk): add a deprecation warning if either of these match. if (axis == 'y' && self.axes_[0].hasOwnProperty(opt)) { return self.axes_[0][opt]; } else if (axis == 'y2' && self.axes_[1].hasOwnProperty(opt)) { return self.axes_[1][opt]; } return self.attr_(opt); }; }; /** * Returns the current rolling period, as set by the user or an option. * @return {number} The number of points in the rolling window */ Dygraph.prototype.rollPeriod = function() { return this.rollPeriod_; }; /** * Returns the currently-visible x-range. This can be affected by zooming, * panning or a call to updateOptions. * Returns a two-element array: [left, right]. * If the Dygraph has dates on the x-axis, these will be millis since epoch. */ Dygraph.prototype.xAxisRange = function() { return this.dateWindow_ ? this.dateWindow_ : this.xAxisExtremes(); }; /** * Returns the lower- and upper-bound x-axis values of the * data set. */ Dygraph.prototype.xAxisExtremes = function() { var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w; if (this.numRows() === 0) { return [0 - pad, 1 + pad]; } var left = this.rawData_[0][0]; var right = this.rawData_[this.rawData_.length - 1][0]; if (pad) { // Must keep this in sync with dygraph-layout _evaluateLimits() var range = right - left; left -= range * pad; right += range * pad; } return [left, right]; }; /** * Returns the currently-visible y-range for an axis. This can be affected by * zooming, panning or a call to updateOptions. Axis indices are zero-based. If * called with no arguments, returns the range of the first axis. * Returns a two-element array: [bottom, top]. */ Dygraph.prototype.yAxisRange = function(idx) { if (typeof(idx) == "undefined") idx = 0; if (idx < 0 || idx >= this.axes_.length) { return null; } var axis = this.axes_[idx]; return [ axis.computedValueRange[0], axis.computedValueRange[1] ]; }; /** * Returns the currently-visible y-ranges for each axis. This can be affected by * zooming, panning, calls to updateOptions, etc. * Returns an array of [bottom, top] pairs, one for each y-axis. */ Dygraph.prototype.yAxisRanges = function() { var ret = []; for (var i = 0; i < this.axes_.length; i++) { ret.push(this.yAxisRange(i)); } return ret; }; // TODO(danvk): use these functions throughout dygraphs. /** * Convert from data coordinates to canvas/div X/Y coordinates. * If specified, do this conversion for the coordinate system of a particular * axis. Uses the first axis by default. * Returns a two-element array: [X, Y] * * Note: use toDomXCoord instead of toDomCoords(x, null) and use toDomYCoord * instead of toDomCoords(null, y, axis). */ Dygraph.prototype.toDomCoords = function(x, y, axis) { return [ this.toDomXCoord(x), this.toDomYCoord(y, axis) ]; }; /** * Convert from data x coordinates to canvas/div X coordinate. * If specified, do this conversion for the coordinate system of a particular * axis. * Returns a single value or null if x is null. */ Dygraph.prototype.toDomXCoord = function(x) { if (x === null) { return null; } var area = this.plotter_.area; var xRange = this.xAxisRange(); return area.x + (x - xRange[0]) / (xRange[1] - xRange[0]) * area.w; }; /** * Convert from data x coordinates to canvas/div Y coordinate and optional * axis. Uses the first axis by default. * * returns a single value or null if y is null. */ Dygraph.prototype.toDomYCoord = function(y, axis) { var pct = this.toPercentYCoord(y, axis); if (pct === null) { return null; } var area = this.plotter_.area; return area.y + pct * area.h; }; /** * Convert from canvas/div coords to data coordinates. * If specified, do this conversion for the coordinate system of a particular * axis. Uses the first axis by default. * Returns a two-element array: [X, Y]. * * Note: use toDataXCoord instead of toDataCoords(x, null) and use toDataYCoord * instead of toDataCoords(null, y, axis). */ Dygraph.prototype.toDataCoords = function(x, y, axis) { return [ this.toDataXCoord(x), this.toDataYCoord(y, axis) ]; }; /** * Convert from canvas/div x coordinate to data coordinate. * * If x is null, this returns null. */ Dygraph.prototype.toDataXCoord = function(x) { if (x === null) { return null; } var area = this.plotter_.area; var xRange = this.xAxisRange(); if (!this.attributes_.getForAxis("logscale", 'x')) { return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]); } else { var pct = (x - area.x) / area.w; return utils.logRangeFraction(xRange[0], xRange[1], pct); } }; /** * Convert from canvas/div y coord to value. * * If y is null, this returns null. * if axis is null, this uses the first axis. */ Dygraph.prototype.toDataYCoord = function(y, axis) { if (y === null) { return null; } var area = this.plotter_.area; var yRange = this.yAxisRange(axis); if (typeof(axis) == "undefined") axis = 0; if (!this.attributes_.getForAxis("logscale", axis)) { return yRange[0] + (area.y + area.h - y) / area.h * (yRange[1] - yRange[0]); } else { // Computing the inverse of toDomCoord. var pct = (y - area.y) / area.h; // Note reversed yRange, y1 is on top with pct==0. return utils.logRangeFraction(yRange[1], yRange[0], pct); } }; /** * Converts a y for an axis to a percentage from the top to the * bottom of the drawing area. * * If the coordinate represents a value visible on the canvas, then * the value will be between 0 and 1, where 0 is the top of the canvas. * However, this method will return values outside the range, as * values can fall outside the canvas. * * If y is null, this returns null. * if axis is null, this uses the first axis. * * @param {number} y The data y-coordinate. * @param {number} [axis] The axis number on which the data coordinate lives. * @return {number} A fraction in [0, 1] where 0 = the top edge. */ Dygraph.prototype.toPercentYCoord = function(y, axis) { if (y === null) { return null; } if (typeof(axis) == "undefined") axis = 0; var yRange = this.yAxisRange(axis); var pct; var logscale = this.attributes_.getForAxis("logscale", axis); if (logscale) { var logr0 = utils.log10(yRange[0]); var logr1 = utils.log10(yRange[1]); pct = (logr1 - utils.log10(y)) / (logr1 - logr0); } else { // yRange[1] - y is unit distance from the bottom. // yRange[1] - yRange[0] is the scale of the range. // (yRange[1] - y) / (yRange[1] - yRange[0]) is the % from the bottom. pct = (yRange[1] - y) / (yRange[1] - yRange[0]); } return pct; }; /** * Converts an x value to a percentage from the left to the right of * the drawing area. * * If the coordinate represents a value visible on the canvas, then * the value will be between 0 and 1, where 0 is the left of the canvas. * However, this method will return values outside the range, as * values can fall outside the canvas. * * If x is null, this returns null. * @param {number} x The data x-coordinate. * @return {number} A fraction in [0, 1] where 0 = the left edge. */ Dygraph.prototype.toPercentXCoord = function(x) { if (x === null) { return null; } var xRange = this.xAxisRange(); var pct; var logscale = this.attributes_.getForAxis("logscale", 'x') ; if (logscale === true) { // logscale can be null so we test for true explicitly. var logr0 = utils.log10(xRange[0]); var logr1 = utils.log10(xRange[1]); pct = (utils.log10(x) - logr0) / (logr1 - logr0); } else { // x - xRange[0] is unit distance from the left. // xRange[1] - xRange[0] is the scale of the range. // The full expression below is the % from the left. pct = (x - xRange[0]) / (xRange[1] - xRange[0]); } return pct; }; /** * Returns the number of columns (including the independent variable). * @return {number} The number of columns. */ Dygraph.prototype.numColumns = function() { if (!this.rawData_) return 0; return this.rawData_[0] ? this.rawData_[0].length : this.attr_("labels").length; }; /** * Returns the number of rows (excluding any header/label row). * @return {number} The number of rows, less any header. */ Dygraph.prototype.numRows = function() { if (!this.rawData_) return 0; return this.rawData_.length; }; /** * Returns the value in the given row and column. If the row and column exceed * the bounds on the data, returns null. Also returns null if the value is * missing. * @param {number} row The row number of the data (0-based). Row 0 is the * first row of data, not a header row. * @param {number} col The column number of the data (0-based) * @return {number} The value in the specified cell or null if the row/col * were out of range. */ Dygraph.prototype.getValue = function(row, col) { if (row < 0 || row > this.rawData_.length) return null; if (col < 0 || col > this.rawData_[row].length) return null; return this.rawData_[row][col]; }; /** * Generates interface elements for the Dygraph: a containing div, a div to * display the current point, and a textbox to adjust the rolling average * period. Also creates the Renderer/Layout elements. * @private */ Dygraph.prototype.createInterface_ = function() { // Create the all-enclosing graph div var enclosing = this.maindiv_; this.graphDiv = document.createElement("div"); // TODO(danvk): any other styles that are useful to set here? this.graphDiv.style.textAlign = 'left'; // This is a CSS "reset" this.graphDiv.style.position = 'relative'; enclosing.appendChild(this.graphDiv); // Create the canvas for interactive parts of the chart. this.canvas_ = utils.createCanvas(); this.canvas_.style.position = "absolute"; // ... and for static parts of the chart. this.hidden_ = this.createPlotKitCanvas_(this.canvas_); this.canvas_ctx_ = utils.getContext(this.canvas_); this.hidden_ctx_ = utils.getContext(this.hidden_); this.resizeElements_(); // The interactive parts of the graph are drawn on top of the chart. this.graphDiv.appendChild(this.hidden_); this.graphDiv.appendChild(this.canvas_); this.mouseEventElement_ = this.createMouseEventElement_(); // Create the grapher this.layout_ = new DygraphLayout(this); var dygraph = this; this.mouseMoveHandler_ = function(e) { dygraph.mouseMove_(e); }; this.mouseOutHandler_ = function(e) { // The mouse has left the chart if: // 1. e.target is inside the chart // 2. e.relatedTarget is outside the chart var target = e.target || e.fromElement; var relatedTarget = e.relatedTarget || e.toElement; if (utils.isNodeContainedBy(target, dygraph.graphDiv) && !utils.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { dygraph.mouseOut_(e); } }; this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_); this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); // Don't recreate and register the resize handler on subsequent calls. // This happens when the graph is resized. if (!this.resizeHandler_) { this.resizeHandler_ = function(e) { dygraph.resize(); }; // Update when the window is resized. // TODO(danvk): drop frames depending on complexity of the chart. this.addAndTrackEvent(window, 'resize', this.resizeHandler_); } }; Dygraph.prototype.resizeElements_ = function() { this.graphDiv.style.width = this.width_ + "px"; this.graphDiv.style.height = this.height_ + "px"; var canvasScale = utils.getContextPixelRatio(this.canvas_ctx_); this.canvas_.width = this.width_ * canvasScale; this.canvas_.height = this.height_ * canvasScale; this.canvas_.style.width = this.width_ + "px"; // for IE this.canvas_.style.height = this.height_ + "px"; // for IE if (canvasScale !== 1) { this.canvas_ctx_.scale(canvasScale, canvasScale); } var hiddenScale = utils.getContextPixelRatio(this.hidden_ctx_); this.hidden_.width = this.width_ * hiddenScale; this.hidden_.height = this.height_ * hiddenScale; this.hidden_.style.width = this.width_ + "px"; // for IE this.hidden_.style.height = this.height_ + "px"; // for IE if (hiddenScale !== 1) { this.hidden_ctx_.scale(hiddenScale, hiddenScale); } }; /** * Detach DOM elements in the dygraph and null out all data references. * Calling this when you're done with a dygraph can dramatically reduce memory * usage. See, e.g., the tests/perf.html example. */ Dygraph.prototype.destroy = function() { this.canvas_ctx_.restore(); this.hidden_ctx_.restore(); // Destroy any plugins, in the reverse order that they were registered. for (var i = this.plugins_.length - 1; i >= 0; i--) { var p = this.plugins_.pop(); if (p.plugin.destroy) p.plugin.destroy(); } var removeRecursive = function(node) { while (node.hasChildNodes()) { removeRecursive(node.firstChild); node.removeChild(node.firstChild); } }; this.removeTrackedEvents_(); // remove mouse event handlers (This may not be necessary anymore) utils.removeEvent(window, 'mouseout', this.mouseOutHandler_); utils.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); // remove window handlers utils.removeEvent(window,'resize', this.resizeHandler_); this.resizeHandler_ = null; removeRecursive(this.maindiv_); var nullOut = function(obj) { for (var n in obj) { if (typeof(obj[n]) === 'object') { obj[n] = null; } } }; // These may not all be necessary, but it can't hurt... nullOut(this.layout_); nullOut(this.plotter_); nullOut(this); }; /** * Creates the canvas on which the chart will be drawn. Only the Renderer ever * draws on this particular canvas. All Dygraph work (i.e. drawing hover dots * or the zoom rectangles) is done on this.canvas_. * @param {Object} canvas The Dygraph canvas over which to overlay the plot * @return {Object} The newly-created canvas * @private */ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { var h = utils.createCanvas(); h.style.position = "absolute"; // TODO(danvk): h should be offset from canvas. canvas needs to include // some extra area to make it easier to zoom in on the far left and far // right. h needs to be precisely the plot area, so that clipping occurs. h.style.top = canvas.style.top; h.style.left = canvas.style.left; h.width = this.width_; h.height = this.height_; h.style.width = this.width_ + "px"; // for IE h.style.height = this.height_ + "px"; // for IE return h; }; /** * Creates an overlay element used to handle mouse events. * @return {Object} The mouse event element. * @private */ Dygraph.prototype.createMouseEventElement_ = function() { return this.canvas_; }; /** * Generate a set of distinct colors for the data series. This is done with a * color wheel. Saturation/Value are customizable, and the hue is * equally-spaced around the color wheel. If a custom set of colors is * specified, that is used instead. * @private */ Dygraph.prototype.setColors_ = function() { var labels = this.getLabels(); var num = labels.length - 1; this.colors_ = []; this.colorsMap_ = {}; // These are used for when no custom colors are specified. var sat = this.getNumericOption('colorSaturation') || 1.0; var val = this.getNumericOption('colorValue') || 0.5; var half = Math.ceil(num / 2); var colors = this.getOption('colors'); var visibility = this.visibility(); for (var i = 0; i < num; i++) { if (!visibility[i]) { continue; } var label = labels[i + 1]; var colorStr = this.attributes_.getForSeries('color', label); if (!colorStr) { if (colors) { colorStr = colors[i % colors.length]; } else { // alternate colors for high contrast. var idx = i % 2 ? (half + (i + 1)/ 2) : Math.ceil((i + 1) / 2); var hue = (1.0 * idx / (1 + num)); colorStr = utils.hsvToRGB(hue, sat, val); } } this.colors_.push(colorStr); this.colorsMap_[label] = colorStr; } }; /** * Return the list of colors. This is either the list of colors passed in the * attributes or the autogenerated list of rgb(r,g,b) strings. * This does not return colors for invisible series. * @return {Array.<string>} The list of colors. */ Dygraph.prototype.getColors = function() { return this.colors_; }; /** * Returns a few attributes of a series, i.e. its color, its visibility, which * axis it's assigned to, and its column in the original data. * Returns null if the series does not exist. * Otherwise, returns an object with column, visibility, color and axis properties. * The "axis" property will be set to 1 for y1 and 2 for y2. * The "column" property can be fed back into getValue(row, column) to get * values for this series. */ Dygraph.prototype.getPropertiesForSeries = function(series_name) { var idx = -1; var labels = this.getLabels(); for (var i = 1; i < labels.length; i++) { if (labels[i] == series_name) { idx = i; break; } } if (idx == -1) return null; return { name: series_name, column: idx, visible: this.visibility()[idx - 1], color: this.colorsMap_[series_name], axis: 1 + this.attributes_.axisForSeries(series_name) }; }; /** * Create the text box to adjust the averaging period * @private */ Dygraph.prototype.createRollInterface_ = function() { // Create a roller if one doesn't exist already. if (!this.roller_) { this.roller_ = document.createElement("input"); this.roller_.type = "text"; this.roller_.style.display = "none"; this.graphDiv.appendChild(this.roller_); } var display = this.getBooleanOption('showRoller') ? 'block' : 'none'; var area = this.plotter_.area; var textAttr = { "position": "absolute", "zIndex": 10, "top": (area.y + area.h - 25) + "px", "left": (area.x + 1) + "px", "display": display }; this.roller_.size = "2"; this.roller_.value = this.rollPeriod_; for (var name in textAttr) { if (textAttr.hasOwnProperty(name)) { this.roller_.style[name] = textAttr[name]; } } var dygraph = this; this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); }; }; /** * Set up all the mouse handlers needed to capture dragging behavior for zoom * events. * @private */ Dygraph.prototype.createDragInterface_ = function() { var context = { // Tracks whether the mouse is down right now isZooming: false, isPanning: false, // is this drag part of a pan? is2DPan: false, // if so, is that pan 1- or 2-dimensional? dragStartX: null, // pixel coordinates dragStartY: null, // pixel coordinates dragEndX: null, // pixel coordinates dragEndY: null, // pixel coordinates dragDirection: null, prevEndX: null, // pixel coordinates prevEndY: null, // pixel coordinates prevDragDirection: null, cancelNextDblclick: false, // see comment in dygraph-interaction-model.js // The value on the left side of the graph when a pan operation starts. initialLeftmostDate: null, // The number of units each pixel spans. (This won't be valid for log // scales) xUnitsPerPixel: null, // TODO(danvk): update this comment // The range in second/value units that the viewport encompasses during a // panning operation. dateRange: null, // Top-left corner of the canvas, in DOM coords // TODO(konigsberg): Rename topLeftCanvasX, topLeftCanvasY. px: 0, py: 0, // Values for use with panEdgeFraction, which limit how far outside the // graph's data boundaries it can be panned. boundedDates: null, // [minDate, maxDate] boundedValues: null, // [[minValue, maxValue] ...] // We cover iframes during mouse interactions. See comments in // dygraph-utils.js for more info on why this is a good idea. tarp: new IFrameTarp(), // contextB is the same thing as this context object but renamed. initializeMouseDown: function(event, g, contextB) { // prevents mouse drags from selecting page text. if (event.preventDefault) { event.preventDefault(); // Firefox, Chrome, etc. } else { event.returnValue = false; // IE event.cancelBubble = true; } var canvasPos = utils.findPos(g.canvas_); contextB.px = canvasPos.x; contextB.py = canvasPos.y; contextB.dragStartX = utils.dragGetX_(event, contextB); contextB.dragStartY = utils.dragGetY_(event, contextB); contextB.cancelNextDblclick = false; contextB.tarp.cover(); }, destroy: function() { var context = this; if (context.isZooming || context.isPanning) { context.isZooming = false; context.dragStartX = null; context.dragStartY = null; } if (context.isPanning) { context.isPanning = false; context.draggingDate = null; context.dateRange = null; for (var i = 0; i < self.axes_.length; i++) { delete self.axes_[i].draggingValue; delete self.axes_[i].dragValueRange; } } context.tarp.uncover(); } }; var interactionModel = this.getOption("interactionModel"); // Self is the graph. var self = this; // Function that binds the graph and context to the handler. var bindHandler = function(handler) { return function(event) { handler(event, self, context); }; }; for (var eventName in interactionModel) { if (!interactionModel.hasOwnProperty(eventName)) continue; this.addAndTrackEvent(this.mouseEventElement_, eventName, bindHandler(interactionModel[eventName])); } // If the user releases the mouse button during a drag, but not over the // canvas, then it doesn't count as a zooming action. if (!interactionModel.willDestroyContextMyself) { var mouseUpHandler = function(event) { context.destroy(); }; this.addAndTrackEvent(document, 'mouseup', mouseUpHandler); } }; /** * Draw a gray zoom rectangle over the desired area of the canvas. Also clears * up any previous zoom rectangles that were drawn. This could be optimized to * avoid extra redrawing, but it's tricky to avoid interactions with the status * dots. * * @param {number} direction the direction of the zoom rectangle. Acceptable * values are utils.HORIZONTAL and utils.VERTICAL. * @param {number} startX The X position where the drag started, in canvas * coordinates. * @param {number} endX The current X position of the drag, in canvas coords. * @param {number} startY The Y position where the drag started, in canvas * coordinates. * @param {number} endY The current Y position of the drag, in canvas coords. * @param {number} prevDirection the value of direction on the previous call to * this function. Used to avoid excess redrawing * @param {number} prevEndX The value of endX on the previous call to this * function. Used to avoid excess redrawing * @param {number} prevEndY The value of endY on the previous call to this * function. Used to avoid excess redrawing * @private */ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, endY, prevDirection, prevEndX, prevEndY) { var ctx = this.canvas_ctx_; // Clean up from the previous rect if necessary if (prevDirection == utils.HORIZONTAL) { ctx.clearRect(Math.min(startX, prevEndX), this.layout_.getPlotArea().y, Math.abs(startX - prevEndX), this.layout_.getPlotArea().h); } else if (prevDirection == utils.VERTICAL) { ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY), this.layout_.getPlotArea().w, Math.abs(startY - prevEndY)); } // Draw a light-grey rectangle to show the new viewing area if (direction == utils.HORIZONTAL) { if (endX && startX) { ctx.fillStyle = "rgba(128,128,128,0.33)"; ctx.fillRect(Math.min(startX, endX), this.layout_.getPlotArea().y, Math.abs(endX - startX), this.layout_.getPlotArea().h); } } else if (direction == utils.VERTICAL) { if (endY && startY) { ctx.fillStyle = "rgba(128,128,128,0.33)"; ctx.fillRect(this.layout_.getPlotArea().x, Math.min(startY, endY), this.layout_.getPlotArea().w, Math.abs(endY - startY)); } } }; /** * Clear the zoom rectangle (and perform no zoom). * @private */ Dygraph.prototype.clearZoomRect_ = function() { this.currentZoomRectArgs_ = null; this.canvas_ctx_.clearRect(0, 0, this.width_, this.height_); }; /** * Zoom to something containing [lowX, highX]. These are pixel coordinates in * the canvas. The exact zoom window may be slightly larger if there are no data * points near lowX or highX. Don't confuse this function with doZoomXDates, * which accepts dates that match the raw data. This function redraws the graph. * * @param {number} lowX The leftmost pixel value that should be visible. * @param {number} highX The rightmost pixel value that should be visible. * @private */ Dygraph.prototype.doZoomX_ = function(lowX, highX) { this.currentZoomRectArgs_ = null; // Find the earliest and latest dates contained in this canvasx range. // Convert the call to date ranges of the raw data. var minDate = this.toDataXCoord(lowX); var maxDate = this.toDataXCoord(highX); this.doZoomXDates_(minDate, maxDate); }; /** * Zoom to something containing [minDate, maxDate] values. Don't confuse this * method with doZoomX which accepts pixel coordinates. This function redraws * the graph. * * @param {number} minDate The minimum date that should be visible. * @param {number} maxDate The maximum date that should be visible. * @private */ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { // TODO(danvk): when xAxisRange is null (i.e. "fit to data", the animation // can produce strange effects. Rather than the x-axis transitioning slowly // between values, it can jerk around.) var old_window = this.xAxisRange(); var new_window = [minDate, maxDate]; this.zoomed_x_ = true; var that = this; this.doAnimatedZoom(old_window, new_window, null, null, function() { if (that.getFunctionOption("zoomCallback")) { that.getFunctionOption("zoomCallback").call(that, minDate, maxDate, that.yAxisRanges()); } }); }; /** * Zoom to something containing [lowY, highY]. These are pixel coordinates in * the canvas. This function redraws the graph. * * @param {number} lowY The topmost pixel value that should be visible. * @param {number} highY The lowest pixel value that should be visible. * @private */ Dygraph.prototype.doZoomY_ = function(lowY, highY) { this.currentZoomRectArgs_ = null; // Find the highest and lowest values in pixel range for each axis. // Note that lowY (in pixels) corresponds to the max Value (in data coords). // This is because pixels increase as you go down on the screen, whereas data // coordinates increase as you go up the screen. var oldValueRanges = this.yAxisRanges(); var newValueRanges = []; for (var i = 0; i < this.axes_.length; i++) { var hi = this.toDataYCoord(lowY, i); var low = this.toDataYCoord(highY, i); newValueRanges.push([low, hi]); } this.zoomed_y_ = true; var that = this; this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() { if (that.getFunctionOption("zoomCallback")) { var xRange = that.xAxisRange(); that.getFunctionOption("zoomCallback").call(that, xRange[0], xRange[1], that.yAxisRanges()); } }); }; /** * Transition function to use in animations. Returns values between 0.0 * (totally old values) and 1.0 (totally new values) for each frame. * @private */ Dygraph.zoomAnimationFunction = function(frame, numFrames) { var k = 1.5; return (1.0 - Math.pow(k, -frame)) / (1.0 - Math.pow(k, -numFrames)); }; /** * Reset the zoom to the original view coordinates. This is the same as * double-clicking on the graph. */ Dygraph.prototype.resetZoom = function() { var dirty = false, dirtyX = false, dirtyY = false; if (this.dateWindow_ !== null) { dirty = true; dirtyX = true; } for (var i = 0; i < this.axes_.length; i++) { if (typeof(this.axes_[i].valueWindow) !== 'undefined' && this.axes_[i].valueWindow !== null) { dirty = true; dirtyY = true; } } // Clear any selection, since it's likely to be drawn in the wrong place. this.clearSelection(); if (dirty) { this.zoomed_x_ = false; this.zoomed_y_ = false; //calculate extremes to avoid lack of padding on reset. var extremes = this.xAxisExtremes(); var minDate = extremes[0], maxDate = extremes[1]; // TODO(danvk): merge this block w/ the code below. if (!this.getBooleanOption("animatedZooms")) { this.dateWindow_ = null; for (i = 0; i < this.axes_.length; i++) { if (this.axes_[i].valueWindow !== null) { delete this.axes_[i].valueWindow; } } this.drawGraph_(); if (this.getFunctionOption("zoomCallback")) { this.getFunctionOption("zoomCallback").call(this, minDate, maxDate, this.yAxisRanges()); } return; } var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null; if (dirtyX) { oldWindow = this.xAxisRange(); newWindow = [minDate, maxDate]; } if (dirtyY) { oldValueRanges = this.yAxisRanges(); // TODO(danvk): this is pretty inefficient var packed = this.gatherDatasets_(this.rolledSeries_, null); var extremes = packed.extremes; // this has the side-effect of modifying this.axes_. // this doesn't make much sense in this context, but it's convenient (we // need this.axes_[*].extremeValues) and not harmful since we'll be // calling drawGraph_ shortly, which clobbers these values. this.computeYAxisRanges_(extremes); newValueRanges = []; for (i = 0; i < this.axes_.length; i++) { var axis = this.axes_[i]; newValueRanges.push((axis.valueRange !== null && axis.valueRange !== undefined) ? axis.valueRange : axis.extremeRange); } } var that = this; this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges, function() { that.dateWindow_ = null; for (var i = 0; i < that.axes_.length; i++) { if (that.axes_[i].valueWindow !== null) { delete that.axes_[i].valueWindow; } } if (that.getFunctionOption("zoomCallback")) { that.getFunctionOption("zoomCallback").call(that, minDate, maxDate, that.yAxisRanges()); } }); } }; /** * Combined animation logic for all zoom functions. * either the x parameters or y parameters may be null. * @private */ Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) { var steps = this.getBooleanOption("animatedZooms") ? Dygraph.ANIMATION_STEPS : 1; var windows = []; var valueRanges = []; var step, frac; if (oldXRange !== null && newXRange !== null) { for (step = 1; step <= steps; step++) { frac = Dygraph.zoomAnimationFunction(step, steps); windows[step-1] = [oldXRange[0]*(1-frac) + frac*newXRange[0], oldXRange[1]*(1-frac) + frac*newXRange[1]]; } } if (oldYRanges !== null && newYRanges !== null) { for (step = 1; step <= steps; step++) { frac = Dygraph.zoomAnimationFunction(step, steps); var thisRange = []; for (var j = 0; j < this.axes_.length; j++) { thisRange.push([oldYRanges[j][0]*(1-frac) + frac*newYRanges[j][0], oldYRanges[j][1]*(1-frac) + frac*newYRanges[j][1]]); } valueRanges[step-1] = thisRange; } } var that = this; utils.repeatAndCleanup(function(step) { if (valueRanges.length) { for (var i = 0; i < that.axes_.length; i++) { var w = valueRanges[step][i]; that.axes_[i].valueWindow = [w[0], w[1]]; } } if (windows.length) { that.dateWindow_ = windows[step]; } that.drawGraph_(); }, steps, Dygraph.ANIMATION_DURATION / steps, callback); }; /** * Get the current graph's area object. * * Returns: {x, y, w, h} */ Dygraph.prototype.getArea = function() { return this.plotter_.area; }; /** * Convert a mouse event to DOM coordinates relative to the graph origin. * * Returns a two-element array: [X, Y]. */ Dygraph.prototype.eventToDomCoords = function(event) { if (event.offsetX && event.offsetY) { return [ event.offsetX, event.offsetY ]; } else { var eve