Below is a graph with its vertices scattered around. Some edges cross. Drag the dots and try to arrange them so that no two edges cross — that proves the graph is planar.
<p class="hint">Drag the dots to remove every edge crossing. A graph is <b>planar</b> if it can be drawn flat with no crossings. Some graphs (K₅ and K₃₃) can <b>never</b> be untangled — press <b>Test planarity</b> to find out instantly.</p>
<div class="picker">
<button data-g="cycle" class="on" type="button">Cycle</button>
<button data-g="cube" type="button">Cube</button>
<button data-g="k5" type="button">K₅</button>
<button data-g="k33" type="button">K₃₃</button>
</div>
<svg id="canvas" viewBox="0 0 320 240" aria-label="graph"></svg>
<div class="status" id="status">Drag the dots to untangle.</div>
<div class="btns">
<button id="test" type="button">Test planarity</button>
<button id="shuffle" type="button" class="ghost">Shuffle</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; }
.picker { display: flex; gap: .4rem; flex-wrap: wrap; margin: 0 0 .5rem; }
.picker button { font: 600 13px system-ui, sans-serif; padding: .35rem .7rem; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; border-radius: 7px; cursor: pointer; }
.picker button.on { background: #1d3557; color: #fff; border-color: #1d3557; }
#canvas { width: 100%; max-width: 320px; height: 240px; background: #f5f8fa; border: 1px solid #cdd9e3;
border-radius: 10px; touch-action: none; display: block; }
.edge { stroke: #8aa0b4; stroke-width: 2; }
.edge.cross { stroke: #e63946; stroke-width: 2.6; }
.node { fill: #1d3557; stroke: #fff; stroke-width: 2; cursor: grab; }
.node:active { cursor: grabbing; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
.btns button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.btns button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Planarity testing demo. The user drags vertices to remove crossings.
// Crossings are detected with a segment-intersection test (live, while dragging).
// "Test planarity" answers the real question: is the graph planar AT ALL?
// For these small named graphs the answer is known from Kuratowski's theorem:
// a graph is non-planar iff it contains K5 or K3,3. Here the planar examples
// (cycle, cube) are planar; k5 and k33 are exactly the two forbidden graphs.
const svg = document.getElementById('canvas');
const NS = 'http://www.w3.org/2000/svg';
const W = 320, H = 240;
// Each graph: list of edges [a,b]; planar flag (the true answer).
const GRAPHS = {
cycle: { n: 6, edges: [[0,1],[1,2],[2,3],[3,4],[4,5],[5,0]], planar: true },
cube: { n: 8, edges: [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],
[0,4],[1,5],[2,6],[3,7]], planar: true },
k5: { n: 5, edges: [[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]], planar: false },
k33: { n: 6, edges: [[0,3],[0,4],[0,5],[1,3],[1,4],[1,5],[2,3],[2,4],[2,5]], planar: false }
};
let key = 'cycle';
let pts = []; // {x,y} per node
let drag = null;
function randomLayout(n){
const p = [];
for(let i=0;i<n;i++) p.push({ x: 40 + Math.random()*(W-80), y: 30 + Math.random()*(H-60) });
return p;
}
function load(k){
key = k; pts = randomLayout(GRAPHS[k].n);
document.querySelectorAll('.picker button').forEach(b=>b.classList.toggle('on', b.dataset.g===k));
setStatus('Drag the dots to untangle.','');
render();
}
// Do segments AB and CD properly cross (excluding shared endpoints)?
function orient(a,b,c){ return Math.sign((b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x)); }
function segCross(a,b,c,d){
const o1=orient(a,b,c), o2=orient(a,b,d), o3=orient(c,d,a), o4=orient(c,d,b);
return o1!==o2 && o3!==o4 && o1!==0 && o2!==0 && o3!==0 && o4!==0;
}
// Count crossings in the CURRENT drawing (only non-adjacent edge pairs).
function countCrossings(){
const E = GRAPHS[key].edges; const set = new Set(); let total = 0;
for(let i=0;i<E.length;i++) for(let j=i+1;j<E.length;j++){
const [a,b]=E[i], [c,d]=E[j];
if(a===c||a===d||b===c||b===d) continue; // share a vertex: never count
if(segCross(pts[a],pts[b],pts[c],pts[d])){ total++; set.add(i); set.add(j); }
}
return { total, set };
}
function render(){
const { set } = countCrossings();
svg.innerHTML = '';
const E = GRAPHS[key].edges;
E.forEach(([a,b],i)=>{
const ln = document.createElementNS(NS,'line');
ln.setAttribute('x1',pts[a].x); ln.setAttribute('y1',pts[a].y);
ln.setAttribute('x2',pts[b].x); ln.setAttribute('y2',pts[b].y);
ln.setAttribute('class','edge'+(set.has(i)?' cross':''));
svg.appendChild(ln);
});
pts.forEach((p,i)=>{
const c = document.createElementNS(NS,'circle');
c.setAttribute('cx',p.x); c.setAttribute('cy',p.y); c.setAttribute('r',9);
c.setAttribute('class','node'); c.dataset.i = i;
svg.appendChild(c);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function toLocal(ev){
const r = svg.getBoundingClientRect();
const t = ev.touches ? ev.touches[0] : ev;
return { x:(t.clientX-r.left)/r.width*W, y:(t.clientY-r.top)/r.height*H };
}
function start(ev){ const t=ev.target; if(t.classList.contains('node')){ drag=+t.dataset.i; ev.preventDefault(); } }
function move(ev){
if(drag===null) return; const p=toLocal(ev);
pts[drag].x=Math.max(12,Math.min(W-12,p.x)); pts[drag].y=Math.max(12,Math.min(H-12,p.y));
render(); ev.preventDefault();
const c=countCrossings().total;
if(c===0) setStatus('No crossings in this drawing!','ok');
else setStatus(fmt('%c% crossing%v% left.', { c: c, v: c>1?'s':'' }),'');
}
function end(){ drag=null; }
svg.addEventListener('mousedown',start); window.addEventListener('mousemove',move); window.addEventListener('mouseup',end);
svg.addEventListener('touchstart',start,{passive:false}); window.addEventListener('touchmove',move,{passive:false}); window.addEventListener('touchend',end);
document.querySelectorAll('.picker button').forEach(b=>b.addEventListener('click',()=>load(b.dataset.g)));
document.getElementById('shuffle').addEventListener('click',()=>{ pts=randomLayout(GRAPHS[key].n); render(); setStatus('Drag the dots to untangle.',''); });
document.getElementById('test').addEventListener('click',()=>{
const planar = GRAPHS[key].planar;
if(planar){
const c = countCrossings().total;
if(c===0) setStatus('Planar — and your drawing already has no crossings!','ok');
else setStatus('Planar: a crossing-free drawing exists. Keep dragging to find it.','ok');
} else {
setStatus(fmt('Non-planar: this graph contains %v% — no drawing is ever crossing-free.', { v: key==='k5'?'K₅':'K₃₃' }),'bad');
}
console.log('graph',key,'planar?',planar,'| current crossings',countCrossings().total);
});
load('cycle');
<p class="hint">Drag the dots to remove every edge crossing. A graph is <b>planar</b> if it can be drawn flat with no crossings. Some graphs (K₅ and K₃₃) can <b>never</b> be untangled — press <b>Test planarity</b> to find out instantly.</p>
<div class="picker">
<button data-g="cycle" class="on" type="button">Cycle</button>
<button data-g="cube" type="button">Cube</button>
<button data-g="k5" type="button">K₅</button>
<button data-g="k33" type="button">K₃₃</button>
</div>
<svg id="canvas" viewBox="0 0 320 240" aria-label="graph"></svg>
<div class="status" id="status">Drag the dots to untangle.</div>
<div class="btns">
<button id="test" type="button">Test planarity</button>
<button id="shuffle" type="button" class="ghost">Shuffle</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; }
.picker { display: flex; gap: .4rem; flex-wrap: wrap; margin: 0 0 .5rem; }
.picker button { font: 600 13px system-ui, sans-serif; padding: .35rem .7rem; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; border-radius: 7px; cursor: pointer; }
.picker button.on { background: #1d3557; color: #fff; border-color: #1d3557; }
#canvas { width: 100%; max-width: 320px; height: 240px; background: #f5f8fa; border: 1px solid #cdd9e3;
border-radius: 10px; touch-action: none; display: block; }
.edge { stroke: #8aa0b4; stroke-width: 2; }
.edge.cross { stroke: #e63946; stroke-width: 2.6; }
.node { fill: #1d3557; stroke: #fff; stroke-width: 2; cursor: grab; }
.node:active { cursor: grabbing; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
.btns button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.btns button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Planarity testing demo. The user drags vertices to remove crossings.
// Crossings are detected with a segment-intersection test (live, while dragging).
// "Test planarity" answers the real question: is the graph planar AT ALL?
// For these small named graphs the answer is known from Kuratowski's theorem:
// a graph is non-planar iff it contains K5 or K3,3. Here the planar examples
// (cycle, cube) are planar; k5 and k33 are exactly the two forbidden graphs.
const svg = document.getElementById('canvas');
const NS = 'http://www.w3.org/2000/svg';
const W = 320, H = 240;
// Each graph: list of edges [a,b]; planar flag (the true answer).
const GRAPHS = {
cycle: { n: 6, edges: [[0,1],[1,2],[2,3],[3,4],[4,5],[5,0]], planar: true },
cube: { n: 8, edges: [[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],
[0,4],[1,5],[2,6],[3,7]], planar: true },
k5: { n: 5, edges: [[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]], planar: false },
k33: { n: 6, edges: [[0,3],[0,4],[0,5],[1,3],[1,4],[1,5],[2,3],[2,4],[2,5]], planar: false }
};
let key = 'cycle';
let pts = []; // {x,y} per node
let drag = null;
function randomLayout(n){
const p = [];
for(let i=0;i<n;i++) p.push({ x: 40 + Math.random()*(W-80), y: 30 + Math.random()*(H-60) });
return p;
}
function load(k){
key = k; pts = randomLayout(GRAPHS[k].n);
document.querySelectorAll('.picker button').forEach(b=>b.classList.toggle('on', b.dataset.g===k));
setStatus('Drag the dots to untangle.','');
render();
}
// Do segments AB and CD properly cross (excluding shared endpoints)?
function orient(a,b,c){ return Math.sign((b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x)); }
function segCross(a,b,c,d){
const o1=orient(a,b,c), o2=orient(a,b,d), o3=orient(c,d,a), o4=orient(c,d,b);
return o1!==o2 && o3!==o4 && o1!==0 && o2!==0 && o3!==0 && o4!==0;
}
// Count crossings in the CURRENT drawing (only non-adjacent edge pairs).
function countCrossings(){
const E = GRAPHS[key].edges; const set = new Set(); let total = 0;
for(let i=0;i<E.length;i++) for(let j=i+1;j<E.length;j++){
const [a,b]=E[i], [c,d]=E[j];
if(a===c||a===d||b===c||b===d) continue; // share a vertex: never count
if(segCross(pts[a],pts[b],pts[c],pts[d])){ total++; set.add(i); set.add(j); }
}
return { total, set };
}
function render(){
const { set } = countCrossings();
svg.innerHTML = '';
const E = GRAPHS[key].edges;
E.forEach(([a,b],i)=>{
const ln = document.createElementNS(NS,'line');
ln.setAttribute('x1',pts[a].x); ln.setAttribute('y1',pts[a].y);
ln.setAttribute('x2',pts[b].x); ln.setAttribute('y2',pts[b].y);
ln.setAttribute('class','edge'+(set.has(i)?' cross':''));
svg.appendChild(ln);
});
pts.forEach((p,i)=>{
const c = document.createElementNS(NS,'circle');
c.setAttribute('cx',p.x); c.setAttribute('cy',p.y); c.setAttribute('r',9);
c.setAttribute('class','node'); c.dataset.i = i;
svg.appendChild(c);
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function toLocal(ev){
const r = svg.getBoundingClientRect();
const t = ev.touches ? ev.touches[0] : ev;
return { x:(t.clientX-r.left)/r.width*W, y:(t.clientY-r.top)/r.height*H };
}
function start(ev){ const t=ev.target; if(t.classList.contains('node')){ drag=+t.dataset.i; ev.preventDefault(); } }
function move(ev){
if(drag===null) return; const p=toLocal(ev);
pts[drag].x=Math.max(12,Math.min(W-12,p.x)); pts[drag].y=Math.max(12,Math.min(H-12,p.y));
render(); ev.preventDefault();
const c=countCrossings().total;
if(c===0) setStatus('No crossings in this drawing!','ok');
else setStatus(fmt('%c% crossing%v% left.', { c: c, v: c>1?'s':'' }),'');
}
function end(){ drag=null; }
svg.addEventListener('mousedown',start); window.addEventListener('mousemove',move); window.addEventListener('mouseup',end);
svg.addEventListener('touchstart',start,{passive:false}); window.addEventListener('touchmove',move,{passive:false}); window.addEventListener('touchend',end);
document.querySelectorAll('.picker button').forEach(b=>b.addEventListener('click',()=>load(b.dataset.g)));
document.getElementById('shuffle').addEventListener('click',()=>{ pts=randomLayout(GRAPHS[key].n); render(); setStatus('Drag the dots to untangle.',''); });
document.getElementById('test').addEventListener('click',()=>{
const planar = GRAPHS[key].planar;
if(planar){
const c = countCrossings().total;
if(c===0) setStatus('Planar — and your drawing already has no crossings!','ok');
else setStatus('Planar: a crossing-free drawing exists. Keep dragging to find it.','ok');
} else {
setStatus(fmt('Non-planar: this graph contains %v% — no drawing is ever crossing-free.', { v: key==='k5'?'K₅':'K₃₃' }),'bad');
}
console.log('graph',key,'planar?',planar,'| current crossings',countCrossings().total);
});
load('cycle');
Switch between the examples. The cycle and the cube can always be untangled if you place the points well. But K5 (five dots, all connected) and K3,₃ (the three-houses puzzle) can never be drawn flat, no matter how you drag — they are the two forbidden shapes at the heart of the theory. The Test planarity button runs the real decision in an instant: it never needs to guess your dragging, it just answers yes or no.
Comments
Loading comments...