Create breadcrumbs based on node hierarchy - javascript

Node hierarchy is like:
{
"nodes":[
{
"assetId":"cfe-3a2b-47e7-b7e9-e2e090ca0d34",
"assetName":"IRCTC",
"assetType":"Company"
},
{
"assetId":"32d9-05b8-4293-af55-2ee4617c6ffe",
"assetName":"Northern Railway Fleet",
"assetType":"Fleet"
},
{
"assetId":"15-b76c-426c-a272-6485359c5836",
"assetName":"Vande Bharat Express",
"assetType":"Train"
}
],
"edges":[
{
"source":"cfe-3a2b-47e7-b7e9-e2e090ca0d34",
"destination":"32d9-05b8-4293-af55-2ee4617c6ffe",
"relation":"HAS"
},
{
"source":"32d9-05b8-4293-af55-2ee4617c6ffe",
"destination":"15-b76c-426c-a272-6485359c5836",
"relation": "HAS"
}
]
}
Basically nodes contain set of assets list and edges contain their mapping or relation. We need to traverse nodes first and say I create a function called
createBreadcrumbs("15-b76c-426c-a272-6485359c5836");
It should check for this node ID in edges object and retrieve its parent.
Breadcrumb would be:
"IRCTC > Northern Railway Fleet > Vande Bharat Express"

You can do that by creating intermediate objects nodeIdToName and nodeIdToParentId for easy lookup in a while loop:
const hierarchy = {
nodes: [ { assetId:"cfe-3a2b-47e7-b7e9-e2e090ca0d34", assetName:"IRCTC", assetType:"Company" }, { assetId:"32d9-05b8-4293-af55-2ee4617c6ffe", assetName:"Northern Railway Fleet", assetType:"Fleet" }, { assetId:"15-b76c-426c-a272-6485359c5836", assetName:"Vande Bharat Express", assetType:"Train" } ],
edges: [ { source:"cfe-3a2b-47e7-b7e9-e2e090ca0d34", destination:"32d9-05b8-4293-af55-2ee4617c6ffe", relation:"HAS" }, { source:"32d9-05b8-4293-af55-2ee4617c6ffe", destination:"15-b76c-426c-a272-6485359c5836", relation: "HAS" } ]
};
const nodeIdToName = hierarchy.nodes.reduce((acc, obj) => {
acc[obj.assetId] = obj.assetName;
return acc;
}, {});
const nodeIdToParentId = hierarchy.edges.reduce((acc, obj) => {
acc[obj.destination] = obj.source;
return acc;
}, {});
function createBreadcrumbs(id) {
let breadcrumb = [];
while(nodeIdToName[id]) {
breadcrumb.push(nodeIdToName[id]);
id = nodeIdToParentId[id];
}
return breadcrumb.reverse().join(' > ');
}
const breadcrumb = createBreadcrumbs("15-b76c-426c-a272-6485359c5836");
console.log({
nodeIdToName,
nodeIdToParentId,
breadcrumb
});
Output:
{
"nodeIdToName": {
"cfe-3a2b-47e7-b7e9-e2e090ca0d34": "IRCTC",
"32d9-05b8-4293-af55-2ee4617c6ffe": "Northern Railway Fleet",
"15-b76c-426c-a272-6485359c5836": "Vande Bharat Express"
},
"nodeIdToParentId": {
"32d9-05b8-4293-af55-2ee4617c6ffe": "cfe-3a2b-47e7-b7e9-e2e090ca0d34",
"15-b76c-426c-a272-6485359c5836": "32d9-05b8-4293-af55-2ee4617c6ffe"
},
"breadcrumb": "IRCTC > Northern Railway Fleet > Vande Bharat Express"
}
Note: nodeIdToName and nodeIdToParentId added in output just for illustration

