diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 26d21ce..ca27135 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -1069,7 +1069,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio grid.resetAutoSize() await grid.draw({ invalid_columns: true }) - await fitRowLabels(viewer, grid) + await fitRowLabels(grid) // Widths are not part of ViewConfig, so persist the layout to keep the // saved copy in step with what is on screen. @@ -1081,47 +1081,53 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio } } - // Pin the row-label column wide enough for the labels it shows. + // 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 - // 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 — // the same circularity that stops headers sizing themselves. // - // __ROW_PATH__ is the key restore_column_size_overrides special-cases for this - // column, and an override is what survives every subsequent draw, which is - // exactly why overrides were defeating resetAutoSize in the first place. - async function fitRowLabels(viewer, grid) { + // Set by column *index* through regular-table's own saveColumnSizes / + // restoreColumnSizes, not as a plugin_config column_size_override. The override + // path maps the key "__ROW_PATH__" to index tree_header_offset - 1, which with + // two group_by levels is index 2 — the first data column, not a row label. + // Pinning that was doing nothing visible because that column is the + // grand-total one, which the stylesheet hides. + async function fitRowLabels(grid) { const cells = [...grid.querySelectorAll('tbody th')] if (cells.length === 0) return - const probe = cells[0] - const style = getComputedStyle(probe) + const style = getComputedStyle(cells[0]) const ctx = (fitRowLabels._ctx ||= document.createElement('canvas').getContext('2d')) ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}` - - // Indentation is real width: a tree label sits inside a flex container with - // the expand control beside it, so measure from the cell's left edge. const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight) - let widest = 0 + + // One width per row-header column, keyed by the rt-col-N the cell carries. + const widest = new Map() for (const cell of cells) { + const col = [...cell.classList] + .map(c => /^rt-col-(\d+)$/.exec(c)) + .find(Boolean) + if (!col) continue + const idx = Number(col[1]) const text = (cell.textContent || '').trim() if (!text) continue - const indent = cell.querySelector('span.rt-tree-container') - ? (parseFloat(getComputedStyle(cell.firstElementChild).paddingLeft) || 0) - : 0 - widest = Math.max(widest, ctx.measureText(text).width + indent) + // Tree indentation occupies real width, so it counts toward the fit. + const inner = cell.querySelector('span.rt-tree-container') + const indent = inner ? (parseFloat(getComputedStyle(inner).paddingLeft) || 0) : 0 + const w = ctx.measureText(text).width + indent + widest.set(idx, Math.max(widest.get(idx) ?? 0, w)) } - if (widest === 0) return + if (widest.size === 0) return - // A few px over, since canvas metrics and rendered text differ slightly with - // font fallback and letter-spacing. - const width = Math.ceil(widest + padding + 8) - - const { table: _t, ...cfg } = await viewer.save() - const pc = { ...(cfg.plugin_config || {}) } - pc.columns = { ...(pc.columns || {}) } - pc.columns.__ROW_PATH__ = { ...(pc.columns.__ROW_PATH__ || {}), column_size_override: width } - await viewer.restore({ ...cfg, plugin_config: pc }) + 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) + } + grid.restoreColumnSizes(sizes) + await grid.draw({ invalid_columns: true }) } const GRID_CSS = `