Merge pull request #469 from danvk/re-smooth-plotter
authorDan Vanderkam <danvdk@gmail.com>
Sun, 16 Nov 2014 21:19:32 +0000 (16:19 -0500)
committerDan Vanderkam <danvdk@gmail.com>
Sun, 16 Nov 2014 21:19:32 +0000 (16:19 -0500)
Smooth plotter using bezier curves

auto_tests/misc/local.html
auto_tests/tests/smooth_plotter.js [new file with mode: 0644]
extras/smooth-plotter.js [new file with mode: 0644]
tests/plotters.html
tests/smooth-plots.html [new file with mode: 0644]

index fa74656..b3b4080 100644 (file)
@@ -7,6 +7,7 @@
   <script type="text/javascript" src="../../excanvas.js"></script>
   <![endif]-->
   <script type="text/javascript" src="../../dygraph-dev.js"></script>
+  <script type="text/javascript" src="../../extras/smooth-plotter.js"></script>
 
   <!-- Scripts for library support -->
   <script type="text/javascript" src="../lib/jquery-1.4.2.js"></script>
@@ -61,6 +62,7 @@
   <script type="text/javascript" src="../tests/plugins_legend.js"></script>
   <script type="text/javascript" src="../tests/two_digit_years.js"></script>
   <script type="text/javascript" src="../tests/hidpi.js"></script>
+  <script type="text/javascript" src="../tests/smooth_plotter.js"></script>
   <script type="text/javascript" src="../tests/update_options.js"></script>
   <script type="text/javascript" src="../tests/update_while_panning.js"></script>
   <script type="text/javascript" src="../tests/utils_test.js"></script>
diff --git a/auto_tests/tests/smooth_plotter.js b/auto_tests/tests/smooth_plotter.js
new file mode 100644 (file)
index 0000000..2268bb3
--- /dev/null
@@ -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 (file)
index 0000000..119bf3f
--- /dev/null
@@ -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;
+
+})();
index 30a4fd9..104ab78 100644 (file)
@@ -51,6 +51,9 @@
     and showing error bars only for some series.</p>
     <div id="mixed-error" class="chart"></div>
 
+    <h2>Smooth Lines</h2>
+    <p>See the <a href="smooth-plots.html">smooth-plots demo</a> for an example of a custom plotter which connects points using bezier curves instead of straight lines.</p>
+
     <script type="text/javascript">
       // Darken a color
       function darkenColor(colorStr) {
diff --git a/tests/smooth-plots.html b/tests/smooth-plots.html
new file mode 100644 (file)
index 0000000..175da2d
--- /dev/null
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7; IE=EmulateIE9">
+    <title>Plotters demo</title>
+    <!--[if IE]>
+    <script type="text/javascript" src="../excanvas.js"></script>
+    <![endif]-->
+    <script type="text/javascript" src="../dygraph-dev.js"></script>
+    <script type="text/javascript" src="../extras/smooth-plotter.js"></script>
+
+    <style type="text/css">
+      body {
+        max-width: 700px;
+      }
+      div.chart {
+        width: 640px;
+        height: 320px;
+      }
+      input[type="range"] {
+        width: 400px;
+      }
+      .smoother {
+        margin-left: 50px;
+      }
+    </style>
+  </head>
+  <body>
+    <h2>Smooth Lines</h2>
+    <p>This plotter draws smooth lines between points using bezier curves.</p>
+    <p class="smoother">Smoothing:&nbsp;<input type="range" id="smoothing-amount" min=0 max=0.7 step=0.01 value=0.33> <span id="smoothing-param">0.33</span></p>
+    <div id="smooth-line" class="chart"></div>
+
+    <p>View source to see how this works. You'll have to source <code>extras/smooth-plotter.js</code> in addition to dygraphs to get this feature. See the <a href="https://github.com/danvk/dygraphs/pull/469">pull request</a> that introduced this plotter to learn more about how it smooths your curves.</p>
+
+<script type="text/javascript">
+// Smooth line plotter
+var functionData = [];
+var vs = [10, 20, 40, 0, 30, 15, 25, 60, 35, 45];
+for (var i = 0; i < 10; i++) {
+  var v = vs[i];
+  functionData.push([i, v, v]);
+}
+
+var g6;
+function drawSmoothPlot() {
+  g6 = new Dygraph(document.getElementById('smooth-line'),
+                   functionData,
+                   {
+                     labels: ['Year', 'Straight', 'Smoothed'],
+                     series: {
+                       Straight: {
+                         color: 'rgba(0,0,0,0.33)',
+                         strokeWidth: 2,
+                         drawPoints: true,
+                         pointSize: 3
+                       },
+                       Smoothed: {
+                         plotter: smoothPlotter,
+                         color: 'red',
+                         strokeWidth: 2
+                       }
+                     },
+                     legend: 'always',
+                     gridLineColor: '#ddd'
+                   });
+}
+drawSmoothPlot();
+
+var smoothRangeEl = document.getElementById('smoothing-amount');
+smoothRangeEl.addEventListener('input', function() {
+  var param = parseFloat(smoothRangeEl.value);
+  smoothPlotter.smoothing = param;
+  document.getElementById('smoothing-param').innerHTML = param;
+  drawSmoothPlot();
+});
+</script>
+</body>
+</html>