You could create 2 Map objects. One maps each node's assetId -> assetName. And another Map object which maps each edge's destination -> to it's source.
Create a function which takes an id. Get the source and node name for the current id.
If the source is not null, then recursively call the function and append the current crumb at the end. Else, return the ,current crumb
const input = {nodes:[{assetId:"cfe-3a2b-47e7-b7e9-e2e090ca0d34",assetName:"IRCTC",assetType:"Company"},{assetId:"32d9-05b8-4293-af55-2ee4617c6ffe",assetName:"Northern Railway Fleet",assetType:"Fleet"},{assetId:"15-b76c-426c-a272-6485359c5836",assetName:"Vande Bharat Express",assetType:"Train"}],edges:[{source:"cfe-3a2b-47e7-b7e9-e2e090ca0d34",destination:"32d9-05b8-4293-af55-2ee4617c6ffe",relation:"HAS"},{source:"32d9-05b8-4293-af55-2ee4617c6ffe",destination:"15-b76c-426c-a272-6485359c5836",relation:"HAS"}]},
nodeMap = new Map ( input.nodes.map(o => [o.assetId, o.assetName]) ),
edgeMap = new Map ( input.edges.map(o => [o.destination, o.source]) )
function getCrumb(id) {
const crumb = nodeMap.get(id),
source = edgeMap.get(id);
return source
? [getCrumb(source), crumb].join(' > ')
: crumb
}
console.log( getCrumb("15-b76c-426c-a272-6485359c5836") )
console.log( getCrumb("32d9-05b8-4293-af55-2ee4617c6ffe") )

I'll present a solution that uses the same idiomatic data structure as the other answers for this problem: an associative array — however, this answer focuses on using functional programming to arrive at the solution:
The first step is to produce a Map of doubly-linked list nodes from the initial input, which will give you all the relational data that you need to solve the described problem, and to derive any other kinds of relational insights from the example input that you presented:
function linkAssets (graph) {
const map = new Map();
for (const asset of graph.nodes) {
map.set(asset.assetId, {value: asset});
}
for (const edge of graph.edges) {
if (edge.relation === "HAS") {
const previous = map.get(edge.source);
const next = map.get(edge.destination);
if (previous && next) {
previous.next = next;
next.previous = previous;
}
}
}
return map;
}
Then, you can use the map of linked list objects to build your breadcrumb path:
function createBreadcrumbs (
graph,
lastAssetId,
{
delimiter = " > ",
transformFn = (asset) => asset.assetName,
} = {},
) {
const map = linkAssets(graph);
const assetPath = [];
let node = map.get(lastAssetId);
while (node) {
assetPath.unshift(transformFn(node.value));
node = node.previous;
}
return assetPath.join(delimiter);
}
Using it looks like this:
const result = createBreadcrumbs(input, "15-b76c-426c-a272-6485359c5836");
console.log(result); // "IRCTC > Northern Railway Fleet > Vande Bharat Express"
And customizing the output is possible as well:
const result2 = createBreadcrumbs(input, "15-b76c-426c-a272-6485359c5836", {
delimiter: " ➡️ ",
transformFn: a => `${a.assetName} (${a.assetType})`,
});
console.log(result2); // "IRCTC (Company) ➡️ Northern Railway Fleet (Fleet) ➡️ Vande Bharat Express (Train)"
Here's a working code snippet example with the code above and your question input:
"use strict";
function linkAssets (graph) {
const map = new Map();
for (const asset of graph.nodes) {
map.set(asset.assetId, {value: asset});
}
for (const edge of graph.edges) {
if (edge.relation === "HAS") {
const previous = map.get(edge.source);
const next = map.get(edge.destination);
if (previous && next) {
previous.next = next;
next.previous = previous;
}
}
}
return map;
}
function createBreadcrumbs (
graph,
lastAssetId,
{
delimiter = " > ",
transformFn = (asset) => asset.assetName,
} = {},
) {
const map = linkAssets(graph);
const assetPath = [];
let node = map.get(lastAssetId);
while (node) {
assetPath.unshift(transformFn(node.value));
node = node.previous;
}
return assetPath.join(delimiter);
}
const input = {"nodes":[{"assetId":"cfe-3a2b-47e7-b7e9-e2e090ca0d34","assetName":"IRCTC","assetType":"Company"},{"assetId":"32d9-05b8-4293-af55-2ee4617c6ffe","assetName":"Northern Railway Fleet","assetType":"Fleet"},{"assetId":"15-b76c-426c-a272-6485359c5836","assetName":"Vande Bharat Express","assetType":"Train"}],"edges":[{"source":"cfe-3a2b-47e7-b7e9-e2e090ca0d34","destination":"32d9-05b8-4293-af55-2ee4617c6ffe","relation":"HAS"},{"source":"32d9-05b8-4293-af55-2ee4617c6ffe","destination":"15-b76c-426c-a272-6485359c5836","relation":"HAS"}]};
const result = createBreadcrumbs(input, "15-b76c-426c-a272-6485359c5836");
console.log(result);
const result2 = createBreadcrumbs(input, "15-b76c-426c-a272-6485359c5836", {
delimiter: " ➡️ ",
transformFn: a => `${a.assetName} (${a.assetType})`,
});
console.log(result2);
Code in the TypeScript Playground

