Construct hierarchy tree from flat list with parent field? [duplicate] - javascript

This question already has answers here:
Build tree array from flat array in javascript
(34 answers)
Closed 2 years ago.
I have a list of "page" objects with a parent field. This parent field references another object in the list. I would like to create a tree hierarchy from this list based on this field.
Here is what my original list looks like:
[
{
id: 1,
title: 'home',
parent: null
},
{
id: 2,
title: 'about',
parent: null
},
{
id: 3,
title: 'team',
parent: 2
},
{
id: 4,
title: 'company',
parent: 2
}
]
I would like to convert it into a tree structure like this:
[
{
id: 1,
title: 'home',
parent: null
},
{
id: 2,
title: 'about',
parent: null,
children: [
{
id: 3,
title: 'team',
parent: 2
},
{
id: 4,
title: 'company',
parent: 2
}
]
]
I was hoping for a reusable function that I can call against an arbitrary list any time. Anyone know of a good way to handle this? Any help or advice would be greatly appreciated!

function treeify(list, idAttr, parentAttr, childrenAttr) {
if (!idAttr) idAttr = 'id';
if (!parentAttr) parentAttr = 'parent';
if (!childrenAttr) childrenAttr = 'children';
var treeList = [];
var lookup = {};
list.forEach(function(obj) {
lookup[obj[idAttr]] = obj;
obj[childrenAttr] = [];
});
list.forEach(function(obj) {
if (obj[parentAttr] != null) {
if (lookup[obj[parentAttr]] !== undefined) {
lookup[obj[parentAttr]][childrenAttr].push(obj);
} else {
//console.log('Missing Parent Data: ' + obj[parentAttr]);
treeList.push(obj);
}
} else {
treeList.push(obj);
}
});
return treeList;
};
Fiddle

The accepted answer was very helpful in my research, but, I had to mentally parse the id params which I understand make the function more flexible, but perhaps a bit harder to reason about for someone new to the algorithm.
In case someone else is has this difficulty, here's essentially the same code, but maybe easier to grok:
const treeify = (arr, pid) => {
const tree = [];
const lookup = {};
// Initialize lookup table with each array item's id as key and
// its children initialized to an empty array
arr.forEach((o) => {
lookup[o.id] = o;
lookup[o.id].children = [];
});
arr.forEach((o) => {
// If the item has a parent we do following:
// 1. access it in constant time now that we have a lookup table
// 2. since children is preconfigured, we simply push the item
if (o.parent !== null) {
lookup[o.parent].children.push(o);
} else {
// no o.parent so this is a "root at the top level of our tree
tree.push(o);
}
});
return tree;
};
It's the same code as accepted answer with some comments to explain what's going on. Here is a use case for this which will result in a list of divs rendered to page with inline marginLeft indentation based on the level:
const arr = [
{id: 1, title: 'All', parent: null},
{id: 2, title: 'Products', parent: 1},
{id: 3, title: 'Photoshop', parent: 2},
{id: 4, title: 'Illustrator', parent: 2},
{id: 4, title: 'Plugins', parent: 3},
{id: 5, title: 'Services', parent: 1},
{id: 6, title: 'Branding', parent: 5},
{id: 7, title: 'Websites', parent: 5},
{id: 8, title: 'Pen Testing', parent: 7}];
const render = (item, parent, level) => {
const div = document.createElement('div');
div.textContent = item.title;
div.style.marginLeft = level * 8 + 'px';
parent.appendChild(div);
if (item.children.length) {
item.children.forEach(child => render(child, div, ++level));
}
return parent;
}
const fragment = document.createDocumentFragment();
treeify(arr)
.map(item => render(item, fragment, 1))
.map(frag => document.body.appendChild(frag))
Codepen if you'd like to run it: https://codepen.io/roblevin/pen/gVRowd?editors=0010
To my mind, the interesting part about this solution is that the lookup table remains flat using the IDs of the items as keys, and only the root object(s) get pushed into the resulting tree list. However, due to the referential nature of JavaScript objects, the root has its children, and children their children, and so on, but it's essentially connected from the root and hence tree-like.

Related

How can I Use RxJs 6 GroupBy to organize stream? Need to concatenate all emissions from multiple Grouped Observables

Input Observable stream:
The data is obtained from an observable stream that is the result of a REST request for projects. The data is obtained as Observable<Project[]>.
const project1: Project = {
id: 1,
title: 'zebra',
rootId: 1,
}
const project2: Project = {
id: 2,
title: 'algebra',
rootId: 2,
}
const project3: Project = {
id: 3,
title: 'Bobcats',
rootId: 1,
}
const project4: Project = {
id: 4,
rootId: 2,
}
const project5: Project = {
id: 5,
title: 'Marigolds',
rootId: 1,
}
const project6: Project = {
id: 6,
title: 'whatever',
rootId: null,
}
const project7: Project = {
id: 7,
title: 'peppercorns',
rootId: null,
}
let groupProjects: Observable<ProjectSummary[]>
= getGroupProjects(of([project1, project2, project3, project4, project5, project6, project7]]));
getGroupProjects(projects$: Observable<ProjectSummary[]>): Observable<ProjectSummary[]> {
const timer$ = timer(5000);
const data = projects$.pipe(takeUntil(timer$), flatMap(projects => projects));
const groupedObservables = data.pipe(
groupBy(projects => projects.rootId),
tap( a => console.log('groupBy:' + a.key))
);
const merged = groupedObservables.pipe(
mergeMap(a => a.pipe(toArray())),
shareReplay(1),
tap( a => console.log('final:' + JSON.stringify(a)))
);
return merged;
}
The desired output is:
Object{ //Root of 1
id: 1,
title: 'zebra',
rootId: null
}
Object{
id: 3, //child of 1
title: 'Bobcats',
rootId: 1
}
Object{
id: 5, //child of 1
title: 'Marigolds',
rootId: 1
}
Object{
id: 2, //root of 2
title: 'algebra',
rootId: 2
}
Object{
id: 4, //child of 2
title: 'dogs',
rootId: 2
}
Object{
id: 6, //unaffiliated
title: 'whatever',
rootId: null
}
Object{
id: 7, //unaffiliated
title: 'peppercorns',
rootId: null
}
The requirement is that groups identified by rootId appear in sequence before their children (children appear after their root) and unaffiliated are listed together. roots are identified when id = rootId, children are identified when rootId != null && id != rootId. Unaffiliated are identified by null root id.
Currently only the last group is emitted. How can I return an observable that emits all groups and in correct sequence? --thanks
groupBy takes a stream of objects and emits a single group when the stream completes, it doesn't work with streams of arrays. What you want is a scan. Scan is like a reduce but it emits each time the source stream emits rather than once at the end.
I am not quite understanding what you are trying to achieve from you question but this should get you started
sourceThatEmitsArrays.pipe(
scan(
(results, emittedArray) => functionThatAddsEmittedArrayToResults(results, emittedArray),
[] // Start with an empty array
)
)
This is the same as a normal reduce function on arrays but emits results each time the source emits.
functionThatAddsEmittedArrayToResults would looks something like
(results, array) => array.reduce(
(newResults, current) => {
const group = findCurrentGroupInNewResultsOrCreateNewGroup(newResults, current);
replacePreviousGroupInResultsOrAddTheNewOne(newResults, group);
return newResults;
},
results // Start with previous results
)

