Skip to content

Add ProportionChart component for data visualization#30

Merged
alecf merged 3 commits intomainfrom
feat/proportion-chart
Feb 7, 2026
Merged

Add ProportionChart component for data visualization#30
alecf merged 3 commits intomainfrom
feat/proportion-chart

Conversation

@alecf
Copy link
Copy Markdown
Contributor

@alecf alecf commented Jan 20, 2026

Summary

Adds ProportionChart component - a horizontal segmented bar chart for visualizing data proportions with interactive tooltips and optional legends.

Migrated from RibbonGraph in gp-dixie with optimized redesign following gp-ui conventions.

Features

  • 📊 Segmented bar visualization showing relative proportions
  • 💬 Interactive tooltip with detailed breakdown and percentages
  • 📋 Optional legend for labeled display
  • 🌙 Dark mode support throughout
  • Enhanced accessibility with ARIA labels and semantic HTML
  • Input validation for negative values and missing color configs
  • 🎨 Flexible styling via Tailwind height classes and className prop
  • 📱 Responsive legend with grid layout

Implementation Highlights

  • DRY principle: Percentage calculated once, reused everywhere
  • Validation: Clear error messages for invalid inputs (negative values, missing colors)
  • Type safety: Fully typed with exported interfaces (ProportionChartProps, ColorConfig, ProportionSegment)
  • Accessibility: role="img", aria-labels, semantic HTML throughout
  • Empty state: Graceful "No data" message when total is zero

Testing

  • 20+ comprehensive test cases covering:
    • Rendering, data handling, validation
    • Height variants, legend display
    • Empty states, accessibility
    • Color application, tooltip interaction
    • HTML attribute passthrough

Demo

Comprehensive playground demo with 6 sections:

  • Basic examples (time breakdown, status distribution, resource usage)
  • Different heights (h-1, h-2, h-4, h-6)
  • With legend
  • Empty state
  • Custom styling
  • Interactive tooltip explanation

Files Added/Modified

New:

  • src/components/ProportionChart/ProportionChart.tsx (234 lines)
  • src/components/ProportionChart/ProportionChart.test.tsx (257 lines)
  • src/components/ProportionChart/index.ts
  • playground/src/components/ProportionChartDemo.tsx (271 lines)

Modified:

  • src/components/index.ts - Export ProportionChart
  • src/styles/theme.css - Add .gp-proportion-chart base class
  • playground/src/App.tsx - Add to sidebar navigation

Usage

```tsx
import { ProportionChart, type ColorConfig } from '@gp/ui'

const data = [
{ key: 'completed', value: 145 },
{ key: 'in-progress', value: 62 },
{ key: 'pending', value: 28 }
]

const colors: Record<string, ColorConfig> = {
completed: { bg: 'bg-green-500', label: 'Completed' },
'in-progress': { bg: 'bg-blue-500', label: 'In Progress' },
pending: { bg: 'bg-yellow-500', label: 'Pending' }
}

<ProportionChart
data={data}
colors={colors}
formatValue={(n) => `${n} tasks`}
height="h-4"
showLegend={true}
ariaLabel="Task distribution by status"
/>
```

🤖 Generated with Claude Code

- Add ProportionChart component (migrated from RibbonGraph in gp-dixie)
- Displays data as horizontal segmented bar showing proportions
- Features: interactive tooltip, optional legend, dark mode support
- Includes input validation for negative values and missing color configs
- Pre-calculates percentages once for DRY principle
- Enhanced accessibility with ARIA labels and semantic HTML
- Comprehensive test suite with 20+ test cases
- Full demo with 6 sections showing different use cases
- Integrated into playground after StatGrid with BarChart4 icon

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Jan 20, 2026

Preview deployed! 🚀

View the playground preview at: https://thegpvc.github.io/gp-ui/pr-30/

Last updated: 2026-01-21T00:07:40.611Z

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Jan 20, 2026

Code Review: ProportionChart Component

Great work on this PR! The ProportionChart component is well-implemented with excellent test coverage and comprehensive documentation. Here's my detailed review:

✅ Strengths

Code Quality

  • Excellent TypeScript typing: All interfaces are properly exported and well-documented with JSDoc comments
  • Comprehensive validation: Proper error handling for negative values and missing color configurations with helpful error messages
  • DRY principle: Percentages calculated once and reused throughout (line 147)
  • Clean component structure: Well-organized with clear separation of concerns
  • Proper use of design patterns: Follows existing codebase conventions (cn() utility, HTML attributes passthrough, className merging)

