From 484798cac54360328ca1fff4e7121c30ebb57779 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Thu, 17 Sep 2026 13:03:46 -0400 Subject: [PATCH] Hide the grand-total column group in rollup mode Rollup mode emits a grand-total column group alongside the subtotals. The subtotals are the point -- they are what per-branch collapse needs -- but the grand total sums across the split, and when the split is Prior Year / Plan / Forecast that sum is meaningless while looking exactly like a real figure to anyone scanning the sheet. The engine cannot separate the two: t_totals is { BEFORE, HIDDEN, AFTER }, and in a rollup the grand total *is* the root of the hierarchy that produces the subtotals. Turning it off turns the subtotals off with it. So it is hidden rather than suppressed -- the view still computes the column, it is simply not painted, and collapsed to zero width so there is no gap where it was. Targeted by psp-split-total, excluding psp-split-subtotal, which is how the datagrid already distinguishes them. That survives adding a measure or rearranging the pivot; hiding by column position would not -- the group spans one column per measure in `columns`. Injected into the plugin's shadow root, since a document stylesheet cannot reach inside it, and re-asserted on config updates because the plugin element is replaced when the plugin changes. Co-Authored-By: Claude Opus 5 (1M context) --- ui/src/views/Forecast.jsx | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 7bad8c1..f73e983 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -659,6 +659,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // the named table in time. await viewer.load(tableRef.current) viewer.setAttribute('theme', dark ? 'Pro Dark' : 'Pro Light') + hideSplitTotal() // restore last-used layout or build default // Strip cfg.table — table is already loaded by reference above; a stale name @@ -705,6 +706,8 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const live = await viewer.save() adoptSplit(live.split_by || [], live.split_by_depth != null ? live.split_by_depth - 1 : null) + // the plugin element is replaced when the plugin changes, so re-assert + hideSplitTotal() const cfg = await captureConfig() if (cfg) await persistLayout(vid, cfg) } catch {} @@ -809,6 +812,46 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio } } + // In rollup mode the datagrid emits a grand-total column group as well as the + // subtotals. The subtotals are the point -- they are what per-branch collapse + // needs -- but the grand total sums across the split, and when the split is + // Prior Year / Plan / Forecast that sum is meaningless and reads as a real + // figure to anyone scanning the sheet. + // + // The engine cannot separate them: t_totals is { BEFORE, HIDDEN, AFTER }, and in + // a rollup the grand total *is* the root of the hierarchy that produces the + // subtotals. So it is hidden rather than suppressed -- the view still computes + // it, it is simply not painted. + // + // psp-split-total marks it and psp-split-subtotal marks the subtotals, so this + // survives adding measures or rearranging the pivot, unlike hiding by position. + // The rule has to live inside the plugin's shadow root; a document stylesheet + // cannot reach it. + const HIDE_SPLIT_TOTAL_CSS = ` + th.psp-split-total:not(.psp-split-subtotal), + td.psp-split-total:not(.psp-split-subtotal) { + visibility: hidden !important; + width: 0 !important; + min-width: 0 !important; + max-width: 0 !important; + padding: 0 !important; + border-left-width: 0 !important; + border-right-width: 0 !important; + overflow: hidden !important; + } + ` + + function hideSplitTotal() { + const viewer = viewerRef.current + const root = viewer?.shadowRoot + ?.querySelector('perspective-viewer-datagrid')?.shadowRoot + if (!root || root.getElementById?.('pf-hide-split-total')) return + const style = document.createElement('style') + style.id = 'pf-hide-split-total' + style.textContent = HIDE_SPLIT_TOTAL_CSS + root.appendChild(style) + } + // Row depth is a ViewConfig field, so it is set through restore() rather than // by calling set_depth() on the view. That is the whole difference: the config // is what the viewer rebuilds its view *from*, so the depth survives every