<div class="controls">
<label>Dice per roll (n): <strong id="nlabel">1</strong>
<input type="range" id="nslider" min="1" max="20" value="1">
</label>
<label>Rolls: <strong id="rlabel">500</strong>
<input type="range" id="rslider" min="100" max="2000" step="100" value="500">
</label>
<button id="rollbtn" type="button">Roll!</button>
</div>
<div class="die-strip">
<span class="die-label">Single die probabilities (biased):</span>
<div id="diebars"></div>
</div>
<canvas id="chart" width="480" height="260"></canvas>
<p id="statsline" class="stats"></p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem 1.2rem; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 110px; accent-color: #1d3557; }
button { font: 600 14px system-ui; padding: .4rem .9rem; background: #1d3557; color: #fff;
border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #274472; }
.die-strip { margin-bottom: .5rem; }
.die-label { font-size: .8rem; color: #555; }
#diebars { display: flex; gap: 3px; align-items: flex-end; height: 40px; margin-top: 4px; }
.diebar { width: 28px; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.diebar-fill { width: 100%; background: #457b9d; border-radius: 3px 3px 0 0; transition: height .3s; }
.diebar-lbl { font-size: 9px; color: #444; }
canvas { display: block; max-width: 100%; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; }
.stats { font-size: .85rem; color: #555; margin: .4rem 0 0; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Central Limit Theorem demo: sum n rolls of a biased die, repeat R times,
// draw histogram and overlay the predicted normal curve.
// Bias: weights [1,1,1,2,3,6] for faces 1..6 (face 6 is 6x more likely than face 1).
const WEIGHTS = [1, 1, 1, 2, 3, 6]; // unnormalized probabilities for faces 1..6
const TOTAL_W = WEIGHTS.reduce((a,b)=>a+b, 0); // 14
const PROBS = WEIGHTS.map(w => w / TOTAL_W);
// Compute die mean and variance
const DIE_MEAN = PROBS.reduce((s,p,i) => s + p*(i+1), 0);
const DIE_VAR = PROBS.reduce((s,p,i) => s + p*Math.pow(i+1 - DIE_MEAN, 2), 0);
// Draw the single-die probability bars
function drawDieBars() {
const cont = document.getElementById('diebars');
cont.innerHTML = '';
const maxP = Math.max(...PROBS);
PROBS.forEach((p, i) => {
const wrap = document.createElement('div'); wrap.className = 'diebar';
const fill = document.createElement('div'); fill.className = 'diebar-fill';
fill.style.height = (p / maxP * 34) + 'px';
const lbl = document.createElement('div'); lbl.className = 'diebar-lbl';
lbl.textContent = i + 1;
wrap.appendChild(fill); wrap.appendChild(lbl);
cont.appendChild(wrap);
});
}
drawDieBars();
// Roll one biased die: returns face value 1..6
function rollDie() {
let r = Math.random() * TOTAL_W, cumW = 0;
for (let i = 0; i < WEIGHTS.length; i++) {
cumW += WEIGHTS[i];
if (r < cumW) return i + 1;
}
return 6;
}
function getN() { return parseInt(document.getElementById('nslider').value, 10); }
function getR() { return parseInt(document.getElementById('rslider').value, 10); }
document.getElementById('nslider').addEventListener('input', () => {
document.getElementById('nlabel').textContent = getN();
});
document.getElementById('rslider').addEventListener('input', () => {
document.getElementById('rlabel').textContent = getR();
});
// Draw histogram + normal curve
function runDemo() {
const n = getN(), R = getR();
// Collect sums
const sums = [];
for (let r = 0; r < R; r++) {
let s = 0;
for (let i = 0; i < n; i++) s += rollDie();
sums.push(s);
}
// Bin into integers
const minS = n, maxS = n * 6;
const freq = new Array(maxS - minS + 1).fill(0);
sums.forEach(s => freq[s - minS]++);
// Predicted normal params
const mu = n * DIE_MEAN;
const sigma = Math.sqrt(n * DIE_VAR);
// Draw
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { l: 38, r: 14, t: 18, b: 28 };
ctx.clearRect(0, 0, W, H);
const bins = freq.length;
const maxFreq = Math.max(...freq);
const xScale = (W - PAD.l - PAD.r) / bins;
const yScale = (H - PAD.t - PAD.b) / (maxFreq || 1);
// Draw bars
ctx.fillStyle = '#a8c7e8';
freq.forEach((f, i) => {
const bh = f * yScale;
ctx.fillRect(PAD.l + i * xScale, H - PAD.b - bh, xScale - 1, bh);
});
// Normal PDF overlay (scaled to match histogram area)
// Area of histogram ≈ R (total count) * 1 bin-width
ctx.beginPath();
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.2;
const norm = (x) => Math.exp(-0.5 * ((x - mu) / sigma) ** 2) / (sigma * Math.sqrt(2 * Math.PI));
for (let i = 0; i <= bins; i++) {
const x = minS + i; // actual sum value at left edge of bin i
const density = norm(x) * R; // expected count per unit
const px = PAD.l + i * xScale;
const py = H - PAD.b - density * yScale;
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.stroke();
// Axes
ctx.strokeStyle = '#b0bec5'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, H - PAD.b); ctx.lineTo(W - PAD.r, H - PAD.b);
ctx.moveTo(PAD.l, H - PAD.b); ctx.lineTo(PAD.l, PAD.t);
ctx.stroke();
// X axis labels (a few tick values)
ctx.fillStyle = '#555'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
const step = Math.max(1, Math.round(bins / 6));
for (let i = 0; i <= bins; i += step) {
const lv = minS + i;
const px = PAD.l + i * xScale;
ctx.fillText(lv, px, H - PAD.b + 12);
}
// Y axis label
ctx.save(); ctx.translate(12, (H - PAD.t - PAD.b) / 2 + PAD.t);
ctx.rotate(-Math.PI / 2); ctx.textAlign = 'center'; ctx.font = '10px system-ui';
ctx.fillText('count', 0, 0); ctx.restore();
// Stats line
const empMean = sums.reduce((a,b)=>a+b,0) / R;
const empVar = sums.reduce((a,b)=>a+(b-empMean)**2, 0) / R;
document.getElementById('statsline').textContent =
fmt('n=%n%, rolls=%R% | empirical mean=%empMean% (expected %mu%), σ=%sqrt% (expected %sigma%) — red curve = N(μ,σ²)', { n: n, R: R, empMean: empMean.toFixed(2), mu: mu.toFixed(2), sqrt: Math.sqrt(empVar).toFixed(2), sigma: sigma.toFixed(2) });
// Console verification (node-verifiable)
console.log('CLT demo:', {n, R, mu: mu.toFixed(4), sigma: sigma.toFixed(4),
empMean: empMean.toFixed(4), empStd: Math.sqrt(empVar).toFixed(4)});
}
document.getElementById('rollbtn').addEventListener('click', runDemo);
runDemo();
<div class="controls">
<label>Dice per roll (n): <strong id="nlabel">1</strong>
<input type="range" id="nslider" min="1" max="20" value="1">
</label>
<label>Rolls: <strong id="rlabel">500</strong>
<input type="range" id="rslider" min="100" max="2000" step="100" value="500">
</label>
<button id="rollbtn" type="button">Roll!</button>
</div>
<div class="die-strip">
<span class="die-label">Single die probabilities (biased):</span>
<div id="diebars"></div>
</div>
<canvas id="chart" width="480" height="260"></canvas>
<p id="statsline" class="stats"></p>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; gap: .7rem 1.2rem; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .4rem; }
input[type=range] { width: 110px; accent-color: #1d3557; }
button { font: 600 14px system-ui; padding: .4rem .9rem; background: #1d3557; color: #fff;
border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #274472; }
.die-strip { margin-bottom: .5rem; }
.die-label { font-size: .8rem; color: #555; }
#diebars { display: flex; gap: 3px; align-items: flex-end; height: 40px; margin-top: 4px; }
.diebar { width: 28px; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.diebar-fill { width: 100%; background: #457b9d; border-radius: 3px 3px 0 0; transition: height .3s; }
.diebar-lbl { font-size: 9px; color: #444; }
canvas { display: block; max-width: 100%; border: 1px solid #dde3ea; border-radius: 8px; background: #f8fafc; }
.stats { font-size: .85rem; color: #555; margin: .4rem 0 0; min-height: 1.2em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Central Limit Theorem demo: sum n rolls of a biased die, repeat R times,
// draw histogram and overlay the predicted normal curve.
// Bias: weights [1,1,1,2,3,6] for faces 1..6 (face 6 is 6x more likely than face 1).
const WEIGHTS = [1, 1, 1, 2, 3, 6]; // unnormalized probabilities for faces 1..6
const TOTAL_W = WEIGHTS.reduce((a,b)=>a+b, 0); // 14
const PROBS = WEIGHTS.map(w => w / TOTAL_W);
// Compute die mean and variance
const DIE_MEAN = PROBS.reduce((s,p,i) => s + p*(i+1), 0);
const DIE_VAR = PROBS.reduce((s,p,i) => s + p*Math.pow(i+1 - DIE_MEAN, 2), 0);
// Draw the single-die probability bars
function drawDieBars() {
const cont = document.getElementById('diebars');
cont.innerHTML = '';
const maxP = Math.max(...PROBS);
PROBS.forEach((p, i) => {
const wrap = document.createElement('div'); wrap.className = 'diebar';
const fill = document.createElement('div'); fill.className = 'diebar-fill';
fill.style.height = (p / maxP * 34) + 'px';
const lbl = document.createElement('div'); lbl.className = 'diebar-lbl';
lbl.textContent = i + 1;
wrap.appendChild(fill); wrap.appendChild(lbl);
cont.appendChild(wrap);
});
}
drawDieBars();
// Roll one biased die: returns face value 1..6
function rollDie() {
let r = Math.random() * TOTAL_W, cumW = 0;
for (let i = 0; i < WEIGHTS.length; i++) {
cumW += WEIGHTS[i];
if (r < cumW) return i + 1;
}
return 6;
}
function getN() { return parseInt(document.getElementById('nslider').value, 10); }
function getR() { return parseInt(document.getElementById('rslider').value, 10); }
document.getElementById('nslider').addEventListener('input', () => {
document.getElementById('nlabel').textContent = getN();
});
document.getElementById('rslider').addEventListener('input', () => {
document.getElementById('rlabel').textContent = getR();
});
// Draw histogram + normal curve
function runDemo() {
const n = getN(), R = getR();
// Collect sums
const sums = [];
for (let r = 0; r < R; r++) {
let s = 0;
for (let i = 0; i < n; i++) s += rollDie();
sums.push(s);
}
// Bin into integers
const minS = n, maxS = n * 6;
const freq = new Array(maxS - minS + 1).fill(0);
sums.forEach(s => freq[s - minS]++);
// Predicted normal params
const mu = n * DIE_MEAN;
const sigma = Math.sqrt(n * DIE_VAR);
// Draw
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { l: 38, r: 14, t: 18, b: 28 };
ctx.clearRect(0, 0, W, H);
const bins = freq.length;
const maxFreq = Math.max(...freq);
const xScale = (W - PAD.l - PAD.r) / bins;
const yScale = (H - PAD.t - PAD.b) / (maxFreq || 1);
// Draw bars
ctx.fillStyle = '#a8c7e8';
freq.forEach((f, i) => {
const bh = f * yScale;
ctx.fillRect(PAD.l + i * xScale, H - PAD.b - bh, xScale - 1, bh);
});
// Normal PDF overlay (scaled to match histogram area)
// Area of histogram ≈ R (total count) * 1 bin-width
ctx.beginPath();
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.2;
const norm = (x) => Math.exp(-0.5 * ((x - mu) / sigma) ** 2) / (sigma * Math.sqrt(2 * Math.PI));
for (let i = 0; i <= bins; i++) {
const x = minS + i; // actual sum value at left edge of bin i
const density = norm(x) * R; // expected count per unit
const px = PAD.l + i * xScale;
const py = H - PAD.b - density * yScale;
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
}
ctx.stroke();
// Axes
ctx.strokeStyle = '#b0bec5'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.l, H - PAD.b); ctx.lineTo(W - PAD.r, H - PAD.b);
ctx.moveTo(PAD.l, H - PAD.b); ctx.lineTo(PAD.l, PAD.t);
ctx.stroke();
// X axis labels (a few tick values)
ctx.fillStyle = '#555'; ctx.font = '10px system-ui'; ctx.textAlign = 'center';
const step = Math.max(1, Math.round(bins / 6));
for (let i = 0; i <= bins; i += step) {
const lv = minS + i;
const px = PAD.l + i * xScale;
ctx.fillText(lv, px, H - PAD.b + 12);
}
// Y axis label
ctx.save(); ctx.translate(12, (H - PAD.t - PAD.b) / 2 + PAD.t);
ctx.rotate(-Math.PI / 2); ctx.textAlign = 'center'; ctx.font = '10px system-ui';
ctx.fillText('count', 0, 0); ctx.restore();
// Stats line
const empMean = sums.reduce((a,b)=>a+b,0) / R;
const empVar = sums.reduce((a,b)=>a+(b-empMean)**2, 0) / R;
document.getElementById('statsline').textContent =
fmt('n=%n%, rolls=%R% | empirical mean=%empMean% (expected %mu%), σ=%sqrt% (expected %sigma%) — red curve = N(μ,σ²)', { n: n, R: R, empMean: empMean.toFixed(2), mu: mu.toFixed(2), sqrt: Math.sqrt(empVar).toFixed(2), sigma: sigma.toFixed(2) });
// Console verification (node-verifiable)
console.log('CLT demo:', {n, R, mu: mu.toFixed(4), sigma: sigma.toFixed(4),
empMean: empMean.toFixed(4), empStd: Math.sqrt(empVar).toFixed(4)});
}
document.getElementById('rollbtn').addEventListener('click', runDemo);
runDemo();
Comments
Loading comments...