Fit only the outermost row-label column

Fitting every row-header column widened the indentation bands too, and
since each group_by level is its own column, widening any but the last
moves the start of the one after it -- the deeper bands stepped right.
Only the last column, the wall before the data columns start, can grow
without disturbing the levels above it, so that is the only one resized
now. Every level is still measured, because which column is outermost
depends on the depth on show.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-22 23:48:50 -04:00
parent 87ad5b1997
commit ad5e61d4d2

View File

@ -1289,7 +1289,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
await grid.draw({ invalid_columns: true })
}
// Pin the row-label columns wide enough for the labels they show.
// Pin the outermost row-label column wide enough for the labels it shows.
// Every level is measured, because which column is the outermost depends on
// the depth on show, but only that one is resized — see below.
//
// Measured with a canvas rather than the DOM: the cell is clipped, so reading
// its box back gives the width it was allotted, not the width of its text —
@ -1328,26 +1330,26 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
}
if (widest.size === 0) return
// Capped, and never narrowed.
// Only the outermost row-header column is fitted.
//
// Each group_by level is its own row-header column, so widening the first to
// its longest label pushes the second to start after it — which reads as the
// deeper level being indented past the end of the shallower one. Fitting the
// widest label exactly is therefore right for the label and wrong for the
// sheet, and a long customer name would push the rest of the pivot off to the
// right.
// Each group_by level is its own column, so widening any but the last moves
// the start of the one after it and the deeper bands step rightwards —
// which reads as the deeper level being indented past the end of the
// shallower one. The last column has only data columns after it, so it is
// the one that can grow without disturbing the levels above it. Fitting
// every level is what pushed the intermediate bands too far right.
//
// The cap trades a rare truncation for a layout that stays legible. Taking
// the max with the current width means Fit never makes a column narrower
// than it already is, so a width set by dragging survives.
// Capped, and never narrowed. The cap trades a rare truncation for a layout
// that stays legible — this column still pushes the data columns rightward,
// it just no longer pushes other labels. Taking the max with the current
// width means a width set by dragging survives.
const outer = Math.max(...widest.keys())
const MAX_ROW_LABEL_W = 260
const sizes = { ...grid.saveColumnSizes() }
for (const [idx, w] of widest) {
// A few px over: canvas metrics and rendered text differ slightly with font
// fallback and letter-spacing.
const fitted = Math.min(Math.ceil(w + padding + 8), MAX_ROW_LABEL_W)
sizes[idx] = Math.max(fitted, sizes[idx] ?? 0)
}
const fitted = Math.min(Math.ceil(widest.get(outer) + padding + 8), MAX_ROW_LABEL_W)
sizes[outer] = Math.max(fitted, sizes[outer] ?? 0)
grid.restoreColumnSizes(sizes)
await grid.draw({ invalid_columns: true })
}