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
25c40460bbd25f05cfd0be9da3535dd4662cfadb
Author: Russell Valentine <russ@coldstonelabs.org>
Date: Thu Jan 26 14:43:14 2012 -0600
Few changes suggested by Dan Vanderkam.
commit
a9965f34b5470c2db5656cac1d8d3debd3a0d4e7
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
62f2905c52e6936339dd1de7af1844aaced95c05
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
bfece39cc4f2e365cf692b81cffdf91a773625ac
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
57539c89e29a1e45a6d1000ebe53c496eb88a61b
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
de286626b93dc7ce49ce50abfba83ffaa29db068
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
f5958230321df474bfb1339b9a97c64f92f20621
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
4b5e255dff699f59a5ced5186c4e4558b09a4003
Author: Russell Valentine <russ@coldstonelabs.org>
Date: Wed Jan 18 14:33:13 2012 -0600
Actual dashedLine coordinates checked now. Oops.
commit
a5930c675465e6566854d3b6e2b9d6fa606ce91c
Author: Russell Valentine <russ@coldstonelabs.org>
Date: Wed Jan 18 14:25:51 2012 -0600
per series stroke pattern support.
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"));
+};
/*global Dygraph:false,RGBColor:false */
"use strict";
+
var DygraphCanvasRenderer = function(dygraph, element, elementContext, layout) {
this.dygraph_ = dygraph;
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)) {
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.
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();
}
}
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();
+};
"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"],
// 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.
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;
+};
/**
* @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 = "<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
* 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 '';
if (!this.visibility()[i - 1]) continue;
c = this.plotter_.colors[labels[i]];
if (html !== '') html += (sepLines ? '<br/>' : ' ');
- html += "<b><span style='color: " + c + ";'>—" + 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;
}
* 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 {
<body>
<h2>Chart with per-series properties</h2>
<div id="demodiv"></div>
-
+ <h2>Chart with per-series properties with legend.</h2>
+ <div id="demodiv2"></div>
<script type="text/javascript">
+ data = function() {
+ var zp = function(x) { if (x < 10) return "0"+x; else return x; };
+ var r = "date,parabola,line,another line,sine wave,sine wave2\n";
+ for (var i=1; i<=31; i++) {
+ r += "200610" + zp(i);
+ r += "," + 10*(i*(31-i));
+ r += "," + 10*(8*i);
+ r += "," + 10*(250 - 8*i);
+ r += "," + 10*(125 + 125 * Math.sin(0.3*i));
+ r += "," + 10*(125 + 125 * Math.sin(0.3*i+Math.PI));
+ r += "\n";
+ }
+ return r;
+ };
g = new Dygraph(
document.getElementById("demodiv"),
- function() {
- var zp = function(x) { if (x < 10) return "0"+x; else return x; };
- var r = "date,parabola,line,another line,sine wave\n";
- for (var i=1; i<=31; i++) {
- r += "200610" + zp(i);
- r += "," + 10*(i*(31-i));
- r += "," + 10*(8*i);
- r += "," + 10*(250 - 8*i);
- r += "," + 10*(125 + 125 * Math.sin(0.3*i));
- r += "\n";
- }
- return r;
- },
+ data,
{
strokeWidth: 2,
'parabola': {
'sine wave': {
strokeWidth: 3,
highlightCircleSize: 10
+ },
+ 'sine wave2': {
+ strokePattern: [10, 2, 5, 2],
+ strokeWidth: 2,
+ highlightCircleSize: 3
+ }
+ }
+ );
+ g2 = new Dygraph(
+ document.getElementById("demodiv2"),
+ data,
+ {
+ legend: 'always',
+ strokeWidth: 2,
+ 'parabola': {
+ strokePattern: null,
+ drawPoints: true,
+ pointSize: 4,
+ highlightCircleSize: 6
+ },
+ 'line': {
+ strokePattern: Dygraph.DASHED_LINE,
+ strokeWidth: 1.0,
+ drawPoints: true,
+ pointSize: 1.5
+ },
+ 'another line': {
+ strokePattern: [25, 5]
+ },
+ 'sine wave': {
+ strokePattern: Dygraph.DOTTED_LINE,
+ strokeWidth: 3,
+ highlightCircleSize: 10
+ },
+ 'sine wave2': {
+ strokePattern: Dygraph.DOT_DASH_LINE,
+ strokeWidth: 2,
+ highlightCircleSize: 3
}
}
);
+
</script>
</body>
</html>