Toggle the pivot's grand-total row

The engine has no rollup mode that keeps subtotals and drops the grand
total, so the row is hidden in the grid's style listener instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-25 00:41:00 -04:00
parent 6deb8e547c
commit d9db2b59ab

View File

@ -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_scale_plug_v2', scalePlug) }, [scalePlug])
useEffect(() => { localStorage.setItem('pf_panel_open', panelOpen ? 'open' : 'closed') }, [panelOpen]) 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 // Esc closes the panel — the usual way out of the floating window
useEffect(() => { useEffect(() => {
if (!panelOpen) return if (!panelOpen) return
@ -1219,11 +1229,27 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
style.textContent = ` style.textContent = `
td.pf-group-start { border-left: 2px solid currentColor; opacity: 1; } 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); } 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) ;(root.head || root).appendChild(style)
} }
table.addStyleListener(() => { 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') const body = table.querySelectorAll('tbody td')
// The deepest column path is a leaf; anything shorter is an aggregate of // 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. // the levels below it, which is what makes a subtotal a subtotal.
@ -2120,6 +2146,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
<div className="pf-group-rule" /> <div className="pf-group-rule" />
<DepthButtons label="Rows" levels={groupFull} depth={expandDepth} <DepthButtons label="Rows" levels={groupFull} depth={expandDepth}
onPick={applyDepth} totalLabel="Total" /> onPick={applyDepth} totalLabel="Total" />
<label className="flex items-center gap-1 text-gray-600 cursor-pointer select-none"
title="Show the grand-total row at the top of the pivot">
<input type="checkbox" checked={showTotalRow}
onChange={e => setShowTotalRow(e.target.checked)} />
Total row
</label>
</> </>
)} )}