Here’s what I implemented a long time ago:
<!DOCTYPE html>
<html>
<head>
<title>Venn Diagram of Common Glyphs in Greek, Latin, and Russian</title>
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<a
href="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e4/Venn_diagram_gr_la_ru.svg/830px-Venn_diagram_gr_la_ru.svg.png">Venn
Diagram of Greek, Latin, and Russian Glyphs</a>
<script src="https://cdn.jsdelivr.net/npm/gojs"></script>
<script src="https://cdn.jsdelivr.net/npm/create-gojs-kit/dist/extensions/Quadtree.js"></script>
<script src="https://cdn.jsdelivr.net/npm/create-gojs-kit/dist/extensions/PackedLayout.js"></script>
<script id="code">
class VennLayout extends go.Layout {
sets = null;
bins = null;
constructor(init) {
super();
if (init) Object.assign(this, init);
}
doLayout(coll) {
if (coll === this.diagram) {
coll = this.diagram.nodes;
} else if (coll instanceof go.Group) {
coll = coll.memberParts.filter(p => p instanceof go.Node);
} else {
coll = coll.filter(p => p instanceof go.Node);
}
var lay = this;
lay.sets = new go.Map();
// first generate an array of the keys that name the "Sets"
var setarr = [];
coll.each(n => {
if (n.category === "Set") {
var key = n.data.key;
lay.sets.add(key, { members: [], key: key, node: n });
setarr.push(key);
}
});
// build tree structure with all combinations
lay.bins = this.buildBin(setarr, 0, []);
// for each node that is not a "Set", add to bin corresponding to node.data.sets array of keys
coll.each(n => {
if (n.category === "Set") return;
n.data.sets.forEach(k => {
var setinfo = lay.sets.get(k);
if (setinfo) {
setinfo.members.push(n);
} else {
//??? throw new Error("unknown set: " + k + " for node " + n.data.key);
}
});
lay.walkBin(lay.bins,
intinfo => n.data.sets.indexOf(intinfo.key) >= 0,
intinfo => n.data.sets.indexOf(intinfo.key) < 0,
intinfo => intinfo.members.push(n));
});
// now report the totals
//var str = "";
//lay.sets.each(kvp => { str += "Set " + kvp.key + ": " + kvp.value.members.length + "\n"; });
//lay.walkBin(lay.bins, k => true, null,
// intinfo => { str += "Intersection " + intinfo.path.join(" ") + ": " + intinfo.members.length + "\n"; });
// compute how big each laid-out collection of nodes in each intersection is
var maxdia = 0.0;
lay.walkBin(lay.bins, null, null, intinfo => {
var packed = lay.layoutCollection(intinfo);
var bounds = lay.diagram.computePartsBounds(new go.List().addAll(intinfo.members));
intinfo.size = bounds.size;
intinfo.center = packed.actualBounds.center;
maxdia = Math.max(maxdia, Math.max(bounds.width, bounds.height));
//str += intinfo.path.join(",") + " " + intinfo.members.length + ": " + intinfo.size.toString() + " " + intinfo.center.toString() + "\n";
});
// position the "sets"
// their diameters should depend on the maximum size of the intersection collections after their layout
var i = 0;
var dia = maxdia * lay.sets.count;
lay.sets.iteratorValues.each(setinfo => {
if (i === 0) { setinfo.node.elt(0).desiredSize = new go.Size(dia, dia); setinfo.node.location = new go.Point(0, 0); i++; }
else if (i === 1) { setinfo.node.elt(0).desiredSize = new go.Size(dia, dia); setinfo.node.location = new go.Point(dia / 2, 0); i++; }
else if (i === 2) { setinfo.node.elt(0).desiredSize = new go.Size(dia, dia); setinfo.node.location = new go.Point(dia / 4, 0.86 * dia / 2); i++; }
else if (i === 3) { throw new Error("too many sets: " + i); }
});
// position the intersection collections
var center = lay.average(setarr);
//str = "";
lay.walkBin(lay.bins, null, null, intinfo => {
var avg = lay.average(intinfo.path);
//str += intinfo.path.join(",") + " " + avg.toString() + "\n";
var c = intinfo.center;
intinfo.members.forEach(n => {
var dst = new go.Point(center.x + 2 * (avg.x - center.x), center.y + 2 * (avg.y - center.y));
n.location = dst.offset(n.location.x - c.x, n.location.y - c.y);
});
});
};
buildBin(arr, idx, path) {
if (idx < arr.length) {
var k = arr[idx];
var t = { key: k };
t[false] = this.buildBin(arr, idx + 1, path);
path.push(k);
t[true] = this.buildBin(arr, idx + 1, path);
path.pop();
return t;
} else {
return {
members: [],
path: path.slice(),
size: new go.Size()
};
}
};
walkBin(bins, present, absent, leaf) {
if (Array.isArray(bins.members)) {
if (leaf) leaf(bins);
} else {
var k = bins.key;
if (!present || present(bins)) {
this.walkBin(bins[true], present, absent, leaf);
}
if (!absent || absent(bins)) {
this.walkBin(bins[false], present, absent, leaf);
}
}
};
layoutCollection(intinfo) {
var arr = intinfo.members;
var coll = new go.List().addAll(arr);
var layout = new PackedLayout();
layout.doLayout(coll);
return layout;
};
average(keys) {
var len = keys.length;
var x = 0.0;
var y = 0.0;
for (var i = 0; i < len; i++) {
var key = keys[i];
var node = this.sets.get(key).node;
if (i === 0) {
x = node.location.x;
y = node.location.y;
} else {
x += node.location.x;
y += node.location.y;
}
}
if (len > 0) {
x = x / len;
y = y / len;
}
return new go.Point(x, y);
}
}
// end VennLayout
myDiagram =
new go.Diagram("myDiagramDiv", {
"animationManager.isEnabled": false,
layout: new VennLayout()
});
myDiagram.nodeTemplateMap.add("Set",
new go.Node({ locationSpot: go.Spot.Center, layerName: "Background", selectable: false })
.add(
new go.Shape("Circle", { fill: "transparent" })
));
myDiagram.nodeTemplate =
new go.Node({ locationSpot: go.Spot.Center })
.add(
new go.TextBlock()
.bind("text")
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: "G", text: "Greek", category: "Set" },
{ key: "L", text: "Latin", category: "Set" },
{ key: "R", text: "Russian", category: "Set" },
{ text: "A", sets: ["G", "L", "R"] },
{ text: "B", sets: ["G", "L", "R"] },
{ text: "C", sets: ["L", "R"] },
{ text: "D", sets: ["L"] },
{ text: "E", sets: ["G", "L", "R"] },
{ text: "F", sets: ["L"] },
{ text: "G", sets: ["L"] },
{ text: "H", sets: ["G", "L", "R"] },
{ text: "I", sets: ["G", "L"] },
{ text: "J", sets: ["L"] },
{ text: "K", sets: ["G", "L", "R"] },
{ text: "L", sets: ["L"] },
{ text: "M", sets: ["G", "L", "R"] },
{ text: "N", sets: ["G", "L"] },
{ text: "O", sets: ["G", "L", "R"] },
{ text: "P", sets: ["G", "L", "R"] },
{ text: "Q", sets: ["L"] },
{ text: "R", sets: ["L"] },
{ text: "S", sets: ["L"] },
{ text: "T", sets: ["G", "L", "R"] },
{ text: "U", sets: ["L"] },
{ text: "V", sets: ["L"] },
{ text: "W", sets: ["L"] },
{ text: "X", sets: ["G", "L", "R"] },
{ text: "Y", sets: ["G", "L", "R"] },
{ text: "Z", sets: ["G", "L"] },
{ text: "Γ", sets: ["G", "R"] },
{ text: "Δ", sets: ["G"] },
{ text: "Θ", sets: ["G"] },
{ text: "Λ", sets: ["G"] },
{ text: "Ξ", sets: ["G"] },
{ text: "Π", sets: ["G", "R"] },
{ text: "Σ", sets: ["G"] },
{ text: "Φ", sets: ["G", "R"] },
{ text: "Ψ", sets: ["G"] },
{ text: "Ω", sets: ["G"] },
{ text: "Б", sets: ["R"] },
{ text: "Д", sets: ["R"] },
{ text: "Ж", sets: ["R"] },
{ text: "З", sets: ["R"] },
{ text: "И", sets: ["R"] },
{ text: "Л", sets: ["R"] },
{ text: "Ц", sets: ["R"] },
{ text: "Ч", sets: ["R"] },
{ text: "Ш", sets: ["R"] },
{ text: "Щ", sets: ["R"] },
{ text: "Ъ", sets: ["R"] },
{ text: "Ы", sets: ["R"] },
{ text: "Ь", sets: ["R"] },
{ text: "Э", sets: ["R"] },
{ text: "Ю", sets: ["R"] },
{ text: "Я", sets: ["R"] }
]);
</script>
</body>
</html>