Identical twins are positioned on the last generation in Genogram

I’m using genogram layout and the following code to setup identical twins

const T1 = diagram.findNodeForKey(key1);
const T2 = diagram.findNodeForKey(key2);

const TPL1 = T1.findTreeParentLink();
const TPL2 = T2.findTreeParentLink();

const TLN1 = $(go.Node, {
  isLayoutPositioned: true, // setting this to false is making TL disappear
});
TLN1.labeledLink = TPL1;
TLN1.segmentIndex = -2;
TLN1.segmentFraction = 0.5;
diagram.add(TLN1);

const TLN2 = $(go.Node, {
  isLayoutPositioned: true, // setting this to false is making TL disappear
});
TLN2.labeledLink = TPL2;
TLN2.segmentIndex = -2;
TLN2.segmentFraction = 0.5;
diagram.add(TLN2);

const TL = $(
  go.Link,
  {
    isLayoutPositioned: false,
    isTreeLink: false,
  },
  $(go.Shape, {
    strokeWidth: 2,
    stroke: "blue",
  })
);
TL.fromNode = TLN1;
TL.toNode = TLN2;
diagram.add(TL);

The issue I’m facing is that identical twins (Twin A and Twin B) are always positioned on the last generation. I tried setting isLayoutPositioned to false on twin link nodes (TLN1, TLN2) but that just made twin link (TL) disappear. Is there any way I could position them on their respective generation?

My guess is that you are unintentionally setting up what the GenogramLayout is treating as a “parent” vertex, thereby resulting in the extra generation or layer.

I think what you are doing is different from what the “setup…” functions do in order to convert the given data into a GraphLinksModel that supports GenogramLayout and features such as specialized links for identical twins. Note the differences between what your code is doing and what the setupIdenticalTwins function does.

You don’t have any problems with regular twins, do you?

I don’t have any problems with regular twins (see Bill, Claire and Carol).

I’m not sure where can I find the setupIdenticalTwins function you are refering to. The one I used is from this topic which is dealing with the same issue.

I see now, you were refering to Genogram twins example. The only difference I see is that I’m adding twin link nodes for anchoring the twin link in the center of the twin-parent links. Is there a way to anchor it without using twin link nodes or a way to make these nodes not to act like parent vertex?

In this design, identical twins are (indirectly) connected with a “TwinLink” category Link. I started explaining it but then realized that your app is using code that is different from what is used in that Genogram Twins sample.

In the unsupported sample that we created long ago, the blue link connecting twins connects at the top of the node. In your app, the link appears to connect the middle of the last segment. Or perhaps slightly higher than middle. So I’m not sure what the best advice should be.

Does it mean that currently the only solution to this issue is to stick with Genogram Twins sample?

I’m confident that a solution can be found with your current environment. But I don’t know what you have.

I’ve setup a codepen reproducing the issue: https://codepen.io/mcerven/pen/GRdoKxz
Any help is appreciated.

Thanks for the CodePen. The problem is that adding label nodes to parent-child links caused confusion in both setup and GenogramLayout, since it violates the previous assumption that any link with a label node would be a “Marriage” link.

Here’s an updated sample. Note that I have renamed the custom Link class and the additional link template different names, since having both be named “TwinLink” was very confusing.

