eaglelandsonce's picture
Update index.html
f2e8b7c verified
Raw
History Blame Contribute Delete
18.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Continuous Distributions Lab (Interactive)</title>
<style>
:root{--bg:#0f172a;--card:#111827;--ink:#e5e7eb;--mut:#94a3b8;--accent:#60a5fa;--ok:#10b981;--warn:#f59e0b}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:linear-gradient(180deg,#0b132b,#0f172a);color:var(--ink);padding:24px}
h1{margin:0 0 4px 0;font-size:1.35rem}
.tag{color:#cbd5e1;margin-bottom:14px}
.wrap{display:grid;grid-template-columns:360px 1fr;gap:16px}
.card{background:rgba(17,24,39,.75);border:1px solid #1f2937;border-radius:16px;padding:14px;box-shadow:0 16px 30px rgba(0,0,0,.25)}
label{display:block;margin:8px 0 4px;color:#cbd5e1;font-size:.9rem}
input[type="number"],select{width:100%}
.row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.buttons{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
button{background:#2563eb;border:0;color:white;font-weight:700;border-radius:999px;padding:10px 14px;cursor:pointer}
button.secondary{background:#334155}
button.warn{background:#b45309}
button:active{transform:scale(.98)}
.pill{border:1px solid #334155;border-radius:999px;padding:.2rem .6rem;color:#cbd5e1;font-size:.8rem}
.stat{display:flex;justify-content:space-between;gap:8px;font-variant-numeric:tabular-nums}
canvas{width:100%;height:360px;border-radius:12px;background:#0b1220;border:1px solid #1f2937}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.small{font-size:.85rem;color:var(--mut)}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(220px,1fr));gap:12px}
</style>
</head>
<body>
<h1>Continuous Distributions Lab</h1>
<div class="tag">Pick a distribution, set parameters, simulate samples, and compare the histogram to the theoretical PDF.</div>
<div class="wrap">
<!-- Controls -->
<div class="card">
<label>Distribution</label>
<select id="dist">
<option value="normal">Normal (ΞΌ, Οƒ)</option>
<option value="uniform">Uniform (a, b)</option>
<option value="exponential">Exponential (Ξ»)</option>
<option value="lognormal">Lognormal (ΞΌ, Οƒ)</option>
<option value="triangular">Triangular (a, c, b)</option>
<option value="logistic">Logistic (ΞΌ, s)</option>
<option value="weibull">Weibull (k, Ξ»)</option>
<option value="t">Student’s t (Ξ½)</option>
<option value="chisq">Chi-square (k)</option>
<option value="cauchy">Cauchy (xβ‚€, Ξ³)</option>
</select>
<div id="paramBox"></div>
<div class="row">
<div>
<label>Samples per click</label>
<input id="n" type="number" min="10" max="200000" value="2000"/>
</div>
<div>
<label>Bins</label>
<input id="bins" type="number" min="10" max="120" value="40"/>
</div>
</div>
<div class="buttons">
<button id="sample">Sample</button>
<button class="secondary" id="more">+10k more</button>
<button class="warn" id="reset">Reset</button>
</div>
<div style="margin-top:10px">
<div class="stat"><span>Total samples</span><span id="count" class="mono">0</span></div>
<div class="stat"><span>Range plotted</span><span id="range" class="mono"></span></div>
</div>
<hr style="border:0;border-top:1px solid #223047;margin:12px 0">
<div class="grid2">
<div class="small">
<div><strong>Sample stats</strong></div>
<div class="stat"><span>Mean</span><span id="sMean" class="mono">–</span></div>
<div class="stat"><span>Variance</span><span id="sVar" class="mono">–</span></div>
<div class="stat"><span>Std. dev</span><span id="sSd" class="mono">–</span></div>
<div class="stat"><span>KS distance</span><span id="ks" class="mono">–</span></div>
</div>
<div class="small">
<div><strong>Theoretical (when defined)</strong></div>
<div class="stat"><span>Mean</span><span id="tMean" class="mono">–</span></div>
<div class="stat"><span>Variance</span><span id="tVar" class="mono">–</span></div>
<div class="stat"><span>Std. dev</span><span id="tSd" class="mono">–</span></div>
<div class="stat"><span class="pill">Blue = histogram, Amber = PDF</span><span></span></div>
</div>
</div>
</div>
<!-- Chart -->
<div class="card">
<canvas id="plot" width="1200" height="540"></canvas>
</div>
</div>
<script>
/* ---------- Utilities ---------- */
const $ = id => document.getElementById(id);
const distSel = $('dist'), nEl=$('n'), binsEl=$('bins');
const paramBox = $('paramBox'), canvas=$('plot'), ctx=canvas.getContext('2d');
const countEl=$('count'), rangeEl=$('range');
const sMean=$('sMean'), sVar=$('sVar'), sSd=$('sSd'), ksEl=$('ks');
const tMean=$('tMean'), tVar=$('tVar'), tSd=$('tSd');
let samples=[], params={}; // current param set
function clamp(x,a,b){return Math.max(a,Math.min(b,x))}
function mean(a){return a.reduce((s,x)=>s+x,0)/a.length}
function variance(a,m){const mu=m??mean(a);return a.reduce((s,x)=>s+(x-mu)*(x-mu),0)/a.length}
function std(a,m){return Math.sqrt(variance(a,m))}
function linspace(a,b,n){const arr=[];const step=(b-a)/(n-1);for(let i=0;i<n;i++)arr.push(a+i*step);return arr}
/* ---------- Parameter UIs ---------- */
const UIs = {
normal(){ return [
['ΞΌ','mu',0,-1e9,1e9,0.0],
['Οƒ (>0)','sigma',1,1e-6,1e6,0.1],
];},
uniform(){ return [
['a','a',0,-1e9,1e9,0.0],
['b (>a)','b',1,-1e9,1e9,1.0],
];},
exponential(){ return [
['Ξ» (>0)','lambda',1e0,1e-6,1e6,0.1],
];},
lognormal(){ return [
['ΞΌ (log-space)','mu',0,-1e6,1e6,0.1],
['Οƒ (>0)','sigma',0.5,1e-6,1e6,0.1],
];},
triangular(){ return [
['a (min)','a',0,-1e6,1e6,0.1],
['c (mode)','c',0.5,-1e6,1e6,0.1],
['b (max)','b',1,-1e6,1e6,0.1],
];},
logistic(){ return [
['ΞΌ','mu',0,-1e6,1e6,0.1],
['s (>0)','s',1,1e-6,1e6,0.1],
];},
weibull(){ return [
['k (>0)','k',1.5,1e-6,1e6,0.1],
['Ξ» (>0)','lambda',1,1e-6,1e6,0.1],
];},
t(){ return [
['Ξ½ (>0)','nu',5,1e-6,1e6,1],
];},
chisq(){ return [
['k (dof >0)','k',4,1e-6,1e6,1],
];},
cauchy(){ return [
['xβ‚€','x0',0,-1e6,1e6,0.1],
['Ξ³ (>0)','gamma',1,1e-6,1e6,0.1],
];}
}
function renderParams(){
paramBox.innerHTML='';
const spec = UIs[distSel.value]();
spec.forEach(([label, key, def, min, max, step])=>{
const id='p_'+key;
const wrapper=document.createElement('div');
wrapper.innerHTML = `<label for="${id}">${label}</label>
<input id="${id}" type="number" step="${step}" value="${def}" min="${min}" max="${max}"/>`;
paramBox.appendChild(wrapper);
params[key]=def;
$(id).addEventListener('input', e=>{
const v = Number(e.target.value);
params[key]=v;
if(distSel.value==='triangular'){
// enforce a <= c <= b
const a=Number($('p_a').value), c=Number($('p_c').value), b=Number($('p_b').value);
if(a>b){$('p_b').value=a+1; params.b=a+1;}
$('p_c').value = clamp(c, Math.min(a,b), Math.max(a,b));
params.c=Number($('p_c').value);
}
redraw();
});
});
}
distSel.addEventListener('change', ()=>{samples=[];renderParams();redraw();});
renderParams();
/* ---------- RNGs ---------- */
function U(){ return Math.random(); }
function randn(){ // Box–Muller
let u=0,v=0; while(u===0)u=U(); while(v===0)v=U();
return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);
}
function chiSquare(k){ // integer/real via sum of normals if k is integer; otherwise gamma approx via Marsaglia-Tsang shape=k/2
if (Number.isInteger(k) && k>0){
let s=0; for(let i=0;i<k;i++){const z=randn(); s+=z*z;} return s;
}
// Marsaglia–Tsang for gamma
const a = k/2;
if(a<1){
// boost trick: sample for a+1 then scale by U^(1/a)
const u = U();
return gammaSample(a+1) * Math.pow(u,1/a) * 2;
}
function gammaSample(shape){
const d = shape - 1/3, c = 1/Math.sqrt(9*d);
while(true){
let x = randn();
let v = Math.pow(1 + c*x, 3);
if(v<=0) continue;
let u = U();
if(u < 1 - 0.0331*Math.pow(x,4)) return d*v;
if(Math.log(u) < 0.5*x*x + d*(1 - v + Math.log(v))) return d*v;
}
}
return 2*gammaSample(a);
}
const RNG = {
normal: ({mu,sigma}) => mu + sigma*randn(),
uniform: ({a,b}) => a + (b-a)*U(),
exponential: ({lambda}) => -Math.log(1-U())/lambda,
lognormal: ({mu,sigma}) => Math.exp(mu + sigma*randn()),
triangular: ({a,c,b}) => {
const u=U(); const F=(c-a)/(b-a);
if(u<F) return a + Math.sqrt(u*(b-a)*(c-a));
return b - Math.sqrt((1-u)*(b-a)*(b-c));
},
logistic: ({mu,s}) => mu + s*Math.log(U()/(1-U())),
weibull: ({k,lambda}) => lambda*Math.pow(-Math.log(1-U()), 1/k),
t: ({nu}) => {
const z = randn();
const v = chiSquare(Math.round(nu)); // good for integer Ξ½; for non-int we approximate
return z / Math.sqrt(v/nu);
},
chisq: ({k}) => chiSquare(Math.round(k)),
cauchy: ({x0,gamma}) => x0 + gamma*Math.tan(Math.PI*(U()-0.5)),
};
/* ---------- PDFs (for overlay & theory stats) ---------- */
function pdf_normal(x,{mu,sigma}){return (1/(sigma*Math.sqrt(2*Math.PI)))*Math.exp(-0.5*Math.pow((x-mu)/sigma,2))}
function pdf_uniform(x,{a,b}){return (x<a||x>b)?0:1/(b-a)}
function pdf_exponential(x,{lambda}){return x<0?0:lambda*Math.exp(-lambda*x)}
function pdf_lognormal(x,{mu,sigma}){return x<=0?0:(1/(x*sigma*Math.sqrt(2*Math.PI)))*Math.exp(-Math.pow(Math.log(x)-mu,2)/(2*sigma*sigma))}
function pdf_triangular(x,{a,c,b}){
if(x<a||x>b) return 0;
if(x<=c) return 2*(x-a)/((b-a)*(c-a));
return 2*(b-x)/((b-a)*(b-c));
}
function pdf_logistic(x,{mu,s}){const t=Math.exp(-(x-mu)/s); const d=(1+t); return (1/s)*(t/(d*d))}
function pdf_weibull(x,{k,lambda}){return x<0?0:(k/Math.pow(lambda,k))*Math.pow(x,k-1)*Math.exp(-Math.pow(x/lambda,k))}
function pdf_t(x,{nu}){const g=Math.gamma?Math.gamma:gammaJS; // fallback below
const c = g((nu+1)/2)/(Math.sqrt(nu*Math.PI)*g(nu/2));
return c*Math.pow(1+x*x/nu, -(nu+1)/2);
}
function pdf_chisq(x,{k}){if(x<0) return 0; const g=Math.gamma?Math.gamma:gammaJS; return Math.pow(x,k/2-1)*Math.exp(-x/2)/(Math.pow(2,k/2)*g(k/2))}
function pdf_cauchy(x,{x0,gamma}){return (1/Math.PI)*(gamma/( (x-x0)*(x-x0)+gamma*gamma ))}
/* Simple Gamma approximation for PDF constants if Math.gamma missing */
function gammaJS(z){ // Lanczos approx
const p=[676.5203681218851,-1259.1392167224028,771.32342877765313,-176.61502916214059,12.507343278686905,-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7];
if(z<0.5) return Math.PI/ (Math.sin(Math.PI*z)*gammaJS(1-z));
z-=1; let x=0.99999999999980993;
for(let i=0;i<p.length;i++) x+=p[i]/(z+i+1);
const t=z+p.length-0.5;
return Math.sqrt(2*Math.PI)*Math.pow(t,z+0.5)*Math.exp(-t)*x;
}
const PDF = {
normal: pdf_normal, uniform: pdf_uniform, exponential: pdf_exponential, lognormal: pdf_lognormal,
triangular: pdf_triangular, logistic: pdf_logistic, weibull: pdf_weibull, t: pdf_t,
chisq: pdf_chisq, cauchy: pdf_cauchy
}
/* Theoretical mean/variance (if defined) */
function theory(dist,p){
switch(dist){
case 'normal': return {mean:p.mu, var:p.sigma*p.sigma};
case 'uniform': return {mean:(p.a+p.b)/2, var:Math.pow(p.b-p.a,2)/12};
case 'exponential': return {mean:1/p.lambda, var:1/(p.lambda*p.lambda)};
case 'lognormal': {
const m=Math.exp(p.mu + p.sigma*p.sigma/2);
const v=(Math.exp(p.sigma*p.sigma)-1)*Math.exp(2*p.mu + p.sigma*p.sigma);
return {mean:m,var:v};
}
case 'triangular': {
const mean=(p.a+p.b+p.c)/3;
const v=(p.a*p.a + p.b*p.b + p.c*p.c - p.a*p.b - p.a*p.c - p.b*p.c)/18;
return {mean:mean,var:v};
}
case 'logistic': return {mean:p.mu, var:(Math.PI*Math.PI/3)*p.s*p.s};
case 'weibull': {
const g=Math.gamma?Math.gamma:gammaJS;
const m=p.lambda*g(1+1/p.k);
const v=p.lambda*p.lambda*( g(1+2/p.k) - Math.pow(g(1+1/p.k),2) );
return {mean:m,var:v};
}
case 't': {
const nu=p.nu;
if(nu>1 && nu<=2) return {mean:0,var:Infinity};
if(nu<=1) return {mean:undefined,var:undefined};
return {mean:0,var: nu/(nu-2)};
}
case 'chisq': return {mean:p.k, var:2*p.k};
case 'cauchy': return {mean:undefined, var:undefined};
}
}
/* ---------- Sampling & Plotting ---------- */
function sampleOnce(dist,p){ return RNG[dist](p); }
function sampleN(m){
const d=distSel.value;
for(let i=0;i<m;i++) samples.push(sampleOnce(d,params));
}
function computeKS(){
if(samples.length===0) return NaN;
const d=distSel.value, p=params;
const s=[...samples].sort((a,b)=>a-b);
let D=0;
for(let i=0;i<s.length;i++){
const x=s[i];
const F_emp=(i+1)/s.length;
const F_th=cdf(d,p,x);
D=Math.max(D,Math.abs(F_emp-F_th));
}
return D;
}
/* CDFs (for KS only) */
function erf(x){ // Abramowitz-Stegun
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 cdf(dist,p,x){
switch(dist){
case 'normal': return 0.5*(1+erf((x-p.mu)/(p.sigma*Math.SQRT2)));
case 'uniform': return x<=p.a?0:x>=p.b?1:(x-p.a)/(p.b-p.a);
case 'exponential': return x<0?0:1-Math.exp(-p.lambda*x);
case 'lognormal': return x<=0?0:0.5*(1+erf((Math.log(x)-p.mu)/(p.sigma*Math.SQRT2)));
case 'triangular': {
const {a,c,b}=p;
if(x<=a) return 0; if(x>=b) return 1;
if(x<=c) return Math.pow(x-a,2)/((b-a)*(c-a));
return 1 - Math.pow(b-x,2)/((b-a)*(b-c));
}
case 'logistic': { const t=1/(1+Math.exp(-(x-p.mu)/p.s)); return t; }
case 'weibull': return x<0?0:1-Math.exp(-Math.pow(x/p.lambda,p.k));
case 't': { // use numeric integration of pdf for KS (coarse but ok)
const xs=linspace(-10,10,800); let A=0,prev=xs[0];
for(const y of xs){ if(y>x) break; A+=PDF.t(y,p)*(y-prev); prev=y;}
return clamp(A+0.5,0,1); // crude center adjust
}
case 'chisq': {
if(x<=0) return 0;
// lower incomplete gamma normalized
const k=p.k/2, t=x/2;
return lowerGammaReg(k,t);
}
case 'cauchy': return (1/Math.PI)*Math.atan((x-p.x0)/p.gamma)+0.5;
}
}
function lowerGammaReg(s,x){ // series
if(x<=0) return 0;
let sum=1/s, term=sum;
for(let n=1;n<100;n++){
term *= x/(s+n);
sum += term;
if(term<1e-12) break;
}
return Math.exp(-x + s*Math.log(x) - Math.log(gammaJS(s))) * sum;
}
/* Draw histogram + pdf */
function redraw(){
// stats box
const th=theory(distSel.value,params)||{};
tMean.textContent = th.mean===undefined?'β€”':Number.isFinite(th.mean)?th.mean.toFixed(4):'∞';
tVar.textContent = th.var===undefined?'β€”':Number.isFinite(th.var)?th.var.toFixed(4):'∞';
tSd.textContent = th.var===undefined?'β€”':Number.isFinite(th.var)?Math.sqrt(th.var).toFixed(4):'∞';
// plot bounds
let xmin, xmax;
if(samples.length>0){
xmin = Math.min(...samples), xmax=Math.max(...samples);
} else {
const d=distSel.value, p=params;
if(d==='uniform'||d==='triangular'){ xmin=p.a; xmax=p.b; }
else if(d==='exponential'||d==='chisq'||d==='weibull'||d==='lognormal'){ xmin=0; xmax=5; }
else { xmin=-5; xmax=5; }
}
if(xmin===xmax){ xmin-=1; xmax+=1; }
const pad = (xmax-xmin)*0.08; xmin-=pad; xmax+=pad;
rangeEl.textContent = `${xmin.toFixed(3)} … ${xmax.toFixed(3)}`;
// bins
const bins = Math.max(5, Math.min(120, Number(binsEl.value)));
const w = (xmax-xmin)/bins;
const counts = new Array(bins).fill(0);
for(const x of samples){
const j = Math.floor((x - xmin)/w);
if(j>=0 && j<bins) counts[j]++;
}
const maxCount = Math.max(1, ...counts);
// clear
ctx.clearRect(0,0,canvas.width,canvas.height);
const W=canvas.width, H=canvas.height, L=50, B=40, R=20, T=14;
// axes
ctx.strokeStyle='#2b3957'; ctx.lineWidth=1;
ctx.beginPath(); ctx.moveTo(L,T); ctx.lineTo(L,H-B); ctx.lineTo(W-R,H-B); ctx.stroke();
// histogram (blue)
const barW=(W-L-R)/bins;
for(let i=0;i<bins;i++){
const h = (H-B-T)*(counts[i]/maxCount);
ctx.fillStyle='rgba(96,165,250,.9)'; // blue
ctx.fillRect(L+i*barW, H-B-h, barW-1, h);
}
// theoretical PDF overlay scaled
const d=distSel.value, p=params, pdf=PDF[d];
if(pdf){
const xs = linspace(xmin,xmax,600);
const ys = xs.map(x=>pdf(x,p));
// scale pdf -> histogram counts height: area under pdf equals total count
const area = ys.reduce((s,y,i)=> s + y*( (i?xs[i]-xs[i-1]:0) ), 0);
const scale = (samples.length) / (area||1); // expected total
const toY = y=> (H-B) - ( (y*scale)/maxCount )*(H-B-T);
ctx.beginPath(); ctx.lineWidth=2; ctx.strokeStyle='#f59e0b';
xs.forEach((x,i)=>{
const px = L + ( (x - xmin)/(xmax-xmin) )*(W-L-R);
const py = toY(ys[i]);
if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py);
});
ctx.stroke();
}
// sample stats + KS
countEl.textContent = samples.length.toLocaleString();
if(samples.length>0){
const m = mean(samples); const v = variance(samples,m); const sd = Math.sqrt(v);
sMean.textContent = Number.isFinite(m)?m.toFixed(4):'β€”';
sVar.textContent = Number.isFinite(v)?v.toFixed(4):'β€”';
sSd.textContent = Number.isFinite(sd)?sd.toFixed(4):'β€”';
const D = computeKS(); ksEl.textContent = isNaN(D)?'β€”':D.toFixed(4);
} else {
sMean.textContent=sVar.textContent=sSd.textContent=ksEl.textContent='β€”';
}
}
/* ---------- Buttons ---------- */
$('sample').addEventListener('click', ()=>{
const n = Number(nEl.value)||1000;
sampleN(n); redraw();
});
$('more').addEventListener('click', ()=>{ sampleN(10000); redraw(); });
$('reset').addEventListener('click', ()=>{ samples=[]; redraw(); });
/* First paint */
redraw();
</script>
</body>
</html>