ADR-0040: Budget card shows real utilization above 100%
Context
A category card's percentage label was capped at 100% even when spending
exceeded the limit. BudgetScreen computed a single
(spent / limit).coerceIn(0f, 1f) fraction and fed the same clamped value to
both the fill bar and the % label. Only the label was wrong: Over by X and
the error coloring already used the true over-limit comparison, and
BudgetViewModel.utilization (used for ordering) was already uncapped, so the
list ranked over-budget rows correctly while the card read 100%.
Decision
The percentage label shows the real, uncapped utilization, truncated to a
whole number: percent(spent, limit) = (spent * 100 / limit) as an Int,
yielding e.g. 120% for 12000 / 10000. The fill bar keeps clamping its
fraction to [0, 1], so the bar still fills the full width and never overflows
the card.
BudgetViewModel.percent(spent: Long, limit: Long?): Intis the single source of the displayed value; it returns0whenlimitis null or<= 0(categories with no limit show no percentage at all).BudgetScreenkeeps an uncappedfractionfor the label and derivesfillFraction = fraction.coerceIn(0f, 1f)foranimateFloatAsState/fillMaxWidth.- Exactly-at-limit still reads
100%;Over by X, error color, and theNo limit set · spent Xbranch are unchanged.
Consequences
- Over-budget cards communicate how far over they are (
119%,120%, …) instead of plateauing at100%. - The bar remains a bounded progress indicator; it is already the accepted convention that it saturates at full width while the text carries the excess.
- The label and the sort order (
utilization) now agree on the underlying ratio, removing the previous label/order inconsistency.
Rejected alternatives
- Cap the label at 100% — the status quo; hides overspend and contradicts
the already-uncapped ordering and
Over by Xtext. - Let the fill bar exceed full width / overflow the card — breaks the rounded-clip layout and gives no additional information beyond the label.
Addendum — 2026-10-16: progress surface and over-limit contrast (#152)
The card no longer tints its whole surface with the progress fill: text drawn
on that alpha-composited error tint measured only 3.0:1 (light) and 3.5:1
(dark), below the WCAG 4.5:1 normal-text minimum. The clamped fill now renders
in a 6 dp track pinned to the card's bottom edge, and all card text sits on the
opaque card container. The over-limit foreground is
budgetOverLimitTextColor(error, cardContainer), which resolves
ensureContrast(error, cardContainer, 4.5), so it clears 4.5:1 in light, dark
and dynamic palettes. The uncapped % label, the Over by X text and the
error-toned fill keep the over-limit state legible without relying on colour
alone. The clamping behaviour above is unchanged.