Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

279
Views
How to calculate row- and colspans dynamically?

I'm using a table component from a UI framework that generates me the following headers

enter image description here

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:

  • Traverse the array recursively
  • Calculate the colspan (width) like so
    • if there are no children => 1
    • if there are any children => sum of colspans from all children of the next depth
  • Calculate the rowspan (height)
    • if there are no children => maximum depth from the whole tree - current depth
    • if there are any children => 1

My 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?

over 4 years ago · Santiago Trujillo
1 answers
Answer question

0

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;
}

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!