Skip to content

Commit 7cd8804

Browse files
committed
Support HSL, RGB, and RGBA theme colors (CHARTS-138)
- Updated global-charts-provider to use normalizeColorToHex for all color processing - Added support for RGB, RGBA, and HSL color formats in theme colors - Updated normalizeColorToHex to handle rgba() colors (previously rejected) - Added comprehensive tests for RGB, HSL, and RGBA color support - Updated existing tests to reflect new color normalization behavior Fixes: https://linear.app/a8c/issue/CHARTS-138
1 parent 392e97c commit 7cd8804

File tree

5 files changed

+170
-27
lines changed

5 files changed

+170
-27
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: changed
3+
4+
Support HSL, RGB, and RGBA theme colors in addition to hex format

projects/js-packages/charts/src/providers/chart-context/global-charts-provider.tsx

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,24 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
6969
if ( Array.isArray( colors ) ) {
7070
for ( const color of colors ) {
7171
if ( color && typeof color === 'string' ) {
72-
let colorValue = color;
73-
74-
// Handle CSS custom properties - resolve them to actual values
75-
// Supports both '--var-name' and 'var(--var-name)' formats
76-
// Use wrapper element to resolve scoped CSS variables
77-
if ( color.startsWith( '--' ) || color.startsWith( 'var(' ) ) {
78-
const resolved = resolveCssVariable( color, wrapperRef.current );
79-
80-
if ( resolved === null || resolved === '' ) {
81-
continue;
82-
}
83-
84-
colorValue = resolved;
72+
// Normalize color to hex format, handling CSS variables, RGB, HSL, etc.
73+
// This uses normalizeColorToHex which resolves CSS variables and converts
74+
// rgb(), rgba(), hsl() formats to hex
75+
const normalizedColor = normalizeColorToHex(
76+
color,
77+
wrapperRef.current,
78+
resolveCssVariable
79+
);
80+
81+
// Skip if normalization failed or returned empty string
82+
if ( ! normalizedColor ) {
83+
continue;
8584
}
8685

87-
// Process hex colors
88-
if ( colorValue.startsWith( '#' ) ) {
89-
resolvedColors.push( colorValue );
90-
const hslColor = d3Hsl( colorValue );
86+
// Only process valid hex colors (normalizeColorToHex ensures this)
87+
if ( normalizedColor.startsWith( '#' ) ) {
88+
resolvedColors.push( normalizedColor );
89+
const hslColor = d3Hsl( normalizedColor );
9190
// d3Hsl returns NaN values for invalid colors
9291
if ( ! isNaN( hslColor.h ) ) {
9392
const hslTuple: [ number, number, number ] = [

projects/js-packages/charts/src/providers/chart-context/test/chart-context.test.tsx

Lines changed: 124 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,7 +1901,7 @@ describe( 'ChartContext', () => {
19011901
};
19021902

19031903
const cssVarTheme: ChartTheme = {
1904-
colors: [ '--rgb-color', '#ff0000' ],
1904+
colors: [ '--rgb-color', '#00ff00' ],
19051905
} as ChartTheme;
19061906

19071907
render(
@@ -1910,14 +1910,135 @@ describe( 'ChartContext', () => {
19101910
</GlobalChartsProvider>
19111911
);
19121912

1913-
// Non-hex colors are currently skipped, should use second color
1913+
// RGB colors should now be converted to hex and used
19141914
const color = contextValue.getElementStyles( {
19151915
data: undefined,
19161916
index: 0,
19171917
} ).color;
19181918

19191919
expect( color ).toBe( '#ff0000' );
19201920
} );
1921+
1922+
it( 'handles CSS variables resolving to HSL colors', () => {
1923+
window.getComputedStyle = jest.fn( () => ( {
1924+
getPropertyValue: ( prop: string ) => {
1925+
if ( prop === '--hsl-color' ) {
1926+
return 'hsl(120, 100%, 50%)'; // HSL format (green)
1927+
}
1928+
return '';
1929+
},
1930+
} ) ) as unknown as typeof window.getComputedStyle;
1931+
1932+
let contextValue: GlobalChartsContextValue;
1933+
1934+
const TestComponent = () => {
1935+
contextValue = useGlobalChartsContext();
1936+
return <div>Test</div>;
1937+
};
1938+
1939+
const cssVarTheme: ChartTheme = {
1940+
colors: [ '--hsl-color', '#ff0000' ],
1941+
} as ChartTheme;
1942+
1943+
render(
1944+
<GlobalChartsProvider theme={ cssVarTheme }>
1945+
<TestComponent />
1946+
</GlobalChartsProvider>
1947+
);
1948+
1949+
// HSL colors should be converted to hex and used
1950+
const color = contextValue.getElementStyles( {
1951+
data: undefined,
1952+
index: 0,
1953+
} ).color;
1954+
1955+
expect( color ).toBe( '#00ff00' );
1956+
} );
1957+
1958+
it( 'handles CSS variables resolving to RGBA colors', () => {
1959+
window.getComputedStyle = jest.fn( () => ( {
1960+
getPropertyValue: ( prop: string ) => {
1961+
if ( prop === '--rgba-color' ) {
1962+
return 'rgba(0, 0, 255, 0.5)'; // RGBA format (blue with transparency)
1963+
}
1964+
return '';
1965+
},
1966+
} ) ) as unknown as typeof window.getComputedStyle;
1967+
1968+
let contextValue: GlobalChartsContextValue;
1969+
1970+
const TestComponent = () => {
1971+
contextValue = useGlobalChartsContext();
1972+
return <div>Test</div>;
1973+
};
1974+
1975+
const cssVarTheme: ChartTheme = {
1976+
colors: [ '--rgba-color', '#ff0000' ],
1977+
} as ChartTheme;
1978+
1979+
render(
1980+
<GlobalChartsProvider theme={ cssVarTheme }>
1981+
<TestComponent />
1982+
</GlobalChartsProvider>
1983+
);
1984+
1985+
// RGBA colors should be converted to hex (transparency info is lost)
1986+
const color = contextValue.getElementStyles( {
1987+
data: undefined,
1988+
index: 0,
1989+
} ).color;
1990+
1991+
expect( color ).toBe( '#0000ff' );
1992+
} );
1993+
1994+
it( 'handles mix of RGB, HSL, and hex in theme colors', () => {
1995+
window.getComputedStyle = jest.fn( () => ( {
1996+
getPropertyValue: ( prop: string ) => {
1997+
if ( prop === '--rgb-red' ) {
1998+
return 'rgb(255, 0, 0)';
1999+
}
2000+
if ( prop === '--hsl-green' ) {
2001+
return 'hsl(120, 100%, 50%)';
2002+
}
2003+
return '';
2004+
},
2005+
} ) ) as unknown as typeof window.getComputedStyle;
2006+
2007+
let contextValue: GlobalChartsContextValue;
2008+
2009+
const TestComponent = () => {
2010+
contextValue = useGlobalChartsContext();
2011+
return <div>Test</div>;
2012+
};
2013+
2014+
const cssVarTheme: ChartTheme = {
2015+
colors: [ '--rgb-red', '--hsl-green', '#0000ff' ],
2016+
} as ChartTheme;
2017+
2018+
render(
2019+
<GlobalChartsProvider theme={ cssVarTheme }>
2020+
<TestComponent />
2021+
</GlobalChartsProvider>
2022+
);
2023+
2024+
// All color formats should be properly converted
2025+
const color1 = contextValue.getElementStyles( {
2026+
data: undefined,
2027+
index: 0,
2028+
} ).color;
2029+
const color2 = contextValue.getElementStyles( {
2030+
data: undefined,
2031+
index: 1,
2032+
} ).color;
2033+
const color3 = contextValue.getElementStyles( {
2034+
data: undefined,
2035+
index: 2,
2036+
} ).color;
2037+
2038+
expect( color1 ).toBe( '#ff0000' ); // RGB red
2039+
expect( color2 ).toBe( '#00ff00' ); // HSL green
2040+
expect( color3 ).toBe( '#0000ff' ); // Hex blue
2041+
} );
19212042
} );
19222043

19232044
describe( 'Error Handling', () => {
@@ -2052,7 +2173,7 @@ describe( 'ChartContext', () => {
20522173
} ).color;
20532174

20542175
expect( color1 ).toBe( '#ff0000' );
2055-
expect( color2 ).toBe( '#bad' ); // Invalid color is still in palette
2176+
expect( color2 ).toBe( '#bbaadd' ); // #bad is expanded to #bbaadd by normalizeColorToHex
20562177
expect( color3 ).toBe( '#0000ff' );
20572178
} );
20582179
} );

projects/js-packages/charts/src/utils/color-utils.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,17 @@ export const parseHslString = ( hslString: string ): [ number, number, number ]
9191

9292
/**
9393
* Parse an RGB string like 'rgb(255, 0, 0)' into a hex color.
94+
* Note: This function specifically handles rgb() format only, not rgba().
95+
* For general color normalization including rgba(), use normalizeColorToHex() instead.
9496
*
95-
* @param rgbString - RGB color string
97+
* @param rgbString - RGB color string (not RGBA)
9698
* @return hex color string or null if invalid
9799
*/
98100
export const parseRgbString = ( rgbString: string ): string | null => {
99101
const lower = rgbString.toLowerCase().trim();
100102

101103
// Check prefix - only handle rgb(), not rgba()
104+
// This is intentional - use normalizeColorToHex for rgba() support
102105
if ( ! lower.startsWith( 'rgb(' ) || lower.startsWith( 'rgba(' ) ) {
103106
return null;
104107
}
@@ -160,12 +163,12 @@ export const normalizeColorToHex = (
160163
return color;
161164
}
162165

163-
// Handle HSL and RGB strings using d3-color
164-
if ( trimmed.startsWith( 'hsl(' ) || trimmed.startsWith( 'rgb(' ) ) {
165-
// Reject rgba() - we only handle rgb()
166-
if ( trimmed.startsWith( 'rgba(' ) ) {
167-
return color;
168-
}
166+
// Handle HSL, RGB, and RGBA strings using d3-color
167+
if (
168+
trimmed.startsWith( 'hsl(' ) ||
169+
trimmed.startsWith( 'rgb(' ) ||
170+
trimmed.startsWith( 'rgba(' )
171+
) {
169172
const parsed = d3Color( trimmed );
170173
if ( parsed ) {
171174
return parsed.formatHex();

projects/js-packages/charts/src/utils/test/color-utils.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,22 @@ describe( 'normalizeColorToHex', () => {
494494
} );
495495
} );
496496

497+
describe( 'RGBA strings', () => {
498+
it( 'converts rgba(255, 0, 0, 1) to #ff0000', () => {
499+
expect( normalizeColorToHex( 'rgba(255, 0, 0, 1)' ) ).toBe( '#ff0000' );
500+
} );
501+
502+
it( 'converts rgba(0, 0, 255, 0.5) to #0000ff', () => {
503+
// Alpha channel is lost in hex conversion
504+
expect( normalizeColorToHex( 'rgba(0, 0, 255, 0.5)' ) ).toBe( '#0000ff' );
505+
} );
506+
507+
it( 'converts rgba(128, 128, 128, 0) to #000000', () => {
508+
// d3-color converts fully transparent colors (alpha=0) to black
509+
expect( normalizeColorToHex( 'rgba(128, 128, 128, 0)' ) ).toBe( '#000000' );
510+
} );
511+
} );
512+
497513
describe( 'CSS variables', () => {
498514
it( 'returns original if no resolveCss function provided', () => {
499515
expect( normalizeColorToHex( '--my-color' ) ).toBe( '--my-color' );

0 commit comments

Comments
 (0)