diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index fd9ff0c..ce4a1d0 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -180,6 +180,16 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio useEffect(() => { localStorage.setItem('pf_scale_plug_v2', scalePlug) }, [scalePlug]) useEffect(() => { localStorage.setItem('pf_panel_open', panelOpen ? 'open' : 'closed') }, [panelOpen]) + // Whether the grand-total row is drawn. Per-browser, like the panel state: it + // is how you like to read the grid, not a property of the layout. + const [showTotalRow, setShowTotalRow] = useState(() => localStorage.getItem('pf_total_row') !== 'hidden') + const showTotalRowRef = useRef(showTotalRow) + useEffect(() => { + localStorage.setItem('pf_total_row', showTotalRow ? 'shown' : 'hidden') + showTotalRowRef.current = showTotalRow + gridRef.current?.regular_table?.draw() + }, [showTotalRow]) + // Esc closes the panel — the usual way out of the floating window useEffect(() => { if (!panelOpen) return @@ -1219,11 +1229,27 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio style.textContent = ` td.pf-group-start { border-left: 2px solid currentColor; opacity: 1; } td.pf-subtotal { font-weight: 600; background: color-mix(in srgb, currentColor 7%, transparent); } + tr.pf-total-row { display: none; } ` ;(root.head || root).appendChild(style) } table.addStyleListener(() => { + // The grand-total row, hidden on request. The engine has no rollup mode + // for "subtotals but no grand total" -- rollup, flat and total are the + // three -- so it is dropped here instead. It is view row 0 with a row + // header blank at every level; y alone is not enough, since under flat + // rollup there is no total and row 0 is a leaf. Tagged per draw, because + // the grid recycles its tr elements as it scrolls. + for (const tr of table.querySelectorAll('tbody tr')) { + const th = tr.querySelector('th') + let meta + try { meta = th ? table.getMeta(th) : null } catch { meta = null } + const isTotal = meta?.y === 0 && Array.isArray(meta.row_header) && + !meta.row_header.some(notBlank) + tr.classList.toggle('pf-total-row', isTotal && !showTotalRowRef.current) + } + const body = table.querySelectorAll('tbody td') // The deepest column path is a leaf; anything shorter is an aggregate of // the levels below it, which is what makes a subtotal a subtotal. @@ -2120,6 +2146,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
+ )}