Skip to content

Commit

Permalink
Add monotone interpolation for lines and areas.
Browse files Browse the repository at this point in the history
Based on the Protovis implementation.
  • Loading branch information
jasondavies committed May 2, 2011
1 parent df2e3c2 commit ae5123f
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 5 deletions.
64 changes: 63 additions & 1 deletion d3.js
Original file line number Diff line number Diff line change
Expand Up @@ -2549,7 +2549,8 @@ var d3_svg_lineInterpolators = {
"basis": d3_svg_lineBasis,
"basis-closed": d3_svg_lineBasisClosed,
"cardinal": d3_svg_lineCardinal,
"cardinal-closed": d3_svg_lineCardinalClosed
"cardinal-closed": d3_svg_lineCardinalClosed,
"monotone": d3_svg_lineMonotone
};

// Linear interpolation; generates "L" commands.
Expand Down Expand Up @@ -2744,6 +2745,67 @@ function d3_svg_lineBasisBezier(path, x, y) {
",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x),
",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y));
}

// Interpolates the given points using Fritsch-Carlson Monotone cubic Hermite
// interpolation. Returns an array of tangent vectors.
function d3_svg_lineMonotoneTangents(points) {
var tangents = [],
d = [],
m = [],
dx = [],
k = 0;

// Compute the slopes of the secant lines between successive points.
for (k = 0; k < points.length-1; k++) {
d[k] = (points[k+1][1] - points[k][1]) / (points[k+1][0] - points[k][0]);
}

// Initialize the tangents at every point as the average of the secants.
m[0] = d[0];
dx[0] = points[1][0] - points[0][0];
for (k = 1; k < points.length - 1; k++) {
m[k] = (d[k-1] + d[k]) / 2;
dx[k] = (points[k+1][0] - points[k-1][0]) / 2;
}
m[k] = d[k-1];
dx[k] = (points[k][0] - points[k-1][0]);

// Step 3. Very important, step 3. Yep. Wouldn't miss it.
for (k = 0; k < points.length - 1; k++) {
if (d[k] == 0) {
m[ k ] = 0;
m[k+1] = 0;
}
}

// Step 4 + 5. Out of 5 or more steps.
for (k = 0; k < points.length - 1; k++) {
if ((Math.abs(m[k]) < 1e-5) || (Math.abs(m[k+1]) < 1e-5)) continue;
var ak = m[k] / d[k],
bk = m[k + 1] / d[k],
s = ak * ak + bk * bk; // monotone constant (?)
if (s > 9) {
var tk = 3 / Math.sqrt(s);
m[k] = tk * ak * d[k];
m[k + 1] = tk * bk * d[k];
}
}

var len;
for (var i = 0; i < points.length; i++) {
len = 1 + m[i] * m[i]; // pv.vector(1, m[i]).norm().times(dx[i]/3)
tangents.push([dx[i] / 3 / len, m[i] * dx[i] / 3 / len]);
}

return tangents;
}

function d3_svg_lineMonotone(points) {
return points.length < 3
? d3_svg_lineLinear(points)
: points[0] +
d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points));
}
d3.svg.area = function() {
var x = d3_svg_lineX,
y0 = d3_svg_areaY0,
Expand Down
4 changes: 2 additions & 2 deletions d3.min.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion examples/spline/spline.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ d3.select("#interpolate")
"basis",
"basis-closed",
"cardinal",
"cardinal-closed"
"cardinal-closed",
"monotone"
])
.enter().append("option")
.attr("value", String)
Expand Down
64 changes: 63 additions & 1 deletion src/svg/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ var d3_svg_lineInterpolators = {
"basis": d3_svg_lineBasis,
"basis-closed": d3_svg_lineBasisClosed,
"cardinal": d3_svg_lineCardinal,
"cardinal-closed": d3_svg_lineCardinalClosed
"cardinal-closed": d3_svg_lineCardinalClosed,
"monotone": d3_svg_lineMonotone
};

// Linear interpolation; generates "L" commands.
Expand Down Expand Up @@ -276,3 +277,64 @@ function d3_svg_lineBasisBezier(path, x, y) {
",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x),
",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y));
}

