How to get altered tree from Immutable tree, maximising reuse of nodes - javascript

I have a tree structure data like this:
[{
id: 54,
name:123,
children: [{
id: 54,
name:123,
children: [{
id: 154,
name:1234,
children []...
}]
}]
}, {
...
}]
I am using Angular 2. As far as I know, change detection kicks in whenever input changes and your change detection strategy is onPush.
To optimise the tree structure updates (e.g. toggling a node at a nested level or changing any attributes of such a node), I used Immutable.
How can Immutable help me to optimise my updates? I read that Immutable reuses references from old data to construct new objects whenever data changes.
How do I use Immutable data structures efficiently to update nodes at a nested level?
Assumptions
I don't have a keyPath for any of the nodes.
Each node has a unique id property value which can be used to query tree data (but how?)
Problems
How can I update a node somewhere at a nested level? What could be the most efficient way of reaching out to that node?
How do I update multiple nodes? I heard of the withMutations API, but is there any other efficient way?
My approach
Deep-copy everything and then modify the newly constructed object:
var newState = deepClone(oldState) // deep copy everything and construct a new object
newState.nodes.forEach(node => {
if(node.id == 54) {
node.id = 789;
}
})
What I am trying to implement:
var newState = Immutable.fromJS(oldState) // create an immutable object
newState = newState.find(node => {
node.set("id", 123);
}); // any changes to object will return new object
With the second solution I hope to achieve re-use of nodes, as pictured below:

Realise that when you use Immutable for your tree structure, you cannot expect to replace a node without also changing the internal references to that node, which means that such a change will need to bubble up to the root of the tree, also changing the root.
More detailed: as soon as you use a method to change a certain property value, you will get a new object returned for that particular node. Then to re-inject that new node in your tree, i.e. in the parent's children list, you will use a method that will create a new children list (since the children property is immutable as well). Now the problem is to attach that children list to the parent, which will result in a new parent node, ...etc. You'll end up recreating all the ancestor nodes of the node you want to change, giving you a new tree instance, which will have some reuse of nodes that were not in the root-to-node path.
To re-use your image, you'll get something like this:
The Immutable API can do this for you with the updateIn method (or setIn if your update concerns only one property of the targeted node). You will need to pass it the key path to identify the (nested) node you want to modify.
So, if for instance you know the id of the node to be changed, you could use a little helper function to find the key path to that particular node.
function findKeyPathOf(tree, childrenKey, predicate) {
var path;
if (Immutable.List.isList(tree)) {
tree.some(function (child, i) {
path = findKeyPathOf(child, childrenKey, predicate);
if (path) return path.unshift(i); // always returns truthy
});
return path;
}
if (predicate(tree)) return [];
path = findKeyPathOf(tree.get(childrenKey), childrenKey, predicate);
if (path) return [childrenKey].concat(path);
}
You need to pass it the tree, the name of the property that has the children (so children in your case), and the function that will identify the node you are looking for. Let's say you want the path to the node with id 4, then you would call it like this:
var keyPath = findKeyPathOf(tree, 'children', node => node.get('id') == 4);
That key path could look something like this -- an alteration of an index in the array, and the children property providing the deeper array:
[0, 'children', 0, 'children', 1]
Then to modify the node at that path, you would do something like this:
var newTree = tree.updateIn(keyPath, node => node.set('name', 'Hello'));
Here is a demo with some sample data:
// Function to get the path to a certain node in the tree
function findKeyPathOf(tree, childrenKey, predicate) {
var path;
if (Immutable.List.isList(tree))
childrenKey = tree.findKey(child =>
path = findKeyPathOf(child, childrenKey, predicate));
else if (predicate(tree))
return [];
else
path = findKeyPathOf(tree.get(childrenKey), childrenKey, predicate);
return path && [childrenKey].concat(path);
}
// Function to compare two trees
function differences(tree1, tree2, childrenKey) {
if (Immutable.List.isList(tree1)) {
return tree1.reduce(function (diffs, child, i) {
return diffs.concat(differences(child, tree2.get(i), childrenKey));
}, []);
}
return (tree1 !== tree2 ? [tree1] : [])
.concat(differences(tree1.get(childrenKey), tree2.get(childrenKey),
childrenKey));
}
// Sample data
var tree = [{
id: 1,
name: 'Mike',
children: [{
id: 2,
name: 'Helen',
children: [{
id: 3,
name: 'John',
children: []
},{
id: 4,
name: 'Sarah',
children: [{
id: 5,
name: 'Joy',
children: []
}]
}]
}]
}, {
id: 6,
name: 'Jack',
children: [{
id: 7,
name: 'Irene',
children: []
},{
id: 8,
name: 'Peter',
children: []
}]
}];
// Create immutable tree from above plain object:
var tree = Immutable.fromJS(tree);
// Use the function to find the node with id == 4:
var keyPath = findKeyPathOf(tree, 'children', node => node.get('id') == 4);
// Found it?
if (keyPath) {
// Set 'name' to 'Hello' in that node:
var newTree = tree.updateIn(keyPath, node => node.set('name', 'Hello'));
// Print the new tree:
console.log(newTree.toJS());
// Compare all nodes to see which ones were altered:
var altered = differences(tree, newTree, 'children').map(x => x.get('id'));
console.log('IDs of nodes that were replaced: ', altered);
} else {
console.log('Not found!');
}
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>

