Reputation: 127
I have a graphical editor that allows to draw a d3 force directed graph (a questionnaire with multiple choices and outcome, for example, if you answer yes to question 1, go to question 2, else go to question 1b).
It has a root node, and nodes can have multiple parents, and multiple children.
Internally, it is represented as a list of nodes and a list of links.
const nodes = [
{id:1, data: {display: "start"}},
{id:2, data: {display: "question 1"}},
{id:3, data: {display: "question 2"}},
{id:4, data: {display: "question 1b"}},
{id:5, data: {display: "End"}},
];
const links = [
{source:1,target:2},
{source:2,target:3},
{source:2,target:4},
{source:4,target:3},
{source:3,target:5},
];
I need to generate a tree showing all possible paths, like so:
{
Id:1
Data: {display: "start"},
Children: [
{
Id:2,
Data: {display: "question 1"},
Children:[
{
Id:3,
Data: {display: "question 2"},
Children:[
{
Id:5
Data: "End",
Children:[]
}
]
},
{
Id:4,
Data: {display: "question 1 b"},
Children:[
{
Id:3,
Data: {display: "question 2"},
Children:[
{
Id:5
Data: "End",
Children:[]
}
]
}
]
}
]
}
]
}
Here is my (very naive) try at it so far:
const nodes = [
{id:1, data: {display: "start"}},
{id:2, data: {display: "question 1"}},
{id:3, data: {display: "question 2"}},
{id:4, data: {display: "question 1b"}},
{id:5, data: {display: "End"}},
];
const links = [
{source:1,target:2},
{source:2,target:3},
{source:2,target:4},
{source:4,target:3},
{source:3,target:5},
]
const startNode = nodes.find(n => n.id == 1);
const jsonTree = {
id: startNode.id,
data: startNode.data,
children: [],
}
const nodeList = [];
nodeList.push(startNode);
while (nodeList.length > 0) {
const currentNode = nodeList.shift();
if (!currentNode) continue;
currentNode.visited = true;
const childNodes = this.findChildrenOfNode(currentNode, nodes, links);
const position = this.findPositionInJson(jsonTree, currentNode);
position.data = currentNode.data;
position.id = currentNode.id
childNodes.forEach((c) => {
if (!c.visited) {
c.visited = true;
nodeList.push(c);
}
const child = {
id:c.id,
data: c.data,
children: [],
};
position.children.push(child);
});
}
function findChildrenOfNode(node, nodes, links) {
const childLinksTargetIds = [];
links.forEach((l) => {
if (node.id === l.source) childLinksTargetIds.push(l.target);
});
return nodes.filter((n) => childLinksTargetIds.includes(n.id));
}
function findPositionInJson(jsonObject, node) {
if (!jsonObject) return null;
if (jsonObject.hasOwnProperty('data')) {
if (jsonObject.id === node.id) return jsonObject;
}
const keys = Object.keys(jsonObject);
for (let i = 0; i < keys.length; i += 1) {
const key = jsonObject[keys[i]];
if (typeof key === 'object') {
const o = this.findPositionInJson(key, node);
if (o != null) return o;
}
}
return null;
}
console.log(jsonTree)
My code will populate the direct children of "1b", but not recursively (The child of "1b" is "2", whose child should be "end")
I suppose there is an algorithm for doing exactly what I'm trying to do. How can it be done?
Upvotes: 1
Views: 872
Reputation: 71451
You can use recursion for a shorter solution:
const nodes = [
{id:1, data: {display: "start"}},
{id:2, data: {display: "question 1"}},
{id:3, data: {display: "question 2"}},
{id:4, data: {display: "question 1b"}},
{id:5, data: {display: "End"}},
];
const links = [
{source:1,target:2},
{source:2,target:3},
{source:2,target:4},
{source:4,target:3},
{source:3,target:5},
];
var nds = Object.fromEntries(nodes.map(x => [x.id, x.data]))
function to_tree(n = 1){
return {Id:n, data:nds[n], children:links.filter(x => x.source === n).map(({source:_, target:t}) => to_tree(t))}
}
console.log(to_tree())
Upvotes: 1
Reputation: 350310
First create an map for mapping id
values to the corresponding objects. Add to each of those objects an empty children
array property. Then populate the children
arrays from the links
information, each time looking up the source
and target
identifiers in the map. Finally return the map entry for id 1:
function buildTree(nodes, links) {
let map = new Map(nodes.map(o => [o.id, {...o, children: []}]));
for (let {source, target} of links) map.get(source).children.push(map.get(target));
return map.get(1);
}
// Demo
const nodes = [
{id:1, data: {display: "start"}},
{id:2, data: {display: "question 1"}},
{id:3, data: {display: "question 2"}},
{id:4, data: {display: "question 1b"}},
{id:5, data: {display: "End"}},
];
const links = [
{source:1,target:2},
{source:2,target:3},
{source:2,target:4},
{source:4,target:3},
{source:3,target:5},
]
console.log(buildTree(nodes, links));
Upvotes: 1