Combining arrays of objects by matching values

I want to combine two arrays of objects, to make it easier for me to display in HTML. The function should find matching values of keys called "id" in arr1, and "source" in arr2. Here's what it looks like:
let arr1 = [
{id = 1,
name = "Anna"},
{id = 2,
name = "Chris"}
]
let arr2 = [
{childName = "Brian",
{source = 1}},
{childName = "Connie",
{source = 2}}
{childName = "Dory",
{source = 1}}
]
I tried different approaches, with best one being using forEach and filter on the arrays. I'm trying to set up a new property in arr1 objects called "children".
arr1.forEach(el => el.children = arr2.filter(checkMatch));
function checkMatch(child){
for(let i=0;i<arr1.length;i++){
child.childName.source === arr1[i].id
}
}
And this results in adding appropriate children to the first object(Anna has Brian and Dory now) so it's correct, but it also adds the same children to the second object (so Chris has also Brian and Dory).
Where is my mistake here? I'm guessing that the loop doesn't work the way I want it to work, but I don't know which one and how it happens.
Since your syntax for creating the objects of arr1 and arr2 are not valid i tried to guess the structure of your objects.
let arr1 = [
{
id: 1,
name: "Anna"
},
{
id: 2,
name: "Chris"
}
];
let arr2 = [
{
childName: "Brian",
source: 1
},
{
childName: "Connie",
source: 2
},
{
childName: "Dory",
source: 1
}
];
arr2.map((child) => {
for (let parent of arr1) {
if (parent.id == child.source) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(child);
}
}
});
console.log(arr1);
There were problems with your JSON, but I tidied and here is option using map and filter
const arr1 = [{
id: 1,
name: "Anna"
},
{
id: 2,
name: "Chris"
}];
const arr2 = [{
childName: "Brian",
parent: {
source: 1
}
},
{
childName: "Connie",
parent: {
source: 2
}
},
{
childName: "Dory",
parent: {
source: 1
}
}];
let merge = arr1.map(p => {
p.children = arr2.filter(c => c.parent.source === p.id).map(c => c.childName);
return p;
});
console.log(merge);
Your json have some problems you should use
:
instead of
=
Also some Braces make the structure incorrect, but I think what you want to do here is fill a children sub array with the childNames of the subject here is my approach:
var json =
[
{
"id" : 1,
"name" : "Anna"
},
{
"id" : 2,
"name" : "Chris"
}
];
var subJson = [
{
"childName" : "Brian",
"source" : 1
},
{
"childName" : "Connie",
"source" : 2
},
{"childName" : "Dory",
"source" : 1
}
];
var newJson = [];
$.each(json,function(index1,item){
newJson.push({"id":item.id,"name":item.name, "children": []});
$.each(subJson,function(index2,subitem){
if(subitem.source == item.id){
newJson[newJson.length - 1].children.push({"childName":subitem.childName}) ;
}
})
})
console.log(newJson);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Hope it helps
The below uses Map for storage and convenient lookup of parents.
const parents = [
{
id: 1,
name: "Anna"
},
{
id: 2,
name: "Chris"
}
]
const children = [
{
childName: "Brian",
source: 1
},
{
childName: "Connie",
source: 2
},
{
childName: "Dory",
source: 1
}
]
// Create a map for easy lookup of parents.
const parentMap = new Map()
// Add parents to the map, based on their id.
parents.forEach(parent => parentMap.set(parent.id, parent))
// Add children to their parents.
children.forEach((child) => {
// Get the parent from the map.
const parent = parentMap.get(child.source)
// Handle parent not found error.
if (!parent) { return console.error('parent not found for', child.childName)}
// Create the children array if it doesn't already exist.
parent.children = parent.children || []
// Add the child to the parent's children array.
parent.children.push(child)
})
// Output the result.
Array.from(parentMap).forEach(parent => console.log(parent[1]))
Result:
{
id: 1,
name: 'Anna',
children: [
{ childName: 'Brian', source: 1 },
{ childName: 'Dory', source: 1 }
]
}
{
id: 2,
name: 'Chris',
children: [
{ childName: 'Connie', source: 2 }
]
}

