@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/index.cjs +347 -61
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +6 -4
  5. package/dist/index.d.cts +16 -0
  6. package/dist/index.d.ts +16 -0
  7. package/dist/index.js +348 -62
  8. package/dist/index.js.map +1 -1
  9. package/package.json +2 -2
  10. package/src/charts/bar-chart/bar-chart.tsx +210 -49
  11. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
  12. package/src/charts/bar-chart/private/comparison-bars.tsx +155 -0
  13. package/src/charts/bar-chart/private/comparison-constants.ts +33 -0
  14. package/src/charts/bar-chart/private/index.ts +9 -0
  15. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +47 -0
  16. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +183 -0
  17. package/src/charts/bar-chart/private/use-bar-chart-options.ts +49 -5
  18. package/src/charts/bar-chart/test/bar-chart.test.tsx +329 -1
  19. package/src/charts/geo-chart/geo-chart.tsx +5 -9
  20. package/src/charts/pie-chart/pie-chart.module.scss +0 -4
  21. package/src/charts/pie-chart/pie-chart.tsx +3 -8
  22. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +0 -5
  23. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +3 -8
  24. package/src/charts/private/center/center.module.scss +4 -0
  25. package/src/charts/private/center/center.tsx +33 -0
  26. package/src/charts/private/center/index.ts +2 -0
  27. package/src/charts/private/center/test/center.test.tsx +60 -0
  28. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +0 -2
  29. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +2 -4
  30. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  31. package/src/providers/chart-context/test/chart-context.test.tsx +13 -1
  32. package/src/providers/chart-context/themes.ts +8 -0
  33. package/src/providers/chart-context/types.ts +2 -0
  34. package/src/types.ts +16 -0
  35. package/src/utils/get-styles.ts +36 -13
  36. package/src/utils/index.ts +6 -1
  37. 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
- <Stack
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
- </Stack>
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
- <Stack
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
- </Stack>
163
+ </Center>
168
164
  );
169
165
  };
170
166
 
@@ -7,8 +7,4 @@
7
7
  width: 100%;
8
8
  }
9
9
 
10
- &__centering {
11
- width: 100%;
12
- height: 100%;
13
- }
14
10
  }
@@ -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
- <Stack
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
- </Stack>
491
+ </Center>
497
492
  );
498
493
  } }
499
494
  </ChartLayout>
@@ -5,11 +5,6 @@
5
5
  width: 100%;
6
6
  }
7
7
 
8
- &__centering {
9
- width: 100%;
10
- height: 100%;
11
- }
12
-
13
8
  .label {
14
9
  font-weight: var(--wpds-typography-font-weight-medium, 499);
15
10
  font-size: var(--wpds-typography-font-size-lg, 16px);
@@ -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
- <Stack
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
- </Stack>
489
+ </Center>
495
490
  );
496
491
  } }
497
492
  </ChartLayout>
@@ -0,0 +1,4 @@
1
+ .center {
2
+ width: 100%;
3
+ height: 100%;
4
+ }
@@ -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,2 @@
1
+ export { Center } from './center';
2
+ export type { CenterProps } from './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,7 +1,5 @@
1
1
  .svg-empty-state {
2
2
  text-align: center;
3
- width: 100%;
4
- height: 100%;
5
3
  font-size: var(--wpds-typography-font-size-md, 13px);
6
4
  color: var(--wpds-color-fg-content-neutral-weak, #6d6d6d);
7
5
  }
@@ -1,4 +1,4 @@
1
- import { Stack } from '@wordpress/ui';
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
- <Stack align="center" justify="center" className={ styles[ 'svg-empty-state' ] }>
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, not line 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' ] >;
@@ -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
- const itemShapeStyles = {
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
- // Return item shape styles if they are not empty
72
- if (
73
- Object.values( itemShapeStyles ).some(
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
  }
@@ -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 { getSeriesLineStyles, getSeriesStroke, getItemShapeStyles } from './get-styles';
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 { getSeriesLineStyles, getSeriesStroke, getItemShapeStyles } from '../get-styles';
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
  } );