Skip to content

[Feature]: Add Bar chart component #144

Description

@goodbounties-nanoclaw-agent

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

  1. Y-axis always includes zero — starts at 0 for all-positive data; extends below 0 (diverging bars) if negatives are present.
  2. Axis max = ceil(maxValue * 1.1) rounded to a "nice" number (multiples of 1/2/5/10/20/50/100/1K/...); ~5 tick marks.
  3. barWidth = (availableWidth / categoryCount) * 0.7; gap = barWidth * 0.3.
  4. Single series MVP — all bars use $primary.
  5. Value labels (when enabled): above bars (vertical) or right of bars (horizontal); hidden if bar height < 20px.
  6. Horizontal layout swaps axes — categories on left y-axis, values on bottom x-axis.
  7. Grid lines: horizontal only, at each y-axis tick, 0.5px $borderColor, dashed "3 3".
  8. X-axis labels centered below bars, truncated with ellipsis if they overflow bar width; y-axis labels right-aligned via formatMetricValue.
  9. Empty state: axes + zero line + "No data" centered.
  10. 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

  • Renders vertical bars with correct proportional heights
  • Y-axis includes zero (positive data starts at 0; mixed data extends below)
  • Y-axis uses nice-number ticks
  • X-axis labels centered under bars, truncated on overflow
  • Grid lines render as subtle dashed lines
  • Horizontal layout works with axes swapped
  • Value labels appear when enabled, hidden when bar < 20px
  • Empty state renders correctly
  • 150-item stress test: renders without crash
  • variant="card" wraps correctly
  • testID + data-testid present
  • No hardcoded colors
  • formatMetricValue used for axis/value labels
  • onBarPress fires correctly

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:

  1. packages/ui/src/components/BarChart.tsx
  2. packages/ui/src/index.ts (add export under // Analytics)
  3. examples/storybook/src/stories/design-system/BarChart.stories.tsx
  4. 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

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