<p class="hint">This is the <b>Moser spindle</b> — 7 points whose every drawn edge is <b>exactly distance 1</b> apart. Pick a color, then click points so that no two connected points share a color. Try to finish with only <b>3 colors</b>: you can't — some edge is always forced to clash.</p>
<div class="palette" id="palette"></div>
<svg id="graph" viewBox="0 0 276 249" role="img" aria-label="Moser spindle graph"></svg>
<div class="status" id="status">Color all 7 points. Avoid same-color neighbors.</div>
<div class="btns">
<button id="check" type="button">Check coloring</button>
<button id="auto" type="button">Show it needs 4</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.palette { display: flex; gap: .5rem; margin: .2rem 0 .4rem; flex-wrap: wrap; }
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
border: 3px solid #fff; box-shadow: 0 0 0 1px #bbb; }
.swatch.sel { box-shadow: 0 0 0 3px #1d3557; }
svg { width: 100%; max-width: 320px; height: auto; display: block; margin: .2rem 0; }
.edge { stroke: #9aa6b2; stroke-width: 2; }
.edge.bad { stroke: #e63946; stroke-width: 3.5; }
.node { stroke: #1d3557; stroke-width: 2; cursor: pointer; }
.node:hover { stroke-width: 3.5; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hadwiger–Nelson: how many colors to paint the plane so no two points exactly distance 1 apart share a color? A "unit-distance graph" is any finite set of points whose edges are pairs at distance exactly 1; its chromatic number is a lower bound for the plane. The Moser spindle (7 points, 11 unit edges) is NOT 3-colorable, which is why the answer is at least 4. (de Grey, 2018, pushed the lower bound to 5 with a far larger unit-distance graph.) Here we demo the spindle.
const POS = { A:[30,125], B:[119,30], C:[157,154], F:[246,60], D:[157,95], E:[119,219], G:[246,190] };
const EDGES = [['A','B'],['A','C'],['A','D'],['A','E'],['B','C'],['B','F'],['C','F'],['F','G'],['D','E'],['D','G'],['E','G']];
const NAMES = Object.keys(POS);
const COLORS = ['#e63946', '#2a9d8f', '#e9c46a', '#577590']; // red, teal, gold, slate
const NAMECOL = ['Red', 'Teal', 'Gold', 'Slate'];
let palette = [0, 1, 2]; // start with only 3 colors offered
let sel = 0; // selected color index
const color = {}; // node -> color index (or undefined)
function buildPalette() {
const p = document.getElementById('palette'); p.innerHTML = '';
palette.forEach(ci => {
const s = document.createElement('div');
s.className = 'swatch' + (ci === sel ? ' sel' : '');
s.style.background = COLORS[ci];
s.title = NAMECOL[ci];
s.addEventListener('click', () => { sel = ci; buildPalette(); });
p.appendChild(s);
});
}
function badEdges() {
return EDGES.filter(([a, b]) =>
color[a] !== undefined && color[b] !== undefined && color[a] === color[b]);
}
function render() {
const svg = document.getElementById('graph');
const bad = new Set(badEdges().map(e => e.join('-')));
let s = '';
EDGES.forEach(([a, b]) => {
const cls = bad.has(a + '-' + b) ? 'edge bad' : 'edge';
s += `<line class="${cls}" x1="${POS[a][0]}" y1="${POS[a][1]}" x2="${POS[b][0]}" y2="${POS[b][1]}"/>`;
});
NAMES.forEach(n => {
const fill = color[n] === undefined ? '#ffffff' : COLORS[color[n]];
s += `<circle class="node" data-n="${n}" cx="${POS[n][0]}" cy="${POS[n][1]}" r="11" fill="${fill}"/>`;
});
svg.innerHTML = s;
svg.querySelectorAll('.node').forEach(el => {
el.addEventListener('click', () => {
color[el.dataset.n] = sel; render();
setStatus('Color all 7 points. Avoid same-color neighbors.', '');
});
});
}
function setStatus(t, cls) { const e = document.getElementById('status'); e.textContent = t; e.className = 'status ' + cls; }
function check() {
const uncolored = NAMES.filter(n => color[n] === undefined).length;
if (uncolored) { setStatus(fmt('%uncolored% point(s) still uncolored.', { uncolored: uncolored }), 'bad'); return; }
const bad = badEdges();
const used = new Set(NAMES.map(n => color[n])).size;
if (bad.length === 0) setStatus(fmt('✓ Proper coloring with %used% colors! (3 is impossible — you used a 4th.)', { used: used }), 'ok');
else setStatus(fmt('✗ %length% edge(s) clash. With only 3 colors this can never be fixed.', { length: bad.length }), 'bad');
}
// Brute force: try to k-color; return a valid assignment or null.
function tryColor(k) {
const adj = {}; NAMES.forEach(n => adj[n] = []);
EDGES.forEach(([a, b]) => { adj[a].push(b); adj[b].push(a); });
const c = {};
function go(i) {
if (i === NAMES.length) return true;
const v = NAMES[i];
for (let col = 0; col < k; col++) {
if (adj[v].every(u => c[u] !== col)) { c[v] = col; if (go(i + 1)) return true; delete c[v]; }
}
return false;
}
return go(0) ? c : null;
}
function auto() {
// Show that 3 fails and 4 works.
if (tryColor(3)) { setStatus('Unexpected: 3 worked.', 'ok'); return; }
const four = tryColor(4);
palette = [0, 1, 2, 3]; buildPalette();
NAMES.forEach(n => color[n] = four[n]);
render();
setStatus('No 3-coloring exists — a 4th color (Slate) was required. So the plane needs ≥ 4.', 'ok');
}
document.getElementById('check').addEventListener('click', check);
document.getElementById('auto').addEventListener('click', auto);
document.getElementById('reset').addEventListener('click', () => {
NAMES.forEach(n => delete color[n]); palette = [0, 1, 2]; sel = 0;
buildPalette(); render(); setStatus('Color all 7 points. Avoid same-color neighbors.', '');
});
buildPalette(); render();
<p class="hint">This is the <b>Moser spindle</b> — 7 points whose every drawn edge is <b>exactly distance 1</b> apart. Pick a color, then click points so that no two connected points share a color. Try to finish with only <b>3 colors</b>: you can't — some edge is always forced to clash.</p>
<div class="palette" id="palette"></div>
<svg id="graph" viewBox="0 0 276 249" role="img" aria-label="Moser spindle graph"></svg>
<div class="status" id="status">Color all 7 points. Avoid same-color neighbors.</div>
<div class="btns">
<button id="check" type="button">Check coloring</button>
<button id="auto" type="button">Show it needs 4</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.palette { display: flex; gap: .5rem; margin: .2rem 0 .4rem; flex-wrap: wrap; }
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
border: 3px solid #fff; box-shadow: 0 0 0 1px #bbb; }
.swatch.sel { box-shadow: 0 0 0 3px #1d3557; }
svg { width: 100%; max-width: 320px; height: auto; display: block; margin: .2rem 0; }
.edge { stroke: #9aa6b2; stroke-width: 2; }
.edge.bad { stroke: #e63946; stroke-width: 3.5; }
.node { stroke: #1d3557; stroke-width: 2; cursor: pointer; }
.node:hover { stroke-width: 3.5; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hadwiger–Nelson: how many colors to paint the plane so no two points exactly distance 1 apart share a color? A "unit-distance graph" is any finite set of points whose edges are pairs at distance exactly 1; its chromatic number is a lower bound for the plane. The Moser spindle (7 points, 11 unit edges) is NOT 3-colorable, which is why the answer is at least 4. (de Grey, 2018, pushed the lower bound to 5 with a far larger unit-distance graph.) Here we demo the spindle.
const POS = { A:[30,125], B:[119,30], C:[157,154], F:[246,60], D:[157,95], E:[119,219], G:[246,190] };
const EDGES = [['A','B'],['A','C'],['A','D'],['A','E'],['B','C'],['B','F'],['C','F'],['F','G'],['D','E'],['D','G'],['E','G']];
const NAMES = Object.keys(POS);
const COLORS = ['#e63946', '#2a9d8f', '#e9c46a', '#577590']; // red, teal, gold, slate
const NAMECOL = ['Red', 'Teal', 'Gold', 'Slate'];
let palette = [0, 1, 2]; // start with only 3 colors offered
let sel = 0; // selected color index
const color = {}; // node -> color index (or undefined)
function buildPalette() {
const p = document.getElementById('palette'); p.innerHTML = '';
palette.forEach(ci => {
const s = document.createElement('div');
s.className = 'swatch' + (ci === sel ? ' sel' : '');
s.style.background = COLORS[ci];
s.title = NAMECOL[ci];
s.addEventListener('click', () => { sel = ci; buildPalette(); });
p.appendChild(s);
});
}
function badEdges() {
return EDGES.filter(([a, b]) =>
color[a] !== undefined && color[b] !== undefined && color[a] === color[b]);
}
function render() {
const svg = document.getElementById('graph');
const bad = new Set(badEdges().map(e => e.join('-')));
let s = '';
EDGES.forEach(([a, b]) => {
const cls = bad.has(a + '-' + b) ? 'edge bad' : 'edge';
s += `<line class="${cls}" x1="${POS[a][0]}" y1="${POS[a][1]}" x2="${POS[b][0]}" y2="${POS[b][1]}"/>`;
});
NAMES.forEach(n => {
const fill = color[n] === undefined ? '#ffffff' : COLORS[color[n]];
s += `<circle class="node" data-n="${n}" cx="${POS[n][0]}" cy="${POS[n][1]}" r="11" fill="${fill}"/>`;
});
svg.innerHTML = s;
svg.querySelectorAll('.node').forEach(el => {
el.addEventListener('click', () => {
color[el.dataset.n] = sel; render();
setStatus('Color all 7 points. Avoid same-color neighbors.', '');
});
});
}
function setStatus(t, cls) { const e = document.getElementById('status'); e.textContent = t; e.className = 'status ' + cls; }
function check() {
const uncolored = NAMES.filter(n => color[n] === undefined).length;
if (uncolored) { setStatus(fmt('%uncolored% point(s) still uncolored.', { uncolored: uncolored }), 'bad'); return; }
const bad = badEdges();
const used = new Set(NAMES.map(n => color[n])).size;
if (bad.length === 0) setStatus(fmt('✓ Proper coloring with %used% colors! (3 is impossible — you used a 4th.)', { used: used }), 'ok');
else setStatus(fmt('✗ %length% edge(s) clash. With only 3 colors this can never be fixed.', { length: bad.length }), 'bad');
}
// Brute force: try to k-color; return a valid assignment or null.
function tryColor(k) {
const adj = {}; NAMES.forEach(n => adj[n] = []);
EDGES.forEach(([a, b]) => { adj[a].push(b); adj[b].push(a); });
const c = {};
function go(i) {
if (i === NAMES.length) return true;
const v = NAMES[i];
for (let col = 0; col < k; col++) {
if (adj[v].every(u => c[u] !== col)) { c[v] = col; if (go(i + 1)) return true; delete c[v]; }
}
return false;
}
return go(0) ? c : null;
}
function auto() {
// Show that 3 fails and 4 works.
if (tryColor(3)) { setStatus('Unexpected: 3 worked.', 'ok'); return; }
const four = tryColor(4);
palette = [0, 1, 2, 3]; buildPalette();
NAMES.forEach(n => color[n] = four[n]);
render();
setStatus('No 3-coloring exists — a 4th color (Slate) was required. So the plane needs ≥ 4.', 'ok');
}
document.getElementById('check').addEventListener('click', check);
document.getElementById('auto').addEventListener('click', auto);
document.getElementById('reset').addEventListener('click', () => {
NAMES.forEach(n => delete color[n]); palette = [0, 1, 2]; sel = 0;
buildPalette(); render(); setStatus('Color all 7 points. Avoid same-color neighbors.', '');
});
buildPalette(); render();
Comments
Loading comments...