@automattic/charts 1.5.3 → 1.7.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 (61) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -2
  3. package/dist/index.cjs +8981 -9569
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +318 -202
  6. package/dist/index.d.cts +1319 -1183
  7. package/dist/index.d.ts +1319 -1183
  8. package/dist/index.js +8561 -9570
  9. package/dist/index.js.map +1 -1
  10. package/dist/visx/group/index.cjs +8 -8
  11. package/dist/visx/group/index.d.cts +2 -3
  12. package/dist/visx/group/index.d.ts +2 -3
  13. package/dist/visx/group/index.js +1 -7
  14. package/dist/visx/legend/index.cjs +20 -10
  15. package/dist/visx/legend/index.d.cts +2 -1
  16. package/dist/visx/legend/index.d.ts +2 -1
  17. package/dist/visx/legend/index.js +2 -10
  18. package/dist/visx/text/index.cjs +20 -12
  19. package/dist/visx/text/index.d.cts +2 -1
  20. package/dist/visx/text/index.d.ts +2 -1
  21. package/dist/visx/text/index.js +2 -12
  22. package/package.json +32 -38
  23. package/src/charts/area-chart/types.ts +1 -1
  24. package/src/charts/bar-chart/bar-chart.tsx +1 -1
  25. package/src/charts/geo-chart/geo-chart.tsx +3 -1
  26. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +113 -1
  27. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +37 -6
  28. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +81 -0
  29. package/src/charts/line-chart/line-chart.tsx +1 -1
  30. package/src/charts/line-chart/private/line-chart-annotation-label-popover.tsx +18 -2
  31. package/src/charts/line-chart/private/line-chart-annotation.tsx +1 -1
  32. package/src/charts/line-chart/types.ts +1 -1
  33. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +2 -2
  34. package/src/charts/private/chart-layout/chart-layout.tsx +1 -2
  35. package/src/charts/private/x-zoom.tsx +2 -2
  36. package/src/components/legend/hooks/use-chart-legend-items.ts +6 -2
  37. package/src/components/legend/legend.tsx +1 -2
  38. package/src/components/legend/utils/label-transform-factory.ts +1 -1
  39. package/src/components/tooltip/accessible-tooltip.tsx +2 -6
  40. package/src/index.ts +6 -5
  41. package/src/providers/chart-context/themes.ts +1 -1
  42. package/src/providers/chart-context/types.ts +7 -2
  43. package/src/types.ts +118 -11
  44. package/src/utils/get-styles.ts +1 -2
  45. package/src/visx/types.ts +30 -1
  46. package/dist/chunk-5WRI5ZAA.js +0 -31
  47. package/dist/chunk-5WRI5ZAA.js.map +0 -1
  48. package/dist/chunk-7OZEQ5HE.cjs +0 -9
  49. package/dist/chunk-7OZEQ5HE.cjs.map +0 -1
  50. package/dist/chunk-DZUJEN5N.cjs +0 -31
  51. package/dist/chunk-DZUJEN5N.cjs.map +0 -1
  52. package/dist/chunk-NFRB2POF.js +0 -9
  53. package/dist/chunk-NFRB2POF.js.map +0 -1
  54. package/dist/index.css.map +0 -1
  55. package/dist/visx/group/index.cjs.map +0 -1
  56. package/dist/visx/group/index.js.map +0 -1
  57. package/dist/visx/legend/index.cjs.map +0 -1
  58. package/dist/visx/legend/index.js.map +0 -1
  59. package/dist/visx/text/index.cjs.map +0 -1
  60. package/dist/visx/text/index.js.map +0 -1
  61. package/tsup.config.ts +0 -46
@@ -1,8 +1,8 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});require('../../chunk-DZUJEN5N.cjs');
2
-
3
- // src/visx/group/index.ts
4
- var _group = require('@visx/group');
5
-
6
-
7
- exports.Group = _group.Group;
8
- //# sourceMappingURL=index.cjs.map
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _visx_group = require("@visx/group");
3
+ Object.defineProperty(exports, "Group", {
4
+ enumerable: true,
5
+ get: function() {
6
+ return _visx_group.Group;
7
+ }
8
+ });
@@ -1,3 +1,2 @@
1
- export { Group } from '@visx/group';
2
- import '@visx/legend';
3
- import '@visx/text';
1
+ import { Group } from "@visx/group";
2
+ export { Group };
@@ -1,3 +1,2 @@
1
- export { Group } from '@visx/group';
2
- import '@visx/legend';
3
- import '@visx/text';
1
+ import { Group } from "@visx/group";
2
+ export { Group };
@@ -1,8 +1,2 @@
1
- import "../../chunk-5WRI5ZAA.js";
2
-
3
- // src/visx/group/index.ts
4
1
  import { Group } from "@visx/group";
