Below is a three-state Markov chain: Sunny, Cloudy, and Rainy. Each state has fixed transition probabilities to the next day's weather. Click Step to advance one day, or Run to watch the chain evolve automatically.
The bar chart tracks how often the chain has visited each state. Press Run and watch the bars stabilize — that convergence is the stationary distribution revealing itself.
<div class="layout">
<div class="left-panel">
<div class="state-label">Current state</div>
<div id="current-state" class="state-display sunny">☀ Sunny</div>
<div class="transition-box" id="transition-box">
<div class="trans-title">Transition probabilities</div>
<div id="trans-rows"></div>
</div>
<div class="step-count">Day <span id="day-count">0</span> | Steps <span id="step-count">0</span></div>
</div>
<div class="right-panel">
<div class="chart-title">Long-run visit frequency</div>
<div id="chart" class="chart"></div>
<div class="target-row" id="target-row"></div>
<div class="legend">— stationary distribution</div>
</div>
</div>
<div class="btns">
<button id="btn-step">Step</button>
<button id="btn-run" class="accent">Run</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
<div class="hint" id="hint">Click <b>Step</b> to advance one day, or <b>Run</b> to watch the chain evolve automatically.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; font-size: 14px; }
.layout { display: flex; gap: 16px; align-items: flex-start; }
.left-panel { flex: 0 0 160px; display: flex; flex-direction: column; gap: 10px; }
.right-panel { flex: 1; }
.state-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #666; }
.state-display { font-size: 18px; font-weight: 700; padding: 10px 14px; border-radius: 10px;
text-align: center; transition: background .3s, color .3s; }
.state-display.sunny { background: #fff3b0; color: #7a5a00; }
.state-display.cloudy { background: #dce8f0; color: #2d4a5e; }
.state-display.rainy { background: #d0d8f8; color: #1a2580; }
.transition-box { background: #f5f7fa; border: 1px solid #dde3ea; border-radius: 8px; padding: 10px; }
.trans-title { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
.trans-row { display: flex; justify-content: space-between; font-size: 13px; padding: 2px 0; }
.trans-pct { font-weight: 600; color: #1d3557; }
.step-count { font-size: 12px; color: #888; text-align: center; }
.chart-title { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #666; margin-bottom: 8px; }
.chart { display: flex; align-items: flex-end; gap: 12px; height: 140px; position: relative; }
.bar-group { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bar-wrap { width: 100%; height: 120px; display: flex; align-items: flex-end; position: relative; }
.bar { width: 100%; border-radius: 5px 5px 0 0; transition: height .25s; }
.bar.sunny { background: #f4c430; }
.bar.cloudy { background: #7eafc9; }
.bar.rainy { background: #5b6bcc; }
.target-line { position: absolute; left: 0; right: 0; height: 2px; background: #e63946; opacity: .75; transition: bottom .25s; }
.bar-lbl { font-size: 11px; color: #555; text-align: center; }
.bar-pct { font-size: 12px; font-weight: 700; color: #333; min-height: 1.2em; text-align: center; }
.target-row { display: flex; gap: 12px; margin-top: 4px; }
.target-cell { flex: 1; text-align: center; font-size: 11px; color: #e63946; font-weight: 600; }
.legend { font-size: 11px; color: #e63946; margin-top: 2px; }
.btns { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .9rem; border-radius: 8px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.accent { background: #2a9d8f; border-color: #2a9d8f; }
button.ghost { background: #fff; color: #1d3557; }
.hint { font-size: 12px; color: #666; margin-top: 8px; line-height: 1.5; }
// Three-state Markov chain: Sunny (0), Cloudy (1), Rainy (2)
// Transition matrix P[from][to]:
const STATES = ['Sunny', 'Cloudy', 'Rainy'];
const CLASSES = ['sunny', 'cloudy', 'rainy'];
const ICONS = ['☀', '⛅', '🌧'];
const P = [
[0.6, 0.3, 0.1], // from Sunny
[0.3, 0.4, 0.3], // from Cloudy
[0.2, 0.4, 0.4] // from Rainy
];
// Compute stationary distribution by power iteration
function stationaryDist(P, iter=2000) {
let pi = [1/3, 1/3, 1/3];
for (let k = 0; k < iter; k++) {
const next = [0, 0, 0];
for (let j = 0; j < 3; j++)
for (let i = 0; i < 3; i++)
next[j] += pi[i] * P[i][j];
pi = next;
}
return pi;
}
const PI = stationaryDist(P); // e.g. ≈ [0.4286, 0.3571, 0.2143]
let state = 0; // current state index
let steps = 0;
let counts = [0, 0, 0];
let running = false;
let timer = null;
function nextState(s) {
const r = Math.random(), row = P[s];
let cum = 0;
for (let j = 0; j < 3; j++) { cum += row[j]; if (r < cum) return j; }
return 2;
}
function updateTransBox(s) {
const box = document.getElementById('trans-rows'); box.innerHTML = '';
P[s].forEach((p, j) => {
const row = document.createElement('div'); row.className = 'trans-row';
row.innerHTML = `<span>${ICONS[j]} ${STATES[j]}</span><span class="trans-pct">${Math.round(p*100)}%</span>`;
box.appendChild(row);
});
}
function updateChart() {
const total = steps || 1;
const chart = document.getElementById('chart');
STATES.forEach((s, i) => {
const freq = counts[i] / total;
const bar = chart.querySelector('.bar.' + CLASSES[i]);
const pct = chart.querySelector('.bar-pct[data-i="' + i + '"]');
const line = chart.querySelector('.target-line[data-i="' + i + '"]');
bar.style.height = (freq * 120).toFixed(1) + 'px';
pct.textContent = steps ? (freq * 100).toFixed(1) + '%' : '';
if (line) line.style.bottom = (PI[i] * 120).toFixed(1) + 'px';
});
}
function buildChart() {
const chart = document.getElementById('chart'); chart.innerHTML = '';
const target = document.getElementById('target-row'); target.innerHTML = '';
STATES.forEach((s, i) => {
const grp = document.createElement('div'); grp.className = 'bar-group';
const pct = document.createElement('div');
pct.className = 'bar-pct'; pct.setAttribute('data-i', i); pct.textContent = '';
const wrap = document.createElement('div'); wrap.className = 'bar-wrap';
const bar = document.createElement('div');
bar.className = 'bar ' + CLASSES[i]; bar.style.height = '0px';
const line = document.createElement('div');
line.className = 'target-line'; line.setAttribute('data-i', i);
line.style.bottom = (PI[i] * 120).toFixed(1) + 'px';
wrap.appendChild(bar); wrap.appendChild(line);
const lbl = document.createElement('div'); lbl.className = 'bar-lbl';
lbl.textContent = ICONS[i] + ' ' + s;
grp.appendChild(pct); grp.appendChild(wrap); grp.appendChild(lbl);
chart.appendChild(grp);
const tc = document.createElement('div'); tc.className = 'target-cell';
tc.textContent = 'π=' + (PI[i]*100).toFixed(1) + '%';
target.appendChild(tc);
});
}
function render() {
const el = document.getElementById('current-state');
el.className = 'state-display ' + CLASSES[state];
el.textContent = ICONS[state] + ' ' + STATES[state];
document.getElementById('day-count').textContent = steps;
document.getElementById('step-count').textContent = steps;
updateTransBox(state);
updateChart();
}
function step() {
state = nextState(state);
steps++;
counts[state]++;
const freq = counts[state] / steps;
const hint = steps < 30
? 'Keep stepping — the bars will stabilize as the chain runs longer.'
: Math.abs(freq - PI[state]) < 0.03
? '✓ Visit frequencies are converging to the stationary distribution!'
: 'Still converging — run a few hundred steps to see the pattern clearly.';
document.getElementById('hint').innerHTML = hint;
render();
}
function startRun() {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
timer = setInterval(() => { step(); }, 80);
}
function stopRun() {
running = false;
clearInterval(timer);
document.getElementById('btn-run').textContent = 'Run';
}
function reset() {
stopRun();
state = 0; steps = 0; counts = [0, 0, 0];
document.getElementById('hint').innerHTML = 'Click <b>Step</b> to advance one day, or <b>Run</b> to watch the chain evolve automatically.';
render();
}
document.getElementById('btn-step').addEventListener('click', () => { stopRun(); step(); });
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-reset').addEventListener('click', reset);
buildChart();
render();
<div class="layout">
<div class="left-panel">
<div class="state-label">Current state</div>
<div id="current-state" class="state-display sunny">☀ Sunny</div>
<div class="transition-box" id="transition-box">
<div class="trans-title">Transition probabilities</div>
<div id="trans-rows"></div>
</div>
<div class="step-count">Day <span id="day-count">0</span> | Steps <span id="step-count">0</span></div>
</div>
<div class="right-panel">
<div class="chart-title">Long-run visit frequency</div>
<div id="chart" class="chart"></div>
<div class="target-row" id="target-row"></div>
<div class="legend">— stationary distribution</div>
</div>
</div>
<div class="btns">
<button id="btn-step">Step</button>
<button id="btn-run" class="accent">Run</button>
<button id="btn-reset" class="ghost">Reset</button>
</div>
<div class="hint" id="hint">Click <b>Step</b> to advance one day, or <b>Run</b> to watch the chain evolve automatically.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; font-size: 14px; }
.layout { display: flex; gap: 16px; align-items: flex-start; }
.left-panel { flex: 0 0 160px; display: flex; flex-direction: column; gap: 10px; }
.right-panel { flex: 1; }
.state-label { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #666; }
.state-display { font-size: 18px; font-weight: 700; padding: 10px 14px; border-radius: 10px;
text-align: center; transition: background .3s, color .3s; }
.state-display.sunny { background: #fff3b0; color: #7a5a00; }
.state-display.cloudy { background: #dce8f0; color: #2d4a5e; }
.state-display.rainy { background: #d0d8f8; color: #1a2580; }
.transition-box { background: #f5f7fa; border: 1px solid #dde3ea; border-radius: 8px; padding: 10px; }
.trans-title { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
.trans-row { display: flex; justify-content: space-between; font-size: 13px; padding: 2px 0; }
.trans-pct { font-weight: 600; color: #1d3557; }
.step-count { font-size: 12px; color: #888; text-align: center; }
.chart-title { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #666; margin-bottom: 8px; }
.chart { display: flex; align-items: flex-end; gap: 12px; height: 140px; position: relative; }
.bar-group { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.bar-wrap { width: 100%; height: 120px; display: flex; align-items: flex-end; position: relative; }
.bar { width: 100%; border-radius: 5px 5px 0 0; transition: height .25s; }
.bar.sunny { background: #f4c430; }
.bar.cloudy { background: #7eafc9; }
.bar.rainy { background: #5b6bcc; }
.target-line { position: absolute; left: 0; right: 0; height: 2px; background: #e63946; opacity: .75; transition: bottom .25s; }
.bar-lbl { font-size: 11px; color: #555; text-align: center; }
.bar-pct { font-size: 12px; font-weight: 700; color: #333; min-height: 1.2em; text-align: center; }
.target-row { display: flex; gap: 12px; margin-top: 4px; }
.target-cell { flex: 1; text-align: center; font-size: 11px; color: #e63946; font-weight: 600; }
.legend { font-size: 11px; color: #e63946; margin-top: 2px; }
.btns { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .9rem; border-radius: 8px; cursor: pointer;
border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.accent { background: #2a9d8f; border-color: #2a9d8f; }
button.ghost { background: #fff; color: #1d3557; }
.hint { font-size: 12px; color: #666; margin-top: 8px; line-height: 1.5; }
// Three-state Markov chain: Sunny (0), Cloudy (1), Rainy (2)
// Transition matrix P[from][to]:
const STATES = ['Sunny', 'Cloudy', 'Rainy'];
const CLASSES = ['sunny', 'cloudy', 'rainy'];
const ICONS = ['☀', '⛅', '🌧'];
const P = [
[0.6, 0.3, 0.1], // from Sunny
[0.3, 0.4, 0.3], // from Cloudy
[0.2, 0.4, 0.4] // from Rainy
];
// Compute stationary distribution by power iteration
function stationaryDist(P, iter=2000) {
let pi = [1/3, 1/3, 1/3];
for (let k = 0; k < iter; k++) {
const next = [0, 0, 0];
for (let j = 0; j < 3; j++)
for (let i = 0; i < 3; i++)
next[j] += pi[i] * P[i][j];
pi = next;
}
return pi;
}
const PI = stationaryDist(P); // e.g. ≈ [0.4286, 0.3571, 0.2143]
let state = 0; // current state index
let steps = 0;
let counts = [0, 0, 0];
let running = false;
let timer = null;
function nextState(s) {
const r = Math.random(), row = P[s];
let cum = 0;
for (let j = 0; j < 3; j++) { cum += row[j]; if (r < cum) return j; }
return 2;
}
function updateTransBox(s) {
const box = document.getElementById('trans-rows'); box.innerHTML = '';
P[s].forEach((p, j) => {
const row = document.createElement('div'); row.className = 'trans-row';
row.innerHTML = `<span>${ICONS[j]} ${STATES[j]}</span><span class="trans-pct">${Math.round(p*100)}%</span>`;
box.appendChild(row);
});
}
function updateChart() {
const total = steps || 1;
const chart = document.getElementById('chart');
STATES.forEach((s, i) => {
const freq = counts[i] / total;
const bar = chart.querySelector('.bar.' + CLASSES[i]);
const pct = chart.querySelector('.bar-pct[data-i="' + i + '"]');
const line = chart.querySelector('.target-line[data-i="' + i + '"]');
bar.style.height = (freq * 120).toFixed(1) + 'px';
pct.textContent = steps ? (freq * 100).toFixed(1) + '%' : '';
if (line) line.style.bottom = (PI[i] * 120).toFixed(1) + 'px';
});
}
function buildChart() {
const chart = document.getElementById('chart'); chart.innerHTML = '';
const target = document.getElementById('target-row'); target.innerHTML = '';
STATES.forEach((s, i) => {
const grp = document.createElement('div'); grp.className = 'bar-group';
const pct = document.createElement('div');
pct.className = 'bar-pct'; pct.setAttribute('data-i', i); pct.textContent = '';
const wrap = document.createElement('div'); wrap.className = 'bar-wrap';
const bar = document.createElement('div');
bar.className = 'bar ' + CLASSES[i]; bar.style.height = '0px';
const line = document.createElement('div');
line.className = 'target-line'; line.setAttribute('data-i', i);
line.style.bottom = (PI[i] * 120).toFixed(1) + 'px';
wrap.appendChild(bar); wrap.appendChild(line);
const lbl = document.createElement('div'); lbl.className = 'bar-lbl';
lbl.textContent = ICONS[i] + ' ' + s;
grp.appendChild(pct); grp.appendChild(wrap); grp.appendChild(lbl);
chart.appendChild(grp);
const tc = document.createElement('div'); tc.className = 'target-cell';
tc.textContent = 'π=' + (PI[i]*100).toFixed(1) + '%';
target.appendChild(tc);
});
}
function render() {
const el = document.getElementById('current-state');
el.className = 'state-display ' + CLASSES[state];
el.textContent = ICONS[state] + ' ' + STATES[state];
document.getElementById('day-count').textContent = steps;
document.getElementById('step-count').textContent = steps;
updateTransBox(state);
updateChart();
}
function step() {
state = nextState(state);
steps++;
counts[state]++;
const freq = counts[state] / steps;
const hint = steps < 30
? 'Keep stepping — the bars will stabilize as the chain runs longer.'
: Math.abs(freq - PI[state]) < 0.03
? '✓ Visit frequencies are converging to the stationary distribution!'
: 'Still converging — run a few hundred steps to see the pattern clearly.';
document.getElementById('hint').innerHTML = hint;
render();
}
function startRun() {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
timer = setInterval(() => { step(); }, 80);
}
function stopRun() {
running = false;
clearInterval(timer);
document.getElementById('btn-run').textContent = 'Run';
}
function reset() {
stopRun();
state = 0; steps = 0; counts = [0, 0, 0];
document.getElementById('hint').innerHTML = 'Click <b>Step</b> to advance one day, or <b>Run</b> to watch the chain evolve automatically.';
render();
}
document.getElementById('btn-step').addEventListener('click', () => { stopRun(); step(); });
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-reset').addEventListener('click', reset);
buildChart();
render();
No matter which state you start in, the long-run visit frequencies always converge to the same values. That is the stationary distribution at work: the chain's inevitable destiny, baked into its transition probabilities from the very beginning.
Comments
Loading comments...