I have constructed a tree graph using html5, css3 but the nodes are static. Now I wanna make that graph dynamic. Here dynamic means suppose the node count increases and there are multiple children, now the graph will be generated with the new nodes and children.
1 Answer
This is just an example using vanilla JavaScript. I've taken a lot of inspiration from d3, including how to store the data as a self-referencing tree of nodes and how to traverse the tree breadth-first.
I've tried to comment the code as well as possible, I hope this gives you some inspiration. I'd try to improve the positioning of the labels a bit, and/or increase the margins so they're better visible.
const data = [{
id: 0,
label: "Command Sequence Starting",
},
{
id: 1,
parents: [0],
label: "W_SCMadl_refresh",
status: "done",
},
{
id: 2,
parents: [1],
label: "W_adl_photo",
status: "done",
},
{
id: 3,
parents: [2],
label: "W_adl_collect",
status: "done",
},
{
id: 4,
parents: [3],
label: "W_adl_collect_cr",
status: "done",
},
{
id: 5,
parents: [4],
label: "W_adl_sync",
status: "aborted",
},
{
id: 6,
parents: [5],
label: "W_adl_attach",
status: "waiting",
},
{
id: 7,
parents: [6],
label: "W_adl_attach",
status: "waiting",
},
{
id: 8,
parents: [7],
label: "W_adl_publish",
status: "waiting",
},
{
id: 9,
parents: [8],
label: "W_adl_ds_ws",
status: "waiting",
},
{
id: 10,
parents: [9],
label: "W64_Shared_Preq_mkdir",
status: "waiting",
},
{
id: 11,
parents: [10, 12],
label: "W64_mkCopyPreq",
status: "waiting",
},
{
id: 12,
parents: [0],
label: "WIN64_MCCMon",
status: "done",
},
];
// Make the data array a self-referencing tree, where each node has pointers to their
// parents and their children nodes
data
.filter(d => d.parents !== undefined)
.forEach(d => {
d.parents = data.filter(p => d.parents.includes(p.id));
d.parents.forEach(p => {
if (p.children === undefined) {
p.children = [];
}
p.children.push(d);
});
});
const root = data.find(d => d.parents === undefined);
// Breadth first traversal of the tree, excuting `fn` for every node
const forEach = (root, fn) => {
const stack = [root];
while (stack.length) {
const current = stack.shift();
if (current.children) {
stack.push(...current.children);
}
fn(current);
}
};
const svg = document.querySelector(".mv-sequence svg");
const margin = {
top: 20,
bottom: 20,
right: 20,
left: 20,
};
const width = +svg.getAttribute("width") - margin.left - margin.right;
const stepHeight = 40;
const namespace = "http://www.w3.org/2000/svg";
const gContainer = document.createElementNS(namespace, "g");
gContainer.setAttribute("transform", `translate(${margin.left},${margin.top})`);
svg.appendChild(gContainer);
const linksContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(linksContainer);
const nodesContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(nodesContainer);
// Give node a level. First complete this loop, then start drawing, because we want to
// be robust against not all parents having a level yet
forEach(
root,
d => {
if (d === root) {
d.level = 0;
return;
}
d.level = Math.max(...d.parents.map(p => p.level)) + 1;
}
);
forEach(
root,
d => {
// Position the node based on the number of siblings.
const siblings = data.filter(n => n.level === d.level);
// If the node is an only child. The root should be in the centre,
// any other node should be in the average of it's parents
if (siblings.length === 1) {
if (d.parents === undefined) {
d.x = width / 2;
} else {
d.x = d.parents.map(p => p.x).reduce((s, v) => s + v, 0) / d.parents.length;
}
return;
}
// Otherwise, divide the space evenly for all sibling nodes
const siblingIndex = siblings.indexOf(d);
const stepWidth = width / (siblings.length - 1);
if (siblings.length % 2 === 0) {
// Even number of siblings
d.x = stepWidth * siblingIndex;
} else {
// Odd number of siblings, the center one must be in the middle
d.x = width / 2 + stepWidth * (siblingIndex - (siblings.length - 1) / 2);
}
}
);
forEach(
root,
d => {
// Append a circle and `text` for all new nodes
d.y = d.level * stepHeight;
const nodeContainer = document.createElementNS(namespace, "g");
nodeContainer.setAttribute("transform", `translate(${d.x}, ${d.y})`);
nodeContainer.classList.add("mv-command", d.status);
nodesContainer.appendChild(nodeContainer);
const circle = document.createElementNS(namespace, "circle");
circle.setAttribute("r", stepHeight / 4);
nodeContainer.appendChild(circle);
const label = document.createElementNS(namespace, "text");
label.setAttribute("dx", stepHeight / 4 + 5);
label.textContent = d.label;
nodeContainer.appendChild(label);
// Append a link from every parent to this node
(d.parents || []).forEach(p => {
const link = document.createElementNS(namespace, "path");
let path = `M${p.x} ${p.y}`;
let dx = d.x - p.x;
let dy = d.y - p.y;
if (dy > stepHeight) {
// Move down to the level of the child node
path += `v${dy - stepHeight}`;
dy = stepHeight;
}
path += `s0 ${dy / 2}, ${dx / 2} ${dy / 2}`;
path += `s${dx / 2} 0, ${dx / 2} ${dy / 2}`;
link.setAttribute("d", path)
linksContainer.appendChild(link);
})
}
);
// Finally, set the height to fit perfectly
svg.setAttribute("height", Math.max(...data.map(d => d.level)) * stepHeight + margin.top + margin.bottom);
.mv-command.done {
fill: #477738;
}
.mv-command.aborted {
fill: #844138;
}
.mv-command.waiting {
fill: #808080;
}
.mv-command.disabled {
fill: #80808080;
}
.mv-command.running {
fill: #005686;
animation: mymove 2s infinite;
}
.mv-command>text {
dominant-baseline: middle;
font-size: 12px;
}
path {
fill: none;
stroke: darkgreen;
stroke-width: 3px;
}
<div class="mv-sequence">
<svg width="200"></svg>
</div>
1 Comment
thelonelyCoder
Really liked the way you demonstrate it 🙌🙌...amazing @Reben_Helsloot