Tree Structure from Adjacency List - javascript

I am trying to generate a hierarchical tree object from a flat array with parent IDs.
// `parent` represents an ID and not the nesting level.
var flat = [
{ id: 1, name: "Business", parent: 0 },
{ id: 2, name: "Management", parent: 1 },
{ id: 3, name: "Leadership", parent: 2 },
{ id: 4, name: "Finance", parent: 1 },
{ id: 5, name: "Fiction", parent: 0 },
{ id: 6, name: "Accounting", parent: 1 },
{ id: 7, name: "Project Management", parent: 2 }
];
The final tree object should look as follows:
{
id: 1,
name: "Business",
children: [
{
id: 2,
name: "Management",
children: [
{ id: 3, name: "Leadership" },
{ id: 7, name: "Project Management" }
]
}
// [...]
]
}
// [...]
You can see my current work on this fiddle, but it only works on the first two levels.
I thought about collecting the orphans (objects from flat without a parent in tree yet) and then looping over them again to see if they now have a parent. But that could mean many loops over the tree object, especially with many categories over multiple levels.
I'm sure there is a more elegant solution.

Looks like you can do this in 2 passes no matter the tree depth:
var flat = [
{ id: 1, name: "Business", parent: 0 },
{ id: 2, name: "Management", parent: 1 },
{ id: 3, name: "Leadership", parent: 2 },
{ id: 4, name: "Finance", parent: 1 },
{ id: 5, name: "Fiction", parent: 0 },
{ id: 6, name: "Accounting", parent: 1 },
{ id: 7, name: "Project Management", parent: 2 }
];
var nodes = [];
var toplevelNodes = [];
var lookupList = {};
for (var i = 0; i < flat.length; i++) {
var n = {
id: flat[i].id,
name: flat[i].name,
parent_id: ((flat[i].parent == 0) ? null : flat[i].parent),
children: []
};
lookupList[n.id] = n;
nodes.push(n);
if (n.parent_id == null) {
toplevelNodes.push(n);
}
}
for (var i = 0; i < nodes.length; i++) {
var n = nodes[i];
if (!(n.parent_id == null)) {
lookupList[n.parent_id].children = lookupList[n.parent_id].children.concat([n]);
}
}
console.log(toplevelNodes);

