--- /dev/null
+/**
+ * @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 = [];
+ };
+};
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) {
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();
-};
"strftime/strftime-min.js",
"rgbcolor/rgbcolor.js",
"stacktrace.js",
+ "dashed-canvas.js",
"dygraph-layout.js",
"dygraph-canvas.js",
"dygraph.js",
--- /dev/null
+<!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>
--- /dev/null
+<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>