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 }) 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 // 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 — // 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 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 // Each group_by level is its own column, so widening any but the last moves
// its longest label pushes the second to start after it — which reads as the // the start of the one after it and the deeper bands step rightwards —
// deeper level being indented past the end of the shallower one. Fitting the // which reads as the deeper level being indented past the end of the
// widest label exactly is therefore right for the label and wrong for the // shallower one. The last column has only data columns after it, so it is
// sheet, and a long customer name would push the rest of the pivot off to the // the one that can grow without disturbing the levels above it. Fitting
// right. // every level is what pushed the intermediate bands too far right.
// //
// The cap trades a rare truncation for a layout that stays legible. Taking // Capped, and never narrowed. The cap trades a rare truncation for a layout
// the max with the current width means Fit never makes a column narrower // that stays legible — this column still pushes the data columns rightward,
// than it already is, so a width set by dragging survives. // 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 MAX_ROW_LABEL_W = 260
const sizes = { ...grid.saveColumnSizes() } const sizes = { ...grid.saveColumnSizes() }
for (const [idx, w] of widest) {
// A few px over: canvas metrics and rendered text differ slightly with font // A few px over: canvas metrics and rendered text differ slightly with font
// fallback and letter-spacing. // fallback and letter-spacing.
const fitted = Math.min(Math.ceil(w + padding + 8), MAX_ROW_LABEL_W) const fitted = Math.min(Math.ceil(widest.get(outer) + padding + 8), MAX_ROW_LABEL_W)
sizes[idx] = Math.max(fitted, sizes[idx] ?? 0) sizes[outer] = Math.max(fitted, sizes[outer] ?? 0)
}
grid.restoreColumnSizes(sizes) grid.restoreColumnSizes(sizes)
await grid.draw({ invalid_columns: true }) await grid.draw({ invalid_columns: true })
} }