Skip to content

ADR-0029: Report chart palette and small-slice merging

  • Status: Accepted
  • Date: 2026-08-23
  • Deciders: maintainers
  • Related: ADR-0028, ui.md, issue #59

Context

reportChartPalette() returned 8 colors straight from MaterialTheme.colorScheme: primary, tertiary, secondary, error, primaryContainer, tertiaryContainer, secondaryContainer, outline. On-device review (light + dark AVDs) found:

  • error red reads as "bad/negative" for an arbitrary category.
  • Container tones are near-invisible against dark-mode cards (legend dots unreadable); outline grey reads as disabled.
  • Adjacent slots can be near-identical hues (primary vs primaryContainer).
  • Slices under ~2% render as hairline arcs with no readable legend row.

Decision

  1. Categorical palette from full-tone accents only (buildReportChartPalette(primary, tertiary, secondary, cardBackground) in ChartPalette.kt, pure JVM-testable):
  2. Slots 0–2 are the theme's primary, tertiary, secondary.
  3. Slots 3–7 derive from primary by 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.
  4. Every slot passes ≥3:1 WCAG contrast against the card's actual container color (surfaceContainer) via ensureContrast, which blends toward white on dark cards and black on light cards until the ratio holds.
  5. Semantic colors (error, container tones, outline) are never inputs.

  6. Small-slice merge: slices under 2% of the month total (SMALL_SLICE_MERGE_THRESHOLD_PCT) merge into one "Other" slice (mergeSmallSlices in ChartAdapters.kt) only when more than one slice qualifies. The merged slice is flagged isOtherBucket with categoryId = null; amounts are exact Long sums 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).