Related

Concentate values in a Nested Array of Object with children in JavaScript

I have a nested array of objects having path as one of the keys. The structure of the nested array is as under:
const data = [
{
Name: "item1",
path: "path1",
children:[
{
Name: "item1.1",
path: "path1.1"
},
{
Name: "item1.2",
path: "path1.2",
children:[
{
Name: "item1.2.1",
path: "path1.2.1",
children:[
{
Name: "item1.2.1.1",
path: "path1.2.1.1"
}
]
},
]
}
]
}
]
I need to concentate the path values without changing the structure of array. The expected result would be:
const newdata: [
{
Name: "item1",
path: "path1",
children:[
{
Name: "item1.1",
path: "path1/path1.1"
},
{
Name: "item1.2",
path: "path1/path1.2",
children:[
{
Name: "item1.2.1",
path: "path1/path1.2/path1.2.1",
children:[
{
Name: "item1.2.1.1",
path: "path1/path1.2/path1.2.1/path1.2.1.1",
}
]
}
]
}
]
}
]
How to do it in JavaScript?
This would be best done with a recursive Function, that iterates through your entire data structure and sets the Path by building it up while traversing your data structure.
The first version creates the path information from scratch, by using the index of each child in the array and building up the index that gets appended to the path string.
Further below i've provided changes to this version, that uses the already existing path information and concatenates the path string as you asked for.
// Recursive Function to iterate through a possible endless nested data structure
// We provide the parameter for the previous index and parentPath to build up the path string
function recursivePath(data, index = "", parentPath = "") {
// We will get an Array with all Items as 'data' which we will loop with forEach
data.forEach((item, i) => {
// We recreate the the index of the item by adding current index of
// this item in the data array to the index structure of the parent items
let itemIndex = index !== "" ? `${index}.${i+1}` : `${i+1}`;
// We do the same for the path, we take the path of the parent
// and add the path information of this item to it.
let itemPath = `${parentPath}path${itemIndex}`;
// We set the path property of this item, which will be returned
// after all items of this data are done.
item.path = itemPath;
// We check if this item has some nested childrens and if it does,
// we will repeat this process for all those childrens
if (item.children && typeof item.children.length) {
// We provide the newly created index on which those childs will build upon
// as the same with the path.
// This can be a bit confusing, but we assume here, that the function will return
//the finished childrens and we save the result to our childrens property.
item.children = recursivePath(item.children, itemIndex, itemPath + "/");
}
});
// Lastly we iterated through all Items and are sure to set the Path for all Items
// and their childrens nested inside and return the entire data array.
return data;
}
// Your Data
const data = [{
Name: "item1",
path: "path1",
children: [{
Name: "item1.1",
path: "path1.1"
},
{
Name: "item1.2",
path: "path1.2",
children: [{
Name: "item1.2.1",
path: "path1.2.1",
children: [{
Name: "item1.2.1.1",
path: "path1.2.1.1"
}]
}, ]
}
]
}];
// We use the recursive function and output the results to the console
console.log(recursivePath(data));
If you would use the stored Path value of each item, you could just append the Value onto the parentPath String and save this new String into item.path
You would just change the line in the function, that creates the itemPath a little bit and you can remove the line that creates the itemIndex.
The parameter itemIndex of the recursive function isn't needed anymore and can be removed too.
// We wont need the index anymore, as we use the already existing
// Path value for the Index of each item
function recursivePath(data, parentPath = "") {
// We create a temporary new Data variable, to hold our changed elements.
let newData = [];
data.forEach((item, i) => {
// We'll create a copy of an Object to modify
let copyItem = {};
// Object.assign() copies all enumerable properties of one object to another
// We'll then use the new object to modify all properties,
// thous the original item will be untouched.
Object.assign(copyItem, item)
// We append the path information of this items path value
// onto the parentPath string
let itemPath = `${parentPath}${item.path}`;
// Same as before
copyItem.path = itemPath;
// Same as before
if (copyItem.children && typeof copyItem.children.length) {
// We removed the itemIndex, as it isnt needed anymore
copyItem.children = recursivePath([...copyItem.children], itemPath + "/");
}
// After modification we add the object to the temporary array
// and return it after all items are modified.
newData.push(copyItem);
});
// Returning the newly created array
return newData;
}
// Your Data
const data = [{
Name: "item1",
path: "path1",
children: [{
Name: "item1.1",
path: "path1.1"
},
{
Name: "item1.2",
path: "path1.2",
children: [{
Name: "item1.2.1",
path: "path1.2.1",
children: [{
Name: "item1.2.1.1",
path: "path1.2.1.1"
}]
}, ]
}
]
}];
// We use the recursive function and output the results to the console
console.log(recursivePath(data));
console.log(data);
Fur further clarification of why we need to copy Arrays and/or Objects provided as a parameter:
Arrays and Objects arent provided as their full content, as those could be huge data structures and moving and copying those every time they are provided as parameter would cause a huge memory dump as every parameter would be a redundant content of their original data.
Therefore only references or in other languages called pointers are provided, which point or reference the memory location, where the content is stored.
If you provide an array or object for a function and modify them, the modification will be stored via the reference on the original array and therefore all further access to this variable will also have those modification.
Thats why we need to copy the content of those variables into new array or objects and return those as they are themself new references but to another array with the same but slightly modified content.
The redundancy doesn't matter, as those variables are only block/closure scoped with the prefix of let before, therefore they are garbage collected after the function resolved.

