ADR-0029: Report chart palette and small-slice merging
Context
reportChartPalette() returned 8 colors straight from MaterialTheme.colorScheme:
primary, tertiary, secondary, error, primaryContainer, tertiaryContainer,
secondaryContainer, outline. On-device review (light + dark AVDs) found:
errorred reads as "bad/negative" for an arbitrary category.- Container tones are near-invisible against dark-mode cards (legend dots
unreadable);
outlinegrey reads as disabled. - Adjacent slots can be near-identical hues (
primaryvsprimaryContainer). - Slices under ~2% render as hairline arcs with no readable legend row.
Decision
- Categorical palette from full-tone accents only
(
buildReportChartPalette(primary, tertiary, secondary, cardBackground)inChartPalette.kt, pure JVM-testable): - Slots 0–2 are the theme's
primary,tertiary,secondary. - Slots 3–7 derive from
primaryby fixed hue rotations (45°/105°/165°/225°/285°) with stepped saturation scales, so adjacent entries never share a hue even when the theme's accents cluster. Derived hues keep Material You / dynamic color working — everything still starts from the user's scheme. - Every slot passes ≥3:1 WCAG contrast against the card's actual
container color (
surfaceContainer) viaensureContrast, which blends toward white on dark cards and black on light cards until the ratio holds. -
Semantic colors (
error, container tones,outline) are never inputs. -
Small-slice merge: slices under 2% of the month total (
SMALL_SLICE_MERGE_THRESHOLD_PCT) merge into one "Other" slice (mergeSmallSlicesinChartAdapters.kt) only when more than one slice qualifies. The merged slice is flaggedisOtherBucketwithcategoryId = null; amounts are exactLongsums so legend, donut and snapshot total always agree.
Consequences
- Dark-mode legend dots and slices are readable; no category carries error-red.
- Percent labels may no longer sum to exactly 100% after rounding — accepted; amounts stay exact.
- The merged "Other" bucket must never drill down to a single category filter (see issue #62 drill-down work).