5
- export {
6
- Group
7
- };
8
- //# sourceMappingURL=index.js.map
2
+ export { Group };
@@ -1,10 +1,20 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});require('../../chunk-DZUJEN5N.cjs');
2
-
3
- // src/visx/legend/index.ts
4
- var _legend = require('@visx/legend');
5
-
6
-
7
-
8
-
9
- exports.CircleShape = _legend.CircleShape; exports.LineShape = _legend.LineShape; exports.RectShape = _legend.RectShape;
10
- //# sourceMappingURL=index.cjs.map
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _visx_legend = require("@visx/legend");
3
+ Object.defineProperty(exports, "CircleShape", {
4
+ enumerable: true,
5
+ get: function() {
6
+ return _visx_legend.CircleShape;
7
+ }
8
+ });
9
+ Object.defineProperty(exports, "LineShape", {
10
+ enumerable: true,
11
+ get: function() {
12
+ return _visx_legend.LineShape;
13
+ }
14
+ });
15
+ Object.defineProperty(exports, "RectShape", {
16
+ enumerable: true,
17
+ get: function() {
18
+ return _visx_legend.RectShape;
19
+ }
20
+ });
@@ -1 +1,2 @@
1
- export { CircleShape, LineShape, RectShape } from '@visx/legend';
1
+ import { CircleShape, LineShape, RectShape } from "@visx/legend";
2
+ export { CircleShape, LineShape, RectShape };
@@ -1 +1,2 @@
1
- export { CircleShape, LineShape, RectShape } from '@visx/legend';
1
+ import { CircleShape, LineShape, RectShape } from "@visx/legend";
2
+ export { CircleShape, LineShape, RectShape };
@@ -1,10 +1,2 @@
1
- import "../../chunk-5WRI5ZAA.js";
2
-
3
- // src/visx/legend/index.ts
4
- import { LineShape, CircleShape, RectShape } from "@visx/legend";
5
- export {
6
- CircleShape,
7
- LineShape,
8
- RectShape
9
- };
10
- //# sourceMappingURL=index.js.map
1
+ import { CircleShape, LineShape, RectShape } from "@visx/legend";
2
+ export { CircleShape, LineShape, RectShape };
@@ -1,12 +1,20 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
-
3
-
4
-
5
- var _chunk7OZEQ5HEcjs = require('../../chunk-7OZEQ5HE.cjs');
6
- require('../../chunk-DZUJEN5N.cjs');
7
-
8
-
9
-
10
-
11
- exports.Text = _chunk7OZEQ5HEcjs.Text; exports.getStringWidth = _chunk7OZEQ5HEcjs.getStringWidth; exports.useText = _chunk7OZEQ5HEcjs.useText;
12
- //# sourceMappingURL=index.cjs.map
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _visx_text = require("@visx/text");
3
+ Object.defineProperty(exports, "Text", {
4
+ enumerable: true,
5
+ get: function() {
6
+ return _visx_text.Text;
7
+ }
8
+ });
9
+ Object.defineProperty(exports, "getStringWidth", {
10
+ enumerable: true,
11
+ get: function() {
12
+ return _visx_text.getStringWidth;
13
+ }
14
+ });
15
+ Object.defineProperty(exports, "useText", {
16
+ enumerable: true,
17
+ get: function() {
18
+ return _visx_text.useText;
19
+ }
20
+ });
@@ -1 +1,2 @@
1
- export { Text, getStringWidth, useText } from '@visx/text';
1
+ import { Text, getStringWidth, useText } from "@visx/text";
2
+ export { Text, getStringWidth, useText };
@@ -1 +1,2 @@
1
- export { Text, getStringWidth, useText } from '@visx/text';
1
+ import { Text, getStringWidth, useText } from "@visx/text";
2
+ export { Text, getStringWidth, useText };
@@ -1,12 +1,2 @@
1
- import {
2
- Text,
3
- getStringWidth,
4
- useText
5
- } from "../../chunk-NFRB2POF.js";
6
- import "../../chunk-5WRI5ZAA.js";
7
- export {
8
- Text,
9
- getStringWidth,
10
- useText
11
- };
12
- //# sourceMappingURL=index.js.map
1
+ import { Text, getStringWidth, useText } from "@visx/text";
2
+ export { Text, getStringWidth, useText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "1.5.3",
3
+ "version": "1.7.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -56,35 +56,35 @@
56
56
  },
