{
// ── OPPORTUNITY 2: Three PDE behaviours side by side ─────────────────────
// Panel A: Heat equation (parabolic) — Gaussian bump spreads irreversibly
// Panel B: Wave equation (hyperbolic) — bump splits and propagates
// Panel C: Laplace equation (elliptic) — static steady state, contour lines
const d3 = await require("d3@7");
const AMBER = "#f59e0b";
const BLUE = "#6b90c4";
const GREY = "#9ca3af";
const GREY_F = "#e5e7eb";
const W = 580, H = 260;
const panelW = 170, panelH = 160, gap = 14;
const PL = 8, PT = 28;
const container = document.createElement("div");
container.style.cssText = "font-family:inherit; max-width:640px;";
// Shared time slider
const ctrlRow = document.createElement("div");
ctrlRow.style.cssText = "display:flex; gap:1rem; align-items:center; margin-bottom:0.4rem; flex-wrap:wrap;";
const tLbl = document.createElement("label");
tLbl.style.cssText = "font-size:0.88em; color:#374151; display:flex; align-items:center; gap:0.4rem;";
tLbl.textContent = "Time t = ";
const tDisp = document.createElement("strong");
tDisp.style.cssText = "color:" + AMBER + "; min-width:3ch;";
tDisp.textContent = "0.00";
const tSlider = document.createElement("input");
tSlider.type = "range"; tSlider.min = 0; tSlider.max = 200; tSlider.value = 0;
tSlider.style.cssText = "width:220px; accent-color:" + AMBER + ";";
tLbl.appendChild(tSlider); tLbl.appendChild(tDisp);
ctrlRow.appendChild(tLbl);
container.appendChild(ctrlRow);
const svg = d3.create("svg")
.attr("viewBox", `0 0 ${W} ${H}`)
.attr("width","100%").style("display","block");
const L = Math.PI; // domain [0, π]
const N_x = 200;
const xs = d3.range(N_x).map(i => i/(N_x-1) * L);
// ── Panel helper ──────────────────────────────────────────────────────────
function makePanel(i, title, subtitle) {
const x0 = PL + i*(panelW+gap);
const g = svg.append("g").attr("transform", `translate(${x0},${PT})`);
g.append("rect").attr("x",0).attr("y",0).attr("width",panelW).attr("height",panelH)
.attr("fill","#fafafa").attr("stroke",GREY_F).attr("stroke-width",1).attr("rx",3);
g.append("text").attr("x",panelW/2).attr("y",-10).attr("text-anchor","middle")
.attr("font-size","10px").attr("fill","#374151").attr("font-weight","600").text(title);
g.append("text").attr("x",panelW/2).attr("y",panelH+14).attr("text-anchor","middle")
.attr("font-size","9px").attr("fill",GREY).attr("font-style","italic").text(subtitle);
return g;
}
const xP = d3.scaleLinear().domain([0, L]).range([6, panelW-6]);
const yP = d3.scaleLinear().domain([-0.1, 1.1]).range([panelH-6, 6]);
// ── Initial condition: Gaussian centred at π/2 ────────────────────────────
function gauss0(x) { return Math.exp(-20*(x-L/2)*(x-L/2)); }
// ── Panel A: Heat equation on [0,π], Dirichlet BCs ───────────────────────
// u(x,t) = Σ Bₙ sin(nx) e^{-n²t} ; Bₙ = (2/π) ∫₀^π f(x)sin(nx)dx
const gA = makePanel(0, "Heat equation", "spreads irreversibly");
// Precompute B_n coefficients of Gaussian (numerically)
const N_modes = 25;
const Bn_heat = d3.range(1, N_modes+1).map(n => {
let s = 0;
xs.forEach((x,i) => {
const dx = i===0||i===N_x-1 ? 0.5/(N_x-1) : 1/(N_x-1);
s += gauss0(x) * Math.sin(n*x) * dx * L;
});
return 2/L * s;
});
function heatSolution(x, t) {
let u = 0;
for (let n = 1; n <= N_modes; n++) {
u += Bn_heat[n-1] * Math.sin(n*x) * Math.exp(-n*n*t);
}
return u;
}
// Initial condition trace
gA.append("path").attr("fill","none").attr("stroke",GREY).attr("stroke-width",1)
.attr("stroke-dasharray","3,2")
.datum(xs.map(x=>[xP(x),yP(gauss0(x))])).attr("d",d3.line().x(d=>d[0]).y(d=>d[1]));
gA.append("line").attr("x1",6).attr("x2",panelW-6).attr("y1",yP(0)).attr("y2",yP(0))
.attr("stroke",GREY_F).attr("stroke-width",0.8);
[0,1].forEach(v=>{ gA.append("text").attr("x",2).attr("y",yP(v)).attr("font-size","7.5px").attr("fill",GREY).attr("text-anchor","end").text(v); });
const heatPath = gA.append("path").attr("fill","none").attr("stroke",AMBER).attr("stroke-width",2);
// ── Panel B: Wave equation d'Alembert (u(x,t) = ½[f(x+ct)+f(x-ct)]) ─────
// c = 1, f defined on ℝ by odd periodic extension of Gaussian
const gB = makePanel(1, "Wave equation", "propagates without decay");
gB.append("path").attr("fill","none").attr("stroke",GREY).attr("stroke-width",1)
.attr("stroke-dasharray","3,2")
.datum(xs.map(x=>[xP(x),yP(gauss0(x))])).attr("d",d3.line().x(d=>d[0]).y(d=>d[1]));
gB.append("line").attr("x1",6).attr("x2",panelW-6).attr("y1",yP(0)).attr("y2",yP(0))
.attr("stroke",GREY_F).attr("stroke-width",0.8);
[0,1].forEach(v=>{ gB.append("text").attr("x",2).attr("y",yP(v)).attr("font-size","7.5px").attr("fill",GREY).attr("text-anchor","end").text(v); });
// Odd periodic extension for D'Alembert
function periodicOdd(x) {
// Reduce to [-π, π] via odd periodic extension of [0,π]
let xn = ((x % (2*L)) + 2*L) % (2*L); // in [0, 2L)
if (xn > L) { xn = 2*L - xn; return -gauss0(xn); }
return gauss0(xn);
}
function waveSolution(x, t) {
return 0.5 * (periodicOdd(x + t) + periodicOdd(x - t));
}
const wavePath = gB.append("path").attr("fill","none").attr("stroke",AMBER).attr("stroke-width",2);
// ── Panel C: Laplace equation (static) — contour lines of sin(πx/L)sinh(π(1-y)/L)/sinh(π/L)
const gC = makePanel(2, "Laplace equation", "steady state only");
// Render Laplace solution on a grid (x ∈ [0,π], y ∈ [0,1]) as filled contours
const xPc = d3.scaleLinear().domain([0,L]).range([6,panelW-6]);
const yPc = d3.scaleLinear().domain([0,1]).range([panelH-6,6]);
function laplace(x, y) {
// u = sum_n Cn sin(nπx/L) sinh(nπ(1-y)/L) / sinh(nπ/L)
// Use n=1 only (boundary: sin(x) on y=0)
return Math.sin(x) * Math.sinh(Math.PI*(1-y)/L) / Math.sinh(Math.PI/L);
}
// Draw coloured heatmap
const lN = 30, lM = 30;
for (let i = 0; i < lN; i++) {
for (let j = 0; j < lM; j++) {
const x0 = i/lN*L, x1 = (i+1)/lN*L;
const y0 = j/lM, y1 = (j+1)/lM;
const v = laplace((x0+x1)/2, (y0+y1)/2);
const t = Math.max(0, Math.min(1, v));
const r = Math.round(255 - t*180);
const g2 = Math.round(220 - t*100);
const b2 = Math.round(180 - t*120);
gC.append("rect")
.attr("x",xPc(x0)).attr("y",yPc(y1))
.attr("width",xPc(x1)-xPc(x0)+0.5).attr("height",yPc(y0)-yPc(y1)+0.5)
.attr("fill",`rgb(${r},${g2},${b2})`);
}
}
// Contour lines
const contourLevels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7];
contourLevels.forEach(lv => {
// Draw iso-x line: y where u(π/2, y) = lv → sinh(π(1-y)/L) = lv*sinh(π/L)
const target = lv * Math.sinh(Math.PI/L);
const yIso = 1 - Math.log(target + Math.sqrt(target*target+1)) / (Math.PI/L);
if (yIso >= 0 && yIso <= 1) {
const pts2 = d3.range(20).map(i => {
const x_ = i/19*L;
const v_ = laplace(x_, yIso + 0.001);
// find y where u(x_,y) = lv by simple search
let yy = yIso;
for (let k = 0; k < 20; k++) {
const uu = laplace(x_, yy);
const duyy = -Math.sin(x_) * Math.PI/L * Math.cosh(Math.PI*(1-yy)/L) / Math.sinh(Math.PI/L);
if (Math.abs(duyy) > 1e-8) yy -= (uu - lv) / duyy;
yy = Math.max(0, Math.min(1, yy));
}
return [xPc(x_), yPc(yy)];
});
gC.append("path").attr("fill","none").attr("stroke","rgba(0,0,0,0.25)").attr("stroke-width",0.8)
.datum(pts2).attr("d",d3.line().x(d=>d[0]).y(d=>d[1]));
}
});
// Boundary labels
gC.append("text").attr("x",panelW/2).attr("y",panelH).attr("text-anchor","middle")
.attr("font-size","7.5px").attr("fill","#b45309").text("u = sin(x) (hot)");
gC.append("text").attr("x",panelW/2).attr("y",12).attr("text-anchor","middle")
.attr("font-size","7.5px").attr("fill",GREY).text("u = 0 (top/sides)");
// ── Time-slider update ────────────────────────────────────────────────────
function update3() {
const t = +tSlider.value / 200 * 2.0;
tDisp.textContent = t.toFixed(2);
heatPath.datum(xs.map(x=>[xP(x),yP(Math.max(0,heatSolution(x,t)))])).attr("d",d3.line().x(d=>d[0]).y(d=>d[1]));
wavePath.datum(xs.map(x=>[xP(x),yP(Math.max(-0.05,waveSolution(x,t)))])).attr("d",d3.line().x(d=>d[0]).y(d=>d[1]));
}
tSlider.oninput = update3;
update3();
// Note for Laplace
svg.append("text").attr("x",PL+2*(panelW+gap)+panelW/2).attr("y",PT+panelH+28)
.attr("text-anchor","middle").attr("font-size","8.5px").attr("fill",BLUE).attr("font-style","italic")
.text("(no time slider — equilibrium only)");
// Legend
const legend = document.createElement("div");
legend.style.cssText = "display:flex; gap:1.2rem; font-size:0.78em; margin-top:0.2rem; flex-wrap:wrap;";
[["─── Solution at time t", AMBER], ["--- Initial condition", GREY]]
.forEach(([lab, col]) => {
const s = document.createElement("span"); s.style.color = col; s.textContent = lab;
legend.appendChild(s);
});
container.appendChild(svg.node());
container.appendChild(legend);
return container;
}