Engineering Journal
Table Formatter
Table Formatter

Transpose as Linear Algebra: What Actually Happens When You Flip a Table

2026-05-11

The Transpose button in TAFNE flips your table. Rows become columns. Columns become rows. It looks like a UI feature. It's a matrix operation.

This post breaks down what's actually happening mathematically, and then shows how the implementation handles the part that makes it genuinely hard: merged cells.

The Mathematics

A matrix transpose is defined as: for every element at position (i, j) in matrix A, place it at position (j, i) in the result.

If your table has dimensions m rows by n columns, the transposed result has n rows by m columns. The shape flips. The data is the same. Only the coordinates change.

In code, a plain 2D array transpose looks like this:

const transposed = [];
for (let c = 0; c < original[0].length; c++) {
    transposed[c] = [];
    for (let r = 0; r < original.length; r++) {
        transposed[c][r] = original[r][c];
    }
}

You iterate columns of the original as rows of the result. Each original[r][c] goes to transposed[c][r]. That's the complete mathematical operation.

For a flat table where every cell occupies exactly one position, this would be enough. HTML tables are not flat.

The Merged Cell Problem

When a cell has colspan="3", it visually occupies three column positions but exists at one DOM element. When a cell has rowspan="2", it occupies two row positions but lives in one place in the HTML tree.

If you apply the naive transpose to a table with merged cells, one of two things happens: you either try to place the same DOM element in multiple positions (which the browser will silently resolve by moving the element to the last position, losing all others), or you lose the information about how much space the cell was supposed to occupy.

Either way, the result is wrong.

The correct approach requires knowing the visual position and span of every cell before the transpose begins. That's the VisualGridMapper's job.

The Implementation

transposeTable() in tableManipulation.js starts by building the visual grid:

const mapper = new VisualGridMapper($originalTable);
const grid = mapper.grid;

grid[row][col] maps every visual coordinate to the DOM element that occupies it, along with an isOrigin flag that marks whether this slot is the element's actual position in the DOM tree.

The transpose iterates columns of the original as rows of the result:

for (let c = 0; c < mapper.maxCols; c++) {
    transposedGrid[c] = [];
    for (let r = 0; r < mapper.maxRows; r++) {
        transposedGrid[c][r] = (grid[r] && grid[r][c]) ? grid[r][c] : null;
    }
}

This is the matrix transposition applied to the visual grid. Position (r, c) in the original becomes position (c, r) in the result. Same math, applied to the spatial map rather than the DOM directly.

Swapping the Spans

When a cell is placed in the transposed table, its span values are swapped:

const cellInfo = mapper.getVisualPosition(gridCell.element);
const newRowspan = cellInfo.colspan;
const newColspan = cellInfo.rowspan;

A cell with colspan="3" and rowspan="1" in the original becomes a cell with rowspan="3" and colspan="1" in the transposed result.

This is mathematically correct. A cell that spans 3 columns in the original occupies 3 positions horizontally. After transposing the space, those 3 positions are now vertical. So the cell should span 3 rows. The span values are coordinates in the cell's local space, and they transform under transposition just like any coordinate.

Handling the Visited Set

The final wrinkle: the visual grid is dense. A cell with colspan="3" appears in three column positions in the same row. After transposing, it appears in three row positions in the same column. When building the transposed table row by row, the function will encounter the same DOM element multiple times: once for each position it occupies.

The visited Set prevents placing the same element more than once:

const visited = new Set();

transposedGrid.forEach((row, rowIndex) => { row.forEach((gridCell, colIndex) => { const key = ${rowIndex},${colIndex}; if (visited.has(key)) return;

if (!gridCell.isOrigin) return; // ... place the cell ...

for (let r = 0; r < newRowspan; r++) { for (let c = 0; c < newColspan; c++) { visited.add(${rowIndex + r},${colIndex + c}); } } }); });

When a cell is placed, all positions it occupies in the transposed grid are marked visited. Subsequent iterations skip those positions. The result is that each DOM element is placed exactly once, in the correct position, with the correct span values.

What Linear Algebra Gave Us

The VisualGridMapper converts the messy, semantically-indexed DOM into a clean, visually-indexed Cartesian grid. Once you have that, matrix transposition is just iterating indices in a different order. The mathematical operation is the same one Cayley described in 1858.

The tricky parts, handling spanning cells, avoiding duplicate insertions, swapping span values correctly, all fall out naturally once the coordinate system is right. The math doesn't change. The representation does.

Source: github.com/carnworkstudios/TAFNE

Read this post in the full Engineering Journal →