X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-canvas.js;h=a688c43975958639e973715cdcbc5727b18712c9;hb=2e4626574edc0a30cc3059694b5d04193e0f32ae;hp=5b7c97312c389a58e452446c37fe1856a011f286;hpb=e60234cd5ea3a1c0cc2c97d711d8ae72c9cd2fba;p=dygraphs.git diff --git a/dygraph-canvas.js b/dygraph-canvas.js index 5b7c973..a688c43 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -24,8 +24,8 @@ * @constructor */ -/*jshint globalstrict: true */ -/*global Dygraph:false,RGBColor:false */ +var DygraphCanvasRenderer = (function() { +/*global Dygraph:false */ "use strict"; @@ -39,8 +39,8 @@ * The chart canvas has already been created by the Dygraph object. The * renderer simply gets a drawing context. * - * @param {Dyraph} dygraph The chart to which this renderer belongs. - * @param {Canvas} element The <canvas> DOM element on which to draw. + * @param {Dygraph} dygraph The chart to which this renderer belongs. + * @param {HTMLCanvasElement} element The <canvas> DOM element on which to draw. * @param {CanvasRenderingContext2D} elementContext The drawing context. * @param {DygraphLayout} layout The chart's DygraphLayout object. * @@ -52,43 +52,33 @@ var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) { this.layout = layout; this.element = element; this.elementContext = elementContext; - this.container = this.element.parentNode; - this.height = this.element.height; - this.width = this.element.width; + this.height = dygraph.height_; + this.width = dygraph.width_; // --- check whether everything is ok before we return - if (!this.isIE && !(DygraphCanvasRenderer.isSupported(this.element))) - throw "Canvas is not supported."; + if (!Dygraph.isCanvasSupported(this.element)) { + throw "Canvas is not supported."; + } // internal state this.area = layout.getPlotArea(); - this.container.style.position = "relative"; - this.container.style.width = this.width + "px"; // Set up a clipping area for the canvas (and the interaction canvas). // This ensures that we don't overdraw. - if (this.dygraph_.isUsingExcanvas_) { - this._createIEClipArea(); - } else { - // on Android 3 and 4, setting a clipping area on a canvas prevents it from - // displaying anything. - if (!Dygraph.isAndroid()) { - var ctx = this.dygraph_.canvas_ctx_; - ctx.beginPath(); - ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h); - ctx.clip(); - - ctx = this.dygraph_.hidden_ctx_; - ctx.beginPath(); - ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h); - ctx.clip(); - } - } -}; + // on Android 3 and 4, setting a clipping area on a canvas prevents it from + // displaying anything. + if (!Dygraph.isAndroid()) { + var ctx = this.dygraph_.canvas_ctx_; + ctx.beginPath(); + ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h); + ctx.clip(); -DygraphCanvasRenderer.prototype.attr_ = function(x) { - return this.dygraph_.attr_(x); + ctx = this.dygraph_.hidden_ctx_; + ctx.beginPath(); + ctx.rect(this.area.x, this.area.y, this.area.w, this.area.h); + ctx.clip(); + } }; /** @@ -98,50 +88,7 @@ DygraphCanvasRenderer.prototype.attr_ = function(x) { * @private */ DygraphCanvasRenderer.prototype.clear = function() { - var context; - if (this.isIE) { - // VML takes a while to start up, so we just poll every this.IEDelay - try { - if (this.clearDelay) { - this.clearDelay.cancel(); - this.clearDelay = null; - } - context = this.elementContext; - } - catch (e) { - // TODO(danvk): this is broken, since MochiKit.Async is gone. - // this.clearDelay = MochiKit.Async.wait(this.IEDelay); - // this.clearDelay.addCallback(bind(this.clear, this)); - return; - } - } - - context = this.elementContext; - context.clearRect(0, 0, this.width, this.height); -}; - -/** - * Checks whether the browser supports the <canvas> tag. - * @private - */ -DygraphCanvasRenderer.isSupported = function(canvasName) { - var canvas = null; - try { - if (typeof(canvasName) == 'undefined' || canvasName === null) { - canvas = document.createElement("canvas"); - } else { - canvas = canvasName; - } - canvas.getContext("2d"); - } - catch (e) { - var ie = navigator.appVersion.match(/MSIE (\d\.\d)/); - var opera = (navigator.userAgent.toLowerCase().indexOf("opera") != -1); - if ((!ie) || (ie[1] < 6) || (opera)) - return false; - return true; - } - return true; + this.elementContext.clearRect(0, 0, this.width, this.height); }; /** @@ -152,79 +99,13 @@ DygraphCanvasRenderer.isSupported = function(canvasName) { * @private */ DygraphCanvasRenderer.prototype.render = function() { - this._renderLineChart(); -}; - -DygraphCanvasRenderer.prototype._createIEClipArea = function() { - var className = 'dygraph-clip-div'; - var graphDiv = this.dygraph_.graphDiv; - - // Remove old clip divs. - for (var i = graphDiv.childNodes.length-1; i >= 0; i--) { - if (graphDiv.childNodes[i].className == className) { - graphDiv.removeChild(graphDiv.childNodes[i]); - } - } + // attaches point.canvas{x,y} + this._updatePoints(); - // Determine background color to give clip divs. - var backgroundColor = document.bgColor; - var element = this.dygraph_.graphDiv; - while (element != document) { - var bgcolor = element.currentStyle.backgroundColor; - if (bgcolor && bgcolor != 'transparent') { - backgroundColor = bgcolor; - break; - } - element = element.parentNode; - } - - function createClipDiv(area) { - if (area.w === 0 || area.h === 0) { - return; - } - var elem = document.createElement('div'); - elem.className = className; - elem.style.backgroundColor = backgroundColor; - elem.style.position = 'absolute'; - elem.style.left = area.x + 'px'; - elem.style.top = area.y + 'px'; - elem.style.width = area.w + 'px'; - elem.style.height = area.h + 'px'; - graphDiv.appendChild(elem); - } - - var plotArea = this.area; - // Left side - createClipDiv({ - x:0, y:0, - w:plotArea.x, - h:this.height - }); - - // Top - createClipDiv({ - x: plotArea.x, y: 0, - w: this.width - plotArea.x, - h: plotArea.y - }); - - // Right side - createClipDiv({ - x: plotArea.x + plotArea.w, y: 0, - w: this.width-plotArea.x - plotArea.w, - h: this.height - }); - - // Bottom - createClipDiv({ - x: plotArea.x, - y: plotArea.y + plotArea.h, - w: this.width - plotArea.x, - h: this.height - plotArea.h - plotArea.y - }); + // actually draws the chart. + this._renderLineChart(); }; - /** * Returns a predicate to be used with an iterator, which will * iterate over points appropriately, depending on whether @@ -232,9 +113,9 @@ DygraphCanvasRenderer.prototype._createIEClipArea = function() { * skip over points with missing yVals. */ DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) { - return connectSeparatedPoints - ? DygraphCanvasRenderer._predicateThatSkipsEmptyPoints - : null; + return connectSeparatedPoints ? + DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : + null; }; DygraphCanvasRenderer._predicateThatSkipsEmptyPoints = @@ -243,35 +124,41 @@ DygraphCanvasRenderer._predicateThatSkipsEmptyPoints = }; /** - * + * Draws a line with the styles passed in and calls all the drawPointCallbacks. + * @param {Object} e The dictionary passed to the plotter function. * @private */ -DygraphCanvasRenderer.prototype._drawStyledLine = function( - ctx, i, setName, color, strokeWidth, strokePattern, drawPoints, +DygraphCanvasRenderer._drawStyledLine = function(e, + color, strokeWidth, strokePattern, drawPoints, drawPointCallback, pointSize) { + var g = e.dygraph; // TODO(konigsberg): Compute attributes outside this method call. - var stepPlot = this.attr_("stepPlot"); - var firstIndexInSet = this.layout.setPointsOffsets[i]; - var setLength = this.layout.setPointsLengths[i]; - var points = this.layout.points; + var stepPlot = g.getBooleanOption("stepPlot", e.setName); + if (!Dygraph.isArrayLike(strokePattern)) { strokePattern = null; } - var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName); - var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + var drawGapPoints = g.getBooleanOption('drawGapEdgePoints', e.setName); + + var points = e.points; + var setName = e.setName; + var iter = Dygraph.createIterator(points, 0, points.length, DygraphCanvasRenderer._getIteratorPredicate( - this.attr_("connectSeparatedPoints"))); + g.getBooleanOption("connectSeparatedPoints", setName))); var stroking = strokePattern && (strokePattern.length >= 2); + var ctx = e.drawingContext; ctx.save(); if (stroking) { ctx.installPattern(strokePattern); } - var pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color); - this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize); + var pointsOnLine = DygraphCanvasRenderer._drawSeries( + e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color); + DygraphCanvasRenderer._drawPointsOnLine( + e, pointsOnLine, drawPointCallback, color, pointSize); if (stroking) { ctx.uninstallPattern(); @@ -280,19 +167,16 @@ DygraphCanvasRenderer.prototype._drawStyledLine = function( ctx.restore(); }; -DygraphCanvasRenderer.prototype._drawPointsOnLine = function(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize) { - for (var idx = 0; idx < pointsOnLine.length; idx++) { - var cb = pointsOnLine[idx]; - ctx.save(); - drawPointCallback( - this.dygraph_, setName, ctx, cb[0], cb[1], color, pointSize); - ctx.restore(); - } -} - -DygraphCanvasRenderer.prototype._drawSeries = function( - ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, - stepPlot, color) { +/** + * This does the actual drawing of lines on the canvas, for just one series. + * Returns a list of [canvasx, canvasy] pairs for points for which a + * drawPointCallback should be fired. These include isolated points, or all + * points if drawPoints=true. + * @param {Object} e The dictionary passed to the plotter function. + * @private + */ +DygraphCanvasRenderer._drawSeries = function(e, + iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color) { var prevCanvasX = null; var prevCanvasY = null; @@ -302,6 +186,7 @@ DygraphCanvasRenderer.prototype._drawSeries = function( var pointsOnLine = []; // Array of [canvasx, canvasy] pairs. var first = true; // the first cycle through the while loop + var ctx = e.drawingContext; ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = strokeWidth; @@ -321,18 +206,21 @@ DygraphCanvasRenderer.prototype._drawSeries = function( point = arr[i]; } + // FIXME: The 'canvasy != canvasy' test here catches NaN values but the test + // doesn't catch Infinity values. Could change this to + // !isFinite(point.canvasy), but I assume it avoids isNaN for performance? if (point.canvasy === null || point.canvasy != point.canvasy) { if (stepPlot && prevCanvasX !== null) { // Draw a horizontal line to the start of the missing data - ctx.moveTo(prevX, prevY); - ctx.lineTo(point.canvasx, prevY); + ctx.moveTo(prevCanvasX, prevCanvasY); + ctx.lineTo(point.canvasx, prevCanvasY); } prevCanvasX = prevCanvasY = null; } else { isIsolated = false; if (drawGapPoints || !prevCanvasX) { iter.nextIdx_ = i; - var peek = iter.next(); + iter.next(); nextCanvasY = iter.hasNext ? iter.peek.canvasy : null; var isNextCanvasYNullOrNaN = nextCanvasY === null || @@ -353,16 +241,15 @@ DygraphCanvasRenderer.prototype._drawSeries = function( if (stepPlot) { ctx.moveTo(prevCanvasX, prevCanvasY); ctx.lineTo(point.canvasx, prevCanvasY); - prevCanvasX = point.canvasx; } - // TODO(danvk): this moveTo is rarely necessary - ctx.moveTo(prevCanvasX, prevCanvasY); ctx.lineTo(point.canvasx, point.canvasy); } + } else { + ctx.moveTo(point.canvasx, point.canvasy); } if (drawPoints || isIsolated) { - pointsOnLine.push([point.canvasx, point.canvasy]); + pointsOnLine.push([point.canvasx, point.canvasy, point.idx]); } prevCanvasX = point.canvasx; prevCanvasY = point.canvasy; @@ -373,49 +260,30 @@ DygraphCanvasRenderer.prototype._drawSeries = function( return pointsOnLine; }; -DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) { - var setNames = this.layout.setNames; - var setName = setNames[i]; - - var strokeWidth = this.dygraph_.attr_("strokeWidth", setName); - var borderWidth = this.dygraph_.attr_("strokeBorderWidth", setName); - var drawPointCallback = this.dygraph_.attr_("drawPointCallback", setName) || - Dygraph.Circles.DEFAULT; - - if (borderWidth && strokeWidth) { - this._drawStyledLine(ctx, i, setName, - this.dygraph_.attr_("strokeBorderColor", setName), - strokeWidth + 2 * borderWidth, - this.dygraph_.attr_("strokePattern", setName), - this.dygraph_.attr_("drawPoints", setName), - drawPointCallback, - this.dygraph_.attr_("pointSize", setName)); +/** + * This fires the drawPointCallback functions, which draw dots on the points by + * default. This gets used when the "drawPoints" option is set, or when there + * are isolated points. + * @param {Object} e The dictionary passed to the plotter function. + * @private + */ +DygraphCanvasRenderer._drawPointsOnLine = function( + e, pointsOnLine, drawPointCallback, color, pointSize) { + var ctx = e.drawingContext; + for (var idx = 0; idx < pointsOnLine.length; idx++) { + var cb = pointsOnLine[idx]; + ctx.save(); + drawPointCallback.call(e.dygraph, + e.dygraph, e.setName, ctx, cb[0], cb[1], color, pointSize, cb[2]); + ctx.restore(); } - - this._drawStyledLine(ctx, i, setName, - this.colors[setName], - strokeWidth, - this.dygraph_.attr_("strokePattern", setName), - this.dygraph_.attr_("drawPoints", setName), - drawPointCallback, - this.dygraph_.attr_("pointSize", setName)); }; /** - * Actually draw the lines chart, including error bars. + * Attaches canvas coordinates to the points array. * @private */ -DygraphCanvasRenderer.prototype._renderLineChart = function() { - var ctx = this.elementContext; - var errorBars = this.attr_("errorBars") || this.attr_("customBars"); - var fillGraph = this.attr_("fillGraph"); - var i; - - var setNames = this.layout.setNames; - var setCount = setNames.length; - - this.colors = this.dygraph_.colorsMap_; - +DygraphCanvasRenderer.prototype._updatePoints = function() { // Update Points // TODO(danvk): here // @@ -428,169 +296,506 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { // unaffected. An alternative is to reduce the stroke width in the // transformed coordinate space, but you can't specify different values for // each dimension (as you can with .scale()). The speedup here is ~12%. - var points = this.layout.points; - for (i = points.length; i--;) { - var point = points[i]; - point.canvasx = this.area.w * point.x + this.area.x; - point.canvasy = this.area.h * point.y + this.area.y; + var sets = this.layout.points; + for (var i = sets.length; i--;) { + var points = sets[i]; + for (var j = points.length; j--;) { + var point = points[j]; + point.canvasx = this.area.w * point.x + this.area.x; + point.canvasy = this.area.h * point.y + this.area.y; + } + } +}; + +/** + * Add canvas Actually draw the lines chart, including error bars. + * + * This function can only be called if DygraphLayout's points array has been + * updated with canvas{x,y} attributes, i.e. by + * DygraphCanvasRenderer._updatePoints. + * + * @param {string=} opt_seriesName when specified, only that series will + * be drawn. (This is used for expedited redrawing with highlightSeriesOpts) + * @param {CanvasRenderingContext2D} opt_ctx when specified, the drawing + * context. However, lines are typically drawn on the object's + * elementContext. + * @private + */ +DygraphCanvasRenderer.prototype._renderLineChart = function(opt_seriesName, opt_ctx) { + var ctx = opt_ctx || this.elementContext; + var i; + + var sets = this.layout.points; + var setNames = this.layout.setNames; + var setName; + + this.colors = this.dygraph_.colorsMap_; + + // Determine which series have specialized plotters. + var plotter_attr = this.dygraph_.getOption("plotter"); + var plotters = plotter_attr; + if (!Dygraph.isArrayLike(plotters)) { + plotters = [plotters]; + } + + var setPlotters = {}; // series name -> plotter fn. + for (i = 0; i < setNames.length; i++) { + setName = setNames[i]; + var setPlotter = this.dygraph_.getOption("plotter", setName); + if (setPlotter == plotter_attr) continue; // not specialized. + + setPlotters[setName] = setPlotter; } - // Draw any "fills", i.e. error bars or the filled area under a series. - // These must all be drawn before any lines, so that the main lines of a - // series are drawn on top. - if (errorBars) { - if (fillGraph) { - this.dygraph_.warn("Can't use fillGraph option with error bars"); + for (i = 0; i < plotters.length; i++) { + var plotter = plotters[i]; + var is_last = (i == plotters.length - 1); + + for (var j = 0; j < sets.length; j++) { + setName = setNames[j]; + if (opt_seriesName && setName != opt_seriesName) continue; + + var points = sets[j]; + + // Only throw in the specialized plotters on the last iteration. + var p = plotter; + if (setName in setPlotters) { + if (is_last) { + p = setPlotters[setName]; + } else { + // Don't use the standard plotters in this case. + continue; + } + } + + var color = this.colors[setName]; + var strokeWidth = this.dygraph_.getOption("strokeWidth", setName); + + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = strokeWidth; + p({ + points: points, + setName: setName, + drawingContext: ctx, + color: color, + strokeWidth: strokeWidth, + dygraph: this.dygraph_, + axis: this.dygraph_.axisPropertiesForSeries(setName), + plotArea: this.area, + seriesIndex: j, + seriesCount: sets.length, + singleSeriesName: opt_seriesName, + allSeriesPoints: sets + }); + ctx.restore(); } + } +}; - ctx.save(); - this.drawErrorBars_(points); - ctx.restore(); - } else if (fillGraph) { - ctx.save(); - this.drawFillBars_(points); - ctx.restore(); +/** + * Standard plotters. These may be used by clients via Dygraph.Plotters. + * See comments there for more details. + */ +DygraphCanvasRenderer._Plotters = { + linePlotter: function(e) { + DygraphCanvasRenderer._linePlotter(e); + }, + + fillPlotter: function(e) { + DygraphCanvasRenderer._fillPlotter(e); + }, + + errorPlotter: function(e) { + DygraphCanvasRenderer._errorPlotter(e); } +}; + +/** + * Plotter which draws the central lines for a series. + * @private + */ +DygraphCanvasRenderer._linePlotter = function(e) { + var g = e.dygraph; + var setName = e.setName; + var strokeWidth = e.strokeWidth; + + // TODO(danvk): Check if there's any performance impact of just calling + // getOption() inside of _drawStyledLine. Passing in so many parameters makes + // this code a bit nasty. + var borderWidth = g.getNumericOption("strokeBorderWidth", setName); + var drawPointCallback = g.getOption("drawPointCallback", setName) || + Dygraph.Circles.DEFAULT; + var strokePattern = g.getOption("strokePattern", setName); + var drawPoints = g.getBooleanOption("drawPoints", setName); + var pointSize = g.getNumericOption("pointSize", setName); - // Drawing the lines. - for (i = 0; i < setCount; i += 1) { - this._drawLine(ctx, i); + if (borderWidth && strokeWidth) { + DygraphCanvasRenderer._drawStyledLine(e, + g.getOption("strokeBorderColor", setName), + strokeWidth + 2 * borderWidth, + strokePattern, + drawPoints, + drawPointCallback, + pointSize + ); } + + DygraphCanvasRenderer._drawStyledLine(e, + e.color, + strokeWidth, + strokePattern, + drawPoints, + drawPointCallback, + pointSize + ); }; /** * Draws the shaded error bars/confidence intervals for each series. * This happens before the center lines are drawn, since the center lines * need to be drawn on top of the error bars for all series. - * * @private */ -DygraphCanvasRenderer.prototype.drawErrorBars_ = function(points) { - var ctx = this.elementContext; - var setNames = this.layout.setNames; - var setCount = setNames.length; - var fillAlpha = this.attr_('fillAlpha'); - var stepPlot = this.attr_('stepPlot'); +DygraphCanvasRenderer._errorPlotter = function(e) { + var g = e.dygraph; + var setName = e.setName; + var errorBars = g.getBooleanOption("errorBars") || + g.getBooleanOption("customBars"); + if (!errorBars) return; + + var fillGraph = g.getBooleanOption("fillGraph", setName); + if (fillGraph) { + console.warn("Can't use fillGraph option with error bars"); + } + + var ctx = e.drawingContext; + var color = e.color; + var fillAlpha = g.getNumericOption('fillAlpha', setName); + var stepPlot = g.getBooleanOption("stepPlot", setName); + var points = e.points; + + var iter = Dygraph.createIterator(points, 0, points.length, + DygraphCanvasRenderer._getIteratorPredicate( + g.getBooleanOption("connectSeparatedPoints", setName))); var newYs; - for (var i = 0; i < setCount; i++) { - var setName = setNames[i]; - var axis = this.dygraph_.axisPropertiesForSeries(setName); - var color = this.colors[setName]; + // setup graphics context + var prevX = NaN; + var prevY = NaN; + var prevYs = [-1, -1]; + // should be same color as the lines but only 15% opaque. + var rgb = Dygraph.toRGB_(color); + var err_color = + 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')'; + ctx.fillStyle = err_color; + ctx.beginPath(); - var firstIndexInSet = this.layout.setPointsOffsets[i]; - var setLength = this.layout.setPointsLengths[i]; + var isNullUndefinedOrNaN = function(x) { + return (x === null || + x === undefined || + isNaN(x)); + }; + + while (iter.hasNext) { + var point = iter.next(); + if ((!stepPlot && isNullUndefinedOrNaN(point.y)) || + (stepPlot && !isNaN(prevY) && isNullUndefinedOrNaN(prevY))) { + prevX = NaN; + continue; + } - var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, - DygraphCanvasRenderer._getIteratorPredicate( - this.attr_("connectSeparatedPoints"))); + newYs = [ point.y_bottom, point.y_top ]; + if (stepPlot) { + prevY = point.y; + } - // setup graphics context - var prevX = NaN; - var prevY = NaN; - var prevYs = [-1, -1]; - var yscale = axis.yscale; - // should be same color as the lines but only 15% opaque. - var rgb = new RGBColor(color); - var err_color = - 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')'; - ctx.fillStyle = err_color; - ctx.beginPath(); - while (iter.hasNext) { - var point = iter.next(); - if (!Dygraph.isOK(point.y)) { - prevX = NaN; - continue; - } + // The documentation specifically disallows nulls inside the point arrays, + // but in case it happens we should do something sensible. + if (isNaN(newYs[0])) newYs[0] = point.y; + if (isNaN(newYs[1])) newYs[1] = point.y; + newYs[0] = e.plotArea.h * newYs[0] + e.plotArea.y; + newYs[1] = e.plotArea.h * newYs[1] + e.plotArea.y; + if (!isNaN(prevX)) { if (stepPlot) { - newYs = [ point.y_bottom, point.y_top ]; - prevY = point.y; + ctx.moveTo(prevX, prevYs[0]); + ctx.lineTo(point.canvasx, prevYs[0]); + ctx.lineTo(point.canvasx, prevYs[1]); } else { - newYs = [ point.y_bottom, point.y_top ]; - } - newYs[0] = this.area.h * newYs[0] + this.area.y; - newYs[1] = this.area.h * newYs[1] + this.area.y; - if (!isNaN(prevX)) { - if (stepPlot) { - ctx.moveTo(prevX, newYs[0]); - } else { - ctx.moveTo(prevX, prevYs[0]); - } + ctx.moveTo(prevX, prevYs[0]); ctx.lineTo(point.canvasx, newYs[0]); ctx.lineTo(point.canvasx, newYs[1]); - if (stepPlot) { - ctx.lineTo(prevX, newYs[1]); + } + ctx.lineTo(prevX, prevYs[1]); + ctx.closePath(); + } + prevYs = newYs; + prevX = point.canvasx; + } + ctx.fill(); +}; + + +/** + * Proxy for CanvasRenderingContext2D which drops moveTo/lineTo calls which are + * superfluous. It accumulates all movements which haven't changed the x-value + * and only applies the two with the most extreme y-values. + * + * Calls to lineTo/moveTo must have non-decreasing x-values. + */ +DygraphCanvasRenderer._fastCanvasProxy = function(context) { + var pendingActions = []; // array of [type, x, y] tuples + var lastRoundedX = null; + + var LINE_TO = 1, + MOVE_TO = 2; + + var actionCount = 0; // number of moveTos and lineTos passed to context. + + // Drop superfluous motions + // Assumes all pendingActions have the same (rounded) x-value. + var compressActions = function(opt_losslessOnly) { + if (pendingActions.length <= 1) return; + + // Lossless compression: drop inconsequential moveTos. + for (var i = pendingActions.length - 1; i > 0; i--) { + var action = pendingActions[i]; + if (action[0] == MOVE_TO) { + var prevAction = pendingActions[i - 1]; + if (prevAction[1] == action[1] && prevAction[2] == action[2]) { + pendingActions.splice(i, 1); + } + } + } + + // Lossless compression: ... drop consecutive moveTos ... + for (var i = 0; i < pendingActions.length - 1; /* incremented internally */) { + var action = pendingActions[i]; + if (action[0] == MOVE_TO && pendingActions[i + 1][0] == MOVE_TO) { + pendingActions.splice(i, 1); + } else { + i++; + } + } + + // Lossy compression: ... drop all but the extreme y-values ... + if (pendingActions.length > 2 && !opt_losslessOnly) { + // keep an initial moveTo, but drop all others. + var startIdx = 0; + if (pendingActions[0][0] == MOVE_TO) startIdx++; + var minIdx = null, maxIdx = null; + for (var i = startIdx; i < pendingActions.length; i++) { + var action = pendingActions[i]; + if (action[0] != LINE_TO) continue; + if (minIdx === null && maxIdx === null) { + minIdx = i; + maxIdx = i; } else { - ctx.lineTo(prevX, prevYs[1]); + var y = action[2]; + if (y < pendingActions[minIdx][2]) { + minIdx = i; + } else if (y > pendingActions[maxIdx][2]) { + maxIdx = i; + } } - ctx.closePath(); } - prevYs = newYs; - prevX = point.canvasx; + var minAction = pendingActions[minIdx], + maxAction = pendingActions[maxIdx]; + pendingActions.splice(startIdx, pendingActions.length - startIdx); + if (minIdx < maxIdx) { + pendingActions.push(minAction); + pendingActions.push(maxAction); + } else if (minIdx > maxIdx) { + pendingActions.push(maxAction); + pendingActions.push(minAction); + } else { + pendingActions.push(minAction); + } } - ctx.fill(); - } + }; + + var flushActions = function(opt_noLossyCompression) { + compressActions(opt_noLossyCompression); + for (var i = 0, len = pendingActions.length; i < len; i++) { + var action = pendingActions[i]; + if (action[0] == LINE_TO) { + context.lineTo(action[1], action[2]); + } else if (action[0] == MOVE_TO) { + context.moveTo(action[1], action[2]); + } + } + actionCount += pendingActions.length; + pendingActions = []; + }; + + var addAction = function(action, x, y) { + var rx = Math.round(x); + if (lastRoundedX === null || rx != lastRoundedX) { + flushActions(); + lastRoundedX = rx; + } + pendingActions.push([action, x, y]); + }; + + return { + moveTo: function(x, y) { + addAction(MOVE_TO, x, y); + }, + lineTo: function(x, y) { + addAction(LINE_TO, x, y); + }, + + // for major operations like stroke/fill, we skip compression to ensure + // that there are no artifacts at the right edge. + stroke: function() { flushActions(true); context.stroke(); }, + fill: function() { flushActions(true); context.fill(); }, + beginPath: function() { flushActions(true); context.beginPath(); }, + closePath: function() { flushActions(true); context.closePath(); }, + + _count: function() { return actionCount; } + }; }; /** * Draws the shaded regions when "fillGraph" is set. Not to be confused with * error bars. * + * For stacked charts, it's more convenient to handle all the series + * simultaneously. So this plotter plots all the points on the first series + * it's asked to draw, then ignores all the other series. + * * @private */ -DygraphCanvasRenderer.prototype.drawFillBars_ = function(points) { - var ctx = this.elementContext; - var setNames = this.layout.setNames; - var setCount = setNames.length; - var fillAlpha = this.attr_('fillAlpha'); - var stepPlot = this.attr_('stepPlot'); - var stackedGraph = this.attr_("stackedGraph"); +DygraphCanvasRenderer._fillPlotter = function(e) { + // Skip if we're drawing a single series for interactive highlight overlay. + if (e.singleSeriesName) return; + + // We'll handle all the series at once, not one-by-one. + if (e.seriesIndex !== 0) return; + + var g = e.dygraph; + var setNames = g.getLabels().slice(1); // remove x-axis + + // getLabels() includes names for invisible series, which are not included in + // allSeriesPoints. We remove those to make the two match. + // TODO(danvk): provide a simpler way to get this information. + for (var i = setNames.length; i >= 0; i--) { + if (!g.visibility()[i]) setNames.splice(i, 1); + } + + var anySeriesFilled = (function() { + for (var i = 0; i < setNames.length; i++) { + if (g.getBooleanOption("fillGraph", setNames[i])) return true; + } + return false; + })(); - var baseline = {}; // for stacked graphs: baseline for filling + if (!anySeriesFilled) return; + + var area = e.plotArea; + var sets = e.allSeriesPoints; + var setCount = sets.length; + + var fillAlpha = g.getNumericOption('fillAlpha'); + var stackedGraph = g.getBooleanOption("stackedGraph"); + var colors = g.getColors(); + + // For stacked graphs, track the baseline for filling. + // + // The filled areas below graph lines are trapezoids with two + // vertical edges. The top edge is the line segment being drawn, and + // the baseline is the bottom edge. Each baseline corresponds to the + // top line segment from the previous stacked line. In the case of + // step plots, the trapezoids are rectangles. + var baseline = {}; var currBaseline; + var prevStepPlot; // for different line drawing modes (line/step) per series + + // Helper function to trace a line back along the baseline. + var traceBackPath = function(ctx, baselineX, baselineY, pathBack) { + ctx.lineTo(baselineX, baselineY); + if (stackedGraph) { + for (var i = pathBack.length - 1; i >= 0; i--) { + var pt = pathBack[i]; + ctx.lineTo(pt[0], pt[1]); + } + } + }; // process sets in reverse order (needed for stacked graphs) - for (var i = setCount - 1; i >= 0; i--) { - var setName = setNames[i]; - var color = this.colors[setName]; - var axis = this.dygraph_.axisPropertiesForSeries(setName); + for (var setIdx = setCount - 1; setIdx >= 0; setIdx--) { + var ctx = e.drawingContext; + var setName = setNames[setIdx]; + if (!g.getBooleanOption('fillGraph', setName)) continue; + + var stepPlot = g.getBooleanOption('stepPlot', setName); + var color = colors[setIdx]; + var axis = g.axisPropertiesForSeries(setName); var axisY = 1.0 + axis.minyval * axis.yscale; if (axisY < 0.0) axisY = 0.0; else if (axisY > 1.0) axisY = 1.0; - axisY = this.area.h * axisY + this.area.y; - var firstIndexInSet = this.layout.setPointsOffsets[i]; - var setLength = this.layout.setPointsLengths[i]; + axisY = area.h * axisY + area.y; - var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + var points = sets[setIdx]; + var iter = Dygraph.createIterator(points, 0, points.length, DygraphCanvasRenderer._getIteratorPredicate( - this.attr_("connectSeparatedPoints"))); + g.getBooleanOption("connectSeparatedPoints", setName))); // setup graphics context var prevX = NaN; var prevYs = [-1, -1]; var newYs; - var yscale = axis.yscale; // should be same color as the lines but only 15% opaque. - var rgb = new RGBColor(color); + var rgb = Dygraph.toRGB_(color); var err_color = 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + fillAlpha + ')'; ctx.fillStyle = err_color; ctx.beginPath(); - while(iter.hasNext) { - var point = iter.next(); - if (!Dygraph.isOK(point.y)) { + var last_x, is_first = true; + + // If the point density is high enough, dropping segments on their way to + // the canvas justifies the overhead of doing so. + if (points.length > 2 * g.width_) { + ctx = DygraphCanvasRenderer._fastCanvasProxy(ctx); + } + + // For filled charts, we draw points from left to right, then back along + // the x-axis to complete a shape for filling. + // For stacked plots, this "back path" is a more complex shape. This array + // stores the [x, y] values needed to trace that shape. + var pathBack = []; + + // TODO(danvk): there are a lot of options at play in this loop. + // The logic would be much clearer if some (e.g. stackGraph and + // stepPlot) were split off into separate sub-plotters. + var point; + while (iter.hasNext) { + point = iter.next(); + if (!Dygraph.isOK(point.y) && !stepPlot) { + traceBackPath(ctx, prevX, prevYs[1], pathBack); + pathBack = []; prevX = NaN; + if (point.y_stacked !== null && !isNaN(point.y_stacked)) { + baseline[point.canvasx] = area.h * point.y_stacked + area.y; + } continue; } if (stackedGraph) { + if (!is_first && last_x == point.xval) { + continue; + } else { + is_first = false; + last_x = point.xval; + } + currBaseline = baseline[point.canvasx]; var lastY; if (currBaseline === undefined) { lastY = axisY; } else { - if(stepPlot) { + if(prevStepPlot) { lastY = currBaseline[0]; } else { lastY = currBaseline; @@ -598,10 +803,10 @@ DygraphCanvasRenderer.prototype.drawFillBars_ = function(points) { } newYs = [ point.canvasy, lastY ]; - if(stepPlot) { + if (stepPlot) { // Step plots must keep track of the top and bottom of // the baseline at each point. - if(prevYs[0] === -1) { + if (prevYs[0] === -1) { baseline[point.canvasx] = [ point.canvasy, axisY ]; } else { baseline[point.canvasx] = [ point.canvasy, prevYs[0] ]; @@ -611,30 +816,47 @@ DygraphCanvasRenderer.prototype.drawFillBars_ = function(points) { } } else { - newYs = [ point.canvasy, axisY ]; + if (isNaN(point.canvasy) && stepPlot) { + newYs = [ area.y + area.h, axisY ]; + } else { + newYs = [ point.canvasy, axisY ]; + } } if (!isNaN(prevX)) { - ctx.moveTo(prevX, prevYs[0]); - + // Move to top fill point if (stepPlot) { ctx.lineTo(point.canvasx, prevYs[0]); - if(currBaseline) { - // Draw to the bottom of the baseline - ctx.lineTo(point.canvasx, currBaseline[1]); - } else { - ctx.lineTo(point.canvasx, newYs[1]); - } + ctx.lineTo(point.canvasx, newYs[0]); } else { ctx.lineTo(point.canvasx, newYs[0]); - ctx.lineTo(point.canvasx, newYs[1]); } - ctx.lineTo(prevX, prevYs[1]); - ctx.closePath(); + // Record the baseline for the reverse path. + if (stackedGraph) { + pathBack.push([prevX, prevYs[1]]); + if (prevStepPlot && currBaseline) { + // Draw to the bottom of the baseline + pathBack.push([point.canvasx, currBaseline[1]]); + } else { + pathBack.push([point.canvasx, newYs[1]]); + } + } + } else { + ctx.moveTo(point.canvasx, newYs[1]); + ctx.lineTo(point.canvasx, newYs[0]); } prevYs = newYs; prevX = point.canvasx; } + prevStepPlot = stepPlot; + if (newYs && point) { + traceBackPath(ctx, point.canvasx, newYs[1], pathBack); + pathBack = []; + } ctx.fill(); } }; + +return DygraphCanvasRenderer; + +})();