X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=d95f9be544626dc3acc9f1007a9857c5a7f098ff;hb=bf5d26a5d349331ab3beba79702692711e9de17c;hp=2bccd60a7170937193d52c16485ef8c07a928315;hpb=7c5c5ece906e40e036e401ab92296c18f5c32f3a;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 2bccd60..d95f9be 100644 --- a/dygraph.js +++ b/dygraph.js @@ -44,7 +44,7 @@ */ /*jshint globalstrict: true */ -/*global DygraphRangeSelector:false, DygraphLayout:false, DygraphCanvasRenderer:false, G_vmlCanvasManager:false */ +/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */ "use strict"; /** @@ -62,11 +62,17 @@ * options, see http://dygraphs.com/options.html. */ var Dygraph = function(div, data, opts, opt_fourth_param) { + // These have to go above the "Hack for IE" in __init__ since .ready() can be + // called as soon as the constructor returns. Once support for OldIE is + // dropped, this can go down with the rest of the initializers. + this.is_initial_draw_ = true; + this.readyFns_ = []; + if (opt_fourth_param !== undefined) { // Old versions of dygraphs took in the series labels as a constructor // parameter. This doesn't make sense anymore, but it's easy to continue // to support this usage. - this.warn("Using deprecated four-argument dygraph constructor"); + Dygraph.warn("Using deprecated four-argument dygraph constructor"); this.__old_init__(div, data, opts, opt_fourth_param); } else { this.__init__(div, data, opts); @@ -74,16 +80,16 @@ var Dygraph = function(div, data, opts, opt_fourth_param) { }; Dygraph.NAME = "Dygraph"; -Dygraph.VERSION = "1.2"; +Dygraph.VERSION = "1.0.1"; Dygraph.__repr__ = function() { - return "[" + this.NAME + " " + this.VERSION + "]"; + return "[" + Dygraph.NAME + " " + Dygraph.VERSION + "]"; }; /** * Returns information about the Dygraph class. */ Dygraph.toString = function() { - return this.__repr__(); + return Dygraph.__repr__(); }; // Various default values @@ -95,14 +101,20 @@ 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 {number} x The number to be formatted * @param {Dygraph} opts An options view - * @param {String} name The name of the point's data series + * @param {string} name The name of the point's data series * @param {Dygraph} g The dygraph object */ Dygraph.numberValueFormatter = function(x, opts, pt, g) { @@ -116,14 +128,60 @@ Dygraph.numberValueFormatter = function(x, opts, pt, g) { 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))) { - return x.toExponential(digits); + label = x.toExponential(digits); } else { - return '' + Dygraph.round_(x, digits); + 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) Dygraph.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; }; /** @@ -135,46 +193,32 @@ Dygraph.numberAxisLabelFormatter = function(x, granularity, opts, g) { }; /** - * Convert a JS date (millis since epoch) to YYYY/MM/DD - * @param {Number} date The JavaScript date (ms since epoch) - * @return {String} A date of the form "YYYY/MM/DD" + * @type {!Array.} * @private + * @constant */ -Dygraph.dateString_ = function(date) { - var zeropad = Dygraph.zeropad; - var d = new Date(date); - - // Get the year: - var year = "" + d.getFullYear(); - // Get a 0 padded month string - var month = zeropad(d.getMonth() + 1); //months are 0-offset, sigh - // Get a 0 padded day string - var day = zeropad(d.getDate()); +Dygraph.SHORT_MONTH_NAMES_ = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - var ret = ""; - var frac = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds(); - if (frac) ret = " " + Dygraph.hmsString_(date); - - return year + "/" + month + "/" + day + ret; -}; /** * Convert a JS date to a string appropriate to display on an axis that * is displaying values at the stated granularity. * @param {Date} date The date to format - * @param {Number} granularity One of the Dygraph granularity constants - * @return {String} The formatted date + * @param {number} granularity One of the Dygraph granularity constants + * @return {string} The formatted date * @private */ Dygraph.dateAxisFormatter = function(date, granularity) { if (granularity >= Dygraph.DECADAL) { - return date.strftime('%Y'); + return '' + date.getFullYear(); } else if (granularity >= Dygraph.MONTHLY) { - return date.strftime('%b %y'); + return Dygraph.SHORT_MONTH_NAMES_[date.getMonth()] + ' ' + date.getFullYear(); } else { var frac = date.getHours() * 3600 + date.getMinutes() * 60 + date.getSeconds() + date.getMilliseconds(); if (frac === 0 || granularity >= Dygraph.DAILY) { - return new Date(date.getTime() + 3600*1000).strftime('%d%b'); + // e.g. '21Jan' (%d%b) + var nd = new Date(date.getTime() + 3600*1000); + return Dygraph.zeropad(nd.getDate()) + Dygraph.SHORT_MONTH_NAMES_[nd.getMonth()]; } else { return Dygraph.hmsString_(date.getTime()); } @@ -239,13 +283,17 @@ Dygraph.DEFAULT_ATTRS = { connectSeparatedPoints: false, stackedGraph: false, + stackedGraphNaNFill: 'all', hideOverlayOnMouseOut: true, // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms. legend: 'onmouseover', // the only relevant value at the moment is 'always'. + legendFollow: false, stepPlot: false, avoidMinZero: false, + xRangePad: 0, + yRangePad: null, drawAxesAtZero: false, // Sizes of the various chart labels. @@ -282,24 +330,35 @@ Dygraph.DEFAULT_ATTRS = { Dygraph.Plotters.linePlotter ], + plugins: [ ], + // per-axis options axes: { x: { pixelsPerLabel: 60, axisLabelFormatter: Dygraph.dateAxisFormatter, valueFormatter: Dygraph.dateString_, + drawGrid: true, + drawAxis: true, + independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, y: { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawGrid: true, + drawAxis: true, + independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, y2: { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawAxis: false, + drawGrid: false, + independentTicks: false, ticker: null // will be set in dygraph-tickers.js } } @@ -335,7 +394,7 @@ Dygraph.prototype.__old_init__ = function(div, file, labels, attrs) { * and context <canvas> 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 {string | Function} file Source data * @param {Object} attrs Miscellaneous other options * @private */ @@ -376,7 +435,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { this.fractions_ = attrs.fractions || false; this.dateWindow_ = attrs.dateWindow || null; - this.is_initial_draw_ = true; this.annotations_ = []; // Zoomed indicators - These indicate when the graph has been zoomed and on what axis. @@ -403,9 +461,11 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { div.style.width = Dygraph.DEFAULT_WIDTH + "px"; } } - // these will be zero if the dygraph's div is hidden. - this.width_ = div.clientWidth; - this.height_ = div.clientHeight; + // 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) { @@ -413,12 +473,6 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // TODO(nikhilk): Add any other stackedGraph checks here. } - // These two options have a bad interaction. See issue 359. - if (attrs.showRangeSelector && attrs.animatedZooms) { - this.warn('You should not set animatedZooms=true when using the range selector.'); - attrs.animatedZooms = false; - } - // DEPRECATION WARNING: All option processing should be moved from // attrs_ and user_attrs_ to options_, which holds all this information. // @@ -452,8 +506,9 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { // Activate plugins. this.plugins_ = []; - for (var i = 0; i < Dygraph.PLUGINS.length; i++) { - var Plugin = Dygraph.PLUGINS[i]; + var plugins = Dygraph.PLUGINS.concat(this.getOption('plugins')); + for (var i = 0; i < plugins.length; i++) { + var Plugin = plugins[i]; var pluginInstance = new Plugin(); var pluginDict = { plugin: pluginInstance, @@ -488,6 +543,8 @@ Dygraph.prototype.__init__ = function(div, file, attrs) { } } + this.createDragInterface_(); + this.start_(); }; @@ -529,6 +586,22 @@ Dygraph.prototype.cascadeEvents_ = function(name, extra_props) { }; /** + * 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'. @@ -560,8 +633,8 @@ Dygraph.prototype.toString = function() { * 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 + * @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. @@ -569,10 +642,10 @@ Dygraph.prototype.toString = function() { Dygraph.prototype.attr_ = function(name, seriesName) { // if (typeof(Dygraph.OPTIONS_REFERENCE) === 'undefined') { - this.error('Must include options reference JS for testing'); + Dygraph.error('Must include options reference JS for testing'); } else if (!Dygraph.OPTIONS_REFERENCE.hasOwnProperty(name)) { - this.error('Dygraphs is using property ' + name + ', which has no entry ' + - 'in the Dygraphs.OPTIONS_REFERENCE listing.'); + Dygraph.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; } @@ -590,24 +663,76 @@ Dygraph.prototype.attr_ = function(name, seriesName) { * 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. + * @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') + * @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][opt]) { + if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { return axis_opts[axis][opt]; } // user-specified attributes always trump defaults, even if they're less @@ -617,7 +742,7 @@ Dygraph.prototype.optionsViewForAxis_ = function(axis) { } axis_opts = self.attrs_.axes; - if (axis_opts && axis_opts[axis] && axis_opts[axis][opt]) { + if (axis_opts && axis_opts[axis] && axis_opts[axis].hasOwnProperty(opt)) { return axis_opts[axis][opt]; } // check old-style axis options @@ -633,7 +758,7 @@ Dygraph.prototype.optionsViewForAxis_ = function(axis) { /** * Returns the current rolling period, as set by the user or an option. - * @return {Number} The number of points in the rolling window + * @return {number} The number of points in the rolling window */ Dygraph.prototype.rollPeriod = function() { return this.rollPeriod_; @@ -654,8 +779,18 @@ Dygraph.prototype.xAxisRange = function() { * 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]; }; @@ -776,7 +911,7 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { var yRange = this.yAxisRange(axis); if (typeof(axis) == "undefined") axis = 0; - if (!this.axes_[axis].logscale) { + 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. @@ -818,9 +953,9 @@ Dygraph.prototype.toDataYCoord = function(y, axis) { * 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. + * @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) { @@ -854,8 +989,8 @@ Dygraph.prototype.toPercentYCoord = function(y, axis) { * 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. + * @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) { @@ -868,44 +1003,31 @@ Dygraph.prototype.toPercentXCoord = function(x) { /** * Returns the number of columns (including the independent variable). - * @return { Integer } The number of columns. + * @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 { Integer } The number of rows, less any header. + * @return {number} The number of rows, less any header. */ Dygraph.prototype.numRows = function() { + if (!this.rawData_) return 0; return this.rawData_.length; }; /** - * Returns the full range of the x-axis, as determined by the most extreme - * values in the data set. Not affected by zooming, visibility, etc. - * TODO(danvk): merge w/ xAxisExtremes - * @return { Array } A [low, high] pair - * @private - */ -Dygraph.prototype.fullXRange_ = function() { - if (this.numRows() > 0) { - return [this.rawData_[0][0], this.rawData_[this.numRows() - 1][0]]; - } else { - return [0, 1]; - } -}; - -/** * 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. + * @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; @@ -925,29 +1047,22 @@ Dygraph.prototype.createInterface_ = function() { var enclosing = this.maindiv_; this.graphDiv = document.createElement("div"); - this.graphDiv.style.width = this.width_ + "px"; - this.graphDiv.style.height = this.height_ + "px"; + + // TODO(danvk): any other styles that are useful to set here? + this.graphDiv.style.textAlign = 'left'; // This is a CSS "reset" enclosing.appendChild(this.graphDiv); // Create the canvas for interactive parts of the chart. this.canvas_ = Dygraph.createCanvas(); this.canvas_.style.position = "absolute"; - this.canvas_.width = this.width_; - this.canvas_.height = this.height_; - this.canvas_.style.width = this.width_ + "px"; // for IE - this.canvas_.style.height = this.height_ + "px"; // for IE - - this.canvas_ctx_ = Dygraph.getContext(this.canvas_); // ... 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_); - if (this.attr_('showRangeSelector')) { - // The range selector must be created here so that its canvases and contexts get created here. - // For some reason, if the canvases and contexts don't get created here, things don't work in IE. - this.rangeSelector_ = new DygraphRangeSelector(this); - } + this.resizeElements_(); // The interactive parts of the graph are drawn on top of the chart. this.graphDiv.appendChild(this.hidden_); @@ -957,32 +1072,61 @@ Dygraph.prototype.createInterface_ = function() { // Create the grapher this.layout_ = new DygraphLayout(this); - if (this.rangeSelector_) { - // This needs to happen after the graph canvases are added to the div and the layout object is created. - this.rangeSelector_.addToGraph(this.graphDiv, this.layout_); - } - var dygraph = this; - this.mouseMoveHandler = function(e) { + this.mouseMoveHandler_ = function(e) { dygraph.mouseMove_(e); }; - this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); - this.mouseOutHandler = function(e) { - dygraph.mouseOut_(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 (Dygraph.isNodeContainedBy(target, dygraph.graphDiv) && + !Dygraph.isNodeContainedBy(relatedTarget, dygraph.graphDiv)) { + dygraph.mouseOut_(e); + } }; - this.addEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); - this.createDragInterface_(); + this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_); + this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); - this.resizeHandler = function(e) { - dygraph.resize(); - }; + // 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.addEvent(window, 'resize', this.resizeHandler); + // 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 = Dygraph.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 = Dygraph.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); + } }; /** @@ -991,6 +1135,9 @@ Dygraph.prototype.createInterface_ = function() { * usage. See, e.g., the tests/perf.html example. */ Dygraph.prototype.destroy = function() { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore(); + var removeRecursive = function(node) { while (node.hasChildNodes()) { removeRecursive(node.firstChild); @@ -998,16 +1145,16 @@ Dygraph.prototype.destroy = function() { } }; - for (var idx = 0; idx < this.registeredEvents_.length; idx++) { - var reg = this.registeredEvents_[idx]; - Dygraph.removeEvent(reg.elem, reg.type, reg.fn); - } - this.registeredEvents_ = []; + this.removeTrackedEvents_(); // remove mouse event handlers (This may not be necessary anymore) - Dygraph.removeEvent(this.mouseEventElement_, 'mouseout', this.mouseOutHandler); - Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler); - Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseUpHandler_); + Dygraph.removeEvent(window, 'mouseout', this.mouseOutHandler_); + Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); + + // remove window handlers + Dygraph.removeEvent(window,'resize',this.resizeHandler_); + this.resizeHandler_ = null; + removeRecursive(this.maindiv_); var nullOut = function(obj) { @@ -1017,9 +1164,6 @@ Dygraph.prototype.destroy = function() { } } }; - // remove event handlers - Dygraph.removeEvent(window,'resize',this.resizeHandler); - this.resizeHandler = null; // These may not all be necessary, but it can't hurt... nullOut(this.layout_); nullOut(this.plotter_); @@ -1081,28 +1225,32 @@ Dygraph.prototype.setColors_ = function() { var num = labels.length - 1; this.colors_ = []; this.colorsMap_ = {}; - var colors = this.attr_('colors'); - var i; - if (!colors) { - var sat = this.attr_('colorSaturation') || 1.0; - var val = this.attr_('colorValue') || 0.5; - var half = Math.ceil(num / 2); - for (i = 1; i <= num; i++) { - if (!this.visibility()[i-1]) continue; - // alternate colors for high contrast. - var idx = i % 2 ? Math.ceil(i / 2) : (half + i / 2); - var hue = (1.0 * idx/ (1 + num)); - var colorStr = Dygraph.hsvToRGB(hue, sat, val); - this.colors_.push(colorStr); - this.colorsMap_[labels[i]] = colorStr; + + // 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; } - } else { - for (i = 0; i < num; i++) { - if (!this.visibility()[i]) continue; - var colorStr = colors[i % colors.length]; - this.colors_.push(colorStr); - this.colorsMap_[labels[1 + i]] = colorStr; + 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 = Dygraph.hsvToRGB(hue, sat, val); + } } + this.colors_.push(colorStr); + this.colorsMap_[label] = colorStr; } }; @@ -1110,7 +1258,7 @@ Dygraph.prototype.setColors_ = function() { * 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} The list of colors. + * @return {Array.} The list of colors. */ Dygraph.prototype.getColors = function() { return this.colors_; @@ -1158,7 +1306,7 @@ Dygraph.prototype.createRollInterface_ = function() { this.graphDiv.appendChild(this.roller_); } - var display = this.attr_('showRoller') ? 'block' : 'none'; + var display = this.getBooleanOption('showRoller') ? 'block' : 'none'; var area = this.plotter_.area; var textAttr = { "position": "absolute", @@ -1180,24 +1328,6 @@ Dygraph.prototype.createRollInterface_ = function() { }; /** - * @private - * Converts page the x-coordinate of the event to pixel x-coordinates on the - * canvas (i.e. DOM Coords). - */ -Dygraph.prototype.dragGetX_ = function(e, context) { - return Dygraph.pageX(e) - context.px; -}; - -/** - * @private - * Converts page the y-coordinate of the event to pixel y-coordinates on the - * canvas (i.e. DOM Coords). - */ -Dygraph.prototype.dragGetY_ = function(e, context) { - return Dygraph.pageY(e) - context.py; -}; - -/** * Set up all the mouse handlers needed to capture dragging behavior for zoom * events. * @private @@ -1254,16 +1384,17 @@ Dygraph.prototype.createDragInterface_ = function() { event.cancelBubble = true; } - contextB.px = Dygraph.findPosX(g.canvas_); - contextB.py = Dygraph.findPosY(g.canvas_); - contextB.dragStartX = g.dragGetX_(event, contextB); - contextB.dragStartY = g.dragGetY_(event, contextB); + var canvasPos = Dygraph.findPos(g.canvas_); + contextB.px = canvasPos.x; + contextB.py = canvasPos.y; + contextB.dragStartX = Dygraph.dragGetX_(event, contextB); + contextB.dragStartY = Dygraph.dragGetY_(event, contextB); contextB.cancelNextDblclick = false; contextB.tarp.cover(); } }; - var interactionModel = this.attr_("interactionModel"); + var interactionModel = this.getOption("interactionModel"); // Self is the graph. var self = this; @@ -1277,13 +1408,13 @@ Dygraph.prototype.createDragInterface_ = function() { for (var eventName in interactionModel) { if (!interactionModel.hasOwnProperty(eventName)) continue; - this.addEvent(this.mouseEventElement_, eventName, + 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. - this.mouseUpHandler_ = function(event) { + var mouseUpHandler = function(event) { if (context.isZooming || context.isPanning) { context.isZooming = false; context.dragStartX = null; @@ -1303,7 +1434,7 @@ Dygraph.prototype.createDragInterface_ = function() { context.tarp.uncover(); }; - this.addEvent(document, 'mouseup', this.mouseUpHandler_); + this.addAndTrackEvent(document, 'mouseup', mouseUpHandler); }; /** @@ -1312,20 +1443,20 @@ Dygraph.prototype.createDragInterface_ = function() { * 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 Dygraph.HORIZONTAL and Dygraph.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 + * @param {number} direction the direction of the zoom rectangle. Acceptable + * values are Dygraph.HORIZONTAL and Dygraph.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, @@ -1337,7 +1468,7 @@ Dygraph.prototype.drawZoomRect_ = function(direction, startX, endX, startY, if (prevDirection == Dygraph.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 == Dygraph.VERTICAL) { ctx.clearRect(this.layout_.getPlotArea().x, Math.min(startY, prevEndY), this.layout_.getPlotArea().w, Math.abs(startY - prevEndY)); } @@ -1377,8 +1508,8 @@ Dygraph.prototype.clearZoomRect_ = function() { * 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. + * @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) { @@ -1405,8 +1536,8 @@ Dygraph.zoomAnimationFunction = function(frame, numFrames) { * 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. + * @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) { @@ -1418,8 +1549,9 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { this.zoomed_x_ = true; var that = this; this.doAnimatedZoom(old_window, new_window, null, null, function() { - if (that.attr_("zoomCallback")) { - that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges()); + if (that.getFunctionOption("zoomCallback")) { + that.getFunctionOption("zoomCallback")( + minDate, maxDate, that.yAxisRanges()); } }); }; @@ -1428,8 +1560,8 @@ Dygraph.prototype.doZoomXDates_ = function(minDate, maxDate) { * 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. + * @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) { @@ -1449,9 +1581,10 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { this.zoomed_y_ = true; var that = this; this.doAnimatedZoom(null, null, oldValueRanges, newValueRanges, function() { - if (that.attr_("zoomCallback")) { + if (that.getFunctionOption("zoomCallback")) { var xRange = that.xAxisRange(); - that.attr_("zoomCallback")(xRange[0], xRange[1], that.yAxisRanges()); + that.getFunctionOption("zoomCallback")( + xRange[0], xRange[1], that.yAxisRanges()); } }); }; @@ -1459,10 +1592,8 @@ Dygraph.prototype.doZoomY_ = function(lowY, highY) { /** * Reset the zoom to the original view coordinates. This is the same as * double-clicking on the graph. - * - * @private */ -Dygraph.prototype.doUnzoom_ = function() { +Dygraph.prototype.resetZoom = function() { var dirty = false, dirtyX = false, dirtyY = false; if (this.dateWindow_ !== null) { dirty = true; @@ -1488,7 +1619,7 @@ Dygraph.prototype.doUnzoom_ = function() { // With only one frame, don't bother calculating extreme ranges. // TODO(danvk): merge this block w/ the code below. - if (!this.attr_("animatedZooms")) { + if (!this.getBooleanOption("animatedZooms")) { this.dateWindow_ = null; for (i = 0; i < this.axes_.length; i++) { if (this.axes_[i].valueWindow !== null) { @@ -1496,8 +1627,9 @@ Dygraph.prototype.doUnzoom_ = function() { } } this.drawGraph_(); - if (this.attr_("zoomCallback")) { - this.attr_("zoomCallback")(minDate, maxDate, this.yAxisRanges()); + if (this.getFunctionOption("zoomCallback")) { + this.getFunctionOption("zoomCallback")( + minDate, maxDate, this.yAxisRanges()); } return; } @@ -1512,7 +1644,7 @@ Dygraph.prototype.doUnzoom_ = function() { oldValueRanges = this.yAxisRanges(); // TODO(danvk): this is pretty inefficient var packed = this.gatherDatasets_(this.rolledSeries_, null); - var extremes = packed[1]; + 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 @@ -1538,8 +1670,9 @@ Dygraph.prototype.doUnzoom_ = function() { delete that.axes_[i].valueWindow; } } - if (that.attr_("zoomCallback")) { - that.attr_("zoomCallback")(minDate, maxDate, that.yAxisRanges()); + if (that.getFunctionOption("zoomCallback")) { + that.getFunctionOption("zoomCallback")( + minDate, maxDate, that.yAxisRanges()); } }); } @@ -1551,7 +1684,8 @@ Dygraph.prototype.doUnzoom_ = function() { * @private */ Dygraph.prototype.doAnimatedZoom = function(oldXRange, newXRange, oldYRanges, newYRanges, callback) { - var steps = this.attr_("animatedZooms") ? Dygraph.ANIMATION_STEPS : 1; + var steps = this.getBooleanOption("animatedZooms") ? + Dygraph.ANIMATION_STEPS : 1; var windows = []; var valueRanges = []; @@ -1607,20 +1741,25 @@ Dygraph.prototype.getArea = function() { * Returns a two-element array: [X, Y]. */ Dygraph.prototype.eventToDomCoords = function(event) { - var canvasx = Dygraph.pageX(event) - Dygraph.findPosX(this.mouseEventElement_); - var canvasy = Dygraph.pageY(event) - Dygraph.findPosY(this.mouseEventElement_); - return [canvasx, canvasy]; + 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; + return [canvasx, canvasy]; + } }; /** * Given a canvas X coordinate, find the closest row. - * @param {Number} domX graph-relative DOM X coordinate - * Returns: row number, integer + * @param {number} domX graph-relative DOM X coordinate + * Returns {number} row number. * @private */ Dygraph.prototype.findClosestRow = function(domX) { var minDistX = Infinity; - var pointIdx = -1, setIdx = -1; + var closestRow = -1; var sets = this.layout_.points; for (var i = 0; i < sets.length; i++) { var points = sets[i]; @@ -1631,14 +1770,12 @@ Dygraph.prototype.findClosestRow = function(domX) { var dist = Math.abs(point.canvasx - domX); if (dist < minDistX) { minDistX = dist; - setIdx = i; - pointIdx = j; + closestRow = point.idx; } } } - // TODO(danvk): remove this function; it's trivial and has only one use. - return this.idxToRow_(setIdx, pointIdx); + return closestRow; }; /** @@ -1648,19 +1785,18 @@ Dygraph.prototype.findClosestRow = function(domX) { * that's closest to the supplied DOM coordinates using the standard * Euclidean X,Y distance. * - * @param {Number} domX graph-relative DOM X coordinate - * @param {Number} domY graph-relative DOM Y coordinate + * @param {number} domX graph-relative DOM X coordinate + * @param {number} domY graph-relative DOM Y coordinate * Returns: {row, seriesName, point} * @private */ Dygraph.prototype.findClosestPoint = function(domX, domY) { var minDist = Infinity; - var idx = -1; - var dist, dx, dy, point, closestPoint, closestSeries; - for ( var setIdx = this.layout_.datasets.length - 1 ; setIdx >= 0 ; --setIdx ) { + var dist, dx, dy, point, closestPoint, closestSeries, closestRow; + for ( var setIdx = this.layout_.points.length - 1 ; setIdx >= 0 ; --setIdx ) { var points = this.layout_.points[setIdx]; for (var i = 0; i < points.length; ++i) { - var point = points[i]; + point = points[i]; if (!Dygraph.isValidPoint(point)) continue; dx = point.canvasx - domX; dy = point.canvasy - domY; @@ -1669,13 +1805,13 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { minDist = dist; closestPoint = point; closestSeries = setIdx; - idx = i; + closestRow = point.idx; } } } var name = this.layout_.setNames[closestSeries]; return { - row: idx + this.getLeftBoundary_(), + row: closestRow, seriesName: name, point: closestPoint }; @@ -1688,18 +1824,17 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { * then finds the series which puts the Y coordinate on top of its filled area, * using linear interpolation between adjacent point pairs. * - * @param {Number} domX graph-relative DOM X coordinate - * @param {Number} domY graph-relative DOM Y coordinate + * @param {number} domX graph-relative DOM X coordinate + * @param {number} domY graph-relative DOM Y coordinate * Returns: {row, seriesName, point} * @private */ Dygraph.prototype.findStackedPoint = function(domX, domY) { var row = this.findClosestRow(domX); - var boundary = this.getLeftBoundary_(); - var rowIdx = row - boundary; - var sets = this.layout_.points; var closestPoint, closestSeries; - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { + for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) { + var boundary = this.getLeftBoundary_(setIdx); + var rowIdx = row - boundary; var points = this.layout_.points[setIdx]; if (rowIdx >= points.length) continue; var p1 = points[rowIdx]; @@ -1756,11 +1891,11 @@ Dygraph.prototype.mouseMove_ = function(event) { var canvasx = canvasCoords[0]; var canvasy = canvasCoords[1]; - var highlightSeriesOpts = this.attr_("highlightSeriesOpts"); + var highlightSeriesOpts = this.getOption("highlightSeriesOpts"); var selectionChanged = false; if (highlightSeriesOpts && !this.isSeriesLocked()) { var closest; - if (this.attr_("stackedGraph")) { + if (this.getBooleanOption("stackedGraph")) { closest = this.findStackedPoint(canvasx, canvasy); } else { closest = this.findClosestPoint(canvasx, canvasy); @@ -1771,44 +1906,32 @@ Dygraph.prototype.mouseMove_ = function(event) { selectionChanged = this.setSelection(idx); } - var callback = this.attr_("highlightCallback"); + var callback = this.getFunctionOption("highlightCallback"); if (callback && selectionChanged) { - callback(event, this.lastx_, this.selPoints_, this.lastRow_, this.highlightSet_); + callback(event, + this.lastx_, + this.selPoints_, + this.lastRow_, + this.highlightSet_); } }; /** - * Fetch left offset from first defined boundaryIds record (see bug #236). + * Fetch left offset from the specified set index or if not passed, the + * first defined boundaryIds record (see bug #236). * @private */ -Dygraph.prototype.getLeftBoundary_ = function() { - for (var i = 0; i < this.boundaryIds_.length; i++) { - if (this.boundaryIds_[i] !== undefined) { - return this.boundaryIds_[i][0]; +Dygraph.prototype.getLeftBoundary_ = function(setIdx) { + if (this.boundaryIds_[setIdx]) { + return this.boundaryIds_[setIdx][0]; + } else { + for (var i = 0; i < this.boundaryIds_.length; i++) { + if (this.boundaryIds_[i] !== undefined) { + return this.boundaryIds_[i][0]; + } } + return 0; } - return 0; -}; - -/** - * Transforms layout_.points index into data row number. - * @param int layout_.points index - * @return int row number, or -1 if none could be found. - * @private - */ -Dygraph.prototype.idxToRow_ = function(setIdx, rowIdx) { - if (rowIdx < 0) return -1; - - var boundary = this.getLeftBoundary_(); - return boundary + rowIdx; - // for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - // var set = this.layout_.datasets[setIdx]; - // if (idx < set.length) { - // return boundary + idx; - // } - // idx -= set.length; - // } - // return -1; }; Dygraph.prototype.animateSelection_ = function(direction) { @@ -1848,7 +1971,8 @@ Dygraph.prototype.animateSelection_ = function(direction) { * @private */ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { - var defaultPrevented = this.cascadeEvents_('select', { + /*var defaultPrevented = */ + this.cascadeEvents_('select', { selectedX: this.lastx_, selectedPoints: this.selPoints_ }); @@ -1857,9 +1981,9 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { // Clear the previously drawn vertical, if there is one var i; var ctx = this.canvas_ctx_; - if (this.attr_('highlightSeriesOpts')) { + if (this.getOption('highlightSeriesOpts')) { ctx.clearRect(0, 0, this.width_, this.height_); - var alpha = 1.0 - this.attr_('highlightSeriesBackgroundAlpha'); + var alpha = 1.0 - this.getNumericOption('highlightSeriesBackgroundAlpha'); if (alpha) { // Activating background fade includes an animation effect for a gradual // fade. TODO(klausw): make this independently configurable if it causes @@ -1885,7 +2009,7 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { var maxCircleSize = 0; var labels = this.attr_('labels'); for (i = 1; i < labels.length; i++) { - var r = this.attr_('highlightCircleSize', labels[i]); + var r = this.getNumericOption('highlightCircleSize', labels[i]); if (r > maxCircleSize) maxCircleSize = r; } var px = this.previousVerticalX_; @@ -1905,17 +2029,17 @@ Dygraph.prototype.updateSelection_ = function(opt_animFraction) { var pt = this.selPoints_[i]; if (!Dygraph.isOK(pt.canvasy)) continue; - var circleSize = this.attr_('highlightCircleSize', pt.name); - var callback = this.attr_("drawHighlightPointCallback", pt.name); + 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; } - ctx.lineWidth = this.attr_('strokeWidth', pt.name); + ctx.lineWidth = this.getNumericOption('strokeWidth', pt.name); ctx.strokeStyle = color; ctx.fillStyle = color; - callback(this.g, pt.name, ctx, canvasx, pt.canvasy, - color, circleSize); + callback(this, pt.name, ctx, canvasx, pt.canvasy, + color, circleSize, pt.idx); } ctx.restore(); @@ -1927,9 +2051,9 @@ 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(). - * @param { Integer } row number that should be highlighted (i.e. appear with + * @param {number} row Row number that should be highlighted (i.e. appear with * hover dots on the chart). Set to false to clear any selection. - * @param { seriesName } optional series name to highlight that series with the + * @param {seriesName} optional series name to highlight that series with the * the highlightSeriesOpts setting. * @param { locked } optional If true, keep seriesName selected when mousing * over the graph, disabling closest-series highlighting. Call clearSelection() @@ -1939,23 +2063,15 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { // Extract the points we've selected this.selPoints_ = []; - if (row !== false) { - row -= this.getLeftBoundary_(); - } - var changed = false; if (row !== false && row >= 0) { if (row != this.lastRow_) changed = true; this.lastRow_ = row; - for (var setIdx = 0; setIdx < this.layout_.datasets.length; ++setIdx) { - var set = this.layout_.datasets[setIdx]; - if (row < set.length) { - var point = this.layout_.points[setIdx][row]; - - if (this.attr_("stackedGraph")) { - point = this.layout_.unstackPointAtIndex(setIdx, row); - } - + for (var setIdx = 0; setIdx < this.layout_.points.length; ++setIdx) { + var points = this.layout_.points[setIdx]; + var setRow = row - this.getLeftBoundary_(setIdx); + if (setRow < points.length) { + var point = points[setRow]; if (point.yval !== null) this.selPoints_.push(point); } } @@ -1991,11 +2107,11 @@ Dygraph.prototype.setSelection = function(row, opt_seriesName, opt_locked) { * @private */ Dygraph.prototype.mouseOut_ = function(event) { - if (this.attr_("unhighlightCallback")) { - this.attr_("unhighlightCallback")(event); + if (this.getFunctionOption("unhighlightCallback")) { + this.getFunctionOption("unhighlightCallback")(event); } - if (this.attr_("hideOverlayOnMouseOut") && !this.lockedSet_) { + if (this.getFunctionOption("hideOverlayOnMouseOut") && !this.lockedSet_) { this.clearSelection(); } }; @@ -2024,7 +2140,7 @@ Dygraph.prototype.clearSelection = function() { /** * Returns the number of the currently selected row. To get data for this row, * you can use the getValue method. - * @return { Integer } row number, or -1 if nothing is selected + * @return {number} row number, or -1 if nothing is selected */ Dygraph.prototype.getSelection = function() { if (!this.selPoints_ || this.selPoints_.length < 1) { @@ -2035,7 +2151,7 @@ Dygraph.prototype.getSelection = function() { var points = this.layout_.points[setIdx]; for (var row = 0; row < points.length; row++) { if (points[row].x == this.selPoints_[0].x) { - return row + this.getLeftBoundary_(); + return points[row].idx; } } } @@ -2060,7 +2176,7 @@ Dygraph.prototype.isSeriesLocked = function() { /** * Fires when there's data available to be graphed. - * @param {String} data Raw CSV data to be plotted + * @param {string} data Raw CSV data to be plotted * @private */ Dygraph.prototype.loadedEvent_ = function(data) { @@ -2078,7 +2194,7 @@ Dygraph.prototype.addXTicks_ = function() { if (this.dateWindow_) { range = [this.dateWindow_[0], this.dateWindow_[1]]; } else { - range = this.fullXRange_(); + range = this.xAxisExtremes(); } var xAxisOptionsView = this.optionsViewForAxis_('x'); @@ -2094,46 +2210,27 @@ Dygraph.prototype.addXTicks_ = function() { }; /** + * Returns the correct handler class for the currently set options. * @private - * Computes the range of the data series (including confidence intervals). - * @param { [Array] } series either [ [x1, y1], [x2, y2], ... ] or - * [ [x1, [y1, dev_low, dev_high]], [x2, [y2, dev_low, dev_high]], ... - * @return [low, high] - */ -Dygraph.prototype.extremeValues_ = function(series) { - var minY = null, maxY = null, j, y; - - var bars = this.attr_("errorBars") || this.attr_("customBars"); - if (bars) { - // With custom bars, maxY is the max of the high values. - for (j = 0; j < series.length; j++) { - y = series[j][1][0]; - if (y === null || isNaN(y)) continue; - var low = y - series[j][1][1]; - var high = y + series[j][1][2]; - if (low > y) low = y; // this can happen with custom bars, - if (high < y) high = y; // e.g. in tests/custom-bars.html - if (maxY === null || high > maxY) { - maxY = high; - } - if (minY === null || low < minY) { - minY = low; - } + */ +Dygraph.prototype.getHandlerClass_ = function() { + var handlerClass; + if (this.attr_('dataHandler')) { + handlerClass = this.attr_('dataHandler'); + } else if (this.fractions_) { + if (this.getBooleanOption('errorBars')) { + handlerClass = Dygraph.DataHandlers.FractionsBarsHandler; + } else { + handlerClass = Dygraph.DataHandlers.DefaultFractionHandler; } + } else if (this.getBooleanOption('customBars')) { + handlerClass = Dygraph.DataHandlers.CustomBarsHandler; + } else if (this.getBooleanOption('errorBars')) { + handlerClass = Dygraph.DataHandlers.ErrorBarsHandler; } else { - for (j = 0; j < series.length; j++) { - y = series[j][1]; - if (y === null || isNaN(y)) continue; - if (maxY === null || y > maxY) { - maxY = y; - } - if (minY === null || y < minY) { - minY = y; - } - } + handlerClass = Dygraph.DataHandlers.DefaultHandler; } - - return [minY, maxY]; + return handlerClass; }; /** @@ -2146,6 +2243,11 @@ Dygraph.prototype.extremeValues_ = function(series) { */ Dygraph.prototype.predraw_ = function() { var start = new Date(); + + // Create the correct dataHandler + this.dataHandler_ = new (this.getHandlerClass_())(); + + this.layout_.computePlotArea(); // TODO(danvk): move more computations out of drawGraph_ and into here. this.computeYAxes_(); @@ -2155,6 +2257,15 @@ Dygraph.prototype.predraw_ = function() { this.cascadeEvents_('clearChart'); this.plotter_.clear(); } + + if (!this.is_initial_draw_) { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore(); + } + + this.canvas_ctx_.save(); + this.hidden_ctx_.save(); + this.plotter_ = new DygraphCanvasRenderer(this, this.hidden_, this.hidden_ctx_, @@ -2166,18 +2277,16 @@ Dygraph.prototype.predraw_ = function() { this.cascadeEvents_('predraw'); - if (this.rangeSelector_) { - this.rangeSelector_.renderStaticLayer(); - } - // Convert the raw data (a 2D array) into the internal format and compute // rolling averages. this.rolledSeries_ = [null]; // x-axis is the first series and it's special for (var i = 1; i < this.numColumns(); i++) { // var logScale = this.attr_('logscale', i); // TODO(klausw): this looks wrong // konigsberg thinks so too. - var logScale = this.attr_('logscale'); - var series = this.extractSeries_(this.rawData_, i, logScale); - series = this.rollingAverage(series, this.rollPeriod_); + var series = this.dataHandler_.extractSeries(this.rawData_, i, this.attributes_); + if (this.rollPeriod_ > 1) { + series = this.dataHandler_.rollingAverage(series, this.rollPeriod_, this.attributes_); + } + this.rolledSeries_.push(series); } @@ -2190,6 +2299,126 @@ Dygraph.prototype.predraw_ = function() { }; /** + * Point structure. + * + * xval_* and yval_* are the original unscaled data values, + * while x_* and y_* are scaled to the range (0.0-1.0) for plotting. + * yval_stacked is the cumulative Y value used for stacking graphs, + * and bottom/top/minus/plus are used for error bar graphs. + * + * @typedef {{ + * idx: number, + * name: string, + * x: ?number, + * xval: ?number, + * y_bottom: ?number, + * y: ?number, + * y_stacked: ?number, + * y_top: ?number, + * yval_minus: ?number, + * yval: ?number, + * yval_plus: ?number, + * yval_stacked + * }} + */ +Dygraph.PointType = undefined; + +/** + * Calculates point stacking for stackedGraph=true. + * + * For stacking purposes, interpolate or extend neighboring data across + * NaN values based on stackedGraphNaNFill settings. This is for display + * only, the underlying data value as shown in the legend remains NaN. + * + * @param {Array.} points Point array for a single series. + * Updates each Point's yval_stacked property. + * @param {Array.} cumulativeYval Accumulated top-of-graph stacked Y + * values for the series seen so far. Index is the row number. Updated + * based on the current series's values. + * @param {Array.} seriesExtremes Min and max values, updated + * to reflect the stacked values. + * @param {string} fillMethod Interpolation method, one of 'all', 'inside', or + * 'none'. + * @private + */ +Dygraph.stackPoints_ = function( + points, cumulativeYval, seriesExtremes, fillMethod) { + var lastXval = null; + var prevPoint = null; + var nextPoint = null; + var nextPointIdx = -1; + + // Find the next stackable point starting from the given index. + var updateNextPoint = function(idx) { + // If we've previously found a non-NaN point and haven't gone past it yet, + // just use that. + if (nextPointIdx >= idx) return; + + // We haven't found a non-NaN point yet or have moved past it, + // look towards the right to find a non-NaN point. + for (var j = idx; j < points.length; ++j) { + // Clear out a previously-found point (if any) since it's no longer + // valid, we shouldn't use it for interpolation anymore. + nextPoint = null; + if (!isNaN(points[j].yval) && points[j].yval !== null) { + nextPointIdx = j; + nextPoint = points[j]; + break; + } + } + }; + + for (var i = 0; i < points.length; ++i) { + var point = points[i]; + var xval = point.xval; + if (cumulativeYval[xval] === undefined) { + cumulativeYval[xval] = 0; + } + + var actualYval = point.yval; + if (isNaN(actualYval) || actualYval === null) { + if(fillMethod == 'none') { + actualYval = 0; + } else { + // Interpolate/extend for stacking purposes if possible. + updateNextPoint(i); + if (prevPoint && nextPoint && fillMethod != 'none') { + // Use linear interpolation between prevPoint and nextPoint. + actualYval = prevPoint.yval + (nextPoint.yval - prevPoint.yval) * + ((xval - prevPoint.xval) / (nextPoint.xval - prevPoint.xval)); + } else if (prevPoint && fillMethod == 'all') { + actualYval = prevPoint.yval; + } else if (nextPoint && fillMethod == 'all') { + actualYval = nextPoint.yval; + } else { + actualYval = 0; + } + } + } else { + prevPoint = point; + } + + var stackedYval = cumulativeYval[xval]; + if (lastXval != xval) { + // If an x-value is repeated, we ignore the duplicates. + stackedYval += actualYval; + cumulativeYval[xval] = stackedYval; + } + lastXval = xval; + + point.yval_stacked = stackedYval; + + if (stackedYval > seriesExtremes[1]) { + seriesExtremes[1] = stackedYval; + } + if (stackedYval < seriesExtremes[0]) { + seriesExtremes[0] = stackedYval; + } + } +}; + + +/** * Loop over all fields and create datasets, calculating extreme y-values for * each series and extreme x-indices as we go. * @@ -2197,128 +2426,103 @@ Dygraph.prototype.predraw_ = function() { * extreme values "speculatively", i.e. without actually setting state on the * dygraph. * - * TODO(danvk): make this more of a true function - * @return [ datasets, seriesExtremes, boundaryIds ] + * @param {Array.)>>} rolledSeries, where + * rolledSeries[seriesIndex][row] = raw point, where + * seriesIndex is the column number starting with 1, and + * rawPoint is [x,y] or [x, [y, err]] or [x, [y, yminus, yplus]]. + * @param {?Array.} dateWindow [xmin, xmax] pair, or null. + * @return {{ + * points: Array.>, + * seriesExtremes: Array.>, + * boundaryIds: Array.}} * @private */ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { var boundaryIds = []; - var cumulative_y = []; // For stacked series. - var datasets = []; + var points = []; + var cumulativeYval = []; // For stacked series. var extremes = {}; // series name -> [low, high] - var i, j, k; - + var seriesIdx, sampleIdx; + var firstIdx, lastIdx; + // Loop over the fields (series). Go from the last to the first, // because if they're stacked that's how we accumulate the values. var num_series = rolledSeries.length - 1; - for (i = num_series; i >= 1; i--) { - if (!this.visibility()[i - 1]) continue; - - // Note: this copy _is_ necessary at the moment. - // If you remove it, it breaks zooming with error bars on. - // TODO(danvk): investigate further & write a test for this. - var series = []; - for (j = 0; j < rolledSeries[i].length; j++) { - series.push(rolledSeries[i][j]); - } + var series; + for (seriesIdx = num_series; seriesIdx >= 1; seriesIdx--) { + if (!this.visibility()[seriesIdx - 1]) continue; // Prune down to the desired range, if necessary (for zooming) // Because there can be lines going to points outside of the visible area, // we actually prune to visible points, plus one on either side. - var bars = this.attr_("errorBars") || this.attr_("customBars"); if (dateWindow) { + series = rolledSeries[seriesIdx]; var low = dateWindow[0]; var high = dateWindow[1]; - var pruned = []; + // TODO(danvk): do binary search instead of linear search. // TODO(danvk): pass firstIdx and lastIdx directly to the renderer. - var firstIdx = null, lastIdx = null; - for (k = 0; k < series.length; k++) { - if (series[k][0] >= low && firstIdx === null) { - firstIdx = k; + firstIdx = null; + lastIdx = null; + for (sampleIdx = 0; sampleIdx < series.length; sampleIdx++) { + if (series[sampleIdx][0] >= low && firstIdx === null) { + firstIdx = sampleIdx; } - if (series[k][0] <= high) { - lastIdx = k; + if (series[sampleIdx][0] <= high) { + lastIdx = sampleIdx; } } + if (firstIdx === null) firstIdx = 0; - if (firstIdx > 0) firstIdx--; - if (lastIdx === null) lastIdx = series.length - 1; - if (lastIdx < series.length - 1) lastIdx++; - boundaryIds[i-1] = [firstIdx, lastIdx]; - for (k = firstIdx; k <= lastIdx; k++) { - pruned.push(series[k]); + var correctedFirstIdx = firstIdx; + var isInvalidValue = true; + while (isInvalidValue && correctedFirstIdx > 0) { + correctedFirstIdx--; + // check if the y value is null. + isInvalidValue = series[correctedFirstIdx][1] === null; } - series = pruned; - } else { - boundaryIds[i-1] = [0, series.length-1]; - } - var seriesExtremes = this.extremeValues_(series); - - if (bars) { - for (j=0; j seriesExtremes[1]) { - seriesExtremes[1] = cumulative_y[x]; - } - if (cumulative_y[x] < seriesExtremes[0]) { - seriesExtremes[0] = cumulative_y[x]; - } - } + if (this.getBooleanOption("stackedGraph")) { + Dygraph.stackPoints_(seriesPoints, cumulativeYval, seriesExtremes, + this.getBooleanOption("stackedGraphNaNFill")); } - var seriesName = this.attr_("labels")[i]; extremes[seriesName] = seriesExtremes; - datasets[i] = series; - } - - // For stacked graphs, a NaN value for any point in the sum should create a - // clean gap in the graph. Back-propagate NaNs to all points at this X value. - if (this.attr_("stackedGraph")) { - for (k = datasets.length - 1; k >= 0; --k) { - // Use the first nonempty dataset to get X values. - if (!datasets[k]) continue; - for (j = 0; j < datasets[k].length; j++) { - var x = datasets[k][j][0]; - if (isNaN(cumulative_y[x])) { - // Set all Y values to NaN at that X value. - for (i = datasets.length - 1; i >= 0; i--) { - if (!datasets[i]) continue; - datasets[i][j][1] = NaN; - } - } - } - break; - } + points[seriesIdx] = seriesPoints; } - return [ datasets, extremes, boundaryIds ]; + return { points: points, extremes: extremes, boundaryIds: boundaryIds }; }; /** @@ -2337,12 +2541,12 @@ Dygraph.prototype.drawGraph_ = function() { this.layout_.removeAllDatasets(); this.setColors_(); - this.attrs_.pointSize = 0.5 * this.attr_('highlightCircleSize'); + this.attrs_.pointSize = 0.5 * this.getNumericOption('highlightCircleSize'); var packed = this.gatherDatasets_(this.rolledSeries_, this.dateWindow_); - var datasets = packed[0]; - var extremes = packed[1]; - this.boundaryIds_ = packed[2]; + var points = packed.points; + var extremes = packed.extremes; + this.boundaryIds_ = packed.boundaryIds; this.setIndexByName_ = {}; var labels = this.attr_("labels"); @@ -2350,10 +2554,10 @@ Dygraph.prototype.drawGraph_ = function() { this.setIndexByName_[labels[0]] = 0; } var dataIdx = 0; - for (var i = 1; i < datasets.length; i++) { + for (var i = 1; i < points.length; i++) { this.setIndexByName_[labels[i]] = i; if (!this.visibility()[i - 1]) continue; - this.layout_.addDataset(labels[i], datasets[i]); + this.layout_.addDataset(labels[i], points[i]); this.datasetIndex_[i] = dataIdx++; } @@ -2365,16 +2569,13 @@ Dygraph.prototype.drawGraph_ = function() { // 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.layout_.setDateWindow(this.dateWindow_); this.zoomed_x_ = tmp_zoomed_x; - this.layout_.evaluateWithError(); + this.layout_.evaluate(); this.renderGraph_(is_initial_draw); - if (this.attr_("timingName")) { + if (this.getStringOption("timingName")) { var end = new Date(); - if (console) { - console.log(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms"); - } + Dygraph.info(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms"); } }; @@ -2388,10 +2589,10 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { this.cascadeEvents_('clearChart'); this.plotter_.clear(); - if (this.attr_('underlayCallback')) { + if (this.getFunctionOption('underlayCallback')) { // NOTE: we pass the dygraph object to this callback twice to avoid breaking // users who expect a deprecated form of this callback. - this.attr_('underlayCallback')( + this.getFunctionOption('underlayCallback')( this.hidden_ctx_, this.layout_.getPlotArea(), this, this); } @@ -2402,19 +2603,22 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { this.cascadeEvents_('willDrawChart', e); this.plotter_.render(); this.cascadeEvents_('didDrawChart', e); + this.lastRow_ = -1; // because plugins/legend.js clears the legend // TODO(danvk): is this a performance bottleneck when panning? // The interaction canvas should already be empty in that situation. this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, this.canvas_.height); - // Generate a static legend before any particular point is selected. - - if (this.rangeSelector_) { - this.rangeSelector_.renderInteractiveLayer(); + if (this.getFunctionOption("drawCallback") !== null) { + this.getFunctionOption("drawCallback")(this, is_initial_draw); } - if (this.attr_("drawCallback") !== null) { - this.attr_("drawCallback")(this, is_initial_draw); + if (is_initial_draw) { + this.readyFired_ = true; + while (this.readyFns_.length > 0) { + var fn = this.readyFns_.pop(); + fn(this); + } } }; @@ -2429,10 +2633,9 @@ 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 i, valueWindows, seriesName, axis, index, opts, v; + 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++) { @@ -2444,7 +2647,7 @@ Dygraph.prototype.computeYAxes_ = function() { // data computation as well as options storage. // Go through once and add all the axes. this.axes_ = []; - + for (axis = 0; axis < this.attributes_.numAxes(); axis++) { // Add a new axis, making a copy of its per-axis options. opts = { g : this }; @@ -2452,27 +2655,22 @@ Dygraph.prototype.computeYAxes_ = function() { this.axes_[axis] = opts; } - // TODO(konigsberg): REMOVE THIS SILLINESS this should just come from DygraphOptions. - // TODO(konigsberg): Add tests for all of these. - // all options which could be applied per-axis: - var axisOptions = [ - 'valueRange', - 'pixelsPerYLabel', - 'axisLabelFontSize', - ]; - - // Copy global axis options over to the first axis. - for (i = 0; i < axisOptions.length; i++) { - var k = axisOptions[i]; - v = this.attr_(k); - if (v) this.axes_[0][k] = v; - } - // TODO(konigsberg): end of REMOVE THIS SILLINESS + // 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. - for (index = 0; index < valueWindows.length; index++) { + + // 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]; } } @@ -2494,7 +2692,7 @@ Dygraph.prototype.computeYAxes_ = function() { /** * Returns the number of y-axes on the chart. - * @return {Number} the number of axes. + * @return {number} the number of axes. */ Dygraph.prototype.numAxes = function() { return this.attributes_.numAxes(); @@ -2503,9 +2701,9 @@ Dygraph.prototype.numAxes = function() { /** * @private * Returns axis properties for the given series. - * @param { String } setName The name of the series for which to get axis + * @param {string} setName The name of the series for which to get axis * properties, e.g. 'Y1'. - * @return { Object } The axis properties. + * @return {Object} The axis properties. */ Dygraph.prototype.axisPropertiesForSeries = function(series) { // TODO(danvk): handle errors. @@ -2519,17 +2717,45 @@ Dygraph.prototype.axisPropertiesForSeries = function(series) { * This fills in the valueRange and ticks fields in each entry of this.axes_. */ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { - var series; + var isNullUndefinedOrNaN = function(num) { + return isNaN(parseFloat(num)); + }; var numAxes = this.attributes_.numAxes(); + var ypadCompat, span, series, ypad; + + var p_axis; // Compute extreme values, a span and tick marks for each axis. for (var i = 0; i < numAxes; i++) { var axis = this.axes_[i]; var logscale = this.attributes_.getForAxis("logscale", i); var includeZero = this.attributes_.getForAxis("includeZero", i); + var independentTicks = this.attributes_.getForAxis("independentTicks", i); series = this.attributes_.seriesForAxis(i); - if (series.length == 0) { + // Add some padding. This supports two Y padding operation modes: + // + // - 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 + // 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. + // + // - new-style (yRangePad set by the user): + // always add the specified Y padding. + // + ypadCompat = true; + ypad = 0.1; // add 10% + if (this.getNumericOption('yRangePad') !== null) { + ypadCompat = false; + // Convert pixel padding to ratio + ypad = this.getNumericOption('yRangePad') / this.plotter_.area.h; + } + + if (series.length === 0) { // If no series are defined or visible then use a reasonable default axis.extremeRange = [0, 1]; } else { @@ -2552,35 +2778,49 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { maxY = Math.max(extremeMaxY, maxY); } } - if (includeZero && minY > 0) minY = 0; + + // Include zero if requested by the user. + if (includeZero && !logscale) { + if (minY > 0) minY = 0; + if (maxY < 0) maxY = 0; + } // Ensure we have a valid scale, otherwise default to [0, 1] for safety. if (minY == Infinity) minY = 0; if (maxY == -Infinity) maxY = 1; - // Add some padding and round up to an integer to be human-friendly. - var span = maxY - minY; - // special case: if we have no sense of scale, use +/-10% of the sole value. - if (span === 0) { span = maxY; } + span = maxY - minY; + // special case: if we have no sense of scale, center on the sole value. + if (span === 0) { + if (maxY !== 0) { + span = Math.abs(maxY); + } else { + // ... and if the sole value is zero, use range 0-1. + maxY = 1; + span = 1; + } + } var maxAxisY, minAxisY; if (logscale) { - maxAxisY = maxY + 0.1 * span; - minAxisY = minY; + if (ypadCompat) { + maxAxisY = maxY + ypad * span; + minAxisY = minY; + } else { + var logpad = Math.exp(Math.log(span) * ypad); + maxAxisY = maxY * logpad; + minAxisY = minY / logpad; + } } else { - maxAxisY = maxY + 0.1 * span; - minAxisY = minY - 0.1 * span; + maxAxisY = maxY + ypad * span; + minAxisY = minY - ypad * span; - // Try to include zero and make it minAxisY (or maxAxisY) if it makes sense. - if (!this.attr_("avoidMinZero")) { + // 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")) { if (minAxisY < 0 && minY >= 0) minAxisY = 0; if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; } - - if (this.attr_("includeZero")) { - if (maxY < 0) maxAxisY = 0; - if (minY > 0) minAxisY = 0; - } } axis.extremeRange = [minAxisY, maxAxisY]; } @@ -2591,24 +2831,50 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { axis.computedValueRange = [axis.valueWindow[0], axis.valueWindow[1]]; } else if (axis.valueRange) { // This is a user-set value range for this axis. - axis.computedValueRange = [axis.valueRange[0], axis.valueRange[1]]; + 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; } - - // Add ticks. By default, all axes inherit the tick positions of the - // primary axis. However, if an axis is specifically marked as having - // independent ticks, then that is permissible as well. - var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); - var ticker = opts('ticker'); - if (i === 0 || axis.independentTicks) { + + + if (independentTicks) { + axis.independentTicks = independentTicks; + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); axis.ticks = ticker(axis.computedValueRange[0], - axis.computedValueRange[1], - this.height_, // TODO(danvk): should be area.height - opts, - this); - } else { - var p_axis = this.axes_[0]; + axis.computedValueRange[1], + this.height_, // TODO(danvk): should be area.height + opts, + this); + // Define the first independent axis as primary axis. + if (!p_axis) p_axis = axis; + } + } + if (p_axis === undefined) { + throw ("Configuration Error: At least one axis has to have the \"independentTicks\" option activated."); + } + // Add ticks. By default, all axes inherit the tick positions of the + // primary axis. However, if an axis is specifically marked as having + // independent ticks, then that is permissible as well. + for (var i = 0; i < numAxes; i++) { + var axis = this.axes_[i]; + + if (!axis.independentTicks) { + var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); + var ticker = opts('ticker'); var p_ticks = p_axis.ticks; var p_scale = p_axis.computedValueRange[1] - p_axis.computedValueRange[0]; var scale = axis.computedValueRange[1] - axis.computedValueRange[0]; @@ -2630,178 +2896,9 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { }; /** - * Extracts one series from the raw data (a 2D array) into an array of (date, - * value) tuples. - * - * This is where undesirable points (i.e. negative values on log scales and - * missing values through which we wish to connect lines) are dropped. - * TODO(danvk): the "missing values" bit above doesn't seem right. - * - * @private - */ -Dygraph.prototype.extractSeries_ = function(rawData, i, logScale) { - // TODO(danvk): pre-allocate series here. - var series = []; - for (var j = 0; j < rawData.length; j++) { - var x = rawData[j][0]; - var point = rawData[j][i]; - if (logScale) { - // On the log scale, points less than zero do not exist. - // This will create a gap in the chart. - if (point <= 0) { - point = null; - } - } - series.push([x, point]); - } - return series; -}; - -/** - * @private - * Calculates the rolling average of a data set. - * If originalData is [label, val], rolls the average of those. - * If originalData is [label, [, it's interpreted as [value, stddev] - * and the roll is returned in the same form, with appropriately reduced - * stddev for each value. - * Note that this is where fractional input (i.e. '5/10') is converted into - * decimal values. - * @param {Array} originalData The data in the appropriate format (see above) - * @param {Number} rollPeriod The number of points over which to average the - * data - */ -Dygraph.prototype.rollingAverage = function(originalData, rollPeriod) { - if (originalData.length < 2) - return originalData; - rollPeriod = Math.min(rollPeriod, originalData.length); - var rollingData = []; - var sigma = this.attr_("sigma"); - - var low, high, i, j, y, sum, num_ok, stddev; - if (this.fractions_) { - var num = 0; - var den = 0; // numerator/denominator - var mult = 100.0; - for (i = 0; i < originalData.length; i++) { - num += originalData[i][1][0]; - den += originalData[i][1][1]; - if (i - rollPeriod >= 0) { - num -= originalData[i - rollPeriod][1][0]; - den -= originalData[i - rollPeriod][1][1]; - } - - var date = originalData[i][0]; - var value = den ? num / den : 0.0; - if (this.attr_("errorBars")) { - if (this.attr_("wilsonInterval")) { - // For more details on this confidence interval, see: - // http://en.wikipedia.org/wiki/Binomial_confidence_interval - if (den) { - var p = value < 0 ? 0 : value, n = den; - var pm = sigma * Math.sqrt(p*(1-p)/n + sigma*sigma/(4*n*n)); - var denom = 1 + sigma * sigma / den; - low = (p + sigma * sigma / (2 * den) - pm) / denom; - high = (p + sigma * sigma / (2 * den) + pm) / denom; - rollingData[i] = [date, - [p * mult, (p - low) * mult, (high - p) * mult]]; - } else { - rollingData[i] = [date, [0, 0, 0]]; - } - } else { - stddev = den ? sigma * Math.sqrt(value * (1 - value) / den) : 1.0; - rollingData[i] = [date, [mult * value, mult * stddev, mult * stddev]]; - } - } else { - rollingData[i] = [date, mult * value]; - } - } - } else if (this.attr_("customBars")) { - low = 0; - var mid = 0; - high = 0; - var count = 0; - for (i = 0; i < originalData.length; i++) { - var data = originalData[i][1]; - y = data[1]; - rollingData[i] = [originalData[i][0], [y, y - data[0], data[2] - y]]; - - if (y !== null && !isNaN(y)) { - low += data[0]; - mid += y; - high += data[2]; - count += 1; - } - if (i - rollPeriod >= 0) { - var prev = originalData[i - rollPeriod]; - if (prev[1][1] !== null && !isNaN(prev[1][1])) { - low -= prev[1][0]; - mid -= prev[1][1]; - high -= prev[1][2]; - count -= 1; - } - } - if (count) { - rollingData[i] = [originalData[i][0], [ 1.0 * mid / count, - 1.0 * (mid - low) / count, - 1.0 * (high - mid) / count ]]; - } else { - rollingData[i] = [originalData[i][0], [null, null, null]]; - } - } - } else { - // Calculate the rolling average for the first rollPeriod - 1 points where - // there is not enough data to roll over the full number of points - if (!this.attr_("errorBars")){ - if (rollPeriod == 1) { - return originalData; - } - - for (i = 0; i < originalData.length; i++) { - sum = 0; - num_ok = 0; - for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { - y = originalData[j][1]; - if (y === null || isNaN(y)) continue; - num_ok++; - sum += originalData[j][1]; - } - if (num_ok) { - rollingData[i] = [originalData[i][0], sum / num_ok]; - } else { - rollingData[i] = [originalData[i][0], null]; - } - } - - } else { - for (i = 0; i < originalData.length; i++) { - sum = 0; - var variance = 0; - num_ok = 0; - for (j = Math.max(0, i - rollPeriod + 1); j < i + 1; j++) { - y = originalData[j][1][0]; - if (y === null || isNaN(y)) continue; - num_ok++; - sum += originalData[j][1][0]; - variance += Math.pow(originalData[j][1][1], 2); - } - if (num_ok) { - stddev = Math.sqrt(variance) / num_ok; - rollingData[i] = [originalData[i][0], - [sum / num_ok, sigma * stddev, sigma * stddev]]; - } else { - rollingData[i] = [originalData[i][0], [null, null, null]]; - } - } - } - } - - return rollingData; -}; - -/** * Detects the type of the str (date or numeric) and sets the various * formatting attributes in this.attrs_ based on this type. - * @param {String} str An x value. + * @param {string} str An x value. * @private */ Dygraph.prototype.detectTypeFromString_ = function(str) { @@ -2834,40 +2931,6 @@ Dygraph.prototype.setXAxisOptions_ = function(isDate) { this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } -} - -/** - * Parses the value as a floating point number. This is like the parseFloat() - * built-in, but with a few differences: - * - the empty string is parsed as null, rather than NaN. - * - if the string cannot be parsed at all, an error is logged. - * If the string can't be parsed, this method returns null. - * @param {String} x The string to be parsed - * @param {Number} opt_line_no The line number from which the string comes. - * @param {String} opt_line The text of the line from which the string comes. - * @private - */ - -// Parse the x as a float or return null if it's not a number. -Dygraph.prototype.parseFloat_ = function(x, opt_line_no, opt_line) { - var val = parseFloat(x); - if (!isNaN(val)) return val; - - // Try to figure out what happeend. - // If the value is the empty string, parse it as null. - if (/^ *$/.test(x)) return null; - - // If it was actually "NaN", return it as NaN. - if (/^ *nan *$/i.test(x)) return NaN; - - // Looks like a parsing error. - var msg = "Unable to parse '" + x + "' as a number"; - if (opt_line !== null && opt_line_no !== null) { - msg += " on line " + (1+opt_line_no) + " ('" + opt_line + "') of CSV."; - } - this.error(msg); - - return null; }; /** @@ -2894,7 +2957,7 @@ Dygraph.prototype.parseCSV_ = function(data) { var vals, j; // Use the default delimiter or fall back to a tab if that makes sense. - var delim = this.attr_('delimiter'); + var delim = this.getStringOption('delimiter'); if (lines[0].indexOf(delim) == -1 && lines[0].indexOf('\t') >= 0) { delim = '\t'; } @@ -2923,7 +2986,7 @@ Dygraph.prototype.parseCSV_ = function(data) { var fields = []; if (!defaultParserSet) { this.detectTypeFromString_(inFields[0]); - xParser = this.attr_("xValueParser"); + xParser = this.getFunctionOption("xValueParser"); defaultParserSet = true; } fields[0] = xParser(inFields[0], this); @@ -2934,27 +2997,27 @@ Dygraph.prototype.parseCSV_ = function(data) { // TODO(danvk): figure out an appropriate way to flag parse errors. vals = inFields[j].split("/"); if (vals.length != 2) { - this.error('Expected fractional "num/den" values in CSV data ' + - "but found a value '" + inFields[j] + "' on line " + - (1 + i) + " ('" + line + "') which is not of this form."); + Dygraph.error('Expected fractional "num/den" values in CSV data ' + + "but found a value '" + inFields[j] + "' on line " + + (1 + i) + " ('" + line + "') which is not of this form."); fields[j] = [0, 0]; } else { - fields[j] = [this.parseFloat_(vals[0], i, line), - this.parseFloat_(vals[1], i, line)]; + fields[j] = [Dygraph.parseFloat_(vals[0], i, line), + Dygraph.parseFloat_(vals[1], i, line)]; } } - } else if (this.attr_("errorBars")) { + } else if (this.getBooleanOption("errorBars")) { // If there are error bars, values are (value, stddev) pairs if (inFields.length % 2 != 1) { - this.error('Expected alternating (value, stdev.) pairs in CSV data ' + - 'but line ' + (1 + i) + ' has an odd number of values (' + - (inFields.length - 1) + "): '" + line + "'"); + Dygraph.error('Expected alternating (value, stdev.) pairs in CSV data ' + + 'but line ' + (1 + i) + ' has an odd number of values (' + + (inFields.length - 1) + "): '" + line + "'"); } for (j = 1; j < inFields.length; j += 2) { - fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line), - this.parseFloat_(inFields[j + 1], i, line)]; + fields[(j + 1) / 2] = [Dygraph.parseFloat_(inFields[j], i, line), + Dygraph.parseFloat_(inFields[j + 1], i, line)]; } - } else if (this.attr_("customBars")) { + } else if (this.getBooleanOption("customBars")) { // Bars are a low;center;high tuple for (j = 1; j < inFields.length; j++) { var val = inFields[j]; @@ -2963,20 +3026,20 @@ Dygraph.prototype.parseCSV_ = function(data) { } else { vals = val.split(";"); if (vals.length == 3) { - fields[j] = [ this.parseFloat_(vals[0], i, line), - this.parseFloat_(vals[1], i, line), - this.parseFloat_(vals[2], i, line) ]; + fields[j] = [ Dygraph.parseFloat_(vals[0], i, line), + Dygraph.parseFloat_(vals[1], i, line), + Dygraph.parseFloat_(vals[2], i, line) ]; } else { - this.warn('When using customBars, values must be either blank ' + - 'or "low;center;high" tuples (got "' + val + - '" on line ' + (1+i)); + Dygraph.warn('When using customBars, values must be either blank ' + + 'or "low;center;high" tuples (got "' + val + + '" on line ' + (1+i)); } } } } else { // Values are just numbers for (j = 1; j < inFields.length; j++) { - fields[j] = this.parseFloat_(inFields[j], i, line); + fields[j] = Dygraph.parseFloat_(inFields[j], i, line); } } if (ret.length > 0 && fields[0] < ret[ret.length - 1][0]) { @@ -2984,9 +3047,9 @@ Dygraph.prototype.parseCSV_ = function(data) { } if (fields.length != expectedCols) { - this.error("Number of columns in line " + i + " (" + fields.length + - ") does not agree with number of labels (" + expectedCols + - ") " + line); + Dygraph.error("Number of columns in line " + i + " (" + fields.length + + ") does not agree with number of labels (" + expectedCols + + ") " + line); } // If the user specified the 'labels' option and none of the cells of the @@ -2999,9 +3062,10 @@ Dygraph.prototype.parseCSV_ = function(data) { if (fields[j]) all_null = false; } if (all_null) { - this.warn("The dygraphs 'labels' option is set, but the first row of " + - "CSV data ('" + line + "') appears to also contain labels. " + - "Will drop the CSV labels and use the option labels."); + Dygraph.warn("The dygraphs 'labels' option is set, but the first row " + + "of CSV data ('" + line + "') appears to also contain " + + "labels. Will drop the CSV labels and use the option " + + "labels."); continue; } } @@ -3009,7 +3073,7 @@ Dygraph.prototype.parseCSV_ = function(data) { } if (outOfOrder) { - this.warn("CSV is out of order; order it correctly to speed loading."); + Dygraph.warn("CSV is out of order; order it correctly to speed loading."); ret.sort(function(a,b) { return a[0] - b[0]; }); } @@ -3017,28 +3081,28 @@ Dygraph.prototype.parseCSV_ = function(data) { }; /** - * @private * 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 * are dates, we need to convert them from Date objects to ms since epoch. - * @param {[Object]} data - * @return {[Object]} data with numeric x values. + * @param {!Array} data + * @return {Object} data with numeric x values. + * @private */ Dygraph.prototype.parseArray_ = function(data) { // Peek at the first x value to see if it's numeric. if (data.length === 0) { - this.error("Can't plot empty data set"); + Dygraph.error("Can't plot empty data set"); return null; } if (data[0].length === 0) { - this.error("Data set cannot contain an empty row"); + Dygraph.error("Data set cannot contain an empty row"); return null; } var i; if (this.attr_("labels") === null) { - this.warn("Using default labels. Set labels explicitly via 'labels' " + - "in the options parameter"); + Dygraph.warn("Using default labels. Set labels explicitly via 'labels' " + + "in the options parameter"); this.attrs_.labels = [ "X" ]; for (i = 1; i < data[0].length; i++) { this.attrs_.labels.push("Y" + i); // Not user_attrs_. @@ -3047,8 +3111,8 @@ Dygraph.prototype.parseArray_ = function(data) { } else { var num_labels = this.attr_("labels"); if (num_labels.length != data[0].length) { - this.error("Mismatch between number of labels (" + num_labels + - ") and number of columns in array (" + data[0].length + ")"); + Dygraph.error("Mismatch between number of labels (" + num_labels + ")" + + " and number of columns in array (" + data[0].length + ")"); return null; } } @@ -3063,13 +3127,13 @@ Dygraph.prototype.parseArray_ = function(data) { var parsedData = Dygraph.clone(data); for (i = 0; i < data.length; i++) { if (parsedData[i].length === 0) { - this.error("Row " + (1 + i) + " of data is empty"); + Dygraph.error("Row " + (1 + i) + " of data is empty"); return null; } if (parsedData[i][0] === null || typeof(parsedData[i][0].getTime) != 'function' || isNaN(parsedData[i][0].getTime())) { - this.error("x value in row " + (1 + i) + " is not a Date"); + Dygraph.error("x value in row " + (1 + i) + " is not a Date"); return null; } parsedData[i][0] = parsedData[i][0].getTime(); @@ -3091,7 +3155,7 @@ Dygraph.prototype.parseArray_ = function(data) { * number. All subsequent columns must be numbers. If there is a clear mismatch * between this.xValueParser_ and the type of the first column, it will be * fixed. Fills out rawData_. - * @param {[Object]} data See above. + * @param {!google.visualization.DataTable} data See above. * @private */ Dygraph.prototype.parseDataTable_ = function(data) { @@ -3123,8 +3187,8 @@ Dygraph.prototype.parseDataTable_ = function(data) { this.attrs_.axes.x.ticker = Dygraph.numericLinearTicks; this.attrs_.axes.x.axisLabelFormatter = this.attrs_.axes.x.valueFormatter; } else { - this.error("only 'date', 'datetime' and 'number' types are supported for " + - "column 1 of DataTable input (Got '" + indepType + "')"); + Dygraph.error("only 'date', 'datetime' and 'number' types are supported " + + "for column 1 of DataTable input (Got '" + indepType + "')"); return null; } @@ -3137,7 +3201,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { var type = data.getColumnType(i); if (type == 'number') { colIdx.push(i); - } else if (type == 'string' && this.attr_('displayAnnotations')) { + } else if (type == 'string' && this.getBooleanOption('displayAnnotations')) { // This is OK -- it's an annotation column. var dataIdx = colIdx[colIdx.length - 1]; if (!annotationCols.hasOwnProperty(dataIdx)) { @@ -3147,8 +3211,8 @@ Dygraph.prototype.parseDataTable_ = function(data) { } hasAnnotations = true; } else { - this.error("Only 'number' is supported as a dependent type with Gviz." + - " 'string' is only supported if displayAnnotations is true"); + Dygraph.error("Only 'number' is supported as a dependent type with Gviz." + + " 'string' is only supported if displayAnnotations is true"); } } @@ -3157,7 +3221,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { var labels = [data.getColumnLabel(0)]; for (i = 0; i < colIdx.length; i++) { labels.push(data.getColumnLabel(colIdx[i])); - if (this.attr_("errorBars")) i += 1; + if (this.getBooleanOption("errorBars")) i += 1; } this.attrs_.labels = labels; cols = labels.length; @@ -3169,8 +3233,8 @@ Dygraph.prototype.parseDataTable_ = function(data) { var row = []; if (typeof(data.getValue(i, 0)) === 'undefined' || data.getValue(i, 0) === null) { - this.warn("Ignoring row " + i + - " of DataTable because of undefined or null first column."); + Dygraph.warn("Ignoring row " + i + + " of DataTable because of undefined or null first column."); continue; } @@ -3179,7 +3243,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { } else { row.push(data.getValue(i, 0)); } - if (!this.attr_("errorBars")) { + if (!this.getBooleanOption("errorBars")) { for (j = 0; j < colIdx.length; j++) { var col = colIdx[j]; row.push(data.getValue(i, col)); @@ -3215,7 +3279,7 @@ Dygraph.prototype.parseDataTable_ = function(data) { } if (outOfOrder) { - this.warn("DataTable is out of order; order it correctly to speed loading."); + Dygraph.warn("DataTable is out of order; order it correctly to speed loading."); ret.sort(function(a,b) { return a[0] - b[0]; }); } this.rawData_ = ret; @@ -3253,7 +3317,16 @@ Dygraph.prototype.start_ = function() { if (line_delimiter) { this.loadedEvent_(data); } else { - var req = new XMLHttpRequest(); + // REMOVE_FOR_IE + var req; + if (window.XMLHttpRequest) { + // Firefox, Opera, IE7, and other browsers will use the native object + req = new XMLHttpRequest(); + } else { + // IE 5 and 6 will use the ActiveX control + req = new ActiveXObject("Microsoft.XMLHTTP"); + } + var caller = this; req.onreadystatechange = function () { if (req.readyState == 4) { @@ -3268,7 +3341,7 @@ Dygraph.prototype.start_ = function() { req.send(null); } } else { - this.error("Unknown data format: " + (typeof data)); + Dygraph.error("Unknown data format: " + (typeof data)); } }; @@ -3282,12 +3355,13 @@ Dygraph.prototype.start_ = function() { * There's a huge variety of options that can be passed to this method. For a * full list, see http://dygraphs.com/options.html. * - * @param {Object} attrs The new properties and values - * @param {Boolean} [block_redraw] Usually the chart is redrawn after every - * call to updateOptions(). If you know better, you can pass true to explicitly - * block the redraw. This can be useful for chaining updateOptions() calls, - * avoiding the occasional infinite loop and preventing redraws when it's not - * necessary (e.g. when updating a callback). + * @param {Object} input_attrs The new properties and values + * @param {boolean} block_redraw Usually the chart is redrawn after every + * call to updateOptions(). If you know better, you can pass true to + * explicitly block the redraw. This can be useful for chaining + * updateOptions() calls, avoiding the occasional infinite loop and + * preventing redraws when it's not necessary (e.g. when updating a + * callback). */ Dygraph.prototype.updateOptions = function(input_attrs, block_redraw) { if (typeof(block_redraw) == 'undefined') block_redraw = false; @@ -3376,6 +3450,10 @@ Dygraph.mapLegacyOptions_ = function(attrs) { map('pixelsPerYLabel', 'y', 'pixelsPerLabel'); map('yAxisLabelFormatter', 'y', 'axisLabelFormatter'); map('yTicker', 'y', 'ticker'); + map('drawXGrid', 'x', 'drawGrid'); + map('drawXAxis', 'x', 'drawAxis'); + map('drawYGrid', 'y', 'drawGrid'); + map('drawYAxis', 'y', 'drawAxis'); return my_attrs; }; @@ -3387,8 +3465,8 @@ Dygraph.mapLegacyOptions_ = function(attrs) { * This is far more efficient than destroying and re-instantiating a * Dygraph, since it doesn't have to reparse the underlying data. * - * @param {Number} [width] Width (in pixels) - * @param {Number} [height] Height (in pixels) + * @param {number} width Width (in pixels) + * @param {number} height Height (in pixels) */ Dygraph.prototype.resize = function(width, height) { if (this.resize_lock) { @@ -3397,8 +3475,8 @@ Dygraph.prototype.resize = function(width, height) { this.resize_lock = true; if ((width === null) != (height === null)) { - this.warn("Dygraph.resize() should be called with zero parameters or " + - "two non-NULL parameters. Pretending it was zero."); + Dygraph.warn("Dygraph.resize() should be called with zero parameters or " + + "two non-NULL parameters. Pretending it was zero."); width = height = null; } @@ -3416,15 +3494,9 @@ Dygraph.prototype.resize = function(width, height) { } if (old_width != this.width_ || old_height != this.height_) { - // TODO(danvk): there should be a clear() method. - this.maindiv_.innerHTML = ""; - this.roller_ = null; - this.attrs_.labelsDiv = null; - this.createInterface_(); - if (this.annotations_.length) { - // createInterface_ reset the layout, so we need to do this. - this.layout_.setAnnotations(this.annotations_); - } + // Resizing a canvas erases it, even when the size doesn't change, so + // any resize needs to be followed by a redraw. + this.resizeElements_(); this.predraw_(); } @@ -3434,7 +3506,7 @@ Dygraph.prototype.resize = function(width, height) { /** * Adjusts the number of points in the rolling average. Updates the graph to * reflect the new averaging period. - * @param {Number} length Number of points over which to average the data. + * @param {number} length Number of points over which to average the data. */ Dygraph.prototype.adjustRoll = function(length) { this.rollPeriod_ = length; @@ -3447,23 +3519,26 @@ Dygraph.prototype.adjustRoll = function(length) { Dygraph.prototype.visibility = function() { // Do lazy-initialization, so that this happens after we know the number of // data series. - if (!this.attr_("visibility")) { + if (!this.getOption("visibility")) { this.attrs_.visibility = []; } // TODO(danvk): it looks like this could go into an infinite loop w/ user_attrs. - while (this.attr_("visibility").length < this.numColumns() - 1) { + while (this.getOption("visibility").length < this.numColumns() - 1) { this.attrs_.visibility.push(true); } - return this.attr_("visibility"); + return this.getOption("visibility"); }; /** * Changes the visiblity of a series. + * + * @param {number} num the series index + * @param {boolean} value true or false, identifying the visibility. */ Dygraph.prototype.setVisibility = function(num, value) { var x = this.visibility(); if (num < 0 || num >= x.length) { - this.warn("invalid series number in setVisibility: " + num); + Dygraph.warn("invalid series number in setVisibility: " + num); } else { x[num] = value; this.predraw_(); @@ -3490,6 +3565,13 @@ 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_) { + Dygraph.warn("Tried to setAnnotations before dygraph was ready. " + + "Try setting them in a ready() block. See " + + "dygraphs.com/tests/annotation.html"); + return; + } + this.layout_.setAnnotations(this.annotations_); if (!suppressDraw) { this.predraw_(); @@ -3506,9 +3588,12 @@ Dygraph.prototype.annotations = function() { /** * Get the list of label names for this graph. The first column is the * x-axis, so the data series names start at index 1. + * + * Returns null when labels have not yet been defined. */ Dygraph.prototype.getLabels = function() { - return this.attr_("labels").slice(); + var labels = this.attr_("labels"); + return labels ? labels.slice() : null; }; /** @@ -3520,12 +3605,23 @@ Dygraph.prototype.indexFromSetName = function(name) { }; /** - * Get the internal dataset index given its name. These are numbered starting from 0, - * and only count visible sets. - * @private + * Trigger a callback when the dygraph has drawn itself and is ready to be + * manipulated. This is primarily useful when dygraphs has to do an XHR for the + * data (i.e. a URL is passed as the data source) and the chart is drawn + * asynchronously. If the chart has already drawn, the callback will fire + * immediately. + * + * This is a good place to call setAnnotation(). + * + * @param {function(!Dygraph)} callback The callback to trigger when the chart + * is ready. */ -Dygraph.prototype.datasetIndexFromSetName_ = function(name) { - return this.datasetIndex_[this.indexFromSetName(name)]; +Dygraph.prototype.ready = function(callback) { + if (this.is_initial_draw_) { + this.readyFns_.push(callback); + } else { + callback(this); + } }; /** @@ -3567,8 +3663,5 @@ Dygraph.addAnnotationRule = function() { } } - this.warn("Unable to add default annotation CSS rule; display may be off."); + Dygraph.warn("Unable to add default annotation CSS rule; display may be off."); }; - -// Older pages may still use this name. -var DateGraph = Dygraph;