X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=src%2Fdygraph-canvas.js;fp=src%2Fdygraph-canvas.js;h=a688c43975958639e973715cdcbc5727b18712c9;hb=3123ca57f71d145bb5bcc4a2f754d3dff3225346;hp=0000000000000000000000000000000000000000;hpb=26ee953643ccd2d32e38e6b60b20e6a01c1dc9ba;p=dygraphs.git diff --git a/src/dygraph-canvas.js b/src/dygraph-canvas.js new file mode 100644 index 0000000..a688c43 --- /dev/null +++ b/src/dygraph-canvas.js @@ -0,0 +1,862 @@ +/** + * @license + * Copyright 2006 Dan Vanderkam (danvdk@gmail.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the + * needs of dygraphs. + * + * In particular, support for: + * - grid overlays + * - error bars + * - dygraphs attribute system + */ + +/** + * The DygraphCanvasRenderer class does the actual rendering of the chart onto + * a canvas. It's based on PlotKit.CanvasRenderer. + * @param {Object} element The canvas to attach to + * @param {Object} elementContext The 2d context of the canvas (injected so it + * can be mocked for testing.) + * @param {Layout} layout The DygraphLayout object for this graph. + * @constructor + */ + +var DygraphCanvasRenderer = (function() { +/*global Dygraph:false */ +"use strict"; + + +/** + * @constructor + * + * This gets called when there are "new points" to chart. This is generally the + * case when the underlying data being charted has changed. It is _not_ called + * in the common case that the user has zoomed or is panning the view. + * + * The chart canvas has already been created by the Dygraph object. The + * renderer simply gets a drawing context. + * + * @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. + * + * TODO(danvk): remove the elementContext property. + */ +var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) { + this.dygraph_ = dygraph; + + this.layout = layout; + this.element = element; + this.elementContext = elementContext; + + this.height = dygraph.height_; + this.width = dygraph.width_; + + // --- check whether everything is ok before we return + if (!Dygraph.isCanvasSupported(this.element)) { + throw "Canvas is not supported."; + } + + // internal state + this.area = layout.getPlotArea(); + + // Set up a clipping area for the canvas (and the interaction canvas). + // This ensures that we don't overdraw. + // 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(); + } +}; + +/** + * Clears out all chart content and DOM elements. + * This is called immediately before render() on every frame, including + * during zooms and pans. + * @private + */ +DygraphCanvasRenderer.prototype.clear = function() { + this.elementContext.clearRect(0, 0, this.width, this.height); +}; + +/** + * This method is responsible for drawing everything on the chart, including + * lines, error bars, fills and axes. + * It is called immediately after clear() on every frame, including during pans + * and zooms. + * @private + */ +DygraphCanvasRenderer.prototype.render = function() { + // attaches point.canvas{x,y} + this._updatePoints(); + + // actually draws the chart. + this._renderLineChart(); +}; + +/** + * Returns a predicate to be used with an iterator, which will + * iterate over points appropriately, depending on whether + * connectSeparatedPoints is true. When it's false, the predicate will + * skip over points with missing yVals. + */ +DygraphCanvasRenderer._getIteratorPredicate = function(connectSeparatedPoints) { + return connectSeparatedPoints ? + DygraphCanvasRenderer._predicateThatSkipsEmptyPoints : + null; +}; + +DygraphCanvasRenderer._predicateThatSkipsEmptyPoints = + function(array, idx) { + return array[idx].yval !== null; +}; + +/** + * 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._drawStyledLine = function(e, + color, strokeWidth, strokePattern, drawPoints, + drawPointCallback, pointSize) { + var g = e.dygraph; + // TODO(konigsberg): Compute attributes outside this method call. + var stepPlot = g.getBooleanOption("stepPlot", e.setName); + + if (!Dygraph.isArrayLike(strokePattern)) { + strokePattern = null; + } + + 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( + g.getBooleanOption("connectSeparatedPoints", setName))); + + var stroking = strokePattern && (strokePattern.length >= 2); + + var ctx = e.drawingContext; + ctx.save(); + if (stroking) { + ctx.installPattern(strokePattern); + } + + var pointsOnLine = DygraphCanvasRenderer._drawSeries( + e, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, color); + DygraphCanvasRenderer._drawPointsOnLine( + e, pointsOnLine, drawPointCallback, color, pointSize); + + if (stroking) { + ctx.uninstallPattern(); + } + + ctx.restore(); +}; + +/** + * 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; + var nextCanvasY = null; + var isIsolated; // true if this point is isolated (no line segments) + var point; // the point being processed in the while loop + 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; + + // NOTE: we break the iterator's encapsulation here for about a 25% speedup. + var arr = iter.array_; + var limit = iter.end_; + var predicate = iter.predicate_; + + for (var i = iter.start_; i < limit; i++) { + point = arr[i]; + if (predicate) { + while (i < limit && !predicate(arr, i)) { + i++; + } + if (i == limit) break; + 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(prevCanvasX, prevCanvasY); + ctx.lineTo(point.canvasx, prevCanvasY); + } + prevCanvasX = prevCanvasY = null; + } else { + isIsolated = false; + if (drawGapPoints || !prevCanvasX) { + iter.nextIdx_ = i; + iter.next(); + nextCanvasY = iter.hasNext ? iter.peek.canvasy : null; + + var isNextCanvasYNullOrNaN = nextCanvasY === null || + nextCanvasY != nextCanvasY; + isIsolated = (!prevCanvasX && isNextCanvasYNullOrNaN); + if (drawGapPoints) { + // Also consider a point to be "isolated" if it's adjacent to a + // null point, excluding the graph edges. + if ((!first && !prevCanvasX) || + (iter.hasNext && isNextCanvasYNullOrNaN)) { + isIsolated = true; + } + } + } + + if (prevCanvasX !== null) { + if (strokeWidth) { + if (stepPlot) { + ctx.moveTo(prevCanvasX, prevCanvasY); + ctx.lineTo(point.canvasx, prevCanvasY); + } + + ctx.lineTo(point.canvasx, point.canvasy); + } + } else { + ctx.moveTo(point.canvasx, point.canvasy); + } + if (drawPoints || isIsolated) { + pointsOnLine.push([point.canvasx, point.canvasy, point.idx]); + } + prevCanvasX = point.canvasx; + prevCanvasY = point.canvasy; + } + first = false; + } + ctx.stroke(); + return pointsOnLine; +}; + +/** + * 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(); + } +}; + +/** + * Attaches canvas coordinates to the points array. + * @private + */ +DygraphCanvasRenderer.prototype._updatePoints = function() { + // Update Points + // TODO(danvk): here + // + // TODO(bhs): this loop is a hot-spot for high-point-count charts. These + // transformations can be pushed into the canvas via linear transformation + // matrices. + // NOTE(danvk): this is trickier than it sounds at first. The transformation + // needs to be done before the .moveTo() and .lineTo() calls, but must be + // undone before the .stroke() call to ensure that the stroke width is + // 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 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; + } + + 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(); + } + } +}; + +/** + * 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); + + 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._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; + + // 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 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; + } + + newYs = [ point.y_bottom, point.y_top ]; + if (stepPlot) { + prevY = point.y; + } + + // 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) { + ctx.moveTo(prevX, prevYs[0]); + ctx.lineTo(point.canvasx, prevYs[0]); + ctx.lineTo(point.canvasx, prevYs[1]); + } else { + ctx.moveTo(prevX, prevYs[0]); + ctx.lineTo(point.canvasx, newYs[0]); + ctx.lineTo(point.canvasx, 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 { + var y = action[2]; + if (y < pendingActions[minIdx][2]) { + minIdx = i; + } else if (y > pendingActions[maxIdx][2]) { + maxIdx = i; + } + } + } + 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); + } + } + }; + + 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._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; + })(); + + 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 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 = area.h * axisY + area.y; + + var points = sets[setIdx]; + var iter = Dygraph.createIterator(points, 0, points.length, + DygraphCanvasRenderer._getIteratorPredicate( + g.getBooleanOption("connectSeparatedPoints", setName))); + + // setup graphics context + var prevX = NaN; + var prevYs = [-1, -1]; + var newYs; + // 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 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(prevStepPlot) { + lastY = currBaseline[0]; + } else { + lastY = currBaseline; + } + } + newYs = [ point.canvasy, lastY ]; + + if (stepPlot) { + // Step plots must keep track of the top and bottom of + // the baseline at each point. + if (prevYs[0] === -1) { + baseline[point.canvasx] = [ point.canvasy, axisY ]; + } else { + baseline[point.canvasx] = [ point.canvasy, prevYs[0] ]; + } + } else { + baseline[point.canvasx] = point.canvasy; + } + + } else { + if (isNaN(point.canvasy) && stepPlot) { + newYs = [ area.y + area.h, axisY ]; + } else { + newYs = [ point.canvasy, axisY ]; + } + } + if (!isNaN(prevX)) { + // Move to top fill point + if (stepPlot) { + ctx.lineTo(point.canvasx, prevYs[0]); + ctx.lineTo(point.canvasx, newYs[0]); + } else { + ctx.lineTo(point.canvasx, newYs[0]); + } + + // 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; + +})();