Update 2, (six years later)
I happened to stumble across this again and realized how much simpler this can be with a little ES6 destructuring and a straightforward recursion:
const makeForest = (id, xs) =>
xs .filter (({parent}) => parent == id)
.map (({id, parent, ...rest}) => ({id, ...rest, children: makeForest (id, xs)}))
var flat = [{id: 1, name: "Business", parent: 0}, {id: 2, name: "Management", parent: 1}, {id: 3, name: "Leadership", parent: 2}, {id: 4, name: "Finance", parent: 1}, {id: 5, name: "Fiction", parent: 0}, {id: 6, name: "Accounting", parent: 1}, {id: 7, name: "Project Management", parent: 2}];
console .log (
makeForest (0, flat)
)
.as-console-wrapper {min-height: 100% !important; top: 0}
Note the name change. We're not making a tree, but an entire forest of trees, with each direct child of the root its own node. It would be relatively trivial to change this to generate a tree, if you had a structure to use for the root. Also note that this will let us find the tree rooted at any id. It doesn't have to be the overall root.
Performance
This does, as you worried about, have a worst-case performance of O(n^2). The actual run time is something like O(n * p) where p is the number of distinct parent nodes in the list. So we only approach the worst case when we have a tree nearly as deep as the length of the list. I would use something like this unless and until I could show that it was a hot spot in my code. My guess is that I would never find a need to replace it.
Lesson
Never count out recursion. This is significantly simpler than any of the other answers here, including my two versions below.
Update 1
I was not happy with extra complexity in my original solution. I'm adding another version that reduces that complexity. It manages to build the data in a single pass. It also gives one the chance to restructure the records in the trees differently from their original format if necessary. (By default, it only removes the parent node.)
Updated Version
Available on JSFiddle.
var makeTree = (function() {
var defaultClone = function(record) {
var newRecord = JSON.parse(JSON.stringify(record));
delete newRecord.parent;
return newRecord;
};
return function(flat, clone) {
return flat.reduce(function(data, record) {
var oldRecord = data.catalog[record.id];
var newRecord = (clone || defaultClone)(record);
if (oldRecord && oldRecord.children) {
newRecord.children = oldRecord.children;
}
data.catalog[record.id] = newRecord;
if (record.parent) {
var parent = data.catalog[record.parent] =
(data.catalog[record.parent] || {id: record.parent});
(parent.children = parent.children || []).push(newRecord);
} else {
data.tree.push(newRecord);
}
return data;
}, {catalog: {}, tree: []}).tree;
}
}());
Note that this will work regardless of the order of the flat list -- the parent nodes do not have to precede their children -- although there is nothing in here to sort the nodes.
Original Version
My solution (also on JSFiddle):
var makeTree = (function() {
var isArray = function(obj) {return Object.prototype.toString.call(obj) == "[object Array]"; };
var clone = function(obj) {return JSON.parse(JSON.stringify(obj));};
var buildTree = function(catalog, structure, start) {
return (structure[start] || []).map(function(id, index) {
var record = catalog[id];
var keys = structure[start][index];
var children = isArray(keys) ? keys.map(function(key) {
return buildTree(catalog, structure, key);
}) : buildTree(catalog, structure, keys);
if (children.length) {
record.children = children;
}
return record;
})
};
return function(flat) {
var catalog = flat.reduce(function(catalog, record) {
catalog[record.id] = clone(record);
delete(catalog[record.id].parent);
return catalog;
}, {});
var structure = flat.reduce(function(tree, record) {
(tree[record.parent] = tree[record.parent] || []).push(record.id);
return tree;
}, {});
return buildTree(catalog, structure, 0); // this might be oversimplified.
}
}());

Related

Create nested array in Javascript

