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