Testing

  • Outstanding test coverage: 20+ test cases covering all major functionality
  • Well-organized test suites: Tests grouped by feature area (rendering, validation, accessibility, etc.)
  • Edge cases covered: Empty states, zero values, negative values, missing configurations

Accessibility

  • Strong A11y implementation:
    • Semantic HTML with proper ARIA roles (role="img", role="list", role="listitem")
    • Custom and auto-generated aria-labels
    • Color indicators properly marked as aria-hidden="true"
    • Individual segment labels for screen readers (line 195)

Documentation

  • Comprehensive demo: 6 different demo sections with varied examples
  • Clear usage examples: Both in JSDoc and the playground
  • Well-written PR description: Clear summary of features and implementation details

🔍 Issues & Suggestions

1. Potential Division by Zero Edge Case (Minor)

Location: ProportionChart.tsx:147

While you filter out zero-value segments (line 130), if the total is calculated from an empty array or all zeros, division by zero could occur at line 147 before the total === 0 check at line 156.

Current flow:

const validSegments = data.filter((d) => d.value > 0)  // line 130
// ... validation ...
const total = validSegments.reduce((sum, d) => sum + d.value, 0)  // line 143
const segments = validSegments.map((d) => {
  const percentage = (d.value / total) * 100  // line 147 - division happens here
  // ...
})
if (total === 0) { ... }  // line 156 - check happens after

Recommendation: Move the total === 0 check before the percentage calculations:

const total = validSegments.reduce((sum, d) => sum + d.value, 0)

// Check for empty state before calculating percentages
if (total === 0) {
  return (/* empty state JSX */)
}

// Now safe to calculate percentages
const segments = validSegments.map((d) => {
  const percentage = (d.value / total) * 100
  // ...
})

This is a defensive programming practice, even though the current code works because validSegments will be empty when total is 0, so the map won't execute.

2. Inconsistent aria-label on Segments (Minor)

Location: ProportionChart.tsx:195

Individual segment divs have aria-labels, but they're decorative parts of a larger role="img" element. The outer container already has comprehensive aria-label.

Current:

<div
  key={seg.key}
  className={cn(seg.bg, 'transition-opacity hover:opacity-80')}
  style={{ width: `${seg.percentage}%` }}
  aria-label={`${seg.label}: ${formatValue(seg.value)} (${seg.percentage.toFixed(0)}%)`}
/>

Recommendation: Consider removing aria-label from individual segments or marking them as aria-hidden="true" since the parent role="img" already describes the entire chart. Screen readers may read both, creating redundancy.

3. Tooltip Accessibility (Minor)

Location: ProportionChart.tsx:200-224

The tooltip contains a role="row" and role="cell" (lines 217-223) without a proper table structure. These ARIA roles should be used within a table context.

Recommendation: Either:

  • Use semantic table elements: <table>, <tr>, <td>
  • Or remove the ARIA roles if this is just presentational formatting

4. Magic Number in Tests (Minor)

Location: ProportionChart.test.tsx:98

The test calculates width: '60%' based on knowing the total is 100, but this isn't explicit.

Recommendation: Make the test more explicit:

it('calculates percentages correctly', () => {
  const total = mockData.reduce((sum, d) => sum + d.value, 0) // 100
  const completedValue = mockData[0].value // 60
  const expectedPercentage = (completedValue / total) * 100 // 60%
  
  const { container } = render(...)
  const completedSegment = container.querySelector('.bg-green-500')
  expect(completedSegment).toHaveStyle({ width: `${expectedPercentage}%` })
})

5. Theme CSS Could Be More Specific (Very Minor)

Location: theme.css:1111-1114

The .gp-proportion-chart class only adds relative w-full, which is very minimal. Consider if this warrants a separate class or could just be inline.

Current approach is fine, but worth considering if future styling needs justify the base class.

🎯 Design System Alignment

Follows CLAUDE.md conventions:

  • Uses cn() utility for class merging
  • Exports all types
  • Passes through HTML attributes
  • No application-specific logic
  • Consistent prop naming (variant, className)

Follows DESIGN_GUIDE.md:

  • Dark mode support throughout
  • Consistent spacing and typography
  • Proper use of semantic colors
  • Accessibility-first approach

🔒 Security & Performance

No security concerns:

  • No XSS vulnerabilities
  • No unsafe operations
  • Proper input validation

Performance is good:

  • Simple calculations with O(n) complexity
  • No unnecessary re-renders
  • Efficient filtering and mapping

📊 Summary

This is a high-quality PR that's ready to merge with only minor suggestions. The component is well-tested, accessible, and follows all established patterns in the codebase.

