i am asking how to add new node or block to the genogram on click and add ancestors or descendants. i worked with flow chart and there is a panel that i can drag and drop to the diagram, how to do so in the genogram.
Derived from an earlier version of the Genogram sample:
<!DOCTYPE html>
<html>
<head>
<title>Genogram</title>
<!-- Copyright 1998-2021 by Northwoods Software Corporation. -->
<meta name="description" content="A genogram is a family tree diagram for visualizing hereditary patterns.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="go.js"></script>
<script id="code">
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
initialAutoScale: go.Diagram.Uniform,
"undoManager.isEnabled": true,
layout: // use a custom layout, defined below
$(GenogramLayout,
{ direction: 90, layerSpacing: 30, columnSpacing: 10 })
});
// start off with a single person
var model = new go.GraphLinksModel();
model.nodeCategoryProperty = "sex";
model.copiesArrays = true;
model.linkLabelKeysProperty = "labelKeys";
model.nodeDataArray = [
{ name: "person", sex: "M", attr: ["A", "G"], loc: "0 0" }
];
myDiagram.model = model;
// Styling and binding conversion functions
// Current there is no way for the user to edit genetic attributes --
// the values in the "attr" Array
// determine the color for each attribute shape
function attrFill(a) {
switch (a) {
case "A": return "green";
case "B": return "orange";
case "C": return "red";
case "D": return "cyan";
case "E": return "gold";
case "F": return "pink";
case "G": return "blue";
case "H": return "brown";
case "I": return "purple";
case "J": return "chartreuse";
case "K": return "lightgray";
case "L": return "magenta";
case "S": return "red";
default: return "transparent";
}
}
// determine the geometry for each attribute shape in a male;
// except for the slash these are all squares at each of the four corners of the overall square
var tlsq = go.Geometry.parse("F M1 1 l19 0 0 19 -19 0z");
var trsq = go.Geometry.parse("F M20 1 l19 0 0 19 -19 0z");
var brsq = go.Geometry.parse("F M20 20 l19 0 0 19 -19 0z");
var blsq = go.Geometry.parse("F M1 20 l19 0 0 19 -19 0z");
var slash = go.Geometry.parse("F M38 0 L40 0 40 2 2 40 0 40 0 38z");
function maleGeometry(a) {
switch (a) {
case "A": return tlsq;
case "B": return tlsq;
case "C": return tlsq;
case "D": return trsq;
case "E": return trsq;
case "F": return trsq;
case "G": return brsq;
case "H": return brsq;
case "I": return brsq;
case "J": return blsq;
case "K": return blsq;
case "L": return blsq;
case "S": return slash;
default: return tlsq;
}
}
// determine the geometry for each attribute shape in a female;
// except for the slash these are all pie shapes at each of the four quadrants of the overall circle
var tlarc = go.Geometry.parse("F M20 20 B 180 90 20 20 19 19 z");
var trarc = go.Geometry.parse("F M20 20 B 270 90 20 20 19 19 z");
var brarc = go.Geometry.parse("F M20 20 B 0 90 20 20 19 19 z");
var blarc = go.Geometry.parse("F M20 20 B 90 90 20 20 19 19 z");
function femaleGeometry(a) {
switch (a) {
case "A": return tlarc;
case "B": return tlarc;
case "C": return tlarc;
case "D": return trarc;
case "E": return trarc;
case "F": return trarc;
case "G": return brarc;
case "H": return brarc;
case "I": return brarc;
case "J": return blarc;
case "K": return blarc;
case "L": return blarc;
case "S": return slash;
default: return tlarc;
}
}
function nodeStyle() {
return [
{ // some node properties
locationSpot: go.Spot.Center,
locationObjectName: "ICON",
selectionObjectName: "ICON",
// show some Buttons when a person is selected
selectionAdornmentTemplate:
$(go.Adornment, "Spot",
$(go.Panel, "Auto",
$(go.Shape,
{
fill: "transparent", strokeWidth: 4, stroke: "dodgerblue",
spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight
},
new go.Binding("figure", "sex", function(s) { return s === "F" ? "Circle" : "Square"; })),
$(go.Placeholder, { margin: 2 })
),
$("Button",
{ alignment: go.Spot.Top, alignmentFocus: go.Spot.Bottom },
// only visible when there aren't already parents
new go.Binding("visible", "", function(ad) {
return !ad.diagram.isReadOnly &&
findParentsMarriageLabelNode(ad.adornedPart) === null;
}).ofObject(),
{
click: function(e, button) {
var node = button.part.adornedPart;
addParentsToPerson(node);
node.isSelected = false;
}
},
$(go.TextBlock, "Add\nParents", { textAlign: "center" })),
$("Button",
{ alignment: go.Spot.Right, alignmentFocus: go.Spot.Left },
// only when changes are allowed
new go.Binding("visible", "", function(ad) { return !ad.diagram.isReadOnly; }).ofObject(),
{
click: function(e, button) {
var node = button.part.adornedPart;
addSpouseToPerson(node);
node.isSelected = false;
}
},
$(go.TextBlock, "Add\nSpouse", { textAlign: "center" })),
$("Button",
{ alignment: go.Spot.Left, alignmentFocus: go.Spot.Right },
// only visible when unmarried
new go.Binding("visible", "", function(ad) {
return !ad.diagram.isReadOnly &&
findMarriageLinks(ad.adornedPart).count === 0;
}).ofObject(),
{
click: function(e, button) {
var node = button.part.adornedPart;
node.diagram.commit(function(diag) {
node.category = (node.category === "M") ? "F" : "M";
}, "changed sex");
}
},
$(go.TextBlock, "Change\nSex", { textAlign: "center" })
)
)
},
// and a node binding
new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify)
];
}
// Templates
// two different node templates, one for each sex,
// named by the category value in the node data object
myDiagram.nodeTemplateMap.add("M", // male
$(go.Node, "Vertical", nodeStyle(),
$(go.Panel,
{ name: "ICON", background: "transparent" },
$(go.Shape, "Square",
{ width: 40, height: 40, strokeWidth: 2, fill: "white", portId: "" }),
$(go.Panel,
{ // for each attribute show a Shape at a particular place in the overall square
itemTemplate:
$(go.Panel,
$(go.Shape,
{ stroke: null, strokeWidth: 0 },
new go.Binding("fill", "", attrFill),
new go.Binding("geometry", "", maleGeometry))
),
margin: 1
},
new go.Binding("itemArray", "attr")
)
),
$(go.TextBlock,
{ textAlign: "center", maxSize: new go.Size(80, NaN), editable: true },
new go.Binding("text", "name").makeTwoWay())
));
myDiagram.nodeTemplateMap.add("F", // female
$(go.Node, "Vertical", nodeStyle(),
$(go.Panel,
{ name: "ICON", background: "transparent" },
$(go.Shape, "Circle",
{ width: 40, height: 40, strokeWidth: 2, fill: "white", portId: "" }),
$(go.Panel,
{ // for each attribute show a Shape at a particular place in the overall circle
itemTemplate:
$(go.Panel,
$(go.Shape,
{ stroke: null, strokeWidth: 0 },
new go.Binding("fill", "", attrFill),
new go.Binding("geometry", "", femaleGeometry))
),
margin: 1
},
new go.Binding("itemArray", "attr")
)
),
$(go.TextBlock,
{ textAlign: "center", maxSize: new go.Size(80, NaN), editable: true },
new go.Binding("text", "name").makeTwoWay())
));
// the representation of each label node -- nothing shows on a Marriage Link
myDiagram.nodeTemplateMap.add("LinkLabel",
$(go.Node,
{ selectable: false, layerName: "Foreground", fromEndSegmentLength: 20 },
$("Button",
new go.Binding("visible", "", function(ad) { return !ad.diagram.isReadOnly; }).ofObject(),
{
click: function(e, button) {
var labnode = button.part;
var child = addChildToMarriage(labnode.labeledLink);
child.diagram.select(child);
}
},
$(go.TextBlock, "+")
)));
myDiagram.linkTemplate = // for parent-child relationships
$(go.Link,
{
routing: go.Link.Orthogonal, curviness: 15,
layerName: "Background", selectable: false,
fromSpot: go.Spot.Bottom, toSpot: go.Spot.Top
},
$(go.Shape, { strokeWidth: 1.5 })
);
myDiagram.linkTemplateMap.add("Marriage", // for marriage relationships
$(go.Link,
{ selectable: false, routing: go.Link.AvoidsNodes },
$(go.Shape, { strokeWidth: 3, stroke: "blue" })
));
}
// Various convenient graph navigation functions
// return the label Node for a marriage which is the Node with which all Links
// to children Nodes are connected; null if there are no parents
function findParentsMarriageLabelNode(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
var it = node.findNodesInto();
while (it.next()) {
var n = it.value;
if (n.category === "LinkLabel") return n;
}
return null;
}
// return the Marriage Link of a person's parent Nodes, if the person has any parents in the diagram;
// null if there are no parents
function findParentsMarriageLink(node) {
var labnode = findParentsMarriageLabelNode(node);
return labnode ? labnode.labeledLink : null;
}
// return an Array of the given node's two parent Nodes; both will be null if no parents are known
function findParentNodes(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
var mlink = findParentsMarriageLink(node);
var parents = [null, null];
if (mlink) {
parents[0] = mlink.fromNode;
parents[1] = mlink.toNode;
}
return parents;
}
// return a collection of Marriage Links connected with a person; may be an empty collection
function findMarriageLinks(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
return node.linksConnected.filter(function(l) { return l.category === "Marriage"; });
}
// return a collection of person Nodes connected by marriage Links
function findSpouseNodes(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
return findMarriageLinks(node).map(function(l) { return l.getOtherNode(node); });
}
// return the label Nodes of Marriage Links, one per marriage;
// each can be Link.fromNode of marriage to children Links
function findMarriageLabelNodes(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
return findMarriageLinks(node).map(function(l) { return findMarriageLabelNode(l); });
}
// return the label Node of a Marriage Link; should not be null if graph is correctly constructed
function findMarriageLabelNode(link) {
if (link.category !== "Marriage") throw new Error("link is not a marriage: " + link);
var labnode = link.labelNodes.first();
if (!labnode) throw new Error("Marriage Link has no label Node " + link);
return labnode;
}
// return the children of a Marriage Link
function findMarriageChildrenNodes(link) {
if (link.category !== "Marriage") throw new Error("link is not a marriage: " + link);
return findMarriageLabelNode(link).findNodesConnected();
}
// Graph modification functions
function addParentsToPerson(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
node.diagram.model.commit(function(m) {
var newfatherdata = {
name: "father of (" + node.data.name + ")",
sex: "M",
attr: ["C", "D"]
};
m.addNodeData(newfatherdata);
var newmotherdata = {
name: "mother of (" + node.data.name + ")",
sex: "F",
attr: ["E", "G"]
};
m.addNodeData(newmotherdata);
var labelnodedata = {
sex: "LinkLabel",
};
m.addNodeData(labelnodedata);
var marriagedata = {
category: "Marriage",
from: m.getKeyForNodeData(newfatherdata),
to: m.getKeyForNodeData(newmotherdata),
labelKeys: [m.getKeyForNodeData(labelnodedata)]
};
m.addLinkData(marriagedata);
var childlinkdata = {
from: m.getKeyForNodeData(labelnodedata),
to: node.key
};
m.addLinkData(childlinkdata);
}, "added parents");
}
function addSpouseToPerson(node) {
if (node.category === "LinkLabel") throw new Error("node is not a person: " + node.key);
node.diagram.model.commit(function(m) {
var newspousedata = {
name: "spouse of " + node.data.name,
sex: (node.category === "M") ? "F" : "M",
attr: ["G", "H"]
};
m.addNodeData(newspousedata);
var labelnodedata = {
sex: "LinkLabel",
};
m.addNodeData(labelnodedata);
var marriagedata = {
category: "Marriage",
from: node.key,
to: m.getKeyForNodeData(newspousedata),
labelKeys: [m.getKeyForNodeData(labelnodedata)]
};
m.addLinkData(marriagedata);
}, "added spouse");
}
function addChildToMarriage(mlink) {
if (mlink.category !== "Marriage") throw new Error("link is not a marriage: " + mlink);
var labnode = findMarriageLabelNode(mlink);
var father = null;
var mother = null;
if (mlink.fromNode.category === "M") mother = mlink.fromNode;
else father = mlink.fromNode;
if (mlink.toNode.category === "M") mother = mlink.toNode;
else father = mlink.toNode;
if (!father || !mother) throw new Error("new child node for marriage link requires two parents " + mlink);
var newdata = null;
mlink.diagram.model.commit(function(m) {
newdata = {
name: "child of (" + father.data.name + ") and (" + mother.data.name + ")",
sex: "M",
attr: ["I", "J"]
};
m.addNodeData(newdata);
var newlink = {
from: labnode.key,
to: m.getKeyForNodeData(newdata)
};
m.addLinkData(newlink);
}, "added child");
return mlink.diagram.findNodeForData(newdata);
}
// A custom layout that shows the two families related to a person's parents
function GenogramLayout() {
go.LayeredDigraphLayout.call(this);
this.initializeOption = go.LayeredDigraphLayout.InitDepthFirstIn;
this.spouseSpacing = 30; // minimum space between spouses
}
go.Diagram.inherit(GenogramLayout, go.LayeredDigraphLayout);
GenogramLayout.prototype.makeNetwork = function(coll) {
// generate LayoutEdges for each parent-child Link
var net = this.createNetwork();
if (coll instanceof go.Diagram) {
this.add(net, coll.nodes, true);
this.add(net, coll.links, true);
} else if (coll instanceof go.Group) {
this.add(net, coll.memberParts, false);
} else if (coll.iterator) {
this.add(net, coll.iterator, false);
}
return net;
};
// internal method for creating LayeredDigraphNetwork where husband/wife pairs are represented
// by a single LayeredDigraphVertex corresponding to the label Node on the marriage Link
GenogramLayout.prototype.add = function(net, coll, nonmemberonly) {
var multiSpousePeople = new go.Set();
// consider all Nodes in the given collection
var it = coll.iterator;
while (it.next()) {
var node = it.value;
if (!(node instanceof go.Node)) continue;
if (!node.isLayoutPositioned || !node.isVisible()) continue;
if (nonmemberonly && node.containingGroup !== null) continue;
// if it's an unmarried Node, or if it's a Link Label Node, create a LayoutVertex for it
if (node.isLinkLabel) {
// get marriage Link
var link = node.labeledLink;
var spouseA = link.fromNode;
var spouseB = link.toNode;
// create vertex representing both husband and wife
var vertex = net.addNode(node);
// now define the vertex size to be big enough to hold both spouses
vertex.width = spouseA.actualBounds.width + 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 marriage link
// assume a marriage Link has a label Node
var marriages = 0;
node.linksConnected.each(function(l) { if (l.isLabeledLink) marriages++; });
if (marriages === 0) {
var vertex = net.addNode(node);
} else if (marriages > 1) {
multiSpousePeople.add(node);
}
}
}
// now do all Links
it.reset();
while (it.next()) {
var link = it.value;
if (!(link instanceof go.Link)) continue;
if (!link.isLayoutPositioned || !link.isVisible()) continue;
if (nonmemberonly && link.containingGroup !== null) continue;
// if it's a parent-child link, add a LayoutEdge for it
if (!link.isLabeledLink) {
var parent = net.findVertex(link.fromNode); // should be a label node
var child = net.findVertex(link.toNode);
if (child !== null) { // an unmarried child
net.linkVertexes(parent, child, link);
} else { // a married child
link.toNode.linksConnected.each(function(l) {
if (!l.isLabeledLink) return; // if it has no label node, it's a parent-child link
// found the Marriage Link, now get its label Node
var mlab = l.labelNodes.first();
// parent-child link should connect with the label node,
// so the LayoutEdge should connect with the LayoutVertex representing the label node
var mlabvert = net.findVertex(mlab);
if (mlabvert !== null) {
net.linkVertexes(parent, mlabvert, link);
}
});
}
}
}
while (multiSpousePeople.count > 0) {
// find all collections of people that are indirectly married to each other
var node = multiSpousePeople.first();
var cohort = new go.Set();
this.extendCohort(cohort, node);
// then encourage them all to be the same generation by connecting them all with a common vertex
var dummyvert = net.createVertex();
net.addVertex(dummyvert);
var marriages = new go.Set();
cohort.each(function(n) {
n.linksConnected.each(function(l) {
marriages.add(l);
})
});
marriages.each(function(link) {
// find the vertex for the marriage link (i.e. for the label node)
var mlab = link.labelNodes.first()
var v = net.findVertex(mlab);
if (v !== null) {
net.linkVertexes(dummyvert, 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
GenogramLayout.prototype.extendCohort = function(coll, node) {
if (coll.has(node)) return;
coll.add(node);
var lay = this;
node.linksConnected.each(function(l) {
if (l.isLabeledLink) { // if it's a marriage link, continue with both spouses
lay.extendCohort(coll, l.fromNode);
lay.extendCohort(coll, l.toNode);
}
});
};
GenogramLayout.prototype.assignLayers = function() {
go.LayeredDigraphLayout.prototype.assignLayers.call(this);
var horiz = this.direction == 0.0 || this.direction == 180.0;
// for every vertex, record the maximum vertex width or height for the vertex's layer
var maxsizes = [];
this.network.vertexes.each(function(v) {
var lay = v.layer;
var max = maxsizes[lay];
if (max === undefined) max = 0;
var sz = (horiz ? v.width : v.height);
if (sz > max) maxsizes[lay] = sz;
});
// now make sure every vertex has the maximum width or height according to which layer it is in,
// and aligned on the left (if horizontal) or the top (if vertical)
this.network.vertexes.each(function(v) {
var lay = v.layer;
var max = maxsizes[lay];
if (horiz) {
v.focus = new go.Point(0, v.height / 2);
v.width = max;
} else {
v.focus = new go.Point(v.width / 2, 0);
v.height = max;
}
});
// from now on, the LayeredDigraphLayout will think that the Node is bigger than it really is
// (other than the ones that are the widest or tallest in their respective layer).
};
GenogramLayout.prototype.commitNodes = function() {
go.LayeredDigraphLayout.prototype.commitNodes.call(this);
// position regular nodes
this.network.vertexes.each(function(v) {
if (v.node !== null && !v.node.isLinkLabel) {
v.node.position = new go.Point(v.x, v.y);
}
});
// position the spouses of each marriage vertex
var layout = this;
this.network.vertexes.each(function(v) {
if (v.node === null) return;
if (!v.node.isLinkLabel) return;
var labnode = v.node;
var lablink = labnode.labeledLink;
// In case the spouses are not actually moved, we need to have the marriage link
// position the label node, because LayoutVertex.commit() was called above on these vertexes.
// Alternatively we could override LayoutVetex.commit to be a no-op for label node vertexes.
lablink.invalidateRoute();
var spouseA = lablink.fromNode;
var spouseB = lablink.toNode;
// prefer fathers on the left, mothers on the right
if (spouseA.data.sex === "F") { // sex is female
var temp = spouseA;
spouseA = spouseB;
spouseB = temp;
}
// see if the parents are on the desired sides, to avoid a link crossing
var aParentsNode = layout.findParentsMarriageLabelNode(spouseA);
var bParentsNode = layout.findParentsMarriageLabelNode(spouseB);
if (aParentsNode !== null && bParentsNode !== null && aParentsNode.position.x > bParentsNode.position.x) {
// swap the spouses
var temp = spouseA;
spouseA = spouseB;
spouseB = temp;
}
spouseA.position = new go.Point(v.x, v.y);
spouseB.position = new go.Point(v.x + spouseA.actualBounds.width + layout.spouseSpacing, v.y);
if (spouseA.opacity === 0) {
var pos = new go.Point(v.centerX - spouseA.actualBounds.width / 2, v.y);
spouseA.position = pos;
spouseB.position = pos;
} else if (spouseB.opacity === 0) {
var pos = new go.Point(v.centerX - spouseB.actualBounds.width / 2, v.y);
spouseA.position = pos;
spouseB.position = pos;
}
});
// position only-child nodes to be under the marriage label node
this.network.vertexes.each(function(v) {
if (v.node === null || v.node.linksConnected.count > 1) return;
var mnode = layout.findParentsMarriageLabelNode(v.node);
if (mnode !== null && mnode.linksConnected.count === 1) { // if only one child
var mvert = layout.network.findVertex(mnode);
var newbnds = v.node.actualBounds.copy();
newbnds.x = mvert.centerX - v.node.actualBounds.width / 2;
// see if there's any empty space at the horizontal mid-point in that layer
var overlaps = layout.diagram.findObjectsIn(newbnds, function(x) { return x.part; }, function(p) { return p !== v.node; }, true);
if (overlaps.count === 0) {
v.node.move(newbnds.position);
}
}
});
};
GenogramLayout.prototype.findParentsMarriageLabelNode = function(node) {
var it = node.findNodesInto();
while (it.next()) {
var n = it.value;
if (n.isLinkLabel) return n;
}
return null;
};
// end GenogramLayout class
// Show the diagram's model in JSON format that the user may edit
function save() {
document.getElementById("mySavedModel").value = myDiagram.model.toJson();
myDiagram.isModified = false;
}
function load() {
myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
}
</script>
</head>
<body onload="init()">
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></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.</p>
<p>
Each person (Node), when selected, shows buttons to add parents (if the person does not already have parents)
and to "marry" another person. In this app, a "marriage" is not a legal relationship but a relationship that
issued 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.
</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.
The overridden <b>add</b> function allows husband/wife pairs to be represented by a single <a>LayeredDigraphVertex</a>.
</p>
<button id="SaveButton" onclick="save()">Save</button>
<button onclick="load()">Load</button>
Diagram Model saved in JSON format:
<textarea id="mySavedModel" style="width:100%;height:300px">
</textarea>
</div>
</body>
</html>