I'm trying to convert my data from API to my needs. Would like to create a nested array from plain array. I would like to group elements by parentId property, if parentId would not exist I would put it as a root. id value is unique. Like so (raw data):
[
{id: 1, name: 'sensor'},
{id: 2, name: 'sensor', parent: 1},
{id: 3, name: 'sensor', parent: 1},
{id: 4, name: 'sensor', parent: 3},
{id: 5, name: 'sensor'},
{id: 6, name: 'sensor', parent: 5}
]
Converted Data:
const results = [
{
id: 1,
name: "sensor",
children: [
{ id: 2, name: "sensor", parent: 1 },
{
id: 3,
name: "sensor",
parent: 1,
children: [{ id: 4, name: "sensor", parent: 3 }]
}
]
},
{ id: 5, name: "sensor", children: [{ id: 6, name: "sensor", parent: 5 }] }
];
I found this recursive method but it assumes that the parent property exist for every element in an array. In my example root level element would not have parent property.
function getNestedChildren(arr, parent) {
var out = []
for(var i in arr) {
if(arr[i].parent == parent) {
var children = getNestedChildren(arr, arr[i].id)
if(children.length) {
arr[i].children = children
}
out.push(arr[i])
}
}
return out
}
You could take an approach which uses both relations, one from children to parent and vice versa. At the end take the children of the root node.
This approach works for unsorted data.
var data = [{ id: 1, name: 'sensor' }, { id: 2, name: 'sensor', parent: 1 }, { id: 3, name: 'sensor', parent: 1 }, { id: 4, name: 'sensor', parent: 3 }, { id: 5, name: 'sensor' }, { id: 6, name: 'sensor', parent: 5 }],
tree = function (data, root) {
var t = {};
data.forEach(o => {
Object.assign(t[o.id] = t[o.id] || {}, o);
t[o.parent] = t[o.parent] || {};
t[o.parent].children = t[o.parent].children || [];
t[o.parent].children.push(t[o.id]);
});
return t[root].children;
}(data, undefined);
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Given the limited amount of information (will update if more info is added).
The algorithm would be, given an array of data entries, check if entry has a parent and if that parent exists, in which case we want to add the entry to the array of children of the parent entry otherwise add the entry as a parent.
var dataFromAPI = [
{id: 1, name: 'sensor'},
{id: 2, name: 'sensor', parent: 1},
{id: 3, name: 'sensor', parent: 1},
{id: 4, name: 'sensor', parent: 3},
{id: 5, name: 'sensor'},
{id: 6, name: 'sensor', parent: 5}
];
var transformedData = { };
dataFromAPI.forEach(function(entry){
if(entry.parent !== undefined && entry.parent in transformedData) {
transformedData[entry.parent].children.push(entry);
} else {
entry["children"] = [];
transformedData[entry.id] = entry;
}
});
console.log(transformedData);
Please note:
there are a couple assumptions made within this algorithm/code. It assumes that all parent entries exist before their child entry. It also only accounts for two levels (parent or child), meaning a child cannot act as the parent (otherwise you'd have to store the children as an object and not an array)
use a for loop to go through each item.
check if parent property exists (or has value).
If not its a child item. Attach it to appropriate parent.
to check if property exists:
var myProp = 'prop';
if (myObj.hasOwnProperty(myProp)) {
alert("yes, i have that property");
}
try
let h={}, r=[]; // result in r
d.forEach(x=> (h[x.id]=x, x.children=[]) );
d.forEach(x=> x.parent ? h[x.parent].children.push(x) : r.push(x) );
let d = [
{id: 1, name: 'sensor'},
{id: 2, name: 'sensor', parent: 1},
{id: 3, name: 'sensor', parent: 1},
{id: 4, name: 'sensor', parent: 3},
{id: 5, name: 'sensor'},
{id: 6, name: 'sensor', parent: 5}
];
let h = {},r = []; // result in r
d.forEach(x => (h[x.id] = x, x.children = []));
d.forEach(x => x.parent ? h[x.parent].children.push(x) : r.push(x));
console.log(r);
If you want that parent element should not have a parent , than you can manually check and remove fields of an object in an array that has null parent. than you can make a tree... here is an example...
const arr2 = [
{id: 1, name: 'gender', parent: null, parent_id: null },
{id: 2, name: 'material', parent: null, parent_id: null },
{id: 3, name: 'male', parent: 1, parent_name: "gender" },
{ id: 5, name: 'female', parent: 1, parent_name: "gender" },
{ id: 4, name: 'shoe', parent: 3, parent_id: "male"},
]
let newarr=[];
for(let i=0 ; i< arr2.length; i++ ){
if(arr2[i].id){
if(newarr[i] != {} ){
newarr[i] = {}
}
newarr[i].id = arr2[i].id
}
if( arr2[i].name ){
newarr[i].name = arr2[i].name
}
if( arr2[i].parent ){
newarr[i].parent = arr2[i].parent
}
if( arr2[i].parent_id ){
newarr[i].parent_id = arr2[i].parent_id
}
}
console.log('newarr', newarr );
let tree = function (data, root) {
var obj = {};
data.forEach(i => {
Object.assign(obj[i.id] = obj[i.id] || {}, i);
obj[i.parent] = obj[i.parent] || {};
obj[i.parent].children = obj[i.parent].children || [];
obj[i.parent].children.push(obj[i.id]);
});
return obj[root].children;
}(newarr, undefined);
console.log('tree ', tree);

Remove matched object from deeply nested array of objects

I have a data tree structure with children:
{ id: 1,
name: "Dog",
parent_id: null,
children: [
{
id: 2,
name: "Food",
parent_id: 1,
children: []
},
{
id: 3,
name: "Water",
parent_id: 1,
children: [
{
id: 4,
name: "Bowl",
parent_id: 3,
children: []
},
{
id: 5,
name: "Oxygen",
parent_id: 3,
children: []
},
{
id: 6,
name: "Hydrogen",
parent_id: 3,
children: []
}
]
}
]
}
This represents a DOM structure that a user could select an item from to delete by clicking the corresponding button in the DOM.
I have a known text title of the selected item for deletion from the DOM set as the variable clickedTitle. I am having trouble finding an algorithm that will allow me to delete the correct object data from the deeply nested tree.
Here is my code:
function askUserForDeleteConfirmation(e) {
const okToDelete = confirm( 'Are you sure you want to delete the item and all of its sub items?' );
if(!okToDelete) {
return;
}
const tree = getTree(); // returns the above data structure
const clickedTitle = getClickedTitle(e); // returns string title of clicked on item from DOM - for example "Dog" or "Bowl"
const updatedTree = removeFromTree(tree, tree, clickedTitle);
return updatedTree;
}
function removeFromTree(curNode, newTree, clickedTitle) {
if(curNode.name === clickedTitle) {
// this correctly finds the matched data item to delete but the next lines don't properly delete it... what to do?
const index = curNode.children.findIndex(child => child.name === clickedTitle);
newTree = curNode.children.slice(index, index + 1);
// TODO - what to do here?
}
for(const node of curNode.children) {
removeFromTree(node, newTree, clickedTitle);
}
return newTree;
}
I have tried to use the info from Removing matched object from array of objects using javascript without success.
If you don't mind modifying the parameter tree in-place, this should do the job. Note that it'll return null if you attempt to remove the root.
const tree = { id: 1, name: "Dog", parent_id: null, children: [ { id: 2, name: "Food", parent_id: 1, children: [] }, { id: 3, name: "Water", parent_id: 1, children: [ { id: 4, name: "Bowl", parent_id: 3, children: [] }, { id: 5, name: "Oxygen", parent_id: 3, children: [] }, { id: 6, name: "Hydrogen", parent_id: 3, children: [] } ] } ] };
const removeFromTree = (root, nameToDelete, parent, idx) => {
if (root.name === nameToDelete) {
if (parent) {
parent.children.splice(idx, 1);
}
else return null;
}
for (const [i, e] of root.children.entries()) {
removeFromTree(e, nameToDelete, root, i);
}
return tree;
};
console.log(removeFromTree(tree, "Oxygen"));
Your current code is very much on the right track. However:
newTree = curNode.children.slice(index, index + 1);
highlights a few issues: we need to manipulate the parent's children array to remove curNode instead of curNode's own children array. I pass parent objects and the child index recursively through the calls, saving the trouble of the linear operation findIndex.
Additionally, slicing from index to index + 1 only extracts one element and doesn't modify curNode.children. It's not obvious how to go about using newArray or returning it through the call stack. splice seems like a more appropriate tool for the task at hand: extracting one element in-place.
Note that this function will delete multiple entries matching nameToDelete.
I like #VictorNascimento's answer, but by applying map then filter, each children list would be iterated twice. Here is an alternative with reduce to avoid that:
function removeFromTree(node, name) {
return node.name == name
? undefined
: {
...node,
children: node.children.reduce(
(children, child) => children.concat(removeFromTree (child, name) || []), [])
}
}
In the case you want a way to remove the items in-place, as #ggorlen proposed, I'd recommend the following solution, that is simpler in my opinion:
function removeFromTree(node, name) {
if (node.name == name) {
node = undefined
} else {
node.children.forEach((child, id) => {
if (!removeFromTree(child, name)) node.children.splice(id, 1)
})
}
return node
}
I've built the algorithm as follows:
function omitNodeWithName(tree, name) {
if (tree.name === name) return undefined;
const children = tree.children.map(child => omitNodeWithName(child, name))
.filter(node => !!node);
return {
...tree,
children
}
}
You can use it to return a new tree without the item:
noHydrogen = omitNodeWithName(tree, "Hydrogen")
If it's ok to use Lodash+Deepdash, then:
let cleaned = _.filterDeep([tree],(item)=>item.name!='Hydrogen',{tree:true});
Here is a Codepen
We use object-scan for many data processing tasks. It's powerful once you wrap your head around it. Here is how you could answer your question
// const objectScan = require('object-scan');
const prune = (name, input) => objectScan(['**[*]'], {
rtn: 'bool',
abort: true,
filterFn: ({ value, parent, property }) => {
if (value.name === name) {
parent.splice(property, 1);
return true;
}
return false;
}
})(input);
const obj = { id: 1, name: 'Dog', parent_id: null, children: [{ id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [{ id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] }] }] };
console.log(prune('Oxygen', obj)); // return true iff pruned
// => true
console.log(obj);
// => { id: 1, name: 'Dog', parent_id: null, children: [ { id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [ { id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] } ] } ] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