Recommendation: ✅ Approve with minor suggestions

The suggestions above are nitpicks that would make the code slightly more robust, but they don't block merging. The current implementation is solid and production-ready.

Nice work! 🎉

- Fix division by zero: Move total === 0 check before percentage calculations
- Improve accessibility: Remove redundant aria-labels from segments, mark as aria-hidden
- Fix tooltip ARIA: Remove improper role="row" and role="cell" attributes
- Improve test clarity: Make percentage calculation explicit in test

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Jan 20, 2026

Code Review: ProportionChart Component

Overall, this is an excellent addition to the design system! The implementation is well-crafted, thoroughly tested, and follows the repository's conventions closely. The code quality is high with comprehensive documentation and accessibility features.


✅ Strengths

Code Quality

  • Excellent TypeScript usage: Fully typed with well-documented interfaces exported for consumers
  • Comprehensive validation: Input validation with clear error messages for negative values and missing color configs
  • DRY principle: Percentages calculated once and reused throughout (line 1005-1013 in ProportionChart.tsx)
  • Clean component structure: Well-organized with early returns for error/empty states
  • Proper use of cn(): Consistent class merging following codebase patterns

Accessibility

  • Strong ARIA support: Proper role="img", aria-label, and semantic HTML throughout
  • Semantic legend: Uses role="list" and role="listitem" appropriately
  • Screen reader friendly: Color indicators marked with aria-hidden="true"
  • Generated default labels: Automatic descriptive aria-labels when not provided

Testing

  • Comprehensive coverage: 20+ test cases covering rendering, validation, accessibility, empty states, and more
  • Well-structured tests: Organized into logical describe blocks
  • Edge cases covered: Zero values, negative values, missing colors, empty states

Documentation

  • Excellent JSDoc: Clear component documentation with multiple examples
  • Thorough demo: 6 demo sections showing various use cases
  • Clear prop descriptions: All props documented with examples

Design System Alignment

  • Follows conventions: Uses compound pattern appropriately, accepts className, follows existing patterns
  • Dark mode support: Comprehensive dark mode classes throughout
  • Minimal theme.css: Only adds minimal base class (4 lines)
  • Consistent naming: .gp-proportion-chart follows established prefix pattern

🔍 Areas for Improvement

1. Minor: Tooltip Content Dark Mode (Line 1038-1056)

The tooltip uses a grid layout with borders, but the border color might not have optimal contrast in all scenarios:

// Line 1053: Consider adding dark mode variant
<div className="mt-1 pt-1 border-t border-gray-600 dark:border-gray-500 flex justify-between">

The current border-gray-600 dark:border-gray-500 works but might appear slightly light against the bg-navy-900 tooltip background. Consider testing visibility or using border-gray-700 dark:border-gray-600 for better contrast.

2. Minor: Empty State Typography Consistency

In the empty state (line 999), the text uses text-xs which is quite small. Consider if text-sm would be more appropriate for better readability, especially for accessibility:

<span className="text-xs text-gray-500 dark:text-gray-400">No data</span>
// Consider: text-sm for better readability

3. Test Coverage: Tooltip Interaction

While the tests verify tooltip markup exists (lines 788-812), they don't test the actual tooltip interaction (hover behavior). Consider adding a test that simulates hover and verifies tooltip content appears. Example:

import { userEvent } from '@testing-library/user-event'

it('shows tooltip on hover with formatted values', async () => {
  const user = userEvent.setup()
  const { container } = render(<ProportionChart ... />)
  const bar = container.querySelector('.cursor-pointer')
  
  await user.hover(bar!)
  // Verify tooltip content appears
  await waitFor(() => {
    expect(screen.getByText('Total')).toBeVisible()
  })
})

4. Documentation: Missing Usage Pattern

The demo and docs don't show an example of handling dynamic data updates. Consider adding a note or example about what happens when data changes (does the chart smoothly update? Should consumers use keys?).

5. Performance: Memoization Opportunity

The segments calculation (lines 1005-1013) runs on every render. For large datasets or frequently re-rendering parents, consider using useMemo:

const segments = useMemo(() => 
  validSegments.map((d) => {
    const percentage = (d.value / total) * 100
    return { ...d, ...colors[d.key], percentage }
  }),
  [validSegments, total, colors]
)

However, this is only needed if performance becomes an issue in practice.

6. Minor: Legend Grid Breakpoints

Line 1064: grid-cols-2 sm:grid-cols-5 jumps from 2 to 5 columns. Consider adding a middle breakpoint for better responsive behavior:

