{
// ── Distribution landscape explorer ──────────────────────────────────────
const W = 500, H = 280, PAD = { top: 24, right: 20, bottom: 40, left: 48 };
const ACCENT = "#3b82f6";
const ACCENT_MUTED = "rgba(59,130,246,0.18)";
// ── state ─────────────────────────────────────────────────────────────────
const distNames = ["Bernoulli", "Binomial", "Poisson", "Exponential", "Normal"];
let distIdx = 4; // default: Normal
let params = { p: 0.5, n: 10, lam: 2, mu: 0, sigma: 1 };
// ── helpers ──────────────────────────────────────────────────────────────
function factorial(k) { let r=1; for(let i=2;i<=k;i++) r*=i; return r; }
function normalPDF(x,mu,sig) { return Math.exp(-0.5*((x-mu)/sig)**2)/(sig*Math.sqrt(2*Math.PI)); }
function normalCDF(x,mu,sig) {
const z=(x-mu)/(sig*Math.SQRT2);
return 0.5*(1+erf(z));
}
function erf(x) {
const sign = x<0?-1:1; x=Math.abs(x);
const a1=0.254829592,a2=-0.284496736,a3=1.421413741,a4=-1.453152027,a5=1.061405429,p=0.3275911;
const t=1/(1+p*x);
const y=1-(((((a5*t+a4)*t)+a3)*t+a2)*t+a1)*t*Math.exp(-x*x);
return sign*y;
}
function poissonPMF(k,lam) { return Math.pow(lam,k)*Math.exp(-lam)/factorial(k); }
function binomPMF(k,n,p) {
if(k<0||k>n) return 0;
const logC = logBinom(n,k);
return Math.exp(logC + k*Math.log(p) + (n-k)*Math.log(1-p));
}
function logBinom(n,k) {
let r=0; for(let i=0;i<k;i++) r+=Math.log(n-i)-Math.log(i+1); return r;
}
function getDistInfo() {
const d = distNames[distIdx];
if(d==="Bernoulli") {
const p=params.p; const mu=p; const variance=p*(1-p); const sigma=Math.sqrt(variance);
const xs=[0,1]; const ys=[1-p,p];
return { discrete:true, xs, ys, mu, variance, sigma, label:`Bernoulli(p=${p.toFixed(2)})` };
}
if(d==="Binomial") {
const {n,p}=params; const mu=n*p; const variance=n*p*(1-p); const sigma=Math.sqrt(variance);
const xs=[...Array(n+1).keys()]; const ys=xs.map(k=>binomPMF(k,n,p));
return { discrete:true, xs, ys, mu, variance, sigma, label:`Binomial(n=${n}, p=${p.toFixed(2)})` };
}
if(d==="Poisson") {
const lam=params.lam; const mu=lam; const variance=lam; const sigma=Math.sqrt(lam);
const maxK=Math.ceil(lam+4*sigma+5);
const xs=[...Array(maxK+1).keys()]; const ys=xs.map(k=>poissonPMF(k,lam));
return { discrete:true, xs, ys, mu, variance, sigma, label:`Poisson(λ=${lam.toFixed(1)})` };
}
if(d==="Exponential") {
const lam=params.lam; const mu=1/lam; const variance=1/(lam*lam); const sigma=1/lam;
const xmax=mu+5*sigma; const nPts=200;
const xs=Array.from({length:nPts},(_,i)=>i*xmax/(nPts-1));
const ys=xs.map(x=>x>=0?lam*Math.exp(-lam*x):0);
return { discrete:false, xs, ys, mu, variance, sigma, label:`Exponential(λ=${lam.toFixed(2)})`, xmax };
}
// Normal
const {mu,sigma}=params; const variance=sigma*sigma;
const xmin=mu-4*sigma, xmax=mu+4*sigma; const nPts=200;
const xs=Array.from({length:nPts},(_,i)=>xmin+i*(xmax-xmin)/(nPts-1));
const ys=xs.map(x=>normalPDF(x,mu,sigma));
return { discrete:false, xs, ys, mu, variance, sigma, label:`Normal(μ=${mu.toFixed(1)}, σ=${sigma.toFixed(1)})`, xmin, xmax };
}
// ── build UI ───────────────────────────────────────────────────────────────
const container = document.createElement("div");
container.style.cssText = "font-family:inherit; max-width:680px; margin:1rem 0;";
// dist selector
const selectorRow = document.createElement("div");
selectorRow.style.cssText = "display:flex; gap:0.4rem; flex-wrap:wrap; margin-bottom:0.75rem;";
const btns = distNames.map((name,i)=>{
const b=document.createElement("button");
b.textContent=name; b.dataset.idx=i;
b.style.cssText = "padding:0.3rem 0.7rem; border-radius:4px; border:1px solid #d1d5db; cursor:pointer; font-size:0.85em; background:#f9fafb;";
b.onclick=()=>{ distIdx=i; update(); };
selectorRow.appendChild(b);
return b;
});
container.appendChild(selectorRow);
// main layout: left (canvas) + right (readouts)
const mainRow = document.createElement("div");
mainRow.style.cssText = "display:flex; gap:1rem; align-items:flex-start;";
// svg
const svg = document.createElementNS("http://www.w3.org/2000/svg","svg");
svg.setAttribute("width", W); svg.setAttribute("height", H);
svg.style.cssText = "border:1px solid #e5e7eb; border-radius:6px; background:#fff; flex-shrink:0;";
mainRow.appendChild(svg);
// readout panel
const readouts = document.createElement("div");
readouts.style.cssText = "min-width:170px; padding:0.75rem; background:#f8fafc; border:1px solid #e5e7eb; border-radius:6px; font-size:0.88em;";
mainRow.appendChild(readouts);
container.appendChild(mainRow);
// sliders area
const slidersDiv = document.createElement("div");
slidersDiv.style.cssText = "margin-top:0.75rem; padding:0.75rem; background:#f8fafc; border:1px solid #e5e7eb; border-radius:6px;";
container.appendChild(slidersDiv);
// label
const labelDiv = document.createElement("div");
labelDiv.style.cssText = "margin-top:0.5rem; font-size:0.82em; color:#6b7280; font-style:italic;";
labelDiv.textContent = "Every distribution is a landscape: a shape, a centre, and a spread.";
container.appendChild(labelDiv);
function makeSlider(labelText, key, min, max, step, getValue, setValue) {
const row = document.createElement("div");
row.style.cssText = "display:flex; align-items:center; gap:0.5rem; margin-bottom:0.4rem;";
const lbl = document.createElement("label");
lbl.style.cssText = "font-size:0.85em; min-width:80px;";
lbl.textContent = labelText;
const inp = document.createElement("input");
inp.type="range"; inp.min=min; inp.max=max; inp.step=step; inp.value=getValue();
inp.style.cssText = "flex:1;";
const val = document.createElement("span");
val.style.cssText = "font-size:0.85em; min-width:35px; text-align:right;";
val.textContent = Number(getValue()).toFixed(step<0.1?2:step<1?1:0);
inp.oninput = ()=>{ setValue(parseFloat(inp.value)); val.textContent=Number(inp.value).toFixed(step<0.1?2:step<1?1:0); update(); };
row.appendChild(lbl); row.appendChild(inp); row.appendChild(val);
return {row, inp, val};
}
// ── draw ──────────────────────────────────────────────────────────────────
function draw(info) {
while(svg.firstChild) svg.removeChild(svg.firstChild);
const cw = W - PAD.left - PAD.right;
const ch = H - PAD.top - PAD.bottom;
const xMin = info.discrete ? Math.min(...info.xs)-0.5 : (info.xmin !== undefined ? info.xmin : Math.min(...info.xs));
const xMax = info.discrete ? Math.max(...info.xs)+0.5 : (info.xmax !== undefined ? info.xmax : Math.max(...info.xs));
const yMax = Math.max(...info.ys)*1.15;
const sx = x => PAD.left + (x-xMin)/(xMax-xMin)*cw;
const sy = y => PAD.top + ch - y/yMax*ch;
// grid lines (light)
for(let i=0;i<=4;i++) {
const y = i*yMax/4;
const line = document.createElementNS("http://www.w3.org/2000/svg","line");
line.setAttribute("x1",PAD.left); line.setAttribute("x2",PAD.left+cw);
line.setAttribute("y1",sy(y)); line.setAttribute("y2",sy(y));
line.setAttribute("stroke","#f0f0f0"); line.setAttribute("stroke-width","1");
svg.appendChild(line);
}
// ±1σ band shading (continuous only)
if(!info.discrete) {
const {mu,sigma,xs,ys}=info;
const x1c=Math.max(xMin,mu-sigma), x2c=Math.min(xMax,mu+sigma);
// build polygon
const bandXs=xs.filter(x=>x>=x1c&&x<=x2c);
if(bandXs.length>1) {
const pts=[`${sx(x1c)},${sy(0)}`,...bandXs.map((x,j)=>{
const y=ys[xs.indexOf(x)]; return `${sx(x)},${sy(y)}`;
}),`${sx(x2c)},${sy(0)}`].join(" ");
const poly=document.createElementNS("http://www.w3.org/2000/svg","polygon");
poly.setAttribute("points",pts); poly.setAttribute("fill",ACCENT_MUTED);
svg.appendChild(poly);
}
}
// distribution shape
if(info.discrete) {
info.xs.forEach((x,i)=>{
const y=info.ys[i];
const line=document.createElementNS("http://www.w3.org/2000/svg","line");
line.setAttribute("x1",sx(x)); line.setAttribute("x2",sx(x));
line.setAttribute("y1",sy(0)); line.setAttribute("y2",sy(y));
line.setAttribute("stroke",ACCENT); line.setAttribute("stroke-width","2");
svg.appendChild(line);
const dot=document.createElementNS("http://www.w3.org/2000/svg","circle");
dot.setAttribute("cx",sx(x)); dot.setAttribute("cy",sy(y)); dot.setAttribute("r","4");
dot.setAttribute("fill",ACCENT);
svg.appendChild(dot);
});
} else {
const pts=info.xs.map((x,i)=>`${sx(x)},${sy(info.ys[i])}`).join(" ");
const area=document.createElementNS("http://www.w3.org/2000/svg","polygon");
area.setAttribute("points",`${sx(info.xs[0])},${sy(0)} ${pts} ${sx(info.xs[info.xs.length-1])},${sy(0)}`);
area.setAttribute("fill",ACCENT); area.setAttribute("fill-opacity","0.35");
svg.appendChild(area);
const path=document.createElementNS("http://www.w3.org/2000/svg","polyline");
path.setAttribute("points",pts); path.setAttribute("fill","none");
path.setAttribute("stroke",ACCENT); path.setAttribute("stroke-width","2");
svg.appendChild(path);
}
// mean line
const muX=sx(info.mu);
const muLine=document.createElementNS("http://www.w3.org/2000/svg","line");
muLine.setAttribute("x1",muX); muLine.setAttribute("x2",muX);
muLine.setAttribute("y1",PAD.top); muLine.setAttribute("y2",PAD.top+ch);
muLine.setAttribute("stroke","#374151"); muLine.setAttribute("stroke-width","1.5");
muLine.setAttribute("stroke-dasharray","5,3");
svg.appendChild(muLine);
const muLbl=document.createElementNS("http://www.w3.org/2000/svg","text");
muLbl.setAttribute("x",muX+4); muLbl.setAttribute("y",PAD.top+10);
muLbl.setAttribute("font-size","11"); muLbl.setAttribute("fill","#374151");
muLbl.textContent=`μ=${info.mu.toFixed(2)}`;
svg.appendChild(muLbl);
// ±1σ bracket
const s1x1=sx(info.mu-info.sigma), s1x2=sx(info.mu+info.sigma);
const bracketY=PAD.top+ch+14;
const bline=document.createElementNS("http://www.w3.org/2000/svg","line");
bline.setAttribute("x1",Math.max(PAD.left,s1x1)); bline.setAttribute("x2",Math.min(PAD.left+cw,s1x2));
bline.setAttribute("y1",bracketY); bline.setAttribute("y2",bracketY);
bline.setAttribute("stroke",ACCENT); bline.setAttribute("stroke-width","2");
svg.appendChild(bline);
const blbl=document.createElementNS("http://www.w3.org/2000/svg","text");
blbl.setAttribute("x",(Math.max(PAD.left,s1x1)+Math.min(PAD.left+cw,s1x2))/2);
blbl.setAttribute("y",bracketY+12); blbl.setAttribute("text-anchor","middle");
blbl.setAttribute("font-size","10"); blbl.setAttribute("fill",ACCENT);
blbl.textContent="±1σ";
svg.appendChild(blbl);
// axes
const xAxis=document.createElementNS("http://www.w3.org/2000/svg","line");
xAxis.setAttribute("x1",PAD.left); xAxis.setAttribute("x2",PAD.left+cw);
xAxis.setAttribute("y1",sy(0)); xAxis.setAttribute("y2",sy(0));
xAxis.setAttribute("stroke","#374151"); xAxis.setAttribute("stroke-width","1");
svg.appendChild(xAxis);
const yAxis=document.createElementNS("http://www.w3.org/2000/svg","line");
yAxis.setAttribute("x1",PAD.left); yAxis.setAttribute("x2",PAD.left);
yAxis.setAttribute("y1",PAD.top); yAxis.setAttribute("y2",sy(0));
yAxis.setAttribute("stroke","#374151"); yAxis.setAttribute("stroke-width","1");
svg.appendChild(yAxis);
// y-axis label
const yLbl=document.createElementNS("http://www.w3.org/2000/svg","text");
yLbl.setAttribute("x",10); yLbl.setAttribute("y",PAD.top+ch/2);
yLbl.setAttribute("font-size","11"); yLbl.setAttribute("fill","#6b7280");
yLbl.setAttribute("text-anchor","middle");
yLbl.setAttribute("transform",`rotate(-90,10,${PAD.top+ch/2})`);
yLbl.textContent = info.discrete ? "Probability" : "Density";
svg.appendChild(yLbl);
// x-axis ticks
const nTicks=5;
for(let i=0;i<=nTicks;i++) {
const xv=xMin+i*(xMax-xMin)/nTicks;
const tick=document.createElementNS("http://www.w3.org/2000/svg","line");
tick.setAttribute("x1",sx(xv)); tick.setAttribute("x2",sx(xv));
tick.setAttribute("y1",sy(0)); tick.setAttribute("y2",sy(0)+5);
tick.setAttribute("stroke","#374151"); tick.setAttribute("stroke-width","1");
svg.appendChild(tick);
const lbl=document.createElementNS("http://www.w3.org/2000/svg","text");
lbl.setAttribute("x",sx(xv)); lbl.setAttribute("y",sy(0)+18);
lbl.setAttribute("text-anchor","middle"); lbl.setAttribute("font-size","10");
lbl.setAttribute("fill","#6b7280");
lbl.textContent = info.discrete ? Math.round(xv) : xv.toFixed(1);
svg.appendChild(lbl);
}
// distribution label top-left
const titleLbl=document.createElementNS("http://www.w3.org/2000/svg","text");
titleLbl.setAttribute("x",PAD.left+4); titleLbl.setAttribute("y",PAD.top-6);
titleLbl.setAttribute("font-size","11"); titleLbl.setAttribute("fill","#374151");
titleLbl.setAttribute("font-weight","500");
titleLbl.textContent=info.label;
svg.appendChild(titleLbl);
}
function buildSliders() {
slidersDiv.innerHTML="";
const d=distNames[distIdx];
const title=document.createElement("div");
title.style.cssText="font-size:0.82em; font-weight:600; color:#374151; margin-bottom:0.4rem;";
title.textContent="Parameters";
slidersDiv.appendChild(title);
if(d==="Bernoulli") {
const {row}=makeSlider("p",null,0.01,0.99,0.01,()=>params.p,v=>params.p=v);
slidersDiv.appendChild(row);
}
if(d==="Binomial") {
const {row:r1}=makeSlider("n",null,1,50,1,()=>params.n,v=>params.n=v);
const {row:r2}=makeSlider("p",null,0.01,0.99,0.01,()=>params.p,v=>params.p=v);
slidersDiv.appendChild(r1); slidersDiv.appendChild(r2);
}
if(d==="Poisson") {
const {row}=makeSlider("λ",null,0.1,20,0.1,()=>params.lam,v=>params.lam=v);
slidersDiv.appendChild(row);
}
if(d==="Exponential") {
const {row}=makeSlider("λ (rate)",null,0.1,5,0.05,()=>params.lam,v=>params.lam=v);
slidersDiv.appendChild(row);
const note=document.createElement("div");
note.style.cssText="font-size:0.78em; color:#6b7280; margin-top:0.2rem;";
note.textContent="Mean = 1/λ (rate ≠ mean)";
slidersDiv.appendChild(note);
}
if(d==="Normal") {
const {row:r1}=makeSlider("μ",null,-5,5,0.1,()=>params.mu,v=>params.mu=v);
const {row:r2}=makeSlider("σ",null,0.1,5,0.1,()=>params.sigma,v=>params.sigma=v);
slidersDiv.appendChild(r1); slidersDiv.appendChild(r2);
}
}
function updateReadouts(info) {
const isSymmetric = ["Bernoulli","Normal"].includes(distNames[distIdx]) ||
(distNames[distIdx]==="Binomial" && Math.abs(params.p-0.5)<0.01);
const medianNote = isSymmetric ? "Symmetric: median = mean" : "Skewed: median ≠ mean";
readouts.innerHTML = `
<div style="font-weight:600; color:#374151; margin-bottom:0.5rem; font-size:0.9em;">Statistics</div>
<div style="margin-bottom:0.35rem;"><span style="color:#6b7280;">Mean:</span> <strong>${info.mu.toFixed(3)}</strong></div>
<div style="margin-bottom:0.35rem;"><span style="color:#6b7280;">Variance:</span> <strong>${info.variance.toFixed(3)}</strong></div>
<div style="margin-bottom:0.35rem;"><span style="color:#6b7280;">Std dev (σ):</span> <strong>${info.sigma.toFixed(3)}</strong></div>
<div style="margin-top:0.6rem; font-size:0.82em; color:#6b7280; font-style:italic;">${medianNote}</div>
`;
}
function update() {
// highlight active button
btns.forEach((b,i)=>{
b.style.background = i===distIdx ? ACCENT : "#f9fafb";
b.style.color = i===distIdx ? "#fff" : "#374151";
b.style.borderColor = i===distIdx ? ACCENT : "#d1d5db";
});
const info=getDistInfo();
draw(info);
buildSliders();
updateReadouts(info);
}
update();
return container;
}