AtActionPark
AtActionPark

Reputation: 127

Build a tree from a list of nodes and links

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.

Example input

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},
];

Expected output

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:[]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

Attempt

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)

Question

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

Answers (2)

Ajax1234
Ajax1234

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

trincot
trincot

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

Related Questions