| 1 | var smoothPlotter = (function() { |
| 2 | "use strict"; |
| 3 | |
| 4 | /** |
| 5 | * Given three sequential points, p0, p1 and p2, find the left and right |
| 6 | * control points for p1. |
| 7 | * |
| 8 | * The three points are expected to have x and y properties. |
| 9 | * |
| 10 | * The alpha parameter controls the amount of smoothing. |
| 11 | * If α=0, then both control points will be the same as p1 (i.e. no smoothing). |
| 12 | * |
| 13 | * Returns [l1x, l1y, r1x, r1y] |
| 14 | * |
| 15 | * It's guaranteed that the line from (l1x, l1y)-(r1x, r1y) passes through p1. |
| 16 | * Unless allowFalseExtrema is set, then it's also guaranteed that: |
| 17 | * l1y ∈ [p0.y, p1.y] |
| 18 | * r1y ∈ [p1.y, p2.y] |
| 19 | * |
| 20 | * The basic algorithm is: |
| 21 | * 1. Put the control points l1 and r1 α of the way down (p0, p1) and (p1, p2). |
| 22 | * 2. Shift l1 and r2 so that the line l1–r1 passes through p1 |
| 23 | * 3. Adjust to prevent false extrema while keeping p1 on the l1–r1 line. |
| 24 | * |
| 25 | * This is loosely based on the HighCharts algorithm. |
| 26 | */ |
| 27 | function getControlPoints(p0, p1, p2, opt_alpha, opt_allowFalseExtrema) { |
| 28 | var alpha = (opt_alpha !== undefined) ? opt_alpha : 1/3; // 0=no smoothing, 1=crazy smoothing |
| 29 | var allowFalseExtrema = opt_allowFalseExtrema || false; |
| 30 | |
| 31 | if (!p2) { |
| 32 | return [p1.x, p1.y, null, null]; |
| 33 | } |
| 34 | |
| 35 | // Step 1: Position the control points along each line segment. |
| 36 | var l1x = (1 - alpha) * p1.x + alpha * p0.x, |
| 37 | l1y = (1 - alpha) * p1.y + alpha * p0.y, |
| 38 | r1x = (1 - alpha) * p1.x + alpha * p2.x, |
| 39 | r1y = (1 - alpha) * p1.y + alpha * p2.y; |
| 40 | |
| 41 | // Step 2: shift the points up so that p1 is on the l1–r1 line. |
| 42 | if (l1x != r1x) { |
| 43 | // This can be derived w/ some basic algebra. |
| 44 | var deltaY = p1.y - r1y - (p1.x - r1x) * (l1y - r1y) / (l1x - r1x); |
| 45 | l1y += deltaY; |
| 46 | r1y += deltaY; |
| 47 | } |
| 48 | |
| 49 | // Step 3: correct to avoid false extrema. |
| 50 | if (!allowFalseExtrema) { |
| 51 | if (l1y > p0.y && l1y > p1.y) { |
| 52 | l1y = Math.max(p0.y, p1.y); |
| 53 | r1y = 2 * p1.y - l1y; |
| 54 | } else if (l1y < p0.y && l1y < p1.y) { |
| 55 | l1y = Math.min(p0.y, p1.y); |
| 56 | r1y = 2 * p1.y - l1y; |
| 57 | } |
| 58 | |
| 59 | if (r1y > p1.y && r1y > p2.y) { |
| 60 | r1y = Math.max(p1.y, p2.y); |
| 61 | l1y = 2 * p1.y - r1y; |
| 62 | } else if (r1y < p1.y && r1y < p2.y) { |
| 63 | r1y = Math.min(p1.y, p2.y); |
| 64 | l1y = 2 * p1.y - r1y; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | return [l1x, l1y, r1x, r1y]; |
| 69 | } |
| 70 | |
| 71 | |
| 72 | // A plotter which uses splines to create a smooth curve. |
| 73 | // See tests/plotters.html for a demo. |
| 74 | // Can be controlled via smoothPlotter.smoothing |
| 75 | function smoothPlotter(e) { |
| 76 | var ctx = e.drawingContext, |
| 77 | points = e.points; |
| 78 | |
| 79 | ctx.beginPath(); |
| 80 | ctx.moveTo(points[0].canvasx, points[0].canvasy); |
| 81 | |
| 82 | // right control point for previous point |
| 83 | var lastRightX = points[0].canvasx, lastRightY = points[0].canvasy; |
| 84 | var isOK = Dygraph.isOK; // i.e. is none of (null, undefined, NaN) |
| 85 | |
| 86 | for (var i = 1; i < points.length; i++) { |
| 87 | var p0 = points[i - 1], |
| 88 | p1 = points[i], |
| 89 | p2 = points[i + 1]; |
| 90 | p0 = p0 && isOK(p0.canvasy) ? p0 : null; |
| 91 | p1 = p1 && isOK(p1.canvasy) ? p1 : null; |
| 92 | p2 = p2 && isOK(p2.canvasy) ? p2 : null; |
| 93 | if (p0 && p1) { |
| 94 | var controls = getControlPoints({x: p0.canvasx, y: p0.canvasy}, |
| 95 | {x: p1.canvasx, y: p1.canvasy}, |
| 96 | p2 && {x: p2.canvasx, y: p2.canvasy}, |
| 97 | smoothPlotter.smoothing); |
| 98 | // Uncomment to show the control points: |
| 99 | // ctx.lineTo(lastRightX, lastRightY); |
| 100 | // ctx.lineTo(controls[0], controls[1]); |
| 101 | // ctx.lineTo(p1.canvasx, p1.canvasy); |
| 102 | lastRightX = (lastRightX !== null) ? lastRightX : p0.canvasx; |
| 103 | lastRightY = (lastRightY !== null) ? lastRightY : p0.canvasy; |
| 104 | ctx.bezierCurveTo(lastRightX, lastRightY, |
| 105 | controls[0], controls[1], |
| 106 | p1.canvasx, p1.canvasy); |
| 107 | lastRightX = controls[2]; |
| 108 | lastRightY = controls[3]; |
| 109 | } else if (p1) { |
| 110 | // We're starting again after a missing point. |
| 111 | ctx.moveTo(p1.canvasx, p1.canvasy); |
| 112 | lastRightX = p1.canvasx; |
| 113 | lastRightY = p1.canvasy; |
| 114 | } else { |
| 115 | lastRightX = lastRightY = null; |
| 116 | } |
| 117 | } |
| 118 | |
| 119 | ctx.stroke(); |
| 120 | } |
| 121 | smoothPlotter.smoothing = 1/3; |
| 122 | smoothPlotter._getControlPoints = getControlPoints; // for testing |
| 123 | |
| 124 | return smoothPlotter; |
| 125 | |
| 126 | })(); |