57
57
  "scripts": {
58
58
  "build": "pnpm run build:prod",
59
- "build:prod": "tsup",
59
+ "build:prod": "tsdown",
60
60
  "storybook": "cd ../storybook && pnpm run storybook:dev",
61
61
  "test": "TZ=UTC jest --config=tests/jest.config.cjs",
62
62
  "test-coverage": "pnpm run test --coverage",
63
63
  "typecheck": "tsgo --noEmit"
64
64
  },
65
65
  "dependencies": {
66
- "@automattic/number-formatters": "^1.2.2",
67
- "@babel/runtime": "7.29.2",
68
- "@react-spring/web": "9.7.5",
69
- "@visx/annotation": "^3.12.0",
70
- "@visx/axis": "^3.12.0",
71
- "@visx/curve": "^3.12.0",
72
- "@visx/event": "^3.12.0",
73
- "@visx/gradient": "^3.12.0",
74
- "@visx/grid": "^3.12.0",
75
- "@visx/group": "^3.12.0",
76
- "@visx/legend": "^3.12.0",
77
- "@visx/pattern": "^3.12.0",
78
- "@visx/responsive": "^3.12.0",
79
- "@visx/scale": "^3.12.0",
80
- "@visx/shape": "^3.12.0",
81
- "@visx/text": "^3.12.0",
82
- "@visx/tooltip": "^3.12.0",
83
- "@visx/vendor": "^3.12.0",
84
- "@visx/xychart": "^3.12.0",
66
+ "@automattic/number-formatters": "^1.2.4",
67
+ "@babel/runtime": "7.29.7",
68
+ "@react-spring/web": "10.1.1",
69
+ "@visx/annotation": "^4.0.0",
70
+ "@visx/axis": "^4.0.0",
71
+ "@visx/curve": "^4.0.0",
72
+ "@visx/event": "^4.0.0",
73
+ "@visx/gradient": "^4.0.0",
74
+ "@visx/grid": "^4.0.0",
75
+ "@visx/group": "^4.0.0",
76
+ "@visx/legend": "^4.0.0",
77
+ "@visx/pattern": "^4.0.0",
78
+ "@visx/responsive": "^4.0.0",
79
+ "@visx/scale": "^4.0.0",
80
+ "@visx/shape": "^4.0.0",
81
+ "@visx/text": "^4.0.0",
82
+ "@visx/tooltip": "^4.0.0",
83
+ "@visx/vendor": "^4.0.0",
84
+ "@visx/xychart": "^4.0.0",
85
85
  "@wordpress/i18n": "^6.0.0",
86
86
  "@wordpress/icons": "^13.0.0",
87
- "@wordpress/theme": "0.15.0",
87
+ "@wordpress/theme": "0.15.1",
88
88
  "@wordpress/ui": "0.13.0",
89
89
  "clsx": "2.1.1",
90
90
  "date-fns": "^4.1.0",
@@ -96,43 +96,37 @@
96
96
  },
