Commit | Line | Data |
---|---|---|
c36a62c2 DV |
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 | })(); |