Typescript - Complex Object merge with Dot Notation

I want to display the data in a Tree View in Angular and need to transform an array of dot-notated elements into a collection of objects with children.
This is the array I'm working with. Notice the key field in every element.
So the structure I need is for example (for the first 4 elements in the array):
const data = [
{
key: 'bs',
children: [
{
key: 'ass',
children: [
{
key: 'fixAss',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 15542000,
children: [
{
key: 'intan',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 8536000,
children: [
{
key: 'concessionBrands',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 8536000,
children: [] // If there are no children in the element this can be empty or left out
}
]
},
{
key: 'tan',
decimals: '0',
unitRef: 'unit_euro',
contextRef: 'period_2019',
value: 6890000,
children: []
}
]
}
]
}
]
}
];
That means elements are combined by having a key attribute which holds the notation for that level (i.e "bs", "ass", "fixAss", ...) and then children of the next level. An element can have values of its own ("decimals", "unitRef",...) and might additionally also have children that are made up the same way. There is no restriction on the amount of levels this can have.
I have the lodash and dot object libraries in my package.json. Any help is very much appreciated.
it seems the dot-object lib has no things to work with something like "children" that you have, so it seems custom code is required to build what you expected
// balanceData got somehow
let data = [];
const getOrBuildPathObject = (path) => {
let currentLevel = data;
let obj = null;
for(let keyFragment of path.split('.')) {
obj = currentLevel.find(v => v.key == keyFragment);
if(!obj) {
obj = {key: keyFragment, children: []};
currentLevel.push(obj);
}
currentLevel = obj.children;
}
return obj;
}
balanceData.forEach((d) => {
let {key, ...data} = d;
Object.assign(getOrBuildPathObject(key), data);
})
should be something like that
I would just iterate through the array and check each key.
Split the key at the dots myArray.split('.') returns an array.
Now iterate through that array and create an Object for each element.
Like
bs.ass.fixAss
Check if a root element bs exists.
If no, create an (empty) bs Element.
Check if an ass element is a child of bs
If no, create an (empty) ass Element
Check if an (empty) fixAss Element exists.
If no, create the fixAss Element with values and add it as child to the ass Element
If yes, fill the values
If its guaranteed that the data will always be in the right order (that means bs.ass.fixAss will always be AFTER bs.ass) then you may skip the checks.
I would use a HashMap for the children (not an array), because that makes it much easier to walk through the tree
myTrees[bs].children[ass].children[fixAss]
The whole thing could be created with plain TypesScript. I do not know any library that would solve this specific problem out of the box.