Recursive iteration over deeply nested object to find parent

I have a data tree structure with children:
{ id: 1,
name: "Dog",
parent_id: null,
children: [
{
id: 2,
name: "Food",
parent_id: 1,
children: []
},
{
id: 3,
name: "Water",
parent_id: 1,
children: [
{
id: 4,
name: "Bowl",
parent_id: 3,
children: []
},
{
id: 5,
name: "Oxygen",
parent_id: 3,
children: []
},
{
id: 6,
name: "Hydrogen",
parent_id: 3,
children: []
}
]
}
]
}
Any child data object can have more children as shown in the above data. This represents a DOM structure that a user could select an item from to add a child to.
I have a known text title of the selected item from the DOM as well as the data the user wants to insert. I am having trouble finding a recursive algorithm that will allow me to add the new data to the correct level of the tree.
Here is a list of me thinking through the problem and trying to pseudo code it:
inputs:
tree (data from above)
parentTitle from clicked item in DOM
outputs:
tree with item inserted
steps:
determine highest used id to know what next unique id is
check current level of data for match with the title of parent
if matched then set id and parent_id in new data and push into children of parent
if no match then check if current level data have children
if current level has children needs to repeat steps 2+ for each until match is found
Here is my code:
function askUserForNewItem(e) {
const tree = getTree(); // returns above tree data structure
const name = prompt( 'Enter new item’s name:' ); // user input to match and insert as new item in tree
const clickedTitle = getClickedTitle(e); // returns string title of clicked on item from DOM - for example "Dog" or "Bowl"
const parent = determineParent(tree, clickedTitle);
const parent_id = parent[0].id;
// TODO - needs to set real unique id (highest unused id)
const newId = 101; // hard coded for now, needs to be dynamic
// TODO - needs to insert into correct level of children array in tree
return tree.children.push({ id: newId, name, emoji, children: [], parent_id: parent_id });
}
function determineParent(tree, clickedTitle) {
if(tree.children.length === 0) {
return false;
}
let treeLevel = tree;
let parent = [];
while(treeLevel.children.length !== 0) {
parent = treeLevel.children.filter(child => child.name === clickedTitle);
if(parent.length !== 0) {
break;
}
else {
// what to do here to make this recursive?
}
}
return parent;
}
So if a user typed "Cat" while clicking the add button for "Dog" then a new object
{
id: 7,
name: "Cat",
parent_id: 1,
children: []
}
Would be inserted into children of the first level "Dog" object in the data tree.
If you want a recursive solution, you should modify the determineParent method so it searches down the tree.
Not sure this is exactly what you are searching for, but i hope you get the general idea
function determineParent(curNode, clickedTitle) {
if(curNode.name===clickedTitle)
return curNode; // found the parent node with the correct clickedTitle
// not found yet, do a recusive search down the tree.
for(const node of curNode.children) {
return determineParent(node,clickedTitle);
}
return null; // not found.
}
the idea is to start at the topmost node (curNode) and first determine if it is the correct parent, if not then take the first children see if it matches and if not search down it's children and so on.
When dealing with recursion it may be necessary to handle the situation where you may experience circular references, consider a scenario where a node has a child that points to the nodes parent or grandparent, the recursive method will run forever (in real life it will run out of stack space and throw an exception).
One way it so include a safeguard counter that is decreased on each recursive call, and then bail out when it reaches zero.
function determineParent(curNode, clickedTitle, safeGuard) {
if(curNode.name===clickedTitle)
return curNode; // found the parent node with the correct clickedTitle
if(safeGuard===0)
return null; // bail out
// not found yet, do a recusive search down the tree.
for(const node of curNode.children) {
return determineParent(node,clickedTitle,--safeGuard);
}
return null; // not found.
}
and then call it like
this.determineParent(tree,"title",100);
to limit the number of searches to 100.
If it's ok to use Lodash+Deepdash, then:
let child = {
name: "Cat",
children: []
};
let maxUsedId=-1;
_.eachDeep([data],(val)=>{
if(val.id>maxUsedId){
maxUsedId = val.id;
}
if(val.name==parentName){
val.children.push(child);
child.parent_id = val.id;
}
},{tree:true});
child.id=maxUsedId+1;
Codepen for this
Not a big fan of reinventing the wheel, especially when it comes to algorithms. Here is how you could use object-scan to solve your problem. We use it for data processing since it is quite powerful once you wrap your head around it
// const objectScan = require('object-scan');
const insert = (tree, parentName, childName) => objectScan(['**.name'], {
abort: true,
rtn: 'bool',
filterFn: ({ value, parent }) => {
if (value === parentName) {
parent.children.push({
id: Math.max(...objectScan(['**.id'], { rtn: 'value' })(tree)) + 1,
name: childName,
parent_id: parent.id,
children: []
});
return true;
}
return false;
}
})(tree);
const data = { id: 1, name: 'Dog', parent_id: null, children: [{ id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [{ id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] }] }] };
console.log(insert(data, 'Dog', 'Cat')); // true iff insert was successful
// => true
console.log(data);
// => { id: 1, name: 'Dog', parent_id: null, children: [ { id: 2, name: 'Food', parent_id: 1, children: [] }, { id: 3, name: 'Water', parent_id: 1, children: [ { id: 4, name: 'Bowl', parent_id: 3, children: [] }, { id: 5, name: 'Oxygen', parent_id: 3, children: [] }, { id: 6, name: 'Hydrogen', parent_id: 3, children: [] } ] }, { id: 7, name: 'Cat', parent_id: 1, children: [] } ] }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.7.1"></script>
Disclaimer: I'm the author of object-scan

