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