How to programatically add new children to a deeply nested object

Consider the following object, which is composed by an unknown number of deeply nested children.
const state = {
id: 1,
children: [
{
id: 3,
children: [
{
id: 4,
children: []
}
]
},
{
id: 2,
children: []
}
]
}
How can I programatically push a new object to the children array of a node knowing only its id and the array of ids of its parents?
I thought using recursion but I couldn't find a solution that worked. I am also using immutability-helper, so I have tried using Array.reduce() to return an object that looked like this:
const newState = {
children: {
[idxOfNodeToChange]: {
children: {$push: newChildren}
}
}
}
so I could pass it to update() but there I am even more stuck since I would still have to traverse through the accumulator every time to go as deep as needed, and I'm not sure how to do that. Any ideas?
Extra info: I'm using a D3 library for React called VX and this structure is required to build a tree component, so I'm stuck on how to add new nodes programatically.
Here you go. You can use this recursive function to search by id and append data to the found node's children array.
function appendChildToNode(node, nodeId, data) {
// If the node is empty, just return it as is.
if (!node) {
return node;
}
let children;
if (node.id === nodeId) {
// If the node has the id we're searching for,
// append the data to its children.
children = [...node.children, data];
} else {
// Otherwise, apply the function recursively to each of its children
children = node.children.map(childNode => appendChildToNode(childNode, nodeId, data));
}
return { ...node, children };
}
It is immutable and you may use it like this:
const newState1 = appendChildToNode(state, 4, { id: 5, children: [] });
const newState2 = appendChildToNode(state, 2, { id: 5, children: [] });
See it working in the example snippet below.
const state = {
id: 1,
children: [{
id: 3,
children: [{
id: 4,
children: []
}]
},
{
id: 2,
children: []
}
]
};
const newState1 = appendChildToNode(state, 4, {
id: 5,
children: []
});
const newState2 = appendChildToNode(state, 2, {
id: 5,
children: []
});
console.log(state); // Orginal state should not be mutated.
console.log(newState1);
console.log(newState2);
function appendChildToNode(node, nodeId, data) {
// If the node is empty, just return it as is.
if (!node) {
return node;
}
let children;
if (node.id === nodeId) {
// If the node has the id we're searching for,
// append the data to its children.
children = [...node.children, data];
} else {
// Otherwise, apply the function recursively to each of its children
children = node.children.map(childNode => appendChildToNode(childNode, nodeId, data));
}
return { ...node, children };
}
Update: The above function uses ES6 spread syntax to append items. If you need to support older browsers w/o transpilation, you can use this updated version using Object.assign and Array#concat.
function appendChildToNode(node, nodeId, data) {
if (!node) {
return node;
}
var children;
if (node.id === nodeId) {
children = node.children.concat([data]);
} else {
children = node.children.map(childNode => appendChildToNode(childNode, nodeId, data));
}
return Object.assign({}, node, { children });
}
Let's find the node with ID 3 in our state, searching one level deep. Take the children of the current node, and find the node with correct ID within those children:
id = 3
node3 = state.children.find(v => v == id)
In that node, find ID 4. Now we're searching in the children of node 3:
id = 4 // ┌ act on node3!
node4 = node3.children.find(v => v == id)
Fitting that to Array.reduce(), the accumulator is the current node. It starts at the root node state, and then traverses the tree downwards: each time, we traverse the tree one level, using the next ID from a list of IDs. We need the recursion to start at the root of the tree, so the initial value is state, our root node.
If we take the above examples and reduce them:
[3, 4].reduce((acc, x) => acc.children.find(v => v === x), state)
// ids traverse one level start at root node
Unrolling it, this is equivalent to:
(state.children.find(v => v === 3)).children.find(v => v === 4)
The general form becomes:
const recursiveTraversal = ids =>
ids.reduce((acc, x) => acc.children.find(v => v === x), state)

