X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=src%2Fdygraph.js;h=531191fa8141bba036abea6589a33695d9eda7a2;hb=64f1c4dfd7425931fcd1bd9949157c0ba6958656;hp=0b42d863ef26b534df69fe75384a091c755fd7b5;hpb=ca05e26581fd8625f0980f511655e4540f73c2e9;p=dygraphs.git diff --git a/src/dygraph.js b/src/dygraph.js index 0b42d86..531191f 100644 --- a/src/dygraph.js +++ b/src/dygraph.js @@ -40,14 +40,34 @@ And error bars will be calculated automatically using a binomial distribution. For further documentation and examples, see http://dygraphs.com/ - */ -// For "production" code, this gets set to false by uglifyjs. -if (typeof(DEBUG) === 'undefined') DEBUG=true; +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'; -var Dygraph = (function() { -/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */ "use strict"; /** @@ -69,7 +89,7 @@ var Dygraph = function(div, data, opts) { }; Dygraph.NAME = "Dygraph"; -Dygraph.VERSION = "1.1.0"; +Dygraph.VERSION = "2.0.0"; // Various default values Dygraph.DEFAULT_ROLL_PERIOD = 1; @@ -80,153 +100,6 @@ Dygraph.DEFAULT_HEIGHT = 320; Dygraph.ANIMATION_STEPS = 12; Dygraph.ANIMATION_DURATION = 200; -// Label constants for the labelsKMB and labelsKMG2 options. -// (i.e. '100000' -> '100K') -Dygraph.KMB_LABELS = [ 'K', 'M', 'B', 'T', 'Q' ]; -Dygraph.KMG2_BIG_LABELS = [ 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' ]; -Dygraph.KMG2_SMALL_LABELS = [ 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y' ]; - -// These are defined before DEFAULT_ATTRS so that it can refer to them. -/** - * @private - * Return a string version of a number. This respects the digitsAfterDecimal - * and maxNumberWidth options. - * @param {number} x The number to be formatted - * @param {Dygraph} opts An options view - */ -Dygraph.numberValueFormatter = function(x, opts) { - var sigFigs = opts('sigFigs'); - - if (sigFigs !== null) { - // User has opted for a fixed number of significant figures. - return Dygraph.floatFormat(x, sigFigs); - } - - var digits = opts('digitsAfterDecimal'); - var maxNumberWidth = opts('maxNumberWidth'); - - var kmb = opts('labelsKMB'); - var kmg2 = opts('labelsKMG2'); - - var label; - - // switch to scientific notation if we underflow or overflow fixed display. - if (x !== 0.0 && - (Math.abs(x) >= Math.pow(10, maxNumberWidth) || - Math.abs(x) < Math.pow(10, -digits))) { - label = x.toExponential(digits); - } else { - label = '' + Dygraph.round_(x, digits); - } - - if (kmb || kmg2) { - var k; - var k_labels = []; - var m_labels = []; - if (kmb) { - k = 1000; - k_labels = Dygraph.KMB_LABELS; - } - if (kmg2) { - if (kmb) console.warn("Setting both labelsKMB and labelsKMG2. Pick one!"); - k = 1024; - k_labels = Dygraph.KMG2_BIG_LABELS; - m_labels = Dygraph.KMG2_SMALL_LABELS; - } - - var absx = Math.abs(x); - var n = Dygraph.pow(k, k_labels.length); - for (var j = k_labels.length - 1; j >= 0; j--, n /= k) { - if (absx >= n) { - label = Dygraph.round_(x / n, digits) + k_labels[j]; - break; - } - } - if (kmg2) { - // TODO(danvk): clean up this logic. Why so different than kmb? - var x_parts = String(x.toExponential()).split('e-'); - if (x_parts.length === 2 && x_parts[1] >= 3 && x_parts[1] <= 24) { - if (x_parts[1] % 3 > 0) { - label = Dygraph.round_(x_parts[0] / - Dygraph.pow(10, (x_parts[1] % 3)), - digits); - } else { - label = Number(x_parts[0]).toFixed(2); - } - label += m_labels[Math.floor(x_parts[1] / 3) - 1]; - } - } - } - - return label; -}; - -/** - * variant for use as an axisLabelFormatter. - * @private - */ -Dygraph.numberAxisLabelFormatter = function(x, granularity, opts) { - return Dygraph.numberValueFormatter.call(this, x, opts); -}; - -/** - * @type {!Array.} - * @private - * @constant - */ -Dygraph.SHORT_MONTH_NAMES_ = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - - -/** - * Convert a JS date to a string appropriate to display on an axis that - * is displaying values at the stated granularity. This respects the - * labelsUTC option. - * @param {Date} date The date to format - * @param {number} granularity One of the Dygraph granularity constants - * @param {Dygraph} opts An options view - * @return {string} The date formatted as local time - * @private - */ -Dygraph.dateAxisLabelFormatter = function(date, granularity, opts) { - var utc = opts('labelsUTC'); - var accessors = utc ? Dygraph.DateAccessorsUTC : Dygraph.DateAccessorsLocal; - - var year = accessors.getFullYear(date), - month = accessors.getMonth(date), - day = accessors.getDate(date), - hours = accessors.getHours(date), - mins = accessors.getMinutes(date), - secs = accessors.getSeconds(date), - millis = accessors.getSeconds(date); - - if (granularity >= Dygraph.DECADAL) { - return '' + year; - } else if (granularity >= Dygraph.MONTHLY) { - return Dygraph.SHORT_MONTH_NAMES_[month] + ' ' + year; - } else { - var frac = hours * 3600 + mins * 60 + secs + 1e-3 * millis; - if (frac === 0 || granularity >= Dygraph.DAILY) { - // e.g. '21 Jan' (%d%b) - return Dygraph.zeropad(day) + ' ' + Dygraph.SHORT_MONTH_NAMES_[month]; - } else { - return Dygraph.hmsString_(hours, mins, secs); - } - } -}; -// alias in case anyone is referencing the old method. -Dygraph.dateAxisFormatter = Dygraph.dateAxisLabelFormatter; - -/** - * Return a string version of a JS date for a value label. This respects the - * labelsUTC option. - * @param {Date} date The date to be formatted - * @param {Dygraph} opts An options view - * @private - */ -Dygraph.dateValueFormatter = function(d, opts) { - return Dygraph.dateString_(d, opts('labelsUTC')); -}; - /** * Standard plotters. These may be used by clients. * Available plotters are: @@ -240,143 +113,6 @@ Dygraph.dateValueFormatter = function(d, opts) { Dygraph.Plotters = DygraphCanvasRenderer._Plotters; -// Default attribute values. -Dygraph.DEFAULT_ATTRS = { - highlightCircleSize: 3, - highlightSeriesOpts: null, - highlightSeriesBackgroundAlpha: 0.5, - - labelsDivWidth: 250, - labelsDivStyles: { - // TODO(danvk): move defaults from createStatusMessage_ here. - }, - labelsSeparateLines: false, - labelsShowZeroValues: true, - labelsKMB: false, - labelsKMG2: false, - showLabelsOnHighlight: true, - - digitsAfterDecimal: 2, - maxNumberWidth: 6, - sigFigs: null, - - strokeWidth: 1.0, - strokeBorderWidth: 0, - strokeBorderColor: "white", - - axisTickSize: 3, - axisLabelFontSize: 14, - rightGap: 5, - - showRoller: false, - xValueParser: Dygraph.dateParser, - - delimiter: ',', - - sigma: 2.0, - errorBars: false, - fractions: false, - wilsonInterval: true, // only relevant if fractions is true - customBars: false, - fillGraph: false, - fillAlpha: 0.15, - connectSeparatedPoints: false, - - stackedGraph: false, - stackedGraphNaNFill: 'all', - hideOverlayOnMouseOut: true, - - legend: 'onmouseover', - stepPlot: false, - avoidMinZero: false, - xRangePad: 0, - yRangePad: null, - drawAxesAtZero: false, - - // Sizes of the various chart labels. - titleHeight: 28, - xLabelHeight: 18, - yLabelWidth: 18, - - axisLineColor: "black", - axisLineWidth: 0.3, - gridLineWidth: 0.3, - axisLabelColor: "black", - axisLabelWidth: 50, - gridLineColor: "rgb(128,128,128)", - - interactionModel: null, // will be set to Dygraph.Interaction.defaultModel - animatedZooms: false, // (for now) - - // Range selector options - showRangeSelector: false, - rangeSelectorHeight: 40, - rangeSelectorPlotStrokeColor: "#808FAB", - rangeSelectorPlotFillGradientColor: "white", - rangeSelectorPlotFillColor: "#A7B1C4", - rangeSelectorBackgroundStrokeColor: "gray", - rangeSelectorBackgroundLineWidth: 1, - rangeSelectorPlotLineWidth:1.5, - rangeSelectorForegroundStrokeColor: "black", - rangeSelectorForegroundLineWidth: 1, - rangeSelectorAlpha: 0.6, - showInRangeSelector: null, - - // The ordering here ensures that central lines always appear above any - // fill bars/error bars. - plotter: [ - Dygraph.Plotters.fillPlotter, - Dygraph.Plotters.errorPlotter, - Dygraph.Plotters.linePlotter - ], - - plugins: [ ], - - // per-axis options - axes: { - x: { - pixelsPerLabel: 70, - axisLabelWidth: 60, - axisLabelFormatter: Dygraph.dateAxisLabelFormatter, - valueFormatter: Dygraph.dateValueFormatter, - drawGrid: true, - drawAxis: true, - independentTicks: true, - ticker: null // will be set in dygraph-tickers.js - }, - y: { - axisLabelWidth: 50, - pixelsPerLabel: 30, - valueFormatter: Dygraph.numberValueFormatter, - axisLabelFormatter: Dygraph.numberAxisLabelFormatter, - drawGrid: true, - drawAxis: true, - independentTicks: true, - ticker: null // will be set in dygraph-tickers.js - }, - y2: { - axisLabelWidth: 50, - pixelsPerLabel: 30, - valueFormatter: Dygraph.numberValueFormatter, - axisLabelFormatter: Dygraph.numberAxisLabelFormatter, - drawAxis: true, // only applies when there are two axes of data. - drawGrid: false, - independentTicks: false, - ticker: null // will be set in dygraph-tickers.js - } - } -}; - -// Directions for panning and zooming. Use bit operations when combined -// values are possible. -Dygraph.HORIZONTAL = 1; -Dygraph.VERTICAL = 2; - -// Installed plugins, in order of precedence (most-general to most-specific). -// Plugins are installed after they are defined, in plugins/install.js. -Dygraph.PLUGINS = [ -]; - // Used for initializing annotation CSS rules only once. Dygraph.addedAnnotationCSS = false; @@ -417,10 +153,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { 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 = ""; @@ -466,11 +198,11 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // 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_ = {}; - Dygraph.update(this.user_attrs_, attrs); + utils.update(this.user_attrs_, attrs); // This sequence ensures that Dygraph.DEFAULT_ATTRS is never modified. this.attrs_ = {}; - Dygraph.updateDeep(this.attrs_, Dygraph.DEFAULT_ATTRS); + utils.updateDeep(this.attrs_, DEFAULT_ATTRS); this.boundaryIds_ = []; this.setIndexByName_ = {}; @@ -560,7 +292,7 @@ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { e.propagationStopped = true; } }; - Dygraph.update(e, extra_props); + utils.update(e, extra_props); var callback_plugin_pairs = this.eventListeners_[name]; if (callback_plugin_pairs) { @@ -596,16 +328,20 @@ Dygraph.prototype.getPluginInstance_ = function(type) { * 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). + * or when the dateWindow or valueRange are updated. Double-clicking or calling + * resetZoom() resets the zoom status for the chart. */ Dygraph.prototype.isZoomed = function(axis) { + const isZoomedX = !!this.dateWindow_; + if (axis === 'x') return isZoomedX; + + const isZoomedY = this.axes_.map(axis => !!axis.valueRange).indexOf(true) >= 0; if (axis === null || axis === undefined) { - return this.zoomed_x_ || this.zoomed_y_; + return isZoomedX || isZoomedY; } - 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'."; + if (axis === 'y') return isZoomedY; + + throw new Error(`axis parameter is [${axis}] must be null, 'x' or 'y'.`); }; /** @@ -629,14 +365,17 @@ Dygraph.prototype.toString = function() { * @return { ... } The value of the option. */ Dygraph.prototype.attr_ = function(name, seriesName) { - if (DEBUG) { - if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') { - console.error('Must include options reference JS for testing'); - } else if (!Dygraph.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. - Dygraph.OPTIONS_REFERENCE[name] = true; + // For "production" code, this gets removed by uglifyjs. + if (typeof(process) !== 'undefined') { + 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); @@ -772,8 +511,7 @@ Dygraph.prototype.xAxisRange = function() { }; /** - * Returns the lower- and upper-bound x-axis values of the - * data set. + * 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; @@ -792,6 +530,22 @@ Dygraph.prototype.xAxisExtremes = function() { }; /** + * Returns the lower- and upper-bound y-axis values for each axis. These are + * the ranges you'll get if you double-click to zoom out or call resetZoom(). + * The return value is an array of [low, high] tuples, one for each y-axis. + */ +Dygraph.prototype.yAxisExtremes = function() { + // TODO(danvk): this is pretty inefficient + const packed = this.gatherDatasets_(this.rolledSeries_, null); + const { extremes } = packed; + const saveAxes = this.axes_; + this.computeYAxisRanges_(extremes); + const newAxes = this.axes_; + this.axes_ = saveAxes; + return newAxes.map(axis => axis.extremeRange); +} + +/** * 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. @@ -894,32 +648,8 @@ Dygraph.prototype.toDataXCoord = function(x) { if (!this.attributes_.getForAxis("logscale", 'x')) { return xRange[0] + (x - area.x) / area.w * (xRange[1] - xRange[0]); } else { - // TODO: remove duplicate code? - // Computing the inverse of toDomCoord. var pct = (x - area.x) / area.w; - - // Computing the inverse of toPercentXCoord. The function was arrived at with - // the following steps: - // - // Original calcuation: - // pct = (log(x) - log(xRange[0])) / (log(xRange[1]) - log(xRange[0]))); - // - // Multiply both sides by the right-side demoninator. - // pct * (log(xRange[1] - log(xRange[0]))) = log(x) - log(xRange[0]) - // - // add log(xRange[0]) to both sides - // log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) = log(x); - // - // Swap both sides of the equation, - // log(x) = log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0])) - // - // Use both sides as the exponent in 10^exp and we're done. - // x = 10 ^ (log(xRange[0]) + (pct * (log(xRange[1]) - log(xRange[0]))) - var logr0 = Dygraph.log10(xRange[0]); - var logr1 = Dygraph.log10(xRange[1]); - var exponent = logr0 + (pct * (logr1 - logr0)); - var value = Math.pow(Dygraph.LOG_SCALE, exponent); - return value; + return utils.logRangeFraction(xRange[0], xRange[1], pct); } }; @@ -943,32 +673,8 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { } else { // Computing the inverse of toDomCoord. var pct = (y - area.y) / area.h; - - // Computing the inverse of toPercentYCoord. The function was arrived at with - // the following steps: - // - // Original calcuation: - // pct = (log(yRange[1]) - log(y)) / (log(yRange[1]) - log(yRange[0])); - // - // Multiply both sides by the right-side demoninator. - // pct * (log(yRange[1]) - log(yRange[0])) = log(yRange[1]) - log(y); - // - // subtract log(yRange[1]) from both sides. - // (pct * (log(yRange[1]) - log(yRange[0]))) - log(yRange[1]) = -log(y); - // - // and multiply both sides by -1. - // log(yRange[1]) - (pct * (logr1 - log(yRange[0])) = log(y); - // - // Swap both sides of the equation, - // log(y) = log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0]))); - // - // Use both sides as the exponent in 10^exp and we're done. - // y = 10 ^ (log(yRange[1]) - (pct * (log(yRange[1]) - log(yRange[0])))); - var logr0 = Dygraph.log10(yRange[0]); - var logr1 = Dygraph.log10(yRange[1]); - var exponent = logr1 - (pct * (logr1 - logr0)); - var value = Math.pow(Dygraph.LOG_SCALE, exponent); - return value; + // Note reversed yRange, y1 is on top with pct==0. + return utils.logRangeFraction(yRange[1], yRange[0], pct); } }; @@ -999,9 +705,9 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { var pct; var logscale = this.attributes_.getForAxis("logscale", axis); if (logscale) { - var logr0 = Dygraph.log10(yRange[0]); - var logr1 = Dygraph.log10(yRange[1]); - pct = (logr1 - Dygraph.log10(y)) / (logr1 - logr0); + 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. @@ -1033,9 +739,9 @@ Dygraph.prototype.toPercentXCoord = function(x) { var pct; var logscale = this.attributes_.getForAxis("logscale", 'x') ; if (logscale === true) { // logscale can be null so we test for true explicitly. - var logr0 = Dygraph.log10(xRange[0]); - var logr1 = Dygraph.log10(xRange[1]); - pct = (Dygraph.log10(x) - logr0) / (logr1 - logr0); + 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. @@ -1098,14 +804,14 @@ Dygraph.prototype.createInterface_ = function() { enclosing.appendChild(this.graphDiv); // Create the canvas for interactive parts of the chart. - this.canvas_ = Dygraph.createCanvas(); + 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_ = Dygraph.getContext(this.canvas_); - this.hidden_ctx_ = Dygraph.getContext(this.hidden_); + this.canvas_ctx_ = utils.getContext(this.canvas_); + this.hidden_ctx_ = utils.getContext(this.hidden_); this.resizeElements_(); @@ -1129,8 +835,8 @@ Dygraph.prototype.createInterface_ = function() { // 2. e.relatedTarget is outside the chart var target = e.target || e.fromElement; var relatedTarget = e.relatedTarget || e.toElement; - if (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) && - !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { + if (utils.isNodeContainedBy(target, dygraph.graphDiv) && + !utils.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { dygraph.mouseOut_(e); } }; @@ -1155,7 +861,9 @@ Dygraph.prototype.resizeElements_ = function() { this.graphDiv.style.width = this.width_ + "px"; this.graphDiv.style.height = this.height_ + "px"; - var canvasScale = Dygraph.getContextPixelRatio(this.canvas_ctx_); + var pixelRatioOption = this.getNumericOption('pixelRatio') + + var canvasScale = pixelRatioOption || 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 @@ -1164,7 +872,7 @@ Dygraph.prototype.resizeElements_ = function() { this.canvas_ctx_.scale(canvasScale, canvasScale); } - var hiddenScale = Dygraph.getContextPixelRatio(this.hidden_ctx_); + var hiddenScale = pixelRatioOption || 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 @@ -1199,11 +907,11 @@ Dygraph.prototype.destroy = function() { this.removeTrackedEvents_(); // remove mouse event handlers (This may not be necessary anymore) - Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_); - Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); + utils.removeEvent(window, 'mouseout', this.mouseOutHandler_); + utils.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); // remove window handlers - Dygraph.removeEvent(window,'resize', this.resizeHandler_); + utils.removeEvent(window,'resize', this.resizeHandler_); this.resizeHandler_ = null; removeRecursive(this.maindiv_); @@ -1230,7 +938,7 @@ Dygraph.prototype.destroy = function() { * @private */ Dygraph.prototype.createPlotKitCanvas_ = function(canvas) { - var h = Dygraph.createCanvas(); + 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 @@ -1286,7 +994,7 @@ Dygraph.prototype.setColors_ = function() { // 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 = Dygraph.hsvToRGB(hue, sat, val); + colorStr = utils.hsvToRGB(hue, sat, val); } } this.colors_.push(colorStr); @@ -1339,32 +1047,28 @@ Dygraph.prototype.getPropertiesForSeries = function(series_name) { */ 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 roller = this.roller_; + if (!roller) { + this.roller_ = roller = document.createElement("input"); + roller.type = "text"; + roller.style.display = "none"; + roller.className = 'dygraph-roller'; + this.graphDiv.appendChild(roller); } var display = this.getBooleanOption('showRoller') ? 'block' : 'none'; - var area = this.plotter_.area; - var textAttr = { "position": "absolute", - "zIndex": 10, + var area = this.getArea(); + var textAttr = { "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]; - } - } + }; + roller.size = "2"; + roller.value = this.rollPeriod_; + utils.update(roller.style, textAttr); - var dygraph = this; - this.roller_.onchange = function() { dygraph.adjustRoll(dygraph.roller_.value); }; + roller.onchange = () => this.adjustRoll(roller.value); }; /** @@ -1412,7 +1116,7 @@ Dygraph.prototype.createDragInterface_ = function() { // We cover iframes during mouse interactions. See comments in // dygraph-utils.js for more info on why this is a good idea. - tarp: new Dygraph.IFrameTarp(), + tarp: new IFrameTarp(), // contextB is the same thing as this context object but renamed. initializeMouseDown: function(event, g, contextB) { @@ -1424,11 +1128,11 @@ Dygraph.prototype.createDragInterface_ = function() { event.cancelBubble = true; } - var canvasPos = Dygraph.findPos(g.canvas_); + var canvasPos = utils.findPos(g.canvas_); contextB.px = canvasPos.x; contextB.py = canvasPos.y; - contextB.dragStartX = Dygraph.dragGetX_(event, contextB); - contextB.dragStartY = Dygraph.dragGetY_(event, contextB); + contextB.dragStartX = utils.dragGetX_(event, contextB); + contextB.dragStartY = utils.dragGetY_(event, contextB); contextB.cancelNextDblclick = false; contextB.tarp.cover(); }, @@ -1490,7 +1194,7 @@ Dygraph.prototype.createDragInterface_ = function() { * dots. * * @param {number} direction the direction of the zoom rectangle. Acceptable - * values are Dygraph.HORIZONTAL and Dygraph.VERTICAL. + * 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. @@ -1511,22 +1215,22 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, var ctx = this.canvas_ctx_; // Clean up from the previous rect if necessary - if (prevDirection == Dygraph.HORIZONTAL) { + 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 == Dygraph.VERTICAL) { + } 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 == Dygraph.HORIZONTAL) { + 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 == Dygraph.VERTICAL) { + } 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), @@ -1578,12 +1282,10 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { // 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()); + const zoomCallback = this.getFunctionOption('zoomCallback'); + this.doAnimatedZoom(old_window, new_window, null, null, () => { + if (zoomCallback) { + zoomCallback.call(this, minDate, maxDate, this.yAxisRanges()); } }); }; @@ -1610,13 +1312,11 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { 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()); + const zoomCallback = this.getFunctionOption('zoomCallback'); + this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, () => { + if (zoomCallback) { + const [minX, maxX] = this.xAxisRange(); + zoomCallback.call(this, minX, maxX, this.yAxisRanges()); } }); }; @@ -1636,88 +1336,57 @@ Dygraph.zoomAnimationFunction = function(frame, numFrames) { * 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; - } - } + const dirtyX = this.isZoomed('x'); + const dirtyY = this.isZoomed('y'); + const dirty = dirtyX || dirtyY; // 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; + if (!dirty) return; - var minDate = this.rawData_[0][0]; - var maxDate = this.rawData_[this.rawData_.length - 1][0]; + // Calculate extremes to avoid lack of padding on reset. + const [minDate, maxDate] = this.xAxisExtremes(); - // With only one frame, don't bother calculating extreme ranges. - // 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; - } + const animatedZooms = this.getBooleanOption('animatedZooms'); + const zoomCallback = this.getFunctionOption('zoomCallback'); - var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null; - if (dirtyX) { - oldWindow = this.xAxisRange(); - newWindow = [minDate, maxDate]; - } + // TODO(danvk): merge this block w/ the code below. + // TODO(danvk): factor out a generic, public zoomTo method. + if (!animatedZooms) { + this.dateWindow_ = null; + this.axes_.forEach(axis => { + if (axis.valueRange) delete axis.valueRange; + }); - if (dirtyY) { - oldValueRanges = this.yAxisRanges(); - // TODO(danvk): this is pretty inefficient - var packed = this.gatherDatasets_(this.rolledSeries_, null); - var extremes = packed.extremes; + this.drawGraph_(); + if (zoomCallback) { + zoomCallback.call(this, minDate, maxDate, this.yAxisRanges()); + } + return; + } - // 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); + var oldWindow=null, newWindow=null, oldValueRanges=null, newValueRanges=null; + if (dirtyX) { + oldWindow = this.xAxisRange(); + newWindow = [minDate, maxDate]; + } - 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); - } - } + if (dirtyY) { + oldValueRanges = this.yAxisRanges(); + newValueRanges = this.yAxisExtremes(); + } - 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()); - } + this.doAnimatedZoom(oldWindow, newWindow, oldValueRanges, newValueRanges, + () => { + this.dateWindow_ = null; + this.axes_.forEach(axis => { + if (axis.valueRange) delete axis.valueRange; }); - } + if (zoomCallback) { + zoomCallback.call(this, minDate, maxDate, this.yAxisRanges()); + } + }); }; /** @@ -1753,18 +1422,17 @@ Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, ne } } - var that = this; - Dygraph.repeatAndCleanup(function(step) { + utils.repeatAndCleanup(step => { if (valueRanges.length) { - for (var i = 0; i < that.axes_.length; i++) { + for (var i = 0; i < this.axes_.length; i++) { var w = valueRanges[step][i]; - that.axes_[i].valueWindow = [w[0], w[1]]; + this.axes_[i].valueRange = [w[0], w[1]]; } } if (windows.length) { - that.dateWindow_ = windows[step]; + this.dateWindow_ = windows[step]; } - that.drawGraph_(); + this.drawGraph_(); }, steps, Dygraph.ANIMATION_DURATION / steps, callback); }; @@ -1786,9 +1454,9 @@ Dygraph.prototype.eventToDomCoords = function(event) { if (event.offsetX && event.offsetY) { return [ event.offsetX, event.offsetY ]; } else { - var eventElementPos = Dygraph.findPos(this.mouseEventElement_); - var canvasx = Dygraph.pageX(event) - eventElementPos.x; - var canvasy = Dygraph.pageY(event) - eventElementPos.y; + var eventElementPos = utils.findPos(this.mouseEventElement_); + var canvasx = utils.pageX(event) - eventElementPos.x; + var canvasy = utils.pageY(event) - eventElementPos.y; return [canvasx, canvasy]; } }; @@ -1808,7 +1476,7 @@ Dygraph.prototype.findClosestRow = function(domX) { var len = points.length; for (var j = 0; j < len; j++) { var point = points[j]; - if (!Dygraph.isValidPoint(point, true)) continue; + if (!utils.isValidPoint(point, true)) continue; var dist = Math.abs(point.canvasx - domX); if (dist < minDistX) { minDistX = dist; @@ -1839,7 +1507,7 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { var points = this.layout_.points[setIdx]; for (var i = 0; i < points.length; ++i) { point = points[i]; - if (!Dygraph.isValidPoint(point)) continue; + if (!utils.isValidPoint(point)) continue; dx = point.canvasx - domX; dy = point.canvasy - domY; dist = dx * dx + dy * dy; @@ -1880,12 +1548,12 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { var points = this.layout_.points[setIdx]; if (rowIdx >= points.length) continue; var p1 = points[rowIdx]; - if (!Dygraph.isValidPoint(p1)) continue; + if (!utils.isValidPoint(p1)) continue; var py = p1.canvasy; if (domX > p1.canvasx && rowIdx + 1 < points.length) { // interpolate series Y value using next point var p2 = points[rowIdx + 1]; - if (Dygraph.isValidPoint(p2)) { + if (utils.isValidPoint(p2)) { var dx = p2.canvasx - p1.canvasx; if (dx > 0) { var r = (domX - p1.canvasx) / dx; @@ -1895,7 +1563,7 @@ Dygraph.prototype.findStackedPoint = function(domX, domY) { } else if (domX < p1.canvasx && rowIdx > 0) { // interpolate series Y value using previous point var p0 = points[rowIdx - 1]; - if (Dygraph.isValidPoint(p0)) { + if (utils.isValidPoint(p0)) { var dx = p1.canvasx - p0.canvasx; if (dx > 0) { var r = (p1.canvasx - domX) / dx; @@ -2000,7 +1668,7 @@ Dygraph.prototype.animateSelection_ = function(direction) { that.clearSelection(); } }; - Dygraph.repeatAndCleanup( + utils.repeatAndCleanup( function(n) { // ignore simultaneous animations if (that.animateId != thisId) return; @@ -2023,8 +1691,8 @@ Dygraph.prototype.animateSelection_ = function(direction) { Dygraph.prototype.updateSelection_ = function(opt_animFraction) { /*var defaultPrevented = */ this.cascadeEvents_('select', { - selectedRow: this.lastRow_, - selectedX: this.lastx_, + selectedRow: this.lastRow_ === -1 ? undefined : this.lastRow_, + selectedX: this.lastx_ === -1 ? undefined : this.lastx_, selectedPoints: this.selPoints_ }); // TODO(danvk): use defaultPrevented here? @@ -2035,6 +1703,8 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { if (this.getOption('highlightSeriesOpts')) { ctx.clearRect(0, 0, this.width_, this.height_); var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha'); + var backgroundColor = utils.toRGB_(this.getOption('highlightSeriesBackgroundColor')); + if (alpha) { // Activating background fade includes an animation effect for a gradual // fade. TODO(klausw): make this independently configurable if it causes @@ -2048,7 +1718,7 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { } alpha *= opt_animFraction; } - ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')'; + ctx.fillStyle = 'rgba(' + backgroundColor.r + ',' + backgroundColor.g + ',' + backgroundColor.b + ',' + alpha + ')'; ctx.fillRect(0, 0, this.width_, this.height_); } @@ -2074,13 +1744,13 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { ctx.save(); for (i = 0; i < this.selPoints_.length; i++) { var pt = this.selPoints_[i]; - if (!Dygraph.isOK(pt.canvasy)) continue; + if (isNaN(pt.canvasy)) continue; var circleSize = this.getNumericOption('highlightCircleSize', pt.name); var callback = this.getFunctionOption("drawHighlightPointCallback", pt.name); var color = this.plotter_.colors[pt.name]; if (!callback) { - callback = Dygraph.Circles.DEFAULT; + callback = utils.Circles.DEFAULT; } ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name); ctx.strokeStyle = color; @@ -2098,6 +1768,10 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { * Manually set the selected points and display information about them in the * legend. The selection can be cleared using clearSelection() and queried * using getSelection(). + * + * To set a selected series but not a selected point, call setSelection with + * row=false and the selected series name. + * * @param {number} row Row number that should be highlighted (i.e. appear with * hover dots on the chart). * @param {seriesName} optional series name to highlight that series with the @@ -2120,7 +1794,7 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { // for. If it is, just use it, otherwise search the array for a point // in the proper place. var setRow = row - this.getLeftBoundary_(setIdx); - if (setRow < points.length && points[setRow].idx == row) { + if (setRow >= 0 && setRow < points.length && points[setRow].idx == row) { var point = points[setRow]; if (point.yval !== null) this.selPoints_.push(point); } else { @@ -2280,16 +1954,16 @@ Dygraph.prototype.getHandlerClass_ = function() { handlerClass = this.attr_('dataHandler'); } else if (this.fractions_) { if (this.getBooleanOption('errorBars')) { - handlerClass = Dygraph.DataHandlers.FractionsBarsHandler; + handlerClass = FractionsBarsHandler; } else { - handlerClass = Dygraph.DataHandlers.DefaultFractionHandler; + handlerClass = DefaultFractionHandler; } } else if (this.getBooleanOption('customBars')) { - handlerClass = Dygraph.DataHandlers.CustomBarsHandler; + handlerClass = CustomBarsHandler; } else if (this.getBooleanOption('errorBars')) { - handlerClass = Dygraph.DataHandlers.ErrorBarsHandler; + handlerClass = ErrorBarsHandler; } else { - handlerClass = Dygraph.DataHandlers.DefaultHandler; + handlerClass = DefaultHandler; } return handlerClass; }; @@ -2611,26 +2285,22 @@ Dygraph.prototype.drawGraph_ = function() { this.setIndexByName_ = {}; var labels = this.attr_("labels"); - if (labels.length > 0) { - this.setIndexByName_[labels[0]] = 0; - } var dataIdx = 0; for (var i = 1; i < points.length; i++) { - this.setIndexByName_[labels[i]] = i; if (!this.visibility()[i - 1]) continue; this.layout_.addDataset(labels[i], points[i]); this.datasetIndex_[i] = dataIdx++; } + for (var i = 0; i < labels.length; i++) { + this.setIndexByName_[labels[i]] = i; + } this.computeYAxisRanges_(extremes); this.layout_.setYAxes(this.axes_); this.addXTicks_(); - // Save the X axis zoomed status as the updateOptions call will tend to set it erroneously - var tmp_zoomed_x = this.zoomed_x_; // Tell PlotKit to use this new data and render itself - this.zoomed_x_ = tmp_zoomed_x; this.layout_.evaluate(); this.renderGraph_(is_initial_draw); @@ -2650,10 +2320,11 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { this.cascadeEvents_('clearChart'); this.plotter_.clear(); - if (this.getFunctionOption('underlayCallback')) { + const underlayCallback = this.getFunctionOption('underlayCallback'); + if (underlayCallback) { // NOTE: we pass the dygraph object to this callback twice to avoid breaking // users who expect a deprecated form of this callback. - this.getFunctionOption('underlayCallback').call(this, + underlayCallback.call(this, this.hidden_ctx_, this.layout_.getPlotArea(), this, this); } @@ -2670,8 +2341,9 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { // The interaction canvas should already be empty in that situation. this.canvas_.getContext('2d').clearRect(0, 0, this.width_, this.height_); - if (this.getFunctionOption("drawCallback") !== null) { - this.getFunctionOption("drawCallback").call(this, this, is_initial_draw); + const drawCallback = this.getFunctionOption("drawCallback"); + if (drawCallback !== null) { + drawCallback.call(this, this, is_initial_draw); } if (is_initial_draw) { this.readyFired_ = true; @@ -2693,15 +2365,7 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { * indices are into the axes_ array. */ Dygraph.prototype.computeYAxes_ = function() { - // Preserve valueWindow settings if they exist, and if the user hasn't - // specified a new valueRange. - var valueWindows, axis, index, opts, v; - if (this.axes_ !== undefined && this.user_attrs_.hasOwnProperty("valueRange") === false) { - valueWindows = []; - for (index = 0; index < this.axes_.length; index++) { - valueWindows.push(this.axes_[index].valueWindow); - } - } + var axis, index, opts, v; // this.axes_ doesn't match this.attributes_.axes_.options. It's used for // data computation as well as options storage. @@ -2711,30 +2375,10 @@ Dygraph.prototype.computeYAxes_ = function() { for (axis = 0; axis < this.attributes_.numAxes(); axis++) { // Add a new axis, making a copy of its per-axis options. opts = { g : this }; - Dygraph.update(opts, this.attributes_.axisOptions(axis)); + utils.update(opts, this.attributes_.axisOptions(axis)); this.axes_[axis] = opts; } - - // Copy global valueRange option over to the first axis. - // NOTE(konigsberg): Are these two statements necessary? - // I tried removing it. The automated tests pass, and manually - // messing with tests/zoom.html showed no trouble. - v = this.attr_('valueRange'); - if (v) this.axes_[0].valueRange = v; - - if (valueWindows !== undefined) { - // Restore valueWindow settings. - - // When going from two axes back to one, we only restore - // one axis. - var idxCount = Math.min(valueWindows.length, this.axes_.length); - - for (index = 0; index < idxCount; index++) { - this.axes_[index].valueWindow = valueWindows[index]; - } - } - for (axis = 0; axis < this.axes_.length; axis++) { if (axis === 0) { opts = this.optionsViewForAxis_('y' + (axis ? '2' : '')); @@ -2797,9 +2441,8 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // // - backwards compatible (yRangePad not set): // 10% padding for automatic Y ranges, but not for user-supplied - // ranges, and move a close-to-zero edge to zero except if - // avoidMinZero is set, since drawing at the edge results in - // invisible lines. Unfortunately lines drawn at the edge of a + // ranges, and move a close-to-zero edge to zero, since drawing at the edge + // results in invisible lines. Unfortunately lines drawn at the edge of a // user-supplied range will still be invisible. If logscale is // set, add a variable amount of padding at the top but // none at the bottom. @@ -2809,10 +2452,11 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // ypadCompat = true; ypad = 0.1; // add 10% - if (this.getNumericOption('yRangePad') !== null) { + const yRangePad = this.getNumericOption('yRangePad'); + if (yRangePad !== null) { ypadCompat = false; // Convert pixel padding to ratio - ypad = this.getNumericOption('yRangePad') / this.plotter_.area.h; + ypad = yRangePad / this.plotter_.area.h; } if (series.length === 0) { @@ -2861,53 +2505,49 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { } } - var maxAxisY, minAxisY; - if (logscale) { - if (ypadCompat) { + var maxAxisY = maxY, minAxisY = minY; + if (ypadCompat) { + if (logscale) { maxAxisY = maxY + ypad * span; minAxisY = minY; } else { - var logpad = Math.exp(Math.log(span) * ypad); - maxAxisY = maxY * logpad; - minAxisY = minY / logpad; - } - } else { - maxAxisY = maxY + ypad * span; - minAxisY = minY - ypad * span; + maxAxisY = maxY + ypad * span; + minAxisY = minY - ypad * span; - // Backwards-compatible behavior: Move the span to start or end at zero if it's - // close to zero, but not if avoidMinZero is set. - if (ypadCompat && !this.getBooleanOption("avoidMinZero")) { + // Backwards-compatible behavior: Move the span to start or end at zero if it's + // close to zero. if (minAxisY < 0 && minY >= 0) minAxisY = 0; if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; } } axis.extremeRange = [minAxisY, maxAxisY]; } - if (axis.valueWindow) { - // This is only set if the user has zoomed on the y-axis. It is never set - // by a user. It takes precedence over axis.valueRange because, if you set - // valueRange, you'd still expect to be able to pan. - axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; - } else if (axis.valueRange) { + if (axis.valueRange) { // This is a user-set value range for this axis. var y0 = isNullUndefinedOrNaN(axis.valueRange[0]) ? axis.extremeRange[0] : axis.valueRange[0]; var y1 = isNullUndefinedOrNaN(axis.valueRange[1]) ? axis.extremeRange[1] : axis.valueRange[1]; - if (!ypadCompat) { - if (axis.logscale) { - var logpad = Math.exp(Math.log(span) * ypad); - y0 *= logpad; - y1 /= logpad; - } else { - span = y1 - y0; - y0 -= span * ypad; - y1 += span * ypad; - } - } axis.computedValueRange = [y0, y1]; } else { axis.computedValueRange = axis.extremeRange; } + if (!ypadCompat) { + // When using yRangePad, adjust the upper/lower bounds to add + // padding unless the user has zoomed/panned the Y axis range. + if (logscale) { + y0 = axis.computedValueRange[0]; + y1 = axis.computedValueRange[1]; + var y0pct = ypad / (2 * ypad - 1); + var y1pct = (ypad - 1) / (2 * ypad - 1); + axis.computedValueRange[0] = utils.logRangeFraction(y0, y1, y0pct); + axis.computedValueRange[1] = utils.logRangeFraction(y0, y1, y1pct); + } else { + y0 = axis.computedValueRange[0]; + y1 = axis.computedValueRange[1]; + span = y1 - y0; + axis.computedValueRange[0] = y0 - span * ypad; + axis.computedValueRange[1] = y1 + span * ypad; + } + } if (independentTicks) { @@ -2978,17 +2618,17 @@ Dygraph.prototype.detectTypeFromString_ = function(str) { Dygraph.prototype.setXAxisOptions_ = function(isDate) { if (isDate) { - this.attrs_.xValueParser = Dygraph.dateParser; - this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter; - this.attrs_.axes.x.ticker = Dygraph.dateTicker; - this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter; + this.attrs_.xValueParser = utils.dateParser; + this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter; + this.attrs_.axes.x.ticker = DygraphTickers.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter; } else { /** @private (shut up, jsdoc!) */ this.attrs_.xValueParser = function(x) { return parseFloat(x); }; // TODO(danvk): use Dygraph.numberValueFormatter here? /** @private (shut up, jsdoc!) */ this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = DygraphTickers.numericTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } }; @@ -3012,7 +2652,7 @@ Dygraph.prototype.setXAxisOptions_ = function(isDate) { */ Dygraph.prototype.parseCSV_ = function(data) { var ret = []; - var line_delimiter = Dygraph.detectLineDelimiter(data); + var line_delimiter = utils.detectLineDelimiter(data); var lines = data.split(line_delimiter || "\n"); var vals, j; @@ -3062,8 +2702,8 @@ Dygraph.prototype.parseCSV_ = function(data) { (1 + i) + " ('" + line + "') which is not of this form."); fields[j] = [0, 0]; } else { - fields[j] = [Dygraph.parseFloat_(vals[0], i, line), - Dygraph.parseFloat_(vals[1], i, line)]; + fields[j] = [utils.parseFloat_(vals[0], i, line), + utils.parseFloat_(vals[1], i, line)]; } } } else if (this.getBooleanOption("errorBars")) { @@ -3074,8 +2714,8 @@ Dygraph.prototype.parseCSV_ = function(data) { (inFields.length - 1) + "): '" + line + "'"); } for (j = 1; j < inFields.length; j += 2) { - fields[(j + 1) / 2] = [Dygraph.parseFloat_(inFields[j], i, line), - Dygraph.parseFloat_(inFields[j + 1], i, line)]; + fields[(j + 1) / 2] = [utils.parseFloat_(inFields[j], i, line), + utils.parseFloat_(inFields[j + 1], i, line)]; } } else if (this.getBooleanOption("customBars")) { // Bars are a low;center;high tuple @@ -3086,9 +2726,9 @@ Dygraph.prototype.parseCSV_ = function(data) { } else { vals = val.split(";"); if (vals.length == 3) { - fields[j] = [ Dygraph.parseFloat_(vals[0], i, line), - Dygraph.parseFloat_(vals[1], i, line), - Dygraph.parseFloat_(vals[2], i, line) ]; + fields[j] = [ utils.parseFloat_(vals[0], i, line), + utils.parseFloat_(vals[1], i, line), + utils.parseFloat_(vals[2], i, line) ]; } else { console.warn('When using customBars, values must be either blank ' + 'or "low;center;high" tuples (got "' + val + @@ -3099,7 +2739,7 @@ Dygraph.prototype.parseCSV_ = function(data) { } else { // Values are just numbers for (j = 1; j < inFields.length; j++) { - fields[j] = Dygraph.parseFloat_(inFields[j], i, line); + fields[j] = utils.parseFloat_(inFields[j], i, line); } } if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) { @@ -3140,6 +2780,23 @@ Dygraph.prototype.parseCSV_ = function(data) { return ret; }; +// In native format, all values must be dates or numbers. +// This check isn't perfect but will catch most mistaken uses of strings. +function validateNativeFormat(data) { + const firstRow = data[0]; + const firstX = firstRow[0]; + if (typeof firstX !== 'number' && !utils.isDateLike(firstX)) { + throw new Error(`Expected number or date but got ${typeof firstX}: ${firstX}.`); + } + for (let i = 1; i < firstRow.length; i++) { + const val = firstRow[i]; + if (val === null || val === undefined) continue; + if (typeof val === 'number') continue; + if (utils.isArrayLike(val)) continue; // e.g. error bars or custom bars. + throw new Error(`Expected number or array but got ${typeof val}: ${val}.`); + } +} + /** * The user has provided their data as a pre-packaged JS array. If the x values * are numeric, this is the same as dygraphs' internal format. If the x values @@ -3159,6 +2816,8 @@ Dygraph.prototype.parseArray_ = function(data) { return null; } + validateNativeFormat(data); + var i; if (this.attr_("labels") === null) { console.warn("Using default labels. Set labels explicitly via 'labels' " + @@ -3177,14 +2836,14 @@ Dygraph.prototype.parseArray_ = function(data) { } } - if (Dygraph.isDateLike(data[0][0])) { + if (utils.isDateLike(data[0][0])) { // Some intelligent defaults for a date x-axis. - this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter; - this.attrs_.axes.x.ticker = Dygraph.dateTicker; - this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter; + this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter; + this.attrs_.axes.x.ticker = DygraphTickers.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter; // Assume they're all dates. - var parsedData = Dygraph.clone(data); + var parsedData = utils.clone(data); for (i = 0; i < data.length; i++) { if (parsedData[i].length === 0) { console.error("Row " + (1 + i) + " of data is empty"); @@ -3203,8 +2862,8 @@ Dygraph.prototype.parseArray_ = function(data) { // Some intelligent defaults for a numeric x-axis. /** @private (shut up, jsdoc!) */ this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; - this.attrs_.axes.x.axisLabelFormatter = Dygraph.numberAxisLabelFormatter; + this.attrs_.axes.x.ticker = DygraphTickers.numericTicks; + this.attrs_.axes.x.axisLabelFormatter = utils.numberAxisLabelFormatter; return data; } }; @@ -3237,14 +2896,14 @@ Dygraph.prototype.parseDataTable_ = function(data) { var indepType = data.getColumnType(0); if (indepType == 'date' || indepType == 'datetime') { - this.attrs_.xValueParser = Dygraph.dateParser; - this.attrs_.axes.x.valueFormatter = Dygraph.dateValueFormatter; - this.attrs_.axes.x.ticker = Dygraph.dateTicker; - this.attrs_.axes.x.axisLabelFormatter = Dygraph.dateAxisLabelFormatter; + this.attrs_.xValueParser = utils.dateParser; + this.attrs_.axes.x.valueFormatter = utils.dateValueFormatter; + this.attrs_.axes.x.ticker = DygraphTickers.dateTicker; + this.attrs_.axes.x.axisLabelFormatter = utils.dateAxisLabelFormatter; } else if (indepType == 'number') { this.attrs_.xValueParser = function(x) { return parseFloat(x); }; this.attrs_.axes.x.valueFormatter = function(x) { return x; }; - this.attrs_.axes.x.ticker = Dygraph.numericTicks; + this.attrs_.axes.x.ticker = DygraphTickers.numericTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } else { throw new Error( @@ -3354,6 +3013,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { /** * Signals to plugins that the chart data has updated. * This happens after the data has updated but before the chart has redrawn. + * @private */ Dygraph.prototype.cascadeDataDidUpdateEvent_ = function() { // TODO(danvk): there are some issues checking xAxisRange() and using @@ -3375,7 +3035,7 @@ Dygraph.prototype.start_ = function() { data = data(); } - if (Dygraph.isArrayLike(data)) { + if (utils.isArrayLike(data)) { this.rawData_ = this.parseArray_(data); this.cascadeDataDidUpdateEvent_(); this.predraw_(); @@ -3387,7 +3047,7 @@ Dygraph.prototype.start_ = function() { this.predraw_(); } else if (typeof data == 'string') { // Heuristic: a newline means it's CSV data. Otherwise it's an URL. - var line_delimiter = Dygraph.detectLineDelimiter(data); + var line_delimiter = utils.detectLineDelimiter(data); if (line_delimiter) { this.loadedEvent_(data); } else { @@ -3450,12 +3110,6 @@ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { } if ('dateWindow' in attrs) { this.dateWindow_ = attrs.dateWindow; - if (!('isZoomedIgnoreProgrammaticZoom' in attrs)) { - this.zoomed_x_ = (attrs.dateWindow !== null); - } - } - if ('valueRange' in attrs && !('isZoomedIgnoreProgrammaticZoom' in attrs)) { - this.zoomed_y_ = (attrs.valueRange !== null); } // TODO(danvk): validate per-series options. @@ -3466,15 +3120,15 @@ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { // highlightCircleSize // Check if this set options will require new points. - var requiresNewPoints = Dygraph.isPixelChangingOptionList(this.attr_("labels"), attrs); + var requiresNewPoints = utils.isPixelChangingOptionList(this.attr_("labels"), attrs); - Dygraph.updateDeep(this.user_attrs_, attrs); + utils.updateDeep(this.user_attrs_, attrs); this.attributes_.reparseSeries(); if (file) { // This event indicates that the data is about to change, but hasn't yet. - // TODO(danvk): support cancelation of the update via this event. + // TODO(danvk): support cancellation of the update via this event. this.cascadeEvents_('dataWillUpdate', {}); this.file_ = file; @@ -3492,6 +3146,7 @@ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { /** * Make a copy of input attributes, removing file as a convenience. + * @private */ Dygraph.copyUserAttrs_ = function(attrs) { var my_attrs = {}; @@ -3580,7 +3235,7 @@ Dygraph.prototype.visibility = function() { * * @param {number|number[]|object} num the series index or an array of series indices * or a boolean array of visibility states by index - * or an object mapping series numbers, as keys, to + * or an object mapping series numbers, as keys, to * visibility state (boolean values) * @param {boolean} value the visibility state expressed as a boolean */ @@ -3645,7 +3300,6 @@ Dygraph.prototype.size = function() { */ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { // Only add the annotation CSS rule once we know it will be used. - Dygraph.addAnnotationRule(); this.annotations_ = ann; if (!this.layout_) { console.warn("Tried to setAnnotations before dygraph was ready. " + @@ -3736,51 +3390,87 @@ Dygraph.prototype.ready = function(callback) { }; /** + * Add an event handler. This event handler is kept until the graph is + * destroyed with a call to graph.destroy(). + * + * @param {!Node} elem The element to add the event to. + * @param {string} type The type of the event, e.g. 'click' or 'mousemove'. + * @param {function(Event):(boolean|undefined)} fn The function to call + * on the event. The function takes one parameter: the event object. * @private - * Adds a default style for the annotation CSS classes to the document. This is - * only executed when annotations are actually used. It is designed to only be - * called once -- all calls after the first will return immediately. - */ -Dygraph.addAnnotationRule = function() { - // TODO(danvk): move this function into plugins/annotations.js? - if (Dygraph.addedAnnotationCSS) return; - - var rule = "border: 1px solid black; " + - "background-color: white; " + - "text-align: center;"; - - var styleSheetElement = document.createElement("style"); - styleSheetElement.type = "text/css"; - document.getElementsByTagName("head")[0].appendChild(styleSheetElement); - - // Find the first style sheet that we can access. - // We may not add a rule to a style sheet from another domain for security - // reasons. This sometimes comes up when using gviz, since the Google gviz JS - // adds its own style sheets from google.com. - for (var i = 0; i < document.styleSheets.length; i++) { - if (document.styleSheets[i].disabled) continue; - var mysheet = document.styleSheets[i]; - try { - if (mysheet.insertRule) { // Firefox - var idx = mysheet.cssRules ? mysheet.cssRules.length : 0; - mysheet.insertRule(".dygraphDefaultAnnotation { " + rule + " }", idx); - } else if (mysheet.addRule) { // IE - mysheet.addRule(".dygraphDefaultAnnotation", rule); - } - Dygraph.addedAnnotationCSS = true; - return; - } catch(err) { - // Was likely a security exception. + */ +Dygraph.prototype.addAndTrackEvent = function(elem, type, fn) { + utils.addEvent(elem, type, fn); + this.registeredEvents_.push({elem, type, fn}); +}; + +Dygraph.prototype.removeTrackedEvents_ = function() { + if (this.registeredEvents_) { + for (var idx = 0; idx < this.registeredEvents_.length; idx++) { + var reg = this.registeredEvents_[idx]; + utils.removeEvent(reg.elem, reg.type, reg.fn); } } - console.warn("Unable to add default annotation CSS rule; display may be off."); + this.registeredEvents_ = []; }; -if (typeof exports === "object" && typeof module !== "undefined") { - module.exports = Dygraph; -} -return Dygraph; +// Installed plugins, in order of precedence (most-general to most-specific). +Dygraph.PLUGINS = [ + LegendPlugin, + AxesPlugin, + RangeSelectorPlugin, // Has to be before ChartLabels so that its callbacks are called after ChartLabels' callbacks. + ChartLabelsPlugin, + AnnotationsPlugin, + GridPlugin +]; -})(); +// There are many symbols which have historically been available through the +// Dygraph class. These are exported here for backwards compatibility. +Dygraph.GVizChart = GVizChart; +Dygraph.DASHED_LINE = utils.DASHED_LINE; +Dygraph.DOT_DASH_LINE = utils.DOT_DASH_LINE; +Dygraph.dateAxisLabelFormatter = utils.dateAxisLabelFormatter; +Dygraph.toRGB_ = utils.toRGB_; +Dygraph.findPos = utils.findPos; +Dygraph.pageX = utils.pageX; +Dygraph.pageY = utils.pageY; +Dygraph.dateString_ = utils.dateString_; +Dygraph.defaultInteractionModel = DygraphInteraction.defaultModel; +Dygraph.nonInteractiveModel = Dygraph.nonInteractiveModel_ = DygraphInteraction.nonInteractiveModel_; +Dygraph.Circles = utils.Circles; + +Dygraph.Plugins = { + Legend: LegendPlugin, + Axes: AxesPlugin, + Annotations: AnnotationsPlugin, + ChartLabels: ChartLabelsPlugin, + Grid: GridPlugin, + RangeSelector: RangeSelectorPlugin +}; + +Dygraph.DataHandlers = { + DefaultHandler, + BarsHandler, + CustomBarsHandler, + DefaultFractionHandler, + ErrorBarsHandler, + FractionsBarsHandler +}; + +Dygraph.startPan = DygraphInteraction.startPan; +Dygraph.startZoom = DygraphInteraction.startZoom; +Dygraph.movePan = DygraphInteraction.movePan; +Dygraph.moveZoom = DygraphInteraction.moveZoom; +Dygraph.endPan = DygraphInteraction.endPan; +Dygraph.endZoom = DygraphInteraction.endZoom; + +Dygraph.numericLinearTicks = DygraphTickers.numericLinearTicks; +Dygraph.numericTicks = DygraphTickers.numericTicks; +Dygraph.dateTicker = DygraphTickers.dateTicker; +Dygraph.Granularity = DygraphTickers.Granularity; +Dygraph.getDateAxis = DygraphTickers.getDateAxis; +Dygraph.floatFormat = utils.floatFormat; + +export default Dygraph;