{
const d3 = await require("d3@7");
// --- layout ---
const W = 660, H = 320;
const margin = { top: 30, right: 20, bottom: 50, left: 55 };
const innerW = W - margin.left - margin.right;
const innerH = H - margin.top - margin.bottom;
// --- normal CDF (erf approximation) ---
function erf(x) {
const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
const sign = x < 0 ? -1 : 1;
const t = 1 / (1 + p * Math.abs(x));
const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return sign * y;
}
function normalCDF(x, mu, sigma) {
return 0.5 * (1 + erf((x - mu) / (sigma * Math.sqrt(2))));
}
function normalPDF(x, mu, sigma) {
const z = (x - mu) / sigma;
return Math.exp(-0.5 * z * z) / (sigma * Math.sqrt(2 * Math.PI));
}
// --- mode state ---
let mode = "sigma"; // "sigma" | "interval"
// --- controls ---
const muSlider = Inputs.range([-10, 10], { step: 0.5, value: 0, label: "μ (mean)" });
const sigSlider = Inputs.range([0.5, 5], { step: 0.1, value: 1, label: "σ (std dev)" });
const bandSel = Inputs.select(["1σ", "2σ", "3σ"], { label: "Shade within", value: "1σ" });
const aInput = Inputs.number({ label: "a", value: -1, step: 0.1 });
const bInput = Inputs.number({ label: "b", value: 1, step: 0.1 });
const modeBtn = document.createElement("button");
modeBtn.textContent = "Switch to P(a < X < b) mode";
modeBtn.style.cssText = "padding:0.3rem 0.8rem;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:0.87em;margin-top:0.3rem;";
// sigma-mode controls wrapper
const sigmaControls = html`<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.5rem 1.5rem;">${muSlider}${sigSlider}${bandSel}</div>`;
const intervalControls = html`<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.5rem 1.5rem;">${muSlider}${sigSlider}<div style="display:flex;gap:0.75rem;">${aInput}${bInput}</div></div>`;
intervalControls.style.display = "none";
// --- SVG ---
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${W} ${H}`)
.attr("width", "100%")
.style("font-family", "inherit");
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xAxisG = g.append("g").attr("transform", `translate(0,${innerH})`);
const yAxisG = g.append("g");
g.append("text")
.attr("x", innerW / 2).attr("y", innerH + 42)
.attr("text-anchor", "middle").attr("font-size", 13).text("x");
g.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -innerH / 2).attr("y", -44)
.attr("text-anchor", "middle").attr("font-size", 13).text("f(x)");
const shadeArea = g.append("path").attr("fill", "#3b82f6").attr("opacity", 0.25);
const curvePath = g.append("path").attr("fill", "none").attr("stroke", "#1d4ed8").attr("stroke-width", 2.5);
const meanLine = g.append("line").attr("stroke", "#374151").attr("stroke-width", 1.5).attr("stroke-dasharray", "5,3");
// sigma band lines
const bandLines = [-3,-2,-1,1,2,3].map(m =>
g.append("line").attr("stroke", "#9ca3af").attr("stroke-width", 1).attr("stroke-dasharray", "3,3").attr("opacity", 0)
);
const probLabel = svg.append("text")
.attr("x", margin.left + innerW - 4)
.attr("y", margin.top + 18)
.attr("text-anchor", "end")
.attr("font-size", 13)
.attr("fill", "#1d4ed8")
.attr("font-weight", "600");
const statsLabel = svg.append("text")
.attr("x", margin.left + 6)
.attr("y", margin.top + 18)
.attr("font-size", 12)
.attr("fill", "#6b7280");
const N_CURVE = 400;
function getRange(mu, sigma) {
return [mu - 4.5 * sigma, mu + 4.5 * sigma];
}
function update() {
const mu = muSlider.value;
const sig = sigSlider.value;
const [xMin, xMax] = getRange(mu, sig);
const yMax = normalPDF(mu, mu, sig) * 1.15;
const xScale = d3.scaleLinear().domain([xMin, xMax]).range([0, innerW]);
const yScale = d3.scaleLinear().domain([0, yMax]).range([innerH, 0]);
xAxisG.call(d3.axisBottom(xScale).ticks(8).tickFormat(d3.format(".1f")));
yAxisG.call(d3.axisLeft(yScale).ticks(5).tickFormat(d3.format(".3f")));
// curve
const curveData = d3.range(N_CURVE).map(i => {
const x = xMin + (i / (N_CURVE - 1)) * (xMax - xMin);
return [xScale(x), yScale(normalPDF(x, mu, sig))];
});
const line = d3.line().x(d => d[0]).y(d => d[1]).curve(d3.curveBasis);
curvePath.attr("d", line(curveData));
// mean line
meanLine
.attr("x1", xScale(mu)).attr("x2", xScale(mu))
.attr("y1", 0).attr("y2", innerH);
statsLabel.text(`μ = ${mu.toFixed(1)} σ = ${sig.toFixed(1)}`);
if (mode === "sigma") {
const bandMap = { "1σ": 1, "2σ": 2, "3σ": 3 };
const k = bandMap[bandSel.value];
const lo = mu - k * sig;
const hi = mu + k * sig;
// shade
const shadeData = d3.range(200).map(i => {
const x = lo + (i / 199) * (hi - lo);
return [xScale(x), yScale(normalPDF(x, mu, sig))];
});
const area = d3.area()
.x(d => d[0]).y0(innerH).y1(d => d[1]).curve(d3.curveBasis);
shadeArea.attr("d", area(shadeData));
const prob = normalCDF(hi, mu, sig) - normalCDF(lo, mu, sig);
probLabel.text(`P(within ${bandSel.value}) = ${(prob * 100).toFixed(2)}%`);
// band dashed lines
[-k, k].forEach((m, i) => {
bandLines[i].attr("x1", xScale(mu + m * sig)).attr("x2", xScale(mu + m * sig))
.attr("y1", 0).attr("y2", innerH).attr("opacity", 0.7);
});
bandLines.slice(2).forEach(l => l.attr("opacity", 0));
} else {
// interval mode
const a = Math.min(aInput.value, bInput.value);
const b = Math.max(aInput.value, bInput.value);
const lo = Math.max(xMin, a);
const hi = Math.min(xMax, b);
if (lo < hi) {
const shadeData = d3.range(200).map(i => {
const x = lo + (i / 199) * (hi - lo);
return [xScale(x), yScale(normalPDF(x, mu, sig))];
});
const area = d3.area()
.x(d => d[0]).y0(innerH).y1(d => d[1]).curve(d3.curveBasis);
shadeArea.attr("d", area(shadeData));
const prob = normalCDF(b, mu, sig) - normalCDF(a, mu, sig);
probLabel.text(`P(${a.toFixed(1)} < X < ${b.toFixed(1)}) = ${(prob * 100).toFixed(2)}%`);
} else {
shadeArea.attr("d", null);
probLabel.text("—");
}
bandLines.forEach(l => l.attr("opacity", 0));
}
}
muSlider.addEventListener("input", update);
sigSlider.addEventListener("input", update);
bandSel.addEventListener("input", update);
aInput.addEventListener("input", update);
bInput.addEventListener("input", update);
modeBtn.addEventListener("click", () => {
mode = mode === "sigma" ? "interval" : "sigma";
modeBtn.textContent = mode === "sigma"
? "Switch to P(a < X < b) mode"
: "Switch to σ-band mode";
sigmaControls.style.display = mode === "sigma" ? "" : "none";
intervalControls.style.display = mode === "interval" ? "" : "none";
update();
});
update();
const container = html`<div style="border:1px solid #e5e7eb;border-radius:8px;padding:1rem 1.25rem;margin:0.75rem 0;">
<div style="font-weight:600;margin-bottom:0.5rem;font-size:0.95em;">Normal distribution explorer</div>
${sigmaControls}
${intervalControls}
<div style="margin-top:0.5rem;">${modeBtn}</div>
${svg.node()}
<p style="font-size:0.82em;color:#6b7280;margin-top:0.4rem;">
Shaded region shows the selected probability. The dashed vertical line marks μ.
Adjust μ to shift the curve left/right; adjust σ to widen or narrow it.
</p>
</div>`;
return container;
}