// Current: grid-cols-2 sm:grid-cols-5
// Consider: grid-cols-2 sm:grid-cols-3 lg:grid-cols-5

This provides a more graceful transition on medium-sized screens.


🛡️ Security & Performance

  • No security concerns: No XSS vulnerabilities, no unsafe operations
  • No performance issues: Efficient calculations, minimal re-renders
  • Input validation: Proper error handling for invalid inputs

📋 Best Practices Checklist

  • ✅ No application-specific logic
  • ✅ Generic and reusable component
  • ✅ Consistent with existing patterns
  • ✅ Uses cn() for class merging
  • ✅ Minimal API surface
  • ✅ Styles via theme.css
  • ✅ Exports types for consumers
  • ✅ Comprehensive documentation
  • ✅ Dark mode support
  • ✅ Accessibility features

🎯 Recommendations

Priority: Merge as-is

This PR is ready to merge. The suggested improvements above are optional enhancements and not blockers. The component is production-ready with excellent code quality, comprehensive tests, and strong accessibility support.

Optional Follow-ups (can be addressed in future PRs if desired):

  1. Add user interaction tests for tooltip behavior
  2. Consider responsive legend breakpoints
  3. Add dynamic data update example to docs
  4. Review tooltip border contrast in dark mode

Great work! This component will be a valuable addition to the design system. The migration from RibbonGraph shows thoughtful redesign and significant improvements. 🎉

@alecf alecf requested a review from dpup January 20, 2026 20:01
Copy link
Copy Markdown
Member

@dpup dpup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked IRL about colors.

@@ -0,0 +1,282 @@
import { ProportionChart, type ColorConfig } from '@gp/ui'

export function ProportionChartDemo() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming thought: <meter> exists, so could potentially align with that naming.

Replace generic Tailwind colors with theme semantic colors:
- Status colors: emerald (success), navy (info), amber (warning), rose (error)
- Time/resource colors: emerald, navy where applicable
- Keep purple and cyan for variety (no theme equivalent)
- Add dark mode variants for all theme colors

All colors now follow DESIGN_GUIDE.md semantic color palette.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown
Contributor

claude bot commented Jan 21, 2026

PR Review: ProportionChart Component

Summary

This is a well-implemented data visualization component with excellent test coverage, comprehensive validation, and strong accessibility support. The code quality is high and follows the repository conventions outlined in CLAUDE.md.

✅ Strengths

Code Quality

  • Excellent validation: Comprehensive input validation with clear error messages for negative values and missing color configs
  • DRY principle: Percentage calculations computed once and reused throughout
  • Type safety: Fully typed with exported interfaces for better DX
  • Clean component structure: Well-organized with clear separation of concerns
  • Proper use of design patterns: Follows established patterns (cn() utility, compound components with Tooltip)

Test Coverage

  • Comprehensive testing: 20+ test cases covering all major scenarios (rendering, data handling, validation, accessibility, empty states, tooltips, legend)
  • Edge cases covered: Zero values, negative values, missing colors, empty state
  • Accessibility testing: Validates ARIA labels, roles, and semantic HTML

Accessibility

  • ARIA support: Proper use of role="img", aria-labels, and semantic HTML
  • Legend semantics: Uses role="list" and role="listitem" correctly
  • Hidden decorative elements: Color indicators marked with aria-hidden="true"
  • Screen reader friendly: Generates meaningful default aria-label when not provided

Documentation

  • Comprehensive JSDoc: Clear documentation with usage examples
  • Demo playground: Excellent 6-section demo showing various use cases
  • Type exports: Exported interfaces make the API clear

Design System Compliance

  • Follows Tailwind v4 conventions with theme.css
  • Uses cn() utility for class merging
  • Accepts className prop for extensibility
  • Dark mode support throughout
  • Follows responsive patterns with grid layouts

🔍 Suggestions & Minor Issues

1. Color Class Validation (Medium Priority)

Issue: The component accepts arbitrary Tailwind classes in ColorConfig.bg but doesn't validate they're actually background color classes.

Current code (ProportionChart.tsx:849):

export interface ColorConfig {
  bg: string  // Could be any string, not necessarily a valid bg class
  label: string
}

Potential problem:

// This would be accepted but wouldn't render correctly:
colors={{ foo: { bg: 'text-red-500', label: 'Foo' } }}

Suggestion: Add a runtime warning in development mode or enhance the TypeScript type:

// Option 1: Type-level guidance (TypeScript only, no runtime)
export interface ColorConfig {
  /** Tailwind background color class (e.g., 'bg-blue-500', 'bg-navy-500 dark:bg-navy-300') */
  bg: string & { __brand?: 'TailwindBgClass' }
  label: string
}

