Embed
The script finds its own origin, so nothing else is needed; the widget runs in a sandboxed frame and cannot touch your page. Set the controls and the two lines update; the preview beneath shows what your readers get.
<script src="https://learn.mimmsy.com/learn-widget.js"></script>
<learn-widget name="mutual-information"></learn-widget> It is CC-BY-4.0: keep the credit line the frame shows.
Preview
Fork
A fork is a copy whose manifest names its parent and the parent's version; lineage is kept forever, and the copy is yours to change. The library is its own repository: clone it, copy mutual-information/ to a new name (one DNS label), set forkedFrom to { "name": "mutual-information", "version": 1 }, change what you want, run npm run check, and open a pull request. Passing the check is the whole gate. Without a checkout, submit the same files to POST /api/widgets and it is served instantly as an unreviewed draft.
Source
The whole widget is these files; the repository has their history.
widget.json
{
"name": "mutual-information",
"title": "Mutual information",
"version": 1,
"claim": "Two variables share I(X;Y) = H(X) + H(Y) − H(X,Y) bits: an umbrella that copies the weather perfectly shares all of the weather's entropy, one matched at reliability ½ shares none, and the overlap can never exceed the smaller of the two entropies.",
"summary": "Set how often it rains and how reliably a citizen matches their umbrella to the sky, watch paired streams of weather and umbrellas, and read the two entropies as bars whose overlap is the mutual information and whose span is the joint entropy.",
"topics": [
"information/divergence",
"information/channels",
"math/probability"
],
"aliases": [
"mutual information",
"joint entropy",
"conditional entropy",
"shared information",
"I(X;Y)"
],
"params": {
"rain": {
"type": "number",
"default": 0.3,
"min": 0.05,
"max": 0.95,
"label": "P(rain)"
},
"reliability": {
"type": "number",
"default": 0.85,
"min": 0,
"max": 1,
"label": "umbrella reliability"
}
},
"check": [
{
"q": "X is a fair coin and Y is a perfect copy of X. What is I(X;Y)?",
"options": [
"0 bits",
"2 bits",
"1 bit",
"Infinite"
],
"answer": 2,
"why": "Y carries exactly the information in X, so I = H(X) = 1 bit, and in the bar picture the overlap can never exceed either bar."
},
{
"q": "X and Y are independent. Their mutual information I(X;Y) is…",
"options": [
"0",
"H(X)",
"H(X) + H(Y)",
"1 bit"
],
"answer": 0,
"why": "Independence means H(X,Y) = H(X) + H(Y), so the bars do not overlap and the two variables share no information."
},
{
"q": "In the language of mutual information, the capacity of a channel is…",
"options": [
"the entropy of the noise",
"the minimum KL divergence across codes",
"the maximum of I(input; output) over input distributions",
"always 1 bit for binary channels"
],
"answer": 2,
"why": "Shannon defined C = max I(X;Y) over input distributions, and the binary symmetric channel's 1 − H(p) is that maximum evaluated explicitly."
}
],
"capabilities": [],
"height": 470,
"requires": [
"coin-entropy",
"binary-symmetric-channel"
],
"forkedFrom": null,
"authors": []
}
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mutual information</title>
<style>
:root {
color-scheme: light dark;
--bg: #f7f3ea; --bg-card: #efe9da; --border: #d8cfba; --text: #211d14; --text-dim: #6e6553;
--accent: #31597f; --accent2: #b04e1b; --hot: #a82433; --ok: #3d6b4f;
--ink-rgb: 33, 29, 20; --paper-rgb: 247, 243, 234; --accent-rgb: 49, 89, 127;
--accent2-rgb: 176, 78, 27; --hot-rgb: 168, 36, 51; --ok-rgb: 61, 107, 79;
--serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #161410; --bg-card: #1e1b15; --border: #383225; --text: #e9e3d3; --text-dim: #9c917c;
--accent: #8fb8e0; --accent2: #dd9355; --hot: #df7a88; --ok: #82bd97;
--ink-rgb: 233, 227, 211; --paper-rgb: 22, 20, 16; --accent-rgb: 143, 184, 224;
--accent2-rgb: 221, 147, 85; --hot-rgb: 223, 122, 136; --ok-rgb: 130, 189, 151;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body { background: transparent; color: var(--text); font-family: var(--serif); font-size: 17px; line-height: 1.5; }
.widget { background: var(--bg-card); border: 1px solid var(--border); border-radius: 4px; padding: 22px; }
.control-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 12px; }
.control-row label { font-size: 14px; color: var(--text-dim); white-space: nowrap; width: 150px; }
input[type="range"] { flex: 1; min-width: 160px; accent-color: var(--accent); margin: 0; }
.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.val { min-width: 4ch; font-size: 14px; }
.btn { font-family: var(--mono); font-size: 12.5px; color: var(--text); background: transparent; border: 1px solid var(--border); border-radius: 3px; padding: 6px 13px; cursor: pointer; transition: border-color .15s, color .15s; }
.btn:hover { border-color: var(--accent); color: var(--accent); }
.pair-row { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.pair-label { flex: none; width: 70px; text-align: right; font-family: var(--mono); font-size: 11px; color: var(--text-dim); }
.stream { flex: 1; font-family: var(--mono); font-size: 15px; letter-spacing: 2px; white-space: nowrap; overflow: hidden; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 8px 14px; user-select: none; line-height: 1.4; }
.stream .h { color: var(--accent); }
.stream .t { color: var(--accent2); }
.stream .u { color: var(--accent); }
.stream .n { color: var(--text-dim); opacity: 0.6; }
.plot { display: block; width: 100%; height: 150px; margin-top: 8px; }
.readout-row { display: flex; gap: 28px; flex-wrap: wrap; align-items: flex-end; margin: 10px 0 0; }
.readout .big { display: block; font-size: 34px; font-weight: 600; color: var(--accent); line-height: 1.1; min-width: 5ch; }
.readout.accent .big { color: var(--accent2); }
.readout.hot .big { color: var(--hot); }
.readout small { color: var(--text-dim); font-size: 12.5px; }
@media (max-width: 560px) { .readout .big { font-size: 27px; } .pair-label { width: 56px; } .control-row label { width: auto; } }
</style>
</head>
<body>
<div class="widget">
<div class="control-row">
<label for="p">P(rain)</label>
<input type="range" id="p" min="5" max="95" value="30">
<span class="mono val" id="p-label">0.30</span>
</div>
<div class="control-row">
<label for="r">umbrella reliability</label>
<input type="range" id="r" min="0" max="100" value="85">
<span class="mono val" id="r-label">0.85</span>
<button class="btn" id="resample">resample</button>
</div>
<div class="pair-row"><span class="pair-label">weather</span><div class="stream" id="stream-x"></div></div>
<div class="pair-row"><span class="pair-label">umbrella</span><div class="stream" id="stream-y"></div></div>
<canvas id="venn" class="plot"></canvas>
<div class="readout-row">
<div class="readout">
<span class="big mono" id="hx">0.00</span>
<small>H(weather) · bits</small>
</div>
<div class="readout accent">
<span class="big mono" id="hy">0.00</span>
<small>H(umbrella) · bits</small>
</div>
<div class="readout hot">
<span class="big mono" id="i">0.00</span>
<small>I(weather; umbrella) · shared bits</small>
</div>
<div class="readout">
<span class="big mono" id="hxy">0.00</span>
<small>H(weather, umbrella) · joint bits</small>
</div>
</div>
</div>
<script src="/w/_sdk/host.js?v=1"></script>
<script src="/w/_lib/info.js?v=1"></script>
<script src="widget.js?v=1"></script>
</body>
</html>
widget.js
/* Mutual information: weather and an umbrella matched to it with some reliability; the bits they share as an overlap. */
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
const { clamp, binaryH, T, readTheme, accentWash, accent2Wash, hotWash, setupCanvas, onResize, MONO } = window.Info;
let p = 0.3;
let r = 0.85;
let host = null;
function stats() {
const marg = p * r + (1 - p) * (1 - r);
const HX = binaryH(p);
const HY = binaryH(marg);
const HYgX = binaryH(r);
return { HX, HY, I: Math.max(0, HY - HYgX), HXY: HX + HYgX };
}
function renderStreams() {
const N = clamp(Math.floor(($('stream-x').clientWidth - 28) / 11), 16, 96);
let sx = '', sy = '';
for (let i = 0; i < N; i++) {
const rain = Math.random() < p;
const u = Math.random() < r ? rain : !rain;
sx += rain ? '<span class="h">R</span>' : '<span class="t">S</span>';
sy += u ? '<span class="u">U</span>' : '<span class="n">·</span>';
}
$('stream-x').innerHTML = sx;
$('stream-y').innerHTML = sy;
}
function drawVenn() {
const { ctx, w, h } = setupCanvas($('venn'));
const { HX, HY, I, HXY } = stats();
const m = { l: 84, r: 18 };
const sx = (w - m.l - m.r) / 2;
const X = (b) => m.l + b * sx;
ctx.clearRect(0, 0, w, h);
ctx.font = '10px ' + MONO;
for (let t = 0; t <= 2; t += 0.5) {
ctx.strokeStyle = T.GRID;
ctx.beginPath();
ctx.moveTo(X(t), 12);
ctx.lineTo(X(t), 118);
ctx.stroke();
ctx.fillStyle = T.DIM;
ctx.textAlign = 'center';
ctx.fillText(t === 2 ? '2 bits' : String(t), X(t), 132);
}
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
ctx.fillStyle = T.DIM;
ctx.fillText('weather', m.l - 10, 28);
ctx.fillText('umbrella', m.l - 10, 58);
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = accentWash(0.28);
ctx.strokeStyle = T.ACCENT;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.roundRect(X(0), 16, Math.max(HX * sx, 1.5), 24, 5);
ctx.fill();
ctx.stroke();
ctx.fillStyle = accent2Wash(0.28);
ctx.strokeStyle = T.ACCENT2;
ctx.beginPath();
ctx.roundRect(X(HX - I), 46, Math.max(HY * sx, 1.5), 24, 5);
ctx.fill();
ctx.stroke();
if (I > 0.004) {
const ox = X(HX - I);
const ow = I * sx;
ctx.fillStyle = hotWash(0.16);
ctx.fillRect(ox, 12, ow, 62);
ctx.strokeStyle = T.HOT;
ctx.setLineDash([4, 4]);
ctx.strokeRect(ox, 12, ow, 62);
ctx.setLineDash([]);
ctx.fillStyle = T.HOT;
ctx.textAlign = 'center';
ctx.fillText('I = ' + I.toFixed(2), ox + ow / 2, 9);
}
ctx.strokeStyle = T.DIM;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(X(0), 86);
ctx.lineTo(X(HXY), 86);
ctx.moveTo(X(0), 82);
ctx.lineTo(X(0), 90);
ctx.moveTo(X(HXY), 82);
ctx.lineTo(X(HXY), 90);
ctx.stroke();
ctx.fillStyle = T.DIM;
ctx.textAlign = 'center';
ctx.fillText('H(X,Y) = ' + HXY.toFixed(2) + ' bits', X(HXY / 2), 102);
}
function render() {
const { HX, HY, I, HXY } = stats();
$('p-label').textContent = p.toFixed(2);
$('r-label').textContent = r.toFixed(2);
$('hx').textContent = HX.toFixed(2);
$('hy').textContent = HY.toFixed(2);
$('i').textContent = I.toFixed(2);
$('hxy').textContent = HXY.toFixed(2);
drawVenn();
}
function setP(v, fromSlider) {
p = clamp(v, 0.05, 0.95);
if (!fromSlider) $('p').value = Math.round(p * 100);
render();
renderStreams();
}
function setR(v, fromSlider) {
r = clamp(v, 0, 1);
if (!fromSlider) $('r').value = Math.round(r * 100);
render();
renderStreams();
}
const ping = (data) => { if (host && host.signal) host.signal('interaction', data); };
$('p').addEventListener('input', () => { setP($('p').value / 100, true); ping({ p, r }); });
$('r').addEventListener('input', () => { setR($('r').value / 100, true); ping({ p, r }); });
$('resample').addEventListener('click', () => { renderStreams(); ping({ resample: true }); });
onResize(render);
readTheme();
render();
renderStreams();
LearnWidget.connect().then((h) => {
host = h;
readTheme();
if (typeof h.params.rain === 'number') setP(h.params.rain);
if (typeof h.params.reliability === 'number') setR(h.params.reliability);
render();
h.on('theme.changed', () => { readTheme(); render(); });
h.on('params.changed', (np) => {
if (typeof np.rain === 'number') setP(np.rain);
if (typeof np.reliability === 'number') setR(np.reliability);
});
});
})();