Show the walk's running total in the ledger

The lines sum to Adjustable, and checking that they do meant adding
eight-digit numbers in your head. The column closes on the Adjustable row, so
the walk visibly lands where it says it does.

Value only. A cumulative price is meaningless -- prices do not add -- and a
second running column for units doubles the width to say what the value
column already implies. It appears only when there is more than one line to
accumulate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Trowbridge 2026-09-18 12:57:50 -04:00
parent 2814b4073c
commit 62c815da63

View File

@ -483,6 +483,21 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
return { ...prev, [key]: { field, raw: cur ? raw : '' } } return { ...prev, [key]: { field, raw: cur ? raw : '' } }
}) })
// A running total across the walk, in the primary measure. The lines sum to
// Adjustable, and without this you are adding eight-digit numbers in your
// head to check that they do.
//
// Value only: a cumulative price is meaningless -- prices do not add -- and a
// second running column for units doubles the width to say something the
// value column already implies.
const runningByKey = (() => {
const out = new Map()
let acc = 0
for (const e of lines) { acc += e.value || 0; out.set(e.key, acc) }
return out
})()
const showRunning = !!valueCol && lines.length > 1
const numCell = 'text-right font-mono tabular-nums whitespace-nowrap px-2' const numCell = 'text-right font-mono tabular-nums whitespace-nowrap px-2'
const rule = <td className="p-0"><div className="border-t border-gray-300 my-1" /></td> const rule = <td className="p-0"><div className="border-t border-gray-300 my-1" /></td>
@ -498,6 +513,11 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
{m.hint && <span className="text-gray-500"> · {m.hint}</span>} {m.hint && <span className="text-gray-500"> · {m.hint}</span>}
</th> </th>
))} ))}
{showRunning && (
<th className="text-right font-normal pb-1 px-2 whitespace-nowrap text-gray-500">
running
</th>
)}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -521,7 +541,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
))} ))}
{exclLines.length > 0 && ( {exclLines.length > 0 && (
<tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}</tr> <tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}{showRunning && <td className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>}</tr>
)} )}
{/* the walk from baseline to current, by initiative */} {/* the walk from baseline to current, by initiative */}
@ -537,10 +557,13 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
{m.key === 'price' ? fmtNum(priceOf(e), m.dp) : fmtNum(e[m.key], m.dp)} {m.key === 'price' ? fmtNum(priceOf(e), m.dp) : fmtNum(e[m.key], m.dp)}
</td> </td>
))} ))}
{showRunning && (
<td className={`${numCell} text-gray-500`}>{fmtNum(runningByKey.get(e.key), 0)}</td>
)}
</tr> </tr>
))} ))}
<tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}</tr> <tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}{showRunning && <td className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>}</tr>
<tr className={onTotal ? 'text-gray-600' : 'font-semibold text-gray-700'}> <tr className={onTotal ? 'text-gray-600' : 'font-semibold text-gray-700'}>
<td className="pr-3 whitespace-nowrap"> <td className="pr-3 whitespace-nowrap">
@ -549,6 +572,9 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
{measures.map(m => ( {measures.map(m => (
<td key={m.key} className={numCell}>{fmtNum(m.current, m.dp)}</td> <td key={m.key} className={numCell}>{fmtNum(m.current, m.dp)}</td>
))} ))}
{showRunning && (
<td className={`${numCell} text-gray-500`}>{fmtNum(total.value, 0)}</td>
)}
</tr> </tr>
@ -571,14 +597,14 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
silently below. */} silently below. */}
{nothingToAdjust && ( {nothingToAdjust && (
<tr> <tr>
<td colSpan={measures.length + 1} className="pt-2 text-amber-700 leading-snug"> <td colSpan={measures.length + 1 + (showRunning ? 1 : 0)} className="pt-2 text-amber-700 leading-snug">
Nothing in this selection can be adjusted all of it is {exclName}, Nothing in this selection can be adjusted all of it is {exclName},
loaded as {(currentTotals?.excludedIters || []).join(' / ') || 'reference'}. loaded as {(currentTotals?.excludedIters || []).join(' / ') || 'reference'}.
</td> </td>
</tr> </tr>
)} )}
<tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}</tr> <tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}{showRunning && <td className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>}</tr>
{/* the edit — three equivalent ways to say the same thing */} {/* the edit — three equivalent ways to say the same thing */}
{FIELDS.map(([field, label]) => ( {FIELDS.map(([field, label]) => (
@ -605,7 +631,7 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se
{outcome && ( {outcome && (
<> <>
<tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}</tr> <tr>{rule}{measures.map(m => <td key={m.key} className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>)}{showRunning && <td className="p-0 px-2"><div className="border-t border-gray-300 my-1" /></td>}</tr>
<tr className="font-semibold text-gray-700"> <tr className="font-semibold text-gray-700">
<td className="pr-3 whitespace-nowrap">Result</td> <td className="pr-3 whitespace-nowrap">Result</td>
{measures.map(m => ( {measures.map(m => (