diff --git a/README.md b/README.md
index 08bee8e..4438a9d 100644
--- a/README.md
+++ b/README.md
@@ -231,7 +231,7 @@ Returns an interpolator between the two hue angles *a* and *b*. If either hue is
### Splines
-Whereas standard interpolators blend from a starting value *a* at *t* = 0 to an ending value *b* at *t* = 1, spline interpolators smoothly blend multiple input values for *t* in [0,1] using piecewise polynomial functions. Only cubic uniform nonrational [B-splines](https://en.wikipedia.org/wiki/B-spline) are currently supported, also known as basis splines.
+Whereas standard interpolators blend from a starting value *a* at *t* = 0 to an ending value *b* at *t* = 1, spline interpolators smoothly blend multiple input values for *t* in [0,1] using piecewise polynomial functions. Cubic uniform nonrational [B-splines](https://en.wikipedia.org/wiki/B-spline), also known as basis splines, are supported, as well as the [cubic Hermite splines](https://en.wikipedia.org/wiki/Cubic_Hermite_spline) and [monotone cubic splines](https://en.wikipedia.org/wiki/Monotone_cubic_interpolation).
# d3.interpolateBasis(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/basis.js), [Examples](https://observablehq.com/@d3/d3-interpolatebasis)
@@ -241,6 +241,24 @@ Returns a uniform nonrational B-spline interpolator through the specified array
Returns a uniform nonrational B-spline interpolator through the specified array of *values*, which must be numbers. The control points are implicitly repeated such that the resulting one-dimensional spline has cyclical C² continuity when repeated around *t* in [0,1]. See also [d3.curveBasisClosed](https://github.com/d3/d3-shape/blob/master/README.md#curveBasisClosed).
+# d3.interpolateCubic(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/cubic.js)
+
+Returns a cubic Hermite spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length - 1).
+
+# d3.interpolateCubicClosed(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/cubic.js)
+
+Returns a closed cubic Hermite spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length), and is cyclical (*f*(1 + *t*) = *f*(*t*)).
+
+
+# d3.interpolateMonotone(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/monotone.js)
+
+Returns a monotone cubic spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length - 1).
+
+# d3.interpolateMonotoneClosed(values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/monotone.js)
+
+Returns a closed monotone cubic spline interpolator through the specified array of *values*, which must be numbers. The interpolator returns *values*[*i*] at *t* = *i* / (*values*.length), and is cyclical (*f*(1 + *t*) = *f*(*t*)).
+
+
### Piecewise
# d3.piecewise(interpolate, values) · [Source](https://github.com/d3/d3-interpolate/blob/master/src/piecewise.js), [Examples](https://observablehq.com/@d3/d3-piecewise)
diff --git a/src/cubic.js b/src/cubic.js
new file mode 100644
index 0000000..4564f5b
--- /dev/null
+++ b/src/cubic.js
@@ -0,0 +1,40 @@
+import {clamp, floor, frac, min} from "./math.js";
+
+export default function cubic(values, type) {
+ let n = values.length - 1, k;
+ values = values.slice();
+ switch (type) {
+ case "closed":
+ values.unshift(values[n]);
+ values.push(values[1]);
+ values.push(values[2]);
+ n += 2;
+ k = 1 - 1 / n;
+ return t => cubic(k * frac(t));
+ case "open":
+ throw new Error('open cubic spline not implemented yet');
+ case "clamped":
+ default:
+ values.push(2 * values[n] - values[n - 1]);
+ values.unshift(2 * values[0] - values[1]);
+ return t => cubic(clamp(t, 0, 1));
+ }
+
+ function cubic(t) {
+ const i = min(n - 1, floor(t * n)),
+ v0 = values[i],
+ v1 = values[i + 1],
+ v2 = values[i + 2],
+ v3 = values[i + 3],
+ d = t * n - i,
+ s20 = v2 - v0,
+ s31 = v3 - v1,
+ s21 = (v2 - v1) * 2;
+ return (((s20 + s31 - 2 * s21) * d + (3 * s21 - 2 * s20 - s31)) * d + s20)
+ * d / 2 + v1;
+ }
+}
+
+export function closed (values) {
+ return cubic(values, "closed");
+}
diff --git a/src/index.js b/src/index.js
index b4dce7d..7578b2d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -5,6 +5,8 @@ export {default as interpolateBasisClosed} from "./basisClosed.js";
export {default as interpolateDate} from "./date.js";
export {default as interpolateDiscrete} from "./discrete.js";
export {default as interpolateHue} from "./hue.js";
+export {default as interpolateCubic, closed as interpolateCubicClosed} from "./cubic.js";
+export {default as interpolateMonotone, closed as interpolateMonotoneClosed} from "./monotone.js";
export {default as interpolateNumber} from "./number.js";
export {default as interpolateNumberArray} from "./numberArray.js";
export {default as interpolateObject} from "./object.js";
diff --git a/src/math.js b/src/math.js
new file mode 100644
index 0000000..cbee5e8
--- /dev/null
+++ b/src/math.js
@@ -0,0 +1,11 @@
+export var abs = Math.abs;
+export var floor = Math.floor;
+export var max = Math.max;
+export var min = Math.min;
+export var sign = Math.sign || function(x) { return x > 0 ? 1 : x < 0 ? -1 : 0; };
+export function frac(t) {
+ return t - floor(t);
+}
+export function clamp(t, lo, hi) {
+ return t < lo ? lo : t > hi ? hi : t;
+}
diff --git a/src/monotone.js b/src/monotone.js
new file mode 100644
index 0000000..95504bc
--- /dev/null
+++ b/src/monotone.js
@@ -0,0 +1,42 @@
+import {abs, clamp, frac, min, sign} from "./math.js";
+
+export default function monotone(values, type) {
+ let n = values.length - 1, k;
+ values = values.slice();
+ switch (type) {
+ case "closed":
+ values.unshift(values[n]);
+ values.push(values[1]);
+ values.push(values[2]);
+ n += 2;
+ k = 1 - 1 / n;
+ return t => monotone(k * frac(t));
+ case "open":
+ throw new Error('open monotone spline not implemented yet');
+ case "clamped":
+ default:
+ values.push(2 * values[n] - values[n - 1]);
+ values.unshift(2 * values[0] - values[1]);
+ return t => monotone(clamp(t, 0, 1));
+ }
+
+ function monotone(t) {
+ const i = Math.min(n - 1, Math.floor(t * n)),
+ y_im1 = values[i],
+ y_i = values[i + 1],
+ y_ip1 = values[i + 2],
+ y_ip2 = values[i + 3],
+ d = t * n - i,
+ s_im1 = n * (y_i - y_im1),
+ s_i = n * (y_ip1 - y_i),
+ s_ip1 = n * (y_ip2 - y_ip1),
+ yp_i = (sign(s_im1) + sign(s_i)) * min(abs(s_im1), abs(s_i), 0.25 * n * abs(y_ip1 - y_im1)),
+ yp_ip1 = (sign(s_i) + sign(s_ip1)) * min(abs(s_i), abs(s_ip1), 0.25 * n * abs(y_ip2 - y_i));
+
+ return (((yp_i + yp_ip1 - 2 * s_i) * d + (3 * s_i - 2 * yp_i - yp_ip1)) * d + yp_i) * (d / n) + y_i;
+ }
+}
+
+export function closed (values) {
+ return monotone(values, "closed");
+}
diff --git a/test/basis-test.js b/test/basis-test.js
new file mode 100644
index 0000000..56dd034
--- /dev/null
+++ b/test/basis-test.js
@@ -0,0 +1,27 @@
+var tape = require("tape"),
+ interpolate = require("../");
+
+require("./inDelta");
+
+tape("interpolateBasis(values)(t) returns the expected values", function(test) {
+ var i = interpolate.interpolateBasis([0, 0, 3]);
+ test.equal(i(-1), 0);
+ test.equal(i(0), 0);
+ test.inDelta(i(0.19), 0.027436);
+ test.inDelta(i(0.21), 0.037044);
+ test.equal(i(1), 3);
+ test.equal(i(1.19), 3);
+ test.end();
+});
+
+tape("interpolateBasisClosed(values)(t) returns the expected values", function(test) {
+ var i = interpolate.interpolateBasisClosed([0, 0, 3]);
+ test.equal(i(-1), 0.5);
+ test.equal(i(0), 0.5);
+ test.inDelta(i(0.19), 0.132350);
+ test.inDelta(i(0.21), 0.150350);
+ test.equal(i(1), 0.5);
+ test.inDelta(i(1.19), 0.132350);
+ test.inDelta(i(0.19 - 3), 0.132350);
+ test.end();
+});
diff --git a/test/cubic-test.js b/test/cubic-test.js
new file mode 100644
index 0000000..ca85508
--- /dev/null
+++ b/test/cubic-test.js
@@ -0,0 +1,37 @@
+var tape = require("tape"),
+ interpolate = require("../");
+
+require("./inDelta");
+
+tape("interpolateCubic(values)(t) returns the expected values", function(test) {
+ var i = interpolate.interpolateCubic([0, 0, 3, 4, 1]);
+ test.equal(i(-1), 0);
+ test.equal(i(0), 0);
+ test.equal(i(0.25), 0);
+ test.equal(i(0.5), 3);
+ test.equal(i(0.75), 4);
+ test.equal(i(1), 1);
+ test.inDelta(i(0.1), -0.144);
+ test.inDelta(i(0.19), -0.207936);
+ test.inDelta(i(0.21), -0.169344);
+ test.equal(i(2), 1);
+ test.end();
+});
+
+tape("interpolateCubicClosed(values)(t) returns the expected values", function(test) {
+ var i = interpolate.interpolateCubicClosed([0, 0, 3, 4, 1]);
+ test.equal(i(0), 0);
+ test.equal(i(0.2), 0);
+ test.equal(i(0.4), 3);
+ test.equal(i(0.6), 4);
+ test.equal(i(0.8), 1);
+ test.equal(i(1), 0);
+ test.inDelta(i(0.1), -0.25);
+ test.inDelta(i(0.19), -0.068875);
+ test.inDelta(i(0.21), 0.0846875);
+ test.inDelta(i(1.1), -0.25);
+ test.inDelta(i(1.19), -0.068875);
+ test.equal(i(-1), 0);
+ test.equal(i(2), 0);
+ test.end();
+});
diff --git a/test/monotone-test.js b/test/monotone-test.js
new file mode 100644
index 0000000..66031aa
--- /dev/null
+++ b/test/monotone-test.js
@@ -0,0 +1,31 @@
+var tape = require("tape"),
+ interpolate = require("../");
+
+require("./inDelta");
+
+tape("interpolateMonotone(values)(t) returns the expected values", function(test) {
+ var i = interpolate.interpolateCubic([3, 2.8, 2.5, 1, 0.95, 0.8, 0.5, 0.1, 0.05]);
+ test.equal(i(-1), 3);
+ test.inDelta(i(0), 3);
+ test.inDelta(i(0.25), 2.5);
+ test.inDelta(i(0.5), 0.95);
+ test.inDelta(i(0.6), 0.8412);
+ test.inDelta(i(0.75), 0.5);
+ test.inDelta(i(1), 0.05);
+ test.inDelta(i(2), 0.05);
+ test.end();
+});
+
+tape("interpolateMonotoneClosed(values)(t) returns the expected values", function(test) {
+ var i = interpolate.interpolateMonotoneClosed([0, 0, 3, 4, 1]);
+ test.equal(i(0), 0);
+ test.inDelta(i(0.2), 0);
+ test.inDelta(i(0.4), 3);
+ test.inDelta(i(0.5), 3.75);
+ test.inDelta(i(0.6), 4);
+ test.inDelta(i(0.8), 1);
+ test.inDelta(i(1), 0);
+ test.inDelta(i(-1), 0);
+ test.inDelta(i(2), 0);
+ test.end();
+});