UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

1,195 lines 53.8 kB
<!DOCTYPE html>
<html lang="en">
  <head>
    
    <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>

<meta name="description" content="A genogram is a family tree diagram for visualizing hereditary patterns." />
<meta itemprop="description" content="A genogram is a family tree diagram for visualizing hereditary patterns." />
<meta property="og:description" content="A genogram is a family tree diagram for visualizing hereditary patterns." />
<meta name="twitter:description" content="A genogram is a family tree diagram for visualizing hereditary patterns." />


<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->


<meta itemprop="name" content="Genogram Family Tree Diagram, with Custom Layout, Printing, Downloading SVG" />
<meta property="og:title" content="Genogram Family Tree Diagram, with Custom Layout, Printing, Downloading SVG" />
<meta name="twitter:title" content="Genogram Family Tree Diagram, with Custom Layout, Printing, Downloading SVG" />


<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/genogram.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/genogram.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/genogram.png" />

<meta property="og:url" content="https://gojs.net/latest/samples/genogram.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/genogram.html" />

<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />

    <title>
      Genogram Family Tree Diagram, with Custom Layout, Printing, Downloading SVG | GoJS Diagramming Library
    </title>
  </head>

  <body>
    <!-- This top nav is not part of the sample code -->
    <nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
  <div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
    <a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
       href="../">
      GoJS
    </a>
    <div class="relative">
      <button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
        <svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
          <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
      </button>
      <div id="topnavList" class="hidden md:block">
        <div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
                    md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
          <a href="../learn/">Learn</a>
          <a href="../samples/">Samples</a>
          <a href="../intro/">Intro</a>
          <a href="../api/">API</a>
          <a href="../download.html">Download</a>
          <a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
          <a id="tc" href="https://nwoods.com/contact.html"
            target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
          <a id="tb" href="https://nwoods.com/sales/index.html"
            target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
        </div>
      </div>
    </div>
  </div>
</nav>
<script>
  window.addEventListener("DOMContentLoaded", function () {
    // topnav
    var topButton = document.getElementById("topnavButton");
    var topnavList = document.getElementById("topnavList");
    if (topButton && topnavList) {
      topButton.addEventListener("click", function (e) {
        topnavList
          .classList
          .toggle("hidden");
        e.stopPropagation();
      });
      document.addEventListener("click", function (e) {
        // if the clicked element isn't the list, close the list
        if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
          topButton.click();
        }
      });

      // set active <a> element
      var url = window
        .location
        .href
        .toLowerCase();
      var aTags = topnavList.getElementsByTagName('a');
      for (var i = 0; i < aTags.length; i++) {
        var lowerhref = aTags[i]
          .href
          .toLowerCase();
        if (lowerhref.endsWith('.html')) 
          lowerhref = lowerhref.slice(0, -5);
        if (url.startsWith(lowerhref)) {
          aTags[i]
            .classList
            .add('active');
          break;
        }
      }
    }
  });
</script>
    <div class="flex flex-col prose">
      <div class="w-full max-w-screen-xl mx-auto">
        <!-- * * * * * * * * * * * * * -->
        <!-- Start of GoJS sample code -->
        <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>

        
          <link rel="stylesheet" href="../assets/css/prism.css"/>
          <script src="../assets/js/prism.js"></script>
        

        <div id="allSampleContent" class="p-4 w-full">
          
            
            
<script src="../extensions/DataInspector.js"></script>
<script id="code">

// A custom layout that shows the two families related to a person's parents
// This depends on the "Mate" category Link relating a mother and a father.
// Each such "Mate" Link must also have a label Node (of category "MateLabel")
// that is the source for Links of the default category connected to their children.
class GenogramLayout extends go.LayeredDigraphLayout {
  constructor(init) {
    super();
    this.MateCategory = "Mate";  // a Link category, for a link connecting the mother and father of children
    this.MateLabelCategory = "MateLabel";  // a Node category, for a node on a "Mate" link, connecting to children
    this.ChildCategory = "";  // the default Link category, for a link connecting a MateLabel node with a child node
    this.initializeOption = go.LayeredDigraphInit.DepthFirstIn;
    this.spouseSpacing = 30;  // minimum space between spouses
    this.isRouting = false;
    if (init) Object.assign(this, init);
  }

  makeNetwork(coll) {
    // generate LayoutEdges for each parent-child Link
    const 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 "Mate" Link
  add(net, coll, nonmemberonly) {
    const horiz = this.direction == 0.0 || this.direction == 180.0;
    const multiSpousePeople = new go.Set();
    // consider all Nodes in the given collection
    const it = coll.iterator;
    while (it.next()) {
      const node = it.value;
      if (!(node instanceof go.Node) || !node.data) 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 "Mate" Link
        const link = node.labeledLink;
        if (link.category === this.MateCategory) {
          const spouseA = link.fromNode;
          const spouseB = link.toNode;
          // create vertex representing both husband and wife
          const vertex = net.addNode(node);
          // now define the vertex size to be big enough to hold both spouses
          if (horiz) {
            vertex.height = spouseA.actualBounds.height + this.spouseSpacing + spouseB.actualBounds.height;
            vertex.width = Math.max(spouseA.actualBounds.width, spouseB.actualBounds.width);
            vertex.focus = new go.Point(vertex.width / 2, spouseA.actualBounds.height + this.spouseSpacing / 2);
          } else {
            vertex.width = spouseA.actualBounds.width + this.spouseSpacing + spouseB.actualBounds.width;
            vertex.height = Math.max(spouseA.actualBounds.height, spouseB.actualBounds.height);
            vertex.focus = new go.Point(spouseA.actualBounds.width + this.spouseSpacing / 2, vertex.height / 2);
          }
        }
      } else {
        // don't add a vertex for any married person!
        // instead, code above adds label node for "Mate" link
        // assume a "Mate" Link has a label Node
        let mates = this.countMates(node);
        if (mates === 0) {
          net.addNode(node);
        } else if (mates > 1) {
          multiSpousePeople.add(node);
        }
      }
    }
    // now do all Links
    it.reset();
    while (it.next()) {
      const 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 === this.ChildCategory && link.data) {
        const parent = net.findVertex(link.fromNode);  // should be a label node
        const child = net.findVertex(link.toNode);
        if (child !== null) {  // an unmarried child
          net.linkVertexes(parent, child, link);
        } else {  // a married child
          link.toNode.linksConnected.each(l => {
            if (l.category !== this.MateCategory || !l.data) return;  // if it has no label node, it's a parent-child link
            // found the Mate Link, now get its label Node
            const 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
            const mlabvert = net.findVertex(mlab);
            if (mlabvert !== null) {
              net.linkVertexes(parent, mlabvert, link);
            }
          });
        }
      }
    }

