From fb63bf1b8b64385b98681417c4ee02b0fb0fd130 Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Sat, 14 Jul 2012 22:11:31 -0500 Subject: [PATCH] factor out dashed-canvas.js and use it in dygraph-canvas.js. one test failing --- dashed-canvas.js | 145 +++++++++++++++++++++++++++++++++++++++ dygraph-canvas.js | 128 +++------------------------------- dygraph-dev.js | 1 + tests/charting-combinations.html | 94 +++++++++++++++++++++++++ tests/dashed-canvas.html | 59 ++++++++++++++++ 5 files changed, 309 insertions(+), 118 deletions(-) create mode 100644 dashed-canvas.js create mode 100644 tests/charting-combinations.html create mode 100644 tests/dashed-canvas.html diff --git a/dashed-canvas.js b/dashed-canvas.js new file mode 100644 index 0000000..beb4d47 --- /dev/null +++ b/dashed-canvas.js @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2012 Dan Vanderkam (danvdk@gmail.com) + * MIT-licensed (http://opensource.org/licenses/MIT) + */ + +/** + * @fileoverview Adds support for dashed lines to the HTML5 canvas. + * + * Usage: + * var ctx = canvas.getContext("2d"); + * ctx.installPattern([10, 5]) // draw 10 pixels, skip 5 pixels, repeat. + * ctx.beginPath(); + * ctx.moveTo(100, 100); // start the first line segment. + * ctx.lineTo(150, 200); + * ctx.lineTo(200, 100); + * ctx.moveTo(300, 150); // start a second, unconnected line + * ctx.lineTo(400, 250); + * ... + * ctx.stroke(); // draw the dashed line. + * ctx.uninstallPattern(); + * + * This is designed to leave the canvas untouched when it's not used. + * If you never install a pattern, or call uninstallPattern(), then the canvas + * will be exactly as it would have if you'd never used this library. The only + * difference from the standard canvas will be the "installPattern" method of + * the drawing context. + */ + +CanvasRenderingContext2D.prototype.installPattern = function(pattern) { + if (typeof(this.isPatternInstalled) !== 'undefined') { + throw "Must un-install old line pattern before installing a new one."; + } + this.isPatternInstalled = true; + + var dashedLineToHistory = [0, 0]; + + // list of connected line segements: + // [ [x1, y1], ..., [xn, yn] ], [ [x1, y1], ..., [xn, yn] ] + var segments = []; + + // Stash away copies of the unmodified line-drawing functions. + var realBeginPath = this.beginPath; + var realLineTo = this.lineTo; + var realMoveTo = this.moveTo; + var realStroke = this.stroke; + + this.uninstallPattern = function() { + this.beginPath = realBeginPath; + this.lineTo = realLineTo; + this.moveTo = realMoveTo; + this.stroke = realStroke; + this.uninstallPattern = undefined; + this.isPatternInstalled = undefined; + }; + + // Keep our own copies of the line segments as they're drawn. + this.beginPath = function() { + segments = []; + realBeginPath.call(this); + }; + this.moveTo = function(x, y) { + segments.push([[x, y]]); + realMoveTo.call(this, x, y); + }; + this.lineTo = function(x, y) { + var last = segments[segments.length - 1]; + last.push([x, y]); + }; + + this.stroke = function() { + if (segments.length === 0) { + // Maybe the user is drawing something other than a line. + // TODO(danvk): test this case. + realStroke.call(this); + return; + } + + for (var i = 0; i < segments.length; i++) { + var seg = segments[i]; + var x1 = seg[0][0], y1 = seg[0][1]; + for (var j = 1; j < seg.length; j++) { + // Draw a dashed line from (x1, y1) - (x2, y2) + var x2 = seg[j][0], y2 = seg[j][1]; + this.save(); + + // Calculate transformation parameters + var dx = (x2-x1); + var dy = (y2-y1); + var len = Math.sqrt(dx*dx + dy*dy); + var rot = Math.atan2(dy, dx); + + // Set transformation + this.translate(x1, y1); + realMoveTo.call(this, 0, 0); + this.rotate(rot); + + // Set last pattern index we used for this pattern. + var patternIndex = dashedLineToHistory[0]; + x = 0; + while (len > x) { + // Get the length of the pattern segment we are dealing with. + segment = pattern[patternIndex]; + // If our last draw didn't complete the pattern segment all the way + // we will try to finish it. Otherwise we will try to do the whole + // segment. + if (dashedLineToHistory[1]) { + x += dashedLineToHistory[1]; + } else { + x += segment; + } + + if (x > len) { + // We were unable to complete this pattern index all the way, keep + // where we are the history so our next draw continues where we + // left off in the pattern. + dashedLineToHistory = [patternIndex, x-len]; + x = len; + } else { + // We completed this patternIndex, we put in the history that we + // are on the beginning of the next segment. + dashedLineToHistory = [(patternIndex+1)%pattern.length, 0]; + } + + // We do a line on a even pattern index and just move on a odd + // pattern index. The move is the empty space in the dash. + if (patternIndex % 2 === 0) { + realLineTo.call(this, x, 0); + } else { + realMoveTo.call(this, x, 0); + } + + // If we are not done, next loop process the next pattern segment, or + // the first segment again if we are at the end of the pattern. + patternIndex = (patternIndex+1) % pattern.length; + } + + this.restore(); + x1 = x2, y1 = y2; + } + } + realStroke.call(this); + segments = []; + }; +}; diff --git a/dygraph-canvas.js b/dygraph-canvas.js index 6cbe102..c3bf8bc 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -263,40 +263,23 @@ DygraphCanvasRenderer.prototype._drawStyledLine = function( DygraphCanvasRenderer._getIteratorPredicate( this.attr_("connectSeparatedPoints"))); + var stroking = strokePattern && (strokePattern.length >= 2); + var pointsOnLine; var strategy; - if (!strokePattern || strokePattern.length <= 1) { - strategy = trivialStrategy(ctx, color, strokeWidth); - } else { - strategy = nonTrivialStrategy(this, ctx, color, strokeWidth, strokePattern); + if (stroking) { + ctx.installPattern(strokePattern); } + + strategy = trivialStrategy(ctx, color, strokeWidth); pointsOnLine = this._drawSeries(ctx, iter, strokeWidth, pointSize, drawPoints, drawGapPoints, stepPlot, strategy); this._drawPointsOnLine(ctx, pointsOnLine, drawPointCallback, setName, color, pointSize); - ctx.restore(); -}; + if (stroking) { + ctx.uninstallPattern(); + } -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)); - }; - }; + ctx.restore(); }; var trivialStrategy = function(ctx, color, strokeWidth) { @@ -660,94 +643,3 @@ DygraphCanvasRenderer.prototype.drawFillBars_ = function(points) { ctx.fill(); } }; - -/** - * This does dashed lines onto a canvas for a given pattern. You must call - * ctx.stroke() after to actually draw it, much line ctx.lineTo(). It remembers - * the state of the line in regards to where we left off on drawing the pattern. - * You can draw a dashed line in several function calls and the pattern will be - * continous as long as you didn't call this function with a different pattern - * in between. - * @param ctx The canvas 2d context to draw on. - * @param x The start of the line's x coordinate. - * @param y The start of the line's y coordinate. - * @param x2 The end of the line's x coordinate. - * @param y2 The end of the line's y coordinate. - * @param pattern The dash pattern to draw, an array of integers where even - * index is drawn and odd index is not drawn (Ex. [10, 2, 5, 2], 10 is drawn 5 - * is drawn, 2 is the space between.). A null pattern, array of length one, or - * empty array will do just a solid line. - * @private - */ -DygraphCanvasRenderer.prototype._dashedLine = function(ctx, x, y, x2, y2, pattern) { - // Original version http://stackoverflow.com/questions/4576724/dotted-stroke-in-canvas - // Modified by Russell Valentine to keep line history and continue the pattern - // where it left off. - var dx, dy, len, rot, patternIndex, segment; - - // If we don't have a pattern or it is an empty array or of size one just - // do a solid line. - if (!pattern || pattern.length <= 1) { - ctx.moveTo(x, y); - ctx.lineTo(x2, y2); - return; - } - - // If we have a different dash pattern than the last time this was called we - // reset our dash history and start the pattern from the begging - // regardless of state of the last pattern. - if (!Dygraph.compareArrays(pattern, this._dashedLineToHistoryPattern)) { - this._dashedLineToHistoryPattern = pattern; - this._dashedLineToHistory = [0, 0]; - } - ctx.save(); - - // Calculate transformation parameters - dx = (x2-x); - dy = (y2-y); - len = Math.sqrt(dx*dx + dy*dy); - rot = Math.atan2(dy, dx); - - // Set transformation - ctx.translate(x, y); - ctx.moveTo(0, 0); - ctx.rotate(rot); - - // Set last pattern index we used for this pattern. - patternIndex = this._dashedLineToHistory[0]; - x = 0; - while (len > x) { - // Get the length of the pattern segment we are dealing with. - segment = pattern[patternIndex]; - // If our last draw didn't complete the pattern segment all the way we - // will try to finish it. Otherwise we will try to do the whole segment. - if (this._dashedLineToHistory[1]) { - x += this._dashedLineToHistory[1]; - } else { - x += segment; - } - if (x > len) { - // We were unable to complete this pattern index all the way, keep - // where we are the history so our next draw continues where we left off - // in the pattern. - this._dashedLineToHistory = [patternIndex, x-len]; - x = len; - } else { - // We completed this patternIndex, we put in the history that we are on - // the beginning of the next segment. - this._dashedLineToHistory = [(patternIndex+1)%pattern.length, 0]; - } - - // We do a line on a even pattern index and just move on a odd pattern index. - // The move is the empty space in the dash. - if(patternIndex % 2 === 0) { - ctx.lineTo(x, 0); - } else { - ctx.moveTo(x, 0); - } - // If we are not done, next loop process the next pattern segment, or the - // first segment again if we are at the end of the pattern. - patternIndex = (patternIndex+1) % pattern.length; - } - ctx.restore(); -}; diff --git a/dygraph-dev.js b/dygraph-dev.js index 207c3f4..4ca6bb3 100644 --- a/dygraph-dev.js +++ b/dygraph-dev.js @@ -19,6 +19,7 @@ "strftime/strftime-min.js", "rgbcolor/rgbcolor.js", "stacktrace.js", + "dashed-canvas.js", "dygraph-layout.js", "dygraph-canvas.js", "dygraph.js", diff --git a/tests/charting-combinations.html b/tests/charting-combinations.html new file mode 100644 index 0000000..e91db73 --- /dev/null +++ b/tests/charting-combinations.html @@ -0,0 +1,94 @@ + + + + + Charting combinations + + + + + + +

There are four options which fundmanentally change the behavior of the standard plotter:

+
    +
  1. errorBars / customBars +
  2. stepPlot +
  3. fillGraph +
  4. strokePattern +
+ +

This page exhaustively checks all combinations of these parameters.

+ +
+
Valid combinations +
    +
+
+ + + + + diff --git a/tests/dashed-canvas.html b/tests/dashed-canvas.html new file mode 100644 index 0000000..dc472fa --- /dev/null +++ b/tests/dashed-canvas.html @@ -0,0 +1,59 @@ + + + + + +

You should see solid black and blue lines with a dashed red line in between +them:

+ + +

You should see a solid black line:

+ + + + + -- 2.7.4