Here’s a VennLayout that supports two or three "Set"s. You can customize how each intersection collection is laid out by setting VennLayout.intersectionLayout – the default is CircularLayout, but you could use PackedLayout as I did originally in this post.
<!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>
<br>
<button onclick="lgr()">Greek, Latin, and Russian</button>
<button onclick="lg()">Latin and Greek</button>
<button onclick="gr()">Greek and Russian</button>
<button onclick="lr()">Latin and Russian</button>
<br>
<button onclick="circular()">Circular Layout</button>
<button onclick="packed()">Packed Layout</button>
<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">
// This Venn-style layout handles two or three sets of nodes, where each node.data object
// has a "sets" property that is an Array listing which "Set"s the node belongs in.
// The model should also hold a category: "Set" node data object for each collection
// that is represented by a circular Shape.
// To arrange the collection of nodes in each "Set" intersection, you can either use
// the default CircularLayout or you can use a different one such as PackedLayout.
class VennLayout extends go.Layout {
_sets = null;
_bins = null;
_intersectionLayout = new go.CircularLayout({ arrangement: go.CircularArrangement.Packed, spacing: 0 });
_spacing = 0;
constructor(init) {
super();
if (init) Object.assign(this, init);
}
// Gets or sets the Layout to use for each collection of nodes.
// This defaults to CircularLayout.
get intersectionLayout() { return this._intersectionLayout; }
set intersectionLayout(lay) {
if (lay !== this._intersectionLayout) {
this._intersectionLayout = lay;
this.invalidateLayout();
}
}
// Gets or sets the minimum distance between intersection collections.
// This may want to be larger than zero when the intersections are laid out in a rectangular fashion,
// since the circles of the "Set"s will tend to intersect with the corners of rectangles.
// This defaults to 0.
get spacing() { return this._spacing; }
set spacing(sp) {
if (sp !== this._spacing) {
this._spacing = sp;
this.invalidateLayout();
}
}
doLayout(coll) {
if (coll === this.diagram) {
coll = this.diagram.nodes.filter(n => n.isVisible());
} else if (coll instanceof go.Group) {
coll = coll.memberParts.filter(p => p.isVisible() && !(p instanceof go.Link));
} else {
coll = coll.filter(p => p.isVisible() && !(p instanceof go.Link));
}
this._sets = new go.Map();
// first generate an array of the keys that name the "Sets"
const setarr = [];
coll.each(n => {
if (n.category === "Set") {
const key = n.data.key;
this._sets.add(key, { members: [], key: key, node: n });
setarr.push(key);
}
});
// build tree structure with all combinations
this._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 => {
const setinfo = this._sets.get(k);
if (setinfo) {
setinfo.members.push(n);
} else {
// be tolerant of superfluous Set membership information -- ignore it
// throw new Error("unknown set: " + k + " for node " + n.data.key);
}
});
this._walkBin(this._bins,
intinfo => n.data.sets.indexOf(intinfo.key) >= 0,
intinfo => n.data.sets.indexOf(intinfo.key) < 0,
intinfo => intinfo.members.push(n));
});
// compute how big each laid-out collection of nodes in each intersection is
let maxdia = 0.0;
this._walkBin(this._bins, null, null, intinfo => {
this._layoutIntersection(intinfo);
const bounds = this.diagram.computePartsBounds(intinfo.members);
intinfo.center = bounds.center;
maxdia = Math.max(maxdia, Math.max(bounds.width + this.spacing/2, bounds.height + this.spacing/2));
});
// position the "sets"
// their diameters should depend on the maximum size of the intersection collections after their layout
let i = 1;
const dia = maxdia * this._sets.count;
this._sets.iteratorValues.each(setinfo => {
const shp = setinfo.node.findObject("SHAPE");
const lab = setinfo.node.findObject("LABEL");
if (i === 1) {
if (shp) shp.desiredSize = new go.Size(dia, dia);
if (lab) { lab.alignment = new go.Spot(0.15, 0.15); lab.alignmentFocus = go.Spot.BottomRight; }
setinfo.node.location = new go.Point(0, 0); i++;
} else if (i === 2) {
if (shp) shp.desiredSize = new go.Size(dia, dia);
if (lab) { lab.alignment = new go.Spot(0.85, 0.15); lab.alignmentFocus = go.Spot.BottomLeft; }
setinfo.node.location = new go.Point(dia / 2, 0); i++;
} else if (i === 3) {
if (shp) shp.desiredSize = new go.Size(dia, dia);
if (lab) { lab.alignment = go.Spot.Bottom; lab.alignmentFocus = go.Spot.Top; }
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
const center = this._average(setarr);
this._walkBin(this._bins, null, null, intinfo => {
const avg = this._average(intinfo.path);
const c = intinfo.center;
intinfo.members.forEach(n => {
const dst = new go.Point(center.x + 2 * (avg.x - center.x), center.y + 2 * (avg.y - center.y));
n.moveTo(dst.x + n.location.x - c.x, dst.y + n.location.y - c.y, true);
});
});
};
_buildBin(arr, idx, path) {
if (idx < arr.length) {
const k = arr[idx];
const 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 {
const 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);
}
}
};
_layoutIntersection(intinfo) {
const arr = intinfo.members;
const coll = new go.List().addAll(arr);
this._intersectionLayout.doLayout(coll);
};
_average(keys) {
const len = keys.length;
let x = 0.0;
let y = 0.0;
for (let i = 0; i < len; i++) {
const key = keys[i];
const 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("Spot", {
locationSpot: go.Spot.Center, locationObjectName: "SHAPE",
layerName: "Background",
selectable: false
})
.add(
new go.Shape("Circle", { name: "SHAPE", fill: "transparent" }),
new go.TextBlock({ name: "LABEL", font: "bold 12pt sans-serif" })
.bind("text")
));
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"] }
]);
function lgr() { // make all Nodes visible
myDiagram.commit(d => {
d.nodes.each(n => n.visible = true);
});
}
function setVisibleIf(a, b) {
myDiagram.commit(d => {
d.nodes.each(n => {
if (n.category === "Set") {
n.visible = n.key === a || n.key === b;
} else {
n.visible = n.data.sets.indexOf(a) >= 0 || n.data.sets.indexOf(b) >= 0;
}
});
});
}
function lg() { setVisibleIf("L", "G"); }
function gr() { setVisibleIf("G", "R"); }
function lr() { setVisibleIf("L", "R"); }
function circular() {
myDiagram.commit(d => {
d.layout.intersectionLayout = new go.CircularLayout({ arrangement: go.CircularArrangement.Packed, spacing: 0 })
d.layout.spacing = 0;
});
}
function packed() {
myDiagram.commit(d => {
d.layout.intersectionLayout = new PackedLayout({ spacing: 0 });
d.layout.spacing = 20;
});
}
</script>
</body>
</html>