    while (multiSpousePeople.count > 0) {
      // find all collections of people that are indirectly married to each other
      const node = multiSpousePeople.first();
      const cohort = new go.Set();
      this.extendCohort(cohort, node);
      const sorted = cohort.toArray();
      sorted.sort((a, b) => this.countMates(b) - this.countMates(a));
      const start = sorted[0];
      const map = new go.Map();
      this.walkMates(start, false, 1000000000, 500000000, map);
      sorted.sort((a, b) => map.get(a) - map.get(b));
      const verts = [];
      const seen = new go.Set();
      for (let i = 0; i < sorted.length-1; i++) {
        const n = sorted[i];
        n.linksConnected.each(l => {
          if (l.category === this.MateCategory) {
            const lab = l.labelNodes.first();
            if (lab) {
              const v = net.findVertex(lab);
              if (v && !seen.has(v)) {
                verts.push(v);
                seen.add(v);
              }
            }
          }
        })
      }
      // then encourage them all to be the same generation by connecting them all with a common vertex
      const dummyvert = net.createVertex();
      net.addVertex(dummyvert);
      for (let i = 0; i < verts.length; i++) {
        const v = verts[i];
        net.linkVertexes(dummyvert, v, null);
        // add pairings to try to keep the desired order
        if (i > 0) {
          const w = verts[i-1];
          const dummy = net.createVertex();
          net.addVertex(dummy);
          net.linkVertexes(dummy, w, null);
          net.linkVertexes(dummy, v, null);
          net.linkVertexes(dummy, w, null);
          net.linkVertexes(dummy, v, null);
        }
      }
      // done with these people, now see if there are any other multiple-married people
      multiSpousePeople.removeAll(cohort);
    }
  }

  // collect all of the people indirectly married with a person
  extendCohort(coll, node) {
    if (coll.has(node)) return;
    coll.add(node);
    node.linksConnected.each(l => {
      if (l.category === this.MateCategory) {  // if it's a "Mate" link, continue with both spouses
        this.extendCohort(coll, l.fromNode);
        this.extendCohort(coll, l.toNode);
      }
    });
  }

  // how many Mate relationships does this person have?
  countMates(node) {
    let count = 0;
    node.linksConnected.each(l => {
      if (l.category === this.MateCategory) count++;
    });
    return count;
  }

  walkMates(node, side, val, level, map) {
    if (map.has(node)) return;
    map.set(node, val);
    const count = this.countMates(node);
    level /= 2;
    let idx = 0;
    node.linksConnected.each(l => {
      if (l.category === this.MateCategory) {
        const other = l.getOtherNode(node);
        if (map.has(other)) return;
        idx++;
        const newside = (idx <= count/2) ? side : !side;
        this.walkMates(other, newside, val + (newside ? level : -level), level, map);
      }
    });
  }

  assignLayers() {
    super.assignLayers();
    const horiz = this.direction == 0.0 || this.direction == 180.0;
    // for every vertex, record the maximum vertex width or height for the vertex's layer
    const maxsizes = [];
    this.network.vertexes.each(v => {
      const lay = v.layer;
      let max = maxsizes[lay];
      if (max === undefined) maxsizes[lay] = max = 0;
      const 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(v => {
      const lay = v.layer;
      const 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).
  }

  initializeIndices() {
    super.initializeIndices();
    const vertical = this.direction === 90 || this.direction === 270;
    this.network.edges.each(e => {
      if (e.fromVertex.node && e.fromVertex.node.isLinkLabel) {
        e.portFromPos = vertical ? e.fromVertex.focusX : e.fromVertex.focusY;
      }
      if (e.toVertex.node && e.toVertex.node.isLinkLabel) {
        e.portToPos = vertical ? e.toVertex.focusX : e.toVertex.focusY;
      }
    });

    // get all vertexes for each layer
    var layers = [];  // Array of Arrays of LayeredDigraphVertexes
    this.network.vertexes.each(v => {
      var lay = v.layer;
      if (layers[lay] === undefined) {
        layers[lay] = [v];
      } else {
        layers[lay].push(v);
      }
    });

    // Order the children so that twins/triplets are more likely to be together.
    // now sort them in each layer how you like
    layers.forEach(a => {
      a.sort((v, w) => {
        const vbirth = this.findMultipleBirth(v);
        const wbirth = this.findMultipleBirth(w);
        if (vbirth < wbirth) return -1;
        if (vbirth > wbirth) return 1;
        return 0;
      });
      a.forEach((v, i) => v.index = i);
    });
  }

  // get the birth order for a person; assume zero if there is no data.multiple property value
  findMultipleBirth(v) {
    const node = v.node;
    if (node && node.data) {
      if (node.category === this.MateLabelCategory) {
        const link = node.labeledLink;
        if (link) {
          const fn = link.fromNode;
          if (fn && fn.data && fn.data.multiple !== undefined) return fn.data.multiple;
          const tn = link.toNode;
          if (tn && tn.data && tn.data.multiple !== undefined) return tn.data.multiple;
        }
      } else {
        if (node.data.multiple !== undefined) return node.data.multiple;
      }
    }
    return 0;
  }

  commitNodes() {
    super.commitNodes();
    // position regular nodes
    this.network.vertexes.each(v => {
      if (v.node !== null && !v.node.isLinkLabel) {
        v.node.position = new go.Point(v.x, v.y);
      }
    });
    const horiz = this.direction == 0.0 || this.direction == 180.0;
    // position the spouses of each "Mate" vertex
    this.network.vertexes.each(v => {
      if (v.node === null) return;
      if (!v.node.isLinkLabel) return;
      const labnode = v.node;
      const lablink = labnode.labeledLink;
      // In case the spouses are not actually moved, we need to have the "Mate" 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();
      let spouseA = lablink.fromNode;
      let spouseB = lablink.toNode;
      if (spouseA.opacity > 0 && spouseB.opacity > 0) {
        // maybe swap if multiple mates are on the other side
        const labA = this.findOtherMateLinkLabelNode(spouseA, lablink);
        const labB = this.findOtherMateLinkLabelNode(spouseB, lablink);
        if (labA) {
          const vA = this.network.findVertex(labA);
          if (vA && vA.x > v.x) {
            const temp = spouseA; spouseA = spouseB; spouseB = temp;
          }
        } else if (labB) {
          const vB = this.network.findVertex(labB);
          if (vB && vB.x < v.x) {
            const temp = spouseA; spouseA = spouseB; spouseB = temp;
          }
        }
        spouseA.moveTo(v.x, v.y);
        if (horiz) {
          spouseB.moveTo(v.x, v.y + spouseA.actualBounds.height + this.spouseSpacing);
        } else {
          spouseB.moveTo(v.x + spouseA.actualBounds.width + this.spouseSpacing, v.y);
        }
      } else if (spouseA.opacity === 0) {
        const pos = horiz
          ? new go.Point(v.x, v.centerY - spouseB.actualBounds.height / 2)
          : new go.Point(v.centerX - spouseB.actualBounds.width / 2, v.y);
        spouseB.move(pos);
        if (horiz) pos.y++; else pos.x++;
        spouseA.move(pos);
      } else if (spouseB.opacity === 0) {
        const pos = horiz
          ? new go.Point(v.x, v.centerY - spouseA.actualBounds.height / 2)
          : new go.Point(v.centerX - spouseA.actualBounds.width / 2, v.y);
        spouseA.move(pos);
        if (horiz) pos.y++; else pos.x++;
        spouseB.move(pos);
      }
    });
  }

  findOtherMateLinkLabelNode(node, link) {
    const it = node.linksConnected;
    while (it.next()) {
      const l = it.value;
      if (l.category === this.MateCategory && l !== link) return l.labelNodes.first();
    }
    return null;
  }
}  // end GenogramLayout class

// custom routing for same multiple birth siblings
class TwinLink extends go.Link {
  computePoints() {
    var result = super.computePoints();
    var pts = this.points;
    if (pts.length >= 4) {
      var birthId = this.toNode.data["multiple"];
      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["multiple"] === birthId) {
            sameBirth++;
            sumX += child.location.x;
          }
        }
        if (sameBirth > 0 && !isNaN(sumX)) {
          var midX = sumX / sameBirth;
          var oldp = pts.elt(pts.length - 3);
          pts.setElt(pts.length - 3, new go.Point(midX, oldp.y));
          pts.setElt(pts.length - 2, pts.elt(pts.length - 1));
        }
      }
    }
    return result;
  }
}  // end TwinLink class

// Navigation functions

function findParents(node) {  // returns an Array of zero or two Nodes
  const parents = [];
  if (!(node instanceof go.Node)) return parents;
  const parent = node.findTreeParentNode();
  if (parent && parent.category === parent.diagram.layout.MateLabelCategory) {
    const link = parent.labeledLink;
    if (link) {
      const from = link.fromNode;
      if (from) parents.push(from);
      const to = link.toNode;
      if (to) parents.push(to);
    }
  }
  return parents;
}

function findMates(node) {  // returns an Array of Nodes
  const mates = [];
  if (!(node instanceof go.Node)) return mates;
  node.findLinksConnected().each(link => {
    if (link.category === link.diagram.layout.MateCategory) {
      mates.push(link.getOtherNode(node));
    }
  });
  // ??? sort this collection
  return mates;
}

function findChildren(node, mate) {  // only children with mate; returns an Array of Nodes
  const children = [];
  node.findLinksConnected().each(link => {
    if (link.category === link.diagram.layout.MateCategory &&
        (!mate || link.getOtherNode(node) === mate)) {
      link.labelNodes.each(label => {
        if (label.category === label.diagram.layout.MateLabelCategory) {
          label.findNodesOutOf().each(child => {
            children.push(child);
          });
        }
      });
    }
  });
  // ??? sort this collection
  return children;
}

// initialize the Diagram, including its templates
function init() {
  myDiagram = new go.Diagram("myDiagramDiv", {
    isReadOnly: true,
    // initial Diagram.scale will cause viewport to include the whole diagram
    initialAutoScale: go.AutoScale.Uniform,
    "animationManager.isInitial": false,
    "toolManager.hoverDelay": 100,  // quicker tooltips
    // if you want to limit how many Nodes or Links the user could select at one time
    maxSelectionCount: 1,
    "ChangedSelection": e => {
      const selnode = e.diagram.selection.first();
      // show the Inspector panel just below the selected Node
      const insp = document.getElementById("myInspectorDiv");
      if (selnode) {
        if (insp) {
          const dp = selnode.getDocumentPoint(go.Spot.BottomRight);
          const vp = e.diagram.transformDocToView(dp);
          insp.style.left = vp.x + "px";
          insp.style.top = vp.y + "px";
          insp.style.display = "block";
        }
      } else {
        if (insp) insp.style.display = "none";
      }
    },
    // use a custom layout, defined above
    layout:
      new GenogramLayout({ isInitial: false, direction: 90, layerSpacing: 20, columnSpacing: 10 }),
  });

  // conversion functions for the attribute/marker shapes

  function computeFill(attr) {
    switch (attr[0].toUpperCase()) {
      case "A": return '#5d8cc1';
      case "B": return '#775a4a';
      case "C": return '#94251e';
      case "D": return '#ca6958';
      case "E": return '#68bfaf';
      case "F": return '#23848a';
      case "G": return '#cfdf41';
      case "H": return '#717c42';
      case "V": return '#332d31';
      default:  return "white";
    }
  }

  function computeAlignment(idx) {
    return new go.Spot(0.5, 0.5, (idx & 1) === 0 ? -12.5 : 12.5, (idx & 2) === 0 ? -12.5 : 12.5);
  }

  myDiagram.nodeTemplate =  // representing a person
    new go.Node("Spot", {
        locationSpot: go.Spot.Center,
        layoutConditions: go.LayoutConditions.Standard & ~go.LayoutConditions.NodeSized,
        mouseEnter: (e, node) => highlightRelated(node, true),
        mouseLeave: (e, node) => highlightRelated(node, false)
      })
      .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringifyFixed(1))
      .add(
        // the main Shape: circle or square
        new go.Shape({
            name: "ICON",
            width: 50, height: 50,
            fill: "white", stroke: "black", strokeWidth: 1,
            portId: ""
          })
          .bind("figure", "sex", s => s === "M" ? "Square" : (s === "F" ? "Circle" : "Triangle"))
          .bind("fill"),
        // show at most 4 attribute/marker shapes
        new go.Panel("Spot", {
            isClipping: true,
            width: 49, height: 49,  // account for strokeWidth of main Shape
            itemTemplate:
              new go.Panel()
                .bindObject("alignment", "itemIndex", computeAlignment)
                .add(  // a square shape that fills a quadrant
                  new go.Shape({
                      width: 25, height: 25, strokeWidth: 0,
                      toolTip:
                        go.GraphObject.build("ToolTip")
                          .add(
                            new go.TextBlock()
                              .bind("text", "")
                          )
                    })
                    .bind("fill", "", computeFill)
                )
          })
          .bind("itemArray", "a")  // an Array of strings, such as ["X23", "ABC3", "qxz23m"]
          .add(
            // the main Shape: circle or square, used as a clipping mask
            new go.Shape({ width: 49, height: 49, strokeWidth: 0 })  // fill and stroke don't matter when clipping
              .bind("figure", "sex", s => s === "M" ? "Square" : (s === "F" ? "Circle" : "Triangle"))
          ),
        // proband marker
        new go.Shape({
            alignment: go.Spot.BottomLeft, alignmentFocus: go.Spot.TopRight,
            fill: "darkorange", stroke: "darkorange", strokeWidth: 3, scale: 2,
            geometryString: "F1 M20 0 L14.5 5.5 12 1z M18 1 L0 10"
          })
          .bindModel("visible", "proband", (key, shp) => shp.part.key === key),
        // highlight
        new go.Shape({ fill: null, stroke: null, strokeWidth: 4, width: 55, height: 55 })
          .bindObject("stroke", "isHighlighted", h => h ? "lightcoral" : null),
        // dead symbol: a slash
        new go.Shape({ opacity: 0, geometryString: "M60 0 L0 60" })
          .bind("opacity", "", data => (isDead(data) && (!data.reproduction || data.reproduction === "T" || data.reproduction ===  "SB")) ? 1 : 0),
        // adoption symbol: brackets
        new go.Shape({ opacity: 0, width: 55,  height: 55, geometryString: "M10 0 L0 0 0 55 10 55 M45 0 L55 0 55 55 45 55" })
          .bind("opacity", "adopted", ad => (ad === "in" || ad === "out") ? 1 : 0),
        // name
        new go.TextBlock({
            alignment: go.Spot.Bottom, alignmentFocus: new go.Spot(0.5, 0, 0, -5),
            height: 28,  // fixed height so that nodes are all the same height
            font: "bold 10pt sans-serif",
            textAlign: "center",
            maxSize: new go.Size(85, NaN),
            background: "rgba(255,255,255,0.75)",
            editable: true
          })
          .bindTwoWay("text", "name")
      );

  function highlightRelated(node, show) {
    if (show) {
      const parts = new go.Set();
      highlightAncestors(node, parts);
      highlightDependents(node, parts);
      if (node.diagram) node.diagram.highlightCollection(parts);
    } else {
      if (node.diagram) node.diagram.clearHighlighteds();
    }
  }

  function highlightAncestors(node, parts) {
    const parents = findParents(node);
    parts.addAll(parents);
    if (node.data.adopted === "in") return;
    parents.forEach(parent => highlightAncestors(parent, parts));
  }

  function highlightDependents(node, parts) {
    const children = findChildren(node);
    children.forEach(child => {
      if (child.data.adopted === "in") return;
      parts.add(child);
      highlightDependents(child, parts);
    });
  }

  function isDead(data) {  // the birth and death properties really ought to be dates in some form
    return !!data.death ? 1 : 0;
  }

  function scrollToData(persondata) {
    const node = myDiagram.findNodeForData(persondata);
    if (node) {
      node.diagram.select(node);
      setTimeout(() => node.diagram.commandHandler.scrollToPart(node), 1);
    }
  }

  myDiagram.linkTemplate =  // for parent-child relationships
    new TwinLink({  // for twins as well as for regular parent-child links, defined above
        selectable: false,
        routing: go.Routing.Orthogonal, fromEndSegmentLength: 50,
        fromSpot: go.Spot.Bottom, toSpot: go.Spot.Top,
        layerName: "Background"
      })
      .bindTwoWay("points")
      .add(
        new go.Shape({ stroke: "black", strokeWidth: 2, strokeMiterLimit: 1 })
          .bindObject("strokeDashArray", "toNode", child => child.data.adopted === "in" ? [6, 4] : null)
          .bindObject("stroke", "isHighlighted", h => h ? "green" : "black")
      );

  myDiagram.linkTemplateMap.add("Mate",  // for relationships that produce offspring
    new go.Link({ // AvoidsNodes routing might be better when people have multiple mates
        selectable: false,
        routing: go.Routing.AvoidsNodes,
        fromSpot: go.Spot.LeftRightSides, toSpot: go.Spot.LeftRightSides,
        isTreeLink: false, layerName: "Background"
      })
      .bindTwoWay("points")
      .add(
        new go.Shape({ strokeWidth: 2, stroke: "blue" })
          .bindObject("stroke", "isHighlighted", h => h ? "green" : "blue"),
        new go.Shape({ visible: false, geometryString: "M12 0 L0 16 M16 0 L 4 16", segmentIndex: 1 })
          .bind("visible", "divorced")
      ));

  // The representation of the one label node on a "Mate" Link -- but nothing shows on a Mate Link.
  // Links to children come out from this node, not directly from the mother or the father nodes.
  myDiagram.nodeTemplateMap.add("MateLabel",
    new go.Node({
        selectable: false,
        width: 1, height: 1,
        locationSpot: go.Spot.Center
      })
      .bindTwoWay("location", "loc", go.Point.parse, go.Point.stringifyFixed(1)));

  // The horizontal line connecting the parent links for identical twins
  myDiagram.linkTemplateMap.add("Identical",  // for connecting twins/triplets
    new go.Link({
        selectable: false, isLayoutPositioned: false,
        isTreeLink: false, layerName: "Background"
      })
      .add(
        new go.Shape({ strokeWidth: 2, stroke: "slateblue"  })
      ));

  // The representation of each twin label node -- nothing shows on a parent-child Link.
  // These are connected by "Identical" links.
  myDiagram.nodeTemplateMap.add("TwinLabel",
    new go.Node({
        selectable: false, isLayoutPositioned: false,
        width: 1, height: 1,
        segmentIndex: -2, segmentFraction: 0.333
      }));

  // set up Data Inspector -- YOU SHOULD REPLACE THIS CODE WITH YOUR OWN DETAIL EDITORS
  myInspector =
    new Inspector("myInspectorDiv", myDiagram, {
      properties: {
        // key would be automatically added for nodes, but we want to declare it read-only also:
        key: { readOnly: true, show: Inspector.showIfPresent },
        name: { show: Inspector.showIfPresent },
        sex: { show: Inspector.showIfPresent },
        mother: { readOnly: true, show: Inspector.showIfPresent },
        father: { readOnly: true, show: Inspector.showIfPresent },
        birth: { show: Inspector.showIfPresent },
        death: { show: Inspector.showIfPresent },
        note: { show: Inspector.showIfPresent },
        multiple: { show: Inspector.showIfPresent },
        identical: { show: Inspector.showIfPresent },
        loc: { show: false },
        points: { readOnly: true, show: false },
        from: { readOnly: true, show: Inspector.showIfPresent },
        to: { readOnly: true, show: Inspector.showIfPresent },
        labelKeys: { show: false },
        category: { show: false },
      }
    });

  // Initialize and implement the various HTML buttons

  // Load a model from Json text, displayed below the Diagram

  function load() {
    myDiagram.clear();  // get rid of any left-over "Identical" links (they are not in the model)
    const str = document.getElementById("mySavedModel").value;
    myDiagram.model = go.Model.fromJson(str);
    myDiagram.model.pointsDigits = 1;  // limit decimals in JSON output for "points" Arrays
    // if not all person nodes have real locations, need to force a layout
    if (!myDiagram.nodes.all(node => node.isLinkLabel || node.location.isReal())) {
      myDiagram.layoutDiagram(true);
    }
    setupIdenticalTwins(myDiagram);  // maybe add some unmodeled "Identical" links
  }

  // Do some extra work in order to show fraternal or identical twins.
  function setupIdenticalTwins(diagram) {
    const model = diagram.model;
    const nodeDataArray = model.nodeDataArray;
    for (let i = 0; i < nodeDataArray.length; i++) {
      const data1 = nodeDataArray[i];
      let identical = data1.identical;
      if (typeof identical === "string") identical = parseInt(identical);
      if (typeof identical === "number" && !isNaN(identical)) {
        const key1 = data1.key;
        const key2 = identical;
        const data2 = model.findNodeDataForKey(key2);
        // check that both parents are the same
        if (data2 !== null && data1.mother === data2.mother && data1.father === data2.father) {
          const T1 = diagram.findNodeForKey(key1);
          const T2 = diagram.findNodeForKey(key2);
          const TPL1 = T1.findTreeParentLink();
          const TPL2 = T2.findTreeParentLink();
          if (TPL1 && TPL2) {
            const tlabtempl = diagram.nodeTemplateMap.get("TwinLabel");
            let TLN1 = TPL1.labelNodes.first();
            if (!TLN1) {
              TLN1 = tlabtempl.copy();
              TLN1.labeledLink = TPL1;
              diagram.add(TLN1);
            }
            let TLN2 = TPL2.labelNodes.first();
            if (!TLN2) {
              TLN2 = tlabtempl.copy();
              TLN2.labeledLink = TPL2;
              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 print() {
    const svgWindow = window.open();
    if (!svgWindow) return; // failure to open a new Window
    svgWindow.document.title = "Genogram";
    svgWindow.document.body.style.margin = "0px";
    const printSize = new go.Size(700, 960);
    const bnds = myDiagram.documentBounds;
    let x = bnds.x;
    let y = bnds.y;
    while (y < bnds.bottom) {
      while (x < bnds.right) {
        const svg = myDiagram.makeSvg({
          scale: 1.0,
          position: new go.Point(x, y),
          size: printSize,
          background: "white"
        });
        svgWindow.document.body.appendChild(svg);
        x += printSize.width;
      }
      x = bnds.x;
      y += printSize.height;
    }
    requestAnimationFrame(() => { svgWindow.print(); svgWindow.close(); });
  }
  document.getElementById("myPrintButton").addEventListener("click", print);

  document.getElementById("myDownloadButton").addEventListener("click", () => {
    myDiagram.commandHandler.downloadSvg({ name: "genogram.svg" });
  });

  function scrollToProband() {
    if (typeof myDiagram.model.modelData.proband === "number") {
      const node = myDiagram.findNodeForKey(myDiagram.model.modelData.proband);
      if (node) myDiagram.commandHandler.scrollToPart(node);
    }
  }
  document.getElementById("myScrollToProband").addEventListener("click", scrollToProband);

  load();
}  // end of init

window.addEventListener("DOMContentLoaded", init);
</script>

<div id="sample">
  <div style="position:relative">
    <div id="myDiagramDiv" style="background-color: #F8F8F8; border: solid 1px black; width:100%; height:600px;"></div>
    <div id="myInspectorDiv" class="inspector" style="display:none; z-index:99; position:absolute; background:whitesmoke; border:solid gray 3px;"></div>
  </div>
  <div>
    <button id="myPrintButton">Print</button>
    <button id="myDownloadButton">Download SVG</button>
    <button id="myScrollToProband">Scroll to Proband</button>
  </div>
  <p>A <em>genogram</em> or <em>pedigree chart</em> is an extended family tree diagram that displays information about
    each person or each relationship.
    The <em>proband</em> is the person about whom the genetic study is focused -- that node is highlighted with an arrow.
    In this case we focus on "Bill".
  </p>
  <p>
    There is support for twins or triplets, both fraternal and identical.
  </p>
  <p>
    When the mouse passes over a node, all other nodes representing people who are direct ancestors or descendants are highlighted.
  </p>
  <p>
    Note that the term "marriage" here does not refer to a legal or cultural kind of relationship,
    but simply one representing the female and male genetic sources for any children.
  </p>
  <p>
    There are functions that convert an attribute value into a brush color or Shape geometry,
    to be added to the Node representing the person.  These can be adapted for your app's specific purposes.
  </p>
  <p>
    Although this uses an <a>Inspector</a> to show the values of the data properties for the first selected node, nothing can be changed in this sample.
    We also have a version of this sample that supports editing the graph.
  </p>
  <p>
    A custom <a>LayeredDigraphLayout</a> does the layout, assuming there is a central person whose mother and father
    each have their own ancestors.
    Husband/wife node pairs are represented by a single <a>LayeredDigraphVertex</a>.
  </p>
  <p>For a simpler family tree, see the <a href="familyTree.html">family tree</a> sample or
    <a href="familyTreeJP.html">Japanese family tree</a> sample.
  </p>
  <!-- the rest is just for demonstration -->
  Diagram model saved in JSON format:
  <textarea id="mySavedModel" style="width:100%;height:250px">
{ "class": "GraphLinksModel",
"copiesArrays": true,
"pointsDigits": 1,
"linkLabelKeysProperty": "labelKeys",
"modelData": {"proband":4},
"nodeDataArray": [
{"key":0,"name":"Aaron","sex":"M","mother":-10,"father":-11,"birth":"","death":"","note":"","a":["A123","B74","D85","G4"]},
{"key":1,"name":"Alice","sex":"F","mother":-12,"father":-13,"birth":"","death":"","note":"","a":["B74","C12","D85","V4"]},
{"key":2,"name":"Bob","sex":"M","mother":1,"father":0,"birth":"","death":"","note":"","a":["E92","F4"]},
{"key":3,"name":"Barbara","sex":"F","mother":"","father":"","birth":"","death":"","note":"","a":["D99","M23"]},
{"key":4,"name":"Bill","sex":"M","mother":1,"father":0,"birth":"","death":"","note":"","a":["A6","B3"]},
{"key":5,"name":"Brooke","sex":"F","mother":"","father":"","birth":"","death":"","note":"","a":["A2"]},
{"key":6,"name":"Claire","sex":"F","mother":1,"father":0,"birth":"","death":"","note":"","a":["B34","G4"]},
{"key":7,"name":"Carol","sex":"F","mother":1,"father":0,"birth":"","death":"","note":"","a":["C23123","G4"]},
{"key":8,"name":"Chloe","sex":"F","mother":1,"father":0,"birth":"","death":"","note":"","a":["A123","Dev97","G4"]},
{"key":9,"name":"Chris","sex":"M","mother":"","father":"","birth":"","death":"","note":"","a":["C23123","E234","H54"]},
{"key":10,"name":"Ellie","sex":"F","mother":3,"father":2,"birth":"","death":"","note":"","a":["D99","F4","G0594"]},
{"key":11,"name":"Dan","sex":"M","mother":3,"father":2,"birth":"","death":"","note":"","a":["F4","G1212"]},
{"key":12,"name":"Elizabeth","sex":"F","mother":"","father":"","birth":"","death":"","note":"","a":["H"]},
{"key":13,"name":"David","sex":"M","mother":5,"father":4,"birth":"","death":"","note":"","a":["A2342","B3"]},
{"key":14,"name":"Emma","sex":"F","mother":5,"father":4,"birth":"","death":"","note":"","a":["B3"]},
{"key":15,"name":"Evan","sex":"M","mother":8,"father":9,"birth":"","death":"","note":"","a":["A123","CV9569"]},
{"key":16,"name":"Ethan","sex":"M","mother":8,"father":9,"birth":"","death":"","note":"","a":["A123","D343","G4"]},
{"key":17,"name":"Eve","sex":"F","mother":"","father":"","birth":"","death":"","note":"","a":["E509468"]},
{"key":18,"name":"Emily","sex":"F","mother":8,"father":9,"birth":"","death":"","note":"","a":["F68","G","H"]},
{"key":19,"name":"Fred","sex":"M","mother":17,"father":16,"birth":"","death":"","note":"","a":["C56","G345834058"]},
{"key":20,"name":"Faith","sex":"F","mother":17,"father":16,"birth":"","death":"","note":"","a":["H0452-a"]},
{"key":21,"name":"Felicia","sex":"F","mother":12,"father":13,"birth":"","death":"","note":"","a":["B3","A549"]},
{"key":22,"name":"Frank","sex":"M","mother":12,"father":13,"birth":"","death":"","note":"","a":["B349058","E867"]},
{"key":23,"name":"Castor","sex":"?","mother":12,"father":13,"birth":"","death":true,"reproduction":"MC","note":"","a":["B3","C23"]},
{"key":24,"name":"Nestor","sex":"?","mother":12,"father":13,"birth":"","death":true,"reproduction":"T","note":"","a":["D456"]},
{"key":27,"name":"Flora","sex":"F","mother":12,"father":13,"adopted":"in","birth":"","death":"","note":"","a":["E766"]},
{"key":28,"name":"Aurora","sex":"F","mother":12,"father":13,"adopted":"out","birth":"","death":"","note":"","a":["B3","F345"]},
{"key":70,"name":"Elsbeth","sex":"F","mother":3,"father":2,"birth":"","multiple":1,"death":"","note":"","a":["F4","D99","G0584"]},
{"key":71,"name":"Daneel","sex":"M","mother":3,"father":2,"birth":"","multiple":1,"death":"","note":"","a":["F4","G4","H567"]},
{"key":72,"name":"Tweedledee","sex":"M","mother":3,"father":2,"birth":"","multiple":2,"death":"","note":"","a":["F4","A37"]},
{"key":73,"name":"Tweedledum","sex":"M","mother":3,"father":2,"birth":"","multiple":2,"identical":72,"death":"","note":"","a":["F4","B54"]},
{"key":74,"name":"Tweedledoe","sex":"F","mother":3,"father":2,"birth":"","multiple":2,"death":"","note":"","a":["F4","D99","C305"]},
{"key":-10,"name":"Paternal Grandfather","sex":"M","mother":-33,"father":-32,"birth":"","death":true,"note":"","a":["D02934","G4"]},
{"key":-11,"name":"Paternal Grandmother","sex":"F","mother":"","father":"","birth":"","death":true,"note":"","a":["E5690"]},
{"key":-32,"name":"Paternal Great","sex":"M","mother":"","father":"","birth":"","death":true,"note":"","a":["F0834"]},
{"key":-33,"name":"Paternal Great","sex":"F","mother":"","father":"","birth":"","death":true,"note":"","a":["G294"]},
{"key":-40,"name":"Great Uncle","sex":"M","mother":-33,"father":-32,"birth":"","death":true,"note":"","a":["H45069","G4"]},
{"key":-41,"name":"Great Aunt","sex":"F","mother":-33,"father":-32,"birth":"","death":true,"note":"","a":["A2"]},
{"key":-20,"name":"Uncle","sex":"M","mother":-11,"father":-10,"birth":"","death":"","note":"","a":["B5408","G4"]},
{"key":-12,"name":"Maternal Grandfather","sex":"M","mother":"","father":"","birth":"","death":"","note":"","a":["C23894"]},
{"key":-13,"name":"Maternal Grandmother","sex":"F","mother":-31,"father":-30,"birth":"","death":"","note":"","a":["D23"]},
{"key":-21,"name":"Aunt","sex":"F","mother":-13,"father":-12,"birth":"","death":"","note":"","a":["E3405"]},
{"key":-22,"name":"Uncle","sex":"M","mother":"","father":"","birth":"","death":"","note":"","a":["F5408"]},
{"key":-23,"name":"Cousin","sex":"M","mother":-21,"father":-22,"birth":"","death":"","note":"","a":["G2173"]},
{"key":-30,"name":"Maternal Great","sex":"M","mother":"","father":"","birth":"","death":true,"note":"","a":["H34"]},
{"key":-31,"name":"Maternal Great","sex":"F","mother":-50,"father":-51,"birth":"","death":true,"note":"","a":["A34"]},
{"key":-42,"name":"Great Uncle","sex":"M","mother":-30,"father":-31,"birth":"","death":true,"note":"","a":["B997"]},
{"key":-43,"name":"Great Aunt","sex":"F","mother":-30,"father":-31,"birth":"","death":"","note":"","a":["C09568"]},
{"key":-50,"name":"Maternal Great Great","sex":"F","mother":"","father":"","birth":"","death":true,"note":"","a":["D68"]},
{"key":-51,"name":"Maternal Great Great","sex":"M","mother":"","father":"","birth":"","death":true,"note":"","a":["E568"]},
{"category":"MateLabel","key":-53},
{"category":"MateLabel","key":-54},
{"category":"MateLabel","key":-55},
{"category":"MateLabel","key":-56},
{"category":"MateLabel","key":-57},
{"category":"MateLabel","key":-58},
{"category":"MateLabel","key":-59},
{"category":"MateLabel","key":-60},
{"category":"MateLabel","key":-61},
{"category":"MateLabel","key":-62},
{"category":"MateLabel","key":-63},
{"category":"MateLabel","key":-64}
],
"linkDataArray": [
{"from":0,"to":1,"category":"Mate","labelKeys":[-53],"divorced":false},
{"from":2,"to":3,"category":"Mate","labelKeys":[-54],"divorced":false},
{"from":4,"to":5,"category":"Mate","labelKeys":[-55],"divorced":false},
{"from":8,"to":9,"category":"Mate","labelKeys":[-56],"divorced":false},
{"from":12,"to":13,"category":"Mate","labelKeys":[-57],"divorced":false},
{"from":17,"to":16,"category":"Mate","labelKeys":[-58],"divorced":false},
{"from":-10,"to":-11,"category":"Mate","labelKeys":[-59],"divorced":false},
{"from":-32,"to":-33,"category":"Mate","labelKeys":[-60],"divorced":false},
{"from":-12,"to":-13,"category":"Mate","labelKeys":[-61],"divorced":false},
{"from":-22,"to":-21,"category":"Mate","labelKeys":[-62],"divorced":false},
{"from":-30,"to":-31,"category":"Mate","labelKeys":[-63],"divorced":false},
{"from":-50,"to":-51,"category":"Mate","labelKeys":[-64],"divorced":false},
{"from":-59,"to":0},
{"from":-61,"to":1},
{"from":-53,"to":2},
{"from":-53,"to":4},
{"from":-53,"to":6},
{"from":-53,"to":7},
{"from":-53,"to":8},
{"from":-54,"to":10},
{"from":-54,"to":11},
{"from":-55,"to":13},
{"from":-55,"to":14},
{"from":-56,"to":15},
{"from":-56,"to":16},
{"from":-56,"to":18},
{"from":-58,"to":19},
{"from":-58,"to":20},
{"from":-57,"to":21},
{"from":-57,"to":22},
{"from":-57,"to":23},
{"from":-57,"to":24},
{"from":-57,"to":27},
{"from":-57,"to":28},
{"from":-54,"to":70},
{"from":-54,"to":71},
{"from":-54,"to":72},
{"from":-54,"to":73},
{"from":-54,"to":74},
{"from":-60,"to":-10},
{"from":-60,"to":-40},
{"from":-60,"to":-41},
{"from":-59,"to":-20},
{"from":-63,"to":-13},
{"from":-61,"to":-21},
{"from":-62,"to":-23},
{"from":-64,"to":-31},
{"from":-63,"to":-42},
{"from":-63,"to":-43}
]}
  </textarea>
</div>

          
        </div>
        <!-- * * * * * * * * * * * * * -->
        <!--  End of GoJS sample code  -->
      </div>

      <div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
        <hr/>
        <h3 class="text-xl">GoJS Features in this sample</h3>
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Item Arrays</h4>
<p>
It is sometimes useful to display a variable number of elements in a node by data binding to a JavaScript Array.
In GoJS, this is simply achieved by binding (or setting) <a href="../api/symbols/Panel.html#itemArray" target="api">Panel.itemArray</a>.
The <a href="../api/symbols/Panel.html" target="api">Panel</a> will create an element in the panel for each value in the Array.
More information can be found in the <a href="../intro/itemArrays.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#itemarrays">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Collections</h4>
<p>
<b>GoJS</b> provides its own collection classes: <a href="../api/symbols/List.html" target="api">List</a>, <a href="../api/symbols/Set.html" target="api">Set</a>, and <a href="../api/symbols/Map.html" target="api">Map</a>.
You can iterate over a collection by using an <a href="../api/symbols/Iterator.html" target="api">Iterator</a>.
More information can be found in the <a href="../intro/collections.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#collections">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Layered Digraph Layout</h4>
<p>
This predefined layout is used for placing Nodes of a general directed graph in layers (rows or columns). This is more general than <a href="../api/symbols/TreeLayout.html">TreeLayout</a>,
as it does not require that the graph be tree-structured.
More information can be found in the <a href="../intro/layouts.html#LayeredDigraphLayout">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#layered-digraph">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Custom Layouts</h4>
<p>
  GoJS allows for the creation of custom layouts to meet specific needs. 
</p>
<p>
There are also many layouts that are extensions -- not predefined in the <code>go.js</code> or <code>go-debug.js</code> library,
but available as source code in one of the three extension directories, with some documentation and corresponding samples.
More information can be found in the <a href="../intro/layouts.html#CustomLayouts">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#customlayout">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Geometry Path Strings</h4>
<p>
The <b>GoJS</b> <a href="../api/symbols/Geometry.html" target="api">Geometry</a> class controls the "shape" of a <a href="../api/symbols/Shape.html" target="api">Shape</a>,
whereas the <a href="../api/symbols/Shape.html#fill" target="api">Shape.fill</a> and <a href="../api/symbols/Shape.html#stroke" target="api">Shape.stroke</a> and other shape properties control the colors and appearance of the shape.
For common shape figures, there are predefined geometries that can be used by setting <a href="../api/symbols/Shape.html#figure" target="api">Shape.figure</a>.
However one can also define custom geometries.
</p>
<p>
One can construct any Geometry by allocating and initializing a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> of at least one <a href="../api/symbols/PathFigure.html" target="api">PathFigure</a> holding some <a href="../api/symbols/PathSegment.html" target="api">PathSegment</a>s.
But you may find that using the string representation of a Geometry is easier to write and save in a database.
Use the static method <a href="../api/symbols/Geometry.html#parse" target="api">Geometry.parse</a> or the <a href="../api/symbols/Shape.html#geometryString" target="api">Shape.geometryString</a> property to transform a geometry path string into a <a href="../api/symbols/Geometry.html" target="api">Geometry</a> object.
</p>
<p>
More information can be found in the <a href="../intro/geometry.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#geometries">Related samples</a>
            </p>
            <hr>
          
        <!-- blacklist tags that do not correspond to a specific GoJS feature -->
          
            <h4>Commands</h4>
<p>
A <a href="../api/symbols/CommandHandler.html" target="api">CommandHandler</a> handles all default keyboard input events in a Diagram.
There are many predefined methods on <a>CommandHandler</a> that implement common commands to operate on the Diagram or the current <a>Diagram.selection></a>.
</p>
<p>
You can override <a>CommandHandler.doKeyDown</a> to handle additional keyboard shortcuts or to change which commands are invoked via the keyboard.
<p>
Your code can invoke a command by calling the appropriate method on the <a>Diagram.commandHandler</a>.
Each command method has a corresponding <b>can...</b> predicate that your code can use to enable or disable any buttons that invoke the command.
Your code can customize the behavior of a command by overriding the method on <a>CommandHandler</a>,
or by setting properties on the <a>CommandHandler</a> or <a>Diagram</a> or <a>Part</a>s --
see <a href="../intro/permissions.html">GoJS Permissions</a>.
</p>
<p>
There are several CommandHandler extensions that provide additional functionality.
<p>
More information can be found in the <a href="../intro/commands.html">GoJS Intro</a>.
</p>
            <p>
              <a href="../samples/index.html#commands">Related samples</a>
            </p>
            <hr>
          
        </div>
    </div>
  </body>
  <!--  This script is part of the gojs.net website, and is not needed to run the sample -->
  <script src="../assets/js/goSamples.js"></script>
</html>