Collapse untagged adjustments into one bridge step

buildSteps keyed every untagged log entry by its own pf_logid, so a
version with two dozen routine untagged scale/recode passes drew two
dozen separate bars -- one per operation, none of them named. The
ledger in OperationPanel.jsx already collapses these into a single
"N untagged adjustments" line; Bridge now does the same, and groups
tagged entries case-insensitively too, carrying the most-used spelling
as the label, matching the ledger's own grouping exactly.

The old per-row "untagged"/"×N" captions are dropped where they'd now
just repeat what the collapsed label already says.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-22 22:55:49 -04:00
parent c5993d5f88
commit 6018001f4e

View File

@ -126,17 +126,32 @@ export function buildSteps(rows, {
// label first, the same precedence pf_segment uses, so the bridge and the
// pivot call a step by the same name
const tag = (meta.label || meta.tag || '').trim()
const label = tag || (meta.note || '').trim() ||
`${(meta.operation || r.pf_iter || 'adj')}${r.pf_logid != null ? ` #${r.pf_logid}` : ''}`
const key = tag ? `tag:${tag}` : `log:${r.pf_logid}`
const g = byTag.get(key) ||
{ key, label, tagged: !!tag, value: 0, units: 0, rows: 0, logIds: new Set(), first: r.pf_logid }
// Untagged entries collapse into one step, the same as the ledger's
// "N untagged adjustments" line in OperationPanel.jsx -- one bar per
// untagged scale/recode would otherwise turn a routine cleanup pass into
// a wall of identical-looking bars. Tagged entries group case-
// insensitively for the same reason the ledger does: "Pull Forward" and
// "pull forward" are one initiative, not two, and the most-used spelling
// is what gets shown.
const key = tag ? `tag:${tag.toLowerCase()}` : 'untagged'
const g = byTag.get(key) || {
key, tagged: !!tag, value: 0, units: 0, rows: 0, logIds: new Set(), first: r.pf_logid,
spellings: tag ? new Map() : null,
}
g.value += v; g.units += u; g.rows += 1
if (r.pf_logid != null) { g.logIds.add(r.pf_logid); g.first = Math.min(g.first ?? r.pf_logid, r.pf_logid) }
if (tag) g.spellings.set(tag, (g.spellings.get(tag) || 0) + 1)
byTag.set(key, g)
}
const mid = [...byTag.values()].sort((a, b) => (a.first ?? 0) - (b.first ?? 0))
const mid = [...byTag.values()]
.sort((a, b) => (a.first ?? 0) - (b.first ?? 0))
.map(g => ({
...g,
label: g.tagged
? [...g.spellings.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || ''
: `${g.logIds.size} untagged adjustment${g.logIds.size === 1 ? '' : 's'}`,
}))
const useBasis = !!basis && basisT.rows > 0
const out = []
@ -534,17 +549,14 @@ export default function BridgeView({
<tspan key={li} x={x + barW / 2} dy={li === 0 ? 0 : 11}>{ln}</tspan>
))}
</text>
{/* below the label, however many lines it took */}
{!isAnchor && s.entries > 1 && (
{/* below the label, however many lines it took. Skipped for the
untagged bucket -- its label already reads "N untagged
adjustments", so a ×N here would just repeat the count. */}
{!isAnchor && s.tagged && s.entries > 1 && (
<text x={x + barW / 2} y={subY} textAnchor="middle" fontSize="9" fill={INK_DIM}>
×{s.entries}
</text>
)}
{!s.tagged && !isAnchor && (
<text x={x + barW / 2} y={subY} textAnchor="middle" fontSize="9" fill={INK_DIM}>
untagged
</text>
)}
</g>
)
})}
@ -587,9 +599,7 @@ export default function BridgeView({
<tbody>
{steps.map(s => (
<tr key={s.key} className="border-b border-gray-100">
<td className="py-1.5 pr-3 text-gray-800">
{s.label}{!s.tagged && s.kind === 'step' && <span className="text-gray-600"> · untagged</span>}
</td>
<td className="py-1.5 pr-3 text-gray-800">{s.label}</td>
<td className="py-1.5 px-2 text-right font-mono tabular-nums text-gray-800">
{s.kind === 'anchor' ? fmt(s.end) : fmtSigned(s.delta)}
</td>