From a3155a60465711b030faabc24608449a5eb6303f Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 22:56:35 +1000 Subject: [PATCH 01/62] Add useChartLegendData hook and types for standalone legend component - Create ChartLegendOptions interface with showValues, withGlyph, glyphSize, and renderGlyph options - Add ChartLegendProps interface extending LegendProps with chartId for future context integration - Implement useChartLegendData hook with overloads for SeriesData[] and DataPoint[]/DataPointPercentage[] - Support both series data and point data with proper color, shape, and glyph handling - Include valueDisplay preference for DataPointPercentage items - Return properly typed LegendItemWithGlyph[] or LegendItemWithoutGlyph[] arrays --- .../src/components/chart-legend/types.ts | 15 ++++ .../chart-legend/use-chart-legend-data.ts | 85 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 projects/js-packages/charts/src/components/chart-legend/types.ts create mode 100644 projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts diff --git a/projects/js-packages/charts/src/components/chart-legend/types.ts b/projects/js-packages/charts/src/components/chart-legend/types.ts new file mode 100644 index 0000000000000..6728688e4e16f --- /dev/null +++ b/projects/js-packages/charts/src/components/chart-legend/types.ts @@ -0,0 +1,15 @@ +import type { LegendProps, LegendItemWithGlyph, LegendItemWithoutGlyph } from '../legend/types'; +import type { GlyphProps } from '@visx/xychart'; +import type { ReactNode } from 'react'; + +export interface ChartLegendOptions { + showValues?: boolean; + withGlyph?: boolean; + glyphSize?: number; + renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode; +} + +export interface ChartLegendProps extends Omit< LegendProps, 'items' > { + items: LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; + chartId?: string; +} diff --git a/projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts b/projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts new file mode 100644 index 0000000000000..f63b166d41660 --- /dev/null +++ b/projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts @@ -0,0 +1,85 @@ +import { useMemo } from 'react'; +import type { ChartLegendOptions } from './types'; +import type { SeriesData, ChartTheme, DataPoint, DataPointPercentage } from '../../types'; +import type { LegendItemWithGlyph, LegendItemWithoutGlyph } from '../legend/types'; + +// Overload for SeriesData[] +export function useChartLegendData( + data: SeriesData[], + theme: ChartTheme, + options?: ChartLegendOptions +): LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; + +// Overload for DataPoint[] or DataPointPercentage[] +export function useChartLegendData( + data: DataPoint[] | DataPointPercentage[], + theme: ChartTheme, + options?: ChartLegendOptions +): LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; + +/** + * Converts chart data to legend items with proper typing based on glyph usage + * @param data - Chart data (SeriesData[], DataPoint[], or DataPointPercentage[]) + * @param theme - Chart theme containing colors and optional glyphs + * @param options - Configuration options for legend item generation + * @return Array of legend items with proper typing for glyph usage + */ +export function useChartLegendData( + data: SeriesData[] | DataPoint[] | DataPointPercentage[], + theme: ChartTheme, + options: ChartLegendOptions = {} +): LegendItemWithGlyph[] | LegendItemWithoutGlyph[] { + return useMemo( () => { + // Check if it's SeriesData[] by checking if first item has a data property + const isSeriesData = data.length > 0 && 'data' in data[ 0 ]; + + // Check if any item will have a glyph to determine return type + const hasGlyphs = options.withGlyph && ( theme.glyphs || options.renderGlyph ); + + if ( isSeriesData ) { + const seriesData = data as SeriesData[]; + if ( hasGlyphs ) { + return seriesData.map( ( group, index ) => ( { + label: group.label, + value: options.showValues ? '' : '', // SeriesData doesn't have a value property + color: group.options?.stroke ?? theme.colors[ index % theme.colors.length ], + shapeStyle: group.options?.legendShapeStyle, + renderGlyph: theme.glyphs?.[ index ] ?? options.renderGlyph!, + glyphSize: options.glyphSize ?? 4, + } ) ) as LegendItemWithGlyph[]; + } + return seriesData.map( ( group, index ) => ( { + label: group.label, + value: options.showValues ? '' : '', // SeriesData doesn't have a value property + color: group.options?.stroke ?? theme.colors[ index % theme.colors.length ], + shapeStyle: group.options?.legendShapeStyle, + glyphSize: options.glyphSize ?? 4, + } ) ) as LegendItemWithoutGlyph[]; + } + // Handle DataPoint[] or DataPointPercentage[] + const pointData = data as DataPoint[] | DataPointPercentage[]; + if ( hasGlyphs ) { + return pointData.map( ( item, index ) => ( { + label: item.label, + value: options.showValues + ? ( 'valueDisplay' in item ? item.valueDisplay : item.value.toString() ) || + item.value.toString() + : '', + color: theme.colors[ index % theme.colors.length ], + shapeStyle: undefined, + renderGlyph: theme.glyphs?.[ index ] ?? options.renderGlyph!, + glyphSize: options.glyphSize ?? 4, + } ) ) as LegendItemWithGlyph[]; + } + return pointData.map( ( item, index ) => ( { + label: item.label, + value: options.showValues + ? ( 'valueDisplay' in item ? item.valueDisplay : item.value.toString() ) || + item.value.toString() + : '', + color: theme.colors[ index % theme.colors.length ], + shapeStyle: undefined, + glyphSize: options.glyphSize ?? 4, + } ) ) as LegendItemWithoutGlyph[]; + }, [ data, theme, options ] ); +} From 7f488d879e36e8917772212bf189e77b2e977438 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 22:57:41 +1000 Subject: [PATCH 02/62] Add ChartLegend standalone component - Create ChartLegend component as a wrapper around BaseLegend - Accept chartId prop for future context integration - Pass through all legend props to BaseLegend for consistent behavior - Enables standalone legend usage independent of chart showLegend prop --- .../charts/src/components/chart-legend/chart-legend.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx diff --git a/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx b/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx new file mode 100644 index 0000000000000..9707287cd5955 --- /dev/null +++ b/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx @@ -0,0 +1,7 @@ +import { BaseLegend } from '../legend/base-legend'; +import type { ChartLegendProps } from './types'; +import type { FC } from 'react'; + +export const ChartLegend: FC< ChartLegendProps > = ( { items, ...props } ) => { + return ; +}; From eb072e6a019c192e6d4e6ec4195d99f81f0a1ea0 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 22:57:59 +1000 Subject: [PATCH 03/62] Refactor LineChart to use useChartLegendData hook - Replace inline legend items creation with useChartLegendData hook - Pass withGlyph and glyphSize options to maintain existing functionality - Eliminates duplicate legend creation logic - Maintains all existing legend behavior and styling --- .../src/components/line-chart/line-chart.tsx | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index ba95e0dea811d..c22b45a87607f 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -6,6 +6,7 @@ import clsx from 'clsx'; import { useId, useMemo, useContext, useState, useRef } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useXYChartTheme, useChartTheme } from '../../providers/theme/theme-provider'; +import { useChartLegendData } from '../chart-legend/use-chart-legend-data'; import { Legend } from '../legend'; import { DefaultGlyph } from '../shared/default-glyph'; import { useChartDataTransform } from '../shared/use-chart-data-transform'; @@ -272,27 +273,12 @@ const LineChartInternal: FC< LineChartProps > = ( { 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: - group?.options?.stroke ?? providerTheme.colors[ index % providerTheme.colors.length ], - shapeStyle: group?.options?.legendShapeStyle, - renderGlyph: withLegendGlyph ? providerTheme.glyphs?.[ index ] ?? renderGlyph : undefined, - glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ), - } ) ), - [ - dataSorted, - providerTheme.colors, - providerTheme.glyphs, - withLegendGlyph, - renderGlyph, - glyphStyle?.radius, - ] - ); + // Create legend items using the reusable hook + const legendItems = useChartLegendData( dataSorted, providerTheme, { + withGlyph: withLegendGlyph, + glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ), + renderGlyph, + } ); // Register chart with context only if data is valid useChartRegistration( chartId, legendItems, providerTheme, 'line', isDataValid, { From 301b80a32b0159b5482cd6c1849a858a6beb4c17 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 22:59:05 +1000 Subject: [PATCH 04/62] Refactor BarChart to use useChartLegendData hook - Replace inline legend items creation with useChartLegendData hook - Add useChartTheme hook to get provider theme for consistent legend colors - Move hook call to proper position to avoid rules-of-hooks violation - Remove duplicate legend creation logic - Maintains all existing legend behavior and styling --- .../src/components/bar-chart/bar-chart.tsx | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx index 033a0c6faf499..4e607302933c7 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx @@ -1,9 +1,10 @@ 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, useId, useState, useRef } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useChartTheme, useXYChartTheme } from '../../providers/theme'; +import { useChartLegendData } from '../chart-legend/use-chart-legend-data'; import { Legend } from '../legend'; import { useChartDataTransform } from '../shared/use-chart-data-transform'; import { useChartMargin } from '../shared/use-chart-margin'; @@ -66,10 +67,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 ); + const chartOptions = useBarChartOptions( dataSorted, horizontal, options ); const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme, horizontal ); const [ legendRef, legendHeight ] = useElementHeight< HTMLDivElement >(); @@ -222,20 +227,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, From 0348c8e8b6d8cae303c3a2ece7580c32a013a267 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 23:01:15 +1000 Subject: [PATCH 05/62] Refactor PieChart and PieSemiCircleChart to use useChartLegendData hook - Replace inline legend items creation with useChartLegendData hook in both components - Enable showValues option for both charts to display percentages in legend - Move hook calls to proper position to avoid rules-of-hooks violations - Remove duplicate legend creation logic - Maintain valueDisplay support for properly formatted percentage values - Keep all existing legend behavior and styling --- .../src/components/pie-chart/pie-chart.tsx | 18 ++++++------------ .../pie-semi-circle-chart.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx index ee2151a398d48..0aa77eb2772b3 100644 --- a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx @@ -1,10 +1,10 @@ import { Group } from '@visx/group'; import { Pie } from '@visx/shape'; import clsx from 'clsx'; -import { useMemo } from 'react'; import useChartMouseHandler from '../../hooks/use-chart-mouse-handler'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useChartTheme, defaultTheme } from '../../providers/theme'; +import { useChartLegendData } from '../chart-legend/use-chart-legend-data'; import { Legend } from '../legend'; import { useElementHeight } from '../shared/use-element-height'; import { withResponsive } from '../shared/with-responsive'; @@ -108,18 +108,12 @@ const PieChartInternal = ( { withTooltips, } ); - const { isValid, message } = validateData( data ); + // Create legend items using the reusable hook + const legendItems = useChartLegendData( data, providerTheme, { + showValues: true, + } ); - // Create legend items (hooks must be called in same order every render) - const legendItems = useMemo( - () => - data.map( ( item, index ) => ( { - label: item.label, - value: item.value.toString(), - color: providerTheme.colors[ index % providerTheme.colors.length ], - } ) ), - [ data, providerTheme.colors ] - ); + const { isValid, message } = validateData( data ); // Register chart with context only if data is valid useChartRegistration( chartId, legendItems, providerTheme, 'pie', isValid, { diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx index ee6ec3efe2421..4686937737eaa 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -7,6 +7,7 @@ import clsx from 'clsx'; import { useCallback, useMemo } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useChartTheme } from '../../providers/theme/theme-provider'; +import { useChartLegendData } from '../chart-legend/use-chart-legend-data'; import { Legend } from '../legend'; import { useElementHeight } from '../shared/use-element-height'; import { withResponsive } from '../shared/with-responsive'; @@ -93,6 +94,11 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip< DataPointPercentage >(); + // Create legend items using the reusable hook + const legendItems = useChartLegendData( data, providerTheme, { + showValues: true, + } ); + const handleMouseMove = useCallback( ( event: MouseEvent, arc: ArcData ) => { const coords = localPoint( event ); @@ -184,7 +190,6 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { // Set the clockwise direction based on the prop const startAngle = clockwise ? -Math.PI / 2 : Math.PI / 2; const endAngle = clockwise ? Math.PI / 2 : -Math.PI / 2; - return (
Date: Wed, 9 Jul 2025 23:01:34 +1000 Subject: [PATCH 06/62] Add ChartLegend exports to package API - Export ChartLegend component and useChartLegendData hook from main index - Export ChartLegendProps and ChartLegendOptions types - Add chart-legend module index file with all exports - Makes standalone legend functionality available to consumers --- .../js-packages/charts/src/components/chart-legend/index.ts | 3 +++ projects/js-packages/charts/src/index.ts | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 projects/js-packages/charts/src/components/chart-legend/index.ts diff --git a/projects/js-packages/charts/src/components/chart-legend/index.ts b/projects/js-packages/charts/src/components/chart-legend/index.ts new file mode 100644 index 0000000000000..9016c3104f4ca --- /dev/null +++ b/projects/js-packages/charts/src/components/chart-legend/index.ts @@ -0,0 +1,3 @@ +export { ChartLegend } from './chart-legend'; +export { useChartLegendData } from './use-chart-legend-data'; +export type { ChartLegendProps, ChartLegendOptions } from './types'; diff --git a/projects/js-packages/charts/src/index.ts b/projects/js-packages/charts/src/index.ts index 550be35b069d1..bcb78d7a3f780 100644 --- a/projects/js-packages/charts/src/index.ts +++ b/projects/js-packages/charts/src/index.ts @@ -8,6 +8,7 @@ export { BarListChart } from './components/bar-list-chart'; // Chart components export { BaseTooltip } from './components/tooltip'; export { Legend } from './components/legend'; +export { ChartLegend, useChartLegendData } from './components/chart-legend'; // Themes export { ThemeProvider } from './providers/theme'; @@ -33,3 +34,4 @@ export type { export type { LineStyles, GridStyles } from '@visx/xychart'; export type { RenderLineStartGlyphProps } from './components/line-chart/line-chart'; +export type { ChartLegendProps, ChartLegendOptions } from './components/chart-legend'; From f29b9c04441a9a3cb6c0dadfddfd50e4d22fe73e Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 23:02:30 +1000 Subject: [PATCH 07/62] Add comprehensive stories and tests for ChartLegend - Create multiple story examples showing different usage patterns - Include stories for standalone usage, chart integration, and alignment options - Add stories demonstrating usage with LineChart, BarChart, and PieChart - Include examples for multiple charts sharing a legend - Add comprehensive test suite for ChartLegend component and useChartLegendData hook - Test both SeriesData and DataPointPercentage data types - Test memoization, glyph handling, and value display functionality - Fix TypeScript any usage in mock functions --- .../chart-legend/chart-legend.test.tsx | 245 ++++++++++++++++++ .../chart-legend/stories/index.stories.tsx | 235 +++++++++++++++++ 2 files changed, 480 insertions(+) create mode 100644 projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx create mode 100644 projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx diff --git a/projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx b/projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx new file mode 100644 index 0000000000000..2e7d36723dadb --- /dev/null +++ b/projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx @@ -0,0 +1,245 @@ +import { render, screen, renderHook } from '@testing-library/react'; +import { defaultTheme } from '../../providers/theme'; +import { ChartLegend } from './chart-legend'; +import { useChartLegendData } from './use-chart-legend-data'; +import type { SeriesData, DataPointPercentage } from '../../types'; + +// Mock the BaseLegend component +jest.mock( '../legend/base-legend', () => ( { + BaseLegend: ( { + items, + orientation, + testId, + }: { + items: Array< { label: string; value: string; color: string } >; + orientation: string; + testId?: string; + } ) => ( +
+
{ orientation }
+ { items.map( ( item: { label: string; value: string; color: string }, index: number ) => ( +
+ { item.label } + { item.value } + { item.color } +
+ ) ) } +
+ ), +} ) ); + +describe( 'ChartLegend', () => { + const mockItems = [ + { label: 'Desktop', value: '65%', color: '#3858E9' }, + { label: 'Mobile', value: '35%', color: '#80C8FF' }, + ]; + + it( 'renders legend items correctly', () => { + render( ); + + expect( screen.getByTestId( 'base-legend' ) ).toBeInTheDocument(); + expect( screen.getByTestId( 'legend-orientation' ) ).toHaveTextContent( 'horizontal' ); + expect( screen.getByTestId( 'legend-item-0' ) ).toBeInTheDocument(); + expect( screen.getByTestId( 'legend-item-1' ) ).toBeInTheDocument(); + } ); + + it( 'passes through props to BaseLegend', () => { + render( + + ); + + expect( screen.getByTestId( 'legend-orientation' ) ).toHaveTextContent( 'vertical' ); + } ); + + it( 'accepts chartId prop for future context integration', () => { + // This test ensures the prop is accepted without error + expect( () => { + render( ); + } ).not.toThrow(); + } ); + + it( 'renders correct legend labels and values', () => { + render( ); + + const labels = screen.getAllByTestId( 'legend-label' ); + const values = screen.getAllByTestId( 'legend-value' ); + const colors = screen.getAllByTestId( 'legend-color' ); + + expect( labels[ 0 ] ).toHaveTextContent( 'Desktop' ); + expect( labels[ 1 ] ).toHaveTextContent( 'Mobile' ); + expect( values[ 0 ] ).toHaveTextContent( '65%' ); + expect( values[ 1 ] ).toHaveTextContent( '35%' ); + expect( colors[ 0 ] ).toHaveTextContent( '#3858E9' ); + expect( colors[ 1 ] ).toHaveTextContent( '#80C8FF' ); + } ); +} ); + +describe( 'useChartLegendData', () => { + const mockTheme = { + ...defaultTheme, + colors: [ '#3858E9', '#80C8FF', '#44B556' ], + }; + + describe( 'with SeriesData[]', () => { + const seriesData: SeriesData[] = [ + { + label: 'Desktop', + data: [ + { date: new Date( '2023-01-01' ), value: 100 }, + { date: new Date( '2023-01-02' ), value: 150 }, + ], + }, + { + label: 'Mobile', + data: [ + { date: new Date( '2023-01-01' ), value: 80 }, + { date: new Date( '2023-01-02' ), value: 90 }, + ], + options: { + stroke: '#FF0000', + legendShapeStyle: { borderRadius: '4px' }, + }, + }, + ]; + + it( 'converts SeriesData to BaseLegendItem format', () => { + const { result } = renderHook( () => useChartLegendData( seriesData, mockTheme ) ); + + expect( result.current ).toEqual( [ + { + label: 'Desktop', + value: '', + color: '#3858E9', + shapeStyle: undefined, + renderGlyph: undefined, + glyphSize: 4, + }, + { + label: 'Mobile', + value: '', + color: '#FF0000', + shapeStyle: { borderRadius: '4px' }, + renderGlyph: undefined, + glyphSize: 4, + }, + ] ); + } ); + + it( 'shows values when showValues option is true', () => { + const { result } = renderHook( () => + useChartLegendData( seriesData, mockTheme, { showValues: true } ) + ); + + expect( result.current[ 0 ].value ).toBe( '' ); + expect( result.current[ 1 ].value ).toBe( '' ); + } ); + + it( 'includes glyph when withGlyph option is true', () => { + const mockRenderGlyph = jest.fn(); + const themeWithGlyph = { + ...mockTheme, + glyphs: [ mockRenderGlyph ], + }; + + const { result } = renderHook( () => + useChartLegendData( seriesData, themeWithGlyph, { withGlyph: true, glyphSize: 8 } ) + ); + + expect( result.current[ 0 ].renderGlyph ).toBe( mockRenderGlyph ); + expect( result.current[ 0 ].glyphSize ).toBe( 8 ); + } ); + } ); + + describe( 'with DataPointPercentage[]', () => { + const pieData: DataPointPercentage[] = [ + { label: 'Desktop', value: 65, percentage: 65 }, + { label: 'Mobile', value: 35, percentage: 35 }, + ]; + + it( 'converts DataPointPercentage to BaseLegendItem format', () => { + const { result } = renderHook( () => useChartLegendData( pieData, mockTheme ) ); + + expect( result.current ).toEqual( [ + { + label: 'Desktop', + value: '', + color: '#3858E9', + shapeStyle: undefined, + renderGlyph: undefined, + glyphSize: 4, + }, + { + label: 'Mobile', + value: '', + color: '#80C8FF', + shapeStyle: undefined, + renderGlyph: undefined, + glyphSize: 4, + }, + ] ); + } ); + + it( 'shows values when showValues option is true', () => { + const { result } = renderHook( () => + useChartLegendData( pieData, mockTheme, { showValues: true } ) + ); + + expect( result.current[ 0 ].value ).toBe( '65' ); + expect( result.current[ 1 ].value ).toBe( '35' ); + } ); + + it( 'cycles through theme colors correctly', () => { + const largeDataSet: DataPointPercentage[] = [ + { label: 'A', value: 25, percentage: 25 }, + { label: 'B', value: 25, percentage: 25 }, + { label: 'C', value: 25, percentage: 25 }, + { label: 'D', value: 25, percentage: 25 }, + ]; + + const { result } = renderHook( () => useChartLegendData( largeDataSet, mockTheme ) ); + + expect( result.current[ 0 ].color ).toBe( '#3858E9' ); + expect( result.current[ 1 ].color ).toBe( '#80C8FF' ); + expect( result.current[ 2 ].color ).toBe( '#44B556' ); + expect( result.current[ 3 ].color ).toBe( '#3858E9' ); // Cycles back to first color + } ); + } ); + + it( 'handles empty data arrays', () => { + const { result } = renderHook( () => useChartLegendData( [], mockTheme ) ); + + expect( result.current ).toEqual( [] ); + } ); + + it( 'memoizes results correctly', () => { + const testData = [ { label: 'Test', value: 100, percentage: 100 } ] as DataPointPercentage[]; + const testOptions = { showValues: true }; + + const { result, rerender } = renderHook( + ( { data, theme, options } ) => useChartLegendData( data, theme, options ), + { + initialProps: { + data: testData, + theme: mockTheme, + options: testOptions, + }, + } + ); + + const firstResult = result.current; + + // Re-render with same props - should return same reference + rerender( { + data: testData, + theme: mockTheme, + options: testOptions, + } ); + + expect( result.current ).toBe( firstResult ); + } ); +} ); diff --git a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx new file mode 100644 index 0000000000000..f0168654caec7 --- /dev/null +++ b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx @@ -0,0 +1,235 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { useChartTheme } from '../../../providers/theme'; +import { BarChart } from '../../bar-chart'; +import { LineChart } from '../../line-chart'; +import { PieChart } from '../../pie-chart'; +import { ChartLegend } from '../chart-legend'; +import { useChartLegendData } from '../use-chart-legend-data'; +import type { SeriesData, DataPointPercentage } from '../../../types'; + +const meta: Meta< typeof ChartLegend > = { + title: 'JS Packages/Charts/Composites/ChartLegend', + component: ChartLegend, + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'A standalone legend component that can be used independently from charts or connected to chart data.', + }, + }, + }, +}; + +export default meta; +type Story = StoryObj< typeof ChartLegend >; + +// Mock data for different chart types +const lineChartData: SeriesData[] = [ + { + label: 'Desktop', + data: [ + { date: new Date( '2023-01-01' ), value: 100 }, + { date: new Date( '2023-01-02' ), value: 150 }, + { date: new Date( '2023-01-03' ), value: 120 }, + ], + }, + { + label: 'Mobile', + data: [ + { date: new Date( '2023-01-01' ), value: 80 }, + { date: new Date( '2023-01-02' ), value: 90 }, + { date: new Date( '2023-01-03' ), value: 110 }, + ], + }, +]; + +const barChartData: SeriesData[] = [ + { + label: 'Q1 Sales', + data: [ + { label: 'Jan', value: 1000 }, + { label: 'Feb', value: 1200 }, + { label: 'Mar', value: 1100 }, + ], + }, + { + label: 'Q2 Sales', + data: [ + { label: 'Jan', value: 800 }, + { label: 'Feb', value: 900 }, + { label: 'Mar', value: 1000 }, + ], + }, +]; + +const pieChartData: DataPointPercentage[] = [ + { label: 'Desktop', value: 65, percentage: 65 }, + { label: 'Mobile', value: 35, percentage: 35 }, +]; + +// Story that demonstrates the standalone component with manual legend items +export const Standalone: Story = { + args: { + items: [ + { label: 'Desktop', value: '65%', color: '#3858E9' }, + { label: 'Mobile', value: '35%', color: '#80C8FF' }, + ], + orientation: 'horizontal', + }, +}; + +// Story showing vertical orientation +export const Vertical: Story = { + args: { + items: [ + { label: 'Desktop', value: '65%', color: '#3858E9' }, + { label: 'Mobile', value: '35%', color: '#80C8FF' }, + { label: 'Tablet', value: '12%', color: '#44B556' }, + ], + orientation: 'vertical', + }, +}; + +// Story showing use with LineChart data +const WithLineChartData = () => { + const theme = useChartTheme(); + const legendItems = useChartLegendData( lineChartData, theme, { + showValues: false, + } ); + + return ( +
+ + +
+ ); +}; + +export const WithLineChart: Story = { + render: () => , + parameters: { + docs: { + description: { + story: 'ChartLegend used with LineChart data, positioned independently below the chart.', + }, + }, + }, +}; + +// Story showing use with BarChart data +const WithBarChartData = () => { + const theme = useChartTheme(); + const legendItems = useChartLegendData( barChartData, theme ); + + return ( +
+ + +
+ ); +}; + +export const WithBarChart: Story = { + render: () => , + parameters: { + docs: { + description: { + story: 'ChartLegend used with BarChart data, positioned vertically beside the chart.', + }, + }, + }, +}; + +// Story showing use with PieChart data +const WithPieChartData = () => { + const theme = useChartTheme(); + const legendItems = useChartLegendData( pieChartData, theme, { + showValues: true, + } ); + + return ( +
+ + +
+ ); +}; + +export const WithPieChart: Story = { + render: () => , + parameters: { + docs: { + description: { + story: 'ChartLegend used with PieChart data, showing values in the legend.', + }, + }, + }, +}; + +// Story showing multiple charts with shared legend +const MultipleChartsSharedLegend = () => { + const theme = useChartTheme(); + const legendItems = useChartLegendData( lineChartData, theme ); + + return ( +
+
+ + +
+ +
+ ); +}; + +export const MultipleChartsSharedLegend: Story = { + render: () => , + parameters: { + docs: { + description: { + story: 'Single ChartLegend component used for multiple charts with shared data series.', + }, + }, + }, +}; + +// Story showing different alignment options +export const AlignmentOptions: Story = { + args: { + items: [ + { label: 'Series 1', value: '25%', color: '#3858E9' }, + { label: 'Series 2', value: '35%', color: '#80C8FF' }, + { label: 'Series 3', value: '40%', color: '#44B556' }, + ], + orientation: 'horizontal', + alignmentHorizontal: 'left', + alignmentVertical: 'top', + }, + parameters: { + docs: { + description: { + story: 'ChartLegend with custom alignment options.', + }, + }, + }, +}; + +// Story showing the legend with custom shapes +export const CustomShape: Story = { + args: { + items: [ + { label: 'Desktop', value: '65%', color: '#3858E9' }, + { label: 'Mobile', value: '35%', color: '#80C8FF' }, + ], + orientation: 'horizontal', + shape: 'circle', + }, + parameters: { + docs: { + description: { + story: 'ChartLegend with circle shape instead of default rectangle.', + }, + }, + }, +}; From ad3bb4a5f6c357a6486004c8b7db41b1635af00a Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 9 Jul 2025 23:34:08 +1000 Subject: [PATCH 08/62] changelog --- .../changelog/add-charts-50-standalone-legend-component | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/js-packages/charts/changelog/add-charts-50-standalone-legend-component diff --git a/projects/js-packages/charts/changelog/add-charts-50-standalone-legend-component b/projects/js-packages/charts/changelog/add-charts-50-standalone-legend-component new file mode 100644 index 0000000000000..150649e8cac6e --- /dev/null +++ b/projects/js-packages/charts/changelog/add-charts-50-standalone-legend-component @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Charts: adds a standalone chart legend component From cf952ea3c09b67e12c21bf182eb5f32745182011 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Fri, 11 Jul 2025 18:55:15 +1000 Subject: [PATCH 09/62] Fix rebase conflicts and Storybook build errors - Resolve merge conflicts from chart context foundation PR - Fix duplicate legendItems declarations in PieChart and PieSemiCircleChart - Maintain custom color handling in PieSemiCircleChart using accessors - Fix type-only import for AnnotationStyles - Fix duplicate identifier in ChartLegend stories --- .../src/components/chart-legend/stories/index.stories.tsx | 4 ++-- .../pie-semi-circle-chart/pie-semi-circle-chart.tsx | 8 +------- projects/js-packages/charts/src/types.ts | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx index f0168654caec7..2b014eb1601e3 100644 --- a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx @@ -168,7 +168,7 @@ export const WithPieChart: Story = { }; // Story showing multiple charts with shared legend -const MultipleChartsSharedLegend = () => { +const MultipleChartsSharedLegendComponent = () => { const theme = useChartTheme(); const legendItems = useChartLegendData( lineChartData, theme ); @@ -184,7 +184,7 @@ const MultipleChartsSharedLegend = () => { }; export const MultipleChartsSharedLegend: Story = { - render: () => , + render: () => , parameters: { docs: { description: { diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx index 4686937737eaa..5884bcd9c5c70 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -7,7 +7,6 @@ import clsx from 'clsx'; import { useCallback, useMemo } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useChartTheme } from '../../providers/theme/theme-provider'; -import { useChartLegendData } from '../chart-legend/use-chart-legend-data'; import { Legend } from '../legend'; import { useElementHeight } from '../shared/use-element-height'; import { withResponsive } from '../shared/with-responsive'; @@ -94,11 +93,6 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip< DataPointPercentage >(); - // Create legend items using the reusable hook - const legendItems = useChartLegendData( data, providerTheme, { - showValues: true, - } ); - const handleMouseMove = useCallback( ( event: MouseEvent, arc: ArcData ) => { const coords = localPoint( event ); @@ -142,7 +136,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { [ providerTheme.colors ] ); - // Create legend items (hooks must be called in same order every render) + // Create legend items with color from accessors (which respects item.color) const legendItems = useMemo( () => data.map( ( item, index ) => ( { diff --git a/projects/js-packages/charts/src/types.ts b/projects/js-packages/charts/src/types.ts index c51a57b157b6e..e71489e4016af 100644 --- a/projects/js-packages/charts/src/types.ts +++ b/projects/js-packages/charts/src/types.ts @@ -1,4 +1,4 @@ -import { AnnotationStyles } from './components/line-chart/line-chart-annotation'; +import type { AnnotationStyles } from './components/line-chart/line-chart-annotation'; import type { AxisScale, Orientation, TickFormatter, AxisRendererProps } from '@visx/axis'; import type { LegendShape } from '@visx/legend/lib/types'; import type { ScaleInput, ScaleType } from '@visx/scale'; From f384eb9ea37872899f6a42fc79a3a8dbb8b6e0b1 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Fri, 11 Jul 2025 22:13:00 +1000 Subject: [PATCH 10/62] Complete ChartLegend component implementation - Add chart context integration to automatically retrieve legend data - Support chartId prop for standalone usage - Maintain fallback to items prop for manual data passing - Add story demonstrating standalone usage with chartId - Component works with showLegend=false charts - Accepts all BaseLegend props through inheritance --- .../components/chart-legend/chart-legend.tsx | 17 +++++++++-- .../chart-legend/stories/index.stories.tsx | 30 +++++++++++++++++++ .../src/components/chart-legend/types.ts | 2 +- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx b/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx index 9707287cd5955..11c2855d19f7e 100644 --- a/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx +++ b/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx @@ -1,7 +1,20 @@ +import { useChartContext } from '../../providers/chart-context'; import { BaseLegend } from '../legend/base-legend'; import type { ChartLegendProps } from './types'; import type { FC } from 'react'; -export const ChartLegend: FC< ChartLegendProps > = ( { items, ...props } ) => { - return ; +export const ChartLegend: FC< ChartLegendProps > = ( { chartId, items, ...props } ) => { + const { getChartData } = useChartContext(); + + // If chartId is provided, get items from chart context + const contextItems = chartId ? getChartData( chartId )?.legendItems : undefined; + + // Use context items if available, otherwise fall back to provided items + const legendItems = ( contextItems || items ) as typeof items; + + if ( ! legendItems ) { + return null; + } + + return ; }; diff --git a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx index 2b014eb1601e3..b90f6f9a23f69 100644 --- a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx @@ -233,3 +233,33 @@ export const CustomShape: Story = { }, }, }; + +// Story showing standalone legend using chartId to automatically get data from context +const StandaloneLegendWithChartIdComponent = () => { + return ( +
+ { /* Chart with legend hidden but still registering data */ } + + { /* Standalone legend that automatically gets data from chart context */ } + +
+ ); +}; + +export const StandaloneLegendWithChartId: Story = { + render: () => , + parameters: { + docs: { + description: { + story: + 'ChartLegend component automatically retrieving legend data from chart context using chartId. The chart has showLegend=false but the standalone legend still works.', + }, + }, + }, +}; diff --git a/projects/js-packages/charts/src/components/chart-legend/types.ts b/projects/js-packages/charts/src/components/chart-legend/types.ts index 6728688e4e16f..e7ea779b4b526 100644 --- a/projects/js-packages/charts/src/components/chart-legend/types.ts +++ b/projects/js-packages/charts/src/components/chart-legend/types.ts @@ -10,6 +10,6 @@ export interface ChartLegendOptions { } export interface ChartLegendProps extends Omit< LegendProps, 'items' > { - items: LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; + items?: LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; chartId?: string; } From dbe429617087baa4cf47ddee91d9287cc29dfb6d Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:03:12 +1000 Subject: [PATCH 11/62] Add shared validation utilities - Create validateSeriesData for line/bar charts - Create validatePercentageData for pie charts - Create validateSemiCircleData for semi-circle charts - Create validateBarChartData for bar-specific validation - Eliminates ~60 lines of duplicated validation code --- .../charts/src/utils/validation.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 projects/js-packages/charts/src/utils/validation.ts diff --git a/projects/js-packages/charts/src/utils/validation.ts b/projects/js-packages/charts/src/utils/validation.ts new file mode 100644 index 0000000000000..b2b11039f0e57 --- /dev/null +++ b/projects/js-packages/charts/src/utils/validation.ts @@ -0,0 +1,131 @@ +import type { SeriesData, DataPointPercentage, DataPointDate, DataPoint } from '../types'; + +/** + * Validation result interface + */ +export interface ValidationResult { + isValid: boolean; + message?: string; +} + +/** + * Validates SeriesData for charts that use series-based data (Line Chart, Bar Chart) + * @param data - The series data to validate + * @return Error message if invalid, null if valid + */ +export function validateSeriesData( data: SeriesData[] ): string | null { + if ( ! data?.length ) return 'No data available'; + + const hasInvalidData = data.some( series => + series.data.some( + ( point: DataPointDate | DataPoint ) => + isNaN( point.value as number ) || + point.value === null || + point.value === undefined || + ( 'date' in point && point.date && isNaN( point.date.getTime() ) ) || + ( ! ( 'date' in point ) && ! ( 'label' in point ) ) + ) + ); + + if ( hasInvalidData ) { + return 'Invalid data: Please check that all data points have valid values and dates/labels'; + } + + return null; +} + +/** + * Validates DataPointPercentage for charts that use percentage-based data (Pie Chart, Pie Semi-Circle Chart) + * @param data - The percentage data to validate + * @param requireTotal100 - Whether to require the total percentage to equal 100% + * @return Validation result with isValid and optional message + */ +export function validatePercentageData( + data: DataPointPercentage[], + requireTotal100 = false +): ValidationResult { + if ( ! data.length ) { + return { isValid: false, message: 'No data available' }; + } + + // Check for negative values + const hasNegativeValues = data.some( item => item.percentage < 0 || item.value < 0 ); + if ( hasNegativeValues ) { + return { isValid: false, message: 'Invalid data: Negative values are not allowed' }; + } + + // Check for invalid values + const hasInvalidValues = data.some( + item => + isNaN( item.percentage ) || + isNaN( item.value ) || + item.percentage === null || + item.value === null || + item.percentage === undefined || + item.value === undefined + ); + if ( hasInvalidValues ) { + return { + isValid: false, + message: + 'Invalid data: Please check that all data points have valid percentage and value numbers', + }; + } + + // Validate total percentage (for pie charts that require 100%) + if ( requireTotal100 ) { + const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 ); + if ( Math.abs( totalPercentage - 100 ) > 0.01 ) { + // Using small epsilon for floating point comparison + return { isValid: false, message: 'Invalid percentage total: Must equal 100' }; + } + } + + return { isValid: true }; +} + +/** + * Validates DataPointPercentage for semi-circle charts (requires total > 0, not necessarily 100%) + * @param data - The percentage data to validate + * @return Validation result with isValid and optional message + */ +export function validateSemiCircleData( data: DataPointPercentage[] ): ValidationResult { + const basicValidation = validatePercentageData( data, false ); + if ( ! basicValidation.isValid ) { + return basicValidation; + } + + // Validate total percentage is greater than 0 + const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 ); + if ( totalPercentage <= 0 ) { + return { isValid: false, message: 'Invalid percentage total: Must be greater than 0' }; + } + + return { isValid: true }; +} + +/** + * Validates data for Bar Chart with additional label requirements + * @param data - The series data to validate + * @return Error message if invalid, null if valid + */ +export function validateBarChartData( data: SeriesData[] ): string | null { + if ( ! data?.length ) return 'No data available'; + + const hasInvalidData = data.some( series => + series.data.some( + ( point: DataPointDate | DataPoint ) => + isNaN( point.value as number ) || + point.value === null || + point.value === undefined || + ( ! ( 'label' in point ) && + ( ! ( 'date' in point && point.date ) || isNaN( point.date.getTime() ) ) ) + ) + ); + + if ( hasInvalidData ) { + return 'Invalid data: Please check that all data points have valid values and labels or dates'; + } + + return null; +} From 5da4e049c25945b9229b6c7974df1223ccb0d69f Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:03:35 +1000 Subject: [PATCH 12/62] Add context-aware Legend component - Enhances existing Legend to support chartId prop - Automatically retrieves legend data from chart context - Falls back to manual items prop for backward compatibility - Enables standalone legend usage with --- .../charts/src/components/legend/legend.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 projects/js-packages/charts/src/components/legend/legend.tsx diff --git a/projects/js-packages/charts/src/components/legend/legend.tsx b/projects/js-packages/charts/src/components/legend/legend.tsx new file mode 100644 index 0000000000000..a38368151f073 --- /dev/null +++ b/projects/js-packages/charts/src/components/legend/legend.tsx @@ -0,0 +1,23 @@ +import { useContext } 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 ); + + // If chartId is provided and context exists, get items from chart context + const contextItems = + chartId && context ? context.getChartData( chartId )?.legendItems : undefined; + + // Use context items if available, otherwise fall back to provided items + const legendItems = ( contextItems || items ) as typeof items; + + if ( ! legendItems ) { + return null; + } + + return ; +}; From 266da39538344aec64009d06f34cd47d1ea06000 Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:06:00 +1000 Subject: [PATCH 13/62] Refactor useChartLegendData hook for better maintainability - Extract formatPointValue for consistent value formatting - Extract createBaseLegendItem for common legend item creation - Extract processSeriesData and processPointData functions - Eliminates ~50 lines of duplicated mapping logic - Improves code readability and testability --- .../legend/use-chart-legend-data.ts | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 projects/js-packages/charts/src/components/legend/use-chart-legend-data.ts diff --git a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.ts b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.ts new file mode 100644 index 0000000000000..718304a0b89f8 --- /dev/null +++ b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.ts @@ -0,0 +1,170 @@ +import { useMemo } from 'react'; +import type { LegendItemWithGlyph, LegendItemWithoutGlyph } from './types'; +import type { ChartTheme, SeriesData, DataPointDate, DataPointPercentage } from '../../types'; + +export interface ChartLegendOptions { + withGlyph?: boolean; + glyphSize?: number; + renderGlyph?: React.ComponentType< any >; + showValues?: boolean; +} + +/** + * Formats the value for a data point based on its type + * @param point + * @param showValues + */ +function formatPointValue( + point: DataPointDate | DataPointPercentage, + showValues: boolean +): string { + if ( ! showValues ) { + return ''; + } + + if ( 'percentage' in point ) { + return `${ point.percentage }%`; + } else if ( 'value' in point ) { + return point.value.toString(); + } + + return ''; +} + +/** + * Creates a base legend item with common properties + * @param label + * @param value + * @param color + */ +function createBaseLegendItem( + label: string, + value: string, + color: string +): Omit< LegendItemWithGlyph, 'glyphSize' | 'renderGlyph' > { + return { + label, + value, + color, + }; +} + +/** + * Processes SeriesData into legend items + * @param seriesData + * @param theme + * @param showValues + * @param withGlyph + * @param glyphSize + * @param renderGlyph + */ +function processSeriesData( + seriesData: SeriesData[], + theme: ChartTheme, + showValues: boolean, + withGlyph: boolean, + glyphSize: number, + renderGlyph?: React.ComponentType< any > +): LegendItemWithGlyph[] | LegendItemWithoutGlyph[] { + const mapper = ( series: SeriesData, index: number ) => { + const baseItem = createBaseLegendItem( + series.label, + showValues ? series.data?.length?.toString() || '0' : '', + theme.colors[ index % theme.colors.length ] + ); + + if ( withGlyph && renderGlyph ) { + return { + ...baseItem, + glyphSize, + renderGlyph, + } as LegendItemWithGlyph; + } + + return baseItem as LegendItemWithoutGlyph; + }; + + return seriesData.map( mapper ) as LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; +} + +/** + * Processes point data into legend items + * @param pointData + * @param theme + * @param showValues + * @param withGlyph + * @param glyphSize + * @param renderGlyph + */ +function processPointData( + pointData: ( DataPointDate | DataPointPercentage )[], + theme: ChartTheme, + showValues: boolean, + withGlyph: boolean, + glyphSize: number, + renderGlyph?: React.ComponentType< any > +): LegendItemWithGlyph[] | LegendItemWithoutGlyph[] { + const mapper = ( point: DataPointDate | DataPointPercentage, index: number ) => { + const baseItem = createBaseLegendItem( + point.label, + formatPointValue( point, showValues ), + theme.colors[ index % theme.colors.length ] + ); + + if ( withGlyph && renderGlyph ) { + return { + ...baseItem, + glyphSize, + renderGlyph, + } as LegendItemWithGlyph; + } + + return baseItem as LegendItemWithoutGlyph; + }; + + return pointData.map( mapper ) as LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; +} + +/** + * Hook to transform chart data into legend items + * @param data + * @param theme + * @param options + */ +export function useChartLegendData< + T extends SeriesData[] | DataPointDate[] | DataPointPercentage[], +>( + data: T, + theme: ChartTheme, + options: ChartLegendOptions = {} +): LegendItemWithGlyph[] | LegendItemWithoutGlyph[] { + const { showValues = true, withGlyph = false, glyphSize = 8, renderGlyph } = options; + + return useMemo( () => { + if ( ! data || ! Array.isArray( data ) || data.length === 0 ) { + return []; + } + + // Handle SeriesData (multiple series with data points) + if ( 'data' in data[ 0 ] ) { + return processSeriesData( + data as SeriesData[], + theme, + showValues, + withGlyph, + glyphSize, + renderGlyph + ); + } + + // Handle DataPointDate or DataPointPercentage (single data points) + return processPointData( + data as ( DataPointDate | DataPointPercentage )[], + theme, + showValues, + withGlyph, + glyphSize, + renderGlyph + ); + }, [ data, theme, showValues, withGlyph, glyphSize, renderGlyph ] ); +} From 16b359316bee6dd89a607cc8bae48ef6f448d34c Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:06:17 +1000 Subject: [PATCH 14/62] Update Legend component infrastructure - Split LegendProps into BaseLegendProps and LegendProps - BaseLegendProps for direct usage (items required) - LegendProps for context usage (items optional, chartId added) - Export new Legend component alongside BaseLegend - Export useChartLegendData and ChartLegendOptions --- .../charts/src/components/legend/base-legend.tsx | 4 ++-- projects/js-packages/charts/src/components/legend/index.ts | 7 +++++-- projects/js-packages/charts/src/components/legend/types.ts | 7 ++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/projects/js-packages/charts/src/components/legend/base-legend.tsx b/projects/js-packages/charts/src/components/legend/base-legend.tsx index 422615d390c7c..eb87ed8c1e042 100644 --- a/projects/js-packages/charts/src/components/legend/base-legend.tsx +++ b/projects/js-packages/charts/src/components/legend/base-legend.tsx @@ -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, @@ -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, diff --git a/projects/js-packages/charts/src/components/legend/index.ts b/projects/js-packages/charts/src/components/legend/index.ts index a7609da9a205f..cc9e9d25c3abb 100644 --- a/projects/js-packages/charts/src/components/legend/index.ts +++ b/projects/js-packages/charts/src/components/legend/index.ts @@ -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'; diff --git a/projects/js-packages/charts/src/components/legend/types.ts b/projects/js-packages/charts/src/components/legend/types.ts index 4855926d248c9..7352ecd93c65e 100644 --- a/projects/js-packages/charts/src/components/legend/types.ts +++ b/projects/js-packages/charts/src/components/legend/types.ts @@ -24,9 +24,14 @@ export type LegendItemWithoutGlyph = BaseLegendItem & { glyphSize?: number; }; -export type LegendProps = Omit< LegendOrdinalProps, 'shapeStyle' > & { +export type BaseLegendProps = Omit< LegendOrdinalProps, 'shapeStyle' > & { items: LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; orientation?: 'horizontal' | 'vertical'; alignmentHorizontal?: 'left' | 'center' | 'right'; alignmentVertical?: 'top' | 'bottom'; }; + +export type LegendProps = Omit< BaseLegendProps, 'items' > & { + items?: LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; + chartId?: string; +}; From 6e8dc228cb404a71062dbc1c5379581da98f5997 Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:06:34 +1000 Subject: [PATCH 15/62] Export ChartContext for direct usage - Export ChartContext to enable direct useContext usage - Allows Legend component to handle missing provider gracefully - Enables flexible context usage patterns --- .../charts/src/providers/chart-context/chart-context.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx index 058296427c1c4..ba5c8075bcdf9 100644 --- a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx +++ b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx @@ -2,7 +2,7 @@ import { createContext, useContext, useCallback, useRef, useMemo } from 'react'; import type { ChartContextValue, ChartRegistration } from './types'; import type { FC, ReactNode } from 'react'; -const ChartContext = createContext< ChartContextValue | null >( null ); +export const ChartContext = createContext< ChartContextValue | null >( null ); export interface ChartProviderProps { children: ReactNode; From efbfd056f2dfd087e100782fed466d5a2ca116b1 Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:06:53 +1000 Subject: [PATCH 16/62] Replace duplicated validation with shared utilities - LineChart: Use validateSeriesData instead of custom validation - BarChart: Use validateBarChartData for label requirements - PieChart: Use validatePercentageData with 100% requirement - PieSemiCircleChart: Use validateSemiCircleData for >0% requirement - Update imports to use BaseLegend for internal chart usage - Eliminates ~60 lines of duplicated validation code --- .../src/components/bar-chart/bar-chart.tsx | 28 ++-------- .../src/components/line-chart/line-chart.tsx | 56 ++++++++++++------- .../src/components/pie-chart/pie-chart.tsx | 35 ++---------- .../pie-semi-circle-chart.tsx | 32 ++--------- 4 files changed, 50 insertions(+), 101 deletions(-) diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx index 4e607302933c7..64ebfb42109a6 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx @@ -4,8 +4,9 @@ import clsx from 'clsx'; import { useCallback, useId, useState, useRef } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useChartTheme, useXYChartTheme } from '../../providers/theme'; -import { useChartLegendData } from '../chart-legend/use-chart-legend-data'; -import { Legend } from '../legend'; +import { validateBarChartData } from '../../utils/validation'; +import { BaseLegend } 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'; @@ -23,25 +24,6 @@ export interface BarChartProps extends BaseChartProps< SeriesData[] > { withPatterns?: boolean; } -// Validation function similar to LineChart -const validateData = ( data: SeriesData[] ) => { - if ( ! data?.length ) return 'No data available'; - - const hasInvalidData = data.some( series => - series.data.some( - point => - isNaN( point.value as number ) || - point.value === null || - point.value === undefined || - ( ! point.label && - ( ! ( 'date' in point && point.date ) || isNaN( point.date.getTime() ) ) ) - ) - ); - - if ( hasInvalidData ) return 'Invalid data'; - return null; -}; - const getPatternId = ( chartId: string, index: number ) => `bar-pattern-${ chartId }-${ index }`; const BarChartInternal: FC< BarChartProps > = ( { @@ -224,7 +206,7 @@ const BarChartInternal: FC< BarChartProps > = ( { }, [ selectedIndex, data, chartId ] ); // Validate data first - const error = validateData( dataSorted ); + const error = validateBarChartData( dataSorted ); const isDataValid = ! error; // Register chart with context only if data is valid @@ -331,7 +313,7 @@ const BarChartInternal: FC< BarChartProps > = ( { { showLegend && ( - { } ); }; -const validateData = ( data: SeriesData[] ) => { - if ( ! data?.length ) return 'No data available'; - - const hasInvalidData = data.some( series => - series.data.some( - ( point: DataPointDate | DataPoint ) => - isNaN( point.value as number ) || - point.value === null || - point.value === undefined || - ( 'date' in point && point.date && isNaN( point.date.getTime() ) ) - ) - ); - - if ( hasInvalidData ) return 'Invalid data'; +const HighlightTooltip: React.FC< { + series: SeriesData[]; + selectedIndex: number | undefined; +} > = ( { series, selectedIndex } ) => { + const tooltipContext = useContext( TooltipContext ); + + useEffect( () => { + if ( ! series ) return; + + if ( selectedIndex === undefined ) { + tooltipContext?.hideTooltip(); + return; + } + + series.forEach( ( s, index ) => { + if ( selectedIndex < s.data.length ) { + const datum = s.data[ selectedIndex ]; + + tooltipContext?.showTooltip( { + datum, + key: s.label, + index, + } ); + } + } ); + + // Don't include tooltipContext in the dependency array to avoid loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ selectedIndex, series ] ); return null; }; @@ -270,7 +286,7 @@ const LineChartInternal: FC< LineChartProps > = ( { const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme ); - const error = validateData( dataSorted ); + const error = validateSeriesData( dataSorted ); const isDataValid = ! error; // Create legend items using the reusable hook @@ -426,7 +442,7 @@ const LineChartInternal: FC< LineChartProps > = ( { { showLegend && ( - { - if ( ! data.length ) { - return { isValid: false, message: 'No data available' }; - } - - // Check for negative values - const hasNegativeValues = data.some( item => item.percentage < 0 || item.value < 0 ); - if ( hasNegativeValues ) { - return { isValid: false, message: 'Invalid data: Negative values are not allowed' }; - } - - // Validate total percentage - const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 ); - if ( Math.abs( totalPercentage - 100 ) > 0.01 ) { - // Using small epsilon for floating point comparison - return { isValid: false, message: 'Invalid percentage total: Must equal 100' }; - } - - return { isValid: true, message: '' }; -}; - /** * Renders a pie or donut chart using the provided data. * @@ -113,7 +88,7 @@ const PieChartInternal = ( { showValues: true, } ); - const { isValid, message } = validateData( data ); + const { isValid, message } = validatePercentageData( data, true ); // Register chart with context only if data is valid useChartRegistration( chartId, legendItems, providerTheme, 'pie', isValid, { @@ -233,7 +208,7 @@ const PieChartInternal = ( { { showLegend && ( - ; -/** - * Validates the semi-circle pie chart data - * @param data - The data to validate - * @return Object containing validation result and error message - */ -const validateData = ( data: DataPointPercentage[] ) => { - if ( ! data.length ) { - return { isValid: false, message: 'No data available' }; - } - - // Check for negative values - const hasNegativeValues = data.some( item => item.percentage < 0 || item.value < 0 ); - if ( hasNegativeValues ) { - return { isValid: false, message: 'Invalid data: Negative values are not allowed' }; - } - - // Validate total percentage is greater than 0 - const totalPercentage = data.reduce( ( sum, item ) => sum + item.percentage, 0 ); - if ( totalPercentage <= 0 ) { - return { isValid: false, message: 'Invalid percentage total: Must be greater than 0' }; - } - - return { isValid: true, message: '' }; -}; - const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { data, chartId: providedChartId, @@ -119,7 +95,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { ); // Validate data first to get validation result - const { isValid, message } = validateData( data ); + const { isValid, message } = validateSemiCircleData( data ); // Define accessors with useMemo to avoid changing dependencies const accessors = useMemo( @@ -274,7 +250,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { ) } { showLegend && ( - Date: Mon, 14 Jul 2025 18:13:53 +1000 Subject: [PATCH 17/62] Remove separate ChartLegend component - Eliminates technical debt from duplicate component - Consolidates legend functionality into enhanced Legend component - Maintains clean architecture with single legend implementation --- .../chart-legend/chart-legend.test.tsx | 245 ---------------- .../components/chart-legend/chart-legend.tsx | 20 -- .../src/components/chart-legend/index.ts | 3 - .../chart-legend/stories/index.stories.tsx | 265 ------------------ .../src/components/chart-legend/types.ts | 15 - .../chart-legend/use-chart-legend-data.ts | 85 ------ 6 files changed, 633 deletions(-) delete mode 100644 projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx delete mode 100644 projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx delete mode 100644 projects/js-packages/charts/src/components/chart-legend/index.ts delete mode 100644 projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx delete mode 100644 projects/js-packages/charts/src/components/chart-legend/types.ts delete mode 100644 projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts diff --git a/projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx b/projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx deleted file mode 100644 index 2e7d36723dadb..0000000000000 --- a/projects/js-packages/charts/src/components/chart-legend/chart-legend.test.tsx +++ /dev/null @@ -1,245 +0,0 @@ -import { render, screen, renderHook } from '@testing-library/react'; -import { defaultTheme } from '../../providers/theme'; -import { ChartLegend } from './chart-legend'; -import { useChartLegendData } from './use-chart-legend-data'; -import type { SeriesData, DataPointPercentage } from '../../types'; - -// Mock the BaseLegend component -jest.mock( '../legend/base-legend', () => ( { - BaseLegend: ( { - items, - orientation, - testId, - }: { - items: Array< { label: string; value: string; color: string } >; - orientation: string; - testId?: string; - } ) => ( -
-
{ orientation }
- { items.map( ( item: { label: string; value: string; color: string }, index: number ) => ( -
- { item.label } - { item.value } - { item.color } -
- ) ) } -
- ), -} ) ); - -describe( 'ChartLegend', () => { - const mockItems = [ - { label: 'Desktop', value: '65%', color: '#3858E9' }, - { label: 'Mobile', value: '35%', color: '#80C8FF' }, - ]; - - it( 'renders legend items correctly', () => { - render( ); - - expect( screen.getByTestId( 'base-legend' ) ).toBeInTheDocument(); - expect( screen.getByTestId( 'legend-orientation' ) ).toHaveTextContent( 'horizontal' ); - expect( screen.getByTestId( 'legend-item-0' ) ).toBeInTheDocument(); - expect( screen.getByTestId( 'legend-item-1' ) ).toBeInTheDocument(); - } ); - - it( 'passes through props to BaseLegend', () => { - render( - - ); - - expect( screen.getByTestId( 'legend-orientation' ) ).toHaveTextContent( 'vertical' ); - } ); - - it( 'accepts chartId prop for future context integration', () => { - // This test ensures the prop is accepted without error - expect( () => { - render( ); - } ).not.toThrow(); - } ); - - it( 'renders correct legend labels and values', () => { - render( ); - - const labels = screen.getAllByTestId( 'legend-label' ); - const values = screen.getAllByTestId( 'legend-value' ); - const colors = screen.getAllByTestId( 'legend-color' ); - - expect( labels[ 0 ] ).toHaveTextContent( 'Desktop' ); - expect( labels[ 1 ] ).toHaveTextContent( 'Mobile' ); - expect( values[ 0 ] ).toHaveTextContent( '65%' ); - expect( values[ 1 ] ).toHaveTextContent( '35%' ); - expect( colors[ 0 ] ).toHaveTextContent( '#3858E9' ); - expect( colors[ 1 ] ).toHaveTextContent( '#80C8FF' ); - } ); -} ); - -describe( 'useChartLegendData', () => { - const mockTheme = { - ...defaultTheme, - colors: [ '#3858E9', '#80C8FF', '#44B556' ], - }; - - describe( 'with SeriesData[]', () => { - const seriesData: SeriesData[] = [ - { - label: 'Desktop', - data: [ - { date: new Date( '2023-01-01' ), value: 100 }, - { date: new Date( '2023-01-02' ), value: 150 }, - ], - }, - { - label: 'Mobile', - data: [ - { date: new Date( '2023-01-01' ), value: 80 }, - { date: new Date( '2023-01-02' ), value: 90 }, - ], - options: { - stroke: '#FF0000', - legendShapeStyle: { borderRadius: '4px' }, - }, - }, - ]; - - it( 'converts SeriesData to BaseLegendItem format', () => { - const { result } = renderHook( () => useChartLegendData( seriesData, mockTheme ) ); - - expect( result.current ).toEqual( [ - { - label: 'Desktop', - value: '', - color: '#3858E9', - shapeStyle: undefined, - renderGlyph: undefined, - glyphSize: 4, - }, - { - label: 'Mobile', - value: '', - color: '#FF0000', - shapeStyle: { borderRadius: '4px' }, - renderGlyph: undefined, - glyphSize: 4, - }, - ] ); - } ); - - it( 'shows values when showValues option is true', () => { - const { result } = renderHook( () => - useChartLegendData( seriesData, mockTheme, { showValues: true } ) - ); - - expect( result.current[ 0 ].value ).toBe( '' ); - expect( result.current[ 1 ].value ).toBe( '' ); - } ); - - it( 'includes glyph when withGlyph option is true', () => { - const mockRenderGlyph = jest.fn(); - const themeWithGlyph = { - ...mockTheme, - glyphs: [ mockRenderGlyph ], - }; - - const { result } = renderHook( () => - useChartLegendData( seriesData, themeWithGlyph, { withGlyph: true, glyphSize: 8 } ) - ); - - expect( result.current[ 0 ].renderGlyph ).toBe( mockRenderGlyph ); - expect( result.current[ 0 ].glyphSize ).toBe( 8 ); - } ); - } ); - - describe( 'with DataPointPercentage[]', () => { - const pieData: DataPointPercentage[] = [ - { label: 'Desktop', value: 65, percentage: 65 }, - { label: 'Mobile', value: 35, percentage: 35 }, - ]; - - it( 'converts DataPointPercentage to BaseLegendItem format', () => { - const { result } = renderHook( () => useChartLegendData( pieData, mockTheme ) ); - - expect( result.current ).toEqual( [ - { - label: 'Desktop', - value: '', - color: '#3858E9', - shapeStyle: undefined, - renderGlyph: undefined, - glyphSize: 4, - }, - { - label: 'Mobile', - value: '', - color: '#80C8FF', - shapeStyle: undefined, - renderGlyph: undefined, - glyphSize: 4, - }, - ] ); - } ); - - it( 'shows values when showValues option is true', () => { - const { result } = renderHook( () => - useChartLegendData( pieData, mockTheme, { showValues: true } ) - ); - - expect( result.current[ 0 ].value ).toBe( '65' ); - expect( result.current[ 1 ].value ).toBe( '35' ); - } ); - - it( 'cycles through theme colors correctly', () => { - const largeDataSet: DataPointPercentage[] = [ - { label: 'A', value: 25, percentage: 25 }, - { label: 'B', value: 25, percentage: 25 }, - { label: 'C', value: 25, percentage: 25 }, - { label: 'D', value: 25, percentage: 25 }, - ]; - - const { result } = renderHook( () => useChartLegendData( largeDataSet, mockTheme ) ); - - expect( result.current[ 0 ].color ).toBe( '#3858E9' ); - expect( result.current[ 1 ].color ).toBe( '#80C8FF' ); - expect( result.current[ 2 ].color ).toBe( '#44B556' ); - expect( result.current[ 3 ].color ).toBe( '#3858E9' ); // Cycles back to first color - } ); - } ); - - it( 'handles empty data arrays', () => { - const { result } = renderHook( () => useChartLegendData( [], mockTheme ) ); - - expect( result.current ).toEqual( [] ); - } ); - - it( 'memoizes results correctly', () => { - const testData = [ { label: 'Test', value: 100, percentage: 100 } ] as DataPointPercentage[]; - const testOptions = { showValues: true }; - - const { result, rerender } = renderHook( - ( { data, theme, options } ) => useChartLegendData( data, theme, options ), - { - initialProps: { - data: testData, - theme: mockTheme, - options: testOptions, - }, - } - ); - - const firstResult = result.current; - - // Re-render with same props - should return same reference - rerender( { - data: testData, - theme: mockTheme, - options: testOptions, - } ); - - expect( result.current ).toBe( firstResult ); - } ); -} ); diff --git a/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx b/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx deleted file mode 100644 index 11c2855d19f7e..0000000000000 --- a/projects/js-packages/charts/src/components/chart-legend/chart-legend.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useChartContext } from '../../providers/chart-context'; -import { BaseLegend } from '../legend/base-legend'; -import type { ChartLegendProps } from './types'; -import type { FC } from 'react'; - -export const ChartLegend: FC< ChartLegendProps > = ( { chartId, items, ...props } ) => { - const { getChartData } = useChartContext(); - - // If chartId is provided, get items from chart context - const contextItems = chartId ? getChartData( chartId )?.legendItems : undefined; - - // Use context items if available, otherwise fall back to provided items - const legendItems = ( contextItems || items ) as typeof items; - - if ( ! legendItems ) { - return null; - } - - return ; -}; diff --git a/projects/js-packages/charts/src/components/chart-legend/index.ts b/projects/js-packages/charts/src/components/chart-legend/index.ts deleted file mode 100644 index 9016c3104f4ca..0000000000000 --- a/projects/js-packages/charts/src/components/chart-legend/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ChartLegend } from './chart-legend'; -export { useChartLegendData } from './use-chart-legend-data'; -export type { ChartLegendProps, ChartLegendOptions } from './types'; diff --git a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx deleted file mode 100644 index b90f6f9a23f69..0000000000000 --- a/projects/js-packages/charts/src/components/chart-legend/stories/index.stories.tsx +++ /dev/null @@ -1,265 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { useChartTheme } from '../../../providers/theme'; -import { BarChart } from '../../bar-chart'; -import { LineChart } from '../../line-chart'; -import { PieChart } from '../../pie-chart'; -import { ChartLegend } from '../chart-legend'; -import { useChartLegendData } from '../use-chart-legend-data'; -import type { SeriesData, DataPointPercentage } from '../../../types'; - -const meta: Meta< typeof ChartLegend > = { - title: 'JS Packages/Charts/Composites/ChartLegend', - component: ChartLegend, - parameters: { - layout: 'centered', - docs: { - description: { - component: - 'A standalone legend component that can be used independently from charts or connected to chart data.', - }, - }, - }, -}; - -export default meta; -type Story = StoryObj< typeof ChartLegend >; - -// Mock data for different chart types -const lineChartData: SeriesData[] = [ - { - label: 'Desktop', - data: [ - { date: new Date( '2023-01-01' ), value: 100 }, - { date: new Date( '2023-01-02' ), value: 150 }, - { date: new Date( '2023-01-03' ), value: 120 }, - ], - }, - { - label: 'Mobile', - data: [ - { date: new Date( '2023-01-01' ), value: 80 }, - { date: new Date( '2023-01-02' ), value: 90 }, - { date: new Date( '2023-01-03' ), value: 110 }, - ], - }, -]; - -const barChartData: SeriesData[] = [ - { - label: 'Q1 Sales', - data: [ - { label: 'Jan', value: 1000 }, - { label: 'Feb', value: 1200 }, - { label: 'Mar', value: 1100 }, - ], - }, - { - label: 'Q2 Sales', - data: [ - { label: 'Jan', value: 800 }, - { label: 'Feb', value: 900 }, - { label: 'Mar', value: 1000 }, - ], - }, -]; - -const pieChartData: DataPointPercentage[] = [ - { label: 'Desktop', value: 65, percentage: 65 }, - { label: 'Mobile', value: 35, percentage: 35 }, -]; - -// Story that demonstrates the standalone component with manual legend items -export const Standalone: Story = { - args: { - items: [ - { label: 'Desktop', value: '65%', color: '#3858E9' }, - { label: 'Mobile', value: '35%', color: '#80C8FF' }, - ], - orientation: 'horizontal', - }, -}; - -// Story showing vertical orientation -export const Vertical: Story = { - args: { - items: [ - { label: 'Desktop', value: '65%', color: '#3858E9' }, - { label: 'Mobile', value: '35%', color: '#80C8FF' }, - { label: 'Tablet', value: '12%', color: '#44B556' }, - ], - orientation: 'vertical', - }, -}; - -// Story showing use with LineChart data -const WithLineChartData = () => { - const theme = useChartTheme(); - const legendItems = useChartLegendData( lineChartData, theme, { - showValues: false, - } ); - - return ( -
- - -
- ); -}; - -export const WithLineChart: Story = { - render: () => , - parameters: { - docs: { - description: { - story: 'ChartLegend used with LineChart data, positioned independently below the chart.', - }, - }, - }, -}; - -// Story showing use with BarChart data -const WithBarChartData = () => { - const theme = useChartTheme(); - const legendItems = useChartLegendData( barChartData, theme ); - - return ( -
- - -
- ); -}; - -export const WithBarChart: Story = { - render: () => , - parameters: { - docs: { - description: { - story: 'ChartLegend used with BarChart data, positioned vertically beside the chart.', - }, - }, - }, -}; - -// Story showing use with PieChart data -const WithPieChartData = () => { - const theme = useChartTheme(); - const legendItems = useChartLegendData( pieChartData, theme, { - showValues: true, - } ); - - return ( -
- - -
- ); -}; - -export const WithPieChart: Story = { - render: () => , - parameters: { - docs: { - description: { - story: 'ChartLegend used with PieChart data, showing values in the legend.', - }, - }, - }, -}; - -// Story showing multiple charts with shared legend -const MultipleChartsSharedLegendComponent = () => { - const theme = useChartTheme(); - const legendItems = useChartLegendData( lineChartData, theme ); - - return ( -
-
- - -
- -
- ); -}; - -export const MultipleChartsSharedLegend: Story = { - render: () => , - parameters: { - docs: { - description: { - story: 'Single ChartLegend component used for multiple charts with shared data series.', - }, - }, - }, -}; - -// Story showing different alignment options -export const AlignmentOptions: Story = { - args: { - items: [ - { label: 'Series 1', value: '25%', color: '#3858E9' }, - { label: 'Series 2', value: '35%', color: '#80C8FF' }, - { label: 'Series 3', value: '40%', color: '#44B556' }, - ], - orientation: 'horizontal', - alignmentHorizontal: 'left', - alignmentVertical: 'top', - }, - parameters: { - docs: { - description: { - story: 'ChartLegend with custom alignment options.', - }, - }, - }, -}; - -// Story showing the legend with custom shapes -export const CustomShape: Story = { - args: { - items: [ - { label: 'Desktop', value: '65%', color: '#3858E9' }, - { label: 'Mobile', value: '35%', color: '#80C8FF' }, - ], - orientation: 'horizontal', - shape: 'circle', - }, - parameters: { - docs: { - description: { - story: 'ChartLegend with circle shape instead of default rectangle.', - }, - }, - }, -}; - -// Story showing standalone legend using chartId to automatically get data from context -const StandaloneLegendWithChartIdComponent = () => { - return ( -
- { /* Chart with legend hidden but still registering data */ } - - { /* Standalone legend that automatically gets data from chart context */ } - -
- ); -}; - -export const StandaloneLegendWithChartId: Story = { - render: () => , - parameters: { - docs: { - description: { - story: - 'ChartLegend component automatically retrieving legend data from chart context using chartId. The chart has showLegend=false but the standalone legend still works.', - }, - }, - }, -}; diff --git a/projects/js-packages/charts/src/components/chart-legend/types.ts b/projects/js-packages/charts/src/components/chart-legend/types.ts deleted file mode 100644 index e7ea779b4b526..0000000000000 --- a/projects/js-packages/charts/src/components/chart-legend/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { LegendProps, LegendItemWithGlyph, LegendItemWithoutGlyph } from '../legend/types'; -import type { GlyphProps } from '@visx/xychart'; -import type { ReactNode } from 'react'; - -export interface ChartLegendOptions { - showValues?: boolean; - withGlyph?: boolean; - glyphSize?: number; - renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode; -} - -export interface ChartLegendProps extends Omit< LegendProps, 'items' > { - items?: LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; - chartId?: string; -} diff --git a/projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts b/projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts deleted file mode 100644 index f63b166d41660..0000000000000 --- a/projects/js-packages/charts/src/components/chart-legend/use-chart-legend-data.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useMemo } from 'react'; -import type { ChartLegendOptions } from './types'; -import type { SeriesData, ChartTheme, DataPoint, DataPointPercentage } from '../../types'; -import type { LegendItemWithGlyph, LegendItemWithoutGlyph } from '../legend/types'; - -// Overload for SeriesData[] -export function useChartLegendData( - data: SeriesData[], - theme: ChartTheme, - options?: ChartLegendOptions -): LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; - -// Overload for DataPoint[] or DataPointPercentage[] -export function useChartLegendData( - data: DataPoint[] | DataPointPercentage[], - theme: ChartTheme, - options?: ChartLegendOptions -): LegendItemWithGlyph[] | LegendItemWithoutGlyph[]; - -/** - * Converts chart data to legend items with proper typing based on glyph usage - * @param data - Chart data (SeriesData[], DataPoint[], or DataPointPercentage[]) - * @param theme - Chart theme containing colors and optional glyphs - * @param options - Configuration options for legend item generation - * @return Array of legend items with proper typing for glyph usage - */ -export function useChartLegendData( - data: SeriesData[] | DataPoint[] | DataPointPercentage[], - theme: ChartTheme, - options: ChartLegendOptions = {} -): LegendItemWithGlyph[] | LegendItemWithoutGlyph[] { - return useMemo( () => { - // Check if it's SeriesData[] by checking if first item has a data property - const isSeriesData = data.length > 0 && 'data' in data[ 0 ]; - - // Check if any item will have a glyph to determine return type - const hasGlyphs = options.withGlyph && ( theme.glyphs || options.renderGlyph ); - - if ( isSeriesData ) { - const seriesData = data as SeriesData[]; - if ( hasGlyphs ) { - return seriesData.map( ( group, index ) => ( { - label: group.label, - value: options.showValues ? '' : '', // SeriesData doesn't have a value property - color: group.options?.stroke ?? theme.colors[ index % theme.colors.length ], - shapeStyle: group.options?.legendShapeStyle, - renderGlyph: theme.glyphs?.[ index ] ?? options.renderGlyph!, - glyphSize: options.glyphSize ?? 4, - } ) ) as LegendItemWithGlyph[]; - } - return seriesData.map( ( group, index ) => ( { - label: group.label, - value: options.showValues ? '' : '', // SeriesData doesn't have a value property - color: group.options?.stroke ?? theme.colors[ index % theme.colors.length ], - shapeStyle: group.options?.legendShapeStyle, - glyphSize: options.glyphSize ?? 4, - } ) ) as LegendItemWithoutGlyph[]; - } - // Handle DataPoint[] or DataPointPercentage[] - const pointData = data as DataPoint[] | DataPointPercentage[]; - if ( hasGlyphs ) { - return pointData.map( ( item, index ) => ( { - label: item.label, - value: options.showValues - ? ( 'valueDisplay' in item ? item.valueDisplay : item.value.toString() ) || - item.value.toString() - : '', - color: theme.colors[ index % theme.colors.length ], - shapeStyle: undefined, - renderGlyph: theme.glyphs?.[ index ] ?? options.renderGlyph!, - glyphSize: options.glyphSize ?? 4, - } ) ) as LegendItemWithGlyph[]; - } - return pointData.map( ( item, index ) => ( { - label: item.label, - value: options.showValues - ? ( 'valueDisplay' in item ? item.valueDisplay : item.value.toString() ) || - item.value.toString() - : '', - color: theme.colors[ index % theme.colors.length ], - shapeStyle: undefined, - glyphSize: options.glyphSize ?? 4, - } ) ) as LegendItemWithoutGlyph[]; - }, [ data, theme, options ] ); -} From 0541cdeb5f759ace5e53328722b2c10a00cb70bb Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:14:08 +1000 Subject: [PATCH 18/62] Update main exports for consolidated legend system - Export enhanced Legend and BaseLegend components - Export useChartLegendData from legend module - Export LegendProps, BaseLegendProps, ChartLegendOptions types - Remove ChartLegend references in favor of unified Legend --- projects/js-packages/charts/src/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/charts/src/index.ts b/projects/js-packages/charts/src/index.ts index bcb78d7a3f780..e7a8aa6df61b7 100644 --- a/projects/js-packages/charts/src/index.ts +++ b/projects/js-packages/charts/src/index.ts @@ -7,8 +7,7 @@ export { BarListChart } from './components/bar-list-chart'; // Chart components export { BaseTooltip } from './components/tooltip'; -export { Legend } from './components/legend'; -export { ChartLegend, useChartLegendData } from './components/chart-legend'; +export { Legend, BaseLegend, useChartLegendData } from './components/legend'; // Themes export { ThemeProvider } from './providers/theme'; @@ -34,4 +33,4 @@ export type { export type { LineStyles, GridStyles } from '@visx/xychart'; export type { RenderLineStartGlyphProps } from './components/line-chart/line-chart'; -export type { ChartLegendProps, ChartLegendOptions } from './components/chart-legend'; +export type { LegendProps, BaseLegendProps, ChartLegendOptions } from './components/legend'; From 53b138df82371de17d12ccf27175eaed30b6365b Mon Sep 17 00:00:00 2001 From: annacmc Date: Mon, 14 Jul 2025 18:14:51 +1000 Subject: [PATCH 19/62] Add comprehensive Storybook documentation for Legend - Replace basic legend stories with enhanced documentation - Add context integration examples with chartId usage - Add real-world dashboard layout example - Document standalone legend capabilities - Include code examples and usage patterns - Demonstrate both manual and automatic legend data --- .../legend/stories/index.stories.tsx | 449 +++++++++++++++++- 1 file changed, 434 insertions(+), 15 deletions(-) diff --git a/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx index 50b9f6c79eb37..f441b478edb74 100644 --- a/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx @@ -1,49 +1,468 @@ import { Meta, StoryObj } from '@storybook/react'; -import { BaseLegend } from '../base-legend'; +import { ChartProvider } from '../../../providers/chart-context'; +import { useChartTheme } from '../../../providers/theme'; +import { BarChart } from '../../bar-chart'; +import { LineChart } from '../../line-chart'; +import { PieChart } from '../../pie-chart'; +import { Legend } from '../legend'; +import { useChartLegendData } from '../use-chart-legend-data'; +import type { SeriesData, DataPointPercentage } from '../../../types'; -const meta: Meta< typeof BaseLegend > = { +const meta: Meta< typeof Legend > = { title: 'JS Packages/Charts/Composites/Legend', - component: BaseLegend, + component: Legend, parameters: { layout: 'centered', docs: { description: { - component: - 'A flexible legend component that can be customized with different styles and orientations.', + component: ` +The Legend component provides a flexible way to display chart legends either as standalone components or integrated with charts through the chart context. + +## Key Features + +- **Standalone Usage**: Display legends independently from charts +- **Context Integration**: Automatically retrieve legend data from charts using \`chartId\` +- **Flexible Positioning**: Place legends anywhere in your layout +- **Works with Hidden Legends**: Charts with \`showLegend={false}\` still provide data to standalone legends +- **Full Customization**: Inherits all props from BaseLegend for complete control + +## Usage Examples + +### Basic Usage with Manual Data +\`\`\`jsx + +\`\`\` + +### Automatic Data from Chart Context +\`\`\`jsx +// Chart registers its legend data with chartId +
); diff --git a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx index 7bd7af68f518b..86f79583029fe 100644 --- a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx +++ b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx @@ -6,7 +6,6 @@ import type { ChartTheme, SeriesData, DataPointPercentage } from '../../types'; const mockTheme: ChartTheme = { colors: [ '#ff0000', '#00ff00', '#0000ff' ], backgroundColor: '#ffffff', - textColor: '#000000', gridColor: '#e0e0e0', legendLabelStyles: {}, seriesLineStyles: [], From 2b9e9e99d5b602086654f749a97352117ec52802 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 16 Jul 2025 08:58:11 +1000 Subject: [PATCH 37/62] add missing tickLength and gridColorDark properties to the ChartTheme mock in the test file --- .../charts/src/components/legend/use-chart-legend-data.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx index 86f79583029fe..a3609a72a2ddf 100644 --- a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx +++ b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx @@ -7,6 +7,8 @@ const mockTheme: ChartTheme = { colors: [ '#ff0000', '#00ff00', '#0000ff' ], backgroundColor: '#ffffff', gridColor: '#e0e0e0', + gridColorDark: '#666666', + tickLength: 5, legendLabelStyles: {}, seriesLineStyles: [], glyphs: [], From cd05c7b615204e0878dd49fb741d72bfc6687e40 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 16 Jul 2025 10:57:03 +1000 Subject: [PATCH 38/62] Revert "add missing tickLength and" This reverts commit 2b9e9e99d5b602086654f749a97352117ec52802. --- .../charts/src/components/legend/use-chart-legend-data.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx index a3609a72a2ddf..86f79583029fe 100644 --- a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx +++ b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx @@ -7,8 +7,6 @@ const mockTheme: ChartTheme = { colors: [ '#ff0000', '#00ff00', '#0000ff' ], backgroundColor: '#ffffff', gridColor: '#e0e0e0', - gridColorDark: '#666666', - tickLength: 5, legendLabelStyles: {}, seriesLineStyles: [], glyphs: [], From 1d3463e04be491c52ea7391bf8a56d3d486f3e27 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 16 Jul 2025 11:30:19 +1000 Subject: [PATCH 39/62] Remove test additions to reduce PR size - Remove use-chart-legend-data.test.tsx (271 lines) - Revert legend.test.tsx to original 9 tests (from 17 tests) - Tests will be added in follow-up branch add/charts-50-tests This makes the main PR focus on core functionality for easier review. --- .../src/components/legend/legend.test.tsx | 124 ------------------ 1 file changed, 124 deletions(-) diff --git a/projects/js-packages/charts/src/components/legend/legend.test.tsx b/projects/js-packages/charts/src/components/legend/legend.test.tsx index 0991737839064..9f18343f6870d 100644 --- a/projects/js-packages/charts/src/components/legend/legend.test.tsx +++ b/projects/js-packages/charts/src/components/legend/legend.test.tsx @@ -1,9 +1,6 @@ import { render, screen } from '@testing-library/react'; -import { ChartContext } from '../../providers/chart-context/chart-context'; import { BaseLegend } from './base-legend'; -import { Legend } from './legend'; import type { LegendProps } from './types'; -import type { ChartContextValue } from '../../providers/chart-context/types'; const TestShape: LegendProps[ 'shape' ] = props => { return ( @@ -86,124 +83,3 @@ describe( 'BaseLegend', () => { expect( screen.getByText( 'Very Long Label That Should Still Display' ) ).toBeInTheDocument(); } ); } ); - -describe( 'Legend', () => { - const defaultItems = [ - { label: 'Series 1', value: '60%', color: '#ff0000' }, - { label: 'Series 2', value: '40%', color: '#00ff00' }, - ]; - - const mockContextValue: ChartContextValue = { - charts: new Map(), - registerChart: jest.fn(), - unregisterChart: jest.fn(), - getChartData: jest.fn(), - }; - - beforeEach( () => { - jest.clearAllMocks(); - mockContextValue.charts.clear(); - } ); - - test( 'renders with direct items prop', () => { - render( ); - expect( screen.getByText( 'Series 1' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Series 2' ) ).toBeInTheDocument(); - expect( screen.getByText( '60%' ) ).toBeInTheDocument(); - expect( screen.getByText( '40%' ) ).toBeInTheDocument(); - } ); - - test( 'renders with chartId and context data', () => { - const contextItems = [ - { label: 'Context Item 1', value: '70%', color: '#0000ff' }, - { label: 'Context Item 2', value: '30%', color: '#ffff00' }, - ]; - - ( mockContextValue.getChartData as jest.Mock ).mockReturnValue( { - legendItems: contextItems, - } ); - - render( - - - - ); - - expect( screen.getByText( 'Context Item 1' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Context Item 2' ) ).toBeInTheDocument(); - expect( screen.getByText( '70%' ) ).toBeInTheDocument(); - expect( screen.getByText( '30%' ) ).toBeInTheDocument(); - expect( mockContextValue.getChartData ).toHaveBeenCalledWith( 'test-chart' ); - } ); - - test( 'falls back to items prop when chartId provided but no context data', () => { - ( mockContextValue.getChartData as jest.Mock ).mockReturnValue( undefined ); - - render( - - - - ); - - expect( screen.getByText( 'Series 1' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Series 2' ) ).toBeInTheDocument(); - expect( mockContextValue.getChartData ).toHaveBeenCalledWith( 'missing-chart' ); - } ); - - test( 'falls back to items prop when chartId provided but no context', () => { - render( ); - - expect( screen.getByText( 'Series 1' ) ).toBeInTheDocument(); - expect( screen.getByText( 'Series 2' ) ).toBeInTheDocument(); - } ); - - test( 'returns null when no items provided and no context data', () => { - ( mockContextValue.getChartData as jest.Mock ).mockReturnValue( undefined ); - - const { container } = render( - - - - ); - - expect( container ).toBeEmptyDOMElement(); - } ); - - test( 'returns null when no items and no chartId provided', () => { - const { container } = render( ); - - expect( container ).toBeEmptyDOMElement(); - } ); - - test( 'passes through other props to BaseLegend', () => { - render( - - ); - - expect( screen.getByTestId( 'legend-vertical' ) ).toBeInTheDocument(); - expect( screen.getByRole( 'list' ) ).toHaveClass( 'custom-legend-class' ); - } ); - - test( 'prioritizes context items over direct items when both are available', () => { - const contextItems = [ { label: 'Priority Item', value: '100%', color: '#purple' } ]; - - ( mockContextValue.getChartData as jest.Mock ).mockReturnValue( { - legendItems: contextItems, - } ); - - render( - - - - ); - - expect( screen.getByText( 'Priority Item' ) ).toBeInTheDocument(); - expect( screen.queryByText( 'Series 1' ) ).not.toBeInTheDocument(); - expect( screen.queryByText( 'Series 2' ) ).not.toBeInTheDocument(); - } ); -} ); From 291e3436ddda0413dd00b1f4098450fabaf28944 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 16 Jul 2025 12:21:55 +1000 Subject: [PATCH 40/62] Complete test file removal for PR size reduction --- .../legend/use-chart-legend-data.test.tsx | 269 ------------------ 1 file changed, 269 deletions(-) delete mode 100644 projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx diff --git a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx b/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx deleted file mode 100644 index 86f79583029fe..0000000000000 --- a/projects/js-packages/charts/src/components/legend/use-chart-legend-data.test.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { renderHook } from '@testing-library/react'; -import { useChartLegendData } from './use-chart-legend-data'; -import type { ChartTheme, SeriesData, DataPointPercentage } from '../../types'; - -// Mock theme data -const mockTheme: ChartTheme = { - colors: [ '#ff0000', '#00ff00', '#0000ff' ], - backgroundColor: '#ffffff', - gridColor: '#e0e0e0', - legendLabelStyles: {}, - seriesLineStyles: [], - glyphs: [], -}; - -// Mock series data -const mockSeriesData: SeriesData[] = [ - { - label: 'Series 1', - data: [ - { date: new Date( '2023-01-01' ), value: 100 }, - { date: new Date( '2023-01-02' ), value: 150 }, - ], - }, - { - label: 'Series 2', - data: [ - { date: new Date( '2023-01-01' ), value: 80 }, - { date: new Date( '2023-01-02' ), value: 90 }, - ], - }, -]; - -// Mock percentage data -const mockPercentageData: DataPointPercentage[] = [ - { label: 'Desktop', percentage: 65, value: 650, color: '#ff0000' }, - { label: 'Mobile', percentage: 35, value: 350, color: '#00ff00' }, -]; - -describe( 'useChartLegendData', () => { - describe( 'SeriesData handling', () => { - test( 'returns legend items without values by default', () => { - const { result } = renderHook( () => useChartLegendData( mockSeriesData, mockTheme ) ); - - expect( result.current ).toEqual( [ - { label: 'Series 1', value: '', color: '#ff0000' }, - { label: 'Series 2', value: '', color: '#00ff00' }, - ] ); - } ); - - test( 'returns legend items with data count when showValues is true', () => { - const { result } = renderHook( () => - useChartLegendData( mockSeriesData, mockTheme, { showValues: true } ) - ); - - expect( result.current ).toEqual( [ - { label: 'Series 1', value: '2', color: '#ff0000' }, - { label: 'Series 2', value: '2', color: '#00ff00' }, - ] ); - } ); - - test( 'handles empty series data', () => { - const emptySeries: SeriesData[] = [ { label: 'Empty Series', data: [] } ]; - - const { result } = renderHook( () => - useChartLegendData( emptySeries, mockTheme, { showValues: true } ) - ); - - expect( result.current ).toEqual( [ - { label: 'Empty Series', value: '0', color: '#ff0000' }, - ] ); - } ); - - test( 'cycles through theme colors for multiple series', () => { - const manySeries: SeriesData[] = [ - { label: 'Series 1', data: [ { date: new Date(), value: 1 } ] }, - { label: 'Series 2', data: [ { date: new Date(), value: 2 } ] }, - { label: 'Series 3', data: [ { date: new Date(), value: 3 } ] }, - { label: 'Series 4', data: [ { date: new Date(), value: 4 } ] }, - ]; - - const { result } = renderHook( () => useChartLegendData( manySeries, mockTheme ) ); - - expect( result.current ).toEqual( [ - { label: 'Series 1', value: '', color: '#ff0000' }, // colors[0] - { label: 'Series 2', value: '', color: '#00ff00' }, // colors[1] - { label: 'Series 3', value: '', color: '#0000ff' }, // colors[2] - { label: 'Series 4', value: '', color: '#ff0000' }, // colors[0] (cycles back) - ] ); - } ); - } ); - - describe( 'DataPointPercentage handling', () => { - test( 'returns legend items without values by default', () => { - const { result } = renderHook( () => useChartLegendData( mockPercentageData, mockTheme ) ); - - expect( result.current ).toEqual( [ - { label: 'Desktop', value: '', color: '#ff0000' }, - { label: 'Mobile', value: '', color: '#00ff00' }, - ] ); - } ); - - test( 'returns legend items with percentage values when showValues is true', () => { - const { result } = renderHook( () => - useChartLegendData( mockPercentageData, mockTheme, { showValues: true } ) - ); - - expect( result.current ).toEqual( [ - { label: 'Desktop', value: '65%', color: '#ff0000' }, - { label: 'Mobile', value: '35%', color: '#00ff00' }, - ] ); - } ); - - test( 'uses data point colors when available', () => { - const { result } = renderHook( () => useChartLegendData( mockPercentageData, mockTheme ) ); - - expect( result.current ).toEqual( [ - { label: 'Desktop', value: '', color: '#ff0000' }, - { label: 'Mobile', value: '', color: '#00ff00' }, - ] ); - } ); - - test( 'falls back to theme colors when data point colors not available', () => { - const dataWithoutColors: DataPointPercentage[] = [ - { label: 'Desktop', percentage: 65, value: 650 }, - { label: 'Mobile', percentage: 35, value: 350 }, - ]; - - const { result } = renderHook( () => useChartLegendData( dataWithoutColors, mockTheme ) ); - - expect( result.current ).toEqual( [ - { label: 'Desktop', value: '', color: '#ff0000' }, - { label: 'Mobile', value: '', color: '#00ff00' }, - ] ); - } ); - } ); - - describe( 'Glyph handling', () => { - test( 'returns legend items with glyph properties when withGlyph is true', () => { - const mockRenderGlyph = jest.fn(); - - const { result } = renderHook( () => - useChartLegendData( mockPercentageData, mockTheme, { - withGlyph: true, - glyphSize: 12, - renderGlyph: mockRenderGlyph, - } ) - ); - - expect( result.current ).toEqual( [ - { - label: 'Desktop', - value: '', - color: '#ff0000', - glyphSize: 12, - renderGlyph: mockRenderGlyph, - }, - { - label: 'Mobile', - value: '', - color: '#00ff00', - glyphSize: 12, - renderGlyph: mockRenderGlyph, - }, - ] ); - } ); - - test( 'returns legend items without glyph properties when withGlyph is false', () => { - const mockRenderGlyph = jest.fn(); - - const { result } = renderHook( () => - useChartLegendData( mockPercentageData, mockTheme, { - withGlyph: false, - glyphSize: 12, - renderGlyph: mockRenderGlyph, - } ) - ); - - expect( result.current ).toEqual( [ - { label: 'Desktop', value: '', color: '#ff0000' }, - { label: 'Mobile', value: '', color: '#00ff00' }, - ] ); - } ); - - test( 'uses default glyphSize when not provided', () => { - const mockRenderGlyph = jest.fn(); - - const { result } = renderHook( () => - useChartLegendData( mockPercentageData, mockTheme, { - withGlyph: true, - renderGlyph: mockRenderGlyph, - } ) - ); - - expect( result.current ).toEqual( [ - { - label: 'Desktop', - value: '', - color: '#ff0000', - glyphSize: 8, - renderGlyph: mockRenderGlyph, - }, - { - label: 'Mobile', - value: '', - color: '#00ff00', - glyphSize: 8, - renderGlyph: mockRenderGlyph, - }, - ] ); - } ); - } ); - - describe( 'Edge cases', () => { - test( 'returns empty array for empty data', () => { - const { result } = renderHook( () => useChartLegendData( [], mockTheme ) ); - - expect( result.current ).toEqual( [] ); - } ); - - test( 'returns empty array for null data', () => { - const { result } = renderHook( () => - useChartLegendData( null as unknown as SeriesData[], mockTheme ) - ); - - expect( result.current ).toEqual( [] ); - } ); - - test( 'returns empty array for undefined data', () => { - const { result } = renderHook( () => - useChartLegendData( undefined as unknown as SeriesData[], mockTheme ) - ); - - expect( result.current ).toEqual( [] ); - } ); - - test( 'memoizes result and only recalculates when dependencies change', () => { - const { result, rerender } = renderHook( - ( { data, theme, options } ) => useChartLegendData( data, theme, options ), - { - initialProps: { - data: mockSeriesData, - theme: mockTheme, - options: { showValues: false }, - }, - } - ); - - const firstResult = result.current; - - // Rerender with same props - should return same reference - rerender( { - data: mockSeriesData, - theme: mockTheme, - options: { showValues: false }, - } ); - - expect( result.current ).toBe( firstResult ); - - // Rerender with different props - should return new reference - rerender( { - data: mockSeriesData, - theme: mockTheme, - options: { showValues: true }, - } ); - - expect( result.current ).not.toBe( firstResult ); - } ); - } ); -} ); From 5511c1194a846e166ca35903aa364663f009dd53 Mon Sep 17 00:00:00 2001 From: annacmc Date: Wed, 16 Jul 2025 13:00:41 +1000 Subject: [PATCH 41/62] Clean up unused imports and variables from HighlightTooltip removal - Remove unused TooltipContext and useEffect imports from line-chart.tsx - Remove unused useState import and version state from chart-context.tsx - Completes ESLint cleanup after removing dead code --- .../src/components/line-chart/line-chart.tsx | 36 ++----------------- .../providers/chart-context/chart-context.tsx | 7 ++-- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index 185d79627224c..f7ee27ab4a82e 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -1,9 +1,9 @@ import { formatNumberCompact } from '@automattic/number-formatters'; import { curveCatmullRom, curveLinear, curveMonotoneX } from '@visx/curve'; import { LinearGradient } from '@visx/gradient'; -import { XYChart, AreaSeries, Grid, Axis, DataContext, TooltipContext } from '@visx/xychart'; +import { XYChart, AreaSeries, Grid, Axis, DataContext } from '@visx/xychart'; import clsx from 'clsx'; -import { useId, useMemo, useContext, useState, useRef, useEffect } from 'react'; +import { useId, useMemo, useContext, useState, useRef } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { ChartContext } from '../../providers/chart-context/chart-context'; import { useXYChartTheme, useChartTheme } from '../../providers/theme/theme-provider'; @@ -179,38 +179,6 @@ const validateData = ( data: SeriesData[] ) => { return null; }; -const HighlightTooltip: React.FC< { - series: SeriesData[]; - selectedIndex: number | undefined; -} > = ( { series, selectedIndex } ) => { - const tooltipContext = useContext( TooltipContext ); - - useEffect( () => { - if ( ! series ) return; - - if ( selectedIndex === undefined ) { - tooltipContext?.hideTooltip(); - return; - } - - series.forEach( ( s, index ) => { - if ( selectedIndex < s.data.length ) { - const datum = s.data[ selectedIndex ]; - - tooltipContext?.showTooltip( { - datum, - key: s.label, - index, - } ); - } - } ); - - // Don't include tooltipContext in the dependency array to avoid loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ selectedIndex, series ] ); - return null; -}; - const LineChartInternal: FC< LineChartProps > = ( { data, chartId: providedChartId, diff --git a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx index f593554805beb..ba5c8075bcdf9 100644 --- a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx +++ b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useCallback, useRef, useState, useMemo } from 'react'; +import { createContext, useContext, useCallback, useRef, useMemo } from 'react'; import type { ChartContextValue, ChartRegistration } from './types'; import type { FC, ReactNode } from 'react'; @@ -10,16 +10,13 @@ export interface ChartProviderProps { export const ChartProvider: FC< ChartProviderProps > = ( { children } ) => { const chartsRef = useRef< Map< string, ChartRegistration > >( new Map() ); - const [ version, setVersion ] = useState( 0 ); const registerChart = useCallback( ( id: string, data: ChartRegistration ) => { chartsRef.current.set( id, data ); - setVersion( prev => prev + 1 ); }, [] ); const unregisterChart = useCallback( ( id: string ) => { chartsRef.current.delete( id ); - setVersion( prev => prev + 1 ); }, [] ); const getChartData = useCallback( ( id: string ) => { @@ -33,7 +30,7 @@ export const ChartProvider: FC< ChartProviderProps > = ( { children } ) => { unregisterChart, getChartData, } ), - [ version, registerChart, unregisterChart, getChartData ] + [ registerChart, unregisterChart, getChartData ] ); return { children }; From 557729d98ba7e09415df9ac30c763c6bedd36518 Mon Sep 17 00:00:00 2001 From: annacmc Date: Thu, 17 Jul 2025 09:54:40 +1000 Subject: [PATCH 42/62] Fix legend regression caused by removed ChartProvider version state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the version state and setVersion calls in ChartProvider that were accidentally removed in commit 5511c119. This state is essential for triggering re-renders when charts register/unregister their legend data, ensuring standalone legends display correctly in Storybook examples. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../charts/src/providers/chart-context/chart-context.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx index ba5c8075bcdf9..9f57300ee8206 100644 --- a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx +++ b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useCallback, useRef, useMemo } from 'react'; +import { createContext, useContext, useCallback, useRef, useState, useMemo } from 'react'; import type { ChartContextValue, ChartRegistration } from './types'; import type { FC, ReactNode } from 'react'; @@ -10,13 +10,16 @@ export interface ChartProviderProps { export const ChartProvider: FC< ChartProviderProps > = ( { children } ) => { const chartsRef = useRef< Map< string, ChartRegistration > >( new Map() ); + const [ version, setVersion ] = useState( 0 ); const registerChart = useCallback( ( id: string, data: ChartRegistration ) => { chartsRef.current.set( id, data ); + setVersion( prev => prev + 1 ); }, [] ); const unregisterChart = useCallback( ( id: string ) => { chartsRef.current.delete( id ); + setVersion( prev => prev + 1 ); }, [] ); const getChartData = useCallback( ( id: string ) => { @@ -30,7 +33,8 @@ export const ChartProvider: FC< ChartProviderProps > = ( { children } ) => { unregisterChart, getChartData, } ), - [ registerChart, unregisterChart, getChartData ] + // eslint-disable-next-line react-hooks/exhaustive-deps + [ version, registerChart, unregisterChart, getChartData ] ); return { children }; From 973a98b1793ce10ea9b085e61e7ecebdf41ba3e4 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Fri, 18 Jul 2025 11:11:55 +1000 Subject: [PATCH 43/62] Fix LineChart context isolation for standalone Legend support LineChart now checks for existing ChartProvider context before creating its own, allowing Legend components to access chart data when using chartId prop. This fixes the standalone legend story examples where LineChart was creating an isolated context that prevented data sharing. Also exports ChartContext from the providers index for cleaner imports. --- .../src/components/line-chart/line-chart.tsx | 30 ++++++++++++++----- .../src/providers/chart-context/index.ts | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index ae15b834d10e2..76d104db374db 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -12,7 +12,12 @@ import { useState, useRef, } from 'react'; -import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; +import { + ChartProvider, + ChartContext, + useChartId, + useChartRegistration, +} from '../../providers/chart-context'; import { useXYChartTheme, useChartTheme } from '../../providers/theme/theme-provider'; import { Legend } from '../legend'; import { useChartLegendData } from '../legend/use-chart-legend-data'; @@ -257,7 +262,7 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( const theme = useXYChartTheme( data ); const internalChartId = useId(); // Ensure unique ids for gradient fill. const chartId = useChartId( providedChartId ); - const [ legendRef, legendHeight ] = useElementHeight< HTMLDivElement >(); + const [ , legendHeight ] = useElementHeight< HTMLDivElement >(); const chartRef = useRef< HTMLDivElement >( null ); const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined ); const [ isNavigating, setIsNavigating ] = useState( false ); @@ -500,7 +505,6 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( className={ styles[ 'line-chart-legend' ] } shape={ legendShape } chartId={ chartId } - ref={ legendRef } /> ) } @@ -518,11 +522,21 @@ type LineChartComponent = React.ForwardRefExoticComponent< Annotation: typeof LineChartAnnotation; }; -const LineChart = forwardRef< LineChartRef, LineChartProps >( ( props, ref ) => ( - - - -) ) as LineChartComponent; +const LineChart = forwardRef< LineChartRef, LineChartProps >( ( props, ref ) => { + const existingContext = useContext( ChartContext ); + + // If we're already in a ChartProvider context, don't create a new one + if ( existingContext ) { + return ; + } + + // Otherwise, create our own ChartProvider + return ( + + + + ); +} ) as LineChartComponent; LineChart.displayName = 'LineChart'; LineChart.AnnotationsOverlay = LineChartAnnotationsOverlay; diff --git a/projects/js-packages/charts/src/providers/chart-context/index.ts b/projects/js-packages/charts/src/providers/chart-context/index.ts index b7fb838a68ff0..b061e19e73964 100644 --- a/projects/js-packages/charts/src/providers/chart-context/index.ts +++ b/projects/js-packages/charts/src/providers/chart-context/index.ts @@ -1,3 +1,3 @@ -export { ChartProvider, useChartContext } from './chart-context'; +export { ChartProvider, ChartContext, useChartContext } from './chart-context'; export { useChartId, useChartRegistration } from './utils'; export type { ChartContextValue, ChartRegistration } from './types'; From 94cb1195f0d1d5ed1ac418107e4252d64e3434f5 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:12:44 +1000 Subject: [PATCH 44/62] Remove unnecessary function dependencies from ChartProvider context value --- .../charts/src/providers/chart-context/chart-context.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx index 9f57300ee8206..283fa6a7ad8c0 100644 --- a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx +++ b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx @@ -33,8 +33,9 @@ export const ChartProvider: FC< ChartProviderProps > = ( { children } ) => { unregisterChart, getChartData, } ), + // Only depend on version - the functions are stable from useCallback with empty deps // eslint-disable-next-line react-hooks/exhaustive-deps - [ version, registerChart, unregisterChart, getChartData ] + [ version ] ); return { children }; From c230f78e438935fb1df5c1d63cc350499c8eccf9 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:13:00 +1000 Subject: [PATCH 45/62] Remove stable functions from useChartRegistration dependencies --- .../js-packages/charts/src/providers/chart-context/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/projects/js-packages/charts/src/providers/chart-context/utils.ts b/projects/js-packages/charts/src/providers/chart-context/utils.ts index 6a280f53c1421..4740d77d0ad9c 100644 --- a/projects/js-packages/charts/src/providers/chart-context/utils.ts +++ b/projects/js-packages/charts/src/providers/chart-context/utils.ts @@ -35,6 +35,7 @@ export const useChartRegistration = ( return () => { unregisterChart( chartId ); }; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ chartId, legendItems, @@ -42,7 +43,7 @@ export const useChartRegistration = ( chartType, memoizedMetadata, isDataValid, - registerChart, - unregisterChart, + // Removed registerChart and unregisterChart from dependencies + // They are stable functions created with useCallback and empty deps ] ); }; From 7feb72ebad71fd75bc31787b52fbadf37be98458 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:13:17 +1000 Subject: [PATCH 46/62] Memoize LineChart legend options to prevent re-renders --- .../src/components/line-chart/line-chart.tsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index 76d104db374db..1189c4f3b89ac 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -337,21 +337,33 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( const error = validateData( dataSorted ); const isDataValid = ! error; + // Memoize legend options to prevent unnecessary re-calculations + const legendOptions = useMemo( + () => ( { + withGlyph: withLegendGlyph, + glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ), + renderGlyph, + } ), + [ withLegendGlyph, glyphStyle?.radius, renderGlyph ] + ); + // Create legend items using the reusable hook - const legendItems = useChartLegendData( dataSorted, providerTheme, { - withGlyph: withLegendGlyph, - glyphSize: Math.max( 0, toNumber( glyphStyle?.radius ) ?? 4 ), - renderGlyph, - } ); + const legendItems = useChartLegendData( dataSorted, providerTheme, legendOptions ); + + // Memoize metadata to prevent unnecessary re-registration + const chartMetadata = useMemo( + () => ( { + withGradientFill, + smoothing, + curveType, + withStartGlyphs, + withLegendGlyph, + } ), + [ withGradientFill, smoothing, curveType, withStartGlyphs, withLegendGlyph ] + ); // Register chart with context only if data is valid - useChartRegistration( chartId, legendItems, providerTheme, 'line', isDataValid, { - withGradientFill, - smoothing, - curveType, - withStartGlyphs, - withLegendGlyph, - } ); + useChartRegistration( chartId, legendItems, providerTheme, 'line', isDataValid, chartMetadata ); const accessors = { xAccessor: ( d: DataPointDate ) => d?.date, From f5317aaa767df3da76c25bfc20ab41a30d590cdf Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:13:32 +1000 Subject: [PATCH 47/62] Memoize PieChart legend options to prevent re-renders --- .../src/components/pie-chart/pie-chart.tsx | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx index bac9a57e7e477..d0f2eaeb3ff7f 100644 --- a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx @@ -1,7 +1,7 @@ import { Group } from '@visx/group'; import { Pie } from '@visx/shape'; import clsx from 'clsx'; -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import useChartMouseHandler from '../../hooks/use-chart-mouse-handler'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { ChartContext } from '../../providers/chart-context/chart-context'; @@ -110,19 +110,26 @@ const PieChartInternal = ( { withTooltips, } ); + // Memoize legend options to prevent unnecessary re-calculations + const legendOptions = useMemo( () => ( { showValues: true } ), [] ); + // Create legend items using the reusable hook - const legendItems = useChartLegendData( data, providerTheme, { - showValues: true, - } ); + const legendItems = useChartLegendData( data, providerTheme, legendOptions ); const { isValid, message } = validateData( data ); + // Memoize metadata to prevent unnecessary re-registration + const chartMetadata = useMemo( + () => ( { + thickness, + gapScale, + cornerScale, + } ), + [ thickness, gapScale, cornerScale ] + ); + // Register chart with context only if data is valid - useChartRegistration( chartId, legendItems, providerTheme, 'pie', isValid, { - thickness, - gapScale, - cornerScale, - } ); + useChartRegistration( chartId, legendItems, providerTheme, 'pie', isValid, chartMetadata ); if ( ! isValid ) { return ( From e932790041ce507a4fd7cacceb04cb09596d6121 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:13:47 +1000 Subject: [PATCH 48/62] Memoize BarChart metadata to prevent re-renders --- .../src/components/bar-chart/bar-chart.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx index cc5ccda461f6c..e9c06a185fe4e 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx @@ -1,7 +1,7 @@ import { PatternLines, PatternCircles, PatternWaves, PatternHexagons } from '@visx/pattern'; import { Axis, BarSeries, BarGroup, Grid, XYChart } from '@visx/xychart'; import clsx from 'clsx'; -import { useCallback, useContext, useId, useState, useRef } from 'react'; +import { useCallback, useContext, useId, useState, useRef, useMemo } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { ChartContext } from '../../providers/chart-context/chart-context'; import { useChartTheme, useXYChartTheme } from '../../providers/theme'; @@ -228,11 +228,17 @@ const BarChartInternal: FC< BarChartProps > = ( { const error = validateData( dataSorted ); const isDataValid = ! error; + // Memoize metadata to prevent unnecessary re-registration + const chartMetadata = useMemo( + () => ( { + orientation, + withPatterns, + } ), + [ orientation, withPatterns ] + ); + // Register chart with context only if data is valid - useChartRegistration( chartId, legendItems, providerTheme, 'bar', isDataValid, { - orientation, - withPatterns, - } ); + useChartRegistration( chartId, legendItems, providerTheme, 'bar', isDataValid, chartMetadata ); if ( error ) { return
{ error }
; From fd53a5f0e177fb1c8ef1d349db0f878c82e262e3 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:14:03 +1000 Subject: [PATCH 49/62] Memoize PieSemiCircleChart metadata to prevent re-renders --- .../pie-semi-circle-chart.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx index fb368cd7dd6a6..08b0643a9c09f 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -147,11 +147,24 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { [ data, accessors ] ); + // Memoize metadata to prevent unnecessary re-registration + const chartMetadata = useMemo( + () => ( { + thickness, + clockwise, + } ), + [ thickness, clockwise ] + ); + // Register chart with context only if data is valid - useChartRegistration( chartId, legendItems, providerTheme, 'pie-semi-circle', isValid, { - thickness, - clockwise, - } ); + useChartRegistration( + chartId, + legendItems, + providerTheme, + 'pie-semi-circle', + isValid, + chartMetadata + ); if ( ! isValid ) { return ( From c6a1d86100f70dabdafbbf7e012c7cfd6716097d Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:14:19 +1000 Subject: [PATCH 50/62] Add useMemo import to chart context tests --- .../src/providers/chart-context/chart-context.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/js-packages/charts/src/providers/chart-context/chart-context.test.tsx b/projects/js-packages/charts/src/providers/chart-context/chart-context.test.tsx index 3145e3db9a048..8207389162904 100644 --- a/projects/js-packages/charts/src/providers/chart-context/chart-context.test.tsx +++ b/projects/js-packages/charts/src/providers/chart-context/chart-context.test.tsx @@ -1,4 +1,5 @@ import { render } from '@testing-library/react'; +import { useMemo } from 'react'; import { ChartProvider, useChartContext } from './chart-context'; import { useChartId, useChartRegistration } from './utils'; import type { ChartContextValue } from './types'; @@ -103,7 +104,9 @@ describe( 'ChartContext', () => { const chartId = useChartId( 'test-chart' ); contextValue = useChartContext(); - useChartRegistration( chartId, mockLegendItems, mockTheme, 'bar', true, { test: true } ); + // Memoize metadata to prevent infinite loop + const metadata = useMemo( () => ( { test: true } ), [] ); + useChartRegistration( chartId, mockLegendItems, mockTheme, 'bar', true, metadata ); return
Test
; }; From b4210b8cc5c9e40035d5a7256cef59126951ab82 Mon Sep 17 00:00:00 2001 From: Anna McPhee Date: Sat, 19 Jul 2025 00:14:34 +1000 Subject: [PATCH 51/62] Add clean source code example to standalone legend story --- .../components/legend/stories/index.stories.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx index f441b478edb74..bb35818d40943 100644 --- a/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx @@ -229,6 +229,22 @@ export const StandaloneLegendWithChartId: Story = { render: () => , parameters: { docs: { + source: { + code: ` +
+ {/* Chart with legend hidden but still registering data */} + + {/* Standalone legend that automatically gets data from chart context */} + +
+
`, + }, description: { story: ` ## Standalone Legend with Chart Context Integration From d9dd0bd9ae7051f40c70cdb1f551b14dc007d903 Mon Sep 17 00:00:00 2001 From: annacmc Date: Thu, 24 Jul 2025 10:46:37 +1000 Subject: [PATCH 52/62] Refactor ChartProvider to use useState instead of useRef + version counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace useRef with version state workaround with direct useState for chart registry. Eliminates manual re-render tracking since useState handles this automatically when registry changes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../providers/chart-context/chart-context.tsx | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx index 283fa6a7ad8c0..a2f9c54ba6a4a 100644 --- a/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx +++ b/projects/js-packages/charts/src/providers/chart-context/chart-context.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useCallback, useRef, useState, useMemo } from 'react'; +import { createContext, useContext, useCallback, useState, useMemo } from 'react'; import type { ChartContextValue, ChartRegistration } from './types'; import type { FC, ReactNode } from 'react'; @@ -9,33 +9,35 @@ export interface ChartProviderProps { } export const ChartProvider: FC< ChartProviderProps > = ( { children } ) => { - const chartsRef = useRef< Map< string, ChartRegistration > >( new Map() ); - const [ version, setVersion ] = useState( 0 ); + const [ charts, setCharts ] = useState< Map< string, ChartRegistration > >( () => new Map() ); const registerChart = useCallback( ( id: string, data: ChartRegistration ) => { - chartsRef.current.set( id, data ); - setVersion( prev => prev + 1 ); + setCharts( prev => new Map( prev ).set( id, data ) ); }, [] ); const unregisterChart = useCallback( ( id: string ) => { - chartsRef.current.delete( id ); - setVersion( prev => prev + 1 ); + setCharts( prev => { + const newMap = new Map( prev ); + newMap.delete( id ); + return newMap; + } ); }, [] ); - const getChartData = useCallback( ( id: string ) => { - return chartsRef.current.get( id ); - }, [] ); + const getChartData = useCallback( + ( id: string ) => { + return charts.get( id ); + }, + [ charts ] + ); const value: ChartContextValue = useMemo( () => ( { - charts: chartsRef.current, + charts, registerChart, unregisterChart, getChartData, } ), - // Only depend on version - the functions are stable from useCallback with empty deps - // eslint-disable-next-line react-hooks/exhaustive-deps - [ version ] + [ charts, registerChart, unregisterChart, getChartData ] ); return { children }; From 0df684c2d15cabcb2f13c41d81e7901ada2773c6 Mon Sep 17 00:00:00 2001 From: annacmc Date: Thu, 24 Jul 2025 14:15:03 +1000 Subject: [PATCH 53/62] Remove blanket overflow: hidden from withResponsive HOC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only PieChart actually needs overflow: hidden. Removing it from the responsive wrapper prevents tooltips and annotations from being cut off in LineChart and BarChart components. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../js-packages/charts/src/components/shared/with-responsive.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/js-packages/charts/src/components/shared/with-responsive.tsx b/projects/js-packages/charts/src/components/shared/with-responsive.tsx index b5bb0376222ac..1865d76782f81 100644 --- a/projects/js-packages/charts/src/components/shared/with-responsive.tsx +++ b/projects/js-packages/charts/src/components/shared/with-responsive.tsx @@ -65,7 +65,6 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o style={ { width: '100%', height: chartProps.height ?? 'auto', - overflow: 'hidden', } } > Date: Thu, 24 Jul 2025 22:08:57 +1000 Subject: [PATCH 54/62] Fix withResponsive HOC to respect explicit width/height props --- .../charts/src/components/shared/with-responsive.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/js-packages/charts/src/components/shared/with-responsive.tsx b/projects/js-packages/charts/src/components/shared/with-responsive.tsx index 1865d76782f81..b93746b5ddd03 100644 --- a/projects/js-packages/charts/src/components/shared/with-responsive.tsx +++ b/projects/js-packages/charts/src/components/shared/with-responsive.tsx @@ -68,9 +68,9 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o } } > From 54c99cbfe19c958bae275a310749034715f3d156 Mon Sep 17 00:00:00 2001 From: annacmc Date: Thu, 24 Jul 2025 22:09:13 +1000 Subject: [PATCH 55/62] Fix PieChart TypeScript props to include BaseChartProps --- .../js-packages/charts/src/components/pie-chart/pie-chart.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx index d0f2eaeb3ff7f..6b41c8bf147bb 100644 --- a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx @@ -15,9 +15,7 @@ import styles from './pie-chart.module.scss'; import type { BaseChartProps, DataPointPercentage } from '../../types'; import type { SVGProps, MouseEvent, ReactNode } from 'react'; -type OmitBaseChartProps = Omit< BaseChartProps< DataPointPercentage[] >, 'width' | 'height' >; - -interface PieChartProps extends OmitBaseChartProps { +interface PieChartProps extends BaseChartProps< DataPointPercentage[] > { /** * Inner radius in pixels. If > 0, creates a donut chart. Defaults to 0. */ From 4b4bfde48a2e0cedb02e39c7239bce66def88ad2 Mon Sep 17 00:00:00 2001 From: annacmc Date: Thu, 24 Jul 2025 22:09:21 +1000 Subject: [PATCH 56/62] Remove hardcoded dimensions from dashboard PieChart to enable responsive sizing --- .../src/components/legend/stories/index.stories.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx b/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx index bb35818d40943..1681db064ee82 100644 --- a/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/legend/stories/index.stories.tsx @@ -338,13 +338,7 @@ const DashboardWithCentralizedLegend = () => {

Device Distribution

- +
From 75df5dc117791bb2ddc30e33a56bae1e6f738a0c Mon Sep 17 00:00:00 2001 From: annacmc Date: Fri, 25 Jul 2025 09:36:15 +1000 Subject: [PATCH 57/62] Remove unnecessary space character from legend value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../js-packages/charts/src/components/legend/base-legend.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/js-packages/charts/src/components/legend/base-legend.tsx b/projects/js-packages/charts/src/components/legend/base-legend.tsx index 2219918d3cee4..eb87ed8c1e042 100644 --- a/projects/js-packages/charts/src/components/legend/base-legend.tsx +++ b/projects/js-packages/charts/src/components/legend/base-legend.tsx @@ -133,7 +133,6 @@ export const BaseLegend = forwardRef< HTMLDivElement, BaseLegendProps >( { label.text } { items.find( item => item.label === label.text )?.value && ( - { ' ' } { items.find( item => item.label === label.text )?.value } ) } From bfb564ad086c5c968a28e0788a1014c523099cf4 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 25 Jul 2025 12:30:27 +1200 Subject: [PATCH 58/62] fix responsive height calculation for line chart --- .../charts/src/components/legend/legend.tsx | 33 ++++++++++--------- .../src/components/line-chart/line-chart.tsx | 5 +-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/projects/js-packages/charts/src/components/legend/legend.tsx b/projects/js-packages/charts/src/components/legend/legend.tsx index f844376f7a4c5..1fa366c0e2ade 100644 --- a/projects/js-packages/charts/src/components/legend/legend.tsx +++ b/projects/js-packages/charts/src/components/legend/legend.tsx @@ -1,24 +1,25 @@ -import { useContext, useMemo } from 'react'; +import { useContext, useMemo, forwardRef } 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 ); +export const Legend = forwardRef< HTMLDivElement, LegendProps >( + ( { chartId, items, ...props }, ref ) => { + // 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 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; + // Use context items if available, otherwise fall back to provided items + const legendItems = ( contextItems || items ) as typeof items; - if ( ! legendItems ) { - return null; - } + if ( ! legendItems ) { + return null; + } - return ; -}; + return ; + } +); diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index 1b9453a998251..91b30de8e9c99 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -263,7 +263,7 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( const theme = useXYChartTheme( data ); const internalChartId = useId(); // Ensure unique ids for gradient fill. const chartId = useChartId( providedChartId ); - const [ , legendHeight ] = useElementHeight< HTMLDivElement >(); + const [ legendRef, legendHeight ] = useElementHeight< HTMLDivElement >(); const chartRef = useRef< HTMLDivElement >( null ); const [ selectedIndex, setSelectedIndex ] = useState< number | undefined >( undefined ); const [ isNavigating, setIsNavigating ] = useState( false ); @@ -390,7 +390,7 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( data-testid="line-chart" style={ { width, - height, + height: height - ( showLegend ? legendHeight : 0 ), display: 'flex', flexDirection: showLegend && legendAlignmentVertical === 'top' ? 'column-reverse' : 'column', @@ -518,6 +518,7 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( className={ styles[ 'line-chart-legend' ] } shape={ legendShape } chartId={ chartId } + ref={ legendRef } /> ) } From cc077426c49330b6c7c08b8a18f01c472c99390e Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 25 Jul 2025 12:30:46 +1200 Subject: [PATCH 59/62] should probably be better always manipulate the container --- .../charts/src/components/shared/with-responsive.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/projects/js-packages/charts/src/components/shared/with-responsive.tsx b/projects/js-packages/charts/src/components/shared/with-responsive.tsx index b93746b5ddd03..a42fc5656fbf6 100644 --- a/projects/js-packages/charts/src/components/shared/with-responsive.tsx +++ b/projects/js-packages/charts/src/components/shared/with-responsive.tsx @@ -63,14 +63,14 @@ export function withResponsive< T extends Exclude< BaseChartProps< unknown >, 'o
From c59730df1e06727d3980a0e29d3d44961129c452 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 25 Jul 2025 12:37:21 +1200 Subject: [PATCH 60/62] make legend implementation consistent --- .../src/components/bar-chart/bar-chart.tsx | 20 +++++++++---------- .../src/components/pie-chart/pie-chart.tsx | 5 +++-- .../pie-semi-circle-chart.tsx | 5 +++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx index e9c06a185fe4e..3a25aebe86292 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx @@ -338,16 +338,16 @@ const BarChartInternal: FC< BarChartProps > = ( { { showLegend && ( -
- -
+ ) } ); diff --git a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx index 6b41c8bf147bb..9f62856e5b7f3 100644 --- a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx @@ -6,7 +6,7 @@ import useChartMouseHandler from '../../hooks/use-chart-mouse-handler'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { ChartContext } from '../../providers/chart-context/chart-context'; import { useChartTheme, defaultTheme } from '../../providers/theme'; -import { BaseLegend } from '../legend'; +import { Legend } from '../legend'; import { useChartLegendData } from '../legend/use-chart-legend-data'; import { useElementHeight } from '../shared/use-element-height'; import { withResponsive } from '../shared/with-responsive'; @@ -240,7 +240,7 @@ const PieChartInternal = ( { { showLegend && ( - ) } diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx index 08b0643a9c09f..94ba256ea1e61 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -7,7 +7,7 @@ import clsx from 'clsx'; import { useCallback, useMemo } from 'react'; import { ChartProvider, useChartId, useChartRegistration } from '../../providers/chart-context'; import { useChartTheme } from '../../providers/theme/theme-provider'; -import { BaseLegend } from '../legend'; +import { Legend } from '../legend'; import { useElementHeight } from '../shared/use-element-height'; import { withResponsive } from '../shared/with-responsive'; import { BaseTooltip } from '../tooltip'; @@ -287,7 +287,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( { ) } { showLegend && ( - = ( { className={ styles[ 'pie-semi-circle-chart-legend' ] } shape={ legendShape } ref={ legendRef } + chartId={ chartId } /> ) } From addfb3261826e7227e1048633288a053f1973fd1 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 25 Jul 2025 12:55:42 +1200 Subject: [PATCH 61/62] revert unnecessary change --- .../js-packages/charts/src/components/line-chart/line-chart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index 91b30de8e9c99..663f89f361685 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -390,7 +390,7 @@ const LineChartInternal = forwardRef< LineChartRef, LineChartProps >( data-testid="line-chart" style={ { width, - height: height - ( showLegend ? legendHeight : 0 ), + height, display: 'flex', flexDirection: showLegend && legendAlignmentVertical === 'top' ? 'column-reverse' : 'column', From 1682004558e40fed1cb60142c40c37fba742d382 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 25 Jul 2025 13:07:48 +1200 Subject: [PATCH 62/62] fix typing --- .../charts/src/components/pie-chart/test/pie-chart.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/projects/js-packages/charts/src/components/pie-chart/test/pie-chart.test.tsx b/projects/js-packages/charts/src/components/pie-chart/test/pie-chart.test.tsx index fcdfbb2d8e3f7..1958dbaa2f6f3 100644 --- a/projects/js-packages/charts/src/components/pie-chart/test/pie-chart.test.tsx +++ b/projects/js-packages/charts/src/components/pie-chart/test/pie-chart.test.tsx @@ -18,7 +18,6 @@ describe( 'PieChart', () => { const renderWithTheme = ( props = {} ) => { return render( - { /* @ts-expect-error TODO Fix the missing props */ } );