Skip to content

Charts: Add standalone Legend component with Chart Context system #44245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 42 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a3155a6
Add useChartLegendData hook and types for standalone legend component
annacmc Jul 9, 2025
7f488d8
Add ChartLegend standalone component
annacmc Jul 9, 2025
eb072e6
Refactor LineChart to use useChartLegendData hook
annacmc Jul 9, 2025
301b80a
Refactor BarChart to use useChartLegendData hook
annacmc Jul 9, 2025
0348c8e
Refactor PieChart and PieSemiCircleChart to use useChartLegendData hook
annacmc Jul 9, 2025
36fe909
Add ChartLegend exports to package API
annacmc Jul 9, 2025
f29b9c0
Add comprehensive stories and tests for ChartLegend
annacmc Jul 9, 2025
ad3bb4a
changelog
annacmc Jul 9, 2025
cf952ea
Fix rebase conflicts and Storybook build errors
Jul 11, 2025
f384eb9
Complete ChartLegend component implementation
Jul 11, 2025
dbe4296
Add shared validation utilities
annacmc Jul 14, 2025
5da4e04
Add context-aware Legend component
annacmc Jul 14, 2025
266da39
Refactor useChartLegendData hook for better maintainability
annacmc Jul 14, 2025
16b3593
Update Legend component infrastructure
annacmc Jul 14, 2025
6e8dc22
Export ChartContext for direct usage
annacmc Jul 14, 2025
efbfd05
Replace duplicated validation with shared utilities
annacmc Jul 14, 2025
e786906
Remove separate ChartLegend component
annacmc Jul 14, 2025
0541cde
Update main exports for consolidated legend system
annacmc Jul 14, 2025
53b138d
Add comprehensive Storybook documentation for Legend
annacmc Jul 14, 2025
0ddf4db
Fix linting issues in legend hook
annacmc Jul 14, 2025
a41fd43
Revert "Replace duplicated validation with shared utilities"
annacmc Jul 14, 2025
d1ff1cc
Revert "Add shared validation utilities"
annacmc Jul 14, 2025
2ee14c3
Fix imports after validation revert
annacmc Jul 14, 2025
ff6cf8e
Fix standalone legend reactivity issue
annacmc Jul 14, 2025
385b20d
Fix infinite re-render loop in ChartProvider
annacmc Jul 14, 2025
073be67
Add comprehensive test coverage for Legend component
annacmc Jul 15, 2025
6faf5d8
Fix standalone legend reactivity issue
annacmc Jul 15, 2025
2b8e6da
Fix LineChart to reuse existing ChartProvider context
annacmc Jul 15, 2025
f6fda18
Fix PieChart to reuse existing ChartProvider context
annacmc Jul 15, 2025
fc3e9b2
Add space between legend label and value
annacmc Jul 15, 2025
71643f6
Fix BarChart to reuse existing ChartProvider context
annacmc Jul 15, 2025
57d4fb1
Fix pie chart container overflow issues
annacmc Jul 15, 2025
bacb6c0
Fix showValues default to preserve previous behavior
annacmc Jul 15, 2025
50cc059
Keep necessary lint suppression for useMemo dependencies
annacmc Jul 15, 2025
da0a72f
Add comprehensive unit tests for useChartLegendData hook
annacmc Jul 15, 2025
35691cc
Fix TypeScript errors in CI/CD
annacmc Jul 15, 2025
2b9e9e9
add missing tickLength and
annacmc Jul 15, 2025
cd05c7b
Revert "add missing tickLength and"
annacmc Jul 16, 2025
1d3463e
Remove test additions to reduce PR size
annacmc Jul 16, 2025
291e343
Complete test file removal for PR size reduction
annacmc Jul 16, 2025
5511c11
Clean up unused imports and variables from HighlightTooltip removal
annacmc Jul 16, 2025
557729d
Fix legend regression caused by removed ChartProvider version state
annacmc Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Charts: adds a standalone chart legend component
60 changes: 32 additions & 28 deletions projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from '@visx/pattern';
import { Axis, BarSeries, BarGroup, Grid, XYChart } from '@visx/xychart';
import clsx from 'clsx';
import { useCallback, useId, useState, useRef, useMemo } from 'react';
import { useCallback, useContext, useId, useState, useRef } from 'react';
import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context';
import { ChartContext } from '../../providers/chart-context/chart-context';
import { useChartTheme, useXYChartTheme } from '../../providers/theme';
import { Legend } from '../legend';
import { useChartLegendData } from '../legend/use-chart-legend-data';
import { useChartDataTransform } from '../shared/use-chart-data-transform';
import { useChartMargin } from '../shared/use-chart-margin';
import { useElementHeight } from '../shared/use-element-height';
Expand Down Expand Up @@ -66,10 +68,14 @@ const BarChartInternal: FC< BarChartProps > = ( {
// Generate a unique chart ID to avoid pattern conflicts with multiple charts
const internalChartId = useId();
const chartId = useChartId( providedChartId );
const providerTheme = useChartTheme();
const theme = useXYChartTheme( data );

const dataSorted = useChartDataTransform( data );

// Create legend items using the reusable hook
const legendItems = useChartLegendData( dataSorted, providerTheme );
Copy link
Preview

Copilot AI Jul 14, 2025

Choose a reason for hiding this comment

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

The default showValues is true, which will display number of data points next to each series, differing from the previous blank values. Add showValues: false to match existing behavior.

Suggested change
const legendItems = useChartLegendData( dataSorted, providerTheme );
const legendItems = useChartLegendData( dataSorted, providerTheme, { showValues: false } );

Copilot uses AI. Check for mistakes.


const chartOptions = useBarChartOptions( dataSorted, horizontal, options );
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme, horizontal );
const [ legendRef, legendHeight ] = useElementHeight< HTMLDivElement >();
Expand Down Expand Up @@ -222,20 +228,7 @@ const BarChartInternal: FC< BarChartProps > = ( {
const error = validateData( dataSorted );
const isDataValid = ! error;

// Create legend items (hooks must be called in same order every render)
const legendItems = useMemo(
() =>
dataSorted.map( ( group, index ) => ( {
label: group.label, // Label for each unique group
value: '', // Empty string since we don't want to show a specific value
color: getColor( group, index ),
shapeStyle: group?.options?.legendShapeStyle,
} ) ),
[ dataSorted, getColor ]
);

// Register chart with context only if data is valid
const providerTheme = useChartTheme();
useChartRegistration( chartId, legendItems, providerTheme, 'bar', isDataValid, {
orientation,
withPatterns,
Expand Down Expand Up @@ -339,25 +332,36 @@ const BarChartInternal: FC< BarChartProps > = ( {
</XYChart>

{ showLegend && (
<Legend
items={ legendItems }
orientation={ legendOrientation }
alignmentHorizontal={ legendAlignmentHorizontal }
alignmentVertical={ legendAlignmentVertical }
className={ styles[ 'bar-chart__legend' ] }
shape={ legendShape }
ref={ legendRef }
/>
<div ref={ legendRef }>
<Legend
items={ legendItems }
orientation={ legendOrientation }
alignmentHorizontal={ legendAlignmentHorizontal }
alignmentVertical={ legendAlignmentVertical }
className={ styles[ 'bar-chart__legend' ] }
shape={ legendShape }
/>
</div>
) }
</div>
);
};

const BarChart: FC< BarChartProps > = props => (
<ChartProvider>
<BarChartInternal { ...props } />
</ChartProvider>
);
const BarChart: FC< BarChartProps > = props => {
const existingContext = useContext( ChartContext );

// If we're already in a ChartProvider context, don't create a new one
if ( existingContext ) {
return <BarChartInternal { ...props } />;
}

// Otherwise, create our own ChartProvider
return (
<ChartProvider>
<BarChartInternal { ...props } />
</ChartProvider>
);
};

BarChart.displayName = 'BarChart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { forwardRef, useCallback } from 'react';
import { useChartTheme } from '../../providers/theme';
import styles from './legend.module.scss';
import { valueOrIdentity, valueOrIdentityString, labelTransformFactory } from './utils';
import type { LegendProps } from './types';
import type { BaseLegendProps } from './types';

const orientationToFlexDirection = {
horizontal: 'row' as const,
Expand All @@ -17,7 +17,7 @@ const orientationToFlexDirection = {
* Base legend component that displays color-coded items with labels based on visx LegendOrdinal.
* We avoid using LegendOrdinal directly to enable support for advanced features such as interactivity.
*/
export const BaseLegend = forwardRef< HTMLDivElement, LegendProps >(
export const BaseLegend = forwardRef< HTMLDivElement, BaseLegendProps >(
(
{
items,
Expand Down Expand Up @@ -133,6 +133,7 @@ export const BaseLegend = forwardRef< HTMLDivElement, LegendProps >(
{ label.text }
{ items.find( item => item.label === label.text )?.value && (
<span className={ styles[ 'legend-item-value' ] }>
{ ' ' }
{ items.find( item => item.label === label.text )?.value }
</span>
) }
Expand Down
7 changes: 5 additions & 2 deletions projects/js-packages/charts/src/components/legend/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export { BaseLegend as Legend } from './base-legend';
export type { LegendProps } from './types';
export { Legend } from './legend';
export { BaseLegend } from './base-legend';
export { useChartLegendData } from './use-chart-legend-data';
export type { LegendProps, BaseLegendProps } from './types';
export type { ChartLegendOptions } from './use-chart-legend-data';
24 changes: 24 additions & 0 deletions projects/js-packages/charts/src/components/legend/legend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useContext, useMemo } from 'react';
import { ChartContext } from '../../providers/chart-context/chart-context';
import { BaseLegend } from './base-legend';
import type { LegendProps } from './types';
import type { FC } from 'react';

export const Legend: FC< LegendProps > = ( { chartId, items, ...props } ) => {
// Get context but don't throw if it doesn't exist
const context = useContext( ChartContext );

// Use useMemo to ensure re-rendering when context changes
const contextItems = useMemo( () => {
return chartId && context ? context.getChartData( chartId )?.legendItems : undefined;
}, [ chartId, context ] );

// Use context items if available, otherwise fall back to provided items
const legendItems = ( contextItems || items ) as typeof items;

if ( ! legendItems ) {
return null;
}

return <BaseLegend items={ legendItems } { ...props } />;
};
Loading