diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 778266a..02d7aa1 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -811,6 +811,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // column list. try { await syncOrderExpressions() + await ensureRowLabelWidth() } catch (err) { // Its own try covers only restore(); save() and the builders sit outside // it, and a throw there would abort the rest of initViewer silently. @@ -1085,6 +1086,40 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio } } + // A floor under the row-label columns, not a fit. + // + // Restoring a layout resets the widths, and the row-header columns are then + // sized from their *header* — which for row headers is a blank corner cell — + // so they come back a few pixels wide and have to be dragged open by hand. + // Fitting them to content is the other extreme: each group_by level is its own + // column, so the first widens to its longest label and shoves the second + // rightwards. + // + // A minimum leaves the default spacing alone where it is already reasonable and + // only intervenes where a column came back unusably narrow. Anything already + // wider — set by dragging, or recorded in a layout — is untouched. + const MIN_ROW_LABEL_W = 130 + + async function ensureRowLabelWidth() { + const grid = (await viewerRef.current?.getPlugin())?.regular_table + if (!grid?.saveColumnSizes) return + const idxs = new Set() + for (const cell of grid.querySelectorAll('tbody th')) { + const m = [...cell.classList].map(c => /^rt-col-(\d+)$/.exec(c)).find(Boolean) + if (m) idxs.add(Number(m[1])) + } + if (idxs.size === 0) return + + const sizes = { ...grid.saveColumnSizes() } + let changed = false + for (const idx of idxs) { + if ((sizes[idx] ?? 0) < MIN_ROW_LABEL_W) { sizes[idx] = MIN_ROW_LABEL_W; changed = true } + } + if (!changed) return + grid.restoreColumnSizes(sizes) + await grid.draw({ invalid_columns: true }) + } + // Pin the row-label columns wide enough for the labels they show. // // Measured with a canvas rather than the DOM: the cell is clipped, so reading @@ -1336,6 +1371,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // back. Re-applied after every restore that comes from a saved config, which // is the general form of the fix initViewer already needed. await syncOrderExpressions() + await ensureRowLabelWidth() setActiveLayoutId(layout.id) // After the sync, so the persisted copy carries the expressions too.