factor out dashed-canvas.js and use it in dygraph-canvas.js. one test failing
authorDan Vanderkam <dan@dygraphs.com>
Sun, 15 Jul 2012 03:11:31 +0000 (22:11 -0500)
committerDan Vanderkam <dan@dygraphs.com>
Sun, 15 Jul 2012 03:11:31 +0000 (22:11 -0500)
dashed-canvas.js [new file with mode: 0644]
dygraph-canvas.js
dygraph-dev.js
tests/charting-combinations.html [new file with mode: 0644]
tests/dashed-canvas.html [new file with mode: 0644]

diff --git a/dashed-canvas.js b/dashed-canvas.js
new file mode 100644 (file)
index 0000000..beb4d47
--- /dev/null
@@ -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 = [];
+  };
+};
index 6cbe102..c3bf8bc 100644 (file)
@@ -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();
-};
index 207c3f4..4ca6bb3 100644 (file)
@@ -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 (file)
index 0000000..e91db73
--- /dev/null
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Charting combinations</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+    <script type="text/javascript" src="data.js"></script>
+    <style type="text/css">
+      .chart {
+        width: 600px;
+        height: 300px;
+      }
+      #container {
+        display: table;
+        float: left;
+      }
+      #results {
+        display: table;
+        float: left;
+        padding-left: 20px;
+      }
+    </style>
+  </head>
+  <body>
+  <p>There are four options which fundmanentally change the behavior of the standard plotter:</p>
+  <ol>
+    <li> errorBars / customBars
+    <li> stepPlot
+    <li> fillGraph
+    <li> strokePattern
+  </ol>
+
+  <p>This page exhaustively checks all combinations of these parameters.</p>
+
+  <div id=container> </div>
+  <div id=results> <b>Valid combinations</b>
+    <ol id='results-ol'>
+    </ol>
+  </div>
+
+  <script type="text/javascript">
+    // NOTE: this is an odd thing to do; dygraphs should really throw here.
+    console.warn = function(x) {
+      throw x;
+    }
+
+    var bools = [false, true];
+    var containerDiv = document.getElementById("container");
+    var resultsList = document.getElementById("results-ol");
+    bools.forEach(function(errorBars) {
+      var data_csv = errorBars ? NoisyData() : data();
+      bools.forEach(function(stepPlot) {
+        bools.forEach(function(fillGraph) {
+          bools.forEach(function(strokePattern) {
+            var title_parts = [];
+            if (errorBars) title_parts.push('errorBars');
+            if (stepPlot) title_parts.push('stepPlot');
+            if (fillGraph) title_parts.push('fillGraph');
+            if (strokePattern) title_parts.push('strokePattern');
+            var title = title_parts.join(', ');
+            if (!title) title = '(none)';
+
+            var title_h2 = document.createElement('h2');
+            title_h2.innerHTML = title;
+            containerDiv.appendChild(title_h2);
+
+            var div = document.createElement('div');
+            div.className = 'chart';
+            containerDiv.appendChild(div);
+
+            try {
+              var g = new Dygraph(div, data_csv, {
+                errorBars: errorBars,
+                stepPlot: stepPlot,
+                fillGraph: fillGraph,
+                strokePattern: strokePattern ? Dygraph.DASHED_LINE : null
+              });
+
+              resultsList.innerHTML += '<li> ' + title + '</li>';
+            } catch(e) {
+              div.className = '';
+              div.innerHTML = e;
+            }
+          });
+        });
+      });
+    });
+  </script>
+
+  </body>
+</html>
diff --git a/tests/dashed-canvas.html b/tests/dashed-canvas.html
new file mode 100644 (file)
index 0000000..dc472fa
--- /dev/null
@@ -0,0 +1,59 @@
+<html>
+<head>
+  <script src="../dashed-canvas.js"></script>
+</head>
+<body>
+<p>You should see solid black and blue lines with a dashed red line in between
+them:</p>
+<canvas id=cnv width=640 height=480></canvas>
+
+<p>You should see a solid black line:</p>
+<canvas id=cnv2 width=640 height=100></canvas>
+
+<script type="text/javascript">
+ctx = document.getElementById("cnv").getContext("2d");
+ctx2 = document.getElementById("cnv2").getContext("2d");
+
+// Draw a line segment -- should be perfectly normal.
+ctx.lineWidth = 2;
+ctx.save();
+ctx.beginPath();
+ctx.moveTo(80, 50);
+ctx.lineTo(280, 400);
+ctx.moveTo(330, 400);
+ctx.lineTo(580, 200);
+ctx.stroke();
+
+// Draw a dashed line segment.
+ctx.installPattern([10, 10]);
+ctx.strokeStyle = 'red';
+ctx.beginPath();
+ctx.moveTo(100, 50);
+ctx.lineTo(300, 400);
+ctx.lineTo(300, 450);
+ctx.moveTo(350, 450);
+ctx.lineTo(350, 400);
+ctx.lineTo(600, 200);
+ctx.stroke();
+
+// An unrelated canvas should not be aware of the pattern.
+ctx2.beginPath();
+ctx2.moveTo(100, 50);
+ctx2.lineTo(600, 50);
+ctx2.stroke();
+
+ctx.uninstallPattern();
+
+// Now that we've uninstalled the pattern, should be normal again.
+ctx.strokeStyle = 'blue';
+ctx.beginPath();
+ctx.moveTo(120, 50);
+ctx.lineTo(320, 400);
+ctx.moveTo(370, 400);
+ctx.lineTo(620, 200);
+ctx.stroke();
+
+ctx.restore();
+</script>
+</body>
+</html>