Redux managing arrays of objects and finding nested objects

I'm not sure what to name this, but basically I'm new to React and Redux and looking for a more correct/cleaner way to do this or just how to do this with my current set up.
I have a state that looks like this
--Character
---id
---name
---race
----id
----raceName
----traits
-----trait
------id
------name
------description
-----trait
------id
------name
------description
---classes
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
----class
-----id
-----className
-----classLevel
-----traits
------trait
-------id
-------name
-------description
------trait
-------id
-------name
-------description
---traits
----trait
-----id
-----name
-----description
----trait
-----id
-----name
-----description
As you can see(hopefully) traits is an array of object TRAIT and classes is an array of object CLASS, in the end the whole state is quite a messy deal. I've read that I can somehow reference them by ID's but I'm not sure how if IDs are autogenerated.
So I kind of have two questions:
How do I simplify/flatten this structure if it even could be done?
If I can't simplify this structure is there anyway I can find a specific Trait with a specific ID without looping through all the objects that have property traits?
Yes. You can find Trait with a specific ID easily. Let know if this is what you are asking.
// Search in traits directly under Character.
const traitForId = this.state.Character.traits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
// Search in the list of traits under each Class.
const classTraits = this.state.Character.classes.map((class) => class.traits).flat();
const classTraitsForId = classTraits.find((trait) => {
return trait.id = "<SPECIFIC_ID>"
})
Find below recursive way to find a Trait irrespective of where it's present in the state.
function findTraitForId(state, specific_id){
if(state.traits){
const traitForId = state.traits.find((trait) => {
return trait.id == specific_id
});
if(traitForId)
return traitForId;
}
return Object.keys(state).filter((key) => key != 'traits').map((stateItem) => {
return findTraitForId(state[stateItem], specific_id);
}).flat();
}
Tried above function for the input
findTraitForId({'classes':[{traits: [{id: 1, name: "A"}, {id: 2, name: "AB"}]}, {traits: [{id: 3, name: "ABC"}, {id: 4, name: "ABCD"}]}], traits: [{id: 5, name: "ABCDE"}, {id: 6, name: "ABCDEF"}]}, 3)
which return
[{id: 3, name: "ABC"}]

Helpers for walk through and searching node in the tree structure (D3.js way)

When I working with tree-structured data in the d3.js I often need to call forEach or filter analog on the data.
For example, here is the data:
data = {
id: 0,
children: [ {
id: 1,
children: [ ... ]
}, {
id: 2,
children: [ ... ]
}]
}
To check if the node with id = 5 exists in the data I have to implement the recursive search function through all the children. Something like this:
function hasNodeWithId(node, id) {
if (node.id === id) {
return true;
} else {
return _.any(node.children || [],
function(n) { return hasNodeWithId(n, id); });
}
}
The search of the node inside the tree is such a common task so I wonder if there are some sort of helper functions in the d3.js library for this. If not, then how do others approach this sort of problems?
P.S.
Sometimes I can use the tree.nodes function to get a flat array:
var theId = 5,
tree = d3.layout.tree(),
nodes = tree.nodes(data), // makes a flat array of all the nodes in the data
theNode = nodes.filter(function(d) { return d.id === theId; });
But calling tree.nodes(data) every time I need to walk through the data is overwhelming and has some side effects (x, y, depth fields) on data.

Categories