@automattic/charts 1.6.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -2
  3. package/dist/index.cjs +380 -60
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +12 -7
  6. package/dist/index.d.cts +119 -30
  7. package/dist/index.d.ts +122 -33
  8. package/dist/index.js +380 -60
  9. package/dist/index.js.map +1 -1
  10. package/package.json +22 -21
  11. package/src/charts/area-chart/types.ts +1 -1
  12. package/src/charts/bar-chart/bar-chart.tsx +197 -43
  13. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +70 -0
  14. package/src/charts/bar-chart/private/comparison-bars.tsx +154 -0
  15. package/src/charts/bar-chart/private/comparison-constants.ts +33 -0
  16. package/src/charts/bar-chart/private/index.ts +9 -0
  17. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +47 -0
  18. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +183 -0
  19. package/src/charts/bar-chart/private/use-bar-chart-options.ts +46 -5
  20. package/src/charts/bar-chart/test/bar-chart.test.tsx +191 -1
  21. package/src/charts/geo-chart/geo-chart.tsx +8 -10
  22. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +27 -14
  23. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +7 -4
  24. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +51 -4
  25. package/src/charts/line-chart/line-chart.tsx +1 -1
  26. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +18 -2
  27. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  28. package/src/charts/line-chart/types.ts +1 -1
  29. package/src/charts/pie-chart/pie-chart.module.scss +0 -4
  30. package/src/charts/pie-chart/pie-chart.tsx +3 -8
  31. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +0 -5
  32. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +5 -10
  33. package/src/charts/private/center/center.module.scss +4 -0
  34. package/src/charts/private/center/center.tsx +33 -0
  35. package/src/charts/private/center/index.ts +2 -0
  36. package/src/charts/private/center/test/center.test.tsx +60 -0
  37. package/src/charts/private/chart-layout/chart-layout.tsx +1 -2
  38. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +0 -2
  39. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +2 -4
  40. package/src/charts/private/x-zoom.tsx +2 -2
  41. package/src/components/legend/hooks/use-chart-legend-items.ts +6 -2
  42. package/src/components/legend/legend.tsx +1 -2
  43. package/src/components/legend/utils/label-transform-factory.ts +1 -1
  44. package/src/components/tooltip/accessible-tooltip.tsx +2 -6
  45. package/src/index.ts +6 -5
  46. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  47. package/src/providers/chart-context/test/chart-context.test.tsx +13 -1
  48. package/src/providers/chart-context/themes.ts +9 -1
  49. package/src/providers/chart-context/types.ts +9 -2
  50. package/src/types.ts +120 -11
  51. package/src/utils/get-styles.ts +36 -14
  52. package/src/utils/index.ts +6 -1
  53. package/src/utils/test/get-styles.test.ts +47 -1
  54. package/src/visx/types.ts +30 -1
package/src/types.ts CHANGED
@@ -1,20 +1,102 @@
1
- import type { CircleSubjectProps } from '@visx/annotation/lib/components/CircleSubject';
2
- import type { ConnectorProps } from '@visx/annotation/lib/components/Connector';
3
- import type { LabelProps } from '@visx/annotation/lib/components/Label';
4
- import type { LineSubjectProps } from '@visx/annotation/lib/components/LineSubject';
1
+ import type {
2
+ CircleSubjectProps,
3
+ ConnectorProps,
4
+ LabelProps,
5
+ LineSubjectProps,
6
+ } from '@visx/annotation';
5
7
  import type { AxisScale, Orientation, TickFormatter, AxisRendererProps } from '@visx/axis';
6
- import type { LegendShape } from '@visx/legend/lib/types';
7
8
  import type { ScaleInput, ScaleType } from '@visx/scale';
8
- import type { TextProps } from '@visx/text/lib/Text';
9
+ import type { TextProps } from '@visx/text';
9
10
  import type { EventHandlerParams, GlyphProps, GridStyles, LineStyles } from '@visx/xychart';
10
- import type { GapSize } from '@wordpress/theme';
11
- import type { CSSProperties, MouseEvent, PointerEvent, ReactNode } from 'react';
12
- import type { GoogleDataTableColumn, GoogleDataTableRow } from 'react-google-charts';
11
+ import type {
12
+ ComponentClass,
13
+ CSSProperties,
14
+ FC,
15
+ MouseEvent,
16
+ PointerEvent,
17
+ ReactElement,
18
+ ReactNode,
19
+ } from 'react';
13
20
 
14
21
  type ValueOf< T > = T[ keyof T ];
15
22
 
16
23
  export type Optional< T, K extends keyof T > = Pick< Partial< T >, K > & Omit< T, K >;
17
24
 
