X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph.js;h=1600cb28ddda8eeb048f9630b487486eee984868;hb=b0963cdbacfdda608252d4154d54cf2e390e30e1;hp=6728bc47143bf8588e8e9c64cc6989c3eb2fd1a9;hpb=ad617f174a6b4ec2aea3d06553534dcaf278a2bb;p=dygraphs.git diff --git a/dygraph.js b/dygraph.js index 6728bc4..1600cb2 100644 --- a/dygraph.js +++ b/dygraph.js @@ -44,7 +44,7 @@ */ /*jshint globalstrict: true */ -/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false */ +/*global DygraphLayout:false, DygraphCanvasRenderer:false, DygraphOptions:false, G_vmlCanvasManager:false,ActiveXObject:false */ "use strict"; /** @@ -62,6 +62,12 @@ * 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 @@ -74,7 +80,7 @@ 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 + "]"; }; @@ -291,6 +297,7 @@ Dygraph.DEFAULT_ATTRS = { connectSeparatedPoints: false, stackedGraph: false, + stackedGraphNaNFill: 'all', hideOverlayOnMouseOut: true, // TODO(danvk): support 'onmouseover' and 'never', and remove synonyms. @@ -345,6 +352,7 @@ Dygraph.DEFAULT_ATTRS = { axisLabelFormatter: Dygraph.dateAxisFormatter, valueFormatter: Dygraph.dateString_, drawGrid: true, + drawAxis: true, independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, @@ -353,6 +361,7 @@ Dygraph.DEFAULT_ATTRS = { valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, drawGrid: true, + drawAxis: true, independentTicks: true, ticker: null // will be set in dygraph-tickers.js }, @@ -360,6 +369,7 @@ Dygraph.DEFAULT_ATTRS = { pixelsPerLabel: 30, valueFormatter: Dygraph.numberValueFormatter, axisLabelFormatter: Dygraph.numberAxisLabelFormatter, + drawAxis: false, drawGrid: false, independentTicks: false, ticker: null // will be set in dygraph-tickers.js @@ -438,7 +448,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. @@ -465,9 +474,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) { @@ -657,13 +668,57 @@ Dygraph.prototype.getOption = function(name, opt_seriesName) { return this.attr_(name, opt_seriesName); }; +/** + * 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)); +}; + +/** + * 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)); +}; + +/** + * 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)); +}; + +/** + * 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) { @@ -717,7 +772,7 @@ Dygraph.prototype.xAxisRange = function() { * data set. */ Dygraph.prototype.xAxisExtremes = function() { - var pad = this.attr_('xRangePad') / this.plotter_.area.w; + var pad = this.getNumericOption('xRangePad') / this.plotter_.area.w; if (this.numRows() === 0) { return [0 - pad, 1 + pad]; } @@ -849,7 +904,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. @@ -985,8 +1040,7 @@ 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); @@ -994,15 +1048,13 @@ Dygraph.prototype.createInterface_ = function() { // 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.resizeElements_(); + + this.canvas_ctx_ = Dygraph.getContext(this.canvas_); this.hidden_ctx_ = Dygraph.getContext(this.hidden_); // The interactive parts of the graph are drawn on top of the chart. @@ -1031,8 +1083,8 @@ Dygraph.prototype.createInterface_ = function() { } }; - this.addEvent(window, 'mouseout', this.mouseOutHandler_); - this.addEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); + this.addAndTrackEvent(window, 'mouseout', this.mouseOutHandler_); + this.addAndTrackEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); // Don't recreate and register the resize handler on subsequent calls. // This happens when the graph is resized. @@ -1043,16 +1095,32 @@ Dygraph.prototype.createInterface_ = function() { // Update when the window is resized. // TODO(danvk): drop frames depending on complexity of the chart. - this.addEvent(window, 'resize', this.resizeHandler_); + this.addAndTrackEvent(window, 'resize', this.resizeHandler_); } }; +Dygraph.prototype.resizeElements_ = function() { + this.graphDiv.style.width = this.width_ + "px"; + this.graphDiv.style.height = this.height_ + "px"; + 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.hidden_.width = this.width_; + this.hidden_.height = this.height_; + this.hidden_.style.width = this.width_ + "px"; // for IE + this.hidden_.style.height = this.height_ + "px"; // for IE +}; + /** * Detach DOM elements in the dygraph and null out all data references. * Calling this when you're done with a dygraph can dramatically reduce memory * usage. See, e.g., the tests/perf.html example. */ Dygraph.prototype.destroy = function() { + this.canvas_ctx_.restore(); + this.hidden_ctx_.restore(); + var removeRecursive = function(node) { while (node.hasChildNodes()) { removeRecursive(node.firstChild); @@ -1060,19 +1128,11 @@ Dygraph.prototype.destroy = function() { } }; - if (this.registeredEvents_) { - 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(window, 'mouseout', this.mouseOutHandler_); Dygraph.removeEvent(this.mouseEventElement_, 'mousemove', this.mouseMoveHandler_); - Dygraph.removeEvent(this.mouseEventElement_, 'mouseup', this.mouseUpHandler_); // remove window handlers Dygraph.removeEvent(window,'resize',this.resizeHandler_); @@ -1148,28 +1208,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; } }; @@ -1225,7 +1289,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", @@ -1330,7 +1394,7 @@ Dygraph.prototype.createDragInterface_ = function() { } }; - var interactionModel = this.attr_("interactionModel"); + var interactionModel = this.getOption("interactionModel"); // Self is the graph. var self = this; @@ -1344,19 +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])); } - // unregister the handler on subsequent calls. - // This happens when the graph is resized. - if (this.mouseUpHandler_) { - Dygraph.removeEvent(document, 'mouseup', this.mouseUpHandler_); - } - // 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; @@ -1376,7 +1434,7 @@ Dygraph.prototype.createDragInterface_ = function() { context.tarp.uncover(); }; - this.addEvent(document, 'mouseup', this.mouseUpHandler_); + this.addAndTrackEvent(document, 'mouseup', mouseUpHandler); }; /** @@ -1410,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)); } @@ -1491,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()); } }); }; @@ -1522,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()); } }); }; @@ -1559,7 +1619,7 @@ Dygraph.prototype.resetZoom = 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) { @@ -1567,8 +1627,9 @@ Dygraph.prototype.resetZoom = 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; } @@ -1583,7 +1644,7 @@ Dygraph.prototype.resetZoom = 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 @@ -1609,8 +1670,9 @@ Dygraph.prototype.resetZoom = 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()); } }); } @@ -1622,7 +1684,8 @@ Dygraph.prototype.resetZoom = 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 = []; @@ -1695,7 +1758,7 @@ Dygraph.prototype.eventToDomCoords = function(event) { */ 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]; @@ -1706,14 +1769,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; }; /** @@ -1730,12 +1791,11 @@ Dygraph.prototype.findClosestRow = function(domX) { */ 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; @@ -1744,13 +1804,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 }; @@ -1770,10 +1830,10 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) { */ Dygraph.prototype.findStackedPoint = function(domX, domY) { var row = this.findClosestRow(domX); - var boundary = this.getLeftBoundary_(); - var rowIdx = row - boundary; 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]; @@ -1830,11 +1890,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); @@ -1845,48 +1905,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.getLeftBoundary_(), + 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) { @@ -1936,9 +1980,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 @@ -1964,7 +2008,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_; @@ -1984,13 +2028,13 @@ 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, @@ -2018,23 +2062,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); } } @@ -2070,11 +2106,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(); } }; @@ -2114,7 +2150,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; } } } @@ -2173,46 +2209,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; }; /** @@ -2225,6 +2242,9 @@ 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(); @@ -2236,6 +2256,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_, @@ -2252,9 +2281,11 @@ Dygraph.prototype.predraw_ = function() { 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); } @@ -2267,6 +2298,122 @@ 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) { + // 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. * @@ -2274,59 +2421,50 @@ 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 errorBars = this.attr_("errorBars"); - var customBars = this.attr_("customBars"); - var bars = errorBars || customBars; - var isValueNull = function(sample) { - if (!bars) { - return sample[1] === null; - } else { - return customBars ? sample[1][1] === null : - errorBars ? sample[1][0] === null : false; - } - }; - + 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. 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; } } @@ -2335,7 +2473,8 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { var isInvalidValue = true; while (isInvalidValue && correctedFirstIdx > 0) { correctedFirstIdx--; - isInvalidValue = isValueNull(series[correctedFirstIdx]); + // check if the y value is null. + isInvalidValue = series[correctedFirstIdx][1] === null; } if (lastIdx === null) lastIdx = series.length - 1; @@ -2343,97 +2482,42 @@ Dygraph.prototype.gatherDatasets_ = function(rolledSeries, dateWindow) { isInvalidValue = true; while (isInvalidValue && correctedLastIdx < series.length - 1) { correctedLastIdx++; - isInvalidValue = isValueNull(series[correctedLastIdx]); + isInvalidValue = series[correctedLastIdx][1] === null; } - boundaryIds[i-1] = [(firstIdx > 0) ? firstIdx - 1 : firstIdx, - (lastIdx < series.length - 1) ? lastIdx + 1 : lastIdx]; - if (correctedFirstIdx!==firstIdx) { - pruned.push(series[correctedFirstIdx]); - } - for (k = firstIdx; k <= lastIdx; k++) { - pruned.push(series[k]); + firstIdx = correctedFirstIdx; } if (correctedLastIdx !== lastIdx) { - pruned.push(series[correctedLastIdx]); + lastIdx = correctedLastIdx; } - - series = pruned; + + boundaryIds[seriesIdx-1] = [firstIdx, lastIdx]; + + // .slice's end is exclusive, we want to include lastIdx. + series = series.slice(firstIdx, lastIdx + 1); } else { - boundaryIds[i-1] = [0, series.length-1]; + series = rolledSeries[seriesIdx]; + boundaryIds[seriesIdx-1] = [0, series.length-1]; } - var seriesExtremes = this.extremeValues_(series); + var seriesName = this.attr_("labels")[seriesIdx]; + var seriesExtremes = this.dataHandler_.getExtremeYValues(series, + dateWindow, this.getBooleanOption("stepPlot",seriesName)); - 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 }; }; /** @@ -2452,12 +2536,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"); @@ -2465,10 +2549,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++; } @@ -2481,12 +2565,12 @@ Dygraph.prototype.drawGraph_ = function() { var tmp_zoomed_x = this.zoomed_x_; // Tell PlotKit to use this new data and render itself this.zoomed_x_ = tmp_zoomed_x; - this.layout_.evaluateWithError(); + this.layout_.evaluate(); this.renderGraph_(is_initial_draw); - if (this.attr_("timingName")) { + if (this.getStringOption("timingName")) { var end = new Date(); - Dygraph.info(this.attr_("timingName") + " - drawGraph: " + (end - start) + "ms"); + Dygraph.info(this.getStringOption("timingName") + " - drawGraph: " + (end - start) + "ms"); } }; @@ -2500,10 +2584,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); } @@ -2521,8 +2605,15 @@ Dygraph.prototype.renderGraph_ = function(is_initial_draw) { this.canvas_.getContext('2d').clearRect(0, 0, this.canvas_.width, this.canvas_.height); - if (this.attr_("drawCallback") !== null) { - this.attr_("drawCallback")(this, is_initial_draw); + if (this.getFunctionOption("drawCallback") !== null) { + this.getFunctionOption("drawCallback")(this, is_initial_draw); + } + if (is_initial_draw) { + this.readyFired_ = true; + while (this.readyFns_.length > 0) { + var fn = this.readyFns_.pop(); + fn(this); + } } }; @@ -2653,10 +2744,10 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // ypadCompat = true; ypad = 0.1; // add 10% - if (this.attr_('yRangePad') !== null) { + if (this.getNumericOption('yRangePad') !== null) { ypadCompat = false; // Convert pixel padding to ratio - ypad = this.attr_('yRangePad') / this.plotter_.area.h; + ypad = this.getNumericOption('yRangePad') / this.plotter_.area.h; } if (series.length === 0) { @@ -2721,7 +2812,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { // 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.attr_("avoidMinZero")) { + if (ypadCompat && !this.getBooleanOption("avoidMinZero")) { if (minAxisY < 0 && minY >= 0) minAxisY = 0; if (maxAxisY > 0 && maxY <= 0) maxAxisY = 0; } @@ -2754,7 +2845,7 @@ Dygraph.prototype.computeYAxisRanges_ = function(extremes) { } - if(independentTicks) { + if (independentTicks) { axis.independentTicks = independentTicks; var opts = this.optionsViewForAxis_('y' + (i ? '2' : '')); var ticker = opts('ticker'); @@ -2800,183 +2891,6 @@ 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 = []; - var errorBars = this.attr_("errorBars"); - var customBars = this.attr_("customBars"); - 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; - } - } - // Fix null points to fit the display type standard. - if(point !== null) { - series.push([x, point]); - } else { - series.push([x, errorBars ? [null, null] : customBars ? [null, null, null] : 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) { - 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 { - // This explicitly preserves NaNs to aid with "independent series". - // See testRollingAveragePreservesNaNs. - var v = (rollPeriod == 1) ? originalData[i][1][0] : null; - rollingData[i] = [originalData[i][0], [v, v, v]]; - } - } - } - } - - 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. @@ -3072,7 +2986,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'; } @@ -3101,7 +3015,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); @@ -3121,7 +3035,7 @@ Dygraph.prototype.parseCSV_ = function(data) { this.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 ' + @@ -3132,7 +3046,7 @@ Dygraph.prototype.parseCSV_ = function(data) { fields[(j + 1) / 2] = [this.parseFloat_(inFields[j], i, line), this.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]; @@ -3315,7 +3229,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)) { @@ -3335,7 +3249,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; @@ -3357,7 +3271,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)); @@ -3431,7 +3345,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) { @@ -3554,6 +3477,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; }; @@ -3594,16 +3521,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_); - } - this.createDragInterface_(); + // 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_(); } @@ -3626,18 +3546,21 @@ 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(); @@ -3671,7 +3594,7 @@ Dygraph.prototype.setAnnotations = function(ann, suppressDraw) { this.annotations_ = ann; if (!this.layout_) { this.warn("Tried to setAnnotations before dygraph was ready. " + - "Try setting them in a drawCallback. See " + + "Try setting them in a ready() block. See " + "dygraphs.com/tests/annotation.html"); return; } @@ -3709,12 +3632,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); + } }; /** @@ -3758,6 +3692,3 @@ Dygraph.addAnnotationRule = function() { this.warn("Unable to add default annotation CSS rule; display may be off."); }; - -// Older pages may still use this name. -var DateGraph = Dygraph;