X-Git-Url: https://adrianiainlam.tk/git/?a=blobdiff_plain;f=dygraph-canvas.js;h=5326673b1d466594a7e7bd35b4de2e8edb22e642;hb=beeabac2f71ae45c510b0355d0e76972083a6ca6;hp=0c44e8479ef4ba71ba5a3b535111725d2c9257d2;hpb=cbfcf21db4a2ef0f5db409591146f975ffe7b7f2;p=dygraphs.git diff --git a/dygraph-canvas.js b/dygraph-canvas.js index 0c44e84..5326673 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -29,6 +29,23 @@ "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 {Dyraph} dygraph The chart to which this renderer belongs. + * @param {Canvas} 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; @@ -45,11 +62,6 @@ var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) { throw "Canvas is not supported."; // internal state - this.xlabels = []; - this.ylabels = []; - this.annotations = []; - this.chartLabels = {}; - this.area = layout.getPlotArea(); this.container.style.position = "relative"; this.container.style.width = this.width + "px"; @@ -79,6 +91,12 @@ DygraphCanvasRenderer.prototype.attr_ = function(x) { return this.dygraph_.attr_(x); }; +/** + * 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() { var context; if (this.isIE) { @@ -100,30 +118,12 @@ DygraphCanvasRenderer.prototype.clear = function() { context = this.elementContext; context.clearRect(0, 0, this.width, this.height); - - function removeArray(ary) { - for (var i = 0; i < ary.length; i++) { - var el = ary[i]; - if (el.parentNode) el.parentNode.removeChild(el); - } - } - - removeArray(this.xlabels); - removeArray(this.ylabels); - removeArray(this.annotations); - - for (var k in this.chartLabels) { - if (!this.chartLabels.hasOwnProperty(k)) continue; - var el = this.chartLabels[k]; - if (el.parentNode) el.parentNode.removeChild(el); - } - this.xlabels = []; - this.ylabels = []; - this.annotations = []; - this.chartLabels = {}; }; - +/** + * Checks whether the browser supports the <canvas> tag. + * @private + */ DygraphCanvasRenderer.isSupported = function(canvasName) { var canvas = null; try { @@ -153,7 +153,11 @@ DygraphCanvasRenderer.prototype.setColors = function(colors) { }; /** - * Draw an X/Y grid on top of the existing plot + * 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() { // Draw the new X/Y grid. Lines appear crisper when pixels are rounded to @@ -208,9 +212,6 @@ DygraphCanvasRenderer.prototype.render = function() { // Do the ordinary rendering, as before this._renderLineChart(); - this._renderAxis(); - this._renderChartLabels(); - this._renderAnnotations(); }; DygraphCanvasRenderer.prototype._createIEClipArea = function() { @@ -282,492 +283,169 @@ DygraphCanvasRenderer.prototype._createIEClipArea = function() { }); }; -DygraphCanvasRenderer.prototype._renderAxis = function() { - if (!this.attr_('drawXAxis') && !this.attr_('drawYAxis')) return; - - // Round pixels to half-integer boundaries for crisper drawing. - function halfUp(x) { return Math.round(x) + 0.5; } - function halfDown(y){ return Math.round(y) - 0.5; } - - var context = this.elementContext; - - var label, x, y, tick, i; - var labelStyle = { - position: "absolute", - fontSize: this.attr_('axisLabelFontSize') + "px", - zIndex: 10, - color: this.attr_('axisLabelColor'), - width: this.attr_('axisLabelWidth') + "px", - // height: this.attr_('axisLabelFontSize') + 2 + "px", - lineHeight: "normal", // Something other than "normal" line-height screws up label positioning. - overflow: "hidden" - }; - var makeDiv = function(txt, axis, prec_axis) { - var div = document.createElement("div"); - for (var name in labelStyle) { - if (labelStyle.hasOwnProperty(name)) { - div.style[name] = labelStyle[name]; - } - } - var inner_div = document.createElement("div"); - inner_div.className = 'dygraph-axis-label' + - ' dygraph-axis-label-' + axis + - (prec_axis ? ' dygraph-axis-label-' + prec_axis : ''); - inner_div.innerHTML=txt; - div.appendChild(inner_div); - return div; - }; - - // axis lines - context.save(); - context.strokeStyle = this.attr_('axisLineColor'); - context.lineWidth = this.attr_('axisLineWidth'); - - if (this.attr_('drawYAxis')) { - if (this.layout.yticks && this.layout.yticks.length > 0) { - var num_axes = this.dygraph_.numAxes(); - for (i = 0; i < this.layout.yticks.length; i++) { - tick = this.layout.yticks[i]; - if (typeof(tick) == "function") return; - x = this.area.x; - var sgn = 1; - var prec_axis = 'y1'; - if (tick[0] == 1) { // right-side y-axis - x = this.area.x + this.area.w; - sgn = -1; - prec_axis = 'y2'; - } - y = this.area.y + tick[1] * this.area.h; - - /* Tick marks are currently clipped, so don't bother drawing them. - context.beginPath(); - context.moveTo(halfUp(x), halfDown(y)); - context.lineTo(halfUp(x - sgn * this.attr_('axisTickSize')), halfDown(y)); - context.closePath(); - context.stroke(); - */ - - label = makeDiv(tick[2], 'y', num_axes == 2 ? prec_axis : null); - var top = (y - this.attr_('axisLabelFontSize') / 2); - if (top < 0) top = 0; - - if (top + this.attr_('axisLabelFontSize') + 3 > this.height) { - label.style.bottom = "0px"; - } else { - label.style.top = top + "px"; - } - if (tick[0] === 0) { - label.style.left = (this.area.x - this.attr_('yAxisLabelWidth') - this.attr_('axisTickSize')) + "px"; - label.style.textAlign = "right"; - } else if (tick[0] == 1) { - label.style.left = (this.area.x + this.area.w + - this.attr_('axisTickSize')) + "px"; - label.style.textAlign = "left"; - } - label.style.width = this.attr_('yAxisLabelWidth') + "px"; - this.container.appendChild(label); - this.ylabels.push(label); - } +/** + * 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; +} - // The lowest tick on the y-axis often overlaps with the leftmost - // tick on the x-axis. Shift the bottom tick up a little bit to - // compensate if necessary. - var bottomTick = this.ylabels[0]; - var fontSize = this.attr_('axisLabelFontSize'); - var bottom = parseInt(bottomTick.style.top, 10) + fontSize; - if (bottom > this.height - fontSize) { - bottomTick.style.top = (parseInt(bottomTick.style.top, 10) - - fontSize / 2) + "px"; - } - } +DygraphCanvasRenderer._predicateThatSkipsEmptyPoints = + function(array, idx) { return array[idx].yval !== null; } - // draw a vertical line on the left to separate the chart from the labels. - context.beginPath(); - context.moveTo(halfUp(this.area.x), halfDown(this.area.y)); - context.lineTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h)); - context.closePath(); - context.stroke(); - - // if there's a secondary y-axis, draw a vertical line for that, too. - if (this.dygraph_.numAxes() == 2) { - context.beginPath(); - context.moveTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y)); - context.lineTo(halfDown(this.area.x + this.area.w), halfDown(this.area.y + this.area.h)); - context.closePath(); - context.stroke(); - } +DygraphCanvasRenderer.prototype._drawStyledLine = function( + ctx, i, setName, color, strokeWidth, strokePattern, drawPoints, + drawPointCallback, pointSize) { + // 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; + if (!Dygraph.isArrayLike(strokePattern)) { + strokePattern = null; } + var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName); - if (this.attr_('drawXAxis')) { - if (this.layout.xticks) { - for (i = 0; i < this.layout.xticks.length; i++) { - tick = this.layout.xticks[i]; - x = this.area.x + tick[0] * this.area.w; - y = this.area.y + this.area.h; - - /* Tick marks are currently clipped, so don't bother drawing them. - context.beginPath(); - context.moveTo(halfUp(x), halfDown(y)); - context.lineTo(halfUp(x), halfDown(y + this.attr_('axisTickSize'))); - context.closePath(); - context.stroke(); - */ - - label = makeDiv(tick[1], 'x'); - label.style.textAlign = "center"; - label.style.top = (y + this.attr_('axisTickSize')) + 'px'; - - var left = (x - this.attr_('axisLabelWidth')/2); - if (left + this.attr_('axisLabelWidth') > this.width) { - left = this.width - this.attr_('xAxisLabelWidth'); - label.style.textAlign = "right"; - } - if (left < 0) { - left = 0; - label.style.textAlign = "left"; - } + ctx.save(); - label.style.left = left + "px"; - label.style.width = this.attr_('xAxisLabelWidth') + "px"; - this.container.appendChild(label); - this.xlabels.push(label); - } - } + var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints"))); - context.beginPath(); - context.moveTo(halfUp(this.area.x), halfDown(this.area.y + this.area.h)); - context.lineTo(halfUp(this.area.x + this.area.w), halfDown(this.area.y + this.area.h)); - context.closePath(); - context.stroke(); + var pointsOnLine; + var strategy; + if (!strokePattern || strokePattern.length <= 1) { + strategy = trivialStrategy(ctx, color, strokeWidth); + } else { + strategy = nonTrivialStrategy(this, ctx, color, strokeWidth, strokePattern); } + pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, strategy); + this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize); - context.restore(); + ctx.restore(); }; - -DygraphCanvasRenderer.prototype._renderChartLabels = function() { - var div, class_div; - - // Generate divs for the chart title, xlabel and ylabel. - // Space for these divs has already been taken away from the charting area in - // the DygraphCanvasRenderer constructor. - if (this.attr_('title')) { - div = document.createElement("div"); - div.style.position = 'absolute'; - div.style.top = '0px'; - div.style.left = this.area.x + 'px'; - div.style.width = this.area.w + 'px'; - div.style.height = this.attr_('titleHeight') + 'px'; - div.style.textAlign = 'center'; - div.style.fontSize = (this.attr_('titleHeight') - 8) + 'px'; - div.style.fontWeight = 'bold'; - class_div = document.createElement("div"); - class_div.className = 'dygraph-label dygraph-title'; - class_div.innerHTML = this.attr_('title'); - div.appendChild(class_div); - this.container.appendChild(div); - this.chartLabels.title = div; - } - - if (this.attr_('xlabel')) { - div = document.createElement("div"); - div.style.position = 'absolute'; - div.style.bottom = 0; // TODO(danvk): this is lazy. Calculate style.top. - div.style.left = this.area.x + 'px'; - div.style.width = this.area.w + 'px'; - div.style.height = this.attr_('xLabelHeight') + 'px'; - div.style.textAlign = 'center'; - div.style.fontSize = (this.attr_('xLabelHeight') - 2) + 'px'; - - class_div = document.createElement("div"); - class_div.className = 'dygraph-label dygraph-xlabel'; - class_div.innerHTML = this.attr_('xlabel'); - div.appendChild(class_div); - this.container.appendChild(div); - this.chartLabels.xlabel = div; - } - - var that = this; - function createRotatedDiv(axis, classes, html) { - var box = { - left: 0, - top: that.area.y, - width: that.attr_('yLabelWidth'), - height: that.area.h +var nonTrivialStrategy = function(renderer, ctx, color, strokeWidth, strokePattern) { + return new function() { + this.init = function() { }; + this.finish = function() { }; + this.startSegment = function() { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = strokeWidth; + }; + this.endSegment = function() { + ctx.stroke(); // should this include closePath? + }; + this.drawLine = function(x1, y1, x2, y2) { + renderer._dashedLine(ctx, x1, y1, x2, y2, strokePattern); + }; + this.skipPixel = function(prevX, prevY, curX, curY) { + // TODO(konigsberg): optimize with http://jsperf.com/math-round-vs-hack/6 ? + return (Math.round(prevX) == Math.round(curX) && + Math.round(prevY) == Math.round(curY)); }; - // TODO(danvk): is this outer div actually necessary? - div = document.createElement("div"); - div.style.position = 'absolute'; - if (axis == 1) { - div.style.left = box.left; - } else { - div.style.right = box.left; - } - div.style.top = box.top + 'px'; - div.style.width = box.width + 'px'; - div.style.height = box.height + 'px'; - div.style.fontSize = (that.attr_('yLabelWidth') - 2) + 'px'; - - var inner_div = document.createElement("div"); - inner_div.style.position = 'absolute'; - inner_div.style.width = box.height + 'px'; - inner_div.style.height = box.width + 'px'; - inner_div.style.top = (box.height / 2 - box.width / 2) + 'px'; - inner_div.style.left = (box.width / 2 - box.height / 2) + 'px'; - inner_div.style.textAlign = 'center'; - - // CSS rotation is an HTML5 feature which is not standardized. Hence every - // browser has its own name for the CSS style. - var val = 'rotate(' + (axis == 1 ? '-' : '') + '90deg)'; - inner_div.style.transform = val; // HTML5 - inner_div.style.WebkitTransform = val; // Safari/Chrome - inner_div.style.MozTransform = val; // Firefox - inner_div.style.OTransform = val; // Opera - inner_div.style.msTransform = val; // IE9 - - if (typeof(document.documentMode) !== 'undefined' && - document.documentMode < 9) { - // We're dealing w/ an old version of IE, so we have to rotate the text - // using a BasicImage transform. This uses a different origin of rotation - // than HTML5 rotation (top left of div vs. its center). - inner_div.style.filter = - 'progid:DXImageTransform.Microsoft.BasicImage(rotation=' + - (axis == 1 ? '3' : '1') + ')'; - inner_div.style.left = '0px'; - inner_div.style.top = '0px'; - } - - class_div = document.createElement("div"); - class_div.className = classes; - class_div.innerHTML = html; - - inner_div.appendChild(class_div); - div.appendChild(inner_div); - return div; - } - - var div; - if (this.attr_('ylabel')) { - div = createRotatedDiv(1, 'dygraph-label dygraph-ylabel', - this.attr_('ylabel')); - this.container.appendChild(div); - this.chartLabels.ylabel = div; - } - if (this.attr_('y2label') && this.dygraph_.numAxes() == 2) { - div = createRotatedDiv(2, 'dygraph-label dygraph-y2label', - this.attr_('y2label')); - this.container.appendChild(div); - this.chartLabels.y2label = div; - } -}; - - -DygraphCanvasRenderer.prototype._renderAnnotations = function() { - var annotationStyle = { - "position": "absolute", - "fontSize": this.attr_('axisLabelFontSize') + "px", - "zIndex": 10, - "overflow": "hidden" }; +}; - var bindEvt = function(eventName, classEventName, p, self) { - return function(e) { - var a = p.annotation; - if (a.hasOwnProperty(eventName)) { - a[eventName](a, p, self.dygraph_, e); - } else if (self.dygraph_.attr_(classEventName)) { - self.dygraph_.attr_(classEventName)(a, p, self.dygraph_,e ); - } +var trivialStrategy = function(ctx, color, strokeWidth) { + return new function() { + this.init = function() { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = strokeWidth; + }; + this.finish = function() { + ctx.stroke(); // should this include closePath? + }; + this.startSegment = function() { }; + this.endSegment = function() { }; + this.drawLine = function(x1, y1, x2, y2) { + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + }; + // don't skip pixels. + this.skipPixel = function() { + return false; }; }; - - // Get a list of point with annotations. - var points = this.layout.annotated_points; - for (var i = 0; i < points.length; i++) { - var p = points[i]; - if (p.canvasx < this.area.x || p.canvasx > this.area.x + this.area.w || - p.canvasy < this.area.y || p.canvasy > this.area.y + this.area.h) { - continue; - } - - var a = p.annotation; - var tick_height = 6; - if (a.hasOwnProperty("tickHeight")) { - tick_height = a.tickHeight; - } - - var div = document.createElement("div"); - for (var name in annotationStyle) { - if (annotationStyle.hasOwnProperty(name)) { - div.style[name] = annotationStyle[name]; - } - } - if (!a.hasOwnProperty('icon')) { - div.className = "dygraphDefaultAnnotation"; - } - if (a.hasOwnProperty('cssClass')) { - div.className += " " + a.cssClass; - } - - var width = a.hasOwnProperty('width') ? a.width : 16; - var height = a.hasOwnProperty('height') ? a.height : 16; - if (a.hasOwnProperty('icon')) { - var img = document.createElement("img"); - img.src = a.icon; - img.width = width; - img.height = height; - div.appendChild(img); - } else if (p.annotation.hasOwnProperty('shortText')) { - div.appendChild(document.createTextNode(p.annotation.shortText)); - } - div.style.left = (p.canvasx - width / 2) + "px"; - if (a.attachAtBottom) { - div.style.top = (this.area.h - height - tick_height) + "px"; - } else { - div.style.top = (p.canvasy - height - tick_height) + "px"; - } - div.style.width = width + "px"; - div.style.height = height + "px"; - div.title = p.annotation.text; - div.style.color = this.colors[p.name]; - div.style.borderColor = this.colors[p.name]; - a.div = div; - - this.dygraph_.addEvent(div, 'click', - bindEvt('clickHandler', 'annotationClickHandler', p, this)); - this.dygraph_.addEvent(div, 'mouseover', - bindEvt('mouseOverHandler', 'annotationMouseOverHandler', p, this)); - this.dygraph_.addEvent(div, 'mouseout', - bindEvt('mouseOutHandler', 'annotationMouseOutHandler', p, this)); - this.dygraph_.addEvent(div, 'dblclick', - bindEvt('dblClickHandler', 'annotationDblClickHandler', p, this)); - - this.container.appendChild(div); - this.annotations.push(div); - - var ctx = this.elementContext; - ctx.strokeStyle = this.colors[p.name]; - ctx.beginPath(); - if (!a.attachAtBottom) { - ctx.moveTo(p.canvasx, p.canvasy); - ctx.lineTo(p.canvasx, p.canvasy - 2 - tick_height); - } else { - ctx.moveTo(p.canvasx, this.area.h); - ctx.lineTo(p.canvasx, this.area.h - 2 - tick_height); - } - ctx.closePath(); - ctx.stroke(); - } }; -DygraphCanvasRenderer.makeNextPointStep_ = function( - connect, points, start, end) { - if (connect) { - return function(j) { - while (++j + start < end) { - if (!(points[start + j].yval === null)) break; - } - return j; - } - } else { - return function(j) { return j + 1 }; +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._drawStyledLine = function( - ctx, i, setName, color, strokeWidth, strokePattern, drawPoints, - drawPointCallback, pointSize) { - var isNullOrNaN = function(x) { - return (x === null || isNaN(x)); - }; +DygraphCanvasRenderer.prototype._drawSeries = function( + ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, + stepPlot, strategy) { - var stepPlot = this.attr_("stepPlot"); - var firstIndexInSet = this.layout.setPointsOffsets[i]; - var setLength = this.layout.setPointsLengths[i]; - var afterLastIndexInSet = firstIndexInSet + setLength; - var points = this.layout.points; - var prevX = null; - var prevY = null; - var nextY = null; + 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. - if (!Dygraph.isArrayLike(strokePattern)) { - strokePattern = null; - } - var drawGapPoints = this.dygraph_.attr_('drawGapEdgePoints', setName); + var first = true; // the first cycle through the while loop - var point, nextPoint; - var next = DygraphCanvasRenderer.makeNextPointStep_( - this.attr_('connectSeparatedPoints'), points, firstIndexInSet, - afterLastIndexInSet); - ctx.save(); - for (var j = 0; j < setLength; j = next(j)) { - point = points[firstIndexInSet + j]; - nextY = (next(j) < setLength) ? - points[firstIndexInSet + next(j)].canvasy : null; - if (isNullOrNaN(point.canvasy)) { - if (stepPlot && prevX !== null) { + strategy.init(); + + while(iter.hasNext()) { + point = iter.next(); + if (point.canvasy === null || point.canvasy != point.canvasy) { + if (stepPlot && prevCanvasX !== null) { // Draw a horizontal line to the start of the missing data - ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = this.attr_('strokeWidth'); - this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); - ctx.stroke(); + strategy.startSegment(); + strategy.drawLine(prevX, prevY, point.canvasx, prevY); + strategy.endSegment(); } - // this will make us move to the next point, not draw a line to it. - prevX = prevY = null; + prevCanvasX = prevCanvasY = null; } else { - // A point is "isolated" if it is non-null but both the previous - // and next points are null. - var isIsolated = (!prevX && isNullOrNaN(nextY)); + nextCanvasY = iter.hasNext() ? iter.peek().canvasy : null; + // TODO: we calculate isNullOrNaN for this point, and the next, and then, when + // we iterate, test for isNullOrNaN again. Why bother? + var isNextCanvasYNullOrNaN = nextCanvasY === null || nextCanvasY != nextCanvasY; + isIsolated = (!prevCanvasX && isNextCanvasYNullOrNaN); if (drawGapPoints) { - // Also consider a point to be is "isolated" if it's adjacent to a + // Also consider a point to be "isolated" if it's adjacent to a // null point, excluding the graph edges. - if ((j > 0 && !prevX) || - (next(j) < setLength && isNullOrNaN(nextY))) { + if ((!first && !prevCanvasX) || + (iter.hasNext() && isNextCanvasYNullOrNaN)) { isIsolated = true; } } - if (prevX === null) { - prevX = point.canvasx; - prevY = point.canvasy; - } else { - // Skip over points that will be drawn in the same pixel. - if (Math.round(prevX) == Math.round(point.canvasx) && - Math.round(prevY) == Math.round(point.canvasy)) { + if (prevCanvasX !== null) { + if (strategy.skipPixel(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy)) { continue; } - // TODO(antrob): skip over points that lie on a line that is already - // going to be drawn. There is no need to have more than 2 - // consecutive points that are collinear. if (strokeWidth) { - ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = strokeWidth; + strategy.startSegment(); if (stepPlot) { - this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); - prevX = point.canvasx; + strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, prevCanvasY); + prevCanvasX = point.canvasx; } - this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern); - prevX = point.canvasx; - prevY = point.canvasy; - ctx.stroke(); + strategy.drawLine(prevCanvasX, prevCanvasY, point.canvasx, point.canvasy); + strategy.endSegment(); } } - if (drawPoints || isIsolated) { pointsOnLine.push([point.canvasx, point.canvasy]); } + prevCanvasX = point.canvasx; + prevCanvasY = point.canvasy; } + first = false; } - 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(); - } - ctx.restore(); + strategy.finish(); + return pointsOnLine; }; DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) { @@ -778,6 +456,7 @@ DygraphCanvasRenderer.prototype._drawLine = function(ctx, i) { 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), @@ -812,19 +491,19 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { var stepPlot = this.attr_("stepPlot"); var points = this.layout.points; var pointsLength = points.length; - var point, i, j, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis; + var point, i, prevX, prevY, prevYs, color, setName, newYs, err_color, rgb, yscale, axis; var setNames = this.layout.setNames; var setCount = setNames.length; - // TODO(danvk): Move this mapping into Dygraph and get it out of here. - this.colors = {}; - for (i = 0; i < setCount; i++) { - this.colors[setNames[i]] = this.colorScheme_[i % this.colorScheme_.length]; - } + this.colors = this.dygraph_.colorsMap_; // 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. for (i = pointsLength; i--;) { point = points[i]; point.canvasx = this.area.w * point.x + this.area.x; @@ -845,11 +524,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { var firstIndexInSet = this.layout.setPointsOffsets[i]; var setLength = this.layout.setPointsLengths[i]; - var afterLastIndexInSet = firstIndexInSet + setLength; - var next = DygraphCanvasRenderer.makeNextPointStep_( - this.attr_('connectSeparatedPoints'), points, - afterLastIndexInSet); + var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints"))); // setup graphics context prevX = NaN; @@ -862,8 +539,8 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { fillAlpha + ')'; ctx.fillStyle = err_color; ctx.beginPath(); - for (j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) { - point = points[j]; + while (iter.hasNext()) { + point = iter.next(); if (point.name == setName) { // TODO(klausw): this is always true if (!Dygraph.isOK(point.y)) { prevX = NaN; @@ -917,11 +594,9 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { axisY = this.area.h * axisY + this.area.y; var firstIndexInSet = this.layout.setPointsOffsets[i]; var setLength = this.layout.setPointsLengths[i]; - var afterLastIndexInSet = firstIndexInSet + setLength; - var next = DygraphCanvasRenderer.makeNextPointStep_( - this.attr_('connectSeparatedPoints'), points, - afterLastIndexInSet); + var iter = Dygraph.createIterator(points, firstIndexInSet, setLength, + DygraphCanvasRenderer._getIteratorPredicate(this.attr_("connectSeparatedPoints"))); // setup graphics context prevX = NaN; @@ -933,8 +608,8 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { fillAlpha + ')'; ctx.fillStyle = err_color; ctx.beginPath(); - for (j = firstIndexInSet; j < afterLastIndexInSet; j = next(j)) { - point = points[j]; + while(iter.hasNext()) { + point = iter.next(); if (point.name == setName) { // TODO(klausw): this is always true if (!Dygraph.isOK(point.y)) { prevX = NaN; @@ -953,7 +628,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { } } newYs = [ point.canvasy, lastY ]; - + if(stepPlot) { // Step plots must keep track of the top and bottom of // the baseline at each point. @@ -965,13 +640,13 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { } else { baseline[point.canvasx] = point.canvasy; } - + } else { newYs = [ point.canvasy, axisY ]; } if (!isNaN(prevX)) { ctx.moveTo(prevX, prevYs[0]); - + if (stepPlot) { ctx.lineTo(point.canvasx, prevYs[0]); if(currBaseline) { @@ -984,7 +659,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { ctx.lineTo(point.canvasx, newYs[0]); ctx.lineTo(point.canvasx, newYs[1]); } - + ctx.lineTo(prevX, prevYs[1]); ctx.closePath(); }