@automattic/charts 1.7.0 → 1.8.0
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 +8 -0
- package/dist/index.cjs +358 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +6 -4
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +358 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/charts/bar-chart/bar-chart.tsx +196 -42
- package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
- package/src/charts/bar-chart/private/comparison-bars.tsx +154 -0
- package/src/charts/bar-chart/private/comparison-constants.ts +33 -0
- package/src/charts/bar-chart/private/index.ts +9 -0
- package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +47 -0
- package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +183 -0
- package/src/charts/bar-chart/private/use-bar-chart-options.ts +46 -5
- package/src/charts/bar-chart/test/bar-chart.test.tsx +191 -1
- package/src/charts/geo-chart/geo-chart.tsx +5 -9
- package/src/charts/pie-chart/pie-chart.module.scss +0 -4
- package/src/charts/pie-chart/pie-chart.tsx +3 -8
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +0 -5
- package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +3 -8
- package/src/charts/private/center/center.module.scss +4 -0
- package/src/charts/private/center/center.tsx +33 -0
- package/src/charts/private/center/index.ts +2 -0
- package/src/charts/private/center/test/center.test.tsx +60 -0
- package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +0 -2
- package/src/charts/private/svg-empty-state/svg-empty-state.tsx +2 -4
- package/src/providers/chart-context/global-charts-provider.tsx +2 -0
- package/src/providers/chart-context/test/chart-context.test.tsx +13 -1
- package/src/providers/chart-context/themes.ts +8 -0
- package/src/providers/chart-context/types.ts +2 -0
- package/src/types.ts +16 -0
- package/src/utils/get-styles.ts +36 -13
- package/src/utils/index.ts +6 -1
- package/src/utils/test/get-styles.test.ts +47 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { createRef } from 'react';
|
|
3
|
+
import { Center } from '../index';
|
|
4
|
+
|
|
5
|
+
describe( 'Center', () => {
|
|
6
|
+
test( 'renders its children', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Center data-testid="center">
|
|
9
|
+
<span>Centered content</span>
|
|
10
|
+
</Center>
|
|
11
|
+
);
|
|
12
|
+
expect( screen.getByTestId( 'center' ) ).toHaveTextContent( 'Centered content' );
|
|
13
|
+
} );
|
|
14
|
+
|
|
15
|
+
test( 'merges a custom className with the center class', () => {
|
|
16
|
+
render(
|
|
17
|
+
<Center data-testid="center" className="custom-class">
|
|
18
|
+
child
|
|
19
|
+
</Center>
|
|
20
|
+
);
|
|
21
|
+
const el = screen.getByTestId( 'center' );
|
|
22
|
+
expect( el ).toHaveClass( 'center' );
|
|
23
|
+
expect( el ).toHaveClass( 'custom-class' );
|
|
24
|
+
} );
|
|
25
|
+
|
|
26
|
+
test( 'forwards its ref to the underlying element', () => {
|
|
27
|
+
const ref = createRef< HTMLDivElement >();
|
|
28
|
+
render( <Center ref={ ref }>child</Center> );
|
|
29
|
+
expect( ref.current ).toBeInstanceOf( HTMLElement );
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
test( 'spreads arbitrary props through to the element', () => {
|
|
33
|
+
render(
|
|
34
|
+
<Center data-testid="center" style={ { width: 120 } }>
|
|
35
|
+
child
|
|
36
|
+
</Center>
|
|
37
|
+
);
|
|
38
|
+
expect( screen.getByTestId( 'center' ) ).toHaveStyle( { width: '120px' } );
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
test( 'centers on both axes by default', () => {
|
|
42
|
+
render( <Center data-testid="center">child</Center> );
|
|
43
|
+
expect( screen.getByTestId( 'center' ) ).toHaveStyle( {
|
|
44
|
+
alignItems: 'center',
|
|
45
|
+
justifyContent: 'center',
|
|
46
|
+
} );
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
test( 'allows overriding the align and justify defaults', () => {
|
|
50
|
+
render(
|
|
51
|
+
<Center data-testid="center" align="flex-start" justify="flex-end">
|
|
52
|
+
child
|
|
53
|
+
</Center>
|
|
54
|
+
);
|
|
55
|
+
expect( screen.getByTestId( 'center' ) ).toHaveStyle( {
|
|
56
|
+
alignItems: 'flex-start',
|
|
57
|
+
justifyContent: 'flex-end',
|
|
58
|
+
} );
|
|
59
|
+
} );
|
|
60
|
+
} );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Center } from '../center';
|
|
2
2
|
import styles from './svg-empty-state.module.scss';
|
|
3
3
|
import type { FC, ReactNode } from 'react';
|
|
4
4
|
|
|
@@ -32,9 +32,7 @@ interface SvgEmptyStateProps {
|
|
|
32
32
|
export const SvgEmptyState: FC< SvgEmptyStateProps > = ( { x, y, width, height, children } ) => {
|
|
33
33
|
return (
|
|
34
34
|
<foreignObject x={ x - width / 2 } y={ y - height / 2 } width={ width } height={ height }>
|
|
35
|
-
<
|
|
36
|
-
{ children }
|
|
37
|
-
</Stack>
|
|
35
|
+
<Center className={ styles[ 'svg-empty-state' ] }>{ children }</Center>
|
|
38
36
|
</foreignObject>
|
|
39
37
|
);
|
|
40
38
|
};
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
12
|
getItemShapeStyles,
|
|
13
|
+
getSeriesBarStyles,
|
|
13
14
|
getSeriesLineStyles,
|
|
14
15
|
mergeThemes,
|
|
15
16
|
resolveCssVariable,
|
|
@@ -206,6 +207,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( { childre
|
|
|
206
207
|
( isPointPercentageData && data?.color ),
|
|
207
208
|
} ),
|
|
208
209
|
lineStyles: isSeriesData ? getSeriesLineStyles( data, index, providerTheme ) : {},
|
|
210
|
+
barStyles: isSeriesData ? getSeriesBarStyles( data, index, providerTheme ) : {},
|
|
209
211
|
glyph: providerTheme.glyphs?.[ index ],
|
|
210
212
|
shapeStyles: isSeriesData
|
|
211
213
|
? getItemShapeStyles( data, index, providerTheme, legendShape )
|
|
@@ -4,6 +4,7 @@ import { GlobalChartsProvider } from '../global-charts-provider';
|
|
|
4
4
|
import { useChartId } from '../hooks/use-chart-id';
|
|
5
5
|
import { useChartRegistration } from '../hooks/use-chart-registration';
|
|
6
6
|
import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
|
|
7
|
+
import { defaultTheme } from '../themes';
|
|
7
8
|
import type { BaseLegendItem } from '../../../components/legend';
|
|
8
9
|
import type { ChartTheme, SeriesData } from '../../../types';
|
|
9
10
|
import type { GlobalChartsContextValue } from '../types';
|
|
@@ -1165,10 +1166,12 @@ describe( 'ChartContext', () => {
|
|
|
1165
1166
|
legendShape: 'rect',
|
|
1166
1167
|
} );
|
|
1167
1168
|
|
|
1168
|
-
// Should get theme legend shape styles
|
|
1169
|
+
// Should get theme legend shape styles (not line styles), with the comparison bar
|
|
1170
|
+
// opacity layered on so the swatch matches the translucent comparison bar.
|
|
1169
1171
|
expect( styles.shapeStyles ).toEqual( {
|
|
1170
1172
|
fill: '#LEGEND1',
|
|
1171
1173
|
stroke: '#BORDER1',
|
|
1174
|
+
opacity: 0.5,
|
|
1172
1175
|
} );
|
|
1173
1176
|
} );
|
|
1174
1177
|
} );
|
|
@@ -2534,4 +2537,13 @@ describe( 'ChartContext', () => {
|
|
|
2534
2537
|
} );
|
|
2535
2538
|
} );
|
|
2536
2539
|
} );
|
|
2540
|
+
|
|
2541
|
+
describe( 'defaultTheme', () => {
|
|
2542
|
+
it( 'exposes default barChart comparison styles', () => {
|
|
2543
|
+
expect( defaultTheme.barChart.barStyles.comparison ).toEqual( {
|
|
2544
|
+
widthFactor: 1.5,
|
|
2545
|
+
opacity: 0.5,
|
|
2546
|
+
} );
|
|
2547
|
+
} );
|
|
2548
|
+
} );
|
|
2537
2549
|
} );
|
|
@@ -69,6 +69,14 @@ const defaultTheme: CompleteChartTheme = {
|
|
|
69
69
|
},
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
|
+
barChart: {
|
|
73
|
+
barStyles: {
|
|
74
|
+
comparison: {
|
|
75
|
+
widthFactor: 1.5,
|
|
76
|
+
opacity: 0.5,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
72
80
|
sparkline: {
|
|
73
81
|
margin: { top: 2, right: 2, bottom: 2, left: 2 },
|
|
74
82
|
strokeWidth: 1.5,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CSSProperties, ReactNode } from 'react';
|
|
2
2
|
import type { BaseLegendItem } from '../../components/legend';
|
|
3
3
|
import type {
|
|
4
|
+
BarStyles,
|
|
4
5
|
ChartType,
|
|
5
6
|
CompleteChartTheme,
|
|
6
7
|
DataPointPercentage,
|
|
@@ -25,6 +26,7 @@ export type GetElementStylesParams = {
|
|
|
25
26
|
export type ElementStyles = {
|
|
26
27
|
color: string;
|
|
27
28
|
lineStyles: LineStyles;
|
|
29
|
+
barStyles: BarStyles;
|
|
28
30
|
glyph: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode;
|
|
29
31
|
shapeStyles: CSSProperties & LineStyles;
|
|
30
32
|
};
|
package/src/types.ts
CHANGED
|
@@ -268,6 +268,16 @@ export type SeriesData = {
|
|
|
268
268
|
options?: SeriesDataOptions;
|
|
269
269
|
};
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Visual styling for a bar series of a given semantic type (e.g. 'comparison').
|
|
273
|
+
* `widthFactor` is the bar width relative to the primary bar slot (1.5 = 150%);
|
|
274
|
+
* `opacity` sets the shadow translucency.
|
|
275
|
+
*/
|
|
276
|
+
export type BarStyles = {
|
|
277
|
+
widthFactor?: number;
|
|
278
|
+
opacity?: number;
|
|
279
|
+
};
|
|
280
|
+
|
|
271
281
|
export type MultipleDataPointsDate = {
|
|
272
282
|
label: string;
|
|
273
283
|
data: DataPointDate[];
|
|
@@ -388,6 +398,9 @@ export type ChartTheme = {
|
|
|
388
398
|
lineChart?: {
|
|
389
399
|
lineStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles > >;
|
|
390
400
|
};
|
|
401
|
+
barChart?: {
|
|
402
|
+
barStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles > >;
|
|
403
|
+
};
|
|
391
404
|
/** Sparkline specific settings */
|
|
392
405
|
sparkline?: {
|
|
393
406
|
/** Margin around the sparkline chart */
|
|
@@ -420,6 +433,9 @@ export type CompleteChartTheme = Required< ChartTheme > & {
|
|
|
420
433
|
lineChart: {
|
|
421
434
|
lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles >;
|
|
422
435
|
};
|
|
436
|
+
barChart: {
|
|
437
|
+
barStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles >;
|
|
438
|
+
};
|
|
423
439
|
legend: Required< NonNullable< ChartTheme[ 'legend' ] > >;
|
|
424
440
|
sparkline: Required< NonNullable< ChartTheme[ 'sparkline' ] > > & {
|
|
425
441
|
margin: Required< NonNullable< ChartTheme[ 'sparkline' ] >[ 'margin' ] >;
|
package/src/utils/get-styles.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChartTheme, LegendShape, SeriesData } from '../types';
|
|
1
|
+
import type { BarStyles, ChartTheme, LegendShape, SeriesData } from '../types';
|
|
2
2
|
import type { LineStyles } from '@visx/xychart';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -28,6 +28,25 @@ export function getSeriesLineStyles(
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Utility to get consolidated bar styles for a series by semantic type.
|
|
33
|
+
* Mirrors getSeriesLineStyles: a series with `options.type` (e.g. 'comparison')
|
|
34
|
+
* resolves to `theme.barChart.barStyles[ type ]`.
|
|
35
|
+
*
|
|
36
|
+
* @param {SeriesData} seriesData - The series data containing styling options
|
|
37
|
+
* @param {number} index - The index of the series in the data array
|
|
38
|
+
* @param {ChartTheme} providerTheme - The chart theme configuration
|
|
39
|
+
* @return {BarStyles} The consolidated bar styles for the series
|
|
40
|
+
*/
|
|
41
|
+
export function getSeriesBarStyles(
|
|
42
|
+
seriesData: SeriesData,
|
|
43
|
+
index: number,
|
|
44
|
+
providerTheme: ChartTheme
|
|
45
|
+
): BarStyles {
|
|
46
|
+
const type = seriesData.options?.type;
|
|
47
|
+
return ( type && providerTheme?.barChart?.barStyles?.[ type ] ) ?? {};
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
/**
|
|
32
51
|
* Utility function to get stroke color for a series
|
|
33
52
|
*
|
|
@@ -61,22 +80,26 @@ export function getItemShapeStyles(
|
|
|
61
80
|
): Record< string, unknown > {
|
|
62
81
|
const seriesShapeStyles = series.options?.legendShapeStyle ?? {};
|
|
63
82
|
const lineStyles = legendShape === 'line' ? getSeriesLineStyles( series, index, theme ) : {};
|
|
83
|
+
// For non-line legends (e.g. bar 'rect'), reflect the comparison bar's opacity on the
|
|
84
|
+
// swatch so the legend marker matches the translucent comparison bar. Line-type legends
|
|
85
|
+
// convey comparison via the dashed stroke (lineStyles) instead.
|
|
86
|
+
const barOpacity =
|
|
87
|
+
legendShape !== 'line' ? getSeriesBarStyles( series, index, theme ).opacity : undefined;
|
|
88
|
+
const barShapeStyles = barOpacity !== undefined ? { opacity: barOpacity } : {};
|
|
64
89
|
const themeShapeStyles = theme.legend?.shapeStyles?.[ index ];
|
|
65
90
|
|
|
66
|
-
|
|
91
|
+
// Series-level styles (custom shape style + line styles) take precedence; otherwise fall
|
|
92
|
+
// back to the per-index theme shape styles.
|
|
93
|
+
const explicitStyles = {
|
|
67
94
|
...seriesShapeStyles,
|
|
68
95
|
...lineStyles,
|
|
69
96
|
};
|
|
97
|
+
const hasExplicitStyles = Object.values( explicitStyles ).some(
|
|
98
|
+
value => value !== undefined && value !== null && value !== ''
|
|
99
|
+
);
|
|
100
|
+
const baseShapeStyles = hasExplicitStyles ? explicitStyles : themeShapeStyles ?? {};
|
|
70
101
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
value => value !== undefined && value !== null && value !== ''
|
|
75
|
-
)
|
|
76
|
-
) {
|
|
77
|
-
return itemShapeStyles;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Fallback to theme shape styles if defined
|
|
81
|
-
return themeShapeStyles ?? {};
|
|
102
|
+
// Layer the comparison bar opacity on top so the swatch matches the translucent bar
|
|
103
|
+
// without discarding the base (custom or theme) shape styles.
|
|
104
|
+
return { ...baseShapeStyles, ...barShapeStyles };
|
|
82
105
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -13,7 +13,12 @@ export { formatPercentage } from './format-percentage';
|
|
|
13
13
|
export { getLongestTickWidth } from './get-longest-tick-width';
|
|
14
14
|
|
|
15
15
|
// Style and theming utilities
|
|
16
|
-
export {
|
|
16
|
+
export {
|
|
17
|
+
getSeriesBarStyles,
|
|
18
|
+
getSeriesLineStyles,
|
|
19
|
+
getSeriesStroke,
|
|
20
|
+
getItemShapeStyles,
|
|
21
|
+
} from './get-styles';
|
|
17
22
|
|
|
18
23
|
// Browser detection utilities
|
|
19
24
|
export { isSafari } from './is-safari';
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { ChartTheme } from '../../types';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getSeriesBarStyles,
|
|
4
|
+
getSeriesLineStyles,
|
|
5
|
+
getSeriesStroke,
|
|
6
|
+
getItemShapeStyles,
|
|
7
|
+
} from '../get-styles';
|
|
3
8
|
|
|
4
9
|
describe( 'Series styling utility functions', () => {
|
|
5
10
|
const mockSeriesData = {
|
|
@@ -157,6 +162,28 @@ describe( 'Series styling utility functions', () => {
|
|
|
157
162
|
expect( result ).toEqual( mockTheme.legend.shapeStyles[ 0 ] );
|
|
158
163
|
} );
|
|
159
164
|
|
|
165
|
+
it( 'applies the comparison bar opacity to the swatch for non-line legends', () => {
|
|
166
|
+
const themeWithBar = {
|
|
167
|
+
...mockTheme,
|
|
168
|
+
barChart: { barStyles: { comparison: { widthFactor: 1.5, opacity: 0.5 } } },
|
|
169
|
+
// No per-index legend shape styles, so the swatch reflects only the comparison opacity.
|
|
170
|
+
legend: { shapeStyles: [] },
|
|
171
|
+
} as ChartTheme;
|
|
172
|
+
const comparisonSeries = {
|
|
173
|
+
...mockSeriesData,
|
|
174
|
+
options: { type: 'comparison' as const },
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// rect (bar) legend: swatch picks up the comparison opacity to match the bar.
|
|
178
|
+
const rectResult = getItemShapeStyles( comparisonSeries, 0, themeWithBar, 'rect' );
|
|
179
|
+
expect( rectResult ).toEqual( { opacity: 0.5 } );
|
|
180
|
+
|
|
181
|
+
// line legend: comparison is conveyed via the dashed stroke, not opacity.
|
|
182
|
+
const lineResult = getItemShapeStyles( comparisonSeries, 0, themeWithBar, 'line' );
|
|
183
|
+
expect( lineResult.opacity ).toBeUndefined();
|
|
184
|
+
expect( lineResult ).toMatchObject( { strokeDasharray: '4 4' } );
|
|
185
|
+
} );
|
|
186
|
+
|
|
160
187
|
it( 'merges custom shape styles with line styles for line shape', () => {
|
|
161
188
|
const customShapeStyle = { fill: '#CUSTOM' };
|
|
162
189
|
const comparisonSeries = {
|
|
@@ -330,4 +357,23 @@ describe( 'Series styling utility functions', () => {
|
|
|
330
357
|
} );
|
|
331
358
|
} );
|
|
332
359
|
} );
|
|
360
|
+
|
|
361
|
+
describe( 'getSeriesBarStyles', () => {
|
|
362
|
+
const themeWithBar = {
|
|
363
|
+
...mockTheme,
|
|
364
|
+
barChart: { barStyles: { comparison: { widthFactor: 1.5, opacity: 0.5 } } },
|
|
365
|
+
} as ChartTheme;
|
|
366
|
+
|
|
367
|
+
it( 'returns comparison bar styles when type is comparison', () => {
|
|
368
|
+
const comparisonSeries = { ...mockSeriesData, options: { type: 'comparison' as const } };
|
|
369
|
+
expect( getSeriesBarStyles( comparisonSeries, 0, themeWithBar ) ).toEqual( {
|
|
370
|
+
widthFactor: 1.5,
|
|
371
|
+
opacity: 0.5,
|
|
372
|
+
} );
|
|
373
|
+
} );
|
|
374
|
+
|
|
375
|
+
it( 'returns empty styles for a series with no type', () => {
|
|
376
|
+
expect( getSeriesBarStyles( mockSeriesData, 0, themeWithBar ) ).toEqual( {} );
|
|
377
|
+
} );
|
|
378
|
+
} );
|
|
333
379
|
} );
|