I have a list of elements, each has an ID and a parent ID. What I want to do is detect when there is a loop in this 'hierarchy', and show which ID starts the loop.
list = [
{
id: '1',
parent: '2'
},
{
id: '2',
parent: '3'
},
{
id: '3',
parent: '4'
},
{
//This id is causing the loop
id: '4',
parent: '1'
}
]
I have tried this to build the tree, which works when there's no loop, but does not work with a loop:
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) {
lookup[obj[parentAttr]][childrenAttr].push(obj);
} else {
treeList.push(obj);
}
});
return treeList;
};
I also cannot detect when there is a loop.
I'd like to return the ID of the element that caused the loop to allow me to fix the data behind it.
You can apply white-grey-black coloring to detect nodes that are (re)visited while visiting their descendants (I've simplified your graph to a list of parent-child pairs):
graph = [
[2, 1],
[3, 2],
[1300023, 3],
[1, 1300023],
];
colors = {}
function visit(vertex) {
if (colors[vertex] === 'black') {
// black = ok
return;
}
if (colors[vertex] === 'grey') {
// grey = visited while its children are being visited
// cycle!
console.log('cycle', colors);
return;
}
// mark as being visited
colors[vertex] = 'grey';
// visit children
graph.forEach(edge => {
if (edge[0] === vertex)
visit(edge[1]);
});
// mark as visited and ok
colors[vertex] = 'black'
}
visit(1)
A nice illustration of this approach: https://algorithms.tutorialhorizon.com/graph-detect-cycle-in-a-directed-graph-using-colors/
You could collect all nodes and the childrens in object and filter all nodes by taking an array of visited nodes.
The infinite array contains all nodes which causes a circular reference.
function isCircular(id, visited = []) {
return visited.includes(id)
|| Object.keys(links[id]).some(k => isCircular(k, visited.concat(id)));
}
var list = [{ id: '1', parent: '2' }, { id: '2', parent: '3' }, { id: '3', parent: '4' }, { id: '4', parent: '1' }],
links = {},
infinite = [];
list.forEach(({ id, parent }) => {
links[parent] = links[parent] || {};
links[parent][id] = true;
});
infinite = list.filter(({ id }) => isCircular(id));
console.log(links);
console.log(infinite);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Related
I would like to add a function in which users can go to the next searched result. Thanks, #ggorlen for helping with the recursive search.
I have a recursive search function that gives the first value and makes them selected = true and if it is in nested array make showTree=true.
How I can add a function in which if the user clicks the next search record then the selected: true will set to the next result and remove the previous one?
and based on the new results showTree will change.
How to add a variable which gets updated based on the number of time search is called...
previous record option so user can go back to the previous result
const expandPath = (nodes, targetLabel) => {
for (const node of nodes) {
if (node.label.includes(targetLabel)) {
return (node.selected = true);
} else if (expandPath(node.item, targetLabel)) {
return (node.showTree = true);
}
}
};
// Output
expandPath(testData, 'ch');
//// add variable for count example: 1 of 25
console.log(testData);
//if user click on nextrecord after search
//nextrecord(){
//logic to remove the selected true from current and add for next
//update showtree
//update recordNumber of totalValue example: 2 of 25
//}
//child3 should get selected true and remove child1 selected true and showtree
//same add showTree= true based on selected value
//if user click on previous record after search
//previousrecord(){
//logic to remove the selected true from current and add for previous
//update showtree
//update recordNumber of totalValue example: 1 of 25
//}
console.log(testData);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script>
// Test Data
const testData = [
{
id: 1,
label: 'parent1',
item: [
{
id: 21,
label: 'child1',
item: [
{
id: 211,
label: 'child31',
item: [
{
id: 2111,
label: 'child2211',
item: [{ id: 21111, label: 'child22111' }]
}
]
},
{ id: 222, label: 'child32' }
]
},
{
id: 22,
label: 'child2',
item: [
{
id: 221,
label: 'child421',
item: [{ id: 2211, label: 'child2211' }]
},
{ id: 222, label: 'child222' }
]
}
]
},
{
id: 2,
label: 'parent2',
item: [
{
id: 21,
label: 'child2',
item: [
{
id: 511,
label: 'child51',
item: [
{
id: 5111,
label: 'child5211',
item: [{ id: 51111, label: 'child52111' }]
}
]
},
{ id: 522, label: 'child352' }
]
}
]
}
];
</script>
You can use refer following code
const testData = [
{
id: 1,
label: 'parent1',
item: [
{
id: 21,
label: 'child1',
item: [
{
id: 211,
label: 'child31',
item: [
{
id: 2111,
label: 'child2211',
item: [{ id: 21111, label: 'child22111' }]
}
]
},
{ id: 222, label: 'child32' }
]
},
{
id: 22,
label: 'child2',
item: [
{
id: 221,
label: 'child421',
item: [{ id: 2211, label: 'child2211' }]
},
{ id: 222, label: 'child222' }
]
}
]
},
{
id: 2,
label: 'parent2',
item: [
{
id: 21,
label: 'child2',
item: [
{
id: 511,
label: 'child51',
item: [
{
id: 5111,
label: 'child5211',
item: [{ id: 51111, label: 'child52111' }]
}
]
},
{ id: 522, label: 'child352' }
]
}
]
}
];
// flatten down tree to array and add parent pointer
const flatten = (data) => {
let flattenData = [data]
if (data.item) {
for (const item of data.item) {
item.parent = data;
flattenData = flattenData.concat(flatten(item));
}
}
return flattenData;
}
let flattenData = [];
// flatten down the test data
for (const data of testData) {
flattenData = flattenData.concat(flatten(data));
}
// to update showTree flag
const toggle = (item, expand = true) => {
const parent = item.parent;
if (parent) {
parent.showTree = expand;
if (parent.parent) {
return toggle(parent, expand);
}
return parent;
}
return item;
}
/**
*
* #param {targetLabel} query
* #returns function navigate with param forward flag
*/
const seach = (query) => {
let index = -1;
const items = flattenData.filter(x => x.label.includes(query));
return (forward = true) => {
if (index > -1) {
items[index].selected = false;
toggle(items[index], false);
}
index = index + (forward ? 1 : -1);
let item = null;
if (index > -1 && index < items.length) {
items[index].selected = true;
item = toggle(items[index], true);
}
return {
item,
index,
length: items.length
}
}
}
const navigate = seach('child5211');
// next result
let result = navigate();
// previous result
result = navigate(false);
// result will look like this
/**
* {
* item: root of current item with showTree and selected falgs or null if out of bound,
* index: current match,
* length: total match
* }
*
*/
Tackling one thing at a time here, you can pretty quickly get the desired 'next' functionality you want by converting you're recursive search to a generator function:
function* expandPath(nodes, targetLabel) {
for (const node of nodes) {
if (node.label.includes(targetLabel)) {
yield (node.selected = true);
} else if (expandPath(node.item, targetLabel)) {
yield (node.showTree = true);
}
}
};
const gen = expandPath(mynodes, "thisTargetLabel");
gen.next()
gen.next() //<-- the next one
It's a little hard to know without more of your context how to answer the other questions, but what it really seems like you need here is state, and (es6) class is a good way to make this:
class Searcher {
constructor(mynodes, mylabel){
this.count=0;
this.nodes=mynodes;
this.label=mylabel;
this.generateMatches(this.nodes);
this.selectNode(this.matches[0]); // select the first node
}
generateMatches(nodes){
this.matches=[];
for (const node of nodes) {
if (node.label.includes(this.label)) {
this.matches.push(node);
} else {
this.generateMatches(nodes.node)
}
}
}
updateTreeById(id, node){
this.nodes.forEach(n=>n.showTree = false);
for (const node of this.nodes) {
if (node.id === id) {
//noop but we are here
} else if(this.updateTreeById(id, this.nodes.node)) {
node.showTree = true;
}
}
}
selectNode(i){
const index = i % this.matches.length;
this.currNodeId = this.matches[index].id;
this.matches[index].selected = true // we are wrapping around
this.count = i; // setting your current count
this.updateTreeById(this.matches[index].id)
// update logic, reset trees
}
nextNode(){
this.selectNode(this.count + 1)
}
prevNode(){
this.selectNode(this.count - 1)
}
}
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}
Good day,
I need to convert strings as such:
Process1_Cat1_Cat2_Value1
Process1_Cat1_Cat2_Value2
Process2_Cat1_Cat2_Value1
into a nested array as such:
var d = [{
text: 'Process1',
children: [{
text: 'Cat1',
children: [{
text: 'Cat2',
children: [{
text: 'Value1'
},
{
text: 'Value2'
}]
}]
}]
},
{
text: 'Process2',
children: [{
text: 'Cat1',
children: [{
text: 'Cat2',
children: [{
text: 'Value1'
}]
}]
}]
},
];
The reason why I need to do this is to make use of a treeview to display my data:
https://www.npmjs.com/package/bootstrap-tree-view
I have looked at the following solution but was not able to get it working due to lowdash library throwing errors on the findWhere function:
Uncaught TypeError: _.findWhere is not a function
http://brandonclapp.com/arranging-an-array-of-flat-paths-into-a-json-tree-like-structure/
See below for the code:
function arrangeIntoTree(paths, cb) {
var tree = [];
// This example uses the underscore.js library.
_.each(paths, function(path) {
var pathParts = path.split('_');
pathParts.shift(); // Remove first blank element from the parts array.
var currentLevel = tree; // initialize currentLevel to root
_.each(pathParts, function(part) {
// check to see if the path already exists.
var existingPath = _.findWhere(currentLevel, {
name: part
});
if (existingPath) {
// The path to this item was already in the tree, so don't add it again.
// Set the current level to this path's children
currentLevel = existingPath.children;
} else {
var newPart = {
name: part,
children: [],
}
currentLevel.push(newPart);
currentLevel = newPart.children;
}
});
});
cb(tree);
}
arrangeIntoTree(paths, function(tree) {
console.log('tree: ', tree);
});
Any help will be appreciated!
You could use an iterative by looking for the text at the actual level. If not found create a new object. Return the children array for the next level until the most nested array. Then add the leaf object.
var data = ['Process1_Cat1_Cat2_Value1', 'Process1_Cat1_Cat2_Value2', 'Process2_Cat1_Cat2_Value1'],
result = data.reduce((r, s) => {
var keys = s.split('_'),
text = keys.pop();
keys
.reduce((q, text) => {
var temp = q.find(o => o.text === text);
if (!temp) {
q.push(temp = { text, children: [] });
}
return temp.children;
}, r)
.push({ text });
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I've tried to build a tree from results of dataset as array of objects, but IE have bad feeling with it.
The problem it's the timing.
I have to process data like this to build tree:
var DataSet = [{
NodeId: 1,
Label: 'Root',
ParentId: null,
Icon: 'icon'
}, {
NodeId: 2,
Label: 'Children',
ParentId: 1,
Icon: 'icon1'
}, {
NodeId: 3,
Label: 'Children',
ParentId: 1,
Icon: 'icon1'
}, {
NodeId: 4,
Label: 'Children_lvl2',
ParentId: 2,
Icon: 'icon2'
}];
The conditions to be a child or root are objects:
var ForBeChild = { ParentId: '%#' }
var ForBeRoot = { ParentId: null }
Where '%#' is any value of object, set it also by condition:
var ColNameAsId = 'NodeId';
For order data as tree, I creating the following function:
var CreateTree = function (Parents, DataToAssign) {
if (!Parents.length) return [];
for (var i = 0; i < Parents.length; i++) {
var ParentData = Parents[i];
var Children = DataToAssign.filter(function (data) {
for (var key in ForBeChild) {
var value = ForBeChild[key];
if (value === '%#') {
if (ParentData[ColNameAsId] != data[key]) {
return false;
}
} else {
if (data[key] != value) {
return false;
}
}
}
return true;
});
ParentData._children = CreateTree(Children, DataToAssign);
}
return Parents;
};
Before calling it, I retrive the parents to pass as param:
var RootNodes = DataSet.filter(function (data) {
for (var key in ForBeRoot) {
if (data[key] != ForBeRoot[key])
return false;
}
return true;
});
Then build the all tree:
var JsonTree = CreateTree(RootNodes, DataSet);
Testing in IE 11.0.60 (Win10) - I know it's not the last - the timing for creating the tree it's 5/6 seconds.
The same code running into Chrome 70.0.3538.67 the timing it's around 1 second.
There is a way that can I improve the code? I need to reduce the timing of IE at minimum possible.
Thanks in advance.
Vale
You could create the tree based on the ParentId and NodeId property, so that, there is no need to loop through the properties. it will reduce the time to create the tree.
Code as below:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript">
var DataSet = [{
NodeId: 1,
Label: 'Root',
ParentId: 0,
Icon: 'icon'
}, {
NodeId: 2,
Label: 'Children',
ParentId: 1,
Icon: 'icon1'
}, {
NodeId: 3,
Label: 'Children',
ParentId: 1,
Icon: 'icon1'
}, {
NodeId: 4,
Label: 'Children_lvl2',
ParentId: 2,
Icon: 'icon2'
}];
$(document).ready(function () {
var data = PopulateTreeNode(DataSet, 0);
});
function PopulateTreeNode(data, parentid) {
var newdata = data.filter(function (value) {
return (value.ParentId == parentid);
});
newdata.forEach(function (e) {
e._children = PopulateTreeNode(data, e.NodeId);
})
return newdata;
};
</script>
the screenshot as below:
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`);
}
}