Skip to content

[Feature]: Add Pie/Donut chart component #143

Description

@goodbounties-nanoclaw-agent

Feature summary

Add a PieDonutChart component to packages/ui — displays categorical data as
proportional arc segments. innerRadius > 0 renders as a donut with an optional center
metric; innerRadius = 0 renders as a classic pie. First of 4 remaining analytics chart
components completing GoodWidget's analytics visualization layer (Scorecard already
shipped in PR #142).

Problem or opportunity

GoodWidget currently has no reusable pie/donut chart primitive — the only precedent is
FundingDistributionChart in governance-widget, a one-off, non-reusable implementation.
Any widget needing to show "parts of a whole" (budget allocation, market share, chain
distribution) has to build this from scratch. PieDonutChart generalizes the SVG arc
technique into a themeable, cross-platform packages/ui primitive any widget can compose.

Proposed solution

Generalize FundingDistributionChart's SVG arc technique (Circle + strokeDasharray/
strokeDashoffset) into a standalone component following Scorecard.tsx's patterns
(createComponent, useTheme, golden-ratio spacing/type scale, formatMetricValue).

Props / API surface (MVP)

Prop Type Required Default Description
data Array<{ label: string; value: number; color?: string }> Yes - Category data (absolute values)
title string No undefined Chart heading
innerRadius number No 0.6 Inner radius as fraction of outer (0 = pie, 0.6 = donut)
centerLabel string No undefined Top text inside donut hole
centerValue string | number No undefined Main metric in center
centerValueFormatter (value: number) => string No formatMetricValue Center value formatting
centerSubLabel string No undefined Bottom text in center
maxSlices number No 7 Segments beyond this aggregate into "Other"
otherLabel string No "Other" Aggregated remainder label
sort 'descending' | 'ascending' | 'none' No 'descending' Segment sort order
showLegend boolean No true Display legend below chart
showPercentages boolean No true Show percentage in legend items
onSegmentPress (item, index) => void No undefined Segment tap callback
variant 'bare' | 'card' No 'bare' Visual variant
testID string No undefined Testing identifier
accessibilityLabel string No auto-generated Screen reader description
width / height number No 188 / 188 SVG dimensions

Behavioral rules

  1. Segments render as Circle elements with strokeDasharray/strokeDashoffset (strokeWidth 20).
  2. Sorted descending by default; first segment starts at -90° (12 o'clock) via G rotation="-90".
  3. If data.length > maxSlices, smallest items merge into one "Other" segment (last palette color).
  4. Percentage = (item.value / sum(all values)) * 100. Integers show no decimal (25%), non-integers show one (33.3%).
  5. Colors from CHART_COLOR_KEYS = ['primary','success','warning','colorDim','error'] via useTheme(); custom item.color overrides; cycles if data exceeds palette length.
  6. Center metric formatted by centerValueFormatter (default formatMetricValue), constrained to inner-radius width to prevent overflow.
  7. Empty state (no data or all values 0): grey ring ($borderColor, 0.18 opacity), centerLabel or "No data", no legend.
  8. Legend: vertical stack below chart, each row = color swatch + label + percentage.
  9. NaN/null/negative values silently excluded from rendering and total calculation.

Shared constraints (apply to all 4 remaining chart components)

  • react-native-svg only — no Canvas, no Chart.js, no DOM SVG.
  • Tamagui primitives (Stack/YStack/XStack/Text/Heading) for non-SVG layout; useTheme() for all colors — zero hardcoded hex.
  • createComponent() for styled sub-pieces; must support React web, React Native, and Web Components pipelines.
  • Follow Scorecard.tsx's SCORECARD_BASE_SIZE_PX/GOLDEN_RATIO constants for spacing/type scale — do not invent a separate system.
  • No animation (no react-native-reanimated, no transitions). No new dependencies (react-native-svg is already a peerDependency).
  • Visual quality target: Nivo Pie — polished in both light and dark themes. Grid/axis chrome muted, data elements saturated/bold, generous padding, legend dots (not squares) matching FundingDistributionChart's 8x8–11x11 circles.
  • Accessibility: root SVG accessibilityRole="image" + accessibilityLabel; decorative elements accessible={false}; both testID and data-testid on root.

Acceptance criteria

  • Renders proportional arcs starting at 12 o'clock, sorted descending
  • Center label, value, sublabel display in donut mode (innerRadius > 0)
  • Pure pie renders when innerRadius=0 (no center content)
  • Legend shows items with correct swatches and percentages
  • maxSlices=7 with 120 items produces exactly 7 segments
  • Integers show no decimal (25%); non-integers show one (33.3%)
  • Empty state: grey ring + "No data"
  • variant="card" wraps correctly (Card primitive untouched)
  • testID + data-testid both present
  • accessibilityRole="image" on root Svg
  • No hardcoded colors
  • onSegmentPress fires correctly
  • Stress test (120 items): renders without crash, legend doesn't overflow container

Additional context

DO NOT:

  • Copy FundingDistributionChart wholesale — extract only the SVG arc technique
  • Use Canvas or web-only APIs
  • Use Icon.tsx (web-only DOM SVG)
  • Modify Card.ts, Text.ts, Icon.tsx, theme.ts, presets.ts, config.ts, or governance-widget

Scope boundary — only create/modify:

  1. packages/ui/src/components/PieDonutChart.tsx
  2. packages/ui/src/index.ts (add export under // Analytics)
  3. examples/storybook/src/stories/design-system/PieDonutChart.stories.tsx
  4. tests/design-system/smoke.spec.ts (add cases)

Mock data: standard (funding categories), single item, two near-equal, empty, and a
120-item stress test (triggers maxSlices aggregation) — full snippets in the spec (link
below).

References:

Create the plan

Based on the above description create an execution plan in a new sub-issue and preserve the original issue content unchanged.
Do not execute the plan until given instructions to do so.

Sub-issue requirements:

  • Title format: [DRAFT][PLAN] <what issue is being planned>
  • Type: Task
  • Description must start with: <sub-issue title>
  • Link the sub-issue back to this parent issue.

Instructions when creating the plan:

  • Map relevant files that are too be used as reference from all repos mentioned
  • import existing @GoodDollar packages
  • Map new components that should be created. Assess when a new component should be created in the new savings widget package or made part of the reusable packages/ui

Plan specification required sections:

  • Required states, flows, and behaviors
  • Execution plan
  • acceptance criteria
  • human-reviewer checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Prepare AI Task

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions