Approach for Interactive Venn Diagram

Hi,

I have a question about implementing an interactive venn diagram in GoJS. Before going too far down one approach, I wanted to ask here for some guidance. The screenshot of the UX is below.

The user will be able to toggle on and off each of the 3 portions of the diagram. The combination of the toggle states determines the join type. My gut instinct is that this is a combination of 3 separate svgs shapes, each with their own ability to act on. But not sure about aligning the 3 to form a single shape. I feel like I could do it with a bunch of tweaking of static values but not sure if there is a more automatic way.

Any examples or tips would be much appreciated. Thanks

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>

Thank you very much @walter!

Will use your snippet as a starting point