Related

Optimize solution for faster execution - NodeJs

I am trying optimize a solution which is used to group the objects based on value present in another array (mapping ) which is written is NodeJS.
function workerFunction(report) {
const groupedArray = []
const granularityKey = 'Month'
const mapping = ['TestField1', 'TestField2'] //will be dynamic based on json input
let activeIIndex = 0;
while (activeIIndex < report.length) {
const index = groupedArray
.findIndex(item => mapping
.every((column) => item[column] === report[activeIIndex][column]))
if (index === -1) {
report[activeIIndex][report[activeIIndex][granularityKey]] = report[activeIIndex]['Cost']
groupedArray.push(report[activeIIndex])
} else {
groupedArray[index][report[activeIIndex][granularityKey]] = report[activeIIndex]['Cost']
}
activeIIndex++;
}
return groupedArray
}
The input to the workerFunction(report) will look like this (usually the array size will be above 300k),
[
{
"TestField1":"value",
"TestField2":"value2",
"Cost":12.5555,
"Month":10
},
{
"TestField1":"value3",
"TestField2":"value4",
"Cost":142.5555,
"Month":10
},
{
"TestField1":"value6",
"TestField2":"value4",
"Cost":15.87,
"Month":10
},
{
"TestField1":"value3",
"TestField2":"value4",
"Cost":16.5555,
"Month":11
}
]
The expected output after passing this json object through workerFunction will be
[
{
"TestField1":"value",
"TestField2":"value2",
"Cost":12.5555,
"Month":10,
"10":12.5555
},
{
"TestField1":"value3",
"TestField2":"value4",
"Cost":142.5555,
"Month":10,
"10":142.5555,
"11":16.5555
},
{
"TestField1":"value6",
"TestField2":"value4",
"Cost":15.87,
"Month":10,
"10":15.87
},
]

Need to convert the array of filepaths into treeview json object

Need to convert the array of file paths into Treeview JSON object
Array Data:
[path1/subpath1/file1.doc",
"path1/subpath1/file2.doc",
"path1/subpath2/file1.doc",
"path1/subpath2/file2.doc",
"path2/subpath1/file1.doc",
"path2/subpath1/file2.doc",
"path2/subpath2/file1.doc",
"path2/subpath2/file2.doc",
"path2/subpath2/additionalpath1/file1.doc"]
I want below object Result:
{
"title": "path1",
"childNodes" : [
{ "title":"subpath1", "childNodes":[{ "title":"file1.doc", "childNodes":[] }] },
{ "title":"subpath2", "childNodes":[{ "title":"file1.doc", "childNodes":[] }] }
]
}
I was able to convert it into an object using the below code snippet but not able to transform the way I want it
let treePath = {};
let formattedData = {};
data.forEach(path => {
let levels = path.split("/");
let file = levels.pop();
let prevLevel = treePath;
let prevProp = levels.shift();
levels.forEach(prop => {
prevLevel[prevProp] = prevLevel[prevProp] || {};
prevLevel = prevLevel[prevProp];
prevProp = prop;
});
prevLevel[prevProp] = (prevLevel[prevProp] || []).concat([file]);
});
How can i do this????
You could reduce the parts of pathes and search for same title.
const
pathes = ["path1/subpath1/file1.doc", "path1/subpath1/file2.doc", "path1/subpath2/file1.doc", "path1/subpath2/file2.doc", "path2/subpath1/file1.doc", "path2/subpath1/file2.doc", "path2/subpath2/file1.doc", "path2/subpath2/file2.doc", "path2/subpath2/additionalpath1/file1.doc"],
result = pathes.reduce((r, path) => {
path.split('/').reduce((childNodes, title) => {
let child = childNodes.find(n => n.title === title);
if (!child) childNodes.push(child = { title, childNodes: [] });
return child.childNodes;
}, r);
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

unable to select all checkboxes in tree using angular2-tree on init

Objective : i have a button named "feed data" so when ever i click it the data will be loaded i mean the tree with checkboxes here my requirement is when ever i click it along with data all the check boxes have to be checked on init i tried using
this.treeComp.treeModel.doForAll((node: TreeNode) => node.setIsSelected(true));
but it is not working below is my code
click(tree: TreeModel) {
this.arrayData = [];
let result: any = {};
let rs = [];
console.log(tree.selectedLeafNodeIds);
Object.keys(tree.selectedLeafNodeIds).forEach(x => {
let node: TreeNode = tree.getNodeById(x);
// console.log(node);
if (node.isSelected) {
if (node.parent.data.name) //if the node has parent
{
rs.push(node.parent.data.name + '.' + node.data.name);
if (!result[node.parent.data.name]) //If the parent is not in the object
result[node.parent.data.name] = {} //create
result[node.parent.data.name][node.data.name] = true;
}
else {
if (!result[node.data.name]) //If the node is not in the object
result[node.data.name] = {} //create
rs.push(node.data.name);
}
}
})
this.arrayData = rs;
tree.selectedLeafNodeIds = {};
}
selectAllNodes() {
this.treeComp.treeModel.doForAll((node: TreeNode) => node.setIsSelected(true));
// firstNode.setIsSelected(true);
}
onTreeLoad(){
console.log('tree');
}
feedData() {
const results = Object.keys(this.data.info).map(k => ({
name: k,
children: this.data.info[k].properties
? Object.keys(this.data.info[k].properties).map(kk => ({ name: kk }))
: []
}));
this.nodes = results;
}
feedAnother() {
const results = Object.keys(this.dataa.info).map(k => ({
name: k,
children: this.dataa.info[k].properties
? Object.keys(this.dataa.info[k].properties).map(kk => ({ name: kk }))
: []
}));
this.nodes = results;
}
onActivate(event) {
this.selectedDataList.push(event.node.data);
console.log(this.selectedDataList)
}
onDeactivate(event) {
const index = this.selectedDataList.indexOf(event.node.data);
this.selectedDataList.splice(index, 1);
console.log(this.selectedDataList)
}
below is my stackblitz https://stackblitz.com/edit/angular-hrbppy
Use updatedata and initialized event to update the tree view to check all checkboxes.
app.component.html
<tree-root #tree *ngIf ="nodes" [nodes]="nodes" [options]="options" [focused]="true"
(initialized)="onTreeLoad()"
(updateData)="updateData()"
(select)="onActivate($event)"
(deselect)="onDeactivate($event)">
</tree-root>
It'll initiate tree-root component only if nodes variable is available,
then in the initialized and updateData event call selectAllNodes method to select all checkboxes.
app.component.ts
updateData() {
this.selectAllNodes();
}
onTreeLoad(){
this.selectAllNodes();
}
Refer to this slackblitz for working example.
just, in your function feed data call to your function this.selectAllNodes() enclosed in a setTimeout. You can see your forked stackblitz
setTimeout(()=>{
this.selectAllNodes()
})
NOTE: I see in your code you try to control in diferents ways the items selected. I simplified using a recursive function.
In this.treeComp.treeModel.selectedLeafNodeIds we have the items that are changed, so
getAllChecked()
{
const itemsChecked=this.getData(
this.treeComp.treeModel.selectedLeafNodeIds,null)
console.log(itemsChecked);
}
getData(nodesChanged,nodes) {
nodes=nodes||this.treeComp.treeModel.nodes
let data: any[] = []
nodes.forEach((node: any) => {
//in nodesChanged we has object like {1200002:true,123132321:false...}
if (nodesChanged[node.id]) //can be not changed, and then it's null because
//it's not in object or can be changed to false
data.push({id:node.id,name:node.name})
//or data.push(node.name); //if only need the "name"
if (node.children)
data=[...data,...this.getData(nodesChanged,node.children)]
}
);
return data
}
Updated I updated the function getData to include the "parent" of the node, but looking the code of #Raghul selvam, his function like me more than mine.
getData(nodesChanged,nodes,prefix) {
nodes=nodes||this.treeComp.treeModel.nodes
let data: any[] = []
nodes.forEach((node: any) => {
if (nodesChanged[node.id])
data.push(prefix?prefix+"."+node.name:node.name)
if (node.children)
data=[...data,...this.getData(nodesChanged,node.children,prefix?prefix+"."+node.name:node.name)]
}
);
return data
}
And call it as
this.getData(this.treeComp.treeModel.selectedLeafNodeIds,null,"")
You could add this in your onTreeLoad function. You could add a boolean flag(treeLoaded) for tracking if the tree has loaded or not.
onTreeLoad(tree){
this.selectAllNodes();
this.treeLoaded = true;
}

Issues printing my groups tree in nodeJS app

I'm trying to print all created groups and they're children so it'll look like that:
[ [ 'Father1', 'Child1', 'Child2', 'Child3' ],
[ 'Father1', 'Child1', 'Child4' ],
[ 'Father1', 'Child1', 'Child5' ] ]
The problems I encountered are varied. from:
var keys = name.keys(o); ^ TypeError: name.keys is not a function to total stack overflow, iv'e debugged the printPath function and it's doing it's job separately but not with my final tree structure.
My tree and print function looks like that:
groups.js:
class groups {
constructor() {
this.root = new Group('root');
}
printPath(name){
this.root.getPath(name)
}
group.js:
class Group {
constructor(name, parent) {
this.name = name;
this.parent = parent || null;
this.children = [];
this.users = new users || null;
}
getPath(name) {
function iter(o, p) {
var keys = name.keys(o);
if (keys.length) {
return keys.forEach(function (k) {
iter(o[k], p.concat(k));
});
}
result.push(p);
}
var result = [];
iter(name, []);
return result;
}
Edit:
For creating a group i'm using a menu handler function:
function createGroup(callback) {
rl.question('Add name for father group: \n', (parent) => {
let parentGroup = programdata.groups.findGroupByName(parent);
if (!parentGroup) {
parentGroup = programdata.groups.root;
}
rl.question('name of new group\n', (groupName) => {
parentGroup.setChildren(new Group(groupName, parentGroup));
console.log(parentGroup);
callback();
});
})
}
findGroupByNameis a nice recursion i made that finds nested groups (feel free to use!) sitting in class groups.
findGroupByName(name) {
if (!name) return null;
return this._findGroupByNameInternal(this.root, name);
}
_findGroupByNameInternal(group, name) {
if (!group) return null;
if (group.name === name) return group;
for (const g of group.children) {
const result = this._findGroupByNameInternal(g, name);
if (!result) continue;
return result;
}
}
And setChildren function placed in class Group:
setChildren(child) {
this.children.push(child);
}
EDIT:
Thank you for the answer, could you please help me realize your method in my menu handler? iv'e tried this: and it giving me nothing.
function createGroup(callback) {
rl.question('Add name for father group: \n', (parent) => {
let parentGroup = programdata.groups.findGroupByName(parent);
let treePath = Group.root.printPath();
if (!parentGroup) {
parentGroup = programdata.groups.root;
}
rl.question('name of new group\n', (groupName) => {
parentGroup.addChild(new Group(groupName, parentGroup));
console.log(treePath);
callback();
});
})
}
The root cause you got the error TypeError: name.keys is not a function is that a string is passed into getPath(name) as argument name, you know the JS string object doesn't have a function property keys.
I refactor your code and fix some error, here is the testable version. Pls put them into the same folder and run test.js.
group.js
class Group {
constructor(name, parent) {
this.name = name;
this.parent = parent || null; // Point to this group's father
this.children = []; // Children of this group, can be sub-group or string
if (!!parent) { // Link to the father
parent.addChild(this);
}
// this.users = new users || null; // Useless, remove it.
}
addChild(...args) {
for(let o in args) {
this.children.push(args[o]);
}
}
/**
* Recursion to build the tree
* #param group
* #returns {*}
*/
iter(group) {
let children = group.children;
if (Array.isArray(children)) { // If the child is a group
if (children.length > 0) {
let result = [];
result.push(group.name);
for (let child of children) {
result.push(group.iter(child));
}
return result;
}
else {
return [];
}
}
else { // If the group is a string
return group;
}
}
getPath() {
return this.iter(this);
}
}
module.exports = Group;
groups.js
let Group = require('./group');
class Groups {
constructor() {
this.root = new Group('root');
}
printPath() {
return this.root.getPath();
}
}
module.exports = Groups;
test.js
let Group = require('./group');
let Groups = require('./groups');
// Root
let rootGroups = new Groups();
// Group 1
let group1 = new Group('Father1', rootGroups.root);
group1.addChild('Child1', 'Child2', 'Child3');
// Group 2
let group2 = new Group('Father1', rootGroups.root);
group2.addChild('Child1', 'Child4');
// Group 3
let group3 = new Group('Father1', rootGroups.root);
group3.addChild('Child1', 'Child5');
let treePath = rootGroups.printPath();
console.log(treePath);
The output is:
[ 'root',
[ 'Father1', 'Child1', 'Child2', 'Child3' ],
[ 'Father1', 'Child1', 'Child4' ],
[ 'Father1', 'Child1', 'Child5' ] ]
Process finished with exit code 0
Enjoy it :)
Ok, found a solution.
Treeshow(){
var node = this.root;
var depth = '-'
recurse( node );
function recurse( node) {
depth +='-'
console.log(depth+node.name);
for (var child in node.children ) {
recurse(node.children[child]);
}
depth = depth.slice(0, -1);
}
}
that will show my tree just like that:
--root
---FooFather
----BarSemiFather
-----FooChild
------BarBaby

change an element in an array of objects

I have data in the form of
data = [
{
"date":"2018-05-18T-6:00:00.000Z",
"something":"something1",
"something":"something1"
},
{
"date":"2018-05-19T-6:00:00.000Z",
"something":"something2",
"something":"something2"
}
]
How do I grab the first element in the objects, edit them, then replace them back in the object?
So it should look like this
data = [
{
"date":"2018-05-18",
"something":"something1",
"something":"something1"
}
{
"date":"2018-05-19",
"something":"something2",
"something":"something2"
}
]
I have tried something like this
var date = [];
const getSessions = () => {
loginService.getUser().then((response) => {
var user_id = response.data.id;
console.log("getUser returning this => ", response.data);
loginService.getUserSessions(user_id).then((response) => {
$scope.sessions = response.data;
for (var i = 0; i < $scope.sessions.length; i++){
date.push($scope.sessions[i].next_class.slice(0,10));
};
$scope.sessions.push(date);
console.log($scope.sessions);
This gets the date shortened but doesn't replace the original date in the object.
You can do something like -
var data = [
{
"date":"2018-05-18T-6:00:00.000Z",
"something":"something1",
},
{
"date":"2018-05-19T-6:00:00.000Z",
"something":"something2"
}
]
data.forEach((record) => {
record.date = record.date.split("T")[0]
})
console.log(data);
You can do this also.
`
newArray = data.map(obj => {
dateIntoString = moment(obj.date).format('YYYY-MM-DD');
obj.date = dateIntoString;
return obj;
});
`

Categories