25
+ /**
26
+ * Mirrors the WordPress Design System gap token scale used by the WordPress UI Stack.
27
+ */
28
+ export type GapSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
29
+
30
+ export type LegendShapeLabel< Data, Output, ExtraAttributes = object > = {
31
+ datum: Data;
32
+ index: number;
33
+ text: string;
34
+ value?: Output;
35
+ } & ExtraAttributes;
36
+
37
+ export type LegendShapeRenderProps< Data, Output > = {
38
+ width?: string | number;
39
+ height?: string | number;
40
+ label: LegendShapeLabel< Data, Output >;
41
+ item: Data;
42
+ itemIndex: number;
43
+ fill?: string;
44
+ size?: string | number;
45
+ style?: CSSProperties;
46
+ };
47
+
48
+ export type LegendShape< Data, Output > =
49
+ | 'rect'
50
+ | 'circle'
51
+ | 'line'
52
+ | FC< LegendShapeRenderProps< Data, Output > >
53
+ | ComponentClass< LegendShapeRenderProps< Data, Output > >;
54
+
55
+ export type GoogleDataTableColumnType =
56
+ | 'string'
57
+ | 'number'
58
+ | 'boolean'
59
+ | 'date'
60
+ | 'datetime'
61
+ | 'timeofday';
62
+
63
+ export enum GoogleDataTableColumnRoleType {
64
+ annotation = 'annotation',
65
+ annotationText = 'annotationText',
66
+ certainty = 'certainty',
67
+ emphasis = 'emphasis',
68
+ interval = 'interval',
69
+ scope = 'scope',
70
+ style = 'style',
71
+ tooltip = 'tooltip',
72
+ domain = 'domain',
73
+ }
74
+
75
+ export type GoogleDataTableColumn =
76
+ | {
77
+ type: GoogleDataTableColumnType;
78
+ label?: string;
79
+ role?: GoogleDataTableColumnRoleType;
80
+ pattern?: string;
81
+ p?: Record< string, unknown >;
82
+ id?: string;
83
+ }
84
+ | string;
85
+
86
+ export type GoogleDataTableCell =
87
+ | {
88
+ v?: unknown;
89
+ f?: string;
90
+ p?: Record< string, unknown >;
91
+ }
92
+ | string
93
+ | number
94
+ | boolean
95
+ | Date
96
+ | null;
97
+
98
+ export type GoogleDataTableRow = GoogleDataTableCell[];
99
+
18
100
  export type ChartType =
19
101
  | 'area'
20
102
  | 'bar'
@@ -101,7 +183,7 @@ export type LeaderboardEntry = {
101
183
  /**
102
184
  * Human-readable name (e.g., 'Direct') or a JSX element (e.g., <h4>Direct</h4>)
103
185
  */
104
- label: string | JSX.Element;
186
+ label: string | ReactElement;
105
187
 
106
188
  /**
107
189
  * Value of the entry
@@ -146,6 +228,17 @@ export type LeaderboardEntry = {
146
228
  * never both, since interactive elements cannot be nested in HTML.
147
229
  */
148
230
  onClick?: ( event: MouseEvent< HTMLButtonElement > ) => void;
231
+
232
+ /**
233
+ * Optional accessible name for the interactive row's `<button>`. Only applies
234
+ * when `onClick` is set — without it the row renders as a Fragment with no
235
+ * element to receive `aria-label`. By default the button derives its name from
236
+ * its rendered content (label text plus the formatted value), which is the
237
+ * right outcome for plain-text labels. Set this when the `label` is JSX whose
238
+ * text content does not yield a clean name on its own — e.g. an image-only
239
+ * label — to give assistive tech a deterministic, human-readable name.
240
+ */
241
+ ariaLabel?: string;
149
242
  };
150
243
 
