From ad5e61d4d2adc6363a22ec77f1dd2a43d5708712 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 22 Sep 2026 23:48:50 -0400 Subject: [PATCH] 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 --- ui/src/views/Forecast.jsx | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index ac98fa1..2ae51d6 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -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) - } + // A few px over: canvas metrics and rendered text differ slightly with font + // fallback and letter-spacing. + 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 }) }