From c36a62c2638dd9a4ce0c91146b9bcb2557168aff Mon Sep 17 00:00:00 2001
From: Dan Vanderkam
Date: Fri, 14 Nov 2014 17:04:37 -0500
Subject: [PATCH] Smooth plotter using bezier curves.
---
auto_tests/misc/local.html | 2 +
auto_tests/tests/smooth_plotter.js | 45 +++++++++++++
extras/smooth-plotter.js | 126 +++++++++++++++++++++++++++++++++++++
tests/plotters.html | 48 ++++++++++++++
4 files changed, 221 insertions(+)
create mode 100644 auto_tests/tests/smooth_plotter.js
create mode 100644 extras/smooth-plotter.js
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..7f5242b 100644
--- a/tests/plotters.html
+++ b/tests/plotters.html
@@ -7,6 +7,7 @@
+
@@ -51,6 +55,11 @@
and showing error bars only for some series.
+ Smooth Lines
+ This plotter draws smooth lines between points using bezier curves:
+ Smoothing:
+
+