diff --git a/projects/js-packages/charts/changelog/charts-111-charts-add-hook-to-get-the-styles-for-any-chart-component b/projects/js-packages/charts/changelog/charts-111-charts-add-hook-to-get-the-styles-for-any-chart-component
new file mode 100644
index 0000000000000..b1f6d850e80be
--- /dev/null
+++ b/projects/js-packages/charts/changelog/charts-111-charts-add-hook-to-get-the-styles-for-any-chart-component
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Charts: Add get element styles utility to global context
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 f042875744e3a..282e2bdc14989 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
@@ -126,24 +126,14 @@ const BarChartInternal: FC< BarChartProps > = ( {
totalPoints,
} );
- const { resolveGroupColor } = useGlobalChartsContext();
-
- const getColor = useCallback(
- ( seriesData: SeriesData, index: number ) =>
- resolveGroupColor( {
- group: seriesData.group,
- index,
- overrideColor: seriesData.options?.stroke,
- } ),
- [ resolveGroupColor ]
- );
+ const { getElementStyles } = useGlobalChartsContext();
const getBarBackground = useCallback(
( index: number ) => () =>
withPatterns
? `url(#${ getPatternId( chartId, index ) })`
- : getColor( dataSorted[ index ], index ),
- [ withPatterns, getColor, dataSorted, chartId ]
+ : getElementStyles( { data: dataSorted[ index ], index } ).color,
+ [ withPatterns, getElementStyles, dataSorted, chartId ]
);
const renderDefaultTooltip = useCallback(
@@ -341,12 +331,15 @@ const BarChartInternal: FC< BarChartProps > = ( {
<>
{ dataSorted.map( ( seriesData, index ) =>
- renderPattern( index, getColor( seriesData, index ) )
+ renderPattern( index, getElementStyles( { data: seriesData, index } ).color )
) }
>
diff --git a/projects/js-packages/charts/src/components/leaderboard-chart/hooks/use-leaderboard-legend-items.ts b/projects/js-packages/charts/src/components/leaderboard-chart/hooks/use-leaderboard-legend-items.ts
index 6f5a7fb74cae2..9a876b3cd6cd8 100644
--- a/projects/js-packages/charts/src/components/leaderboard-chart/hooks/use-leaderboard-legend-items.ts
+++ b/projects/js-packages/charts/src/components/leaderboard-chart/hooks/use-leaderboard-legend-items.ts
@@ -36,7 +36,7 @@ export function useLeaderboardLegendItems( {
};
} ): BaseLegendItem[] {
const { leaderboardChart: leaderboardChartSettings } = useGlobalChartsTheme();
- const { resolveGroupColor } = useGlobalChartsContext();
+ const { getElementStyles } = useGlobalChartsContext();
return useMemo( () => {
if ( ! data || data.length === 0 ) {
@@ -46,7 +46,7 @@ export function useLeaderboardLegendItems( {
const items: BaseLegendItem[] = [];
// Add current period legend item
- const resolvedPrimaryColor = resolveGroupColor( {
+ const { color: resolvedPrimaryColor } = getElementStyles( {
index: 0,
overrideColor: primaryColor || leaderboardChartSettings.primaryColor,
} );
@@ -55,13 +55,11 @@ export function useLeaderboardLegendItems( {
label: legendLabels?.primary || __( 'Current period', 'jetpack-charts' ),
value: '',
color: resolvedPrimaryColor,
- index: 0,
- overrideColor: primaryColor,
} );
// Add comparison period legend item if comparison is enabled and overlay label is not enabled
if ( withComparison && ! withOverlayLabel ) {
- const resolvedSecondaryColor = resolveGroupColor( {
+ const { color: resolvedSecondaryColor } = getElementStyles( {
index: 1,
overrideColor: secondaryColor || leaderboardChartSettings.secondaryColor,
} );
@@ -70,8 +68,6 @@ export function useLeaderboardLegendItems( {
label: legendLabels?.comparison || __( 'Previous period', 'jetpack-charts' ),
value: '',
color: resolvedSecondaryColor,
- index: 1,
- overrideColor: secondaryColor,
} );
}
@@ -83,7 +79,7 @@ export function useLeaderboardLegendItems( {
withComparison,
legendLabels,
leaderboardChartSettings,
- resolveGroupColor,
+ getElementStyles,
withOverlayLabel,
] );
}
diff --git a/projects/js-packages/charts/src/components/leaderboard-chart/leaderboard-chart.tsx b/projects/js-packages/charts/src/components/leaderboard-chart/leaderboard-chart.tsx
index 75da420cdd9b9..831a31bfe678b 100644
--- a/projects/js-packages/charts/src/components/leaderboard-chart/leaderboard-chart.tsx
+++ b/projects/js-packages/charts/src/components/leaderboard-chart/leaderboard-chart.tsx
@@ -158,12 +158,12 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
secondaryColor: settingsSecondaryColor,
deltaColors,
} = leaderboardChartSettings;
- const { resolveGroupColor } = useGlobalChartsContext();
- const resolvedPrimaryColor = resolveGroupColor( {
+ const { getElementStyles } = useGlobalChartsContext();
+ const { color: resolvedPrimaryColor } = getElementStyles( {
index: 0,
overrideColor: primaryColor || settingsPrimaryColor,
} );
- const resolvedSecondaryColor = resolveGroupColor( {
+ const { color: resolvedSecondaryColor } = getElementStyles( {
index: 1,
overrideColor: secondaryColor || settingsSecondaryColor,
} );
diff --git a/projects/js-packages/charts/src/components/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx b/projects/js-packages/charts/src/components/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx
index ea494f768824a..f8bf6471a7ac4 100644
--- a/projects/js-packages/charts/src/components/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx
+++ b/projects/js-packages/charts/src/components/leaderboard-chart/test/use-leaderboard-legend-items.test.tsx
@@ -82,8 +82,6 @@ describe( 'useLeaderboardLegendItems', () => {
label: 'Current period',
value: '',
color: expect.any( String ),
- index: 0,
- overrideColor: undefined,
} );
} );
@@ -106,8 +104,6 @@ describe( 'useLeaderboardLegendItems', () => {
label: 'Current period',
value: '',
color: expect.any( String ),
- index: 0,
- overrideColor: undefined,
} );
// Previous period item
@@ -115,8 +111,6 @@ describe( 'useLeaderboardLegendItems', () => {
label: 'Previous period',
value: '',
color: expect.any( String ),
- index: 1,
- overrideColor: undefined,
} );
} );
} );
@@ -143,8 +137,6 @@ describe( 'useLeaderboardLegendItems', () => {
// Note: The actual color will be resolved by the context, but we can check structure
expect( result.current[ 0 ].color ).toBeTruthy();
expect( result.current[ 1 ].color ).toBeTruthy();
- expect( result.current[ 0 ].overrideColor ).toBeUndefined();
- expect( result.current[ 1 ].overrideColor ).toBeUndefined();
} );
it( 'should use custom primary color override', () => {
@@ -161,7 +153,7 @@ describe( 'useLeaderboardLegendItems', () => {
{ wrapper }
);
- expect( result.current[ 0 ].overrideColor ).toBe( customPrimary );
+ expect( result.current[ 0 ].color ).toBe( customPrimary );
} );
it( 'should use custom secondary color override', () => {
@@ -180,8 +172,8 @@ describe( 'useLeaderboardLegendItems', () => {
{ wrapper }
);
- expect( result.current[ 0 ].overrideColor ).toBe( customPrimary );
- expect( result.current[ 1 ].overrideColor ).toBe( customSecondary );
+ expect( result.current[ 0 ].color ).toBe( customPrimary );
+ expect( result.current[ 1 ].color ).toBe( customSecondary );
} );
it( 'should use both custom colors with comparison', () => {
@@ -201,8 +193,8 @@ describe( 'useLeaderboardLegendItems', () => {
);
expect( result.current ).toHaveLength( 2 );
- expect( result.current[ 0 ].overrideColor ).toBe( customPrimary );
- expect( result.current[ 1 ].overrideColor ).toBe( customSecondary );
+ expect( result.current[ 0 ].color ).toBe( customPrimary );
+ expect( result.current[ 1 ].color ).toBe( customSecondary );
} );
} );
@@ -576,7 +568,7 @@ describe( 'useLeaderboardLegendItems', () => {
} );
describe( 'Index and structure validation', () => {
- it( 'should have correct indices for items', () => {
+ it( 'should have correct number of items with comparison', () => {
const wrapper = createWrapper();
const { result } = renderHook(
() =>
@@ -588,8 +580,9 @@ describe( 'useLeaderboardLegendItems', () => {
{ wrapper }
);
- expect( result.current[ 0 ].index ).toBe( 0 );
- expect( result.current[ 1 ].index ).toBe( 1 );
+ expect( result.current ).toHaveLength( 2 );
+ expect( result.current[ 0 ].label ).toBe( 'Current period' );
+ expect( result.current[ 1 ].label ).toBe( 'Previous period' );
} );
it( 'should have empty value strings for all items', () => {
diff --git a/projects/js-packages/charts/src/components/legend/hooks/use-chart-legend-items.ts b/projects/js-packages/charts/src/components/legend/hooks/use-chart-legend-items.ts
index dc950e1b08a92..22d1f85554c05 100644
--- a/projects/js-packages/charts/src/components/legend/hooks/use-chart-legend-items.ts
+++ b/projects/js-packages/charts/src/components/legend/hooks/use-chart-legend-items.ts
@@ -1,7 +1,11 @@
import { useMemo } from 'react';
-import { useGlobalChartsTheme } from '../../../providers';
-import { getItemShapeStyles, getSeriesStroke, formatPercentage } from '../../../utils';
-import type { ChartTheme, SeriesData, DataPointDate, DataPointPercentage } from '../../../types';
+import {
+ useGlobalChartsContext,
+ type GetElementStylesParams,
+ type ElementStyles,
+} from '../../../providers';
+import { formatPercentage } from '../../../utils';
+import type { SeriesData, DataPointDate, DataPointPercentage } from '../../../types';
import type { BaseLegendItem } from '../types';
import type { LegendShape } from '@visx/legend/lib/types';
import type { GlyphProps } from '@visx/xychart';
@@ -15,6 +19,7 @@ export interface ChartLegendOptions {
renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
showValues?: boolean;
legendValueDisplay?: LegendValueDisplay;
+ legendShape?: LegendShape< SeriesData[], number >;
}
/**
@@ -57,19 +62,49 @@ function formatPointValue(
}
/**
- * Processes SeriesData into legend items
- * @param seriesData - The series data to process
- * @param theme - The chart theme for colors
- * @param showValues - Whether to show values in legend
+ * Applies glyph configuration to a legend item if needed
+ * @param baseItem - The base legend item
* @param withGlyph - Whether to include glyph rendering
+ * @param glyph - Glyph component from theme
+ * @param renderGlyph - Custom glyph render function
* @param glyphSize - Size of the glyph
- * @param renderGlyph - Component to render the glyph
- * @param legendShape - The shape to use for the legend
+ * @return The legend item with glyph configuration applied if applicable
+ */
+function applyGlyphToLegendItem(
+ baseItem: BaseLegendItem,
+ withGlyph: boolean,
+ glyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode,
+ renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode,
+ glyphSize?: number
+): BaseLegendItem {
+ if ( withGlyph ) {
+ const glyphToUse = glyph || renderGlyph;
+ if ( glyphToUse ) {
+ return {
+ ...baseItem,
+ glyphSize,
+ renderGlyph: glyphToUse,
+ };
+ }
+ }
+
+ return baseItem;
+}
+
+/**
+ * Processes SeriesData into legend items
+ * @param seriesData - The series data to process
+ * @param getElementStyles - Function to get element styles
+ * @param showValues - Whether to show values in legend
+ * @param withGlyph - Whether to include glyph rendering
+ * @param glyphSize - Size of the glyph
+ * @param renderGlyph - Component to render the glyph
+ * @param legendShape - The shape type for legend items (string literal or React component)
* @return Array of processed legend items
*/
function processSeriesData(
seriesData: SeriesData[],
- theme: ChartTheme,
+ getElementStyles: ( params: GetElementStylesParams ) => ElementStyles,
showValues: boolean,
withGlyph: boolean,
glyphSize: number,
@@ -77,26 +112,20 @@ function processSeriesData(
legendShape?: LegendShape< SeriesData[], number >
): BaseLegendItem[] {
const mapper = ( series: SeriesData, index: number ) => {
- const { shapeStyles } = getItemShapeStyles( series, index, theme, legendShape );
- const baseItem = {
+ const { color, glyph, shapeStyles } = getElementStyles( {
+ data: series,
+ index,
+ legendShape,
+ } );
+
+ const baseItem: BaseLegendItem = {
label: series.label,
value: showValues ? series.data?.length?.toString() || '0' : '',
- color: getSeriesStroke( series, index, theme.colors ),
+ color,
shapeStyle: shapeStyles,
- group: series.group,
- index,
- overrideColor: series.options?.stroke,
};
- if ( withGlyph && renderGlyph ) {
- return {
- ...baseItem,
- glyphSize,
- renderGlyph,
- };
- }
-
- return baseItem;
+ return applyGlyphToLegendItem( baseItem, withGlyph, glyph, renderGlyph, glyphSize );
};
return seriesData.map( mapper );
@@ -105,44 +134,40 @@ function processSeriesData(
/**
* Processes point data into legend items
* @param pointData - The point data to process
- * @param theme - The chart theme for colors
+ * @param getElementStyles - Function to get element styles
* @param showValues - Whether to show values in legend
* @param legendValueDisplay - What type of value to display
* @param withGlyph - Whether to include glyph rendering
* @param glyphSize - Size of the glyph
* @param renderGlyph - Component to render the glyph
+ * @param legendShape - The shape type for legend items (string literal or React component)
* @return Array of processed legend items
*/
function processPointData(
pointData: ( DataPointDate | DataPointPercentage )[],
- theme: ChartTheme,
+ getElementStyles: ( params: GetElementStylesParams ) => ElementStyles,
showValues: boolean,
legendValueDisplay: LegendValueDisplay,
withGlyph: boolean,
glyphSize: number,
- renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode
+ renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode,
+ legendShape?: LegendShape< SeriesData[], number >
): BaseLegendItem[] {
const mapper = ( point: DataPointDate | DataPointPercentage, index: number ) => {
- const baseItem = {
+ const { color, glyph, shapeStyles } = getElementStyles( {
+ data: point as DataPointPercentage,
+ index,
+ legendShape,
+ } );
+
+ const baseItem: BaseLegendItem = {
label: point.label,
value: formatPointValue( point, showValues, legendValueDisplay ),
- color: ( point as DataPointPercentage ).color ?? theme.colors[ index % theme.colors.length ],
- group: ( point as DataPointPercentage ).group,
- index,
- overrideColor: ( point as DataPointPercentage ).color,
+ color,
+ shapeStyle: shapeStyles,
};
- if ( withGlyph && renderGlyph ) {
- const itemWithGlyph = {
- ...baseItem,
- glyphSize,
- renderGlyph,
- };
-
- return itemWithGlyph;
- }
-
- return baseItem;
+ return applyGlyphToLegendItem( baseItem, withGlyph, glyph, renderGlyph, glyphSize );
};
return pointData.map( mapper );
@@ -169,7 +194,7 @@ export function useChartLegendItems<
glyphSize = 8,
renderGlyph,
} = options;
- const theme = useGlobalChartsTheme();
+ const { getElementStyles } = useGlobalChartsContext();
return useMemo( () => {
if ( ! data || ! Array.isArray( data ) || data.length === 0 ) {
@@ -180,7 +205,7 @@ export function useChartLegendItems<
if ( 'data' in data[ 0 ] ) {
return processSeriesData(
data as SeriesData[],
- theme,
+ getElementStyles,
showValues,
withGlyph,
glyphSize,
@@ -192,16 +217,17 @@ export function useChartLegendItems<
// Handle DataPointDate or DataPointPercentage (single data points)
return processPointData(
data as ( DataPointDate | DataPointPercentage )[],
- theme,
+ getElementStyles,
showValues,
legendValueDisplay,
withGlyph,
glyphSize,
- renderGlyph
+ renderGlyph,
+ legendShape
);
}, [
data,
- theme,
+ getElementStyles,
showValues,
legendValueDisplay,
withGlyph,
diff --git a/projects/js-packages/charts/src/components/legend/private/base-legend.tsx b/projects/js-packages/charts/src/components/legend/private/base-legend.tsx
index b9c8aceffb73a..97d6055585676 100644
--- a/projects/js-packages/charts/src/components/legend/private/base-legend.tsx
+++ b/projects/js-packages/charts/src/components/legend/private/base-legend.tsx
@@ -2,16 +2,9 @@ import { Group } from '@visx/group';
import { LegendItem, LegendLabel, LegendOrdinal, LegendShape } from '@visx/legend';
import { scaleOrdinal } from '@visx/scale';
import clsx from 'clsx';
-import {
- type RefAttributes,
- type ForwardRefExoticComponent,
- forwardRef,
- useCallback,
- useMemo,
- useContext,
-} from 'react';
+import { type RefAttributes, type ForwardRefExoticComponent, forwardRef, useCallback } from 'react';
import { useTextTruncation } from '../../../hooks';
-import { useGlobalChartsTheme, GlobalChartsContext } from '../../../providers';
+import { useGlobalChartsTheme } from '../../../providers';
import { valueOrIdentity, valueOrIdentityString, labelTransformFactory } from '../utils';
import styles from './base-legend.module.scss';
import type { BaseLegendProps } from '../types';
@@ -90,37 +83,16 @@ export const BaseLegend: ForwardRefExoticComponent<
ref
) => {
const theme = useGlobalChartsTheme();
- const context = useContext( GlobalChartsContext );
- const resolveGroupColor = context?.resolveGroupColor;
-
- // Resolve colors dynamically for items that have group info
- const itemsWithResolvedColors = useMemo( () => {
- return items.map( item => {
- // If item has group info and we have a context, resolve color dynamically
- if ( item.group !== undefined && item.index !== undefined && resolveGroupColor ) {
- const resolvedColor = resolveGroupColor( {
- group: item.group,
- index: item.index,
- overrideColor: item.overrideColor,
- } );
- return { ...item, color: resolvedColor };
- }
- // Otherwise use the static color
- return item;
- } );
- }, [ items, resolveGroupColor ] );
const legendScale = scaleOrdinal( {
- domain: itemsWithResolvedColors.map( item => item.label ),
- range: itemsWithResolvedColors.map( item => item.color ),
+ domain: items.map( item => item.label ),
+ range: items.map( item => item.color ),
} );
const domain = legendScale.domain();
- // For right-aligned vertical legends, use row-reverse to align text consistently
-
const getShapeStyle = useCallback(
- ( { index }: { index: number } ) => itemsWithResolvedColors[ index ]?.shapeStyle,
- [ itemsWithResolvedColors ]
+ ( { index }: { index: number } ) => items[ index ]?.shapeStyle,
+ [ items ]
);
return (
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 d69864d2b050d..0d767d5e2e1c3 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
@@ -177,7 +177,7 @@ const WithLineChartData = () => {
withGradientFill={ false }
withLegendGlyph={ false }
/>
-
+
);
};
@@ -231,7 +231,7 @@ const StandaloneLegendWithChartIdComponent = () => {
withLegendGlyph={ false }
/>
{ /* Standalone legend that automatically gets data from chart context */ }
-
+
);
};
@@ -365,7 +365,7 @@ const DashboardWithCentralizedLegend = () => {
>
Revenue Trends
-
+
@@ -391,7 +391,7 @@ const DashboardWithCentralizedLegend = () => {
>
Device Distribution
-
+
@@ -551,7 +551,7 @@ This interactive story demonstrates all the text overflow and wrapping features
- **Text Overflow Modes**:
- **Wrap** (default): Text wraps naturally to multiple lines when it exceeds maxWidth
- **Ellipsis**: Truncates text with ellipsis (...) and shows tooltip on hover
-
+
- **Orientation**: Switch between horizontal and vertical layouts
- **Max Width**: Adjust the maximum width constraint with the slider (50-300px)
- **Position & Alignment**: Control legend placement
diff --git a/projects/js-packages/charts/src/components/legend/types.ts b/projects/js-packages/charts/src/components/legend/types.ts
index 603ccef4f8c05..651a55a0d0984 100644
--- a/projects/js-packages/charts/src/components/legend/types.ts
+++ b/projects/js-packages/charts/src/components/legend/types.ts
@@ -38,8 +38,4 @@ export type BaseLegendItem = {
glyphSize?: number;
renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
shapeStyle?: CSSProperties & LineStyles;
- // Optional group info for dynamic color resolution
- group?: string;
- index?: number;
- overrideColor?: string;
};
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 289c167cf8b8a..ef97b87d3ba12 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
@@ -19,7 +19,7 @@ import {
useGlobalChartsContext,
useGlobalChartsTheme,
} from '../../providers';
-import { attachSubComponents, getSeriesLineStyles } from '../../utils';
+import { attachSubComponents } from '../../utils';
import { Legend, useChartLegendItems } from '../legend';
import { DefaultGlyph } from '../private/default-glyph';
import { SingleChartContext, type SingleChartRef } from '../private/single-chart-context';
@@ -252,7 +252,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
);
const dataSorted = useChartDataTransform( data );
- const { resolveGroupColor } = useGlobalChartsContext();
+ const { getElementStyles } = useGlobalChartsContext();
// Use the keyboard navigation hook
const { tooltipRef, onChartFocus, onChartBlur, onChartKeyDown } = useKeyboardNavigation( {
@@ -301,23 +301,20 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
series.label === props.key || series.data.includes( props.datum as DataPointDate )
);
- // Resolve group color for tooltip glyph
const seriesData = dataSorted[ seriesIndex ];
- const resolvedColor = seriesData
- ? resolveGroupColor( {
- group: seriesData.group,
- index: seriesIndex,
- overrideColor: seriesData.options?.stroke,
- } )
- : props.color;
-
- const propsWithResolvedColor = { ...props, color: resolvedColor };
- const themeGlyph = providerTheme.glyphs?.[ seriesIndex ];
+
+ const { color, glyph: themeGlyph } = getElementStyles( {
+ data: seriesData,
+ index: seriesIndex,
+ } );
+
+ const propsWithResolvedColor = { ...props, color };
+
return themeGlyph
? themeGlyph( propsWithResolvedColor )
: renderGlyph( propsWithResolvedColor );
};
- }, [ dataSorted, providerTheme.glyphs, renderGlyph, resolveGroupColor ] );
+ }, [ dataSorted, renderGlyph, getElementStyles ] );
const defaultMargin = useChartMargin( height, chartOptions, dataSorted, theme );
@@ -422,15 +419,13 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
{ dataSorted.map( ( seriesData, index ) => {
- const stroke = resolveGroupColor( {
- group: seriesData.group,
+ const { color, lineStyles, glyph } = getElementStyles( {
+ data: seriesData,
index,
- overrideColor: seriesData.options?.stroke,
} );
- const lineStyles = getSeriesLineStyles( seriesData, index, providerTheme );
const lineProps = {
- stroke,
+ stroke: color,
...lineStyles,
};
@@ -440,8 +435,8 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
@@ -450,7 +445,7 @@ const LineChartInternal = forwardRef< SingleChartRef, LineChartProps >(
{ withGradientFill && (
d.value,
- // Use the color property from the data object as a last resort. The theme provides colours by default.
- fill: ( { group, index, color: overrideColor }: DataPointPercentage & { index: number } ) =>
- resolveGroupColor( { group, index, overrideColor } ),
+ fill: ( d: DataPointPercentage & { index: number } ) => {
+ return getElementStyles( { data: d, index: d.index } ).color;
+ },
};
return (
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 cae6c9e88e4cb..423e20b738660 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
@@ -160,7 +160,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
// Validate data first to get validation result
const { isValid, message } = validateData( data );
- const { resolveGroupColor } = useGlobalChartsContext();
+ const { getElementStyles } = useGlobalChartsContext();
// Define accessors with useMemo to avoid changing dependencies
const accessors = useMemo(
@@ -170,10 +170,10 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
a: DataPointPercentage & { index: number },
b: DataPointPercentage & { index: number }
) => b.value - a.value,
- fill: ( { group, index, color: overrideColor }: DataPointPercentage & { index: number } ) =>
- resolveGroupColor( { group, index, overrideColor } ),
+ fill: ( d: DataPointPercentage & { index: number } ) =>
+ getElementStyles( { data: d, index: d.index } ).color,
} ),
- [ resolveGroupColor ]
+ [ getElementStyles ]
);
// Memoize legend options to prevent unnecessary re-calculations
diff --git a/projects/js-packages/charts/src/providers/chart-context/global-charts-provider.tsx b/projects/js-packages/charts/src/providers/chart-context/global-charts-provider.tsx
index 49ddf88eea930..7bfe90def26a8 100644
--- a/projects/js-packages/charts/src/providers/chart-context/global-charts-provider.tsx
+++ b/projects/js-packages/charts/src/providers/chart-context/global-charts-provider.tsx
@@ -1,5 +1,5 @@
-import { createContext, useCallback, useMemo, useState, useEffect, useRef } from 'react';
-import { mergeThemes } from '../../utils';
+import { createContext, useCallback, useMemo, useState, useEffect } from 'react';
+import { getItemShapeStyles, getSeriesLineStyles, mergeThemes } from '../../utils';
import { defaultTheme } from './themes';
import type { GlobalChartsContextValue, ChartRegistration } from './types';
import type { ChartTheme, CompleteChartTheme } from '../../types';
@@ -9,27 +9,24 @@ export const GlobalChartsContext = createContext< GlobalChartsContextValue | nul
export interface GlobalChartsProviderProps {
children: ReactNode;
- /** Optional theme override. Considered static for provider lifecycle. */
theme?: Partial< ChartTheme >;
}
-export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
- children,
- theme = {},
-} ) => {
+export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { children, theme } ) => {
const [ charts, setCharts ] = useState< Map< string, ChartRegistration > >( () => new Map() );
- const providerTheme: CompleteChartTheme = useMemo(
- () => mergeThemes( defaultTheme, theme ),
- [ theme ]
- );
+ const providerTheme: CompleteChartTheme = useMemo( () => {
+ return theme ? mergeThemes( defaultTheme, theme ) : defaultTheme;
+ }, [ theme ] );
- // Stable group -> color mapping for this provider lifecycle
- const groupToColorMapRef = useRef< Map< string, string > >( new Map() );
+ const [ groupToColorMap, setGroupToColorMap ] = useState< Map< string, string > >(
+ () => new Map()
+ );
- // Reset group color mappings when theme changes
+ // Reset group color mappings when theme colors change
useEffect( () => {
- groupToColorMapRef.current = new Map();
+ // Create a completely new Map instance to trigger dependencies, e.g. useChartLegendItems
+ setGroupToColorMap( new Map() );
}, [ providerTheme.colors ] );
const registerChart = useCallback( ( id: string, data: ChartRegistration ) => {
@@ -51,33 +48,65 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
[ charts ]
);
- const resolveGroupColor = useCallback< GlobalChartsContextValue[ 'resolveGroupColor' ] >(
- ( { group, index, overrideColor } ) => {
- // Highest precedence: explicit series stroke
+ const resolveColor = useCallback(
+ ( {
+ group,
+ index,
+ overrideColor,
+ }: {
+ group?: string;
+ index: number;
+ overrideColor?: string;
+ } ): string => {
+ // Highest precedence: eg. explicit series stroke or chart color prop
if ( overrideColor ) {
return overrideColor;
}
- const palette = providerTheme.colors ?? [];
+ const { colors } = providerTheme;
// If group provided, maintain a stable assignment
if ( group ) {
- const existing = groupToColorMapRef.current.get( group );
+ const existing = groupToColorMap.get( group );
if ( existing ) {
return existing;
}
// Assign next color from palette in a deterministic cycling manner
- const assignedCount = groupToColorMapRef.current.size;
- const color = palette.length > 0 ? palette[ assignedCount % palette.length ] : '#000000';
- groupToColorMapRef.current.set( group, color );
+ const assignedCount = groupToColorMap.size;
+ const color = colors.length > 0 ? colors[ assignedCount % colors.length ] : '#000000';
+ groupToColorMap.set( group, color );
return color;
}
// Fallback: index-based color cycling
- return palette.length > 0 ? palette[ ( index || 0 ) % palette.length ] : '#000000';
+ return colors.length > 0 ? colors[ ( index || 0 ) % colors.length ] : '#000000';
+ },
+ [ providerTheme, groupToColorMap ]
+ );
+
+ const getElementStyles = useCallback< GlobalChartsContextValue[ 'getElementStyles' ] >(
+ ( { data, index, overrideColor, legendShape } ) => {
+ const isSeriesData = data && typeof data === 'object' && 'data' in data && 'options' in data;
+ const isPointPercentageData = data && typeof data === 'object' && 'percentage' in data;
+
+ return {
+ color: resolveColor( {
+ group: data?.group,
+ index,
+ overrideColor:
+ overrideColor ||
+ ( isSeriesData && data?.options?.stroke ) ||
+ ( isPointPercentageData && data?.color ),
+ } ),
+ lineStyles: isSeriesData ? getSeriesLineStyles( data, index, providerTheme ) : {},
+ glyph: providerTheme.glyphs?.[ index ],
+ shapeStyles: isSeriesData
+ ? getItemShapeStyles( data, index, providerTheme, legendShape )
+ : {},
+ };
},
- [ providerTheme.colors ]
+ [ providerTheme, resolveColor ]
);
const value: GlobalChartsContextValue = useMemo(
@@ -87,9 +116,9 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
unregisterChart,
getChartData,
theme: providerTheme,
- resolveGroupColor,
+ getElementStyles,
} ),
- [ charts, registerChart, unregisterChart, getChartData, providerTheme, resolveGroupColor ]
+ [ charts, registerChart, unregisterChart, getChartData, providerTheme, getElementStyles ]
);
return { children };
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 bd89ef2f46737..83018ad412e73 100644
--- a/projects/js-packages/charts/src/providers/chart-context/index.ts
+++ b/projects/js-packages/charts/src/providers/chart-context/index.ts
@@ -3,5 +3,10 @@ export { useGlobalChartsContext } from './hooks/use-global-charts-context';
export { useChartId } from './hooks/use-chart-id';
export { useChartRegistration } from './hooks/use-chart-registration';
export { useGlobalChartsTheme } from './hooks/use-global-charts-theme';
-export type { GlobalChartsContextValue, ChartRegistration } from './types';
+export type {
+ GlobalChartsContextValue,
+ ChartRegistration,
+ GetElementStylesParams,
+ ElementStyles,
+} from './types';
export * from './themes';
diff --git a/projects/js-packages/charts/src/providers/chart-context/test/chart-context.test.tsx b/projects/js-packages/charts/src/providers/chart-context/test/chart-context.test.tsx
index 6936aa2ca3e7a..13ed9a42724fc 100644
--- a/projects/js-packages/charts/src/providers/chart-context/test/chart-context.test.tsx
+++ b/projects/js-packages/charts/src/providers/chart-context/test/chart-context.test.tsx
@@ -5,7 +5,7 @@ import { useChartId } from '../hooks/use-chart-id';
import { useChartRegistration } from '../hooks/use-chart-registration';
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
import type { BaseLegendItem } from '../../../components/legend';
-import type { ChartTheme } from '../../../types';
+import type { ChartTheme, SeriesData } from '../../../types';
import type { GlobalChartsContextValue } from '../types';
describe( 'ChartContext', () => {
@@ -18,6 +18,13 @@ describe( 'ChartContext', () => {
{ label: 'Series 2', value: '200', color: '#00ff00' },
];
+ // Helper function to create mock data for color resolution tests
+ const createMockDataWithGroup = ( group: string | undefined ): SeriesData => ( {
+ label: 'Test',
+ data: [ { value: 100 } ],
+ group,
+ } );
+
describe( 'GlobalChartsProvider', () => {
it( 'provides context to child components', () => {
let contextValue: GlobalChartsContextValue;
@@ -217,8 +224,8 @@ describe( 'ChartContext', () => {
} );
} );
- describe( 'Group Color Resolver', () => {
- it( 'provides resolveGroupColor function', () => {
+ describe( 'Color resolution', () => {
+ it( 'provides getElementStyles function for color resolution', () => {
let contextValue: GlobalChartsContextValue;
const TestComponent = () => {
@@ -232,7 +239,7 @@ describe( 'ChartContext', () => {
);
- expect( contextValue.resolveGroupColor ).toBeInstanceOf( Function );
+ expect( contextValue.getElementStyles ).toBeInstanceOf( Function );
} );
it( 'returns consistent colors for same group across different indices', () => {
@@ -249,9 +256,18 @@ describe( 'ChartContext', () => {
);
- const color1 = contextValue.resolveGroupColor( { group: 'united-states', index: 0 } );
- const color2 = contextValue.resolveGroupColor( { group: 'united-states', index: 5 } );
- const color3 = contextValue.resolveGroupColor( { group: 'united-states', index: 10 } );
+ const color1 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 0,
+ } ).color;
+ const color2 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 5,
+ } ).color;
+ const color3 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 10,
+ } ).color;
expect( color1 ).toBe( color2 );
expect( color2 ).toBe( color3 );
@@ -271,9 +287,18 @@ describe( 'ChartContext', () => {
);
- const usColor = contextValue.resolveGroupColor( { group: 'united-states', index: 0 } );
- const gbColor = contextValue.resolveGroupColor( { group: 'great-britain', index: 0 } );
- const jpColor = contextValue.resolveGroupColor( { group: 'japan', index: 0 } );
+ const usColor = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 0,
+ } ).color;
+ const gbColor = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'great-britain' ),
+ index: 0,
+ } ).color;
+ const jpColor = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'japan' ),
+ index: 0,
+ } ).color;
expect( usColor ).not.toBe( gbColor );
expect( gbColor ).not.toBe( jpColor );
@@ -295,15 +320,15 @@ describe( 'ChartContext', () => {
);
const overrideColor = '#ff6600';
- const colorWithOverride = contextValue.resolveGroupColor( {
- group: 'united-states',
+ const colorWithOverride = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
index: 0,
overrideColor,
- } );
- const colorWithoutOverride = contextValue.resolveGroupColor( {
- group: 'united-states',
+ } ).color;
+ const colorWithoutOverride = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
index: 0,
- } );
+ } ).color;
expect( colorWithOverride ).toBe( overrideColor );
expect( colorWithoutOverride ).not.toBe( overrideColor );
@@ -323,7 +348,10 @@ describe( 'ChartContext', () => {
);
- const color = contextValue.resolveGroupColor( { group: undefined, index: 0 } );
+ const color = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( undefined ),
+ index: 0,
+ } ).color;
expect( color ).toBe( mockTheme.colors[ 0 ] );
} );
@@ -342,7 +370,10 @@ describe( 'ChartContext', () => {
);
- const color = contextValue.resolveGroupColor( { group: '', index: 0 } );
+ const color = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( '' ),
+ index: 0,
+ } ).color;
expect( color ).toBe( mockTheme.colors[ 0 ] );
} );
@@ -361,12 +392,18 @@ describe( 'ChartContext', () => {
);
- const color1 = contextValue.resolveGroupColor( { group: undefined, index: 0 } );
- const color2 = contextValue.resolveGroupColor( { group: '', index: 1 } );
- const color3 = contextValue.resolveGroupColor( {
- group: null as string | undefined,
+ const color1 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( undefined ),
+ index: 0,
+ } ).color;
+ const color2 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( '' ),
+ index: 1,
+ } ).color;
+ const color3 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( null as string | undefined ),
index: 2,
- } );
+ } ).color;
expect( color1 ).toBe( mockTheme.colors[ 0 ] );
expect( color2 ).toBe( mockTheme.colors[ 1 ] );
@@ -388,8 +425,14 @@ describe( 'ChartContext', () => {
);
// mockTheme has 3 colors, so index 3 should wrap to index 0
- const color1 = contextValue.resolveGroupColor( { group: undefined, index: 3 } );
- const color2 = contextValue.resolveGroupColor( { group: undefined, index: 0 } );
+ const color1 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( undefined ),
+ index: 3,
+ } ).color;
+ const color2 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( undefined ),
+ index: 0,
+ } ).color;
expect( color1 ).toBe( color2 );
expect( color1 ).toBe( mockTheme.colors[ 0 ] );
@@ -412,9 +455,14 @@ describe( 'ChartContext', () => {
const groupName = 'consistent-group';
const colors = [];
- // Call resolveGroupColor multiple times for the same group
+ // Call getElementStyles multiple times for the same group
for ( let i = 0; i < 10; i++ ) {
- colors.push( contextValue.resolveGroupColor( { group: groupName, index: i } ) );
+ colors.push(
+ contextValue.getElementStyles( {
+ data: createMockDataWithGroup( groupName ),
+ index: i,
+ } ).color
+ );
}
// All colors should be the same
@@ -441,12 +489,15 @@ describe( 'ChartContext', () => {
const groupName = 'test-group';
const overrideColor = '#purple';
- const groupColor = contextValue.resolveGroupColor( { group: groupName, index: 0 } );
- const overriddenColor = contextValue.resolveGroupColor( {
- group: groupName,
+ const groupColor = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( groupName ),
+ index: 0,
+ } ).color;
+ const overriddenColor = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( groupName ),
index: 0,
overrideColor,
- } );
+ } ).color;
expect( groupColor ).not.toBe( overrideColor );
expect( overriddenColor ).toBe( overrideColor );
@@ -474,8 +525,12 @@ describe( 'ChartContext', () => {
];
// Get initial colors for all groups
- const initialColors = initialGroups.map( ( { group, index } ) =>
- contextValue.resolveGroupColor( { group, index } )
+ const initialColors = initialGroups.map(
+ ( { group, index } ) =>
+ contextValue.getElementStyles( {
+ data: createMockDataWithGroup( group ),
+ index,
+ } ).color
);
// Simulate removing the middle group (great-britain)
@@ -486,8 +541,12 @@ describe( 'ChartContext', () => {
];
// Get colors after "filtering"
- const filteredColors = filteredGroups.map( ( { group, index } ) =>
- contextValue.resolveGroupColor( { group, index } )
+ const filteredColors = filteredGroups.map(
+ ( { group, index } ) =>
+ contextValue.getElementStyles( {
+ data: createMockDataWithGroup( group ),
+ index,
+ } ).color
);
// Colors should remain the same despite index changes
@@ -513,18 +572,42 @@ describe( 'ChartContext', () => {
);
// Get initial colors for all groups
- const usColor1 = contextValue.resolveGroupColor( { group: 'united-states', index: 0 } );
- const gbColor1 = contextValue.resolveGroupColor( { group: 'great-britain', index: 1 } );
- const jpColor1 = contextValue.resolveGroupColor( { group: 'japan', index: 2 } );
+ const usColor1 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 0,
+ } ).color;
+ const gbColor1 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'great-britain' ),
+ index: 1,
+ } ).color;
+ const jpColor1 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'japan' ),
+ index: 2,
+ } ).color;
// Simulate removing great-britain (only US and Japan visible)
- const usColor2 = contextValue.resolveGroupColor( { group: 'united-states', index: 0 } );
- const jpColor2 = contextValue.resolveGroupColor( { group: 'japan', index: 1 } );
+ const usColor2 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 0,
+ } ).color;
+ const jpColor2 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'japan' ),
+ index: 1,
+ } ).color;
// Simulate re-adding great-britain back (all groups visible again)
- const usColor3 = contextValue.resolveGroupColor( { group: 'united-states', index: 0 } );
- const gbColor3 = contextValue.resolveGroupColor( { group: 'great-britain', index: 1 } );
- const jpColor3 = contextValue.resolveGroupColor( { group: 'japan', index: 2 } );
+ const usColor3 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'united-states' ),
+ index: 0,
+ } ).color;
+ const gbColor3 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'great-britain' ),
+ index: 1,
+ } ).color;
+ const jpColor3 = contextValue.getElementStyles( {
+ data: createMockDataWithGroup( 'japan' ),
+ index: 2,
+ } ).color;
// All colors should remain stable throughout the process
expect( usColor1 ).toBe( usColor2 );
@@ -543,12 +626,12 @@ describe( 'ChartContext', () => {
} );
describe( 'Context stability', () => {
- it( 'maintains stable function references', () => {
+ it( 'maintains stable function references when no theme changes', () => {
const functionRefs: Array< {
registerChart: GlobalChartsContextValue[ 'registerChart' ];
unregisterChart: GlobalChartsContextValue[ 'unregisterChart' ];
getChartData: GlobalChartsContextValue[ 'getChartData' ];
- resolveGroupColor: GlobalChartsContextValue[ 'resolveGroupColor' ];
+ getElementStyles: GlobalChartsContextValue[ 'getElementStyles' ];
} > = [];
const TestComponent = () => {
@@ -557,28 +640,621 @@ describe( 'ChartContext', () => {
registerChart: context.registerChart,
unregisterChart: context.unregisterChart,
getChartData: context.getChartData,
- resolveGroupColor: context.resolveGroupColor,
+ getElementStyles: context.getElementStyles,
} );
return Test
;
};
const { rerender } = render(
-
+
);
rerender(
-
+
);
- expect( functionRefs ).toHaveLength( 2 );
- expect( functionRefs[ 0 ].registerChart ).toBe( functionRefs[ 1 ].registerChart );
- expect( functionRefs[ 0 ].unregisterChart ).toBe( functionRefs[ 1 ].unregisterChart );
- expect( functionRefs[ 0 ].getChartData ).toBe( functionRefs[ 1 ].getChartData );
- expect( functionRefs[ 0 ].resolveGroupColor ).toBe( functionRefs[ 1 ].resolveGroupColor );
+ // After initial mount and theme effect, function refs should be stable across re-renders
+ const lastTwoRefs = functionRefs.slice( -2 );
+ expect( lastTwoRefs ).toHaveLength( 2 );
+ expect( lastTwoRefs[ 0 ].registerChart ).toBe( lastTwoRefs[ 1 ].registerChart );
+ expect( lastTwoRefs[ 0 ].unregisterChart ).toBe( lastTwoRefs[ 1 ].unregisterChart );
+ expect( lastTwoRefs[ 0 ].getChartData ).toBe( lastTwoRefs[ 1 ].getChartData );
+ expect( lastTwoRefs[ 0 ].getElementStyles ).toBe( lastTwoRefs[ 1 ].getElementStyles );
+ } );
+ } );
+
+ describe( 'getElementStyles - DataPointPercentage handling', () => {
+ it( 'handles DataPointPercentage data with color override', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const percentageDataWithColor = {
+ label: 'Custom Color Point',
+ value: 100,
+ percentage: 50,
+ color: '#ff9900',
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: percentageDataWithColor,
+ index: 0,
+ } );
+
+ expect( styles.color ).toBe( '#ff9900' );
+ expect( styles.lineStyles ).toEqual( {} );
+ expect( styles.shapeStyles ).toEqual( {} );
+ } );
+
+ it( 'handles DataPointPercentage data without color override', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const percentageDataWithoutColor = {
+ label: 'No Color Point',
+ value: 100,
+ percentage: 50,
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: percentageDataWithoutColor,
+ index: 1,
+ } );
+
+ expect( styles.color ).toBe( mockTheme.colors[ 1 ] );
+ expect( styles.lineStyles ).toEqual( {} );
+ expect( styles.shapeStyles ).toEqual( {} );
+ } );
+
+ it( 'handles DataPointPercentage data with group', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const percentageDataWithGroup = {
+ label: 'Grouped Point',
+ value: 100,
+ percentage: 50,
+ group: 'pie-segment-group',
+ };
+
+ const styles1 = contextValue.getElementStyles( {
+ data: percentageDataWithGroup,
+ index: 0,
+ } );
+ const styles2 = contextValue.getElementStyles( {
+ data: percentageDataWithGroup,
+ index: 5,
+ } );
+
+ // Should have same color due to group consistency
+ expect( styles1.color ).toBe( styles2.color );
+ } );
+
+ it( 'prioritizes DataPointPercentage color over group color', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const percentageDataWithBoth = {
+ label: 'Both Color and Group',
+ value: 100,
+ percentage: 50,
+ color: '#priority-color',
+ group: 'test-group',
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: percentageDataWithBoth,
+ index: 0,
+ } );
+
+ expect( styles.color ).toBe( '#priority-color' );
+ } );
+ } );
+
+ describe( 'getElementStyles - SeriesData line and shape styles', () => {
+ it( 'returns line styles for SeriesData', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const extendedTheme = {
+ ...mockTheme,
+ seriesLineStyles: [ { strokeWidth: 2 }, { strokeWidth: 3, strokeDasharray: '2 2' } ],
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesData = {
+ label: 'Test Series',
+ data: [ { value: 100 } ],
+ options: {
+ seriesLineStyle: { strokeWidth: 5, strokeDasharray: '10 5' },
+ },
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesData,
+ index: 0,
+ } );
+
+ expect( styles.lineStyles ).toEqual( {
+ strokeWidth: 5,
+ strokeDasharray: '10 5',
+ } );
+ } );
+
+ it( 'returns shape styles for SeriesData', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const extendedTheme = {
+ ...mockTheme,
+ legendShapeStyles: [
+ { fill: '#LEGEND1', stroke: '#BORDER1' },
+ { fill: '#LEGEND2', strokeWidth: 3 },
+ ],
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesDataWithShapeStyle = {
+ label: 'Test Series',
+ data: [ { value: 100 } ],
+ options: {
+ legendShapeStyle: { fill: '#CUSTOM', strokeWidth: 5 },
+ },
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesDataWithShapeStyle,
+ index: 0,
+ } );
+
+ expect( styles.shapeStyles ).toEqual( {
+ fill: '#CUSTOM',
+ strokeWidth: 5,
+ } );
+ } );
+
+ it( 'combines line styles with shape styles when legendShape is line', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const extendedTheme = {
+ ...mockTheme,
+ lineChart: {
+ lineStyles: {
+ comparison: {
+ strokeDasharray: '4 4',
+ strokeLinecap: 'square' as const,
+ strokeWidth: 1.5,
+ },
+ },
+ },
+ };
+
+ render(
+
+
+
+ );
+
+ const comparisonSeries = {
+ label: 'Comparison Series',
+ data: [ { value: 100 } ],
+ options: {
+ type: 'comparison' as const,
+ legendShapeStyle: { fill: '#CUSTOM' },
+ },
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: comparisonSeries,
+ index: 0,
+ legendShape: 'line',
+ } );
+
+ expect( styles.shapeStyles ).toEqual( {
+ fill: '#CUSTOM',
+ strokeDasharray: '4 4',
+ strokeLinecap: 'square',
+ strokeWidth: 1.5,
+ } );
+ } );
+
+ it( 'does not include line styles in shapeStyles when legendShape is not line', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const extendedTheme = {
+ ...mockTheme,
+ lineChart: {
+ lineStyles: {
+ comparison: {
+ strokeDasharray: '4 4',
+ strokeLinecap: 'square' as const,
+ strokeWidth: 1.5,
+ },
+ },
+ },
+ legendShapeStyles: [ { fill: '#LEGEND1', stroke: '#BORDER1' } ],
+ };
+
+ render(
+
+
+
+ );
+
+ const comparisonSeries = {
+ label: 'Comparison Series',
+ data: [ { value: 100 } ],
+ options: {
+ type: 'comparison' as const,
+ },
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: comparisonSeries,
+ index: 0,
+ legendShape: 'rect',
+ } );
+
+ // Should get theme legend shape styles, not line styles
+ expect( styles.shapeStyles ).toEqual( {
+ fill: '#LEGEND1',
+ stroke: '#BORDER1',
+ } );
+ } );
+ } );
+
+ describe( 'getElementStyles - glyph assignment', () => {
+ it( 'assigns glyph from theme based on index', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const mockGlyph1 = jest.fn();
+ const mockGlyph2 = jest.fn();
+
+ const themeWithGlyphs = {
+ ...mockTheme,
+ glyphs: [ mockGlyph1, mockGlyph2 ],
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesData = {
+ label: 'Test Series',
+ data: [ { value: 100 } ],
+ };
+
+ const styles1 = contextValue.getElementStyles( {
+ data: seriesData,
+ index: 0,
+ } );
+ const styles2 = contextValue.getElementStyles( {
+ data: seriesData,
+ index: 1,
+ } );
+
+ expect( styles1.glyph ).toBe( mockGlyph1 );
+ expect( styles2.glyph ).toBe( mockGlyph2 );
+ } );
+
+ it( 'handles undefined glyph when index exceeds glyph array', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const mockGlyph = jest.fn();
+ const themeWithGlyphs = {
+ ...mockTheme,
+ glyphs: [ mockGlyph ],
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesData = {
+ label: 'Test Series',
+ data: [ { value: 100 } ],
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesData,
+ index: 5, // Beyond array length
+ } );
+
+ expect( styles.glyph ).toBeUndefined();
+ } );
+
+ it( 'handles missing glyphs in theme', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesData = {
+ label: 'Test Series',
+ data: [ { value: 100 } ],
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesData,
+ index: 0,
+ } );
+
+ expect( styles.glyph ).toBeUndefined();
+ } );
+ } );
+
+ describe( 'getElementStyles - complete object structure', () => {
+ it( 'returns complete ElementStyles object for SeriesData', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const mockGlyph = jest.fn();
+ const completeTheme = {
+ ...mockTheme,
+ glyphs: [ mockGlyph ],
+ seriesLineStyles: [ { strokeWidth: 2 } ],
+ legendShapeStyles: [ { fill: '#SHAPE1' } ],
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesData = {
+ label: 'Test Series',
+ data: [ { value: 100 } ],
+ group: 'test-group',
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesData,
+ index: 0,
+ } );
+
+ // Verify all properties are present
+ expect( styles ).toHaveProperty( 'color' );
+ expect( styles ).toHaveProperty( 'lineStyles' );
+ expect( styles ).toHaveProperty( 'glyph' );
+ expect( styles ).toHaveProperty( 'shapeStyles' );
+
+ // Verify types
+ expect( typeof styles.color ).toBe( 'string' );
+ expect( typeof styles.lineStyles ).toBe( 'object' );
+ expect( typeof styles.glyph ).toBe( 'function' );
+ expect( typeof styles.shapeStyles ).toBe( 'object' );
+ } );
+
+ it( 'returns complete ElementStyles object for DataPointPercentage', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ const mockGlyph = jest.fn();
+ const completeTheme = {
+ ...mockTheme,
+ glyphs: [ mockGlyph ],
+ };
+
+ render(
+
+
+
+ );
+
+ const percentageData = {
+ label: 'Test Point',
+ value: 100,
+ percentage: 50,
+ color: '#custom',
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: percentageData,
+ index: 0,
+ } );
+
+ // Verify all properties are present
+ expect( styles ).toHaveProperty( 'color' );
+ expect( styles ).toHaveProperty( 'lineStyles' );
+ expect( styles ).toHaveProperty( 'glyph' );
+ expect( styles ).toHaveProperty( 'shapeStyles' );
+
+ // Verify values for percentage data
+ expect( styles.color ).toBe( '#custom' );
+ expect( styles.lineStyles ).toEqual( {} );
+ expect( styles.glyph ).toBe( mockGlyph );
+ expect( styles.shapeStyles ).toEqual( {} );
+ } );
+
+ it( 'handles undefined data gracefully', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const styles = contextValue.getElementStyles( {
+ data: undefined,
+ index: 0,
+ } );
+
+ expect( styles ).toHaveProperty( 'color' );
+ expect( styles ).toHaveProperty( 'lineStyles' );
+ expect( styles ).toHaveProperty( 'glyph' );
+ expect( styles ).toHaveProperty( 'shapeStyles' );
+
+ expect( styles.color ).toBe( mockTheme.colors[ 0 ] );
+ expect( styles.lineStyles ).toEqual( {} );
+ expect( styles.glyph ).toBeUndefined();
+ expect( styles.shapeStyles ).toEqual( {} );
+ } );
+ } );
+
+ describe( 'getElementStyles - overrideColor precedence with SeriesData', () => {
+ it( 'prioritizes explicit overrideColor over series stroke', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesWithStroke = {
+ label: 'Series with stroke',
+ data: [ { value: 100 } ],
+ options: { stroke: '#series-stroke' },
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesWithStroke,
+ index: 0,
+ overrideColor: '#explicit-override',
+ } );
+
+ expect( styles.color ).toBe( '#explicit-override' );
+ } );
+
+ it( 'uses series stroke when no explicit overrideColor', () => {
+ let contextValue: GlobalChartsContextValue;
+
+ const TestComponent = () => {
+ contextValue = useGlobalChartsContext();
+ return Test
;
+ };
+
+ render(
+
+
+
+ );
+
+ const seriesWithStroke = {
+ label: 'Series with stroke',
+ data: [ { value: 100 } ],
+ options: { stroke: '#series-stroke' },
+ };
+
+ const styles = contextValue.getElementStyles( {
+ data: seriesWithStroke,
+ index: 0,
+ } );
+
+ expect( styles.color ).toBe( '#series-stroke' );
} );
} );
} );
diff --git a/projects/js-packages/charts/src/providers/chart-context/types.ts b/projects/js-packages/charts/src/providers/chart-context/types.ts
index 574576c45b48d..0daeb1449d7a8 100644
--- a/projects/js-packages/charts/src/providers/chart-context/types.ts
+++ b/projects/js-packages/charts/src/providers/chart-context/types.ts
@@ -1,5 +1,8 @@
+import { CSSProperties, ReactNode } from 'react';
import type { BaseLegendItem } from '../../components/legend';
-import type { CompleteChartTheme } from '../../types';
+import type { CompleteChartTheme, DataPointPercentage, SeriesData } from '../../types';
+import type { LegendShape } from '@visx/legend/lib/types';
+import type { GlyphProps, LineStyles } from '@visx/xychart';
export interface ChartRegistration {
legendItems: BaseLegendItem[];
@@ -7,22 +10,25 @@ export interface ChartRegistration {
metadata?: Record< string, unknown >;
}
+export type GetElementStylesParams = {
+ index: number;
+ data?: SeriesData | DataPointPercentage;
+ overrideColor?: string;
+ legendShape?: LegendShape< SeriesData[], number >;
+};
+
+export type ElementStyles = {
+ color: string;
+ lineStyles: LineStyles;
+ glyph: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
+ shapeStyles: CSSProperties & LineStyles;
+};
+
export interface GlobalChartsContextValue {
charts: Map< string, ChartRegistration >;
registerChart: ( id: string, data: ChartRegistration ) => void;
unregisterChart: ( id: string ) => void;
getChartData: ( id: string ) => ChartRegistration | undefined;
- /** Theme provided by the GlobalChartsProvider (merged with defaults) */
theme: CompleteChartTheme;
- /**
- * Resolve a stable color for a series.
- * - If an override color is passed, it wins.
- * - If a group is provided, returns a stable color per group across charts.
- * - If no group, falls back to index-based color from the theme palette.
- */
- resolveGroupColor: ( params: {
- group?: string;
- index: number;
- overrideColor?: string;
- } ) => string;
+ getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
}
diff --git a/projects/js-packages/charts/src/types.ts b/projects/js-packages/charts/src/types.ts
index 6c4c455d0c2c9..98a0fa5d2952f 100644
--- a/projects/js-packages/charts/src/types.ts
+++ b/projects/js-packages/charts/src/types.ts
@@ -163,7 +163,7 @@ export type ChartTheme = {
/** Styles for series lines */
seriesLineStyles?: LineStyles[];
/** Styles for legend shapes */
- legendShapeStyles?: ( CSSProperties & LineStyles )[];
+ legendShapeStyles?: Record< string, unknown >[];
/** Array of render functions for glyphs */
glyphs?: Array< < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode >;
/** Styles for legend labels */
diff --git a/projects/js-packages/charts/src/utils/get-styles.ts b/projects/js-packages/charts/src/utils/get-styles.ts
index 57e3fc5ba5cd9..1c0dc4191df61 100644
--- a/projects/js-packages/charts/src/utils/get-styles.ts
+++ b/projects/js-packages/charts/src/utils/get-styles.ts
@@ -1,7 +1,6 @@
import type { ChartTheme, SeriesData } from '../types';
import type { LegendShape } from '@visx/legend/lib/types';
import type { LineStyles } from '@visx/xychart';
-import type { CSSProperties } from 'react';
/**
* Utility function to get consolidated line styles for a series
@@ -53,14 +52,14 @@ export function getSeriesStroke(
* @param {number} index - The index of the series in the data array
* @param {ChartTheme} theme - The chart theme configuration
* @param {LegendShape} legendShape - The shape to use for the item (optional)
- * @return {CSSProperties} The shape styles for the item
+ * @return {Record< string, unknown >} The shape styles for the item
*/
export function getItemShapeStyles(
series: SeriesData,
index: number,
theme: ChartTheme,
legendShape?: LegendShape< SeriesData[], number >
-): { shapeStyles: CSSProperties & LineStyles } {
+): Record< string, unknown > {
const seriesShapeStyles = series.options?.legendShapeStyle ?? {};
const lineStyles = legendShape === 'line' ? getSeriesLineStyles( series, index, theme ) : {};
const themeShapeStyles = theme.legendShapeStyles?.[ index ];
@@ -76,9 +75,9 @@ export function getItemShapeStyles(
value => value !== undefined && value !== null && value !== ''
)
) {
- return { shapeStyles: itemShapeStyles as CSSProperties & LineStyles };
+ return itemShapeStyles;
}
// Fallback to theme shape styles if defined
- return { shapeStyles: themeShapeStyles ?? {} };
+ return themeShapeStyles ?? {};
}
diff --git a/projects/js-packages/charts/src/utils/test/get-styles.test.ts b/projects/js-packages/charts/src/utils/test/get-styles.test.ts
index d1f62d89455b3..dd4b1669ee71f 100644
--- a/projects/js-packages/charts/src/utils/test/get-styles.test.ts
+++ b/projects/js-packages/charts/src/utils/test/get-styles.test.ts
@@ -131,7 +131,7 @@ describe( 'Series styling utility functions', () => {
};
const result = getItemShapeStyles( seriesWithShapeStyle, 0, mockTheme as ChartTheme, 'rect' );
- expect( result.shapeStyles ).toEqual( customShapeStyle );
+ expect( result ).toEqual( customShapeStyle );
} );
it( 'combines line styles when legendShape is "line"', () => {
@@ -141,7 +141,7 @@ describe( 'Series styling utility functions', () => {
};
const result = getItemShapeStyles( comparisonSeries, 0, mockTheme as ChartTheme, 'line' );
- expect( result.shapeStyles ).toEqual( {
+ expect( result ).toEqual( {
strokeDasharray: '4 4',
strokeLinecap: 'square',
strokeWidth: 1.5,
@@ -155,7 +155,7 @@ describe( 'Series styling utility functions', () => {
};
const result = getItemShapeStyles( comparisonSeries, 0, mockTheme as ChartTheme, 'rect' );
- expect( result.shapeStyles ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
+ expect( result ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
} );
it( 'merges custom shape styles with line styles for line shape', () => {
@@ -169,7 +169,7 @@ describe( 'Series styling utility functions', () => {
};
const result = getItemShapeStyles( comparisonSeries, 0, mockTheme as ChartTheme, 'line' );
- expect( result.shapeStyles ).toEqual( {
+ expect( result ).toEqual( {
fill: '#CUSTOM',
strokeDasharray: '4 4',
strokeLinecap: 'square',
@@ -189,7 +189,7 @@ describe( 'Series styling utility functions', () => {
mockTheme as ChartTheme,
'rect'
);
- expect( result.shapeStyles ).toEqual( mockTheme.legendShapeStyles[ 1 ] );
+ expect( result ).toEqual( mockTheme.legendShapeStyles[ 1 ] );
} );
it( 'returns empty object when no theme shape styles and no meaningful custom styles', () => {
@@ -199,7 +199,7 @@ describe( 'Series styling utility functions', () => {
} as ChartTheme;
const result = getItemShapeStyles( mockSeriesData, 0, themeWithoutShapeStyles, 'rect' );
- expect( result.shapeStyles ).toEqual( {} );
+ expect( result ).toEqual( {} );
} );
it( 'returns custom styles even with undefined/null values when meaningful values exist', () => {
@@ -221,7 +221,7 @@ describe( 'Series styling utility functions', () => {
mockTheme as ChartTheme,
'rect'
);
- expect( result.shapeStyles ).toEqual( {
+ expect( result ).toEqual( {
fill: '#VALID',
stroke: '',
strokeWidth: undefined,
@@ -242,7 +242,7 @@ describe( 'Series styling utility functions', () => {
};
const result = getItemShapeStyles( complexSeries, 0, mockTheme as ChartTheme, 'line' );
- expect( result.shapeStyles ).toEqual( {
+ expect( result ).toEqual( {
fill: '#OVERRIDE',
strokeWidth: 1.5, // Semantic style wins over custom style
strokeDasharray: '4 4',
@@ -252,12 +252,12 @@ describe( 'Series styling utility functions', () => {
it( 'returns empty object when index exceeds theme shape styles array length', () => {
const result = getItemShapeStyles( mockSeriesData, 3, mockTheme as ChartTheme, 'rect' );
- expect( result.shapeStyles ).toEqual( {} ); // index 3 doesn't exist in array of length 2
+ expect( result ).toEqual( {} ); // index 3 doesn't exist in array of length 2
} );
it( 'works without legendShape parameter', () => {
const result = getItemShapeStyles( mockSeriesData, 0, mockTheme as ChartTheme );
- expect( result.shapeStyles ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
+ expect( result ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
} );
it( 'handles other legendShape string values like "circle"', () => {
@@ -268,7 +268,7 @@ describe( 'Series styling utility functions', () => {
const result = getItemShapeStyles( comparisonSeries, 0, mockTheme as ChartTheme, 'circle' );
// Should not include line styles for circle shape
- expect( result.shapeStyles ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
+ expect( result ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
} );
it( 'handles React component as legendShape parameter', () => {
@@ -285,7 +285,7 @@ describe( 'Series styling utility functions', () => {
CustomShape
);
// Should not include line styles for component shape
- expect( result.shapeStyles ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
+ expect( result ).toEqual( mockTheme.legendShapeStyles[ 0 ] );
} );
it( 'prioritizes series line styles over theme line styles when legendShape is line', () => {
@@ -302,7 +302,7 @@ describe( 'Series styling utility functions', () => {
mockTheme as ChartTheme,
'line'
);
- expect( result.shapeStyles ).toEqual( {
+ expect( result ).toEqual( {
strokeWidth: 99,
strokeDasharray: '10 10',
} );
@@ -324,7 +324,7 @@ describe( 'Series styling utility functions', () => {
'line'
);
// Should return line styles even though legendShapeStyle is empty
- expect( result.shapeStyles ).toEqual( {
+ expect( result ).toEqual( {
strokeDasharray: '4 4',
strokeLinecap: 'square',
strokeWidth: 1.5,