How to remove child by id and retrive the parent objects using filter

I am trying to filter the parent, by removing it's child id only by not matching. in case if there is no child exist, the parent should be removed.
I try like this, but not works.
var rm = 7;
var objects = [
{
name: "parent1",
id: 1,
blog: [
{
name: "child1",
id: 1
},
{
name: "child2",
id: 2
}
]
},
{
name: "parent2",
id: 2,
blog: [
{
name: "child3",
id: 3
},
{
name: "child4",
id: 4
}
]
},
{
name: "parent3",
id: 3,
blog: [
{
name: "child5",
id: 5
},
{
name: "child6",
id: 6
}
]
},
{
name: "parent4",
id: 3,
blog: [
{
name: "child6",
id: 7
}
]
},
]
var result = objects.filter(value => {
if(!value.blog) return;
return value.blog.some(blog => blog.id !== rm)
})
console.log(result);
What is wrong here, or some one show me the correct approach?
looking for :
need to remove the blog if the id is same as rm, parent with other children required to exist.
need to remove the parent, after remove the children, in case there is no child(blog) exist.
Live Demo
Loop through the list of parents, and inside that loop, try to remove blogs with the given id first. Once you have done that, you can check if the blogs property has became empty, and if so, filter it out:
// We're going to filter out objects with no blogs
var result = objects.filter(value => {
// First filter blogs that match the given id
value.blog = value.blog.filter(blog => blog.id !== rm);
// Then, if the new length is different than 0, keep the parent
return value.blog.length;
})
I think the below code is what you are looking for
var result = objects.map(value => {
const blog = value.blog.filter(blog => blog.id !== rm);
if(blog.length === 0) {
return;
}
value.blog = blog;
return value;
}).filter(item => item);
Demo: https://jsfiddle.net/7Lp82z4k/3/
var result = objects.map(parent => {
parent.blog = parent.blog.filter(child => child.id !== rm);
return parent}).filter(parent => parent.blog && parent.blog.length > 0);

