diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 901a60a..d0f2d9b 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -942,6 +942,11 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio // data that only the cell metadata knows. The listener runs on every draw, so // it survives scrolling and virtualisation -- a stylesheet cannot, since the // DOM cells are recycled across columns as you scroll. + // Blank for this purpose means "no value at this level", which Perspective + // writes as a zero-width space rather than an empty string. + const notBlank = (v) => + v != null && String(v).replace(/[\s\u200b-\u200d\ufeff]/g, '') !== '' + function applyGroupRules() { const grid = gridRef.current const table = grid?.regular_table @@ -967,6 +972,11 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio 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. + // + // The empty levels are not empty strings. Perspective pads a subtotal's + // path with zero-width spaces -- ['04 - Forecast', '\u200b', 'sales_usd'] + // -- so every path is the same length and a naive `!== ''` test finds no + // subtotals at all. let depth = 0 const metas = [] for (const td of body) { @@ -974,7 +984,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio try { meta = table.getMeta(td) } catch { meta = null } metas.push([td, meta]) const path = meta?.column_header - if (Array.isArray(path)) depth = Math.max(depth, path.filter(v => v != null && v !== '').length) + if (Array.isArray(path)) depth = Math.max(depth, path.filter(notBlank).length) } let prevGroup = null @@ -982,7 +992,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio td.classList.remove('pf-group-start', 'pf-subtotal') const path = meta?.column_header if (!Array.isArray(path) || !path.length) { prevGroup = null; continue } - const named = path.filter(v => v != null && v !== '') + const named = path.filter(notBlank) const group = named[0] if (group !== prevGroup) { td.classList.add('pf-group-start'); prevGroup = group } if (named.length < depth) td.classList.add('pf-subtotal')