97
97
  "devDependencies": {
98
98
  "@automattic/jetpack-webpack-config": "workspace:*",
99
- "@babel/core": "7.29.0",
100
- "@babel/preset-react": "7.28.5",
101
- "@babel/preset-typescript": "7.28.5",
99
+ "@babel/core": "^7.29.7",
102
100
  "@storybook/addon-docs": "10.3.6",
103
101
  "@storybook/react": "10.3.6",
104
102
  "@testing-library/dom": "^10.0.0",
105
103
  "@testing-library/jest-dom": "^6.0.0",
106
104
  "@testing-library/react": "^16.0.0",
107
105
  "@testing-library/user-event": "^14.4.3",
106
+ "@tsdown/css": "0.22.2",
108
107
  "@types/jest": "^30.0.0",
109
108
  "@types/react": "18.3.28",
110
109
  "@types/react-dom": "18.3.7",
111
110
  "@typescript/native-preview": "7.0.0-dev.20260225.1",
112
- "@visx/glyph": "3.12.0",
113
- "@wordpress/components": "35.0.0",
114
- "@wordpress/element": "8.0.0",
115
- "@wordpress/private-apis": "1.48.0",
111
+ "@visx/glyph": "4.0.0",
112
+ "@wordpress/components": "35.0.1",
113
+ "@wordpress/element": "8.0.1",
114
+ "@wordpress/private-apis": "1.48.1",
116
115
  "babel-jest": "30.4.1",
117
- "babel-plugin-react-remove-properties": "^0.3.1",
118
- "esbuild": "0.27.4",
119
- "esbuild-plugin-babel": "^0.2.3",
120
- "esbuild-sass-plugin": "^3.1.0",
121
116
  "identity-obj-proxy": "^3.0.0",
122
117
  "jest": "30.4.2",
123
118
  "jest-extended": "7.0.0",
124
- "postcss": "8.5.14",
125
- "postcss-modules": "6.0.1",
119
+ "magic-string": "0.30.21",
126
120
  "react": "18.3.1",
127
121
  "react-dom": "18.3.1",
128
122
  "sass-embedded": "1.97.3",
129
123
  "storybook": "10.3.6",
130
- "tsup": "8.5.1",
124
+ "tsdown": "0.22.2",
131
125
  "typescript": "5.9.3"
132
126
  },
133
127
  "peerDependencies": {
134
- "react": "^17.0.0 || ^18.0.0",
135
- "react-dom": "^17.0.0 || ^18.0.0"
128
+ "react": "^18.0.0 || ^19.0.0",
129
+ "react-dom": "^18.0.0 || ^19.0.0"
136
130
  },
