mkCurve = (x0, y0, m, k) => d3.range(Math.max(1, x0 - 8), Math.min(38, x0 + 8) + 0.25, 0.25)
.map(t => [t, y0 + m * (t - x0) + k * (t - x0) ** 2])
panelPlot = (title, C) => {
const A0 = [10, 10], B0 = [14, 8]
return Plot.plot({
height: 270,
x: { label: "x₁", domain: [0, 40] },
y: { label: "x₂", domain: [0, 23] },
marks: [
Plot.line([[0, 20], [20, 0]], { stroke: "#ef4444", strokeWidth: 1.8 }),
Plot.line([[0, 15], [30, 0]], { stroke: "#8b5cf6", strokeWidth: 1.5, strokeDasharray: "5,3" }),
Plot.line([[0, 20], [40, 0]], { stroke: "#10b981", strokeWidth: 1.8 }),
Plot.line(mkCurve(10, 10, -1, 0.05), { stroke: "#3b82f6", strokeWidth: 1.8 }), // 过 A,切初始线(斜率 −1)
Plot.line(mkCurve(14, 8, -0.5, 0.05), { stroke: "#3b82f6", strokeWidth: 1.2, strokeOpacity: 0.55 }), // 过 B,切补偿线(斜率 −0.5)
Plot.line(mkCurve(C[0], C[1], -0.5, 0.05), { stroke: "#3b82f6", strokeWidth: 1.2, strokeOpacity: 0.55 }), // 过 C,切最终线
Plot.dot([A0, B0, [C[0], C[1]]], { fill: ["#ef4444", "#8b5cf6", "#10b981"], r: 5.5, stroke: "black" }),
Plot.text([A0, B0, [C[0], C[1]]], { text: ["A", "B", "C"], dy: -11, fontSize: 13, fontWeight: "bold" }),
...arrowMark(10, 14, 1.8, "#2563eb"),
Plot.text([[12, 1.8]], { text: ["SE"], dy: -7, fontSize: 11, fill: "#2563eb", fontWeight: "bold" }),
...arrowMark(14, C[0], 0.7, "#f59e0b"),
Plot.text([[(14 + C[0]) / 2, 0.7]], { text: ["IE"], dy: -7, fontSize: 11, fill: "#f59e0b", fontWeight: "bold" }),
...arrowMark(10, C[0], 2.9, "#6b7280", "4,3"),
Plot.text([[(10 + C[0]) / 2, 2.9]], { text: ["TE"], dy: -7, fontSize: 11, fill: "#6b7280", fontWeight: "bold" })
],
title: title
})
}
html`<div>${panelPlot("正常品:IE 与 SE 同向", [16, 12])}
${panelPlot("劣等品:IE 反向,但 |IE| < |SE|", [13, 13.5])}
${panelPlot("吉芬品:IE 反向且压过 SE,TE 变号", [8, 16])}</div>`