A straight line splits the plane into two sides — call them red and blue. Shattering a set of points means: for every possible way of coloring them red/blue, some line gets the colors right.
Click the points below to flip their colors. The demo searches for a separating line and tells you whether one exists. Try all the colorings with 3 points — every one works. Then switch to 4 points and find the coloring that defies every line.
<p class="hint">A straight line cuts the plane into a red side and a blue side.
Click a point to flip its color. The demo searches for a line that separates the
colors. With <b>3</b> points every coloring works — a line <b>shatters</b> them.
With <b>4</b> points one coloring (the diagonal "XOR") defeats every line.</p>
<div class="btns">
<button id="mode3" type="button">3 points</button>
<button id="mode4" type="button" class="ghost">4 points</button>
<button id="all" type="button" class="ghost">Test all colorings</button>
</div>
<svg id="plane" viewBox="0 0 300 220" width="100%" preserveAspectRatio="xMidYMid meet"></svg>
<div class="status" id="status">Click points to flip their color.</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 .7rem; line-height: 1.45; }
#plane { background: #f4f7fa; border: 1px solid #cdd9e3; border-radius: 10px; max-width: 420px; display: block; }
.dot { cursor: pointer; stroke: #1d3557; stroke-width: 1.5; }
.dot.red { fill: #e63946; }
.dot.blue { fill: #3a78c2; }
.sep { stroke: #0a7d33; stroke-width: 2.5; stroke-dasharray: 6 4; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
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] ?? ''));
// VC dimension of a line in the plane = 3. A set is "shattered" if EVERY red/blue
// coloring can be separated by some straight line. 3 points in general position
// can be shattered (all 2^3 colorings work); 4 points never can (the diagonal
// XOR coloring is not linearly separable). We test separability by brute force.
const SVG = 'http://www.w3.org/2000/svg';
// 3 points: a triangle in general position. 4 points: a square (the classic XOR case).
const SETS = {
3: [[80,150],[150,60],[220,150]],
4: [[80,60],[220,60],[80,160],[220,160]]
};
let n = 3;
let pts = SETS[3].map(p => p.slice());
let labels = pts.map(() => 0); // 0 = blue, 1 = red
// Is a labelling linearly separable? Search many candidate lines (orientations and
// offsets); a coloring is separable iff some line puts all reds on one side and all
// blues on the other. Returns the separating line params or null.
function separating(points, lab){
const STEPS = 180;
for(let a=0;a<STEPS;a++){
const th = Math.PI * a / STEPS;
const nx = Math.cos(th), ny = Math.sin(th); // line normal
// project points; try every threshold between consecutive projections, both polarities
const proj = points.map(([x,y]) => nx*x + ny*y);
const sorted = [...proj].sort((u,v)=>u-v);
for(let i=0;i<=sorted.length;i++){
const t = i===0 ? sorted[0]-1 : i===sorted.length ? sorted[sorted.length-1]+1
: (sorted[i-1]+sorted[i])/2;
for(const pol of [1,-1]){
let ok = true;
for(let k=0;k<points.length;k++){
const side = (proj[k] - t) * pol >= 0 ? 1 : 0; // 1 = red side
if(side !== lab[k]) { ok = false; break; }
}
if(ok) return { nx, ny, t, pol };
}
}
}
return null;
}
function draw(line){
const s = document.getElementById('plane'); s.innerHTML='';
if(line){
// draw the separating line: points where nx*x+ny*y = t, clipped to the box
const { nx, ny, t } = line; const seg = [];
const W=300,H=220;
const edges = [[0,0,W,0],[W,0,W,H],[W,H,0,H],[0,H,0,0]];
for(const [x1,y1,x2,y2] of edges){
const d1 = nx*x1+ny*y1 - t, d2 = nx*x2+ny*y2 - t;
if((d1<=0&&d2>=0)||(d1>=0&&d2<=0)){
const r = d1===d2 ? 0 : d1/(d1-d2);
seg.push([x1+(x2-x1)*r, y1+(y2-y1)*r]);
}
}
if(seg.length>=2){
const ln=document.createElementNS(SVG,'line');
ln.setAttribute('x1',seg[0][0]); ln.setAttribute('y1',seg[0][1]);
ln.setAttribute('x2',seg[1][0]); ln.setAttribute('y2',seg[1][1]);
ln.setAttribute('class','sep'); s.appendChild(ln);
}
}
pts.forEach((p,i)=>{
const c=document.createElementNS(SVG,'circle');
c.setAttribute('cx',p[0]); c.setAttribute('cy',p[1]); c.setAttribute('r',13);
c.setAttribute('class','dot '+(labels[i]?'red':'blue'));
c.addEventListener('click',()=>{ labels[i]^=1; update(); });
s.appendChild(c);
});
}
function setStatus(t,cls){ const el=document.getElementById('status'); el.textContent=t; el.className='status '+cls; }
function update(){
const line = separating(pts, labels);
if(line){ draw(line); setStatus('Separable — a line splits these colors.','ok'); }
else { draw(null); setStatus('Not separable — no line can split this coloring!','bad'); }
}
function testAll(){
let bad = 0;
for(let m=0;m<(1<<n);m++){
const lab = pts.map((_,i)=>(m>>i)&1);
if(!separating(pts, lab)) bad++;
}
if(bad===0) setStatus(fmt('All %v% colorings are separable — these %n% points are SHATTERED.', { v: 1<<n, n: n }),'ok');
else setStatus(fmt('%bad% of %v% colorings cannot be separated — these %n% points are NOT shattered.', { bad: bad, v: 1<<n, n: n }),'bad');
}
function setMode(k){
n=k; pts=SETS[k].map(p=>p.slice()); labels=pts.map(()=>0);
document.getElementById('mode3').className = k===3?'':'ghost';
document.getElementById('mode4').className = k===4?'':'ghost';
update();
}
document.getElementById('mode3').addEventListener('click',()=>setMode(3));
document.getElementById('mode4').addEventListener('click',()=>setMode(4));
document.getElementById('all').addEventListener('click',testAll);
update();
<p class="hint">A straight line cuts the plane into a red side and a blue side.
Click a point to flip its color. The demo searches for a line that separates the
colors. With <b>3</b> points every coloring works — a line <b>shatters</b> them.
With <b>4</b> points one coloring (the diagonal "XOR") defeats every line.</p>
<div class="btns">
<button id="mode3" type="button">3 points</button>
<button id="mode4" type="button" class="ghost">4 points</button>
<button id="all" type="button" class="ghost">Test all colorings</button>
</div>
<svg id="plane" viewBox="0 0 300 220" width="100%" preserveAspectRatio="xMidYMid meet"></svg>
<div class="status" id="status">Click points to flip their color.</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 .7rem; line-height: 1.45; }
#plane { background: #f4f7fa; border: 1px solid #cdd9e3; border-radius: 10px; max-width: 420px; display: block; }
.dot { cursor: pointer; stroke: #1d3557; stroke-width: 1.5; }
.dot.red { fill: #e63946; }
.dot.blue { fill: #3a78c2; }
.sep { stroke: #0a7d33; stroke-width: 2.5; stroke-dasharray: 6 4; }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
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] ?? ''));
// VC dimension of a line in the plane = 3. A set is "shattered" if EVERY red/blue
// coloring can be separated by some straight line. 3 points in general position
// can be shattered (all 2^3 colorings work); 4 points never can (the diagonal
// XOR coloring is not linearly separable). We test separability by brute force.
const SVG = 'http://www.w3.org/2000/svg';
// 3 points: a triangle in general position. 4 points: a square (the classic XOR case).
const SETS = {
3: [[80,150],[150,60],[220,150]],
4: [[80,60],[220,60],[80,160],[220,160]]
};
let n = 3;
let pts = SETS[3].map(p => p.slice());
let labels = pts.map(() => 0); // 0 = blue, 1 = red
// Is a labelling linearly separable? Search many candidate lines (orientations and
// offsets); a coloring is separable iff some line puts all reds on one side and all
// blues on the other. Returns the separating line params or null.
function separating(points, lab){
const STEPS = 180;
for(let a=0;a<STEPS;a++){
const th = Math.PI * a / STEPS;
const nx = Math.cos(th), ny = Math.sin(th); // line normal
// project points; try every threshold between consecutive projections, both polarities
const proj = points.map(([x,y]) => nx*x + ny*y);
const sorted = [...proj].sort((u,v)=>u-v);
for(let i=0;i<=sorted.length;i++){
const t = i===0 ? sorted[0]-1 : i===sorted.length ? sorted[sorted.length-1]+1
: (sorted[i-1]+sorted[i])/2;
for(const pol of [1,-1]){
let ok = true;
for(let k=0;k<points.length;k++){
const side = (proj[k] - t) * pol >= 0 ? 1 : 0; // 1 = red side
if(side !== lab[k]) { ok = false; break; }
}
if(ok) return { nx, ny, t, pol };
}
}
}
return null;
}
function draw(line){
const s = document.getElementById('plane'); s.innerHTML='';
if(line){
// draw the separating line: points where nx*x+ny*y = t, clipped to the box
const { nx, ny, t } = line; const seg = [];
const W=300,H=220;
const edges = [[0,0,W,0],[W,0,W,H],[W,H,0,H],[0,H,0,0]];
for(const [x1,y1,x2,y2] of edges){
const d1 = nx*x1+ny*y1 - t, d2 = nx*x2+ny*y2 - t;
if((d1<=0&&d2>=0)||(d1>=0&&d2<=0)){
const r = d1===d2 ? 0 : d1/(d1-d2);
seg.push([x1+(x2-x1)*r, y1+(y2-y1)*r]);
}
}
if(seg.length>=2){
const ln=document.createElementNS(SVG,'line');
ln.setAttribute('x1',seg[0][0]); ln.setAttribute('y1',seg[0][1]);
ln.setAttribute('x2',seg[1][0]); ln.setAttribute('y2',seg[1][1]);
ln.setAttribute('class','sep'); s.appendChild(ln);
}
}
pts.forEach((p,i)=>{
const c=document.createElementNS(SVG,'circle');
c.setAttribute('cx',p[0]); c.setAttribute('cy',p[1]); c.setAttribute('r',13);
c.setAttribute('class','dot '+(labels[i]?'red':'blue'));
c.addEventListener('click',()=>{ labels[i]^=1; update(); });
s.appendChild(c);
});
}
function setStatus(t,cls){ const el=document.getElementById('status'); el.textContent=t; el.className='status '+cls; }
function update(){
const line = separating(pts, labels);
if(line){ draw(line); setStatus('Separable — a line splits these colors.','ok'); }
else { draw(null); setStatus('Not separable — no line can split this coloring!','bad'); }
}
function testAll(){
let bad = 0;
for(let m=0;m<(1<<n);m++){
const lab = pts.map((_,i)=>(m>>i)&1);
if(!separating(pts, lab)) bad++;
}
if(bad===0) setStatus(fmt('All %v% colorings are separable — these %n% points are SHATTERED.', { v: 1<<n, n: n }),'ok');
else setStatus(fmt('%bad% of %v% colorings cannot be separated — these %n% points are NOT shattered.', { bad: bad, v: 1<<n, n: n }),'bad');
}
function setMode(k){
n=k; pts=SETS[k].map(p=>p.slice()); labels=pts.map(()=>0);
document.getElementById('mode3').className = k===3?'':'ghost';
document.getElementById('mode4').className = k===4?'':'ghost';
update();
}
document.getElementById('mode3').addEventListener('click',()=>setMode(3));
document.getElementById('mode4').addEventListener('click',()=>setMode(4));
document.getElementById('all').addEventListener('click',testAll);
update();
Comments
Loading comments...