how to make such a mind map graphic
Is the graph always tree-structured? That is, no node will have more than one link coming into it, and there are no cycles.
That should be straight-forward to implement. I can try that later today or tomorrow when I have more time.
Yes, all correct.
Thank you. Highly anticipated…
Here you go:
[EDIT: removed duplication of data.color
property – now only on group data, not on Trunk node data too.
<!DOCTYPE html>
<html>
<head>
<title>Alternating Tree Layout</title>
<!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
class AlternatingTreeLayout extends go.TreeLayout {
constructor(init) {
super();
if (init) Object.assign(this, init);
}
makeNetwork(coll) {
var net = super.makeNetwork(coll);
net.vertexes.each(v => {
const grp = v.node;
if (grp instanceof go.Group) {
const nd = grp.memberParts.first();
if (!(nd instanceof go.Node)) return;
const rt = nd.findTreeRoot();
const h2 = rt.actualBounds.height/2;
v.focusY = grp.data.up ? v.height-h2 : h2;
v.width -= this.layerSpacing;
}
});
return net;
}
}
const myDiagram =
new go.Diagram("myDiagramDiv", {
layout: new AlternatingTreeLayout(),
"draggingTool.dragsTree": true,
"undoManager.isEnabled": true
});
// the template for simple nodes in the subtrees
myDiagram.nodeTemplate =
new go.Node()
.add(
new go.TextBlock({ margin: 2 })
.bind("text")
);
// the template for "Trunk" nodes, which can either be top-level or can be a member of a group
myDiagram.nodeTemplateMap.add("T",
new go.Node("Auto", { height: 30 })
.bindObject("width", "", n => n.isTopLevel ? 60 : 30)
.add(
new go.Shape("Circle", { fill: "white", strokeWidth: 2 })
.bind("fill")
.bindObject("stroke", "", n => (n.isTopLevel ? n.data.color : n.containingGroup.data.color) || "black")
.bindObject("figure", "", n => n.isTopLevel ? "RoundedRectangle" : "Circle"),
new go.TextBlock({ textAlign: "center" })
.bind("text")
.bind("stroke", "fill", c => go.Brush.isDark(c) ? "white" : "black")
));
// get the color from the containing group, or else from the toNode's containing group
function computeColor(link) {
const grp = link.containingGroup;
if (grp) return grp.data.color || "black";
return link.toNode?.containingGroup?.data.color || "black";
}
myDiagram.linkTemplate =
new go.Link({ routing: go.Link.Orthogonal })
.add(
new go.Shape({ strokeWidth: 2 })
.bindObject("stroke", "", computeColor)
);
// dynamically produce a TreeLayout for subtrees according to whether it's upward or downward
function makeLayout(up) {
const lay = new go.TreeLayout({
treeStyle: go.TreeStyle.RootOnly,
layerSpacing: 0,
nodeSpacing: 5,
nodeIndent: 35, // should be greater than the root node's actualBounds.height
alignment: go.TreeAlignment.Start,
portSpot: go.Spot.Bottom,
alternateLayerSpacing: 30,
alternateNodeSpacing: 5
});
if (up) {
lay.alignment = go.TreeAlignment.End;
lay.portSpot = go.Spot.Top;
}
return lay;
}
// groups have no visual rendering -- just a layout
myDiagram.groupTemplate =
new go.Group({ layout: makeLayout(false) })
.bind("layout", "up", makeLayout)
.add(new go.Placeholder())
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Start", text: "Start", fill: "indigo", category: "T" },
{ key: "A", isGroup: true, up: false, color: "red" },
{ key: 1, group: "A", text: "A", category: "T" },
{ key: 2, group: "A", text: "A 1" },
{ key: 3, group: "A", text: "A 2" },
{ key: 4, group: "A", text: "A 2 1" },
{ key: 5, group: "A", text: "A 2 2" },
{ key: "B", isGroup: true, up: true, color: "orange" },
{ key: 11, group: "B", text: "B", category: "T" },
{ key: 12, group: "B", text: "B 1" },
{ key: 13, group: "B", text: "B 2" },
{ key: 14, group: "B", text: "B 2 1" },
{ key: 15, group: "B", text: "B 2 2" },
{ key: 16, group: "B", text: "B 2 3" },
{ key: "C", isGroup: true, up: false, color: "green" },
{ key: 21, group: "C", text: "C", category: "T" },
{ key: 22, group: "C", text: "C 1" },
{ key: 23, group: "C", text: "C 2" },
{ key: 24, group: "C", text: "C 2 1" },
{ key: 25, group: "C", text: "C 2 2" },
{ key: 26, group: "C", text: "C 3" },
{ key: "End", text: "End", color: "slateblue", category: "T" },
],
[
{ from: "Start", to: 1 },
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 3, to: 4 },
{ from: 3, to: 5 },
{ from: 1, to: 11 },
{ from: 11, to: 12 },
{ from: 11, to: 13 },
{ from: 13, to: 14 },
{ from: 13, to: 15 },
{ from: 13, to: 16 },
{ from: 11, to: 21 },
{ from: 21, to: 22 },
{ from: 21, to: 23 },
{ from: 23, to: 24 },
{ from: 23, to: 25 },
{ from: 21, to: 26 },
{ from: 21, to: "End" },
]);
</script>
</body>
</html>
Thank you very much!!! you have solved my problem for the past few days.
Let me study digestion.
I have updated the code and added some comments.
I have added a new category of link template, “E” (meaning “Extra”), for that backwards-looking dashed link.
Note that groups with data.up
=== null have a regular TreeLayout and are not positioned by the AlternatingTreeLayout as part of its tree.
<!DOCTYPE html>
<html>
<head>
<title>Alternating Tree Layout</title>
<!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<textarea id="mySavedModel" style="width:100%;height:450px"></textarea>
<script src="https://cdn.jsdelivr.net/npm/gojs/release/go-debug.js"></script>
<script id="code">
class AlternatingTreeLayout extends go.TreeLayout {
constructor(init) {
super();
if (init) Object.assign(this, init);
}
makeNetwork(coll) {
var net = super.makeNetwork(coll);
net.vertexes.each(v => {
const grp = v.node;
if (grp instanceof go.Group) {
const nd = grp.memberParts.first();
if (!(nd instanceof go.Node)) return;
const rt = nd.findTreeRoot();
const h2 = rt.actualBounds.height/2;
v.focusY = grp.data.up ? v.height-h2 : h2;
v.width -= this.layerSpacing;
}
});
return net;
}
}
const myDiagram =
new go.Diagram("myDiagramDiv", {
layout: new AlternatingTreeLayout(),
"draggingTool.dragsTree": true,
"undoManager.isEnabled": true,
"ModelChanged": e => { // just for demonstration purposes,
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
}
});
// the template for simple nodes in the subtrees
myDiagram.nodeTemplate =
new go.Node()
.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringifyFixed(1))
.add(
new go.TextBlock({ margin: 2 })
.bind("text")
);
// the template for "Trunk" nodes, which can either be top-level or can be a member of a group
myDiagram.nodeTemplateMap.add("T",
new go.Node("Auto", { height: 30 })
.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringifyFixed(1))
.bindObject("width", "", n => n.isTopLevel ? 60 : 30)
.add(
new go.Shape("Circle", { fill: "white", strokeWidth: 2 })
.bind("fill")
.bindObject("stroke", "", n => (n.isTopLevel ? n.data.color : n.containingGroup.data.color) || "black")
.bindObject("figure", "", n => n.isTopLevel ? "RoundedRectangle" : "Circle"),
new go.TextBlock({ textAlign: "center" })
.bind("text")
.bind("stroke", "fill", c => go.Brush.isDark(c) ? "white" : "black")
));
// get the color from the containing group, or else from the toNode's containing group
function computeColor(link) {
const grp = link.containingGroup;
if (grp) return grp.data.color || "black";
return link.toNode?.containingGroup?.data.color || "black";
}
myDiagram.linkTemplate =
new go.Link({ routing: go.Link.Orthogonal })
.add(
new go.Shape({ strokeWidth: 2 })
.bindObject("stroke", "", computeColor)
);
myDiagram.linkTemplateMap.add("E",
new go.Link({
isLayoutPositioned: false,
curve: go.Curve.Bezier,
fromSpot: go.Spot.None, toSpot: go.Spot.None
})
.add(
new go.Shape({ strokeWidth: 2, strokeDashArray: [6, 3] })
.bindObject("stroke", "", link => link.toNode.data.fill),
new go.Shape({ fromArrow: "Backward", strokeWidth: 0 })
.bindObject("fill", "", link => link.toNode.data.fill)
)
);
// dynamically produce a TreeLayout for subtrees according to whether it's upward or downward
// null means neither upwards nor downwards
function makeLayout(up) {
if (up === null) return new go.TreeLayout({
layerSpacing: 30,
nodeSpacing: 5
});
const lay = new go.TreeLayout({
treeStyle: go.TreeStyle.RootOnly,
layerSpacing: 0,
nodeSpacing: 5,
nodeIndent: 35, // should be greater than the root node's actualBounds.height
alignment: go.TreeAlignment.Start,
portSpot: go.Spot.Bottom,
alternateLayerSpacing: 30,
alternateNodeSpacing: 5
});
if (up) {
lay.alignment = go.TreeAlignment.End;
lay.portSpot = go.Spot.Top;
}
return lay;
}
// groups have no visual rendering -- just a layout
myDiagram.groupTemplate =
new go.Group({ layout: makeLayout(null) })
.bindTwoWay("location", "loc", go.Point.parse, go.Point.stringifyFixed(1))
.bind("layout", "up", makeLayout)
.bind("isLayoutPositioned", "up", up => up !== null)
.add(new go.Placeholder())
myDiagram.model = new go.GraphLinksModel(
[
{ key: "Start", text: "Start", fill: "indigo", category: "T" },
{ key: "A", isGroup: true, up: false, color: "red" },
{ key: 1, group: "A", text: "A", category: "T" },
{ key: 2, group: "A", text: "A 1" },
{ key: 3, group: "A", text: "A 2" },
{ key: 4, group: "A", text: "A 2 1" },
{ key: 5, group: "A", text: "A 2 2" },
{ key: "B", isGroup: true, up: true, color: "orange" },
{ key: 11, group: "B", text: "B", category: "T" },
{ key: 12, group: "B", text: "B 1" },
{ key: 13, group: "B", text: "B 2" },
{ key: 14, group: "B", text: "B 2 1" },
{ key: 15, group: "B", text: "B 2 2" },
{ key: 16, group: "B", text: "B 2 3" },
{ key: "C", isGroup: true, up: false, color: "green" },
{ key: 21, group: "C", text: "C", category: "T" },
{ key: 22, group: "C", text: "C 1" },
{ key: 23, group: "C", text: "C 2" },
{ key: 24, group: "C", text: "C 2 1" },
{ key: 25, group: "C", text: "C 2 2" },
{ key: 26, group: "C", text: "C 3" },
{ key: "End", text: "End", color: "slateblue", category: "T" },
{ key: "D", isGroup: true, up: null, loc: "334 -59" },
{ key: 31, group: "D", text: "D", category: "T", fill: "green", loc: "334 -47.35" },
{ key: 32, group: "D", text: "D 1", loc: "377 -59" },
{ key: 33, group: "D", text: "D 2", loc: "377 -35.7" },
{ key: 34, group: "D", text: "D 1 1", loc: "431 -59" },
],
[
{ from: "Start", to: 1 },
{ from: 1, to: 2 },
{ from: 1, to: 3 },
{ from: 3, to: 4 },
{ from: 3, to: 5 },
{ from: 1, to: 11 },
{ from: 11, to: 12 },
{ from: 11, to: 13 },
{ from: 13, to: 14 },
{ from: 13, to: 15 },
{ from: 13, to: 16 },
{ from: 11, to: 21 },
{ from: 21, to: 22 },
{ from: 21, to: 23 },
{ from: 23, to: 24 },
{ from: 23, to: 25 },
{ from: 21, to: 26 },
{ from: 21, to: "End" },
{ from: 12, to: 31, category: "E" },
{ from: 31, to: 32 },
{ from: 31, to: 33 },
{ from: 32, to: 34 },
]);
</script>
</body>
</html>
thank you! I’ll give it a try.