// Interpolates the given points using Fritsch-Carlson Monotone cubic Hermite
// interpolation. Returns an array of tangent vectors.
function d3_svg_lineMonotoneTangents(points) {
var tangents = [],
d = [],
m = [],
dx = [],
k = 0;

// Compute the slopes of the secant lines between successive points.
for (k = 0; k < points.length-1; k++) {
d[k] = (points[k+1][1] - points[k][1]) / (points[k+1][0] - points[k][0]);
}

// Initialize the tangents at every point as the average of the secants.
m[0] = d[0];
dx[0] = points[1][0] - points[0][0];
for (k = 1; k < points.length - 1; k++) {
m[k] = (d[k-1] + d[k]) / 2;
dx[k] = (points[k+1][0] - points[k-1][0]) / 2;
}
m[k] = d[k-1];
dx[k] = (points[k][0] - points[k-1][0]);

// Step 3. Very important, step 3. Yep. Wouldn't miss it.
for (k = 0; k < points.length - 1; k++) {
if (d[k] == 0) {
m[ k ] = 0;
m[k+1] = 0;
}
}

// Step 4 + 5. Out of 5 or more steps.
for (k = 0; k < points.length - 1; k++) {
if ((Math.abs(m[k]) < 1e-5) || (Math.abs(m[k+1]) < 1e-5)) continue;
var ak = m[k] / d[k],
bk = m[k + 1] / d[k],
s = ak * ak + bk * bk; // monotone constant (?)
if (s > 9) {
var tk = 3 / Math.sqrt(s);
m[k] = tk * ak * d[k];
m[k + 1] = tk * bk * d[k];
}
}

var len;
for (var i = 0; i < points.length; i++) {
len = 1 + m[i] * m[i]; // pv.vector(1, m[i]).norm().times(dx[i]/3)
tangents.push([dx[i] / 3 / len, m[i] * dx[i] / 3 / len]);
}

return tangents;
}

function d3_svg_lineMonotone(points) {
return points.length < 3
? d3_svg_lineLinear(points)
: points[0] +
d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points));
}
9 changes: 9 additions & 0 deletions tests/test-svg-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,12 @@ console.log(" [[0, 0]]:", area([[0, 0]]));
console.log(" [[0, 0], [5, 5]]:", area([[0, 0], [5, 5]]));
console.log(" [[0, 0], [5, 5], [10, 0]]:", area([[0, 0], [5, 5], [10, 0]]));
console.log("");

var area = d3.svg.area()
.interpolate("monotone");

console.log("interpolate(monotone):");
console.log(" [[0, 0]]:", area([[0, 0]]));
console.log(" [[0, 0], [5, 5]]:", area([[0, 0], [5, 5]]));
console.log(" [[0, 0], [5, 5], [10, 0]]:", area([[0, 0], [5, 5], [10, 0]]));
console.log("");
5 changes: 5 additions & 0 deletions tests/test-svg-area.out
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ interpolate(cardinal-closed):
[[0, 0], [5, 5]]: M0,0L5,5L5,0L0,0Z
[[0, 0], [5, 5], [10, 0]]: M0,0C-0.7500000000000001,0.7500000000000001,3.5,5,5,5S10.75,0.7500000000000001,10,0S0.7500000000000001,-0.7500000000000001,0,0L10,0C10.75,0,6.5,0,5,0S-0.7500000000000001,0,0,0S9.25,0,10,0Z

interpolate(monotone):
[[0, 0]]: M0,0L0,0Z
[[0, 0], [5, 5]]: M0,0L5,5L5,0L0,0Z
[[0, 0], [5, 5], [10, 0]]: M0,0C0.8333333333333334,0.8333333333333334,3.333333333333333,5,5,5S9.166666666666666,0.8333333333333334,10,0L10,0C8.333333333333334,0,6.666666666666667,0,5,0S1.6666666666666667,0,0,0Z

9 changes: 9 additions & 0 deletions tests/test-svg-line.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,12 @@ console.log(" [[0, 0]]:", line([[0, 0]]));
console.log(" [[0, 0], [5, 5]]:", line([[0, 0], [5, 5]]));
console.log(" [[0, 0], [5, 5], [10, 0]]:", line([[0, 0], [5, 5], [10, 0]]));
console.log("");

var line = d3.svg.line()
.interpolate("monotone");

console.log("interpolate(monotone):");
console.log(" [[0, 0]]:", line([[0, 0]]));
console.log(" [[0, 0], [5, 5]]:", line([[0, 0], [5, 5]]));
console.log(" [[0, 0], [5, 5], [10, 0]]:", line([[0, 0], [5, 5], [10, 0]]));
console.log("");
5 changes: 5 additions & 0 deletions tests/test-svg-line.out
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ interpolate(cardinal-closed):
[[0, 0], [5, 5]]: M0,0L5,5
[[0, 0], [5, 5], [10, 0]]: M0,0C-0.7500000000000001,0.7500000000000001,3.5,5,5,5S10.75,0.7500000000000001,10,0S0.7500000000000001,-0.7500000000000001,0,0

interpolate(monotone):
[[0, 0]]: M0,0
[[0, 0], [5, 5]]: M0,0L5,5
[[0, 0], [5, 5], [10, 0]]: M0,0C0.8333333333333334,0.8333333333333334,3.333333333333333,5,5,5S9.166666666666666,0.8333333333333334,10,0

0 comments on commit ae5123f

Please sign in to comment.