Skip to content

Commit

Permalink
Support stroke patterns (i.e. dashed/dotted lines).
Browse files Browse the repository at this point in the history
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 <danvk@google.com>
Date:   Thu Jan 26 17:33:08 2012 -0500

    Merge branch 'stroke_pattern' of https://github.com/bluthen/dygraphs into bluthen

commit 25c4046
Author: Russell Valentine <russ@coldstonelabs.org>
Date:   Thu Jan 26 14:43:14 2012 -0600

    Few changes suggested by Dan Vanderkam.

commit a9965f3
Author: Russell Valentine <russ@coldstonelabs.org>
Date:   Thu Jan 26 00:41:44 2012 -0600

    Stroke patterns in the legend that scale to 1em.

commit 62f2905
Author: Russell Valentine <russ@coldstonelabs.org>
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 bfece39
Author: Russell Valentine <russ@coldstonelabs.org>
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 57539c8
Author: Russell Valentine <russ@coldstonelabs.org>
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 de28662
Author: Russell Valentine <russ@coldstonelabs.org>
Date:   Wed Jan 18 22:48:16 2012 -0600

    Use "font-weight: bold"  in style instead of the bold tag.

commit f595823
Author: Russell Valentine <russ@coldstonelabs.org>
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 4b5e255
Author: Russell Valentine <russ@coldstonelabs.org>
Date:   Wed Jan 18 14:33:13 2012 -0600

    Actual dashedLine coordinates checked now. Oops.

commit a5930c6
Author: Russell Valentine <russ@coldstonelabs.org>
Date:   Wed Jan 18 14:25:51 2012 -0600

    per series stroke pattern support.
  • Loading branch information
bluthen authored and danvk committed Jan 26, 2012
1 parent db2e28c commit 79253bd
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 24 deletions.
24 changes: 24 additions & 0 deletions auto_tests/tests/simple_drawing.js
Expand Up @@ -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"));
};
104 changes: 99 additions & 5 deletions dygraph-canvas.js
Expand Up @@ -28,6 +28,7 @@
/*global Dygraph:false,RGBColor:false */
"use strict";


var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
this.dygraph_ = dygraph;

Expand Down Expand Up @@ -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)) {
Expand All @@ -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.
Expand All @@ -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();
}
}
Expand All @@ -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();
};
7 changes: 7 additions & 0 deletions dygraph-options-reference.js
Expand Up @@ -263,6 +263,13 @@ Dygraph.OPTIONS_REFERENCE = // <JSON>
"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<integer>",
"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"],
Expand Down
30 changes: 30 additions & 0 deletions dygraph-utils.js
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
};
92 changes: 87 additions & 5 deletions dygraph.js
Expand Up @@ -1548,6 +1548,79 @@ Dygraph.prototype.idxToRow_ = function(idx) {
return -1;
};

/**
* @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 "&mdash;";
}
if (!strokePattern || strokePattern.length <= 1) {
// Solid line
dash = "<div style=\"display: inline-block; position: relative; " +
"bottom: .5ex; padding-left: 1em; height: 1px; " +
"border-bottom: 2px solid " + color + ";\"></div>";
} 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 += "<div style=\"display: inline-block; position: relative; " +
"bottom: .5ex; margin-right: " + marginRight + "em; padding-left: " +
paddingLeft + "em; height: 1px; border-bottom: 2px solid " + color +
";\"></div>";
}
}
}
return dash;
};

/**
* @private
* Generates HTML for the legend which is displayed when hovering over the
Expand All @@ -1556,12 +1629,13 @@ Dygraph.prototype.idxToRow_ = function(idx) {
* @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 '';

Expand All @@ -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 ? '<br/>' : ' ');
html += "<b><span style='color: " + c + ";'>&mdash;" + labels[i] +
"</span></b>";
strokePattern = this.attr_("strokePattern", labels[i]);
dash = this.generateLegendDashHTML_(strokePattern, c, oneEmWidth);
html += "<span style='font-weight: bold; color: " + c + ";'>" + dash +
" " + labels[i] + "</span>";
}
return html;
}
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 79253bd

Please sign in to comment.