I'm using a table component from a UI framework that generates me the following headers
based on this configuration
(final result)
[
[
{
"title": "Section 1",
"rowspan": "4",
"colspan": "1"
},
{
"title": "Section 2",
"rowspan": "1",
"colspan": "2"
},
{
"title": "Section 3",
"rowspan": "1",
"colspan": "3"
},
{
"title": "Section 4",
"rowspan": "1",
"colspan": "1"
}
],
[
{
"title": "Section 2.1",
"rowspan": "3",
"colspan": "1"
},
{
"title": "Section 2.2",
"rowspan": "3",
"colspan": "1"
},
{
"title": "Section 3.1",
"rowspan": "3",
"colspan": "1"
},
{
"title": "Section 3.2",
"rowspan": "1",
"colspan": "2"
},
{
"title": "Section 4.1",
"rowspan": "1",
"colspan": "1"
}
],
[
{
"title": "Section 3.2.1",
"rowspan": "2",
"colspan": "1"
},
{
"title": "Section 3.2.2",
"rowspan": "2",
"colspan": "1"
},
{
"title": "Section 4.1.1",
"rowspan": "1",
"colspan": "1"
}
],
[
{
"title": "Section 4.1.1.1",
"rowspan": "1",
"colspan": "1"
}
]
]
I want to simplify the configuration. There should be just a tree like structure and I want to calculate the row- and colspans dynamically
(starting point)
[
{
"title": "Section 1"
},
{
"title": "Section 2",
"children": [
{
"title": "Section 2.1"
},
{
"title": "Section 2.2"
}
]
},
{
"title": "Section 3",
"children": [
{
"title": "Section 3.1"
},
{
"title": "Section 3.2",
"children": [
{
"title": "Section 3.2.1"
},
{
"title": "Section 3.2.2"
}
]
}
]
},
{
"title": "Section 4",
"children": [
{
"title": "Section 4.1",
"children": [
{
"title": "Section 4.1.1",
"children": [
{
"title": "Section 4.1.1.1"
}
]
}
]
}
]
}
]
I thought about how to map the starting point array to the final result array. These are my assumptions:
11My current approach
const configuredHeaders = [{
title: "Section 1"
},
{
title: "Section 2",
children: [{
title: "Section 2.1"
},
{
title: "Section 2.2"
}
]
},
{
title: "Section 3",
children: [{
title: "Section 3.1"
},
{
title: "Section 3.2",
children: [{
title: "Section 3.2.1"
},
{
title: "Section 3.2.2"
}
]
}
]
},
{
title: "Section 4",
children: [{
title: "Section 4.1",
children: [{
title: "Section 4.1.1",
children: [{
title: "Section 4.1.1.1"
}]
}]
}]
}];
const finalHeaders = [];
traverseColumnHeaders(configuredHeaders, -1); // first depth should start with 0
function traverseColumnHeaders(headers, parentDepth) {
headers.forEach(header => {
header.currentDepth = parentDepth + 1;
if (header.children) {
traverseColumnHeaders(header.children, header.currentDepth);
header.colspan = header.children.reduce((currentSum, child) => currentSum + child.colspan, 0); // sum of child colspans
header.rowspan = 1;
} else {
header.colspan = 1;
// header.rowspan = ... maximum depth - header.currentDepth ...
}
addHeaderToFinalHeaders(header);
});
}
function addHeaderToFinalHeaders({ currentDepth, title, rowspan, colspan }) {
const depthExistsInFinalHeaders = currentDepth in finalHeaders;
if (!depthExistsInFinalHeaders) {
finalHeaders[currentDepth] = [];
}
finalHeaders[currentDepth].push({ title, rowspan, colspan });
}
console.log(finalHeaders);
This one looks quite good to me because I only need to traverse the collection once. But as you can see some rowspan fields are not calculated yet because I don't know the maximum depth at this point.
To solve this I could initialize a global variable, search for the maximum depth like so
(I took the solution from here Getting the depth of a tree data structure in a simpler way for now)
function getMaximumDepth(headers) {
return getDepth(headers) -1;
};
function getDepth(headers) {
return 1 + Math.max(0, ...headers.map(({ children = [] }) => getDepth(children)));
}
const maximumDepth = getMaximumDepth(configuredHeaders);
and calculate
header.rowspan = maximumDepth - header.currentDepth
but now I have to traverse the structure twice. Is there a way to optimize my solution?
I believe there is no reasonable way to do what you want with only a single iteration. One of the premises of what you're trying to accomplish is that cells must span the rows to the depth of the tree. So that is knowledge that must exist as a prerequisite while traversing the array to transform it into the finalHeaders form.
As I get older I find that I'd rather optimize for code clarity rather than for some execution concern. Obviously I'm not ignoring efficiency as a concern, but even phones are exceeding fast at looping through small datasets. But I have to live with and maintain the code. I'll be cursing myself in 6 months if I write some obtuse code that is "efficient" and I have to spend 45 minutes trying to remember how it even works.
Besides, your goal to only traverse the structure once is already impossible. To determine colspan you are using reduce which is itself iterating over all the children. Plus any display code will be iterating over the data. So there's already a lot of iteration going on. I'd council against trying to come up with some clever way to accomplish this in a single iteration at the cost of code clarity.
The following is a working example. It is essentially what you've outlined in your question with maybe some small tweaks by me but no substantial changes to the logic:
const configuredHeaders = [{
title: "Section 1"
},
{
title: "Section 2",
children: [{
title: "Section 2.1"
},
{
title: "Section 2.2"
}
]
},
{
title: "Section 3",
children: [{
title: "Section 3.1"
},
{
title: "Section 3.2",
children: [{
title: "Section 3.2.1"
},
{
title: "Section 3.2.2"
}
]
}
]
},
{
title: "Section 4",
children: [{
title: "Section 4.1",
children: [{
title: "Section 4.1.1",
children: [{
title: "Section 4.1.1.1"
}]
}]
}]
}
];
const maximumDepth = getMaximumDepth(configuredHeaders);
const finalHeaders = [];
traverseColumnHeaders(configuredHeaders);
function traverseColumnHeaders(headers, depth = 0) {
headers.forEach(header => {
header.depth = depth;
if (header.children) {
traverseColumnHeaders(header.children, depth + 1);
header.colspan = header.children.reduce((currentSum, child) => currentSum + child.colspan, 0); // sum of child colspans
header.rowspan = 1;
} else {
header.colspan = 1;
header.rowspan = maximumDepth - depth;
}
addHeaderToFinalHeaders(header);
});
}
function addHeaderToFinalHeaders({depth, title, rowspan, colspan}) {
if (!finalHeaders[depth]) finalHeaders[depth] = [];
finalHeaders[depth].push({title, rowspan, colspan});
}
function getMaximumDepth(headers) {
return getDepth(headers) - 1;
function getDepth(headers) {
return 1 + Math.max(0, ...headers.map(({children = []}) => getDepth(children)));
}
}
//console.log(finalHeaders);
document.body.insertAdjacentHTML('beforeend', table(finalHeaders));
function table(headers) {
return `
<table>
<thead>
${headers.map(header => `
<tr>${header.map(cell => `<th colspan="${cell.colspan}" rowspan="${cell.rowspan}">${cell.title}</th>`).join('')}</tr>
`).join('')}
</thead>
</table>`;
}
table, th {
border: 1px solid black;
}