I am trying to create a tree using the array of json objects below. I want to set a category as a child of another category if its sub_category_1 and I want the sub_category_2 also to be a children of that sub_category_1
[
{
category: 'CS',
sub_category_1: null,
sub_category_2: null
}, {
category: 'TS',
sub_category_1: null,
sub_category_2: null
}, {
category: 'CS',
sub_category_1: 'Accuracy',
sub_category_2: null
}, {
category: 'CS',
sub_category_1: 'Accuracy',
sub_category_2: 'Members Accuracy'
}
]
I've attempted to chain lodash methods like groupBy and transform, but am having a hard time getting to the resulting format I require.
Here's a skeleton of what direction I was heading:
_(arr).groupBy('category').transform(function(result, obj, type) {
return result.push({
name: type,
children: obj
});
}).value();
Expected output :
[{
category: 'CS',
children: [
{
category: 'Accuracy',
children: [
{
category: 'Members Accuracy'
}
...
]
}
...
]
}, {
category: 'TS'
}]
If you are certain that the category descriptions will come in the correct order (main, then sub-category, then sub-sub-category) you can use vanilla JavaScript to build up the tree:
// data constructor for a category
const Category = (category, children = []) =>
({ category, children });
// 1. Ensure only the `sub_category_n` values are taken and
// 2. they are in ascending order and
// 3. any null values are removed
const subCategories = obj =>
Object.entries(obj)
.filter(([key]) => /sub_category_\d+/.test(key))
.filter(([, value]) => value != null)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB, {numeric: true}))
.map(([, subCategoryName]) => subCategoryName);
// take a category descriptor and turn it into full path of category names:
// { category: "X", sub_category_1: "Y", sub_category_1: "Z" }
// turns into ["X", "Y", "Z"]
const toPath = ({category, ...sub}) =>
[category, ...subCategories(sub)];
// create a key from the path elements separated by Unit Separator characters
// or return a unique symbol for no parent
const toKey = (path) =>
path.join("\u241F") || Symbol("no parent");
const toHierarchy = arr => {
const result = [];
//keep track of categories that have been created
const seen = new Map();
for (const item of arr) {
const path = toPath(item);
//last item in the path is what we want to create
const childName = path[path.length-1];
//parent key is the path without the last item
const parentKey = toKey(path.slice(0, -1));
//the child key is the full path
const childKey = toKey(path)
//skip if it's seen
if (seen.has(childKey))
continue;
const child = Category(childName);
seen.set(childKey, child);
//if there is no parent, add as a main category. Otherwise as a child
const parentList = seen.get(parentKey)?.children ?? result;
parentList.push(child);
}
return result;
}
const input = [
{
category: 'CS',
sub_category_1: null,
sub_category_2: null
}, {
category: 'TS',
sub_category_1: null,
sub_category_2: null
}, {
category: 'CS',
sub_category_1: 'Accuracy',
sub_category_2: null
}, {
category: 'CS',
sub_category_1: 'Accuracy',
sub_category_2: 'Members Accuracy'
}
];
console.log(toHierarchy(input));
.as-console-wrapper { max-height: 100% !important; }
If the list of categories might be unordered, you can analyse the whole path and create any missing categories, not just one per object:
const Category = (category, children = []) =>
({ category, children });
const subCategories = obj =>
Object.entries(obj)
.filter(([key]) => /sub_category_\d+/.test(key))
.filter(([, value]) => value != null)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB, {numeric: true}))
.map(([, subCategoryName]) => subCategoryName);
const toPath = ({category, ...sub}) =>
[category, ...subCategories(sub)];
const toKey = (path, key = []) =>
path.concat(key).join("\u241F") || Symbol("no parent");
const toHierarchy = arr => {
const result = [];
const seen = new Map()
for (const item of arr) {
const path = toPath(item);
for (const [index, childName] of path.entries()) {
const parentKey = toKey(path.slice(0, index));
const childKey = toKey(path.slice(0, index+1));
if (!seen.has(childKey)) {
const child = Category(childName);
seen.set(childKey, child);
const parentList = seen.get(parentKey)?.children ?? result;
parentList.push(child);
}
}
}
return result;
}
const input = [
{
category: 'CS',
sub_category_1: 'Accuracy',
sub_category_2: null
}, {
category: 'CS',
sub_category_1: null,
sub_category_2: null
}, {
category: 'TS',
sub_category_1: null,
sub_category_2: null
}, {
category: 'CS',
sub_category_1: 'Accuracy',
sub_category_2: 'Members Accuracy'
}
];
console.log(toHierarchy(input));
.as-console-wrapper { max-height: 100% !important; }