@automattic/charts 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/index.cjs +5661 -5466
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +40 -28
- package/dist/index.d.cts +31 -7
- package/dist/index.d.ts +32 -8
- package/dist/index.js +1239 -1046
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -7
- package/src/charts/geo-chart/geo-chart.tsx +148 -3
- package/src/charts/geo-chart/index.ts +1 -1
- package/src/charts/geo-chart/test/geo-chart.test.tsx +122 -2
- package/src/charts/geo-chart/types.ts +11 -0
- package/src/charts/heatmap-chart/heatmap-chart.module.scss +11 -11
- package/src/charts/heatmap-chart/heatmap-chart.tsx +37 -36
- package/src/charts/heatmap-chart/private/build-calendar-data.ts +17 -1
- package/src/charts/heatmap-chart/private/heatmap-context.ts +10 -0
- package/src/charts/heatmap-chart/private/heatmap-legend.tsx +3 -2
- package/src/charts/heatmap-chart/private/index.ts +2 -0
- package/src/charts/heatmap-chart/test/build-calendar-data.test.ts +23 -1
- package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +10 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +15 -3
- package/src/charts/leaderboard-chart/leaderboard-chart.tsx +65 -37
- package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +46 -0
- package/src/charts/line-chart/line-chart.module.scss +2 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
- package/src/charts/private/x-zoom.module.scss +2 -2
- package/src/components/trend-indicator/trend-indicator.module.scss +3 -3
- package/src/hooks/test/use-chart-margin.test.tsx +8 -5
- package/src/hooks/use-chart-margin.tsx +11 -1
- package/src/index.ts +1 -1
- package/src/providers/chart-context/global-charts-provider.tsx +7 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +40 -0
- package/src/providers/chart-context/themes.ts +17 -14
- package/src/providers/chart-context/types.ts +3 -0
- package/src/types.ts +9 -6
- package/src/utils/color-utils.ts +28 -0
- package/src/utils/test/color-utils.test.ts +29 -0
|
@@ -186,6 +186,11 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
186
186
|
[ colorCache, groupToColorMap ]
|
|
187
187
|
);
|
|
188
188
|
|
|
189
|
+
const resolveThemeColor = useCallback< GlobalChartsContextValue[ 'resolveThemeColor' ] >(
|
|
190
|
+
value => ( value ? normalizeColorToHex( value, wrapperRef.current, resolveCssVariable ) : '' ),
|
|
191
|
+
[]
|
|
192
|
+
);
|
|
193
|
+
|
|
189
194
|
const getElementStyles = useCallback< GlobalChartsContextValue[ 'getElementStyles' ] >(
|
|
190
195
|
( { data, index, overrideColor, legendShape } ) => {
|
|
191
196
|
const isSeriesData = data && typeof data === 'object' && 'data' in data && 'options' in data;
|
|
@@ -264,6 +269,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
264
269
|
getChartData,
|
|
265
270
|
theme: providerTheme,
|
|
266
271
|
getElementStyles,
|
|
272
|
+
resolveThemeColor,
|
|
267
273
|
toggleSeriesVisibility,
|
|
268
274
|
isSeriesVisible,
|
|
269
275
|
getHiddenSeries,
|
|
@@ -276,6 +282,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
276
282
|
getChartData,
|
|
277
283
|
providerTheme,
|
|
278
284
|
getElementStyles,
|
|
285
|
+
resolveThemeColor,
|
|
279
286
|
toggleSeriesVisibility,
|
|
280
287
|
isSeriesVisible,
|
|
281
288
|
getHiddenSeries,
|
|
@@ -2536,6 +2536,46 @@ describe( 'ChartContext', () => {
|
|
|
2536
2536
|
expect( afterRerenderColor ).toBe( '#ff0000' );
|
|
2537
2537
|
} );
|
|
2538
2538
|
} );
|
|
2539
|
+
|
|
2540
|
+
describe( 'resolveThemeColor', () => {
|
|
2541
|
+
let contextValue: GlobalChartsContextValue;
|
|
2542
|
+
|
|
2543
|
+
const TestComponent = () => {
|
|
2544
|
+
contextValue = useGlobalChartsContext();
|
|
2545
|
+
return <div>Test</div>;
|
|
2546
|
+
};
|
|
2547
|
+
|
|
2548
|
+
const mountProvider = () =>
|
|
2549
|
+
render(
|
|
2550
|
+
<GlobalChartsProvider>
|
|
2551
|
+
<TestComponent />
|
|
2552
|
+
</GlobalChartsProvider>
|
|
2553
|
+
);
|
|
2554
|
+
|
|
2555
|
+
it( 'resolves a CSS variable against the provider scope, not the document root', () => {
|
|
2556
|
+
window.getComputedStyle = jest.fn( ( element: Element ) => ( {
|
|
2557
|
+
getPropertyValue: ( prop: string ) =>
|
|
2558
|
+
prop === '--surface' && element !== document.documentElement ? '#1e1e1e' : '#ffffff',
|
|
2559
|
+
} ) ) as unknown as typeof window.getComputedStyle;
|
|
2560
|
+
|
|
2561
|
+
mountProvider();
|
|
2562
|
+
|
|
2563
|
+
expect( contextValue.resolveThemeColor( 'var(--surface, #fff)' ) ).toBe( '#1e1e1e' );
|
|
2564
|
+
} );
|
|
2565
|
+
|
|
2566
|
+
it( 'passes a resolved color through to hex', () => {
|
|
2567
|
+
mountProvider();
|
|
2568
|
+
|
|
2569
|
+
expect( contextValue.resolveThemeColor( '#abcdef' ) ).toBe( '#abcdef' );
|
|
2570
|
+
expect( contextValue.resolveThemeColor( 'rgb(255, 0, 0)' ) ).toBe( '#ff0000' );
|
|
2571
|
+
} );
|
|
2572
|
+
|
|
2573
|
+
it( 'returns an empty string for an empty value', () => {
|
|
2574
|
+
mountProvider();
|
|
2575
|
+
|
|
2576
|
+
expect( contextValue.resolveThemeColor( '' ) ).toBe( '' );
|
|
2577
|
+
} );
|
|
2578
|
+
} );
|
|
2539
2579
|
} );
|
|
2540
2580
|
|
|
2541
2581
|
describe( 'defaultTheme', () => {
|
|
@@ -4,7 +4,7 @@ import type { CompleteChartTheme } from '../../types';
|
|
|
4
4
|
* Default theme configuration
|
|
5
5
|
*/
|
|
6
6
|
const defaultTheme: CompleteChartTheme = {
|
|
7
|
-
backgroundColor: 'var(--wpds-color-
|
|
7
|
+
backgroundColor: 'var(--wpds-color-background-surface-neutral-strong, #fff)',
|
|
8
8
|
labelBackgroundColor: 'transparent', // label background color (transparent by default)
|
|
9
9
|
// White label text sits on top of arbitrary series colors, so it has no WPDS
|
|
10
10
|
// content-foreground equivalent and stays hardcoded (tokenization outlier).
|
|
@@ -21,7 +21,7 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
21
21
|
xAxisLineStyles: { stroke: 'var(--wpds-color-stroke-surface-neutral, #dbdbdb)', strokeWidth: 1 },
|
|
22
22
|
legend: {
|
|
23
23
|
labelStyles: {
|
|
24
|
-
color: 'var(--wpds-color-
|
|
24
|
+
color: 'var(--wpds-color-foreground-content-neutral, #1e1e1e)',
|
|
25
25
|
},
|
|
26
26
|
containerStyles: {},
|
|
27
27
|
shapeStyles: [],
|
|
@@ -33,24 +33,27 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
33
33
|
// that `buildChartTheme` injects as an inline style on SVG `<text>`
|
|
34
34
|
// elements for axis labels and ticks. Setting `inherit` lets SVG text
|
|
35
35
|
// pick up the host application's font-family via normal CSS inheritance.
|
|
36
|
-
svgLabelSmall: {
|
|
36
|
+
svgLabelSmall: {
|
|
37
|
+
fill: 'var(--wpds-color-foreground-content-neutral, #1e1e1e)',
|
|
38
|
+
fontFamily: 'inherit',
|
|
39
|
+
},
|
|
37
40
|
svgLabelBig: { fontFamily: 'inherit' },
|
|
38
41
|
annotationStyles: {
|
|
39
42
|
label: {
|
|
40
|
-
anchorLineStroke: 'var(--wpds-color-
|
|
41
|
-
backgroundFill: 'var(--wpds-color-
|
|
43
|
+
anchorLineStroke: 'var(--wpds-color-foreground-content-neutral, #1e1e1e)',
|
|
44
|
+
backgroundFill: 'var(--wpds-color-background-surface-neutral-strong, #fff)',
|
|
42
45
|
},
|
|
43
46
|
connector: {
|
|
44
|
-
stroke: 'var(--wpds-color-
|
|
47
|
+
stroke: 'var(--wpds-color-foreground-content-neutral, #1e1e1e)',
|
|
45
48
|
},
|
|
46
49
|
circleSubject: {
|
|
47
50
|
stroke: 'transparent',
|
|
48
|
-
fill: 'var(--wpds-color-
|
|
51
|
+
fill: 'var(--wpds-color-foreground-content-neutral, #1e1e1e)',
|
|
49
52
|
radius: 5,
|
|
50
53
|
},
|
|
51
54
|
},
|
|
52
55
|
geoChart: {
|
|
53
|
-
featureFillColor: 'var(--wpds-color-
|
|
56
|
+
featureFillColor: 'var(--wpds-color-background-surface-neutral-weak, #f4f4f4)',
|
|
54
57
|
},
|
|
55
58
|
leaderboardChart: {
|
|
56
59
|
rowGap: 12,
|
|
@@ -58,15 +61,15 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
58
61
|
labelSpacing: 'xs',
|
|
59
62
|
// [negative, neutral, positive]
|
|
60
63
|
deltaColors: [
|
|
61
|
-
'var(--wpds-color-
|
|
62
|
-
'var(--wpds-color-
|
|
63
|
-
'var(--wpds-color-
|
|
64
|
+
'var(--wpds-color-foreground-content-error-weak, #cc1818)',
|
|
65
|
+
'var(--wpds-color-foreground-content-neutral-weak, #707070)',
|
|
66
|
+
'var(--wpds-color-foreground-content-success-weak, #008030)',
|
|
64
67
|
],
|
|
65
68
|
},
|
|
66
69
|
conversionFunnelChart: {
|
|
67
|
-
backgroundColor: 'var(--wpds-color-
|
|
68
|
-
positiveChangeColor: 'var(--wpds-color-
|
|
69
|
-
negativeChangeColor: 'var(--wpds-color-
|
|
70
|
+
backgroundColor: 'var(--wpds-color-background-surface-neutral-weak, #f4f4f4)',
|
|
71
|
+
positiveChangeColor: 'var(--wpds-color-foreground-content-success-weak, #008030)',
|
|
72
|
+
negativeChangeColor: 'var(--wpds-color-foreground-content-error-weak, #cc1818)',
|
|
70
73
|
},
|
|
71
74
|
lineChart: {
|
|
72
75
|
lineStyles: {
|
|
@@ -38,6 +38,9 @@ export interface GlobalChartsContextValue {
|
|
|
38
38
|
getChartData: ( id: string ) => ChartRegistration | undefined;
|
|
39
39
|
theme: CompleteChartTheme;
|
|
40
40
|
getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
|
|
41
|
+
// Resolve a theme color or CSS variable to a hex string in the provider's scope.
|
|
42
|
+
// Resolves at call time so it tracks ambient theme changes (e.g. a light/dark switch).
|
|
43
|
+
resolveThemeColor: ( value: string ) => string;
|
|
41
44
|
// Series visibility management for interactive legends
|
|
42
45
|
toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
|
|
43
46
|
isSeriesVisible: ( chartId: string, seriesLabel: string ) => boolean;
|
package/src/types.ts
CHANGED
|
@@ -191,9 +191,10 @@ export type LeaderboardEntry = {
|
|
|
191
191
|
currentValue: number;
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
|
-
* Value of the entry in the previous period
|
|
194
|
+
* Value of the entry in the previous period. Omit when this row has no
|
|
195
|
+
* matching comparison-period value.
|
|
195
196
|
*/
|
|
196
|
-
previousValue
|
|
197
|
+
previousValue?: number;
|
|
197
198
|
|
|
198
199
|
/**
|
|
199
200
|
* Width of current bar, as % of the current value
|
|
@@ -201,14 +202,16 @@ export type LeaderboardEntry = {
|
|
|
201
202
|
currentShare: number;
|
|
202
203
|
|
|
203
204
|
/**
|
|
204
|
-
* Width of previous bar, as % of the current value
|
|
205
|
+
* Width of previous bar, as % of the current value. Omit when this row has
|
|
206
|
+
* no matching comparison-period value.
|
|
205
207
|
*/
|
|
206
|
-
previousShare
|
|
208
|
+
previousShare?: number;
|
|
207
209
|
|
|
208
210
|
/**
|
|
209
|
-
* Delta of the entry
|
|
211
|
+
* Delta of the entry. Omit when this row has no matching comparison-period
|
|
212
|
+
* value.
|
|
210
213
|
*/
|
|
211
|
-
delta
|
|
214
|
+
delta?: number;
|
|
212
215
|
|
|
213
216
|
/**
|
|
214
217
|
* Optional color for the entry's image/icon
|
package/src/utils/color-utils.ts
CHANGED
|
@@ -233,6 +233,34 @@ export const lightenHexColor = ( hex: string, blend: number ): string => {
|
|
|
233
233
|
.padStart( 2, '0' ) }${ newB.toString( 16 ).padStart( 2, '0' ) }`;
|
|
234
234
|
};
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Blend one hex color toward another, per-channel in sRGB.
|
|
238
|
+
*
|
|
239
|
+
* @param fromHex - Starting hex color, returned when blend is 0
|
|
240
|
+
* @param toHex - Target hex color, returned when blend is 1
|
|
241
|
+
* @param blend - Amount toward toHex, clamped to [0, 1]
|
|
242
|
+
* @return Blended hex color string
|
|
243
|
+
* @throws {Error} if either hex string is malformed
|
|
244
|
+
*/
|
|
245
|
+
export const mixHexColors = ( fromHex: string, toHex: string, blend: number ): string => {
|
|
246
|
+
validateHexColor( fromHex );
|
|
247
|
+
validateHexColor( toHex );
|
|
248
|
+
|
|
249
|
+
const amount = Math.min( 1, Math.max( 0, blend ) );
|
|
250
|
+
const channel = ( start: number, end: number ): string =>
|
|
251
|
+
Math.round( start + ( end - start ) * amount )
|
|
252
|
+
.toString( 16 )
|
|
253
|
+
.padStart( 2, '0' );
|
|
254
|
+
|
|
255
|
+
return `#${ channel(
|
|
256
|
+
parseInt( fromHex.slice( 1, 3 ), 16 ),
|
|
257
|
+
parseInt( toHex.slice( 1, 3 ), 16 )
|
|
258
|
+
) }${ channel(
|
|
259
|
+
parseInt( fromHex.slice( 3, 5 ), 16 ),
|
|
260
|
+
parseInt( toHex.slice( 3, 5 ), 16 )
|
|
261
|
+
) }${ channel( parseInt( fromHex.slice( 5, 7 ), 16 ), parseInt( toHex.slice( 5, 7 ), 16 ) ) }`;
|
|
262
|
+
};
|
|
263
|
+
|
|
236
264
|
/**
|
|
237
265
|
* WCAG relative luminance of a hex color (0 = black, 1 = white).
|
|
238
266
|
*
|
|
@@ -2,6 +2,7 @@ import { hsl as d3Hsl } from '@visx/vendor/d3-color';
|
|
|
2
2
|
import {
|
|
3
3
|
getColorDistance,
|
|
4
4
|
lightenHexColor,
|
|
5
|
+
mixHexColors,
|
|
5
6
|
isValidHexColor,
|
|
6
7
|
hexToRgba,
|
|
7
8
|
validateHexColor,
|
|
@@ -585,6 +586,34 @@ describe( 'lightenHexColor', () => {
|
|
|
585
586
|
} );
|
|
586
587
|
} );
|
|
587
588
|
|
|
589
|
+
describe( 'mixHexColors', () => {
|
|
590
|
+
it( 'returns the from color at blend 0', () => {
|
|
591
|
+
expect( mixHexColors( '#ff0000', '#0000ff', 0 ) ).toBe( '#ff0000' );
|
|
592
|
+
} );
|
|
593
|
+
|
|
594
|
+
it( 'returns the to color at blend 1', () => {
|
|
595
|
+
expect( mixHexColors( '#ff0000', '#0000ff', 1 ) ).toBe( '#0000ff' );
|
|
596
|
+
} );
|
|
597
|
+
|
|
598
|
+
it( 'blends halfway between the two colors', () => {
|
|
599
|
+
expect( mixHexColors( '#000000', '#ffffff', 0.5 ) ).toBe( '#808080' );
|
|
600
|
+
} );
|
|
601
|
+
|
|
602
|
+
it( 'matches lightenHexColor when the target is white', () => {
|
|
603
|
+
expect( mixHexColors( '#98c8df', '#ffffff', 0.8 ) ).toBe( lightenHexColor( '#98c8df', 0.8 ) );
|
|
604
|
+
} );
|
|
605
|
+
|
|
606
|
+
it( 'clamps blend outside [0, 1]', () => {
|
|
607
|
+
expect( mixHexColors( '#123456', '#abcdef', -1 ) ).toBe( '#123456' );
|
|
608
|
+
expect( mixHexColors( '#123456', '#abcdef', 2 ) ).toBe( '#abcdef' );
|
|
609
|
+
} );
|
|
610
|
+
|
|
611
|
+
it( 'throws on a malformed hex', () => {
|
|
612
|
+
expect( () => mixHexColors( '#fff', '#000000', 0.5 ) ).toThrow();
|
|
613
|
+
expect( () => mixHexColors( '#000000', 'nope', 0.5 ) ).toThrow();
|
|
614
|
+
} );
|
|
615
|
+
} );
|
|
616
|
+
|
|
588
617
|
describe( 'parseHslString', () => {
|
|
589
618
|
describe( 'Valid HSL strings', () => {
|
|
590
619
|
it( 'parses hsl(120, 50%, 50%)', () => {
|