137
131
  "engines": {
138
132
  "node": ">=20.11.0"
@@ -1,6 +1,6 @@
1
1
  import type { BaseChartProps, DataPointDate, SeriesData } from '../../types';
2
+ import type { RenderTooltipParams } from '../../visx/types';
2
3
  import type { CurveType } from '../line-chart/types';
3
- import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
4
4
  import type { ReactNode } from 'react';
5
5
 
6
6
  export interface AreaChartProps extends BaseChartProps< SeriesData[] > {
@@ -29,8 +29,8 @@ import { withResponsive } from '../private/with-responsive';
29
29
  import styles from './bar-chart.module.scss';
30
30
  import { useBarChartOptions } from './private';
31
31
  import type { BaseChartProps, DataPointDate, SeriesData, Optional } from '../../types';
32
+ import type { RenderTooltipParams } from '../../visx/types';
32
33
  import type { ResponsiveConfig } from '../private/with-responsive';
33
- import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
34
34
  import type { FC, ReactNode, ComponentType } from 'react';
35
35
 
36
36
  export interface BarChartProps extends BaseChartProps< SeriesData[] > {
@@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n';
5
5
  import { Stack } from '@wordpress/ui';
6
6
  import clsx from 'clsx';
7
7
  import { FC, useContext, useMemo } from 'react';
8
- import { Chart, type GoogleChartOptions } from 'react-google-charts';
8
+ import { Chart } from 'react-google-charts';
9
9
  /**
10
10
  * Internal dependencies
11
11
  */
@@ -20,6 +20,8 @@ import { GeoChartProps } from './types';
20
20
  const DEFAULT_FEATURE_FILL_COLOR = '#ffffff';
21
21
  const DEFAULT_BACKGROUND_COLOR = '#ffffff';
22
22
 
23
+ type GoogleChartOptions = Record< string, unknown >;
24
+
23
25
  /**
24
26
  * Renders a geographical chart using Google Charts GeoChart to visualize data.
25
27
  *
@@ -31,7 +31,7 @@
31
31
  &.is-overlay {
32
32
  grid-template: "overlap" 1fr / 1fr;
33
33
 
34
- /* contains a single cell callded 'overlap' */
34
+ /* contains a single cell called 'overlap' */
35
35
 
36
36
  >* {
37
37
  grid-area: overlap;
@@ -45,6 +45,9 @@
45
45
  .bar {
46
46
  height: 100%;
47
47
  min-height: 6px;
48
+ // Keeps a sliver of bar visible for tiny shares, and stops the interactive
49
+ // row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
50
+ min-width: var(--wpds-dimension-size-5xs, 4px);
48
51
 
49
52
  border-radius: var(--a8c--charts--leaderboard--bar--border-radius, 9999px);
50
53
  z-index: -1; /* places it behind the label */
@@ -80,3 +83,112 @@
80
83
  font-size: var(--wpds-typography-font-size-md, 13px);
81
84
  font-style: italic;
82
85
  }
86
+
87
+ .interactiveRow {
88
+ --focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
89
+
90
+ display: grid;
91
+ grid-column: 1 / -1;
92
+ grid-template-columns: subgrid;
93
+ align-items: center;
94
+ position: relative;
95
+
96
+ // Native button reset.
97
+ box-sizing: border-box;
98
+ appearance: none;
99
+ width: 100%;
100
+ margin: 0;
101
+ // Reserve a ring's worth of space on every edge so the inset focus outline
102
+ // hugs the row without the bar painting over it (see :focus-visible below).
103
+ padding: var(--focus-ring-width);
104
+ border: 0;
105
+ background: none;
106
+ color: inherit;
107
+ font: inherit;
108
+ text-align: inherit;
109
+ cursor: pointer;
110
+
111
+ // On hover/focus the bar dims and its inline-end edge pulls back while the
112
+ // value/delta slides toward the inline start to clear room for the chevron
113
+ // revealed at the inline-end edge. The value always slides the full inset
114
+ // (it must clear the chevron), but the bar pulls back in proportion to its
115
+ // length (see getBarWidth in leaderboard-chart.tsx): the full-length bar —
116
+ // the one that reaches the value — pulls back the whole inset to keep its
117
+ // gap, while shorter bars move proportionally less, down to ~0. The bar is
118
+ // a grid item aligned to the inline start, so its inset mirrors in RTL; the
119
+ // value slide is given an RTL override below. No row background.
120
+ .bar {
121
+ transition: opacity 0.15s ease, width 0.15s ease;
122
+ }
123
+
124
+ .valueContainer {
125
+ transition: transform 0.15s ease;
126
+ }
127
+
128
+ &:hover,
129
+ &:focus-visible {
130
+
131
+ // Fixed reserve for the chevron — the value slides by it in full, and the
132
+ // bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
133
+ --a8c--charts--leaderboard--bar--hover-inset: var(--wpds-dimension-gap-2xl, 32px);
134
+
135
+ .bar {
136
+ opacity: 0.5;
137
+ }
138
+
139
+ .valueContainer {
140
+ transform: translateX(calc(var(--a8c--charts--leaderboard--bar--hover-inset) * -1));
141
+ }
142
+
143
+ .chevron {
144
+ opacity: 1;
145
+ }
146
+ }
147
+
148
+ // In RTL the value slides toward the inline start the other way (rightward).
149
+ [dir="rtl"] &:hover .valueContainer,
150
+ [dir="rtl"] &:focus-visible .valueContainer {
151
+ transform: translateX(var(--a8c--charts--leaderboard--bar--hover-inset));
152
+ }
153
+
154
+ &:focus-visible {
155
+ border-radius: var(--wpds-border-radius-sm, 2px);
156
+ outline:
157
+ var(--focus-ring-width) solid
158
+ var(--wpds-color-stroke-focus-brand, var(--wp-admin-theme-color, #3858e9));
159
+ // Inset by its own width so the outer edge hugs the row boundary: the
160
+ // outline then sits in the row's padding (also a ring width), clear of the
161
+ // bar, and stays inside the overflow:auto container that would clip an
162
+ // outset ring.
163
+ outline-offset: calc(-1 * var(--focus-ring-width));
164
+ }
165
+ }
166
+
167
+ .chevron {
168
+ position: absolute;
169
+ inset-inline-end: var(--wpds-dimension-padding-xs, 4px);
170
+ top: 0;
171
+ bottom: 0;
172
+ margin-block: auto;
173
+ opacity: 0;
174
+ color: var(--wpds-color-fg-content-neutral-weak, #707070);
175
+ transition: opacity 0.15s ease;
176
+ pointer-events: none;
177
+
178
+ // The glyph points toward the inline end, so flip it to point left in RTL.
179
+ [dir="rtl"] & {
180
+ transform: scaleX(-1);
181
+ }
182
+ }
183
+
184
+ @media ( prefers-reduced-motion: reduce ) {
185
+
186
+ .interactiveRow {
187
+
188
+ .bar,
189
+ .valueContainer,
190
+ .chevron {
191
+ transition: none;
192
+ }
193
+ }
194
+ }
@@ -2,6 +2,7 @@
2
2
  import { __experimentalGrid as Grid } from '@wordpress/components';
3
3
  import { Fragment } from '@wordpress/element';
4
4
  import { __ } from '@wordpress/i18n';
5
+ import { Icon, chevronRight } from '@wordpress/icons';
5
6
  import { Stack, Text } from '@wordpress/ui';
6
7
  import clsx from 'clsx';
7
8
  import { useContext, useMemo, type FC } from 'react';
@@ -51,7 +52,20 @@ const defaultDeltaFormatter = ( value: number ): string => {
51
52
  } );
52
53
  };
53
54
 
54
- const BarLabel = ( { label }: { label: string | JSX.Element } ) => (
55
+ /**
56
+ * Build a bar's width. A hover-inset CSS variable (0 by default) is subtracted
57
+ * on hover, scaled by the bar's share so the pull-back is proportional to its
58
+ * length: the full-length (100%) bar — the one that reaches the value — pulls
59
+ * back the whole inset to keep its gap with the value, while shorter bars pull
60
+ * back proportionally less, down to ~0 for a very short bar.
61
+ *
62
+ * @param share - The bar's share of the row width, as a percentage.
63
+ * @return A CSS width value.
64
+ */
65
+ const getBarWidth = ( share: number ): string =>
66
+ `calc(${ share }% - var(--a8c--charts--leaderboard--bar--hover-inset, 0px) * ${ share } / 100)`;
67
+
68
+ const BarLabel = ( { label }: { label: LeaderboardEntry[ 'label' ] } ) => (
55
69
  <>{ typeof label === 'string' ? <Text className={ styles.label }>{ label }</Text> : label }</>
56
70
  );
57
71
 
@@ -87,7 +101,7 @@ const BarWithLabel = ( {
87
101
  [ styles[ 'bar--animated' ] ]: animation,
88
102
  } ) }
89
103
  style={ {
90
- width: entry.currentShare + '%',
104
+ width: getBarWidth( entry.currentShare ),
91
105
  backgroundColor: primaryColor,
92
106
  } }
93
107
  ></div>
@@ -99,7 +113,7 @@ const BarWithLabel = ( {
99
113
  [ styles[ 'bar--animated' ] ]: animation,
100
114
  } ) }
101
115
  style={ {
102
- width: entry.previousShare + '%',
116
+ width: getBarWidth( entry.previousShare ),
103
117
  backgroundColor: secondaryColor,
104
118
  } }
105
119
  ></div>
@@ -324,8 +338,8 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
324
338
  const colorIndex = Math.sign( entry.delta ) + 1;
325
339
  const deltaColor = deltaColors[ colorIndex ];
326
340
 
327
- return (
328
- <Fragment key={ entry.id }>
341
+ const rowCells = (
342
+ <>
329
343
  <Stack direction="column" gap={ labelSpacing }>
330
344
  <BarWithLabel
331
345
  entry={ entry }
@@ -354,8 +368,25 @@ const LeaderboardChartInternal: FC< LeaderboardChartProps > = ( {
354
368
  </Text>
355
369
  ) }
356
370
  </Stack>
357
- </Fragment>
371
+ </>
358
372
  );
373
+
374
+ if ( entry.onClick ) {
375
+ return (
376
+ <button
377
+ key={ entry.id }
378
+ type="button"
379
+ className={ styles.interactiveRow }
380
+ onClick={ entry.onClick }
381
+ aria-label={ entry.ariaLabel }
382
+ >
383
+ { rowCells }
384
+ <Icon className={ styles.chevron } icon={ chevronRight } size={ 24 } />
385
+ </button>
386
+ );
387
+ }
388
+
389
+ return <Fragment key={ entry.id }>{ rowCells }</Fragment>;
359
390
  } ) }
360
391
  </Grid>
361
392
  ) }
