diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 5982bfd..3f4fb3e 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -419,22 +419,31 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio const dateNames = new Set(colMetaRef.current.filter(c => c.role === 'date').map(c => c.cname)) const ITER_ORDER = ['baseline', 'scale', 'recode', 'clone'] + // A slice carries every value as a string -- it is built from filters the + // grid reports and from a payload that has to survive JSON. Perspective + // matches on type, so a string '2027' against an integer column does not + // 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. + const schema = await tableRef.current.schema() + const typed = (col, val) => { + switch (schema[col]) { + case 'integer': case 'float': return Number(val) + case 'boolean': return val === true || val === 'true' + case 'date': case 'datetime': return Number(val) + default: return String(val) + } + } + async function totalsFor(sliceObj) { - const filters = [ - // pf_segment and pf_bucket are computed server-side but are ordinary - // string 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. - ...Object.entries(sliceObj) - .filter(([col]) => COMPUTED_SLICE_COLS.has(col)) - .map(([col, val]) => [col, '==', String(val)]), - ...Object.entries(sliceObj) - .filter(([col]) => dimNames.has(col)) - .map(([col, val]) => [col, '==', val]), - ...Object.entries(sliceObj) - .filter(([col]) => dateNames.has(col)) - .map(([col, val]) => [col, '==', Number(val)]), - ] + // pf_segment and pf_bucket are computed server-side but are ordinary + // 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) + .filter(([col]) => COMPUTED_SLICE_COLS.has(col) || dimNames.has(col) || dateNames.has(col) + || schema[col] !== undefined) + .map(([col, val]) => [col, '==', typed(col, val)]) const view = await tableRef.current.view({ filter: filters }) const rows = await view.to_json() await view.delete()