You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Type: Task Parent issues:#143 (primary), #144, #145, #146 Sub-issue link: to be created as a sub-issue of #143, cross-referencing #144/#145/#146, once posted
[DRAFT][PLAN] Add Pie/Donut, Bar, Line/Area, and Data Table chart components to packages/ui
This plans the implementation of the 4 remaining analytics chart components described in #143–#146: PieDonutChart, BarChart, LineAreaChart, and DataTable, all for packages/ui. Combined into one plan per Thales's request — they share one PR (#142),
one file-touch footprint (packages/ui/src/index.ts, tests/design-system/smoke.spec.ts),
and one build order. Scorecard (the first of these 5 components) already shipped in
PR #142 per its own plan, #141.
Reference files mapped
GoodDollar/GoodWidget (this repo):
packages/ui/src/components/Scorecard.tsx — the pattern all 4 components must follow: createComponent-based internal Frame/Text sub-pieces, useTheme() for every color,
a private resolveThemeColor(theme, token) helper with a $color fallback, and the SCORECARD_BASE_SIZE_PX = 24 / GOLDEN_RATIO = 1.618 constants driving all spacing
and type-scale math. Confirmed live on feat/analytics-components (renamed branch).
packages/governance-widget/src/FundingDistributionChart.tsx — direct precedent for PieDonutChart's SVG arc technique (Circle + strokeDasharray/strokeDashoffset, G rotation="-90" origin trick, CHART_COLOR_KEYS-style palette). Extract the
technique; do not import from governance-widget or modify this file (per DO-NOT).
packages/ui/src/utils/formatMetricValue.ts — shared number formatter (compact
K/M/B/T, decimal, none), already exported from packages/ui. All 4 new components
use this directly for axis/value/cell formatting — no new formatter needed.
packages/ui/src/createComponent.ts — styled-component wrapper + theme-manifest
auto-registration; every internal styled sub-piece in all 4 components uses this.
packages/ui/src/components/Card.ts — variant="card" wraps each component's content
in this existing primitive (same approach as Scorecard), never modified directly.
packages/ui/src/theme.ts / presets.ts — confirmed $primary, $success, $warning, $colorDim/$placeholderColor, $error, $borderColor all resolve
under the default preset for the CHART_COLOR_KEYS palette
(['primary','success','warning','colorDim','error']).
examples/storybook/src/stories/design-system/Scorecard.stories.tsx and TokenAmount.stories.tsx — story pattern to follow (title: 'Design System/ Primitives/<Component>', decorators: [withDefaultPreset], tags: ['autodocs', 'showcase'], multi-instance Default story covering all mock-data rows/states).
tests/design-system/smoke.spec.ts — Playwright convention for packages/ui
primitives (gotoStory/getStoryFrame/screenshotStory helpers); this is the
correct convention here, not tests/widgets/<name>/states.spec.ts (that's for widget
packages, per standing repo convention).
packages/ai-credits-widget/src/CreditsManagementCard.tsx — StatCell/stat-grid
aesthetic reference cited for DataTable, per [Feature]: Add Data Table component #146.
External reference: Nivo Pie/Bar/Line/stacked-Area demos (nivo.rocks) — visual
quality bar for all 4, per each issue and Thales's Notion mockups.
Source of truth: full spec at GoodDollar/data-team (branch feat/pipeline-v5-hardening) projects/goodwidget-components/spec.md — already inlined
per-component into issues #143–#146; re-verified live for this plan (confirmed props,
behavioral rules, scope boundaries, and DO-NOT lists match between spec and issues).
react-native-svg — Svg, Path, Circle, Rect, Line, G, Defs, LinearGradient, Stop (Pie/Donut, Bar, Line/Area only — DataTable is pure Tamagui
layout, no SVG).
Reusable "parts of a whole" primitive any widget can compose; matches issue's explicit instruction and the FundingDistributionChart/Scorecard precedent of one self-contained file per primitive.
BarChart
packages/ui/src/components/BarChart.tsx (new)
Same reasoning — categorical comparison is a cross-widget need, not specific to any one widget package.
Same reasoning — trend/time-series display, the most broadly reusable of the 4.
DataTable
packages/ui/src/components/DataTable.tsx (new)
Same reasoning — exact-value lookup complement to the 3 chart types; pure Tamagui layout keeps it consistent with the others despite no SVG use.
resolveThemeColor helper
New:packages/ui/src/utils/resolveThemeColor.ts, exported from packages/ui
Spec's Appendix explicitly leaves this as an open call ("duplicate into each chart component or extract to packages/ui/src/utils/"). Scorecard.tsx already has a private copy; duplicating it 4 more times (5 total) crosses from "acceptable repetition" into a real shared utility — same justification formatMetricValue used when extracted for Scorecard's 2nd-consumer case, and this is now a 5th+ consumer. Proposing extraction; not touching Scorecard.tsx to point at it — that file already shipped under a prior issue and is out of this task's scope boundary. Flagged below as a human-reviewer call, not made silently.
Required states, flows, and behaviors
Shared across all 4: empty state (no data — themed placeholder, never a crash), a
single-item/point state, a stress-test state at each component's documented volume
ceiling (120 items / 150 categories / 1000+ points / 150 rows) that degrades gracefully
(clip/truncate/aggregate) rather than crashing, bare vs card variant, full testID/data-testid/accessibilityRole="image" (SVG components) baseline, zero
hardcoded colors, no animation, no new dependencies.
PieDonutChart (#143): pie (innerRadius=0) vs donut (innerRadius>0, with optional
center label/value/sublabel) modes; descending-sort-by-default arcs starting at 12
o'clock; maxSlices aggregation into "Other"; percentage formatting (int vs 1-decimal);
legend with swatch+label+percentage; onSegmentPress; NaN/null/negative values silently
excluded from render and total.
BarChart (#144): vertical vs horizontal layout (axes swap); zero-inclusive y-axis
with nice-number ticks (~5); barWidth/gap formula; single-series only ($primary);
optional value labels (hidden below 20px bar height); dashed grid lines; label
truncation/right-alignment; onBarPress; NaN/null excluded silently.
LineAreaChart (#145):linear/monotone/step interpolation; optional area fill
via vertical LinearGradient; multi-series (each own Path+color); connectNulls
true/false gap behavior; auto dot visibility (<20 points); nice-number y-ticks +
adaptive x-label thinning; reference lines; optional secondary y-axis (flagged as a
readability caveat in acceptance criteria, not a bug); onPointPress.
Build order per spec/issues: Pie/Donut → Bar → Line/Area → Table. All work targets
the already-renamed feat/analytics-components branch (PR #142).
Shared prep: extract resolveThemeColor to packages/ui/src/utils/resolveThemeColor.ts
(theme token → resolved color string, $color fallback + console warning, mirroring Scorecard.tsx's private version) and export it from packages/ui's index. One-time
step before component 1, since Pie/Donut needs it first.
Exports: each component + its prop types added to packages/ui/src/index.ts
under the existing // Analytics section (alongside Scorecard), in build order.
Storybook stories: one .stories.tsx per component under examples/storybook/src/stories/design-system/, each with a Default story covering
every required state (bare, card, empty, single-item, stress) using the mock datasets
from spec.md/issues, plus a Controllable story where relevant.
Playwright smoke tests: one test case per component added to tests/design-system/smoke.spec.ts (gotoStory/screenshotStory pattern);
commit baseline PNGs to tests/design-system/test-results/story-<component>-default.png.
Manual QA per component, immediately after each is built (not batched at the
end): build/lint/typecheck scoped to changed files; visual check in Storybook against
the Nivo target and Thales's mockups, both light and dark theme.
Push incrementally to feat/analytics-components; do not wait on Laurence's review
at this stage (confirmed by Thales — full 5-component review happens once all 4 land).
PR description updated to list all 4 new components and note the resolveThemeColor
extraction decision (so reviewers see it called out, not discovered in the diff).
All 4 use react-native-svg only where SVG is needed (Table: none), zero
hardcoded colors, no animation, no new dependencies
All 4 follow Scorecard.tsx's spacing/type-scale constants — no separate spacing
system invented
resolveThemeColor extracted to packages/ui/src/utils/ and used consistently by
all 4 new components
All 4 exported from packages/ui/src/index.ts under // Analytics
Storybook stories exist for all 4 (Design System/Primitives/<Component>),
covering every required state
tests/design-system/smoke.spec.ts has a passing case per component, each with a
committed baseline screenshot
None of Card.ts, Text.ts, Icon.tsx, theme.ts, presets.ts, config.ts,
or any governance-widget file modified
Screenshots of bare + card variants for all 4 components in dark theme provided
(per spec.md Appendix) alongside light-theme equivalents
Human-reviewer checklist
Verify the resolveThemeColor extraction is a good call — and whether Scorecard.tsx's existing private copy should be migrated to it in a follow-up
(explicitly out of scope here; flagging so it isn't lost)
Verify all 4 components' visual output actually reaches the Nivo-quality bar in
both light and dark theme, not just "renders without crashing"
Verify each stress test (120/150/1000+/150) degrades gracefully rather than
merely "not crashing" — check legend overflow, sub-pixel bar clipping, and table
scroll performance specifically
Verify LineAreaChart's secondary-y-axis feature doesn't get misused in the
Storybook mock data in a way that reads as misleading (spec flags dual axes as a
reader-trust caveat, not a bug)
Confirm build order was respected and each component was QA'd before moving to
the next, not batched at the end
Verify no hardcoded colors slipped into any of the 4 diffs
Verify Playwright baseline screenshots match the attached design references
closely enough for visual sign-off
Confirm PR description clearly documents the resolveThemeColor extraction
decision for reviewer visibility
Type: Task
Parent issues: #143 (primary), #144, #145, #146
Sub-issue link: to be created as a sub-issue of #143, cross-referencing #144/#145/#146, once posted
[DRAFT][PLAN] Add Pie/Donut, Bar, Line/Area, and Data Table chart components to packages/ui
This plans the implementation of the 4 remaining analytics chart components described in
#143–#146:
PieDonutChart,BarChart,LineAreaChart, andDataTable, all forpackages/ui. Combined into one plan per Thales's request — they share one PR (#142),one file-touch footprint (
packages/ui/src/index.ts,tests/design-system/smoke.spec.ts),and one build order.
Scorecard(the first of these 5 components) already shipped inPR #142 per its own plan, #141.
Reference files mapped
GoodDollar/GoodWidget (this repo):
packages/ui/src/components/Scorecard.tsx— the pattern all 4 components must follow:createComponent-based internal Frame/Text sub-pieces,useTheme()for every color,a private
resolveThemeColor(theme, token)helper with a$colorfallback, and theSCORECARD_BASE_SIZE_PX = 24/GOLDEN_RATIO = 1.618constants driving all spacingand type-scale math. Confirmed live on
feat/analytics-components(renamed branch).packages/governance-widget/src/FundingDistributionChart.tsx— direct precedent forPieDonutChart's SVG arc technique (Circle+strokeDasharray/strokeDashoffset,G rotation="-90"origin trick,CHART_COLOR_KEYS-style palette). Extract thetechnique; do not import from
governance-widgetor modify this file (per DO-NOT).packages/ui/src/utils/formatMetricValue.ts— shared number formatter (compactK/M/B/T, decimal, none), already exported from
packages/ui. All 4 new componentsuse this directly for axis/value/cell formatting — no new formatter needed.
packages/ui/src/createComponent.ts— styled-component wrapper + theme-manifestauto-registration; every internal styled sub-piece in all 4 components uses this.
packages/ui/src/components/Card.ts—variant="card"wraps each component's contentin this existing primitive (same approach as
Scorecard), never modified directly.packages/ui/src/theme.ts/presets.ts— confirmed$primary,$success,$warning,$colorDim/$placeholderColor,$error,$borderColorall resolveunder the default preset for the
CHART_COLOR_KEYSpalette(
['primary','success','warning','colorDim','error']).packages/ui/src/components/Icon.tsx— do not use. Same web-only DOM-SVG gapflagged in [DRAFT][PLAN] Add Scorecard (KPI card) chart component to packages/ui #141;
DataTable's sort arrows must be unicode glyphs (per its ownDO-NOT), and no other component needs iconography.
examples/storybook/src/stories/design-system/Scorecard.stories.tsxandTokenAmount.stories.tsx— story pattern to follow (title: 'Design System/ Primitives/<Component>',decorators: [withDefaultPreset],tags: ['autodocs', 'showcase'], multi-instanceDefaultstory covering all mock-data rows/states).tests/design-system/smoke.spec.ts— Playwright convention forpackages/uiprimitives (
gotoStory/getStoryFrame/screenshotStoryhelpers); this is thecorrect convention here, not
tests/widgets/<name>/states.spec.ts(that's for widgetpackages, per standing repo convention).
packages/ai-credits-widget/src/CreditsManagementCard.tsx—StatCell/stat-gridaesthetic reference cited for
DataTable, per [Feature]: Add Data Table component #146.External reference: Nivo Pie/Bar/Line/stacked-Area demos (nivo.rocks) — visual
quality bar for all 4, per each issue and Thales's Notion mockups.
Source of truth: full spec at
GoodDollar/data-team(branchfeat/pipeline-v5-hardening)projects/goodwidget-components/spec.md— already inlinedper-component into issues #143–#146; re-verified live for this plan (confirmed props,
behavioral rules, scope boundaries, and DO-NOT lists match between spec and issues).
Existing @GoodDollar package imports
@goodwidget/ui—Stack/YStack/XStack/Text/Heading/ScrollView,createComponent,formatMetricValue(all 4 components);Card(forvariant="card").tamagui—useTheme().react-native-svg—Svg,Path,Circle,Rect,Line,G,Defs,LinearGradient,Stop(Pie/Donut, Bar, Line/Area only —DataTableis pure Tamaguilayout, no SVG).
New components — placement assessment
PieDonutChartpackages/ui/src/components/PieDonutChart.tsx(new)FundingDistributionChart/Scorecardprecedent of one self-contained file per primitive.BarChartpackages/ui/src/components/BarChart.tsx(new)LineAreaChartpackages/ui/src/components/LineAreaChart.tsx(new)DataTablepackages/ui/src/components/DataTable.tsx(new)resolveThemeColorhelperpackages/ui/src/utils/resolveThemeColor.ts, exported frompackages/uipackages/ui/src/utils/").Scorecard.tsxalready has a private copy; duplicating it 4 more times (5 total) crosses from "acceptable repetition" into a real shared utility — same justificationformatMetricValueused when extracted for Scorecard's 2nd-consumer case, and this is now a 5th+ consumer. Proposing extraction; not touchingScorecard.tsxto point at it — that file already shipped under a prior issue and is out of this task's scope boundary. Flagged below as a human-reviewer call, not made silently.Required states, flows, and behaviors
Shared across all 4: empty state (no data — themed placeholder, never a crash), a
single-item/point state, a stress-test state at each component's documented volume
ceiling (120 items / 150 categories / 1000+ points / 150 rows) that degrades gracefully
(clip/truncate/aggregate) rather than crashing,
barevscardvariant, fulltestID/data-testid/accessibilityRole="image"(SVG components) baseline, zerohardcoded colors, no animation, no new dependencies.
PieDonutChart (#143): pie (
innerRadius=0) vs donut (innerRadius>0, with optionalcenter label/value/sublabel) modes; descending-sort-by-default arcs starting at 12
o'clock;
maxSlicesaggregation into "Other"; percentage formatting (int vs 1-decimal);legend with swatch+label+percentage;
onSegmentPress; NaN/null/negative values silentlyexcluded from render and total.
BarChart (#144): vertical vs horizontal layout (axes swap); zero-inclusive y-axis
with nice-number ticks (~5);
barWidth/gap formula; single-series only ($primary);optional value labels (hidden below 20px bar height); dashed grid lines; label
truncation/right-alignment;
onBarPress; NaN/null excluded silently.LineAreaChart (#145):
linear/monotone/stepinterpolation; optional area fillvia vertical
LinearGradient; multi-series (each ownPath+color);connectNullstrue/false gap behavior; auto dot visibility (<20 points); nice-number y-ticks +
adaptive x-label thinning; reference lines; optional secondary y-axis (flagged as a
readability caveat in acceptance criteria, not a bug);
onPointPress.DataTable (#146): typed columns (
text/number/date/currency) with per-typedefault formatters; center-default alignment; client-side sort (asc → desc → clear,
unicode arrow,
defaultSortsupport); striped rows; sticky header; horizontalScrollViewoverflow (noFlatList/pagination); null cells render"--"; compact mode;onRowPress.Execution plan
Build order per spec/issues: Pie/Donut → Bar → Line/Area → Table. All work targets
the already-renamed
feat/analytics-componentsbranch (PR #142).resolveThemeColortopackages/ui/src/utils/resolveThemeColor.ts(theme token → resolved color string,
$colorfallback + console warning, mirroringScorecard.tsx's private version) and export it frompackages/ui's index. One-timestep before component 1, since Pie/Donut needs it first.
Circlesegments,G rotation="-90"), sort +maxSlices/"Other" aggregation, percentage calc/formatting, center content (donutmode), legend, empty/stress states,
onSegmentPress.bar/gap width formula), vertical + horizontal layout, value labels, grid, empty/stress
states,
onBarPress.gradient fill, multi-series +
connectNulls, dots, reference lines, secondary y-axis,empty/single-point/stress states,
onPointPress.state + cycle, striped/sticky/compact/scroll behaviors, empty state,
onRowPress.packages/ui/src/index.tsunder the existing
// Analyticssection (alongsideScorecard), in build order..stories.tsxper component underexamples/storybook/src/stories/design-system/, each with aDefaultstory coveringevery required state (bare, card, empty, single-item, stress) using the mock datasets
from spec.md/issues, plus a
Controllablestory where relevant.tests/design-system/smoke.spec.ts(gotoStory/screenshotStorypattern);commit baseline PNGs to
tests/design-system/test-results/story-<component>-default.png.end): build/lint/typecheck scoped to changed files; visual check in Storybook against
the Nivo target and Thales's mockups, both light and dark theme.
feat/analytics-components; do not wait on Laurence's reviewat this stage (confirmed by Thales — full 5-component review happens once all 4 land).
resolveThemeColorextraction decision (so reviewers see it called out, not discovered in the diff).
Acceptance criteria
([Feature]: Add Pie/Donut chart component #143, [Feature]: Add Bar chart component #144, [Feature]: Add Line/Area chart component #145, [Feature]: Add Data Table component #146) — combined here at the category level:
legend, empty state, 120-item stress test,
onSegmentPresslabels, empty state, 150-category stress test,
onBarPressconnectNulls, reference lines, secondary y-axis, empty/single-point states,1000+-point stress test,
onPointPressbehaviors, empty state, 150-row stress test,
onRowPressreact-native-svgonly where SVG is needed (Table: none), zerohardcoded colors, no animation, no new dependencies
Scorecard.tsx's spacing/type-scale constants — no separate spacingsystem invented
resolveThemeColorextracted topackages/ui/src/utils/and used consistently byall 4 new components
packages/ui/src/index.tsunder// AnalyticsDesign System/Primitives/<Component>),covering every required state
tests/design-system/smoke.spec.tshas a passing case per component, each with acommitted baseline screenshot
Card.ts,Text.ts,Icon.tsx,theme.ts,presets.ts,config.ts,or any
governance-widgetfile modified(per spec.md Appendix) alongside light-theme equivalents
Human-reviewer checklist
resolveThemeColorextraction is a good call — and whetherScorecard.tsx's existing private copy should be migrated to it in a follow-up(explicitly out of scope here; flagging so it isn't lost)
both light and dark theme, not just "renders without crashing"
merely "not crashing" — check legend overflow, sub-pixel bar clipping, and table
scroll performance specifically
LineAreaChart's secondary-y-axis feature doesn't get misused in theStorybook mock data in a way that reads as misleading (spec flags dual axes as a
reader-trust caveat, not a bug)
the next, not batched at the end
closely enough for visual sign-off
resolveThemeColorextractiondecision for reviewer visibility