<!DOCTYPE html>
<html>
<head>
  <title>Genogram</title>
  <!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <script src="go.js"></script>
  <script id="code">
    function init() {
      var $ = go.GraphObject.make;
      myDiagram =
        $(go.Diagram, "myDiagramDiv",
          {
            initialAutoScale: go.Diagram.Uniform,
            // when a node is selected, draw a big yellow circle behind it
            nodeSelectionAdornmentTemplate:
              $(go.Adornment, "Auto",
                { layerName: "Grid" },  // the predefined layer that is behind everything else
                $(go.Shape, "Circle", { fill: "yellow", stroke: null }),
                $(go.Placeholder)
              ),
            layout:  // use a custom layout, defined below
              $(GenogramLayout, { direction: 90, layerSpacing: 30, columnSpacing: 10 })
          });

      // determine the color for each attribute shape
      function attrFill(a) {
        switch (a) {
          case "A": return "green";
          case "B": return "orange";
          case "C": return "red";
          case "D": return "cyan";
          case "E": return "gold";
          case "F": return "pink";
          case "G": return "blue";
          case "H": return "brown";
          case "I": return "purple";
          case "J": return "chartreuse";
          case "K": return "lightgray";
          case "L": return "magenta";
          case "S": return "red";
          default: return "transparent";
        }
      }

      // determine the geometry for each attribute shape in a male;
      // except for the slash these are all squares at each of the four corners of the overall square
      var tlsq = go.Geometry.parse("F M1 1 l19 0 0 19 -19 0z");
      var trsq = go.Geometry.parse("F M20 1 l19 0 0 19 -19 0z");
      var brsq = go.Geometry.parse("F M20 20 l19 0 0 19 -19 0z");
      var blsq = go.Geometry.parse("F M1 20 l19 0 0 19 -19 0z");
      var slash = go.Geometry.parse("F M38 0 L40 0 40 2 2 40 0 40 0 38z");
      function maleGeometry(a) {
        switch (a) {
          case "A": return tlsq;
          case "B": return tlsq;
          case "C": return tlsq;
          case "D": return trsq;
          case "E": return trsq;
          case "F": return trsq;
          case "G": return brsq;
          case "H": return brsq;
          case "I": return brsq;
          case "J": return blsq;
          case "K": return blsq;
          case "L": return blsq;
          case "S": return slash;
          default: return tlsq;
        }
      }

      // determine the geometry for each attribute shape in a female;
      // except for the slash these are all pie shapes at each of the four quadrants of the overall circle
      var tlarc = go.Geometry.parse("F M20 20 B 180 90 20 20 19 19 z");
      var trarc = go.Geometry.parse("F M20 20 B 270 90 20 20 19 19 z");
      var brarc = go.Geometry.parse("F M20 20 B 0 90 20 20 19 19 z");
      var blarc = go.Geometry.parse("F M20 20 B 90 90 20 20 19 19 z");
      function femaleGeometry(a) {
        switch (a) {
          case "A": return tlarc;
          case "B": return tlarc;
          case "C": return tlarc;
          case "D": return trarc;
          case "E": return trarc;
          case "F": return trarc;
          case "G": return brarc;
          case "H": return brarc;
          case "I": return brarc;
          case "J": return blarc;
          case "K": return blarc;
          case "L": return blarc;
          case "S": return slash;
          default: return tlarc;
        }
      }


      // two different node templates, one for each sex,
      // named by the category value in the node data object
      myDiagram.nodeTemplateMap.add("M",  // male
        $(go.Node, "Vertical",
          { locationSpot: go.Spot.Center, locationObjectName: "ICON" },
          $(go.Panel,
            { name: "ICON" },
            $(go.Shape, "Square",
              { width: 40, height: 40, strokeWidth: 2, fill: "white", portId: "" }),
            $(go.Panel,
              { // for each attribute show a Shape at a particular place in the overall square
                itemTemplate:
                  $(go.Panel,
                    $(go.Shape,
                      { stroke: null, strokeWidth: 0 },
                      new go.Binding("fill", "", attrFill),
                      new go.Binding("geometry", "", maleGeometry))
                  ),
                margin: 1
              },
              new go.Binding("itemArray", "a")
            )
          ),
          $(go.TextBlock,
            { textAlign: "center", maxSize: new go.Size(80, NaN) },
            new go.Binding("text", "n"))
        ));

      myDiagram.nodeTemplateMap.add("F",  // female
        $(go.Node, "Vertical",
          { locationSpot: go.Spot.Center, locationObjectName: "ICON" },
          $(go.Panel,
            { name: "ICON" },
            $(go.Shape, "Circle",
              { width: 40, height: 40, strokeWidth: 2, fill: "white", portId: "" }),
            $(go.Panel,
              { // for each attribute show a Shape at a particular place in the overall circle
                itemTemplate:
                  $(go.Panel,
                    $(go.Shape,
                      { stroke: null, strokeWidth: 0 },
                      new go.Binding("fill", "", attrFill),
                      new go.Binding("geometry", "", femaleGeometry))
                  ),
                margin: 1
              },
              new go.Binding("itemArray", "a")
            )
          ),
          $(go.TextBlock,
            { textAlign: "center", maxSize: new go.Size(80, NaN) },
            new go.Binding("text", "n"))
        ));

      // the representation of each label node -- nothing shows on a Marriage Link
      myDiagram.nodeTemplateMap.add("LinkLabel",
        $(go.Node, { selectable: false, width: 1, height: 1, fromEndSegmentLength: 20 }));


      myDiagram.linkTemplate =  // for parent-child relationships
        $(ParentChildLink,  // custom routing for same birth siblings
          {
            routing: go.Link.Orthogonal, curviness: 10,
            layerName: "Background", selectable: false,
            fromSpot: go.Spot.Bottom, toSpot: go.Spot.Top
          },
          $(go.Shape, { strokeWidth: 2 })
        );

      myDiagram.linkTemplateMap.add("Marriage",  // for marriage relationships
        $(go.Link, { selectable: false, isTreeLink: false },
          $(go.Shape, { strokeWidth: 2, stroke: "darkgreen" })
      ));

      myDiagram.linkTemplateMap.add("Identical",  // for connecting twins/triplets
        $(go.Link, 
          { selectable: false, isTreeLink: false, isLayoutPositioned: false },
          $(go.Shape, { strokeWidth: 2, stroke: "blue" })
      ));


      // n: name, s: sex, m: mother, f: father, ux: wife, vir: husband, a: attributes/markers
      setupDiagram(myDiagram, [
          { key: 0, n: "Aaron", s: "M", m:-10, f:-11, ux: 1, a: ["C", "F", "K"] },
          { key: 1, n: "Alice", s: "F", m:-12, f:-13, a: ["B", "H", "K"] },
          { key: 2, n: "Bob", s: "M", m: 1, f: 0, ux: 3, a: ["C", "H", "L"] },
          { key: 3, n: "Barbara", s: "F", a: ["C"] },
          { key: 4, n: "Bill", s: "M", m: 1, f: 0, ux: 5, a: ["E", "H"], birth: 1 },
          { key: 5, n: "Brooke", s: "F", a: ["B", "H", "L"] },
          { key: 6, n: "Claire", s: "F", m: 1, f: 0, a: ["C"], birth: 1, identical: 7 },
          { key: 7, n: "Carol", s: "F", m: 1, f: 0, a: ["C", "I"], birth: 1 },
          { key: 72, n: "Tweedledee", s: "M", m: 1, f: 0, a: ["C", "I"], birth: 2 },
          { key: 73, n: "Tweedledum", s: "M", m: 1, f: 0, a: ["C", "I"], birth: 2 },
          { key: 8, n: "Chloe", s: "F", m: 1, f: 0, vir: 9, a: ["E"] },
          { key: 9, n: "Chris", s: "M", a: ["B", "H"] },
          { key: 10, n: "Ellie", s: "F", m: 3, f: 2, a: ["E", "G"] },
          { key: 11, n: "Dan", s: "M", m: 3, f: 2, a: ["B", "J"] },
          { key: 12, n: "Elizabeth", s: "F", vir: 13, a: ["J"] },
          { key: 13, n: "David", s: "M", m: 5, f: 4, a: ["B", "H"] },
          { key: 14, n: "Emma", s: "F", m: 5, f: 4, a: ["E", "G"] },
          { key: 15, n: "Evan", s: "M", m: 8, f: 9, a: ["F", "H"] },
          { key: 16, n: "Ethan", s: "M", m: 8, f: 9, a: ["D", "K", "S"] },
          { key: 17, n: "Eve", s: "F", vir: 16, a: ["B", "F", "L", "S"] },
          { key: 18, n: "Emily", s: "F", m: 8, f: 9 },
          { key: 19, n: "Fred", s: "M", m: 17, f: 16, a: ["B"] },
          { key: 20, n: "Faith", s: "F", m: 17, f: 16, a: ["L"] },
          { key: 21, n: "Felicia", s: "F", m: 12, f: 13, a: ["H"] },
          { key: 22, n: "Frank", s: "M", m: 12, f: 13, a: ["B", "H"] },

          // "Aaron"'s ancestors
          { key: -10, n: "Paternal Grandfather", s: "M", m: -33, f: -32, ux: -11, a: ["A"] },
          { key: -11, n: "Paternal Grandmother", s: "F", a: ["E"] },
          { key: -32, n: "Paternal Great", s: "M", ux: -33, a: ["F", "H"] },
          { key: -33, n: "Paternal Great", s: "F" },
          { key: -40, n: "Great Uncle", s: "M", m: -33, f: -32, a: ["F", "H"] },
          { key: -41, n: "Great Aunt", s: "F", m: -33, f: -32, a: ["B", "I"] },
          { key: -20, n: "Uncle", s: "M", m: -11, f: -10, a: ["A"] },

          // "Alice"'s ancestors
          { key: -12, n: "Maternal Grandfather", s: "M", ux: -13, a: ["D", "L"] },
          { key: -13, n: "Maternal Grandmother", s: "F", m: -31, f: -30, a: ["H"] },
          { key: -21, n: "Aunt", s: "F", m: -13, f: -12, a: ["C", "I"] },
          { key: -22, n: "uncle", s: "M", ux: -21 },
          { key: -23, n: "cousin", s: "M", m: -21, f: -22 },
          { key: -30, n: "Maternal Great", s: "M", ux: -31, a: ["D", "J"] },
          { key: -31, n: "Maternal Great", s: "F", m: -50, f: -51, a: ["B", "H", "L"] },
          { key: -42, n: "Great Uncle", s: "M", m: -30, f: -31, a: ["C", "J"] },
          { key: -43, n: "Great Aunt", s: "F", m: -30, f: -31, a: ["E", "G"] },
          { key: -50, n: "Maternal Great Great", s: "F", ux: -51, a: ["D", "I"] },
          { key: -51, n: "Maternal Great Great", s: "M", a: ["B", "H"] }
        ],
        4 /* focus on this person */);
    }


    // create and initialize the Diagram.model given an array of node data representing people
    function setupDiagram(diagram, array, focusId) {
      diagram.model =
        go.GraphObject.make(go.GraphLinksModel,
          { // declare support for link label nodes
            linkLabelKeysProperty: "labelKeys",
            // this property determines which template is used
            nodeCategoryProperty: "s",
            // create all of the nodes for people
            nodeDataArray: array
          });
      setupMarriages(diagram);
      setupParents(diagram);
      setupIdenticalTwins(diagram);

      var node = diagram.findNodeForKey(focusId);
      if (node !== null) {
        diagram.select(node);
        node.linksConnected.each(function(l) {
          if (l.category !== "Marriage") return;
          l.opacity = 0;
          var spouse = l.getOtherNode(node);
          spouse.opacity = 0;
          spouse.pickable = false;
        });
      }
    }

    function findMarriage(diagram, a, b) {  // A and B are node keys
      var nodeA = diagram.findNodeForKey(a);
      var nodeB = diagram.findNodeForKey(b);
      if (nodeA !== null && nodeB !== null) {
        var it = nodeA.findLinksBetween(nodeB);  // in either direction
        while (it.next()) {
          var link = it.value;
          if (link.data !== null && link.category === "Marriage") return link;
        }
      }
      return null;
    }

    function findParentsMarriage(diagram, a) {  // A is a node key
      var node = diagram.findNodeForKey(a);
      if (node !== null) {
        var it = node.findLinksInto();
        while (it.next()) {
          var link = it.value;
          if (link.category !== "") continue;
          var labelnode = link.fromNode;  // the label node for the marriage relationship
          return labelnode.labeledLink;  // return the Marriage link
        }
      }
      return null;
    }

    // now process the node data to determine marriages
    function setupMarriages(diagram) {
      var model = diagram.model;
      var nodeDataArray = model.nodeDataArray;
      for (var i = 0; i < nodeDataArray.length; i++) {
        var data = nodeDataArray[i];
        var key = data.key;
        var uxs = data.ux;
        if (uxs !== undefined) {
          if (typeof uxs === "number") uxs = [ uxs ];
          for (var j = 0; j < uxs.length; j++) {
            var wife = uxs[j];
            if (key === wife) {
              // or warn no reflexive marriages
              continue;
            }
            var link = findMarriage(diagram, key, wife);
            if (link === null) {
              // add a label node for the marriage link
              var mlab = { s: "LinkLabel" };
              model.addNodeData(mlab);
              // add the marriage link itself, also referring to the label node
              var mdata = { from: key, to: wife, labelKeys: [mlab.key], category: "Marriage" };
              model.addLinkData(mdata);
            }
          }
        }
        var virs = data.vir;
        if (virs !== undefined) {
          if (typeof virs === "number") virs = [ virs ];
          for (var j = 0; j < virs.length; j++) {
            var husband = virs[j];
            if (key === husband) {
              // or warn no reflexive marriages
              continue;
            }
            var link = findMarriage(diagram, key, husband);
            if (link === null) {
              // add a label node for the marriage link
              var mlab = { s: "LinkLabel" };
              model.addNodeData(mlab);
              // add the marriage link itself, also referring to the label node
              var mdata = { from: key, to: husband, labelKeys: [mlab.key], category: "Marriage" };
              model.addLinkData(mdata);
            }
          }
        }
      }
    }

    // process parent-child relationships once all marriages are known
    function setupParents(diagram) {
      var model = diagram.model;
      var nodeDataArray = model.nodeDataArray;
      for (var i = 0; i < nodeDataArray.length; i++) {
        var data = nodeDataArray[i];
        var key = data.key;
        var mother = data.m;
        var father = data.f;
        if (mother !== undefined && father !== undefined) {
          var link = findMarriage(diagram, mother, father);
          if (link === null) {
            // or warn no known mother or no known father or no known marriage between them
            if (window.console) window.console.log("unknown marriage: " + mother + " & " + father);
            continue;
          }
          var mdata = link.data;
          var mlabkey = mdata.labelKeys[0];
          var cdata = { from: mlabkey, to: key };
          myDiagram.model.addLinkData(cdata);
        }
      }
    }

    function setupIdenticalTwins(diagram) {
      const model = diagram.model;
      const nodeDataArray = model.nodeDataArray;
      for (let i = 0; i < nodeDataArray.length; i++) {
        const data1 = nodeDataArray[i];
        if (typeof data1.identical === "number") {
          const key1 = data1.key;
          const key2 = data1.identical;
          const data2 = model.findNodeDataForKey(key2);

          if (data2 !== null && data1.m === data2.m && data1.f === data2.f) {
            const T1 = diagram.findNodeForKey(key1);
            const T2 = diagram.findNodeForKey(key2);

            const TPL1 = T1.findTreeParentLink();
            const TPL2 = T2.findTreeParentLink();

            if (TPL1 && TPL2) {
              let TLN1 = TPL1.labelNodes.first();
              if (!TLN1) {
                TLN1 = new go.Node();
                TLN1.isLayoutPositioned = false;
                TLN1.labeledLink = TPL1;
                TLN1.segmentIndex = -2;
                TLN1.segmentFraction = 0.5;
                diagram.add(TLN1);
              }

              let TLN2 = TPL2.labelNodes.first();
              if (!TLN2) {
                TLN2 = new go.Node();
                TLN2.isLayoutPositioned = false;
                TLN2.labeledLink = TPL2;
                TLN2.segmentIndex = -2;
                TLN2.segmentFraction = 0.5;
                diagram.add(TLN2);
              }

              let TL = TLN1.findLinksBetween(TLN2).first();
              if (!TL) {
                const tlinktempl = diagram.linkTemplateMap.get("Identical");
                TL = tlinktempl.copy();
                TL.fromNode = TLN1;
                TL.toNode = TLN2;
                diagram.add(TL);
              }
            }
          }
        }
      }
    }

    function addParents(diagram) {
      var person = diagram.selection.first();
      if (person === null) return;  // no one is selected
      if (findParentsMarriage(diagram, person.data.key) !== null) {  // already has parents
        alert(person.data.n + " already has parents");
        return;
      }
      diagram.startTransaction("add parents");
      var model = myDiagram.model;
      // add a mother
      var mom = { n: "New Mother", s: "F", a: ["A"] };
      model.addNodeData(mom);
      // add a father
      var dad = { n: "New Father", s: "M", a: ["B"] };
      model.addNodeData(dad);
      // add a label node for the marriage link
      var mlab = { s: "LinkLabel" };
      model.addNodeData(mlab);
      // add the marriage link itself, referring to the parents' keys, and also referring to the label node
      var mdata = { from: mom.key, to: dad.key, labelKeys: [mlab.key], category: "Marriage" };
      model.addLinkData(mdata);
      // add child link
      var cdata = { from: mlab.key, to: person.data.key };
      model.addLinkData(cdata);
      diagram.commitTransaction("add parents");
    }


    // A custom layout that shows the two families related to a person's parents
    function GenogramLayout() {
      go.LayeredDigraphLayout.call(this);
      this.initializeOption = go.LayeredDigraphLayout.InitDepthFirstIn;
    }
    go.Diagram.inherit(GenogramLayout, go.LayeredDigraphLayout);

    GenogramLayout.prototype.makeNetwork = function(coll) {
      // generate LayoutEdges for each parent-child Link
      var net = this.createNetwork();
      if (coll instanceof go.Diagram) {
        this.add(net, coll.nodes, true);
        this.add(net, coll.links, true);
      } else if (coll instanceof go.Group) {
        this.add(net, coll.memberParts, false);
      } else if (coll.iterator) {
        this.add(net, coll.iterator, false);
      }
      return net;
    };

    // internal method for creating LayeredDigraphNetwork where husband/wife pairs are represented
    // by a single LayeredDigraphVertex corresponding to the label Node on the marriage Link
    GenogramLayout.prototype.add = function(net, coll, nonmemberonly) {
      // consider all Nodes in the given collection
      var it = coll.iterator;
      while (it.next()) {
        var node = it.value;
        if (!(node instanceof go.Node)) continue;
        if (!node.isLayoutPositioned || !node.isVisible()) continue;
        if (nonmemberonly && node.containingGroup !== null) continue;
        // if it's an unmarried Node, or if it's a Link Label Node, create a LayoutVertex for it
        if (node.isLinkLabel) {
          // get marriage Link
          var link = node.labeledLink;
          var spouseA = link.fromNode;
          var spouseB = link.toNode;
          // create vertex representing both husband and wife
          var vertex = net.addNode(node);
          // now define the vertex size to be big enough to hold both spouses
          vertex.width = spouseA.actualBounds.width + 30 + spouseB.actualBounds.width;
          vertex.height = Math.max(spouseA.actualBounds.height, spouseB.actualBounds.height);
          vertex.focus = new go.Point(spouseA.actualBounds.width + 30/2, vertex.height/2);
        } else {
          // don't add a vertex for any married person!
          // instead, code above adds label node for marriage link
          // assume a marriage Link has a label Node
          if (!node.linksConnected.any(function(l) { return l.category === "Marriage"; })) {
            var vertex = net.addNode(node);
          }
        }
      }
      // now do all Links
      it.reset();
      while (it.next()) {
        var link = it.value;
        if (!(link instanceof go.Link)) continue;
        if (!link.isLayoutPositioned || !link.isVisible()) continue;
        if (nonmemberonly && link.containingGroup !== null) continue;
        // if it's a parent-child link, add a LayoutEdge for it
        if (link.category !== "Marriage") {
          var parent = net.findVertex(link.fromNode);  // should be a label node
          var child = net.findVertex(link.toNode);
          if (child !== null) {  // an unmarried child
            net.linkVertexes(parent, child, link);
          } else {  // a married child
            link.toNode.linksConnected.each(function(l) {
              if (l.category !== "Marriage") return;  // if it has no label node, it's a parent-child link
              // found the Marriage Link, now get its label Node
              var mlab = l.labelNodes.first();
              // parent-child link should connect with the label node,
              // so the LayoutEdge should connect with the LayoutVertex representing the label node
              var mlabvert = net.findVertex(mlab);
              if (mlabvert !== null) {
                net.linkVertexes(parent, mlabvert, link);
              }
            });
          }
        }
      }
    };

    GenogramLayout.prototype.assignLayers = function() {
      go.LayeredDigraphLayout.prototype.assignLayers.call(this);
      var horiz = this.direction == 0.0 || this.direction == 180.0;
      // for every vertex, record the maximum vertex width or height for the vertex's layer
      var maxsizes = [];
      this.network.vertexes.each(function(v) {
        var lay = v.layer;
        var max = maxsizes[lay];
        if (max === undefined) max = 0;
        var sz = (horiz ? v.width : v.height);
        if (sz > max) maxsizes[lay] = sz;
      });
      // now make sure every vertex has the maximum width or height according to which layer it is in,
      // and aligned on the left (if horizontal) or the top (if vertical)
      this.network.vertexes.each(function(v) {
        var lay = v.layer;
        var max = maxsizes[lay];
        if (horiz) {
          v.focus = new go.Point(0, v.height / 2);
          v.width = max;
        } else {
          v.focus = new go.Point(v.width / 2, 0);
          v.height = max;
        }
      });
      // from now on, the LayeredDigraphLayout will think that the Node is bigger than it really is
      // (other than the ones that are the widest or tallest in their respective layer).
    };

    GenogramLayout.prototype.commitNodes = function() {
      go.LayeredDigraphLayout.prototype.commitNodes.call(this);
      // position regular nodes
      this.network.vertexes.each(function(v) {
        if (v.node !== null && !v.node.isLinkLabel) {
          v.node.position = new go.Point(v.x, v.y);
        }
      });
      // position the spouses of each marriage vertex
      var layout = this;
      this.network.vertexes.each(function(v) {
        if (v.node === null) return;
        if (!v.node.isLinkLabel) return;
        var labnode = v.node;
        var lablink = labnode.labeledLink;
        // In case the spouses are not actually moved, we need to have the marriage link
        // position the label node, because LayoutVertex.commit() was called above on these vertexes.
        // Alternatively we could override LayoutVetex.commit to be a no-op for label node vertexes.
        lablink.invalidateRoute();
        var spouseA = lablink.fromNode;
        var spouseB = lablink.toNode;
        // prefer fathers on the left, mothers on the right
        if (spouseA.data.s === "F") {  // sex is female
          var temp = spouseA;
          spouseA = spouseB;
          spouseB = temp;
        }
        // see if the parents are on the desired sides, to avoid a link crossing
        var aParentsNode = layout.findParentsMarriageLabelNode(spouseA);
        var bParentsNode = layout.findParentsMarriageLabelNode(spouseB);
        if (aParentsNode !== null && bParentsNode !== null && aParentsNode.position.x > bParentsNode.position.x) {
          // swap the spouses
          var temp = spouseA;
          spouseA = spouseB;
          spouseB = temp;
        }
        spouseA.position = new go.Point(v.x, v.y);
        spouseB.position = new go.Point(v.x + spouseA.actualBounds.width + 30, v.y);
        if (spouseA.opacity === 0) {
          var pos = new go.Point(v.centerX - spouseA.actualBounds.width/2, v.y);
          spouseA.position = pos;
          spouseB.position = pos;
        } else if (spouseB.opacity === 0) {
          var pos = new go.Point(v.centerX - spouseB.actualBounds.width/2, v.y);
          spouseA.position = pos;
          spouseB.position = pos;
        }
      });
    };

    GenogramLayout.prototype.findParentsMarriageLabelNode = function(node) {
      var it = node.findNodesInto();
      while (it.next()) {
        var n = it.value;
        if (n.isLinkLabel) return n;
      }
      return null;
    };

    // end GenogramLayout class

    // custom routing for same birth siblings
    function ParentChildLink() {
      go.Link.call(this);
    }
    go.Diagram.inherit(ParentChildLink, go.Link);

    ParentChildLink.prototype.computePoints = function() {
      var result = go.Link.prototype.computePoints.call(this);
      var pts = this.points;
      if (pts.length >= 4) {
        var birthId = this.toNode.data["birth"];
        if (birthId) {
          var parents = this.fromNode;
          var sameBirth = 0;
          var sumX = 0;
          var it = parents.findNodesOutOf();
          while (it.next()) {
            var child = it.value;
            if (child.data["birth"] === birthId) {
              sameBirth++;
              sumX += child.location.x;
            }
          }
          if (sameBirth > 0) {
            var midX = sumX/sameBirth;
            var midY = pts.elt(0).y + 57;  //??? adjust to suit your node sizes and layer spacing
            pts.setElt(pts.length-4, new go.Point(pts.elt(0).x, midY));
            pts.setElt(pts.length-3, new go.Point(midX, midY));
            pts.setElt(pts.length-2, pts.elt(pts.length-1));
          }
        }
      }
      return result;
    };
    // end ParentChildLink class
    </script>
</head>
<body onload="init()">
<div id="sample">
  <div id="myDiagramDiv" style="border: solid 1px blue; width:100%; height:600px"></div>
  <button onclick="addParents(myDiagram)">Add Parents</button> to selected parent-less person<br />
  <p>A <em>genogram</em> is an extended family tree diagram that show information about each person or each relationship.</p>
  <p>The node data representing the people, processed by the <code>setupDiagram</code> function:</p>
  <pre id="peopleData">[
    { key: 0, n: "Aaron", s: "M", m:-10, f:-11, ux: 1, a: ["C", "F", "K"] },
    { key: 1, n: "Alice", s: "F", m:-12, f:-13, a: ["B", "H", "K"] },
    { key: 2, n: "Bob", s: "M", m: 1, f: 0, ux: 3, a: ["C", "H", "L"] },
    { key: 3, n: "Barbara", s: "F", a: ["C"] },
    { key: 4, n: "Bill", s: "M", m: 1, f: 0, ux: 5, a: ["E", "H"], birth: 1 },
    { key: 5, n: "Brooke", s: "F", a: ["B", "H", "L"] },
    { key: 6, n: "Claire", s: "F", m: 1, f: 0, a: ["C"], birth: 1 },
    { key: 7, n: "Carol", s: "F", m: 1, f: 0, a: ["C", "I"], birth: 1 },
    { key: 72, n: "Tweedledee", s: "M", m: 1, f: 0, a: ["C", "I"], birth: 2 },
    { key: 73, n: "Tweedledum", s: "M", m: 1, f: 0, a: ["C", "I"], birth: 2 },
    { key: 8, n: "Chloe", s: "F", m: 1, f: 0, vir: 9, a: ["E"] },
    { key: 9, n: "Chris", s: "M", a: ["B", "H"] },
    { key: 10, n: "Ellie", s: "F", m: 3, f: 2, a: ["E", "G"] },
    { key: 11, n: "Dan", s: "M", m: 3, f: 2, a: ["B", "J"] },
    { key: 12, n: "Elizabeth", s: "F", vir: 13, a: ["J"] },
    { key: 13, n: "David", s: "M", m: 5, f: 4, a: ["B", "H"] },
    { key: 14, n: "Emma", s: "F", m: 5, f: 4, a: ["E", "G"] },
    { key: 15, n: "Evan", s: "M", m: 8, f: 9, a: ["F", "H"] },
    { key: 16, n: "Ethan", s: "M", m: 8, f: 9, a: ["D", "K", "S"] },
    { key: 17, n: "Eve", s: "F", vir: 16, a: ["B", "F", "L", "S"] },
    { key: 18, n: "Emily", s: "F", m: 8, f: 9 },
    { key: 19, n: "Fred", s: "M", m: 17, f: 16, a: ["B"] },
    { key: 20, n: "Faith", s: "F", m: 17, f: 16, a: ["L"] },
    { key: 21, n: "Felicia", s: "F", m: 12, f: 13, a: ["H"] },
    { key: 22, n: "Frank", s: "M", m: 12, f: 13, a: ["B", "H"] },

    // "Aaron"'s ancestors
    { key: -10, n: "Paternal Grandfather", s: "M", m: -33, f: -32, ux: -11, a: ["A"] },
    { key: -11, n: "Paternal Grandmother", s: "F", a: ["E"] },
    { key: -32, n: "Paternal Great", s: "M", ux: -33, a: ["F", "H"] },
    { key: -33, n: "Paternal Great", s: "F" },
    { key: -40, n: "Great Uncle", s: "M", m: -33, f: -32, a: ["F", "H"] },
    { key: -41, n: "Great Aunt", s: "F", m: -33, f: -32, a: ["B", "I"] },
    { key: -20, n: "Uncle", s: "M", m: -11, f: -10, a: ["A"] },
          
    // "Alice"'s ancestors
    { key: -12, n: "Maternal Grandfather", s: "M", ux: -13, a: ["D", "L"] },
    { key: -13, n: "Maternal Grandmother", s: "F", m: -31, f: -30, a: ["H"] },
    { key: -21, n: "Aunt", s: "F", m: -13, f: -12, a: ["C", "I"] },
    { key: -22, n: "uncle", s: "M", ux: -21 },
    { key: -23, n: "cousin", s: "M", m: -21, f: -22 },
    { key: -30, n: "Maternal Great", s: "M", ux: -31, a: ["D", "J"] },
    { key: -31, n: "Maternal Great", s: "F", m: -50, f: -51, a: ["B", "H", "L"] },
    { key: -42, n: "Great Uncle", s: "M", m: -30, f: -31, a: ["C", "J"] },
    { key: -43, n: "Great Aunt", s: "F", m: -30, f: -31, a: ["E", "G"] },
    { key: -50, n: "Maternal Great Great", s: "F", ux: -51, a: ["D", "I"] },
    { key: -51, n: "Maternal Great Great", s: "M", a: ["B", "H"] }
]</pre>
  The properties are:
  <ul>
    <li><b>key</b>, the unique ID of the person</li>
    <li><b>n</b>, the person's name</li>
    <li><b>s</b>, the person's sex</li>
    <li><b>m</b>, the person's mother's key</li>
    <li><b>f</b>, the person's father's key</li>
    <li><b>ux</b>, the person's wife</li>
    <li><b>vir</b>, the person's husband</li>
    <li><b>a</b>, an Array of the attributes or markers that the person has</li>
    <li><b>birth</b>, an ID for a birth (could be birthdate), when there are twins or triplets, unique per marriage</li>
  </ul>
  There are functions that convert an attribute value into a brush color or Shape geometry,
  to be added to the Node representing the person.
  <p>A custom <a>LayeredDigraphLayout</a> does the layout, assuming there is a central person whose mother and father
  each have their own ancestors.  In this case we focus on "Bill", but any of the children of "Alice" and "Aaron" would work.</p>
</div>
</body>
</html>

That solved it, thank you.