diff --git a/ui/src/components/BridgeView.jsx b/ui/src/components/BridgeView.jsx index c2a14b7..c959db8 100644 --- a/ui/src/components/BridgeView.jsx +++ b/ui/src/components/BridgeView.jsx @@ -12,12 +12,6 @@ import { useState, useEffect, useRef, useCallback } from 'react' -// pf_segment / pf_bucket are computed server-side but are ordinary columns in -// the loaded table, so a slice keyed on one of them still filters directly. -// Mirrors the copy in Forecast.jsx -- dropping them here is what let a -// bucket-keyed slice bridge more rows than the operation it was drawn from. -const COMPUTED_SLICE_COLS = new Set(['pf_segment', 'pf_bucket']) - const UP = '#2a78d6' // increase const DOWN = '#e34948' // decrease const ANCHOR = '#6b7280' // baseline / current — neutral, 4.83:1 on white @@ -290,6 +284,14 @@ export default function BridgeView({ // The union of the selected slices — the same reach an operation would // have. Perspective view filters are AND-only, so each slice needs its own // view; rows matching more than one slice are counted once. + // + // pf_bucket/pf_segment are deliberately left out here, even though a + // click can carry one (e.g. the column axis split by bucket): the walk + // needs rows from every bucket, not just the one the slice landed on, + // or there is nothing left for "Compare to" to compare against and + // every basis collapses to the one bucket fetched. Which bucket the + // walk starts and ends on is decided inside buildSteps, from + // forecastBucket, not by filtering it out of the fetch. const dimNames = new Set(colMeta.filter(c => c.role === 'dimension').map(c => c.cname)) const dateNames = new Set(colMeta.filter(c => c.role === 'date').map(c => c.cname)) const schema = await tableRef.current.schema() @@ -301,18 +303,33 @@ export default function BridgeView({ default: return String(val) } } + // The pivot's own filter (e.g. sseas_e <= 2027) is not part of a + // clicked slice — perspective-click reports only the cell's own + // dimensions — so it has to be read off the viewer and applied + // alongside every per-slice view, the same reason Forecast.jsx's + // ledger totals do this. Without it, a grid scoped narrower than the + // whole version bridges rows the grid is hiding. + let viewFilter = [] + let expressions = {} + if (viewerRef?.current) { + const cfg = await viewerRef.current.save() + viewFilter = (cfg.filter || []).filter(f => Array.isArray(f) && f.length >= 2) + expressions = cfg.expressions || {} + } const seen = new Set() rows = [] for (const sl of slices) { - const f = Object.entries(sl) - .filter(([c]) => COMPUTED_SLICE_COLS.has(c) || dimNames.has(c) || dateNames.has(c)) + const sliceFilters = Object.entries(sl) + .filter(([c]) => (dimNames.has(c) || dateNames.has(c)) && !viewFilter.some(vf => vf[0] === c)) // A null is filtered with `is null`, not `== null`: the engine // matches on type, and a null threshold against a string column is // the same dropped predicate that widens a slice back toward its // parent group — see totalsFor in Forecast.jsx for the same fix. .map(([c, v]) => (v === null ? [c, 'is null'] : [c, '==', typed(c, v)])) - if (!f.length) continue - const view = await tableRef.current.view({ filter: f }) + if (!sliceFilters.length) continue + const viewCfg = { filter: [...viewFilter, ...sliceFilters] } + if (Object.keys(expressions).length) viewCfg.expressions = expressions + const view = await tableRef.current.view(viewCfg) const part = await view.to_json() await view.delete() for (const r of part) {