151
244
  export type GradientStop = {
@@ -175,6 +268,16 @@ export type SeriesData = {
175
268
  options?: SeriesDataOptions;
176
269
  };
177
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
+
178
281
  export type MultipleDataPointsDate = {
179
282
  label: string;
180
283
  data: DataPointDate[];
@@ -273,7 +376,7 @@ export type ChartTheme = {
273
376
  /** Gap between columns in the leaderboard grid */
274
377
  columnGap?: number;
275
378
  /** Spacing between label and progress bars */
276
- labelSpacing?: number;
379
+ labelSpacing?: GapSize;
277
380
  /** Primary color for current period bars */
278
381
  primaryColor?: string;
279
382
  /** Secondary color for comparison period bars */
@@ -295,6 +398,9 @@ export type ChartTheme = {
295
398
  lineChart?: {
296
399
  lineStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles > >;
297
400
  };
401
+ barChart?: {
402
+ barStyles?: Partial< Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles > >;
403
+ };
298
404
  /** Sparkline specific settings */
299
405
  sparkline?: {
300
406
  /** Margin around the sparkline chart */
@@ -327,6 +433,9 @@ export type CompleteChartTheme = Required< ChartTheme > & {
327
433
  lineChart: {
328
434
  lineStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, LineStyles >;
329
435
  };
436
+ barChart: {
437
+ barStyles: Record< NonNullable< SeriesDataOptions[ 'type' ] >, BarStyles >;
438
+ };
330
439
  legend: Required< NonNullable< ChartTheme[ 'legend' ] > >;
331
440
  sparkline: Required< NonNullable< ChartTheme[ 'sparkline' ] > > & {
332
441
  margin: Required< NonNullable< ChartTheme[ 'sparkline' ] >[ 'margin' ] >;
@@ -1,5 +1,4 @@
1
- import type { ChartTheme, SeriesData } from '../types';
2
- import type { LegendShape } from '@visx/legend/lib/types';
1
+ import type { BarStyles, ChartTheme, LegendShape, SeriesData } from '../types';
3
2
  import type { LineStyles } from '@visx/xychart';
4
3
 
5
4
  /**
@@ -29,6 +28,25 @@ export function getSeriesLineStyles(
29
28
  );
30
29
  }
31
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
+
32
50
  /**
33
51
  * Utility function to get stroke color for a series
34
52
  *
@@ -62,22 +80,26 @@ export function getItemShapeStyles(
62
80
  ): Record< string, unknown > {
63
81
  const seriesShapeStyles = series.options?.legendShapeStyle ?? {};
64
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 } : {};
65
89
  const themeShapeStyles = theme.legend?.shapeStyles?.[ index ];
66
90
 
67
- 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 = {
68
94
  ...seriesShapeStyles,
69
95
  ...lineStyles,
70
96
  };
97
+ const hasExplicitStyles = Object.values( explicitStyles ).some(
98
+ value => value !== undefined && value !== null && value !== ''
99
+ );
100
+ const baseShapeStyles = hasExplicitStyles ? explicitStyles : themeShapeStyles ?? {};
71
101
 
72
- // Return item shape styles if they are not empty
73
- if (
74
- Object.values( itemShapeStyles ).some(
75
- value => value !== undefined && value !== null && value !== ''
76
- )
77
- ) {
78
- return itemShapeStyles;
79
- }
80
-
81
- // Fallback to theme shape styles if defined
82
- 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 };
83
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
  } );
package/src/visx/types.ts CHANGED
@@ -1,2 +1,31 @@
1
- export type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
1
+ import type { PickD3Scale } from '@visx/scale';
2
+ import type { TooltipProps as VisxTooltipProps, UseTooltipPortalOptions } from '@visx/tooltip';
3
+ import type { GlyphProps, TooltipContextType } from '@visx/xychart';
4
+ import type { ReactNode, SVGProps } from 'react';
5
+
6
+ export type RenderTooltipParams< Datum extends object > = TooltipContextType< Datum > & {
7
+ colorScale?: PickD3Scale< 'ordinal', string, string >;
8
+ };
9
+
10
+ export interface RenderTooltipGlyphProps< Datum extends object > extends GlyphProps< Datum > {
11
+ glyphStyle?: SVGProps< SVGCircleElement >;
12
+ isNearestDatum: boolean;
13
+ }
14
+
15
+ export type XyChartTooltipProps< Datum extends object > = {
16
+ renderTooltip: ( params: RenderTooltipParams< Datum > ) => ReactNode;
17
+ renderGlyph?: ( params: RenderTooltipGlyphProps< Datum > ) => ReactNode;
18
+ snapTooltipToDatumX?: boolean;
19
+ snapTooltipToDatumY?: boolean;
20
+ showVerticalCrosshair?: boolean;
21
+ showHorizontalCrosshair?: boolean;
22
+ showDatumGlyph?: boolean;
23
+ showSeriesGlyphs?: boolean;
24
+ verticalCrosshairStyle?: SVGProps< SVGLineElement >;
25
+ horizontalCrosshairStyle?: SVGProps< SVGLineElement >;
26
+ glyphStyle?: SVGProps< SVGCircleElement >;
27
+ resizeObserverPolyfill?: UseTooltipPortalOptions[ 'polyfill' ];
28
+ } & Omit< VisxTooltipProps, 'left' | 'top' | 'children' > &
29
+ Pick< UseTooltipPortalOptions, 'debounce' | 'detectBounds' | 'scroll' | 'zIndex' >;
30
+
2
31
  export type { LineStyles, GridStyles } from '@visx/xychart';