While searching a solution for an issue i had about Tree Structure from Adjacency List. i came across an answer that solved it for me but i cannot understand how it's working and i feel like i'm missing some basic understanding about javascript.
The answer by #TreyE in this question gives the next solution for sorting the tree ->
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);
This works just fine but i just cannot understand how all this logic gets ordered under toplevelNodes if the only thing that was pushed to it was the first level elements. The rest is done on nodes and lookuplist. Again i feel like i'm missing some basic understanding of javascript. how does everything sorts into a nice tree of childrens under topLevelNodes array ?
The secret lies in lookupList which is an Object. Items (or live items) stored inside that Object are not deep clones, those are just "pointers" (not exactly to memory addresses like in C), to existing items in memory, as reference. If a node changes (i.e: by adding more children to a Node) that "reference" to that element will be modified as well, since it's actually that same Node.
Right in here:
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]);
}
}
Here's a banally simplified example:
const node_1 = {id: 1, name: "foo", children: []};
const node_2 = {id: 2, name: "bar", children: []};
const lookupList = {};
// store node_1 into lookup - as reference:
lookupList[node_1.id] = node_1;
// (Unrelated to lookupList) add node_2 as child of node_1
node_1.children.push(node_2);
// Let's sniff lookup for node_1 (id: 1)
console.log(lookupList["1"]); // There's also node_2 as child of node_1
console.log(lookupList["1"] === node_1); // true (it's the same Node)
In JS variables that point to object or array hold reference to that object and not a copy of it.
If you have multiple variables pointing to the same object and mutate the object via one variable, the object under the other reference would be mutated as well (because both variables point to the same object):
var a = {hello: 'a'}
var b = a
b.hello = 'b'
console.log(a.hello)
// prints 'b'
The same thing happens in your code: nodes, toplevelNodes, lookupList all hold references to the same node objects inside them, when you mutate one in one place, it's also updated in other places.
Basically this is the line that does the magic:
lookupList[n.parent_id].children = ...
If you are looking for deeper understanding of this topic, please check this section: https://eloquentjavascript.net/04_data.html#h_C3n45IkMhg
lookupList maps ids to their nodes, since thats whats added to it in the first loop: lookupList[n.id] = n
In the second loop, every node that isn't a top level node is added to its parent's children array. Its parent is retrieved looking up the node's parent_id property in lookupList (lookupList[n.parent_id])
Maybe another approach would be more easily understood? This will modify nodes (née flat) in-place, though:
/**
Augment each node in the "nodes" list with a `children` property,
and return the top-level nodes.
*/
function treeify(nodes) {
const children = {};
// Walk through the flat list of nodes once...
nodes.forEach((n) => {
// Retrieve or initialize a list for `n.parent`'s children.
const childList = children[n.parent] || (children[n.parent] = []);
// Add this node there.
childList.push(n);
});
// ... and walk through it again, to assign the new `children` property.
nodes.forEach((n) => {
// Pick each "children" property from the children map,
// or in case there is none, come up with an empty list.
n.children = children[n.id] || [];
});
// Nodes with parent 0 are top-level; return them.
return children[0];
}
const nodes = [
{ 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 },
];
const tree = treeify(nodes);
// Node.js specific printing stuff...
const util = require("util");
console.log(util.inspect(tree, null, { depth: 5 }));
This outputs
[
{
id: 1,
name: 'Business',
parent: 0,
children: [
{
id: 2,
name: 'Management',
parent: 1,
children: [
{ id: 3, name: 'Leadership', parent: 2, children: [] },
{
id: 7,
name: 'Project Management',
parent: 2,
children: []
}
]
},
{ id: 4, name: 'Finance', parent: 1, children: [] },
{ id: 6, name: 'Accounting', parent: 1, children: [] }
]
},
{ id: 5, name: 'Fiction', parent: 0, children: [] }
]
Related
I have an array of topics each linked to a parent topic. So, basically what I want to do is form an array with all the children topics nested under the parent.
So if I had an table of topics looking like this:
id
name
parent_id
1
Topic 1
null
2
Topic 2
1
3
Topic 3
2
4
Topic 4
2
5
Topic 5
4
And if the json array looks like this:
[
{ id: 1, name: "Topic 1", parent_id: null },
{ id: 2, name: "Topic 2", parent_id: 1 },
{ id: 3, name: "Topic 3", parent_id: 2 },
{ id: 4, name: "Topic 4", parent_id: 2 },
{ id: 5, name: "Topic 5", parent_id: 4 },
]
I want the output array to look like this:
[
{
id: 1,
name: "Topic 1",
children: [
{
id: 2,
name: "Topic 2",
children: [
{ id: 3, name: "Topic 3", children: [] },
{
id: 4,
name: "Topic 4",
children: [{ id: 5, name: "Topic 5", children: [] }],
},
],
},
],
},
]
assume nothing, control everything
A tree is a graph with parent->child relationships between the nodes. To construct this graph efficiently, we must first index the flat array of nodes -
{
A => [child, child, child, ...],
B => [child, child, ...],
C => [...],
...
}
The input array does not need to be ordered in a specific way for this technique to work. In fact, it's important that our modules make no assumptions about the order or shape of our data. This puts the caller (you!) in control and ensures the highest degree of flexibility and reusability -
import * as index from "./index.js"
import * as graph from "./graph.js"
const data = [
{ id: 1, name: "Topic 1", parent_id: null },
{ id: 2, name: "Topic 2", parent_id: 1 },
{ id: 3, name: "Topic 3", parent_id: 2 },
{ id: 4, name: "Topic 4", parent_id: 2 },
{ id: 5, name: "Topic 5", parent_id: 4 },
]
const tree =
graph.new(
// create index by foreign key, node.parent_id
index.new(data, node => node.parent_id),
// construct node's children using primary key, node.id
(node, children) => ({...node, children: children(node.id)})
)
console.log(JSON.stringify(tree, null, 2))
[
{
"id": 1,
"name": "Topic 1",
"parent_id": null,
"children": [
{
"id": 2,
"name": "Topic 2",
"parent_id": 1,
"children": [
{
"id": 3,
"name": "Topic 3",
"parent_id": 2,
"children": []
},
{
"id": 4,
"name": "Topic 4",
"parent_id": 2,
"children": [
{
"id": 5,
"name": "Topic 5",
"parent_id": 4,
"children": []
}
]
}
]
}
]
}
]
Did you notice how index.new accepts a function to declare the node's foreign key and graph.new uses the node's primary key? This means you can reuse these modules for nodes of any shape or arrangement, without needing to modify the module's code. Let's review index and graph now -
// index.js
const empty = _ =>
new Map
const update = (t, k, f) =>
t.set(k, f(r.get(k)))
const append = (t, k, v) =>
update(t, k, (a = []) => [...a, v])
const _new = (a = [], f) =>
a.reduce((t, v) => append(t, f(v), v), empty())
export { empty, update, append, new:_new }
// graph.js
const empty = _ =>
{}
const _new = (i, f, root = null) => {
const many = (a = []) =>
a.map(v => one(v))
const one = (v) =>
f(v, next => many(i.get(next)))
return many(i.get(root))
}
exports { empty, new:_new }
Expand and run the code below to verify the result in your own browser -
const callcc = f => {
const box = Symbol()
try { return f(unbox => { throw {box, unbox} }) }
catch (e) { if (e?.box == box) return e.unbox; throw e }
}
// stacksnippets doesn't support modules yet
const module = callcc
const index = module(exports => {
const empty = _ => new Map
const update = (t, k, f) =>
t.set(k, f(t.get(k)))
const append = (t, k, v) =>
update(t, k, (a = []) => [...a, v])
const _new = (a = [], f) =>
a.reduce((t, v) => append(t, f(v), v), empty())
exports({ empty, update, append, new:_new })
})
const graph = module(exports => {
const empty = _ => {}
const _new = (i, f, root = null) => {
const many = (a = []) =>
a.map(v => one(v))
const one = (v) =>
f(v, next => many(i.get(next)))
return many(i.get(root))
}
exports({ empty, new:_new })
})
const data = [
{ id: 1, name: "Topic 1", parent_id: null },
{ id: 2, name: "Topic 2", parent_id: 1 },
{ id: 3, name: "Topic 3", parent_id: 2 },
{ id: 4, name: "Topic 4", parent_id: 2 },
{ id: 5, name: "Topic 5", parent_id: 4 },
]
const tree =
graph.new(
// create index by node.parent_id
index.new(data, node => node.parent_id),
// construct node's children using node.id
(node, children) => ({...node, children: children(node.id)})
)
console.log(JSON.stringify(tree, null, 2))
.as-console-wrapper { min-height: 100%; top: 0 }
Get the IDs that are at the root level, i.e. with no parents. For these IDs, recursively build their grouped data, similar to writing a depth first search recursion.
Here are the steps taken in the implementation below:
Using the given data, store each ID's children to allow for an easy access during recursion. The structure looks like { ID -> Set of its children }. This type of representation of a graph is also known as an Adjacency List.
Obtain IDs that are at the root level, i.e. IDs that don't have any parents. For these IDs, recursively build the grouped data using recursion on their children
(Could be optional depending on the requirements) Keep track of the already processed IDs to avoid reprocessing
// Helper function, returns a map of {ID -> Set of its child IDs}
// that represents the tree structure.
function getAdjListRepr(data) {
const adjListRepr = new Map();
for (const entry of data) {
if (!adjListRepr.has(entry.id)) {
adjListRepr.set(entry.id, new Set());
}
if (entry.parent_id !== null) {
if (!adjListRepr.has(entry.parent_id)) {
adjListRepr.set(entry.parent_id, new Set());
}
adjListRepr.get(entry.parent_id).add(entry.id);
}
}
return adjListRepr;
}
// Helper function, returns the IDs that are at the root level, i.e.
// IDs that don't have a parent.
function getRootIds(data) {
const rootIds = [];
for (const entry of data) {
if (entry.parent_id === null) {
rootIds.push(entry.id);
}
}
return rootIds;
}
// Helper function, creates the grouped data for a specific
// ID using recursion on the child IDs.
function getGroupedDataForId(id, idToData, adjListRepr, alreadyProcessedIds) {
if (!idToData.has(id) || !adjListRepr.has(id) ||
alreadyProcessedIds.has(id)) {
return null;
}
const result = idToData.get(id);
result.children = [];
for (const childId of adjListRepr.get(id)) {
// Recursion
const childIdData = getGroupedDataForId(
/*id=*/ childId, idToData, adjListRepr, alreadyProcessedIds);
if (childIdData !== null) {
result.children.push(childIdData);
}
alreadyProcessedIds.add(childId);
}
alreadyProcessedIds.add(id);
return result;
}
// Returns the final grouped data.
function getGroupedData(data) {
// A map of ID to its data
const idToData = new Map(data.map(entry => [entry.id, entry]));
// Adjacency list representation of the tree
const adjListRepr = getAdjListRepr(data);
// Root IDs, i.e. IDs with no parent
const rootIds = getRootIds(data);
if (rootIds.size === 0) {
// Invalid data
return null;
}
// IDs that have been grouped with children
const alreadyProcessedIds = new Set();
// Final result
const result = [];
// Recursively add IDs
for (const rootId of rootIds) {
const groupedData = getGroupedDataForId(
/*id=*/ rootId, idToData, adjListRepr, alreadyProcessedIds);
if (groupedData !== null) {
result.push(groupedData);
}
}
return result;
}
// Test
const data = [
{'id': 1, 'name': 'Topic 1', 'parent_id': null},
{'id': 2, 'name': 'Topic 2', 'parent_id': 1},
{'id': 3, 'name': 'Topic 3', 'parent_id': 2},
{'id': 4, 'name': 'Topic 4', 'parent_id': 2},
{'id': 5, 'name': 'Topic 5', 'parent_id': 4},
{'id': 6, 'name': 'Topic 6', 'parent_id': null},
{'id': 7, 'name': 'Topic 7', 'parent_id': 1},
];
console.log(JSON.stringify(getGroupedData(data)));
We can achieve this with a simple recursive function and iterate from the top of your list of objects.
function BuildChild(data, currentChild){
//Creating current child object
var child = {};
child.id = currentChild.id;
child.name = currentChild.name;
child.children = [];
//Looking for childrens in all input data
var currentChildren = data.filter(item => item.parent_id == child.id);
if(currentChildren.length > 0){
currentChildren.forEach(function(item){
//Iterating threw children and calling the recursive function
//Adding the result to our current children
child.children.push(BuildChild(data,item));
});
}
return child; }
You can then call it while getting the "top parent" of your list :
BuildChild(data, data.find(child => child.parent_id == null))
Snippet with result :
function BuildChild(data, currentChild){
//Creating current child object
var child = {};
child.id = currentChild.id;
child.name = currentChild.name;
child.children = [];
//Looking for childrens in all input data
var currentChildren = data.filter(item => item.parent_id == child.id);
if(currentChildren.length > 0){
currentChildren.forEach(function(item){
//Iterating threw children and calling the recursive function
//Adding the result to our current children
child.children.push(BuildChild(data,item));
});
}
return child;
}
var data = [
{ id: 1, name: "Topic 1", parent_id: null },
{ id: 2, name: "Topic 2", parent_id: 1 },
{ id: 3, name: "Topic 3", parent_id: 2 },
{ id: 4, name: "Topic 4", parent_id: 2 },
{ id: 5, name: "Topic 5", parent_id: 4 },
]
console.log(BuildChild(data, data.find(child => child.parent_id == null)))
I have an object that contains the following object template:
0:{
id: 1,
name: Name 1
children: 0:{
id: 2,
name: Name 2
children: {}
},
1:{
id: 3,
name: Name 3
children: {}
}
},
1:{
id: 4,
name: Name 4
children: 0:{
id: 5,
name: Name 5
children: {}
}
},
However as you can see every "children" key can also have an exact copy of an object, an so can the children of these children and so on. How can I convert every instance of "children", including the children inside the children and so on, into an array of objects like the example below?
0:{
id: 1,
name: Name 1
children: [
{
id: 2,
name: Name 2
children: {}
},
{
id: 3,
name: Name 3
children: {}
}
]
},
1:{
id: 4,
name: Name 4
children: [
{
id: 5,
name: Name 5
children: {}
}
]
},
Something like this should work:
function flatObject(arg) {
const result = [];
const goTo = (o, index = 0) => {
// If no more objects, return
if (typeof o[index] === "undefined")
return;
const e = o[index];
// Push this object
result.push({ id: e.id, name: e.name });
// goto childrens and append them
goTo(e.children);
// Goto next item
goTo(o, index + 1);
}
// Call function
goTo(arg);
return result;
}
It uses a recursive function to go through each item, and it goes through each children key in order to append them in the result array.
EDIT FOR MODIFIED QUESTION
What you are looking for is the Object.entries() method, that according to the MDN
The Object.entries() method returns an array of a given object's own enumerable string-keyed property [key, value] pairs.
function flatObject(arg) {
// Iterate all
Object.keys(arg).forEach(key => {
const ObjectToArray = (o) => {
// Convert children to array
o.children = Object.entries(o.children).map(e => e[1]);
// Convert children of children
o.children.forEach(child => ObjectToArray(child));
};
// Push childrens
ObjectToArray(arg[key]);
});
return arg;
}
Say the original collection is:
var data = [{
id: 1,
children: [{
id: 2
}, {
id: 3
}]
}, {
id: 4,
children: [{
id: 5
}]
}]
Want to filter it with this given values of id property:
var selectedIds = [1, 3, 4]
The results should be:
var result = [{
id: 1,
children: [{
id: 3
}]
}, {
id: 4,
children: []
}]
How to accomplish this using Lodash methods?
You can use map and filter together to apply filter on nested element,
_.map(data, elem => {
elem.children = _.filter(elem.children, child => selectedIds.includes(child.id));
return elem;
});
NOTE: Assuming the filter has to be applied only on children property.
To not modify the original data,
_.map(data, elem => {
let children = _.filter(elem.children, child => selectedIds.includes(child.id));
return Object.assign({}, elem, {children});
});
You'll need to recursively walk your collection (and each item's children). I don't believe lodash has a native way of implementing this. I wrote a function called matcher that takes the collection and selected ids, and returns items that match those ids, and passes the children in and does the same thing. I wasn't sure how to handle a situation where a parent isn't selected but the children are.... I just assumed you'd throw them all away.
Point is, you have a very particular structure (items and children that are items) that requires you to write a specific recursive function that knows how to walk that structure.
const matcher = (collection, selected) => _.reduce(collection, (result, item) => {
if (selected.includes(item.id)) {
result.push({
id: item.id,
children: matcher(item.children, selected)
})
}
return result
}, [])
const result = matcher(data, selectedIds)
You could check for id and take only children with wanted id.
function getParts(array, ids) {
return array.reduce((r, { id, children }) =>
r.concat(ids.includes(id)
? { id, children: getParts(children || [], ids) }
: []
), []);
}
var data = [{ id: 1, children: [{ id: 2 }, { id: 3 }] }, { id: 4, children: [{ id: 5 }] }];
console.log(getParts(data, [1, 3, 4]));
console.log(getParts(data, [1, 3, 5]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I have Javascript tree structure
const tree = [
{id: 120 , children:[]},
{id: 110 , children:[
{id: 12 , children:[
{id: 3 , children:[]},
{id: 4 , children:[]}
]}
]},
{id: 10 , children:[
{id: 13 , children:[]}
]}
]
and i have this function to find the parent of given node
const _findParent = (tree, component, _parent) => {
let parent = null
// iterate
tree.forEach(node => {
if(node.id === component.id){
return _parent
}
parent = node. children ? _findParent(node.children, component, node) : parent
})
return parent
}
but it returns null, I cant find where i miss the parent object.
Basically you check the children, but your children is always an array which is a truthy value. In this case, you could check if children is an array.
The use of Array#forEach does not work with a return value for using outside of the callback.
I suggest to use Array#some, which allows an early exit, which is necessary if a node is found.
Then I suggest to use a second variable for getting a nested result and if truthy, then assign to parent for return and exit the iteration.
const _findParent = (tree, component, _parent) => {
let parent;
tree.some(node => {
var temp;
if (node.id === component.id) {
parent = _parent;
return true;
}
temp = Array.isArray(node.children) && _findParent(node.children, component, node);
if (temp) {
parent = temp;
return true;
}
});
return parent;
}
const tree = [{ id: 120, children: [] }, { id: 110, children: [{ id: 12, children: [{ id: 3, children: [] }, { id: 4, children: [] }] }] }, { id: 10, children: [{ id: 13, children: [] }] }];
console.log(_findParent(tree, { id: 4 }));
console.log(_findParent(tree, { id: 42 }));
I have an array of objects with parentId and sort values that I'd like to put into an array with nested 'children' and sorted appropriately.
For example, here's the data:
[{
id: 1,
sort: 2,
parentId: null,
name: 'A'
}, {
id: 2,
sort: 1,
parentId: 1,
name: 'A.1'
}, {
id: 3
sort: 2,
parentId: 1,
name: 'A.2'
}, {
id: 4,
sort: 1,
parentId: null,
name: 'B'
}]
The way I'd like to transform this would be such as:
[{
id: 4,
sort: 1,
parentId: null,
name: 'B',
children: []
}, {
id: 1,
sort: 2,
parentId: null,
name: 'A',
children: [{
id: 2,
sort: 1,
parentId: 1,
name: 'A.1'
}, {
id: 3
sort: 2,
parentId: 1,
name: 'A.2'
}]
}]
This is sorted (id 4 being at the top, since sort is 1) and the children are nested and also sorted accordingly.
Any suggestions on a good way to do this? I can recursively loop through to apply children, but not sure how I can maintain sorting on this.
This is a proposal which sorts first and filters after that.
The sorting takes the properties parentId and sort. This is necessary for the next step, because the "filtering" needs a sorted array.
Later the array is filterd with Array#filter(), Here is thisArgs used for referencing nodes for a possible insertation of children.
Edit: Update for unsorted (id/parentId) data.
var array = [{ id: 1, sort: 2, parentId: null, name: 'A' }, { id: 2, sort: 1, parentId: 1, name: 'A.1' }, { id: 3, sort: 2, parentId: 1, name: 'A.2' }, { id: 4, sort: 1, parentId: null, name: 'B' }],
nested;
array.sort(function (a, b) {
return (a.parentId || -1) - (b.parentId || -1) || a.sort - b.sort;
});
nested = array.filter(function (a) {
a.children = this[a.id] && this[a.id].children;
this[a.id] = a;
if (a.parentId === null) {
return true;
}
this[a.parentId] = this[a.parentId] || {};
this[a.parentId].children = this[a.parentId].children || [];
this[a.parentId].children.push(a);
}, Object.create(null));
document.write('<pre>' + JSON.stringify(nested, 0, 4) + '</pre>');
I gave it a try and came back, and there are already other answers, but I'm posting it anyway.
This method modifies the original Array:
var items = [{id: 1,sort: 2,parentId: null,name: 'A'}, {id: 2,sort: 1,parentId: 1,name: 'A.1'}, {id: 3,sort: 2,parentId: 1,name: 'A.2'}, {id: 4,sort: 1,parentId: null,name: 'B'}];
function generate_tree(arr){
var references = {};
arr.sort(function(a,b){
// Save references to each item by id while sorting
references[a.id] = a; references[b.id] = b;
// Add a children property
a.children = []; b.children = [];
if(a.sort > b.sort) return 1;
if(a.sort < b.sort) return -1;
return 0;
});
for(var i=0; i<arr.length; i++){
var item = arr[i];
if(item.parentId !== null && references.hasOwnProperty(item.parentId)){
references[item.parentId].children.push(arr.splice(i,1)[0]);
i--; // Because the current index now contains the next item
}
}
return arr;
}
document.body.innerHTML = "<pre>" + JSON.stringify(generate_tree(items), null, 4) + "</pre>";
I'd create a new data structure, to look like:
{ 1: {
id: 1,
sort: 2,
parentId: null,
name: 'A'
},
2: {
id: 4,
sort: 1,
parentId: null,
name: 'B'
}
}
Things to notice: the new structure is an object, not an array, that contains only the topmost elements in it (the ones with parentId null)
Then, do a for over the original array, and assign new_obj[ orig_arr_curr_elem[parentId] ].children.push(orig_arr_curr_elem)
Then create a new array with the elems from new_obj sort() the (or the children property) however you want
Code that implements the steps 1 and 2 (run this using node):
var util = require('util');
var old_arr = [{
id: 1,
sort: 2,
parentId: null,
name: 'A'
}, {
id: 2,
sort: 1,
parentId: 1,
name: 'A.1'
}, {
id: 3,
sort: 2,
parentId: 1,
name: 'A.2'
}, {
id: 4,
sort: 1,
parentId: null,
name: 'B'
}];
var new_obj = {};
for (var i = 0; i < old_arr.length; i++){
if ( old_arr[i].parentId == null )
new_obj[ old_arr[i].id ] = old_arr[i];
}
for (var i = 0; i < old_arr.length; i++){
if ( old_arr[i].parentId == null ) continue;
new_obj[ old_arr[i].parentId ].children = new_obj[ old_arr[i].parentId ].children || [];
new_obj[ old_arr[i].parentId ].children.push( old_arr[i] );
}
console.log(util.inspect(new_obj, {showHidden: false, depth: null}));