Create a recursive function that takes a flat array and converts to a tree data structure

So I'm trying to write a recursive function that takes a flat array of objects with their value, id, and the id of their parent node and transform it to a tree structure, where the children of the structure are an array of nodes. Children need to be sorted by id and if its null it can be the root node.
The function im trying to write function toTree(data), only should take in the data array. I've been unable to do it without a parent. What I have so far is a function(below) that takes data and parent to get started.
input:
const tasks = [
{ id: 1, parent: null, value: 'Make breakfast' },
{ id: 2, parent: 1, value: 'Brew coffee' },
{ id: 3, parent: 2, value: 'Boil water' },
{ id: 4, parent: 2, value: 'Grind coffee beans' },
{ id: 5, parent: 2, value: 'Pour water over coffee grounds' }
];
output:
{
id: 1,
parent: null,
value: 'Make Breakfast',
children: [
{
id: 2,
parent: 1,
value: 'Brew coffee',
children: [
{ id: 3, parent: 2, value: 'Boil water' },
{ id: 4, parent: 2, value: 'Grind coffee beans' },
{ id: 5, parent: 2, value: 'Pour water over coffee grounds' }
]
}
]
}
funciton toTree(data) {
customtoTree (data, null);
}
function customToTree (data, parent) {
const out = [];
data.forEach((obj) => {
if (obj.parent === parent) {
const children = customToTree(data,obj.parent);
if (children.length) {
obj[children[0]] = children;
}
const {id,parent, ...content} = obj;
out.push(content);
}
});
return out;
}
I would really like to understand the correct logic on how to do this and think about this and how to do it without giving a parent explicitly.
I had the same question during an interview, and I haven't been able to solve it. I was also confused that the function should only take the array as a first and only argument.
But after reworking it later (and with some very good suggestions from a brilliant man), I realized that you can call the function with the array as the first and only argument the first time and then during the recursion call passing the parent as a second argument.
Inside the function, you only need to check if the second argument is undefined, if it is, you search in the array for your root object and assign it to your second argument.
So here is my solution, I hope it will be clearer :
function toTree(arr, item) {
if (!item) {
item = arr.find(item => item.parent === null)
}
let parent = {...item}
parent.children =
arr.filter(x => x.parent === item.id)
.sort((a, b) => a.id - b.id)
.map(y => toTree(arr, y))
return parent
}
toTree(tasks)
I couldn't check for more test cases but this is something I was quickly able to come up with which passes your use case, it looks not so good, I would recommend to use it as initial structure and then build on it. Also, I am assuming that tasks are sorted in ascending order by parent, i.e child will only appear after its parent in tasks array
const tasks = [
{ id: 1, parent: null, value: 'Make breakfast' },
{ id: 2, parent: 1, value: 'Brew coffee' },
{ id: 3, parent: 2, value: 'Boil water' },
{ id: 4, parent: 2, value: 'Grind coffee beans' },
{ id: 5, parent: 2, value: 'Pour water over coffee grounds' },
{ id: 6, parent: 5, value: 'Pour water over coffee grounds' },
{ id: 7, parent: 5, value: 'Pour water over coffee grounds' }
];
function Tree() {
this.root = null;
// this function makes node root, if root is empty, otherwise delegate it to recursive function
this.add = function(node) {
if(this.root == null)
this.root = new Node(node);
else
// lets start our processing by considering root as parent
this.addChild(node, this.root);
}
this.addChild = function(node, parent) {
// if the provided parent is actual parent, add the node to its children
if(parent.id == node.parent) {
parent.children[node.id] = new Node(node);
} else if(parent.children[node.parent]) {
// if the provided parent children contains actual parent call addChild with that node
this.addChild(node, parent.children[node.parent])
} else if(Object.keys(parent.children).length > 0) {
// iterate over children and call addChild with each child to search for parent
for(let p in parent.children) {
this.addChild(node, parent.children[p]);
}
} else {
console.log('parent was not found');
}
}
}
function Node (node) {
this.id = node.id;
this.parent = node.parent;
this.value = node.value;
this.children = {};
}
const tree = new Tree();
// We are assuming that tasks are sorted in ascending order by parent
for(let t of tasks) {
tree.add(t);
}
console.log(JSON.stringify(tree.root))
Let me know if you have questions. Lets crack it together
If your input is already sorted by id and no child node can come before its parent Node in the list, then you can do this in one loop and don't even need recursion:
const tasks = [
{ id: 1, parent: null, value: 'Make breakfast' },
{ id: 2, parent: 1, value: 'Brew coffee' },
{ id: 3, parent: 2, value: 'Boil water' },
{ id: 4, parent: 2, value: 'Grind coffee beans' },
{ id: 5, parent: 2, value: 'Pour water over coffee grounds' }
];
const tasksById = Object.create(null);
// abusing filter to do the work of a forEach()
// while also filtering the tasks down to a list with `parent: null`
const root = tasks.filter((value) => {
const { id, parent } = value;
tasksById[id] = value;
if(parent == null) return true;
(tasksById[parent].children || (tasksById[parent].children = [])).push(value);
});
console.log("rootNodes", root);
console.log("tasksById", tasksById);
.as-console-wrapper{top:0;max-height:100%!important}

