console
function pchip(xs, ys, mono, extra) {
if (xs.length == 0) {
return function nan() { return NaN; };
}
if (xs.length == 1) {
if (!extra && x < xs[0]) return ys[0];
if (!extra && x > xs[xs.length - 1]) return ys[xs.length - 1];
return function constance() { return ys[0]; };
}
if (xs.length == 2) {
var k = (ys[1] - ys[0]) / (xs[1] - xs[0]);
var b = ys[0] - k * xs[0];
return function linear_fn(x) {
return b + k * x;
};
}
function piece(t, y0, y1, dy0, dy1) {
var a = (dy0 + dy1 + (2 * y0 - 2 * y1) / t) / t / t;
var b = (-2 * dy0 - dy1 - (3 * y0 - 3 * y1) / t) / t;
var c = dy0;
var d = y0;
return function (x) { return d + x * (c + x * (b + x * a)) }
}
function abs(x) {
if (x < 0) return -x;
return x;
}
var delta = [];
var di = [];
var i, l = ys.length;
for (i = 0; i < l - 1; i++) {
delta[i] = (ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]);
}
delta[-1] = 2 * delta[0] - delta[1];
delta[-2] = 2 * delta[-1] - delta[0];
delta[l - 1] = 2 * delta[l - 2] - delta[l - 3];
delta[l] = 2 * delta[l - 1] - delta[l - 2];
var w1, w2, w, s1, s2, h1, h2, a;
if (mono) {
for (i = 0; i < l; i++) {
s1 = delta[i - 1];
s2 = delta[i];
h1 = xs[i] - xs[i - 1];
h2 = xs[i + 1] - xs[i];
if (isNaN(h1)) h1 = h2;
if (isNaN(h2)) h2 = h1;
a = (h1 + 2 * h2) / 3 / (h1 + h2);
if (s1 * s2 > 0) {
di[i] = s1 * s2 / (a * s2 + (1 - a) * s1);
} else {
di[i] = 0;
}
}
} else {
for (i = 0; i < l; i++) {
w1 = abs(delta[i + 1] - delta[i]) + abs(delta[i + 1] + delta[i]) / 2;
w2 = abs(delta[i - 1] - delta[i]) + abs(delta[i - 1] + delta[i]) / 2;
if (w1 == 0 && w2 == 0) w = 0.5;
else w = w1 / (w1 + w2);
di[i] = (delta[i - 1] - delta[i]) * w + delta[i];
}
}
var fnList = [];
for (i = 1; i < l; i++) {
fnList[i - 1] = piece(xs[i] - xs[i - 1], ys[i - 1], ys[i], di[i - 1], di[i]);
}
l -= 1;
return function piecewize(x) {
if (x < xs[0]) {
if (extra) {
return ys[0] + di[0] * (x - xs[0]);
} else {
return ys[0];
}
}
if (x > xs[l]) {
if (extra) {
return ys[l] + di[l] * (x - xs[l]);
} else {
return ys[l];
}
}
var i;
for (i = 1; i < l; i++) {
if (xs[i] > x) break;
}
return fnList[i - 1](x - xs[i - 1]);
}
}
cv.width = window.innerWidth - 30;
cv.height = cv.width * 0.75;
pts = [];
cv.onclick = function (e) {
var rect = cv.getBoundingClientRect()
var x = e.clientX - rect.left;
var y = e.clientY - rect.top;
pts.push({ x: x, y: y });
pts.sort(function (a, b) {
return a.x - b.x;
});
var xs = pts.map(function (p) { return p.x });
var ys = pts.map(function (p) { return p.y });
var fn = pchip(xs, ys, false, true);
var fn2 = pchip(xs, ys, true, true)
var i;
var ctx = cv.getContext("2d");
ctx.clearRect(0, 0, 9999, 9999)
ctx.fillStyle = "red";
for (i = pts.length - 1; i >= 0; i--) {
ctx.fillRect(pts[i].x - 1, pts[i].y - 1, 3, 3);
}
ctx.fillStyle = isDark() ? "white" : "black";
for (i = 0; i < cv.width; i++) {
ctx.fillRect(i, fn(i), 1, 1);
}
ctx.fillStyle = isDark() ? "yellow" : "green";
for (i = 0; i < cv.width; i++) {
ctx.fillRect(i, fn2(i), 1, 1);
}
}
cv.oncontextmenu = function (event) {
pts.length = 0;
cv.getContext("2d").clearRect(0, 0, 9999, 9999);
event.preventDefault();
}
function isDark() {
return !!window.matchMedia('(prefers-color-scheme: dark)').matches;;
}
window.onresize = function() {
var ele = document.querySelector("details div");
ele.style.height = "auto";
ele.style.setProperty("--ch", getComputedStyle(ele).height);
ele.style.height = null;
}
window.onresize();
<h1>分段三次多项式插值</h1>
<p>
使用 MATLAB 中的 “<a href="https://ww2.mathworks.cn/help/matlab/ref/makima.html">makima</a>” 和 “<a href="https://ww2.mathworks.cn/help/matlab/ref/pchip.html">phicp</a>” 插值方案。
</p>
<details>
<summary>如果你需要些背景……</summary>
<div>
<p>
插值解决的问题是:已知某函数,给定一些点,让函数过这个点。
最简单的方案是每两个点之间使用直线连接,但在所给定点处,曲线不光滑。
另外的方法是使用一个多项式进行插值。“拉格朗日插值”和“牛顿插值”干的就是这件事。
使用多项式插值,在点少的时候可以取得不错的效果,在各个方面用于数据的精细估计。
然而,当给定的点多了之后,就会有“龙塔效应”,插值出来的曲线会有剧烈的抖动。
</p>
<p>
分段三次多项式插值是另外的常见方案。每两个点之间采用三次多项式。相比于简单的直线连接,
曲线更加光滑;相比于单多项式插值,阶数少,不会有剧烈的“龙塔效应”。因此,分段三次多项式插值
在各种场景下得到应用。
</p>
<p>
最常见的插值方案是“三次样条线“,保证曲线二阶导数连续。但在实际使用的时候仍有有可能产生抖动。而这里的两个插值方案
设计为“好看”。“makima” 具有适当的抖动和平滑性,而 “phicp” 保证 “不会超调” 。这些插值方法只保证一阶导数连续,
因此适合数据输入以及数据可视化,而不适合用于求精度的插值。
</p>
</div>
</details>
<p>在下面的方框点(至少三个)点查看效果。右键清除选择.</p>
<p>绿/黄色点为 phicp 插值方案,黑/白色点为 makima 插值方案。</p>
<canvas id="cv"></canvas>
canvas {
outline: cornflowerblue solid 1px;
}
canvas {
background-color: white;
}
@media (prefers-color-scheme: dark) {
canvas {
background-color: black;
}
}
body{
font-family: sans-serif;
background: white;
max-width: 800px;
margin: auto;
padding: 0 1em;
}
h1 {
text-align: center;
border-bottom: 1px solid red;
}
p {
margin-top: 0px;
margin-bottom: 1.2em;
font-weight: 300;
text-indent: 2em;
}
details {
padding: 0em 1em;
transition: all 0.3s;
box-sizing: border-box;
border: solid 1px;
border-color: white;
border-bottom: 2px solid currentColor;
}
details div {
height: 0;
overflow: hidden;
transition: all 0.3s;
margin-bottom: -1em;
}
summary {
color: cornflowerblue;
text-decoration: underline;
cursor:pointer;
margin-left: -1em;
transition: margin-left 0.3s;
user-select: none;
padding-bottom: 0em;
}
[open] summary {
margin-left: 0;
user-select: text;
padding-bottom: 1em;
}
details[open] div {
height: var(--ch);
}
@media (prefers-color-scheme: dark) {
body {
background-color: black;
}
details {
border-color:black;
}
}
details[open] {
padding: 1em 1em;
border-color: gray;
border-width: 1px;
}