How to change an array of objects containing ids and children references into a nested tree structured object?

I would like to create an object with a tree structure from data that looks as follow:
nodes: [
{ name: "Ben", id: 1 next: [2, 3], depth: 0 },
{ name: "Mike", id: 2, next: [4, 5], depth: 1 },
{ name: "Jen", id: 3, next: [6], depth: 1 },
{ name: "Sue", id: 4, next [], depth: 2 },
{ name: "Jeff", id: 5, next: [], depth: 2 },
{ name: "Bob", id: 6, next: [], depth: 3 }
]
The tree like object would look like this:
root:
{ name: "Ben", children:
[
{ name: "Mike", children:
[
{ name: "Sue" },
{ name: "Jeff" }
]
},
{ name: "Jen", children:
[
{ name: "Bob" }
]
}
]
}
I can assign the root and go through the objects in the next array like this:
const root = { name: nodes[0].name };
root.children = [];
nodes[0].next.map(function (next) {
nodes.map((node, i) => {
if (next === node.id) {
root.children.push({name: nodes[i].name})
}
})
});
I'm not sure how to find next for the nodes pushed to the children array. The number of objects in the nodes array may vary, so the depth of children arrays may vary too. How can you create a new children array based on this variable and push the right properties to it?
First pass, map the nodes by id into something resembling the desired output. This has the added benefit of not mutating the original nodes objects
const idMap = nodes.reduce((map, { id, name }) => map.set(id, { name }), new Map())
Then iterate the nodes, reducing that to an array of roots
const roots = nodes.reduce((arr, node) => {
let obj = idMap.get(node.id)
if (node.next && node.next.length) {
obj.children = node.next.map(id => idMap.get(id))
}
if (node.depth === 0) {
arr.push(obj)
}
return arr
}, [])
Then find the first one with children
const root = roots.find(rootNode => rootNode.children)
const nodes = [{"name":"Ben","id":1,"next":[2,3],"depth":0},{"name":"Mike","id":2,"next":[4,5],"depth":1},{"name":"Jen","id":3,"next":[6],"depth":1},{"name":"Sue","id":4,"next":[],"depth":2},{"name":"Jeff","id":5,"next":[],"depth":2},{"name":"Bob","id":6,"next":[],"depth":3}]
const idMap = nodes.reduce((map, { id, name }) => map.set(id, { name }), new Map())
const roots = nodes.reduce((arr, node) => {
let obj = idMap.get(node.id)
if (node.next && node.next.length) {
obj.children = node.next.map(id => idMap.get(id))
}
if (node.depth === 0) {
arr.push(obj)
}
return arr
}, [])
const root = roots.find(rootNode => rootNode.children)
console.info(root)
You can use recursive function and go until next array length is 0 !!
var nodes = [
{ name: "Ben", id: 1, next: [2,3], depth: 0},
{ name: "Mike", id: 2, next: [4,5], depth: 1},
{ name: "Jen", id: 3, next: [6], depth: 1},
{ name: "Sue", id: 4, next: [], depth: 2},
{ name: "Jeff", id: 5, next: [], depth: 2 },
{ name: "Bob", id: 6, next: [], depth: 3 }
];
var root = {};
nodes.forEach(v => {
if(v.depth == 0) { // check root 0
root.name = v.name
if(v.next.length > 0 ) { // check next array has more than 0
root.children = []
findChild(root,v);
}
}
});
function findChild(root,c) {
c.next.forEach(v => {
var child = nodes.find(x => x.id === v)
var next = {name: child.name};
if(child.next.length > 0) {
next.children = [];
findChild(next,child);
}
root.children.push(next); // push to real root
})
}
console.log(root);
You should probably use an id lookup table:
const hash = {};
for(const node of nodes)
hash[node.id] = node;
for(const node of nodes) node.children = node.next.map(id => ((hash[id].parent = node), hash[id]));
const roots = nodes.filter(n => !n.parent);
As there could be multiple roots, this gives an array of roots.

