From: Dan Vanderkam Date: Sun, 16 Nov 2014 21:19:32 +0000 (-0500) Subject: Merge pull request #469 from danvk/re-smooth-plotter X-Git-Tag: v1.1.0~25 X-Git-Url: https://adrianiainlam.tk/git/?a=commitdiff_plain;h=eec4fd0cb7dfc4dc661f00fca0c5ede94e8512a7;hp=33b5c4b245bf29f3e908931bf70dde00b5fc8a51;p=dygraphs.git Merge pull request #469 from danvk/re-smooth-plotter Smooth plotter using bezier curves --- diff --git a/auto_tests/misc/local.html b/auto_tests/misc/local.html index fa74656..b3b4080 100644 --- a/auto_tests/misc/local.html +++ b/auto_tests/misc/local.html @@ -7,6 +7,7 @@ + @@ -61,6 +62,7 @@ + diff --git a/auto_tests/tests/smooth_plotter.js b/auto_tests/tests/smooth_plotter.js new file mode 100644 index 0000000..2268bb3 --- /dev/null +++ b/auto_tests/tests/smooth_plotter.js @@ -0,0 +1,45 @@ +/** + * @fileoverview Tests for the smooth (bezier curve) plotter. + * + * @author danvdk@gmail.com (Dan Vanderkam) + */ +var smoothPlotterTestCase = TestCase("smooth-plotter"); + +var getControlPoints = smoothPlotter._getControlPoints; + +smoothPlotterTestCase.prototype.setUp = function() { +}; + +smoothPlotterTestCase.prototype.tearDown = function() { +}; + +smoothPlotterTestCase.prototype.testNoSmoothing = function() { + var lastPt = {x: 10, y: 0}, + pt = {x: 11, y: 1}, + nextPt = {x: 12, y: 0}, + alpha = 0; + + assertEquals([11, 1, 11, 1], getControlPoints(lastPt, pt, nextPt, alpha)); +}; + +smoothPlotterTestCase.prototype.testHalfSmoothing = function() { + var lastPt = {x: 10, y: 0}, + pt = {x: 11, y: 1}, + nextPt = {x: 12, y: 0}, + alpha = 0.5; + + assertEquals([10.5, 1, 11.5, 1], getControlPoints(lastPt, pt, nextPt, alpha)); +} + +smoothPlotterTestCase.prototype.testExtrema = function() { + var lastPt = {x: 10, y: 0}, + pt = {x: 11, y: 1}, + nextPt = {x: 12, y: 1}, + alpha = 0.5; + + assertEquals([10.5, 0.75, 11.5, 1.25], + getControlPoints(lastPt, pt, nextPt, alpha, true)); + + assertEquals([10.5, 1, 11.5, 1], + getControlPoints(lastPt, pt, nextPt, alpha, false)); +} diff --git a/extras/smooth-plotter.js b/extras/smooth-plotter.js new file mode 100644 index 0000000..119bf3f --- /dev/null +++ b/extras/smooth-plotter.js @@ -0,0 +1,126 @@ +var smoothPlotter = (function() { +"use strict"; + +/** + * Given three sequential points, p0, p1 and p2, find the left and right + * control points for p1. + * + * The three points are expected to have x and y properties. + * + * The alpha parameter controls the amount of smoothing. + * If α=0, then both control points will be the same as p1 (i.e. no smoothing). + * + * Returns [l1x, l1y, r1x, r1y] + * + * It's guaranteed that the line from (l1x, l1y)-(r1x, r1y) passes through p1. + * Unless allowFalseExtrema is set, then it's also guaranteed that: + * l1y ∈ [p0.y, p1.y] + * r1y ∈ [p1.y, p2.y] + * + * The basic algorithm is: + * 1. Put the control points l1 and r1 α of the way down (p0, p1) and (p1, p2). + * 2. Shift l1 and r2 so that the line l1–r1 passes through p1 + * 3. Adjust to prevent false extrema while keeping p1 on the l1–r1 line. + * + * This is loosely based on the HighCharts algorithm. + */ +function getControlPoints(p0, p1, p2, opt_alpha, opt_allowFalseExtrema) { + var alpha = (opt_alpha !== undefined) ? opt_alpha : 1/3; // 0=no smoothing, 1=crazy smoothing + var allowFalseExtrema = opt_allowFalseExtrema || false; + + if (!p2) { + return [p1.x, p1.y, null, null]; + } + + // Step 1: Position the control points along each line segment. + var l1x = (1 - alpha) * p1.x + alpha * p0.x, + l1y = (1 - alpha) * p1.y + alpha * p0.y, + r1x = (1 - alpha) * p1.x + alpha * p2.x, + r1y = (1 - alpha) * p1.y + alpha * p2.y; + + // Step 2: shift the points up so that p1 is on the l1–r1 line. + if (l1x != r1x) { + // This can be derived w/ some basic algebra. + var deltaY = p1.y - r1y - (p1.x - r1x) * (l1y - r1y) / (l1x - r1x); + l1y += deltaY; + r1y += deltaY; + } + + // Step 3: correct to avoid false extrema. + if (!allowFalseExtrema) { + if (l1y > p0.y && l1y > p1.y) { + l1y = Math.max(p0.y, p1.y); + r1y = 2 * p1.y - l1y; + } else if (l1y < p0.y && l1y < p1.y) { + l1y = Math.min(p0.y, p1.y); + r1y = 2 * p1.y - l1y; + } + + if (r1y > p1.y && r1y > p2.y) { + r1y = Math.max(p1.y, p2.y); + l1y = 2 * p1.y - r1y; + } else if (r1y < p1.y && r1y < p2.y) { + r1y = Math.min(p1.y, p2.y); + l1y = 2 * p1.y - r1y; + } + } + + return [l1x, l1y, r1x, r1y]; +} + + +// A plotter which uses splines to create a smooth curve. +// See tests/plotters.html for a demo. +// Can be controlled via smoothPlotter.smoothing +function smoothPlotter(e) { + var ctx = e.drawingContext, + points = e.points; + + ctx.beginPath(); + ctx.moveTo(points[0].canvasx, points[0].canvasy); + + // right control point for previous point + var lastRightX = points[0].canvasx, lastRightY = points[0].canvasy; + var isOK = Dygraph.isOK; // i.e. is none of (null, undefined, NaN) + + for (var i = 1; i < points.length; i++) { + var p0 = points[i - 1], + p1 = points[i], + p2 = points[i + 1]; + p0 = p0 && isOK(p0.canvasy) ? p0 : null; + p1 = p1 && isOK(p1.canvasy) ? p1 : null; + p2 = p2 && isOK(p2.canvasy) ? p2 : null; + if (p0 && p1) { + var controls = getControlPoints({x: p0.canvasx, y: p0.canvasy}, + {x: p1.canvasx, y: p1.canvasy}, + p2 && {x: p2.canvasx, y: p2.canvasy}, + smoothPlotter.smoothing); + // Uncomment to show the control points: + // ctx.lineTo(lastRightX, lastRightY); + // ctx.lineTo(controls[0], controls[1]); + // ctx.lineTo(p1.canvasx, p1.canvasy); + lastRightX = (lastRightX !== null) ? lastRightX : p0.canvasx; + lastRightY = (lastRightY !== null) ? lastRightY : p0.canvasy; + ctx.bezierCurveTo(lastRightX, lastRightY, + controls[0], controls[1], + p1.canvasx, p1.canvasy); + lastRightX = controls[2]; + lastRightY = controls[3]; + } else if (p1) { + // We're starting again after a missing point. + ctx.moveTo(p1.canvasx, p1.canvasy); + lastRightX = p1.canvasx; + lastRightY = p1.canvasy; + } else { + lastRightX = lastRightY = null; + } + } + + ctx.stroke(); +} +smoothPlotter.smoothing = 1/3; +smoothPlotter._getControlPoints = getControlPoints; // for testing + +return smoothPlotter; + +})(); diff --git a/tests/plotters.html b/tests/plotters.html index 30a4fd9..104ab78 100644 --- a/tests/plotters.html +++ b/tests/plotters.html @@ -51,6 +51,9 @@ and showing error bars only for some series.

+

Smooth Lines

+

See the smooth-plots demo for an example of a custom plotter which connects points using bezier curves instead of straight lines.

+ + + + + + +

Smooth Lines

+

This plotter draws smooth lines between points using bezier curves.

+

Smoothing:  0.33

+
+ +

View source to see how this works. You'll have to source extras/smooth-plotter.js in addition to dygraphs to get this feature. See the pull request that introduced this plotter to learn more about how it smooths your curves.

+ + + +