Feature summary
Add a BarChart component to packages/ui for discrete categorical comparison —
the highest-accuracy chart type for comparing values across categories (position on a
common scale). Supports vertical and horizontal layouts. Second of 4 remaining analytics
chart components completing GoodWidget's analytics visualization layer (Scorecard already
shipped in PR #142).
Problem or opportunity
GoodWidget has no reusable bar chart primitive for comparing magnitudes across categories
(claims per chain, funding per house, monthly totals) or showing ranking. Without it,
widgets needing "which is bigger / by how much" comparisons have no standard, theme-aware
building block to reach for.
Proposed solution
Follow Scorecard.tsx's patterns (createComponent, useTheme, golden-ratio spacing,
formatMetricValue).
Props / API surface (MVP)
| Prop |
Type |
Required |
Default |
Description |
| data |
Array<{ category: string; value: number }> |
Yes |
- |
Bar data |
| title |
string |
No |
undefined |
Chart heading |
| layout |
'vertical' | 'horizontal' |
No |
'vertical' |
Bar orientation |
| showGrid |
boolean |
No |
true |
Show grid lines |
| showValueLabels |
boolean |
No |
false |
Display values on bars |
| valueFormatter |
(value: number) => string |
No |
formatMetricValue |
Value/axis formatting |
| xAxisLabel / yAxisLabel |
string |
No |
undefined |
Axis titles |
| barCornerRadius |
number |
No |
0 |
Rounded top corners |
| onBarPress |
(item, index) => void |
No |
undefined |
Bar tap callback |
| variant |
'bare' | 'card' |
No |
'bare' |
Visual variant |
| testID |
string |
No |
undefined |
Testing identifier |
| accessibilityLabel |
string |
No |
auto-generated |
Screen reader description |
| width |
number | string |
No |
'100%' |
Chart width |
| height |
number |
No |
200 |
Chart height |
| padding |
{ top, right, bottom, left } |
No |
{16,16,40,48} |
Internal padding for axes |
Behavioral rules
- Y-axis always includes zero — starts at 0 for all-positive data; extends below 0 (diverging bars) if negatives are present.
- Axis max =
ceil(maxValue * 1.1) rounded to a "nice" number (multiples of 1/2/5/10/20/50/100/1K/...); ~5 tick marks.
barWidth = (availableWidth / categoryCount) * 0.7; gap = barWidth * 0.3.
- Single series MVP — all bars use
$primary.
- Value labels (when enabled): above bars (vertical) or right of bars (horizontal); hidden if bar height < 20px.
- Horizontal layout swaps axes — categories on left y-axis, values on bottom x-axis.
- Grid lines: horizontal only, at each y-axis tick, 0.5px
$borderColor, dashed "3 3".
- X-axis labels centered below bars, truncated with ellipsis if they overflow bar width; y-axis labels right-aligned via
formatMetricValue.
- Empty state: axes + zero line + "No data" centered.
- NaN/null values excluded silently.
Shared constraints (apply to all 4 remaining chart components — see Pie/Donut issue #143 for full text): react-native-svg only, Tamagui + useTheme() for layout/color, createComponent(), Scorecard spacing/type-scale constants, no animation, no new dependencies, Nivo-quality visual target (muted grid/axes, bold data elements, generous padding), full accessibility baseline (accessibilityRole="image", testID/data-testid).
Visual quality target: Nivo Bar.
Acceptance criteria
Additional context
DO NOT:
- Implement grouped or stacked bars — single series only
- Add a secondary y-axis
- Add horizontal scroll (truncate/clip instead)
- Use
Icon.tsx or web-only APIs
- Modify
Card.ts, Text.ts, Icon.tsx, theme.ts, presets.ts, config.ts, or governance-widget
Scope boundary — only create/modify:
packages/ui/src/components/BarChart.tsx
packages/ui/src/index.ts (add export under // Analytics)
examples/storybook/src/stories/design-system/BarChart.stories.tsx
tests/design-system/smoke.spec.ts (add cases)
Mock data: claims-by-chain (standard), houses (horizontal, long labels), single bar,
empty, and a 150-category stress test (bars become sub-pixel-narrow — must clip/degrade
gracefully, not crash) — 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
Feature summary
Add a
BarChartcomponent topackages/uifor discrete categorical comparison —the highest-accuracy chart type for comparing values across categories (position on a
common scale). Supports vertical and horizontal layouts. Second of 4 remaining analytics
chart components completing GoodWidget's analytics visualization layer (Scorecard already
shipped in PR #142).
Problem or opportunity
GoodWidget has no reusable bar chart primitive for comparing magnitudes across categories
(claims per chain, funding per house, monthly totals) or showing ranking. Without it,
widgets needing "which is bigger / by how much" comparisons have no standard, theme-aware
building block to reach for.
Proposed solution
Follow
Scorecard.tsx's patterns (createComponent,useTheme, golden-ratio spacing,formatMetricValue).Props / API surface (MVP)
Array<{ category: string; value: number }>(value: number) => string(item, index) => void{ top, right, bottom, left }{16,16,40,48}Behavioral rules
ceil(maxValue * 1.1)rounded to a "nice" number (multiples of 1/2/5/10/20/50/100/1K/...); ~5 tick marks.barWidth = (availableWidth / categoryCount) * 0.7; gap =barWidth * 0.3.$primary.$borderColor, dashed "3 3".formatMetricValue.Shared constraints (apply to all 4 remaining chart components — see Pie/Donut issue #143 for full text):
react-native-svgonly, Tamagui +useTheme()for layout/color,createComponent(), Scorecard spacing/type-scale constants, no animation, no new dependencies, Nivo-quality visual target (muted grid/axes, bold data elements, generous padding), full accessibility baseline (accessibilityRole="image",testID/data-testid).Visual quality target: Nivo Bar.
Acceptance criteria
variant="card"wraps correctlytestID+data-testidpresentformatMetricValueused for axis/value labelsonBarPressfires correctlyAdditional context
DO NOT:
Icon.tsxor web-only APIsCard.ts,Text.ts,Icon.tsx,theme.ts,presets.ts,config.ts, orgovernance-widgetScope boundary — only create/modify:
packages/ui/src/components/BarChart.tsxpackages/ui/src/index.ts(add export under// Analytics)examples/storybook/src/stories/design-system/BarChart.stories.tsxtests/design-system/smoke.spec.ts(add cases)Mock data: claims-by-chain (standard), houses (horizontal, long labels), single bar,
empty, and a 150-category stress test (bars become sub-pixel-narrow — must clip/degrade
gracefully, not crash) — full snippets in the spec (link below).
References:
feat/analytics-components)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:
[DRAFT][PLAN] <what issue is being planned>Task<sub-issue title>Instructions when creating the plan:
Plan specification required sections: