Tell the bridge which bucket is the forecast
It was the literal 'Forecast'. The moment the buckets were renamed to carry their sort prefix -- '04 - Forecast' -- nothing matched: every row counted as a comparison rather than a step, so the walk had no middle, the loads came to nothing, and the bridge showed the basis cancelling itself exactly to zero with a Forecast anchor of 0.00 over 0 rows. The version's adjustment_bucket is the right source, being the same value an unbucketed adjustment is labelled with, so the bridge and the pivot agree by construction rather than by both hardcoding the same string. If that value names no bucket in the data -- renamed since, or never configured -- it falls back to whichever bucket actually holds the adjustments. A bridge that is merely mislabelled beats one that is silently empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
50f4c50b3a
commit
2814b4073c
@ -71,11 +71,28 @@ export function buildSteps(rows, {
|
||||
valueCol, unitsCol, logMeta = {},
|
||||
excludeIters = ['reference'], // kept for callers with no bucket data
|
||||
basis = null, // a pf_bucket name, or null for composition
|
||||
forecastBucket = 'Forecast',
|
||||
forecastBucket,
|
||||
}) {
|
||||
const hasBuckets = rows.some(r => r.pf_bucket != null)
|
||||
const excl = new Set(excludeIters)
|
||||
|
||||
// Which bucket is the forecast. It used to be the literal 'Forecast', and the
|
||||
// moment the buckets were renamed to carry their sort prefix -- '04 - Forecast'
|
||||
// -- no row matched it: every row counted as a comparison, the walk had no
|
||||
// steps, and the bridge showed the basis cancelling itself to zero.
|
||||
//
|
||||
// The caller passes the version's adjustment_bucket, which is the same value
|
||||
// an unbucketed adjustment is labelled with, so the two agree by construction.
|
||||
// Falling back to whichever bucket actually holds the adjustments keeps a
|
||||
// renamed or unconfigured version working rather than silently empty.
|
||||
const fcBucket = (() => {
|
||||
if (forecastBucket && rows.some(r => r.pf_bucket === forecastBucket)) return forecastBucket
|
||||
const adjusted = rows.find(r => r.pf_bucket && !['baseline', 'reference'].includes(r.pf_iter))
|
||||
if (adjusted) return adjusted.pf_bucket
|
||||
const baseline = rows.find(r => r.pf_bucket && r.pf_iter === 'baseline')
|
||||
return baseline ? baseline.pf_bucket : (forecastBucket || 'Forecast')
|
||||
})()
|
||||
|
||||
const num = (r, col) => (col ? (parseFloat(r[col]) || 0) : 0)
|
||||
const blank = () => ({ value: 0, units: 0, rows: 0 })
|
||||
|
||||
@ -96,7 +113,7 @@ export function buildSteps(rows, {
|
||||
}
|
||||
|
||||
// Anything outside the forecast is a comparison, never a step.
|
||||
if (hasBuckets && bucket !== forecastBucket) {
|
||||
if (hasBuckets && bucket !== fcBucket) {
|
||||
if (basis && bucket === basis) { basisT.value += v; basisT.units += u; basisT.rows += 1 }
|
||||
continue
|
||||
}
|
||||
@ -137,7 +154,7 @@ export function buildSteps(rows, {
|
||||
})
|
||||
} else {
|
||||
out.push({
|
||||
key: 'baseline', label: hasBuckets ? forecastBucket + ' loads' : 'Baseline', kind: 'anchor',
|
||||
key: 'baseline', label: hasBuckets ? fcBucket + ' loads' : 'Baseline', kind: 'anchor',
|
||||
delta: loads.value, start: 0, end: loads.value,
|
||||
units: loads.units, rows: loads.rows, entries: 1,
|
||||
})
|
||||
@ -151,7 +168,7 @@ export function buildSteps(rows, {
|
||||
}
|
||||
|
||||
out.push({
|
||||
key: 'current', label: hasBuckets ? forecastBucket : 'Current', kind: 'anchor',
|
||||
key: 'current', label: hasBuckets ? fcBucket : 'Current', kind: 'anchor',
|
||||
delta: running, start: 0, end: running,
|
||||
units: loads.units + mid.reduce((a, g) => a + (g.units || 0), 0),
|
||||
rows: loads.rows + mid.reduce((a, g) => a + g.rows, 0),
|
||||
@ -194,7 +211,7 @@ export function layoutSteps(steps, width, H = 340, PAD = { t: 24, r: 16, b: 64,
|
||||
export default function BridgeView({
|
||||
open, onClose, tableRef, viewerRef, logMeta = {},
|
||||
valueCol, unitsCol, colMeta = [], slices = [],
|
||||
excludeIters = ['reference'], versionName,
|
||||
excludeIters = ['reference'], versionName, forecastBucket,
|
||||
}) {
|
||||
const hasSelection = slices.length > 0
|
||||
// 'selection' | 'filtered' | 'all'
|
||||
@ -260,14 +277,16 @@ export default function BridgeView({
|
||||
await view.delete()
|
||||
}
|
||||
|
||||
setSteps(buildSteps(rows, { valueCol, unitsCol, logMeta, excludeIters, basis: basis || null }))
|
||||
setSteps(buildSteps(rows, {
|
||||
valueCol, unitsCol, logMeta, excludeIters, basis: basis || null, forecastBucket,
|
||||
}))
|
||||
} catch (err) {
|
||||
setError(err.message || String(err))
|
||||
setSteps(null)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [tableRef, viewerRef, scope, logMeta, valueCol, unitsCol, excludeIters, slices, colMeta, basis])
|
||||
}, [tableRef, viewerRef, scope, logMeta, valueCol, unitsCol, excludeIters, slices, colMeta, basis, forecastBucket])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSelection && scope === 'selection') setScope('filtered')
|
||||
|
||||
@ -2006,6 +2006,11 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
slices={slices}
|
||||
excludeIters={versions.find(v => String(v.id) === String(versionId))?.exclude_iters || ['reference']}
|
||||
versionName={versions.find(v => String(v.id) === String(versionId))?.name}
|
||||
// Which bucket *is* the forecast. Not a constant: it is the version's
|
||||
// adjustment_bucket, which is how an adjustment with no bucket of its
|
||||
// own gets labelled, so the two always agree by construction.
|
||||
forecastBucket={versions.find(v => String(v.id) === String(versionId))?.adjustment_bucket
|
||||
|| '04 - Forecast'}
|
||||
/>
|
||||
|
||||
{/* Main area — the panel lives in one of three shells, chosen by `dock` */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user