Make the pivot's filter scope the ledger and the write
perspective-click reports only the clicked cell's own dimensions -- the view-level filter is not in it -- so a slice never carried the season the grid was scoped to. The ledger therefore counted rows the grid was hiding (921,225.71 on screen against 956,485.13 in the panel) and an operation would have written them. Both now read the filter off the viewer. The ledger applies it to its own view, where the values are already in the table's types and any operator works. The operation merges the equalities into each slice, cell values winning on a shared column since a cell cannot contradict the filter it was drawn inside, and refuses outright on any other operator: a range or an in-list cannot travel in a slice, and dropping it silently is the widening this is meant to stop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6b63e9a5f3
commit
9753846d34
@ -430,6 +430,22 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
// filter to nothing, it is dropped: the ledger then totalled rows the pivot
|
||||
// was hiding, which is how a season filter on sseas_e went unnoticed while
|
||||
// the numbers disagreed by exactly the out-of-season rows.
|
||||
// The pivot's own filter is not part of a clicked slice -- perspective-click
|
||||
// reports only the cell's own dimensions -- so the ledger has to read it off
|
||||
// the viewer and apply it alongside. Without this the ledger totals rows the
|
||||
// grid is hiding, and the operation writes them: a grid scoped to
|
||||
// sseas_e = 2027 gave a cell of 921,225.71 against a ledger of 956,485.13.
|
||||
//
|
||||
// Taken from viewer.save(), so the values are already in the table's own
|
||||
// types and the operators are whatever the user set -- ranges and in-lists
|
||||
// included, which a slice cannot express.
|
||||
const viewFilter = await (async () => {
|
||||
try {
|
||||
const cfg = await viewerRef.current?.save()
|
||||
return (cfg?.filter || []).filter(f => Array.isArray(f) && f.length >= 2)
|
||||
} catch { return [] }
|
||||
})()
|
||||
|
||||
const schema = await tableRef.current.schema()
|
||||
const typed = (col, val) => {
|
||||
switch (schema[col]) {
|
||||
@ -445,10 +461,16 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
// columns in the loaded table, so here they filter directly. They have to
|
||||
// be applied, or the ledger totals a wider selection than the operation
|
||||
// will write.
|
||||
const filters = Object.entries(sliceObj)
|
||||
const filters = [
|
||||
...viewFilter,
|
||||
...Object.entries(sliceObj)
|
||||
.filter(([col]) => COMPUTED_SLICE_COLS.has(col) || dimNames.has(col) || dateNames.has(col)
|
||||
|| schema[col] !== undefined)
|
||||
.map(([col, val]) => [col, '==', typed(col, val)])
|
||||
// a cell inside the filtered view cannot contradict it, so a repeated
|
||||
// column is the same predicate twice and harmless
|
||||
.filter(([col]) => !viewFilter.some(f => f[0] === col))
|
||||
.map(([col, val]) => [col, '==', typed(col, val)]),
|
||||
]
|
||||
const view = await tableRef.current.view({ filter: filters })
|
||||
const rows = await view.to_json()
|
||||
await view.delete()
|
||||
@ -1259,7 +1281,24 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
async function submitOp(op) {
|
||||
if (!slices.length) { flash('Select a slice first', 'error'); return }
|
||||
|
||||
const body = buildPayload(op)
|
||||
// The pivot's filter scopes what the ledger counted, so it has to scope what
|
||||
// gets written too -- otherwise the panel shows one number and the operation
|
||||
// changes a larger set. Only equalities can travel in a slice; anything else
|
||||
// is refused rather than dropped, because dropping it is exactly the silent
|
||||
// widening this is here to prevent.
|
||||
let viewFilter = []
|
||||
try {
|
||||
const cfg = await viewerRef.current?.save()
|
||||
viewFilter = (cfg?.filter || []).filter(f => Array.isArray(f) && f.length >= 2)
|
||||
} catch { viewFilter = [] }
|
||||
const unsendable = viewFilter.filter(f => f[1] !== '==')
|
||||
if (unsendable.length) {
|
||||
flash(`The pivot filter ${unsendable.map(f => f.join(' ')).join(', ')} cannot be `
|
||||
+ `applied to an operation. Narrow the selection instead, or use "==".`, 'error')
|
||||
return
|
||||
}
|
||||
|
||||
const body = buildPayload(op, viewFilter)
|
||||
if (!body) return
|
||||
if (body.slices.some(sl => !Object.keys(sl).length)) {
|
||||
flash('No dimension or date columns in slice — check col_meta', 'error'); return
|
||||
@ -1388,8 +1427,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
return out
|
||||
}
|
||||
|
||||
function buildPayload(op) {
|
||||
function buildPayload(op, viewFilter = []) {
|
||||
if (!slices.length) return null
|
||||
const scope = Object.fromEntries(viewFilter.map(([col, , val]) => [col, val]))
|
||||
// Two clicked cells can differ only by a column the operation cannot filter on
|
||||
// (pf_iter, say, which is not in col_meta and so is dropped here). Those become
|
||||
// the same effective slice, and sending it twice would apply the change twice
|
||||
@ -1397,7 +1437,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
const seen = new Set()
|
||||
const effectiveSlices = []
|
||||
for (const sl of slices) {
|
||||
const eff = buildEffectiveSlice(sl)
|
||||
// the view's scope first, so a cell's own value wins if they name the
|
||||
// same column -- it cannot contradict the filter it was drawn inside
|
||||
const eff = buildEffectiveSlice({ ...scope, ...sl })
|
||||
const key = JSON.stringify(Object.keys(eff).sort().map(k => [k, eff[k]]))
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user