From 79253bd07d1fd37d53e0793c08030df4aaf871be Mon Sep 17 00:00:00 2001 From: bluthen Date: Thu, 26 Jan 2012 17:37:43 -0500 Subject: [PATCH] Support stroke patterns (i.e. dashed/dotted lines). Patterns can be arbitrarily complicated sequences of lines and breaks, or pre-built constants like Dygraph.DOTTED_LINE. See tests/per-series.html for an example of how this works. commit 214caf668bb8472605c505c124fd14e5d3b5a956 Merge: db2e28c 25c4046 Author: Dan Vanderkam Date: Thu Jan 26 17:33:08 2012 -0500 Merge branch 'stroke_pattern' of https://github.com/bluthen/dygraphs into bluthen commit 25c40460bbd25f05cfd0be9da3535dd4662cfadb Author: Russell Valentine Date: Thu Jan 26 14:43:14 2012 -0600 Few changes suggested by Dan Vanderkam. commit a9965f34b5470c2db5656cac1d8d3debd3a0d4e7 Author: Russell Valentine Date: Thu Jan 26 00:41:44 2012 -0600 Stroke patterns in the legend that scale to 1em. commit 62f2905c52e6936339dd1de7af1844aaced95c05 Author: Russell Valentine Date: Sat Jan 21 12:14:03 2012 -0600 Sets dimensions for graph div in dash test so it could pass if default size ever changes. commit bfece39cc4f2e365cf692b81cffdf91a773625ac Author: Russell Valentine Date: Fri Jan 20 17:22:33 2012 -0600 Added a simple dash unit test. It tests if it draw the correct number of lines and remembers pattern history between points. commit 57539c89e29a1e45a6d1000ebe53c496eb88a61b Author: Russell Valentine Date: Wed Jan 18 23:50:13 2012 -0600 Comment wording changes. Added default string stroke patterns object to allow for more code reuse. commit de286626b93dc7ce49ce50abfba83ffaa29db068 Author: Russell Valentine Date: Wed Jan 18 22:48:16 2012 -0600 Use "font-weight: bold" in style instead of the bold tag. commit f5958230321df474bfb1339b9a97c64f92f20621 Author: Russell Valentine Date: Wed Jan 18 22:12:50 2012 -0600 Reverted CanvasAssertions.js to have no modifications. Moved array compare function to dygraph-utils. Moved dashedLine into DygraphCanvasRenderer instead of in CanvasRenderingContext2D. Added some comments and used better variable names. Included are some lint warning fixes and style conformity changes. commit 4b5e255dff699f59a5ced5186c4e4558b09a4003 Author: Russell Valentine Date: Wed Jan 18 14:33:13 2012 -0600 Actual dashedLine coordinates checked now. Oops. commit a5930c675465e6566854d3b6e2b9d6fa606ce91c Author: Russell Valentine Date: Wed Jan 18 14:25:51 2012 -0600 per series stroke pattern support. --- auto_tests/tests/simple_drawing.js | 24 +++++++++ dygraph-canvas.js | 104 +++++++++++++++++++++++++++++++++++-- dygraph-options-reference.js | 7 +++ dygraph-utils.js | 30 +++++++++++ dygraph.js | 92 ++++++++++++++++++++++++++++++-- tests/per-series.html | 70 ++++++++++++++++++++----- 6 files changed, 303 insertions(+), 24 deletions(-) diff --git a/auto_tests/tests/simple_drawing.js b/auto_tests/tests/simple_drawing.js index ca19301..0ea13a4 100644 --- a/auto_tests/tests/simple_drawing.js +++ b/auto_tests/tests/simple_drawing.js @@ -56,3 +56,27 @@ SimpleDrawingTestCase.prototype.testDrawSimpleRangePlusOne = function() { lineWidth: 1 }); } + +/** + * Tests that it is drawing dashes, and it remember the dash history between + * points. + */ +SimpleDrawingTestCase.prototype.testDrawSimpleDash = function() { + var opts = { + drawXGrid: false, + drawYGrid: false, + drawXAxis: false, + drawYAxis: false, + 'Y1': {strokePattern: [25, 7, 7, 7]}, + colors: ['#ff0000'] + }; + + var graph = document.getElementById("graph"); + // Set the dims so we pass if default changes. + graph.style.width='480px'; + graph.style.height='320px'; + var g = new Dygraph(graph, [[1, 4], [2, 5], [3, 3], [4, 7], [5, 9]], opts); + htx = g.hidden_ctx_; + + assertEquals(29, CanvasAssertions.numLinesDrawn(htx, "#ff0000")); +}; diff --git a/dygraph-canvas.js b/dygraph-canvas.js index a62616a..c0d142f 100644 --- a/dygraph-canvas.js +++ b/dygraph-canvas.js @@ -28,6 +28,7 @@ /*global Dygraph:false,RGBColor:false */ "use strict"; + var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) { this.dygraph_ = dygraph; @@ -838,6 +839,10 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { prevX = null; prevY = null; var drawPoints = this.dygraph_.attr_("drawPoints", setName); + var strokePattern = this.dygraph_.attr_("strokePattern", setName); + if (!Dygraph.isArrayLike(strokePattern)) { + strokePattern = null; + } for (j = firstIndexInSet; j < afterLastIndexInSet; j++) { point = points[j]; if (isNullOrNaN(point.canvasy)) { @@ -846,8 +851,7 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = this.attr_('strokeWidth'); - ctx.moveTo(prevX, prevY); - ctx.lineTo(point.canvasx, prevY); + this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); ctx.stroke(); } // this will make us move to the next point, not draw a line to it. @@ -873,13 +877,12 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = strokeWidth; - ctx.moveTo(prevX, prevY); if (stepPlot) { - ctx.lineTo(point.canvasx, prevY); + this._dashedLine(ctx, prevX, prevY, point.canvasx, prevY, strokePattern); } + this._dashedLine(ctx, prevX, prevY, point.canvasx, point.canvasy, strokePattern); prevX = point.canvasx; prevY = point.canvasy; - ctx.lineTo(prevX, prevY); ctx.stroke(); } } @@ -898,3 +901,94 @@ DygraphCanvasRenderer.prototype._renderLineChart = function() { context.restore(); }; + +/** + * 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-options-reference.js b/dygraph-options-reference.js index fec5254..a94a54f 100644 --- a/dygraph-options-reference.js +++ b/dygraph-options-reference.js @@ -263,6 +263,13 @@ Dygraph.OPTIONS_REFERENCE = // "example": "0.5, 2.0", "description": "The width of the lines connecting data points. This can be used to increase the contrast or some graphs." }, + "strokePattern": { + "default": "null", + "labels": ["Data Line display"], + "type": "array", + "example": "[10, 2, 5, 2]", + "description": "A custom pattern array where the even index is a draw and odd is a space in pixels. If null then it draws a solid line. The array should have a even length as any odd lengthed array could be expressed as a smaller even length array." + }, "wilsonInterval": { "default": "true", "labels": ["Error Bars"], diff --git a/dygraph-utils.js b/dygraph-utils.js index 8b9dfd9..5a21430 100644 --- a/dygraph-utils.js +++ b/dygraph-utils.js @@ -35,6 +35,13 @@ Dygraph.ERROR = 3; // https://github.com/eriwen/javascript-stacktrace Dygraph.LOG_STACK_TRACES = false; +/** A dotted line stroke pattern. */ +Dygraph.DOTTED_LINE = [2, 2]; +/** A dashed line stroke pattern. */ +Dygraph.DASHED_LINE = [7, 3]; +/** A dot dash stroke pattern. */ +Dygraph.DOT_DASH_LINE = [7, 2, 2, 2]; + /** * @private * Log an error on the JS console at the given severity. @@ -775,3 +782,26 @@ Dygraph.isPixelChangingOptionList = function(labels, attrs) { return requiresNewPoints; }; + +/** + * Compares two arrays to see if they are equal. If either parameter is not an + * array it will return false. Does a shallow compare + * Dygraph.compareArrays([[1,2], [3, 4]], [[1,2], [3,4]]) === false. + * @param array1 first array + * @param array2 second array + * @return True if both parameters are arrays, and contents are equal. + */ +Dygraph.compareArrays = function(array1, array2) { + if (!Dygraph.isArrayLike(array1) || !Dygraph.isArrayLike(array2)) { + return false; + } + if (array1.length !== array2.length) { + return false; + } + for (var i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; +}; diff --git a/dygraph.js b/dygraph.js index c633df7..1c25f9f 100644 --- a/dygraph.js +++ b/dygraph.js @@ -1550,18 +1550,92 @@ Dygraph.prototype.idxToRow_ = function(idx) { /** * @private + * Generates legend html dash for any stroke pattern. It will try to scale the + * pattern to fit in 1em width. Or if small enough repeat the partern for 1em + * width. + * @param strokePattern The pattern + * @param color The color of the series. + * @param oneEmWidth The width in pixels of 1em in the legend. + */ +Dygraph.prototype.generateLegendDashHTML_ = function(strokePattern, color, oneEmWidth) { + var dash = ""; + var i, j, paddingLeft, marginRight; + var strokePixelLength = 0, segmentLoop = 0; + var normalizedPattern = []; + var loop; + // IE 7,8 fail at these divs, so they get boring legend, have not tested 9. + var isIE = (/MSIE/.test(navigator.userAgent) && !window.opera); + if(isIE) { + return "—"; + } + if (!strokePattern || strokePattern.length <= 1) { + // Solid line + dash = "
"; + } else { + // Compute the length of the pixels including the first segment twice, + // since we repeat it. + for (i = 0; i <= strokePattern.length; i++) { + strokePixelLength += strokePattern[i%strokePattern.length]; + } + + // See if we can loop the pattern by itself at least twice. + loop = Math.floor(oneEmWidth/(strokePixelLength-strokePattern[0])); + if (loop > 1) { + // This pattern fits at least two times, no scaling just convert to em; + for (i = 0; i < strokePattern.length; i++) { + normalizedPattern[i] = strokePattern[i]/oneEmWidth; + } + // Since we are repeating the pattern, we don't worry about repeating the + // first segment in one draw. + segmentLoop = normalizedPattern.length; + } else { + // If the pattern doesn't fit in the legend we scale it to fit. + loop = 1; + for (i = 0; i < strokePattern.length; i++) { + normalizedPattern[i] = strokePattern[i]/strokePixelLength; + } + // For the scaled patterns we do redraw the first segment. + segmentLoop = normalizedPattern.length+1; + } + // Now make the pattern. + for (j = 0; j < loop; j++) { + for (i = 0; i < segmentLoop; i+=2) { + // The padding is the drawn segment. + paddingLeft = normalizedPattern[i%normalizedPattern.length]; + if (i < strokePattern.length) { + // The margin is the space segment. + marginRight = normalizedPattern[(i+1)%normalizedPattern.length]; + } else { + // The repeated first segment has no right margin. + marginRight = 0; + } + dash += "
"; + } + } + } + return dash; +}; + +/** + * @private * Generates HTML for the legend which is displayed when hovering over the * chart. If no selected points are specified, a default legend is returned * (this may just be the empty string). * @param { Number } [x] The x-value of the selected points. * @param { [Object] } [sel_points] List of selected points for the given * x-value. Should have properties like 'name', 'yval' and 'canvasy'. + * @param { Number } [oneEmWidth] The pixel width for 1em in the legend. */ -Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { +Dygraph.prototype.generateLegendHTML_ = function(x, sel_points, oneEmWidth) { // If no points are selected, we display a default legend. Traditionally, // this has been blank. But a better default would be a conventional legend, // which provides essential information for a non-interactive chart. - var html, sepLines, i, c; + var html, sepLines, i, c, dash, strokePattern; if (typeof(x) === 'undefined') { if (this.attr_('legend') != 'always') return ''; @@ -1572,8 +1646,10 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { if (!this.visibility()[i - 1]) continue; c = this.plotter_.colors[labels[i]]; if (html !== '') html += (sepLines ? '
' : ' '); - html += "—" + labels[i] + - ""; + strokePattern = this.attr_("strokePattern", labels[i]); + dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth); + html += "" + dash + + " " + labels[i] + ""; } return html; } @@ -1616,8 +1692,14 @@ Dygraph.prototype.generateLegendHTML_ = function(x, sel_points) { * x-value. Should have properties like 'name', 'yval' and 'canvasy'. */ Dygraph.prototype.setLegendHTML_ = function(x, sel_points) { - var html = this.generateLegendHTML_(x, sel_points); var labelsDiv = this.attr_("labelsDiv"); + var sizeSpan = document.createElement('span'); + // Calculates the width of 1em in pixels for the legend. + sizeSpan.setAttribute('style', 'margin: 0; padding: 0 0 0 1em; border: 0;'); + labelsDiv.appendChild(sizeSpan); + var oneEmWidth=sizeSpan.offsetWidth; + + var html = this.generateLegendHTML_(x, sel_points, oneEmWidth); if (labelsDiv !== null) { labelsDiv.innerHTML = html; } else { diff --git a/tests/per-series.html b/tests/per-series.html index c1b400a..98501ac 100644 --- a/tests/per-series.html +++ b/tests/per-series.html @@ -16,23 +16,26 @@

Chart with per-series properties

- +

Chart with per-series properties with legend.

+
-- 2.7.4