add new data inside redux state tree

I have an app with a tree of nested nodes. all nodes are same type.
{
id: 1,
title: "node_1",
children: [
{
id: 2,
title: "node_2",
children: []
},
{
id: 3,
title: "node_3",
children: []
}
]
}
When user expanded some node (for example node with id === 3) i have to perform request to database and insert response (array children) inside of "children" property of node with id === 3 . So as result app state should be like this:
{
id: 1,
title: "node_1",
children: [
{
id: 2,
title: "node_2",
children: []
},
{
id: 3,
title: "node_3",
children: [
{
id: 4,
title: "node_4",
children: []
},
{
id: 5,
title: "node_5",
children: []
}
]
}
]
}
how can i paste array of children inside node_3 children property?
Given:
const layer1Id = 1;
const layer2Id = 3;
const newArray = [
{
id: 4,
title: "node_4",
children: [],
},
{
id: 5,
title: "node_5",
children: [],
}
];
Then, in the reducer you'll do:
return Object.assign({}, state, { children: state.children.map(child => {
if (child.id !== layer1Id) return child;
return Object.assign({}, child, { children: child.children.map(node => {
if (node.id !== layer2Id) return node;
return Object.assign({}, node, { children: node.children.concat(newArray) });
})});
})});
To make sure you don't mutate the previous state.
If it is dynamically or deeply nested, I'll recommend you to write some recursive function and use that instead.
Edit: here's sample recursive solution (untested). The indices are in order by level (ie: indices[0] refers to first level's id, indices[1] refers to second level's id):
const state = { ... };
const indices = [1, 3, 4, 5, 6];
const newArray = [ ... ];
const recursion = (node, ids, newChildren) => {
let children;
if (ids.length === 0) {
children = newChildren;
} else {
children = node.children.map(child => {
if (child.id !== ids[0]) return child;
return Object.assign({}, child, { children: recursion(child, ids.slice(1), newChildren) });
});
}
return Object.assign({}, node, { children });
};
recursion(state, indecies, newArray);
The suggested approach for relational or normalized data in a Redux store is to organize it in "normalized" fashion, similar to database tables. That will make it easier to manage updates. See http://redux.js.org/docs/FAQ.html#organizing-state-nested-data, How to handle tree-shaped entities in Redux reducers?, and https://github.com/reactjs/redux/pull/1269.
Just iterate through children array and push to correct one .
var id = expandedItemId;
for(var i = 0; i < obj.children.length; i++){
if(obj.id == expandedItemId){
obj.children.push(`data got from server`);
}
}

Categories