diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index ca27135..ce580d2 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -1120,11 +1120,25 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio } if (widest.size === 0) return + // Capped, and never narrowed. + // + // 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. + // + // 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. + 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. - sizes[idx] = Math.ceil(w + padding + 8) + const fitted = Math.min(Math.ceil(w + padding + 8), MAX_ROW_LABEL_W) + sizes[idx] = Math.max(fitted, sizes[idx] ?? 0) } grid.restoreColumnSizes(sizes) await grid.draw({ invalid_columns: true })