How do I find the path to a deeply nested unique key in an object and assign child objects accordingly?

With a given flat list:
let list = [
{
key: 1,
parent: null,
},
{
key: 2,
parent: 1,
},
{
key: 3,
parent: null,
},
{
key: 4,
parent: 1,
},
{
key: 5,
parent: 2,
}
]
How do I create a nested object like the one below?
let nest = {
children: [
{
key: 1,
children: [
{
key: 2,
children: [
{
key: 5,
children: []
}
]
},
{
key: 4,
children: []
}
]
},
{
key: 3,
children: []
}
]
}
I'm not sure how to approach this. The solution I have in mind would have to iterate over the list over and over again, to check if the object's parent is either null, in which case it gets assigned as a top-level object, or the object's parent already exists, in which case we get the path to the parent, and assign the child to that parent.
P.S.
I don't think this is a duplicate of any of the below
this checks for a key in a flat object.
this doesn't show anything that would return a path, given a unique key.
For building a tree, you could use a single loop approach, by using not only the given key for building a node, but using parent as well for building a node, where the dendency is obviously.
It uses an object where all keys are usesd as reference, like
{
1: {
key: 1,
children: [
{
/**id:4**/
key: 2,
children: [
{
/**id:6**/
key: 5,
children: []
}
]
},
{
/**id:8**/
key: 4,
children: []
}
]
},
2: /**ref:4**/,
3: {
key: 3,
children: []
},
4: /**ref:8**/,
5: /**ref:6**/
}
The main advantage beside the single loop is, it works with unsorted data, because of the structure to use keys and parent information together.
var list = [{ key: 1, parent: null, }, { key: 2, parent: 1, }, { key: 3, parent: null, }, { key: 4, parent: 1, }, { key: 5, parent: 2, }],
tree = function (data, root) {
var r = [], o = {};
data.forEach(function (a) {
var temp = { key: a.key };
temp.children = o[a.key] && o[a.key].children || [];
o[a.key] = temp;
if (a.parent === root) {
r.push(temp);
} else {
o[a.parent] = o[a.parent] || {};
o[a.parent].children = o[a.parent].children || [];
o[a.parent].children.push(temp);
}
});
return r;
}(list, null),
nest = { children: tree };
console.log(nest);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Categories

Resources