create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
1,133 lines (1,057 loc) • 53.8 kB
HTML
<!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 ex