UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

1,133 lines (1,057 loc) 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 ex