@automattic/charts 1.7.0 → 1.8.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 +13 -0
- package/dist/index.cjs +347 -61
- 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 +348 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/charts/bar-chart/bar-chart.tsx +210 -49
- package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
- package/src/charts/bar-chart/private/comparison-bars.tsx +155 -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 +49 -5
- package/src/charts/bar-chart/test/bar-chart.test.tsx +329 -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
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* External dependencies
|
|
3
3
|
*/
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
|
-
import { Stack } from '@wordpress/ui';
|
|
6
5
|
import clsx from 'clsx';
|
|
7
6
|
import { FC, useContext, useMemo } from 'react';
|
|
8
7
|
import { Chart } from 'react-google-charts';
|
|
@@ -13,6 +12,7 @@ import { GlobalChartsContext, GlobalChartsProvider, useGlobalChartsContext } fro
|
|
|
13
12
|
import { lightenHexColor, normalizeColorToHex } from '../../utils/color-utils';
|
|
14
13
|
import { resolveCssVariable } from '../../utils/resolve-css-var';
|
|
15
14
|
import { sanitizeHtml } from '../../utils/sanitize-html';
|
|
15
|
+
import { Center } from '../private/center';
|
|
16
16
|
import { withResponsive } from '../private/with-responsive';
|
|
17
17
|
import styles from './geo-chart.module.scss';
|
|
18
18
|
import { GeoChartProps } from './types';
|
|
@@ -60,15 +60,13 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
60
60
|
|
|
61
61
|
// Render loading placeholder
|
|
62
62
|
const loadingPlaceholder = (
|
|
63
|
-
<
|
|
64
|
-
align="center"
|
|
65
|
-
justify="center"
|
|
63
|
+
<Center
|
|
66
64
|
className={ clsx( 'geo-chart', styles.container, className ) }
|
|
67
65
|
data-testid="geo-chart-loading"
|
|
68
66
|
style={ { width, height } }
|
|
69
67
|
>
|
|
70
68
|
{ renderPlaceholder ? renderPlaceholder() : __( 'Loading map', 'jetpack-charts' ) }
|
|
71
|
-
</
|
|
69
|
+
</Center>
|
|
72
70
|
);
|
|
73
71
|
|
|
74
72
|
// Google charts doesn't accept CSS variables, so we need to convert them to hex colors
|
|
@@ -149,9 +147,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
149
147
|
);
|
|
150
148
|
|
|
151
149
|
return (
|
|
152
|
-
<
|
|
153
|
-
align="center"
|
|
154
|
-
justify="center"
|
|
150
|
+
<Center
|
|
155
151
|
className={ clsx( 'geo-chart', styles.container, className ) }
|
|
156
152
|
data-testid="geo-chart"
|
|
157
153
|
style={ { width, height, backgroundColor } }
|
|
@@ -164,7 +160,7 @@ const GeoChartInternal: FC< GeoChartProps > = ( {
|
|
|
164
160
|
options={ options }
|
|
165
161
|
loader={ loadingPlaceholder }
|
|
166
162
|
/>
|
|
167
|
-
</
|
|
163
|
+
</Center>
|
|
168
164
|
);
|
|
169
165
|
};
|
|
170
166
|
|
|
@@ -2,7 +2,6 @@ import { Group } from '@visx/group';
|
|
|
2
2
|
import { Pie } from '@visx/shape';
|
|
3
3
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
|
4
4
|
import { __ } from '@wordpress/i18n';
|
|
5
|
-
import { Stack } from '@wordpress/ui';
|
|
6
5
|
import clsx from 'clsx';
|
|
7
6
|
import { useCallback, useContext, useMemo } from 'react';
|
|
8
7
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
} from '../../providers';
|
|
23
22
|
import { attachSubComponents, resolveFontSize } from '../../utils';
|
|
24
23
|
import { getStringWidth } from '../../visx/text';
|
|
24
|
+
import { Center } from '../private/center';
|
|
25
25
|
import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
|
|
26
26
|
import { ChartLayout } from '../private/chart-layout';
|
|
27
27
|
import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
|
|
@@ -358,12 +358,7 @@ const PieChartInternal = ( {
|
|
|
358
358
|
: 0;
|
|
359
359
|
|
|
360
360
|
return (
|
|
361
|
-
<
|
|
362
|
-
ref={ containerRef }
|
|
363
|
-
align="center"
|
|
364
|
-
justify="center"
|
|
365
|
-
className={ styles[ 'pie-chart__centering' ] }
|
|
366
|
-
>
|
|
361
|
+
<Center ref={ containerRef }>
|
|
367
362
|
<svg
|
|
368
363
|
viewBox={ `0 0 ${ width } ${ height }` }
|
|
369
364
|
preserveAspectRatio="xMidYMid meet"
|
|
@@ -493,7 +488,7 @@ const PieChartInternal = ( {
|
|
|
493
488
|
{ ! allSegmentsHidden && svgChildren }
|
|
494
489
|
</Group>
|
|
495
490
|
</svg>
|
|
496
|
-
</
|
|
491
|
+
</Center>
|
|
497
492
|
);
|
|
498
493
|
} }
|
|
499
494
|
</ChartLayout>
|
|
@@ -3,7 +3,6 @@ import { Pie } from '@visx/shape';
|
|
|
3
3
|
import { Text } from '@visx/text';
|
|
4
4
|
import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
|
|
5
5
|
import { __ } from '@wordpress/i18n';
|
|
6
|
-
import { Stack } from '@wordpress/ui';
|
|
7
6
|
import clsx from 'clsx';
|
|
8
7
|
import { useCallback, useContext, useMemo } from 'react';
|
|
9
8
|
import { Legend, useChartLegendItems } from '../../components/legend';
|
|
@@ -21,6 +20,7 @@ import {
|
|
|
21
20
|
GlobalChartsContext,
|
|
22
21
|
} from '../../providers';
|
|
23
22
|
import { attachSubComponents } from '../../utils';
|
|
23
|
+
import { Center } from '../private/center';
|
|
24
24
|
import { ChartSVG, ChartHTML, useChartChildren } from '../private/chart-composition';
|
|
25
25
|
import { ChartLayout } from '../private/chart-layout';
|
|
26
26
|
import { RadialWipeAnimation } from '../private/radial-wipe-animation';
|
|
@@ -397,12 +397,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
397
397
|
const innerRadius = radius * ( 1 - thickness );
|
|
398
398
|
|
|
399
399
|
return (
|
|
400
|
-
<
|
|
401
|
-
ref={ containerRef }
|
|
402
|
-
align="center"
|
|
403
|
-
justify="center"
|
|
404
|
-
className={ styles[ 'pie-semi-circle-chart__centering' ] }
|
|
405
|
-
>
|
|
400
|
+
<Center ref={ containerRef }>
|
|
406
401
|
<svg
|
|
407
402
|
width={ width }
|
|
408
403
|
height={ height }
|
|
@@ -491,7 +486,7 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
|
|
|
491
486
|
) }
|
|
492
487
|
</Group>
|
|
493
488
|
</svg>
|
|
494
|
-
</
|
|
489
|
+
</Center>
|
|
495
490
|
);
|
|
496
491
|
} }
|
|
497
492
|
</ChartLayout>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Stack } from '@wordpress/ui';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
|
4
|
+
import styles from './center.module.scss';
|
|
5
|
+
|
|
6
|
+
export type CenterProps = ComponentPropsWithoutRef< typeof Stack >;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Centers its children on both axes and fills its parent.
|
|
10
|
+
*
|
|
11
|
+
* A thin wrapper around `Stack` with `align="center"` and `justify="center"`
|
|
12
|
+
* defaults (both overridable) plus `width: 100%; height: 100%`. Reads more
|
|
13
|
+
* honestly than a `Stack` with both axes centered, and lets call sites drop
|
|
14
|
+
* ad-hoc `*__centering` classes. Forwards its ref and spreads remaining props
|
|
15
|
+
* onto the underlying `Stack`.
|
|
16
|
+
*
|
|
17
|
+
* @param props - Stack props; `align`/`justify` default to `"center"`.
|
|
18
|
+
* @param ref - Forwarded to the underlying element.
|
|
19
|
+
* @return The centered layout element.
|
|
20
|
+
*/
|
|
21
|
+
export const Center = forwardRef< HTMLDivElement, CenterProps >(
|
|
22
|
+
( { align = 'center', justify = 'center', className, ...props }, ref ) => (
|
|
23
|
+
<Stack
|
|
24
|
+
ref={ ref }
|
|
25
|
+
align={ align }
|
|
26
|
+
justify={ justify }
|
|
27
|
+
className={ clsx( styles.center, className ) }
|
|
28
|
+
{ ...props }
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
Center.displayName = 'Center';
|
|
@@ -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
|
} );
|