// Option 2: Runtime validation in dev mode
if (process.env.NODE_ENV === 'development') {
  Object.entries(colors).forEach(([key, config]) => {
    if (!config.bg.includes('bg-')) {
      console.warn(
        `ProportionChart: Color config for "${key}" may be invalid. ` +
        `Expected a background class (e.g., 'bg-blue-500'), got: "${config.bg}"`
      )
    }
  })
}

2. Legend Responsive Breakpoint (Low Priority)

Issue: The legend uses sm:grid-cols-5 which could cause layout issues with many items on medium screens.

Current code (ProportionChart.tsx:1064):

className="mt-3 grid grid-cols-2 sm:grid-cols-5 gap-3"

Observation: Jump from 2 columns to 5 columns is large. With 3-4 items, this works well. With 7+ items, medium screens might look awkward.

Suggestion: Consider a more gradual progression:

className="mt-3 grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3"

3. Tooltip Percentage Precision (Very Low Priority)

Issue: Percentage is rounded to 0 decimals which might hide small segments.

Current code (ProportionChart.tsx:1048):

{seg.percentage.toFixed(0)}%

Example: A segment that's 0.8% would show as "1%" while 0.4% shows as "0%".

Suggestion: Consider using 1 decimal place for better accuracy, or conditionally show decimals for small percentages:

{seg.percentage < 1 ? seg.percentage.toFixed(1) : seg.percentage.toFixed(0)}%

4. Empty State Consistency (Very Low Priority)

Issue: Empty state validation happens before percentage calculation but filters are applied earlier.

Current observation: The logic is correct, but the flow could be slightly more explicit:

// Current flow:
const validSegments = data.filter((d) => d.value > 0)  // Line 967
const total = validSegments.reduce(...)                 // Line 980
if (total === 0) { /* empty state */ }                  // Line 984

Suggestion: Add a comment clarifying why we check total === 0 after filtering, or move the comment closer to the check:

// Calculate total from non-zero segments
const total = validSegments.reduce((sum, d) => sum + d.value, 0)

// Edge case: Handle when all segments are zero or array is empty
if (total === 0) {

5. Theme CSS Class Documentation (Low Priority)

Issue: The base theme class .gp-proportion-chart only applies relative w-full but components might not understand why.

Current code (theme.css:1111-1113):

.gp-proportion-chart {
  @apply relative w-full;
}

Observation: The relative positioning isn't currently being used. If it's for future tooltip positioning or other features, a comment would help. If it's not needed, consider removing it.

Suggestion: Add a comment or remove unused relative:

/* Base container for proportion chart - w-full for responsive behavior */
.gp-proportion-chart {
  @apply w-full;
}

🛡️ Security

✅ No security concerns identified. The component:

  • Doesn't execute user-provided code
  • Properly escapes all values in JSX
  • Uses safe DOM manipulation

⚡ Performance

✅ Performance is good:

  • Efficient single-pass calculations
  • No unnecessary re-renders
  • Percentage pre-calculated and reused (great DRY implementation)

Minor note: For extremely large datasets (100+ segments), consider adding a prop to limit visible segments or group small ones into "Other", but this is likely not needed for typical use cases.

🧪 Testing

✅ Excellent test coverage:

  • All major paths covered
  • Edge cases tested (negative values, missing colors, zero values)
  • Accessibility attributes validated
  • Both with and without legend
  • HTML passthrough tested

Optional enhancement: Consider adding a visual regression test or screenshot test if the repo uses those, but unit tests are comprehensive.

📋 Missing Elements

None identified - the PR includes all necessary files:

  • ✅ Component implementation
  • ✅ Comprehensive tests
  • ✅ Type definitions and exports
  • ✅ Theme CSS additions
  • ✅ Playground demo
  • ✅ Updated exports

🎯 Recommendation

APPROVE - This is high-quality work that's ready to merge. The suggestions above are minor enhancements that could be addressed in follow-up PRs if desired. The component is:

  • Well-tested
  • Accessible
  • Follows design system conventions
  • Properly documented
  • Has no blocking issues

Great work on the migration from RibbonGraph with improvements! 🎉


Reviewed with Claude Code

@alecf alecf merged commit 2812554 into main Feb 7, 2026
4 checks passed
@alecf alecf deleted the feat/proportion-chart branch February 7, 2026 02:37
github-actions bot added a commit that referenced this pull request Feb 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 7, 2026

🧹 Preview deployment has been cleaned up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants