Pin the row-label columns by index, not through __ROW_PATH__

Row labels still did not fit, because the override was landing on the wrong
column. restore_column_size_overrides maps the key "__ROW_PATH__" to index
tree_header_offset - 1, which with two group_by levels is index 2 — the
first data column. That column is the grand total, which the stylesheet
hides, so pinning it changed nothing visible.

The row labels are separate columns (rt-col-0 and rt-col-1 here, one per
group_by level), so each is measured on its own and set by index through
regular-table's saveColumnSizes / restoreColumnSizes, which are index-based
and public.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-17 14:59:34 -04:00
parent e7c266a871
commit 9ccaabd4c1

View File

@ -1069,7 +1069,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
grid.resetAutoSize() grid.resetAutoSize()
await grid.draw({ invalid_columns: true }) 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 // Widths are not part of ViewConfig, so persist the layout to keep the
// saved copy in step with what is on screen. // 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 // 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. // the same circularity that stops headers sizing themselves.
// //
// __ROW_PATH__ is the key restore_column_size_overrides special-cases for this // Set by column *index* through regular-table's own saveColumnSizes /
// column, and an override is what survives every subsequent draw, which is // restoreColumnSizes, not as a plugin_config column_size_override. The override
// exactly why overrides were defeating resetAutoSize in the first place. // path maps the key "__ROW_PATH__" to index tree_header_offset - 1, which with
async function fitRowLabels(viewer, grid) { // 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')] const cells = [...grid.querySelectorAll('tbody th')]
if (cells.length === 0) return if (cells.length === 0) return
const probe = cells[0] const style = getComputedStyle(cells[0])
const style = getComputedStyle(probe)
const ctx = (fitRowLabels._ctx ||= document.createElement('canvas').getContext('2d')) const ctx = (fitRowLabels._ctx ||= document.createElement('canvas').getContext('2d'))
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}` 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) 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) { 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() const text = (cell.textContent || '').trim()
if (!text) continue if (!text) continue
const indent = cell.querySelector('span.rt-tree-container') // Tree indentation occupies real width, so it counts toward the fit.
? (parseFloat(getComputedStyle(cell.firstElementChild).paddingLeft) || 0) const inner = cell.querySelector('span.rt-tree-container')
: 0 const indent = inner ? (parseFloat(getComputedStyle(inner).paddingLeft) || 0) : 0
widest = Math.max(widest, ctx.measureText(text).width + indent) 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 const sizes = { ...grid.saveColumnSizes() }
// font fallback and letter-spacing. for (const [idx, w] of widest) {
const width = Math.ceil(widest + padding + 8) // A few px over: canvas metrics and rendered text differ slightly with font
// fallback and letter-spacing.
const { table: _t, ...cfg } = await viewer.save() sizes[idx] = Math.ceil(w + padding + 8)
const pc = { ...(cfg.plugin_config || {}) } }
pc.columns = { ...(pc.columns || {}) } grid.restoreColumnSizes(sizes)
pc.columns.__ROW_PATH__ = { ...(pc.columns.__ROW_PATH__ || {}), column_size_override: width } await grid.draw({ invalid_columns: true })
await viewer.restore({ ...cfg, plugin_config: pc })
} }
const GRID_CSS = ` const GRID_CSS = `