@@ -1,4 +1,5 @@
1
1
  import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import LeaderboardChart from '../leaderboard-chart';
3
4
  import type { LeaderboardEntry } from '../../../types';
4
5
 
@@ -367,4 +368,84 @@ describe( 'LeaderboardChart', () => {
367
368
  expect( chartContainer ).toHaveStyle( { width: '500px', height: '240px' } );
368
369
  } );
369
370
  } );
371
+
372
+ describe( 'Interactive items', () => {
373
+ it( 'renders a button for an entry with onClick', () => {
374
+ render( <LeaderboardChart data={ [ { ...mockData[ 0 ], onClick: jest.fn() } ] } /> );
375
+ expect( screen.getByRole( 'button' ) ).toBeInTheDocument();
376
+ expect( screen.getByRole( 'button' ).tagName ).toBe( 'BUTTON' );
377
+ } );
378
+
379
+ it( 'calls onClick when the row is clicked', async () => {
380
+ const user = userEvent.setup();
381
+ const onClick = jest.fn();
382
+ render( <LeaderboardChart data={ [ { ...mockData[ 0 ], onClick } ] } /> );
383
+ await user.click( screen.getByRole( 'button' ) );
384
+ expect( onClick ).toHaveBeenCalledTimes( 1 );
385
+ } );
386
+
387
+ it( 'activates onClick via the keyboard (Enter and Space)', async () => {
388
+ const user = userEvent.setup();
389
+ const onClick = jest.fn();
390
+ render( <LeaderboardChart data={ [ { ...mockData[ 0 ], onClick } ] } /> );
391
+
392
+ await user.tab();
393
+ expect( screen.getByRole( 'button' ) ).toHaveFocus();
394
+
395
+ await user.keyboard( '[Enter]' );
396
+ await user.keyboard( '[Space]' );
397
+ expect( onClick ).toHaveBeenCalledTimes( 2 );
398
+ } );
399
+
400
+ it( 'gives the interactive row an accessible name from the label and value', () => {
401
+ render( <LeaderboardChart data={ [ { ...mockData[ 0 ], onClick: jest.fn() } ] } /> );
402
+ // mockData[0] label is 'Direct', currentValue 12500 → '12.5K'
403
+ expect( screen.getByRole( 'button' ) ).toHaveAccessibleName( /Direct.*12\.5K/ );
404
+ } );
405
+
406
+ it( 'derives the accessible name from an image label via its alt text', () => {
407
+ render(
408
+ <LeaderboardChart
409
+ data={ [
410
+ {
411
+ ...mockData[ 0 ],
412
+ label: <img src="https://example.com/flag.svg" alt="United States" />,
413
+ onClick: jest.fn(),
414
+ },
415
+ ] }
416
+ />
417
+ );
418
+ expect( screen.getByRole( 'button' ) ).toHaveAccessibleName( /United States.*12\.5K/ );
419
+ } );
420
+
421
+ it( 'uses ariaLabel as the accessible name when provided', () => {
422
+ render(
423
+ <LeaderboardChart
424
+ data={ [
425
+ {
426
+ ...mockData[ 0 ],
427
+ label: <img src="https://example.com/flag.svg" alt="" />,
428
+ ariaLabel: 'United States: 12.5K visitors',
429
+ onClick: jest.fn(),
430
+ },
431
+ ] }
432
+ />
433
+ );
434
+ expect( screen.getByRole( 'button' ) ).toHaveAccessibleName(
435
+ 'United States: 12.5K visitors'
436
+ );
437
+ } );
438
+
439
+ it( 'does not render a button for entries without onClick', () => {
440
+ render( <LeaderboardChart data={ [ mockData[ 0 ] ] } /> );
441
+ expect( screen.queryByRole( 'button' ) ).not.toBeInTheDocument();
442
+ } );
443
+
444
+ it( 'renders a button only for interactive entries in mixed data', () => {
445
+ render(
446
+ <LeaderboardChart data={ [ { ...mockData[ 0 ], onClick: jest.fn() }, mockData[ 1 ] ] } />
447
+ );
448
+ expect( screen.getAllByRole( 'button' ) ).toHaveLength( 1 );
449
+ } );
450
+ } );
370
451
  } );
@@ -42,10 +42,10 @@ import styles from './line-chart.module.scss';
42
42
  import { LineChartAnnotation, LineChartAnnotationsOverlay, LineChartGlyph } from './private';
43
43
  import type { RenderLineGlyphProps, LineChartProps, TooltipDatum } from './types';
44
44
  import type { DataPoint, DataPointDate, SeriesData, Optional } from '../../types';
45
+ import type { RenderTooltipParams } from '../../visx/types';
45
46
  import type { ResponsiveConfig } from '../private/with-responsive';
46
47
  import type { TickFormatter } from '@visx/axis';
47
48
  import type { GlyphProps } from '@visx/xychart';
48
- import type { RenderTooltipParams } from '@visx/xychart/lib/components/Tooltip';
49
49
  import type { FC, Ref } from 'react';
50
50
 
51
51
  const defaultRenderGlyph = < Datum extends object >( props: RenderLineGlyphProps< Datum > ) => {