@automattic/charts 0.57.0 → 0.59.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 (267) hide show
  1. package/CHANGELOG.md +36 -2
  2. package/README.md +7 -54
  3. package/dist/index.cjs +9607 -21
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +32 -49
  6. package/dist/index.css.map +1 -1
  7. package/dist/index.d.cts +1612 -33
  8. package/dist/index.d.ts +1612 -33
  9. package/dist/index.js +9640 -54
  10. package/dist/index.js.map +1 -1
  11. package/package.json +9 -126
  12. package/src/charts/bar-chart/bar-chart.module.scss +0 -5
  13. package/src/charts/bar-chart/bar-chart.tsx +142 -149
  14. package/src/charts/bar-chart/test/bar-chart.test.tsx +48 -31
  15. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +54 -74
  16. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +4 -5
  17. package/src/charts/leaderboard-chart/types.ts +1 -11
  18. package/src/charts/line-chart/line-chart.module.scss +0 -5
  19. package/src/charts/line-chart/line-chart.tsx +202 -193
  20. package/src/charts/line-chart/private/line-chart-annotations-overlay.tsx +1 -2
  21. package/src/charts/line-chart/test/line-chart.test.tsx +49 -27
  22. package/src/charts/line-chart/types.ts +0 -1
  23. package/src/charts/pie-chart/pie-chart.module.scss +2 -10
  24. package/src/charts/pie-chart/pie-chart.tsx +212 -212
  25. package/src/charts/pie-chart/test/composition-api.test.tsx +44 -3
  26. package/src/charts/pie-chart/test/pie-chart.test.tsx +51 -44
  27. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +2 -8
  28. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +166 -168
  29. package/src/charts/pie-semi-circle-chart/test/pie-semi-circle-chart.test.tsx +58 -30
  30. package/src/charts/private/chart-composition/index.ts +2 -0
  31. package/src/charts/private/chart-composition/render-legend-slot.ts +22 -0
  32. package/src/charts/private/chart-composition/test/render-legend-slot.test.tsx +60 -0
  33. package/src/charts/private/chart-composition/test/use-chart-children.test.tsx +91 -0
  34. package/src/charts/private/chart-composition/use-chart-children.ts +34 -2
  35. package/src/charts/private/chart-layout/chart-layout.module.scss +7 -0
  36. package/src/charts/private/chart-layout/chart-layout.tsx +106 -0
  37. package/src/charts/private/chart-layout/index.ts +2 -0
  38. package/src/charts/private/chart-layout/test/chart-layout.test.tsx +167 -0
  39. package/src/charts/private/single-chart-context/single-chart-context.tsx +2 -2
  40. package/src/charts/private/svg-empty-state/index.ts +1 -0
  41. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +7 -0
  42. package/src/charts/private/svg-empty-state/svg-empty-state.tsx +40 -0
  43. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +12 -8
  44. package/src/components/legend/hooks/use-chart-legend-items.ts +12 -13
  45. package/src/components/legend/index.ts +1 -8
  46. package/src/components/legend/legend.tsx +33 -8
  47. package/src/components/legend/private/base-legend.module.scss +19 -37
  48. package/src/components/legend/private/base-legend.tsx +0 -2
  49. package/src/components/legend/test/legend.test.tsx +93 -1
  50. package/src/components/legend/types.ts +7 -34
  51. package/src/hooks/index.ts +1 -1
  52. package/src/hooks/use-data-with-percentages.ts +24 -0
  53. package/src/hooks/use-interactive-legend-data.ts +18 -15
  54. package/src/index.ts +66 -9
  55. package/src/providers/chart-context/global-charts-provider.tsx +7 -1
  56. package/src/providers/chart-context/hooks/use-chart-registration.ts +2 -1
  57. package/src/providers/chart-context/types.ts +2 -2
  58. package/src/types.ts +110 -45
  59. package/src/utils/date-parsing.ts +10 -1
  60. package/src/utils/test/date-parsing.test.ts +12 -0
  61. package/src/utils/test/resolve-css-var.test.ts +4 -2
  62. package/tsup.config.ts +1 -1
  63. package/dist/base-tooltip-DOq93wjU.d.cts +0 -38
  64. package/dist/base-tooltip-DOq93wjU.d.ts +0 -38
  65. package/dist/charts/bar-chart/index.cjs +0 -15
  66. package/dist/charts/bar-chart/index.cjs.map +0 -1
  67. package/dist/charts/bar-chart/index.css +0 -153
  68. package/dist/charts/bar-chart/index.css.map +0 -1
  69. package/dist/charts/bar-chart/index.d.cts +0 -37
  70. package/dist/charts/bar-chart/index.d.ts +0 -37
  71. package/dist/charts/bar-chart/index.js +0 -15
  72. package/dist/charts/bar-chart/index.js.map +0 -1
  73. package/dist/charts/bar-list-chart/index.cjs +0 -16
  74. package/dist/charts/bar-list-chart/index.cjs.map +0 -1
  75. package/dist/charts/bar-list-chart/index.css +0 -153
  76. package/dist/charts/bar-list-chart/index.css.map +0 -1
  77. package/dist/charts/bar-list-chart/index.d.cts +0 -92
  78. package/dist/charts/bar-list-chart/index.d.ts +0 -92
  79. package/dist/charts/bar-list-chart/index.js +0 -16
  80. package/dist/charts/bar-list-chart/index.js.map +0 -1
  81. package/dist/charts/conversion-funnel-chart/index.cjs +0 -11
  82. package/dist/charts/conversion-funnel-chart/index.cjs.map +0 -1
  83. package/dist/charts/conversion-funnel-chart/index.css +0 -251
  84. package/dist/charts/conversion-funnel-chart/index.css.map +0 -1
  85. package/dist/charts/conversion-funnel-chart/index.d.cts +0 -97
  86. package/dist/charts/conversion-funnel-chart/index.d.ts +0 -97
  87. package/dist/charts/conversion-funnel-chart/index.js +0 -11
  88. package/dist/charts/conversion-funnel-chart/index.js.map +0 -1
  89. package/dist/charts/geo-chart/index.cjs +0 -13
  90. package/dist/charts/geo-chart/index.cjs.map +0 -1
  91. package/dist/charts/geo-chart/index.css +0 -117
  92. package/dist/charts/geo-chart/index.css.map +0 -1
  93. package/dist/charts/geo-chart/index.d.cts +0 -67
  94. package/dist/charts/geo-chart/index.d.ts +0 -67
  95. package/dist/charts/geo-chart/index.js +0 -13
  96. package/dist/charts/geo-chart/index.js.map +0 -1
  97. package/dist/charts/leaderboard-chart/index.cjs +0 -20
  98. package/dist/charts/leaderboard-chart/index.cjs.map +0 -1
  99. package/dist/charts/leaderboard-chart/index.css +0 -172
  100. package/dist/charts/leaderboard-chart/index.css.map +0 -1
  101. package/dist/charts/leaderboard-chart/index.d.cts +0 -46
  102. package/dist/charts/leaderboard-chart/index.d.ts +0 -46
  103. package/dist/charts/leaderboard-chart/index.js +0 -20
  104. package/dist/charts/leaderboard-chart/index.js.map +0 -1
  105. package/dist/charts/line-chart/index.cjs +0 -15
  106. package/dist/charts/line-chart/index.cjs.map +0 -1
  107. package/dist/charts/line-chart/index.css +0 -225
  108. package/dist/charts/line-chart/index.css.map +0 -1
  109. package/dist/charts/line-chart/index.d.cts +0 -99
  110. package/dist/charts/line-chart/index.d.ts +0 -99
  111. package/dist/charts/line-chart/index.js +0 -15
  112. package/dist/charts/line-chart/index.js.map +0 -1
  113. package/dist/charts/pie-chart/index.cjs +0 -18
  114. package/dist/charts/pie-chart/index.cjs.map +0 -1
  115. package/dist/charts/pie-chart/index.css +0 -143
  116. package/dist/charts/pie-chart/index.css.map +0 -1
  117. package/dist/charts/pie-chart/index.d.cts +0 -97
  118. package/dist/charts/pie-chart/index.d.ts +0 -97
  119. package/dist/charts/pie-chart/index.js +0 -18
  120. package/dist/charts/pie-chart/index.js.map +0 -1
  121. package/dist/charts/pie-semi-circle-chart/index.cjs +0 -17
  122. package/dist/charts/pie-semi-circle-chart/index.cjs.map +0 -1
  123. package/dist/charts/pie-semi-circle-chart/index.css +0 -144
  124. package/dist/charts/pie-semi-circle-chart/index.css.map +0 -1
  125. package/dist/charts/pie-semi-circle-chart/index.d.cts +0 -94
  126. package/dist/charts/pie-semi-circle-chart/index.d.ts +0 -94
  127. package/dist/charts/pie-semi-circle-chart/index.js +0 -17
  128. package/dist/charts/pie-semi-circle-chart/index.js.map +0 -1
  129. package/dist/charts/sparkline/index.cjs +0 -16
  130. package/dist/charts/sparkline/index.cjs.map +0 -1
  131. package/dist/charts/sparkline/index.css +0 -242
  132. package/dist/charts/sparkline/index.css.map +0 -1
  133. package/dist/charts/sparkline/index.d.cts +0 -113
  134. package/dist/charts/sparkline/index.d.ts +0 -113
  135. package/dist/charts/sparkline/index.js +0 -16
  136. package/dist/charts/sparkline/index.js.map +0 -1
  137. package/dist/chunk-2A34OA5O.cjs +0 -51
  138. package/dist/chunk-2A34OA5O.cjs.map +0 -1
  139. package/dist/chunk-2NCY7R4G.js +0 -3897
  140. package/dist/chunk-2NCY7R4G.js.map +0 -1
  141. package/dist/chunk-32DH6JDF.js +0 -1263
  142. package/dist/chunk-32DH6JDF.js.map +0 -1
  143. package/dist/chunk-4OPFE4RM.js +0 -614
  144. package/dist/chunk-4OPFE4RM.js.map +0 -1
  145. package/dist/chunk-6CCZL2JJ.js +0 -63
  146. package/dist/chunk-6CCZL2JJ.js.map +0 -1
  147. package/dist/chunk-77OKCVQN.cjs +0 -421
  148. package/dist/chunk-77OKCVQN.cjs.map +0 -1
  149. package/dist/chunk-7FQX4ALL.cjs +0 -219
  150. package/dist/chunk-7FQX4ALL.cjs.map +0 -1
  151. package/dist/chunk-ASLARV7L.cjs +0 -81
  152. package/dist/chunk-ASLARV7L.cjs.map +0 -1
  153. package/dist/chunk-BCX5THDQ.js +0 -403
  154. package/dist/chunk-BCX5THDQ.js.map +0 -1
  155. package/dist/chunk-BPYKWMI7.js +0 -194
  156. package/dist/chunk-BPYKWMI7.js.map +0 -1
  157. package/dist/chunk-CZGYJKG6.js +0 -421
  158. package/dist/chunk-CZGYJKG6.js.map +0 -1
  159. package/dist/chunk-D2UH4CFE.cjs +0 -120
  160. package/dist/chunk-D2UH4CFE.cjs.map +0 -1
  161. package/dist/chunk-DAU3HNEG.js +0 -344
  162. package/dist/chunk-DAU3HNEG.js.map +0 -1
  163. package/dist/chunk-H2V4JMSA.js +0 -219
  164. package/dist/chunk-H2V4JMSA.js.map +0 -1
  165. package/dist/chunk-I2276W3I.cjs +0 -66
  166. package/dist/chunk-I2276W3I.cjs.map +0 -1
  167. package/dist/chunk-I35UYJJR.cjs +0 -468
  168. package/dist/chunk-I35UYJJR.cjs.map +0 -1
  169. package/dist/chunk-IU4DYUAV.js +0 -120
  170. package/dist/chunk-IU4DYUAV.js.map +0 -1
  171. package/dist/chunk-KXRWNFQJ.js +0 -51
  172. package/dist/chunk-KXRWNFQJ.js.map +0 -1
  173. package/dist/chunk-OP6PHB2U.js +0 -81
  174. package/dist/chunk-OP6PHB2U.js.map +0 -1
  175. package/dist/chunk-PXLEMUGJ.js +0 -165
  176. package/dist/chunk-PXLEMUGJ.js.map +0 -1
  177. package/dist/chunk-RCY6XLGU.cjs +0 -63
  178. package/dist/chunk-RCY6XLGU.cjs.map +0 -1
  179. package/dist/chunk-RHHVEJHJ.cjs +0 -1263
  180. package/dist/chunk-RHHVEJHJ.cjs.map +0 -1
  181. package/dist/chunk-TO3OQBXG.cjs +0 -165
  182. package/dist/chunk-TO3OQBXG.cjs.map +0 -1
  183. package/dist/chunk-V36ERY7Y.js +0 -375
  184. package/dist/chunk-V36ERY7Y.js.map +0 -1
  185. package/dist/chunk-VJM5XCB4.cjs +0 -614
  186. package/dist/chunk-VJM5XCB4.cjs.map +0 -1
  187. package/dist/chunk-VTS3PNMS.cjs +0 -344
  188. package/dist/chunk-VTS3PNMS.cjs.map +0 -1
  189. package/dist/chunk-WLODYNLB.js +0 -1067
  190. package/dist/chunk-WLODYNLB.js.map +0 -1
  191. package/dist/chunk-XKRJL2QT.cjs +0 -375
  192. package/dist/chunk-XKRJL2QT.cjs.map +0 -1
  193. package/dist/chunk-XWYZIFZW.js +0 -66
  194. package/dist/chunk-XWYZIFZW.js.map +0 -1
  195. package/dist/chunk-Y3NNQMAX.cjs +0 -194
  196. package/dist/chunk-Y3NNQMAX.cjs.map +0 -1
  197. package/dist/chunk-YE2T52VZ.cjs +0 -1067
  198. package/dist/chunk-YE2T52VZ.cjs.map +0 -1
  199. package/dist/chunk-Z26M4V2M.js +0 -468
  200. package/dist/chunk-Z26M4V2M.js.map +0 -1
  201. package/dist/chunk-Z45KX47P.cjs +0 -3897
  202. package/dist/chunk-Z45KX47P.cjs.map +0 -1
  203. package/dist/chunk-ZH4F5RMG.cjs +0 -403
  204. package/dist/chunk-ZH4F5RMG.cjs.map +0 -1
  205. package/dist/components/legend/index.cjs +0 -11
  206. package/dist/components/legend/index.cjs.map +0 -1
  207. package/dist/components/legend/index.css +0 -103
  208. package/dist/components/legend/index.css.map +0 -1
  209. package/dist/components/legend/index.d.cts +0 -37
  210. package/dist/components/legend/index.d.ts +0 -37
  211. package/dist/components/legend/index.js +0 -11
  212. package/dist/components/legend/index.js.map +0 -1
  213. package/dist/components/tooltip/index.cjs +0 -12
  214. package/dist/components/tooltip/index.cjs.map +0 -1
  215. package/dist/components/tooltip/index.css +0 -13
  216. package/dist/components/tooltip/index.css.map +0 -1
  217. package/dist/components/tooltip/index.d.cts +0 -71
  218. package/dist/components/tooltip/index.d.ts +0 -71
  219. package/dist/components/tooltip/index.js +0 -12
  220. package/dist/components/tooltip/index.js.map +0 -1
  221. package/dist/components/trend-indicator/index.cjs +0 -8
  222. package/dist/components/trend-indicator/index.cjs.map +0 -1
  223. package/dist/components/trend-indicator/index.css +0 -27
  224. package/dist/components/trend-indicator/index.css.map +0 -1
  225. package/dist/components/trend-indicator/index.d.cts +0 -44
  226. package/dist/components/trend-indicator/index.d.ts +0 -44
  227. package/dist/components/trend-indicator/index.js +0 -8
  228. package/dist/components/trend-indicator/index.js.map +0 -1
  229. package/dist/format-metric-value-MXm5DtQ_.d.cts +0 -24
  230. package/dist/format-metric-value-MXm5DtQ_.d.ts +0 -24
  231. package/dist/hooks/index.cjs +0 -31
  232. package/dist/hooks/index.cjs.map +0 -1
  233. package/dist/hooks/index.css +0 -103
  234. package/dist/hooks/index.css.map +0 -1
  235. package/dist/hooks/index.d.cts +0 -272
  236. package/dist/hooks/index.d.ts +0 -272
  237. package/dist/hooks/index.js +0 -31
  238. package/dist/hooks/index.js.map +0 -1
  239. package/dist/leaderboard-chart-BKYYXcg2.d.ts +0 -83
  240. package/dist/leaderboard-chart-DR7CGb0L.d.cts +0 -83
  241. package/dist/legend-C2grwnWk.d.cts +0 -9
  242. package/dist/legend-Cj0xM5dU.d.ts +0 -9
  243. package/dist/providers/index.cjs +0 -21
  244. package/dist/providers/index.cjs.map +0 -1
  245. package/dist/providers/index.css +0 -103
  246. package/dist/providers/index.css.map +0 -1
  247. package/dist/providers/index.d.cts +0 -28
  248. package/dist/providers/index.d.ts +0 -28
  249. package/dist/providers/index.js +0 -21
  250. package/dist/providers/index.js.map +0 -1
  251. package/dist/themes-BmVGrYnF.d.ts +0 -67
  252. package/dist/themes-CyjKm-P_.d.cts +0 -67
  253. package/dist/types-CuUEszrM.d.ts +0 -19
  254. package/dist/types-DZordNiO.d.cts +0 -505
  255. package/dist/types-DZordNiO.d.ts +0 -505
  256. package/dist/types-I67mddpr.d.cts +0 -78
  257. package/dist/types-I67mddpr.d.ts +0 -78
  258. package/dist/types-KtOPPzPX.d.cts +0 -19
  259. package/dist/utils/index.cjs +0 -44
  260. package/dist/utils/index.cjs.map +0 -1
  261. package/dist/utils/index.d.cts +0 -226
  262. package/dist/utils/index.d.ts +0 -226
  263. package/dist/utils/index.js +0 -44
  264. package/dist/utils/index.js.map +0 -1
  265. package/dist/with-responsive-CNfhzAUu.d.cts +0 -18
  266. package/dist/with-responsive-CNfhzAUu.d.ts +0 -18
  267. package/src/hooks/use-has-legend-child.ts +0 -22
@@ -1,7 +1,7 @@
1
1
  import { renderHook } from '@testing-library/react';
2
2
  import { GlobalChartsProvider } from '../../../../providers';
3
3
  import { useChartLegendItems } from '../use-chart-legend-items';
4
- import type { DataPointPercentage, DataPointDate, SeriesData } from '../../../../types';
4
+ import type { DataPointPercentageCalculated, DataPointDate, SeriesData } from '../../../../types';
5
5
  import type { ReactNode } from 'react';
6
6
 
7
7
  // Wrapper component to provide GlobalChartsProvider context
@@ -11,8 +11,8 @@ const wrapper = ( { children }: { children: ReactNode } ) => (
11
11
 
12
12
  describe( 'useChartLegendItems', () => {
13
13
  describe( 'Number Formatting (i18n)', () => {
14
- describe( 'DataPointPercentage', () => {
15
- const percentageData: DataPointPercentage[] = [
14
+ describe( 'DataPointPercentageCalculated', () => {
15
+ const percentageData: DataPointPercentageCalculated[] = [
16
16
  { label: 'Item 1', value: 80000, percentage: 60.6 },
17
17
  { label: 'Item 2', value: 30000, percentage: 22.7 },
18
18
  { label: 'Item 3', value: 22000, percentage: 16.7 },
@@ -50,7 +50,7 @@ describe( 'useChartLegendItems', () => {
50
50
  } );
51
51
 
52
52
  test( 'uses valueDisplay when provided, falling back to formatted value', () => {
53
- const dataWithDisplay: DataPointPercentage[] = [
53
+ const dataWithDisplay: DataPointPercentageCalculated[] = [
54
54
  { label: 'Item 1', value: 80000, percentage: 60, valueDisplay: 'Custom 80K' },
55
55
  { label: 'Item 2', value: 30000, percentage: 30 },
56
56
  ];
@@ -194,7 +194,9 @@ describe( 'useChartLegendItems', () => {
194
194
  } );
195
195
 
196
196
  test( 'handles data with zero values', () => {
197
- const zeroData: DataPointPercentage[] = [ { label: 'Zero Value', value: 0, percentage: 0 } ];
197
+ const zeroData: DataPointPercentageCalculated[] = [
198
+ { label: 'Zero Value', value: 0, percentage: 0 },
199
+ ];
198
200
 
199
201
  const { result } = renderHook(
200
202
  () =>
@@ -209,7 +211,7 @@ describe( 'useChartLegendItems', () => {
209
211
  } );
210
212
 
211
213
  test( 'handles very large numbers', () => {
212
- const largeData: DataPointPercentage[] = [
214
+ const largeData: DataPointPercentageCalculated[] = [
213
215
  { label: 'Large', value: 1234567890, percentage: 100 },
214
216
  ];
215
217
 
@@ -227,7 +229,7 @@ describe( 'useChartLegendItems', () => {
227
229
  } );
228
230
 
229
231
  test( 'handles decimal values', () => {
230
- const decimalData: DataPointPercentage[] = [
232
+ const decimalData: DataPointPercentageCalculated[] = [
231
233
  { label: 'Decimal', value: 1234.5678, percentage: 100 },
232
234
  ];
233
235
 
@@ -247,7 +249,9 @@ describe( 'useChartLegendItems', () => {
247
249
 
248
250
  describe( 'Label and Color', () => {
249
251
  test( 'preserves label and color from data', () => {
250
- const data: DataPointPercentage[] = [ { label: 'Test Label', value: 100, percentage: 100 } ];
252
+ const data: DataPointPercentageCalculated[] = [
253
+ { label: 'Test Label', value: 100, percentage: 100 },
254
+ ];
251
255
 
252
256
  const { result } = renderHook(
253
257
  () =>
@@ -6,7 +6,7 @@ import {
6
6
  type ElementStyles,
7
7
  } from '../../../providers';
8
8
  import { formatPercentage } from '../../../utils';
9
- import type { SeriesData, DataPointDate, DataPointPercentage } from '../../../types';
9
+ import type { SeriesData, DataPointDate, DataPointPercentageCalculated } from '../../../types';
10
10
  import type { BaseLegendItem } from '../types';
11
11
  import type { LegendShape } from '@visx/legend/lib/types';
12
12
  import type { GlyphProps } from '@visx/xychart';
@@ -31,7 +31,7 @@ export interface ChartLegendOptions {
31
31
  * @return Formatted value string
32
32
  */
33
33
  function formatPointValue(
34
- point: DataPointDate | DataPointPercentage,
34
+ point: DataPointDate | DataPointPercentageCalculated,
35
35
  showValues: boolean,
36
36
  legendValueDisplay: LegendValueDisplay = 'percentage'
37
37
  ): string {
@@ -39,16 +39,15 @@ function formatPointValue(
39
39
  return '';
40
40
  }
41
41
 
42
- // Handle DataPointPercentage (pie chart data)
42
+ // Handle DataPointPercentageCalculated (pie chart data with calculated percentage)
43
43
  if ( 'percentage' in point ) {
44
- const percentagePoint = point as DataPointPercentage;
45
44
  switch ( legendValueDisplay ) {
46
45
  case 'percentage':
47
- return formatPercentage( percentagePoint.percentage );
46
+ return formatPercentage( point.percentage );
48
47
  case 'value':
49
- return formatNumber( percentagePoint.value );
48
+ return formatNumber( point.value );
50
49
  case 'valueDisplay':
51
- return percentagePoint.valueDisplay || formatNumber( percentagePoint.value );
50
+ return point.valueDisplay || formatNumber( point.value );
52
51
  default:
53
52
  return '';
54
53
  }
@@ -145,7 +144,7 @@ function processSeriesData(
145
144
  * @return Array of processed legend items
146
145
  */
147
146
  function processPointData(
148
- pointData: ( DataPointDate | DataPointPercentage )[],
147
+ pointData: ( DataPointDate | DataPointPercentageCalculated )[],
149
148
  getElementStyles: ( params: GetElementStylesParams ) => ElementStyles,
150
149
  showValues: boolean,
151
150
  legendValueDisplay: LegendValueDisplay,
@@ -154,9 +153,9 @@ function processPointData(
154
153
  renderGlyph?: < Datum extends object >( props: GlyphProps< Datum > ) => ReactNode,
155
154
  legendShape?: LegendShape< SeriesData[], number >
156
155
  ): BaseLegendItem[] {
157
- const mapper = ( point: DataPointDate | DataPointPercentage, index: number ) => {
156
+ const mapper = ( point: DataPointDate | DataPointPercentageCalculated, index: number ) => {
158
157
  const { color, glyph, shapeStyles } = getElementStyles( {
159
- data: point as DataPointPercentage,
158
+ data: point as DataPointPercentageCalculated,
160
159
  index,
161
160
  legendShape,
162
161
  } );
@@ -182,7 +181,7 @@ function processPointData(
182
181
  * @return Array of legend items ready for display
183
182
  */
184
183
  export function useChartLegendItems<
185
- T extends SeriesData[] | DataPointDate[] | DataPointPercentage[],
184
+ T extends SeriesData[] | DataPointDate[] | DataPointPercentageCalculated[],
186
185
  >(
187
186
  data: T,
188
187
  options: ChartLegendOptions = {},
@@ -215,9 +214,9 @@ export function useChartLegendItems<
215
214
  );
216
215
  }
217
216
 
218
- // Handle DataPointDate or DataPointPercentage (single data points)
217
+ // Handle DataPointDate or DataPointPercentageCalculated (single data points)
219
218
  return processPointData(
220
- data as ( DataPointDate | DataPointPercentage )[],
219
+ data as ( DataPointDate | DataPointPercentageCalculated )[],
221
220
  getElementStyles,
222
221
  showValues,
223
222
  legendValueDisplay,
@@ -1,11 +1,4 @@
1
1
  export { Legend } from './legend';
2
2
  export { useChartLegendItems } from './hooks/use-chart-legend-items';
3
- export type {
4
- LegendProps,
5
- BaseLegendProps,
6
- BaseLegendItem,
7
- LegendItemStyles,
8
- LegendLabelStyles,
9
- LegendShapeStyles,
10
- } from './types';
3
+ export type { LegendProps, BaseLegendProps, BaseLegendItem } from './types';
11
4
  export type { ChartLegendOptions, LegendValueDisplay } from './hooks/use-chart-legend-items';
@@ -3,9 +3,21 @@ import { SingleChartContext } from '../../charts/private/single-chart-context';
3
3
  import { GlobalChartsContext } from '../../providers';
4
4
  import { BaseLegend } from './private';
5
5
  import type { LegendProps } from './types';
6
+ import type { ChartType } from '../../types';
7
+ import type { LegendShape } from '@visx/legend/lib/types';
8
+
9
+ const defaultShapeByChartType: Partial<
10
+ Record< ChartType, Extract< LegendShape< unknown, unknown >, string > >
11
+ > = {
12
+ line: 'line',
13
+ bar: 'rect',
14
+ pie: 'circle',
15
+ 'pie-semi-circle': 'circle',
16
+ leaderboard: 'circle',
17
+ };
6
18
 
7
19
  export const Legend = forwardRef< HTMLDivElement, LegendProps >(
8
- ( { chartId, items, ...props }, ref ) => {
20
+ ( { chartId, items, shape, ...props }, ref ) => {
9
21
  // Get context but don't throw if it doesn't exist
10
22
  const context = useContext( GlobalChartsContext );
11
23
  const singleChartContext = useContext( SingleChartContext );
@@ -14,12 +26,17 @@ export const Legend = forwardRef< HTMLDivElement, LegendProps >(
14
26
  // When chartId is not provided, we use the context's chartId, meaning it is in a single chart context
15
27
  const contextChartId = chartId ?? singleChartContext?.chartId;
16
28
 
17
- // Use useMemo to ensure re-rendering when context changes
18
- const contextItems = useMemo( () => {
19
- return contextChartId && context
20
- ? context.getChartData( contextChartId )?.legendItems
21
- : undefined;
22
- }, [ contextChartId, context ] );
29
+ const chartData = useMemo(
30
+ () => ( contextChartId && context ? context.getChartData( contextChartId ) : undefined ),
31
+ [ contextChartId, context ]
32
+ );
33
+
34
+ const contextItems = chartData?.legendItems;
35
+
36
+ // Derive the default legend shape from the chart type when no explicit shape is provided
37
+ const resolvedShape =
38
+ shape ??
39
+ ( chartData?.chartType ? defaultShapeByChartType[ chartData.chartType ] : undefined );
23
40
 
24
41
  // Provided items take precedence over context items
25
42
  const legendItems = ( items || contextItems ) as typeof items;
@@ -28,6 +45,14 @@ export const Legend = forwardRef< HTMLDivElement, LegendProps >(
28
45
  return null;
29
46
  }
30
47
 
31
- return <BaseLegend ref={ ref } items={ legendItems } { ...props } chartId={ contextChartId } />;
48
+ return (
49
+ <BaseLegend
50
+ ref={ ref }
51
+ items={ legendItems }
52
+ shape={ resolvedShape }
53
+ { ...props }
54
+ chartId={ contextChartId }
55
+ />
56
+ );
32
57
  }
33
58
  );
@@ -1,4 +1,5 @@
1
1
  .legend {
2
+ align-self: stretch;
2
3
 
3
4
  &--horizontal {
4
5
  display: flex;
@@ -11,52 +12,33 @@
11
12
  display: flex;
12
13
  flex-direction: column;
13
14
  gap: 8px;
14
-
15
- &.legend--alignment-start {
16
- align-items: flex-start;
17
- }
18
-
19
- &.legend--alignment-center {
20
- align-items: center;
21
- }
22
-
23
- &.legend--alignment-end {
24
- align-items: flex-end;
25
- }
26
15
  }
27
16
 
28
- // Position-based styles
29
- &--position-top {
30
- position: relative;
31
-
32
- &.legend--alignment-start {
33
- justify-content: flex-start;
34
- }
35
-
36
- &.legend--alignment-center {
37
- justify-content: center;
38
- }
17
+ &--alignment-start {
18
+ justify-content: flex-start;
19
+ }
39
20
 
40
- &.legend--alignment-end {
41
- justify-content: flex-end;
42
- }
21
+ &--alignment-center {
22
+ justify-content: center;
43
23
  }
44
24
 
45
- &--position-bottom {
46
- position: relative;
25
+ &--alignment-end {
26
+ justify-content: flex-end;
27
+ }
47
28
 
48
- &.legend--alignment-start {
49
- justify-content: flex-start;
50
- }
29
+ // Vertical legends align on the cross-axis instead
30
+ &--vertical.legend--alignment-start {
31
+ align-items: flex-start;
32
+ }
51
33
 
52
- &.legend--alignment-center {
53
- justify-content: center;
54
- }
34
+ &--vertical.legend--alignment-center {
35
+ align-items: center;
36
+ }
55
37
 
56
- &.legend--alignment-end {
57
- justify-content: flex-end;
58
- }
38
+ &--vertical.legend--alignment-end {
39
+ align-items: flex-end;
59
40
  }
41
+
60
42
  }
61
43
 
62
44
  .legend-item {
@@ -67,7 +67,6 @@ export const BaseLegend: ForwardRefExoticComponent<
67
67
  items,
68
68
  className,
69
69
  orientation = 'horizontal',
70
- position = 'bottom',
71
70
  alignment = 'center',
72
71
  shape = 'rect',
73
72
  fill = valueOrIdentityString,
@@ -177,7 +176,6 @@ export const BaseLegend: ForwardRefExoticComponent<
177
176
  styles.legend,
178
177
  styles[ `legend--${ orientation }` ],
179
178
  styles[ `legend--alignment-${ alignment }` ],
180
- styles[ `legend--position-${ position }` ],
181
179
  className
182
180
  ) }
183
181
  style={ {
@@ -1,8 +1,12 @@
1
1
  /* eslint-disable react/jsx-no-bind */
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
- import { GlobalChartsProvider } from '../../../providers';
4
+ import { useMemo } from 'react';
5
+ import { SingleChartContext } from '../../../charts/private/single-chart-context';
6
+ import { GlobalChartsProvider, useChartId, useChartRegistration } from '../../../providers';
7
+ import { Legend } from '../legend';
5
8
  import { BaseLegend } from '../private/base-legend';
9
+ import type { ChartType } from '../../../types';
6
10
  import type { LegendProps } from '../types';
7
11
 
8
12
  const TestShape: LegendProps[ 'shape' ] = props => {
@@ -425,6 +429,94 @@ describe( 'BaseLegend', () => {
425
429
  } );
426
430
  } );
427
431
 
432
+ describe( 'Legend shape defaults from chart type', () => {
433
+ const legendItems = [
434
+ { label: 'Series 1', color: '#ff0000' },
435
+ { label: 'Series 2', color: '#00ff00' },
436
+ ];
437
+
438
+ const CustomShape: LegendProps[ 'shape' ] = props => (
439
+ <span data-testid="custom-shape" style={ { color: props.fill as string } } />
440
+ );
441
+
442
+ const ChartRegistrar = ( {
443
+ chartType,
444
+ chartId,
445
+ }: {
446
+ chartType: ChartType;
447
+ chartId: string;
448
+ } ) => {
449
+ const resolvedId = useChartId( chartId );
450
+ const metadata = useMemo( () => ( {} ), [] );
451
+ useChartRegistration( {
452
+ chartId: resolvedId,
453
+ legendItems,
454
+ chartType,
455
+ isDataValid: true,
456
+ metadata,
457
+ } );
458
+ return null;
459
+ };
460
+
461
+ const renderLegendWithChartType = (
462
+ chartType: ChartType,
463
+ explicitShape?: LegendProps[ 'shape' ]
464
+ ) => {
465
+ const chartId = `test-${ chartType }`;
466
+
467
+ return render(
468
+ <GlobalChartsProvider>
469
+ <ChartRegistrar chartType={ chartType } chartId={ chartId } />
470
+ <SingleChartContext.Provider value={ { chartId } }>
471
+ <Legend shape={ explicitShape } />
472
+ </SingleChartContext.Provider>
473
+ </GlobalChartsProvider>
474
+ );
475
+ };
476
+
477
+ it( 'uses line shape for line chart type', () => {
478
+ renderLegendWithChartType( 'line' );
479
+ expect( screen.getByRole( 'list' ) ).toBeInTheDocument();
480
+ expect( screen.getAllByTestId( 'legend-item' ) ).toHaveLength( 2 );
481
+
482
+ const html = document.body.innerHTML;
483
+ expect( html ).toContain( '<line' );
484
+ } );
485
+
486
+ it( 'uses rect shape for bar chart type', () => {
487
+ renderLegendWithChartType( 'bar' );
488
+ expect( screen.getByRole( 'list' ) ).toBeInTheDocument();
489
+
490
+ // visx ShapeRect renders a <div> with inline background style inside
491
+ // .visx-legend-shape. No testids or roles on these elements, so direct
492
+ // node access is necessary.
493
+ // eslint-disable-next-line testing-library/no-node-access
494
+ const shapes = document.querySelectorAll( '.visx-legend-shape > div' );
495
+ expect( shapes ).toHaveLength( 2 );
496
+ shapes.forEach( shape => {
497
+ expect( ( shape as HTMLElement ).style.background ).toBeTruthy();
498
+ } );
499
+ } );
500
+
501
+ it( 'uses circle shape for pie chart type', () => {
502
+ renderLegendWithChartType( 'pie' );
503
+ expect( screen.getByRole( 'list' ) ).toBeInTheDocument();
504
+
505
+ const html = document.body.innerHTML;
506
+ expect( html ).toContain( '<circle' );
507
+ expect( html ).not.toContain( '<line' );
508
+ } );
509
+
510
+ it( 'allows explicit shape to override chart type default', () => {
511
+ renderLegendWithChartType( 'line', CustomShape );
512
+ expect( screen.getByRole( 'list' ) ).toBeInTheDocument();
513
+ expect( screen.getAllByTestId( 'custom-shape' ) ).toHaveLength( 2 );
514
+
515
+ const html = document.body.innerHTML;
516
+ expect( html ).not.toContain( '<line' );
517
+ } );
518
+ } );
519
+
428
520
  describe( 'Interactive legend', () => {
429
521
  it( 'renders interactive legend items with proper attributes', () => {
430
522
  render(
@@ -1,4 +1,10 @@
1
1
  import { LegendOrdinal } from '@visx/legend';
2
+ import type {
3
+ LegendItemStyles,
4
+ LegendLabelStyles,
5
+ LegendPosition,
6
+ LegendShapeStyles,
7
+ } from '../../types';
2
8
  import type { GlyphProps, LineStyles } from '@visx/xychart';
3
9
  import type { ComponentProps, CSSProperties, ReactNode } from 'react';
4
10
 
@@ -7,43 +13,10 @@ type VisxLegendProps = Pick<
7
13
  'className' | 'shape' | 'fill' | 'size' | 'labelFormat' | 'labelTransform'
8
14
  >;
9
15
 
10
- export type LegendItemStyles = {
11
- /** Margin around each legend item. */
12
- margin?: CSSProperties[ 'margin' ];
13
- /** Flex direction for items within each legend entry. */
14
- flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse';
15
- };
16
-
17
- export type LegendLabelStyles = Pick< CSSProperties, 'justifyContent' | 'flex' | 'margin' > & {
18
- /**
19
- * Maximum width for legend label text as a CSS value (e.g. '200px', '50%', '10rem').
20
- * When set, text overflow behavior is controlled by textOverflow.
21
- */
22
- maxWidth?: string;
23
- /**
24
- * Controls how text behaves when it exceeds maxWidth.
25
- * - 'ellipsis': Truncate with ellipsis (ideal for widgets/small devices)
26
- * - 'wrap': Wrap text to multiple lines (default, ideal for larger displays)
27
- */
28
- textOverflow?: 'ellipsis' | 'wrap';
29
- };
30
-
31
- export type LegendShapeStyles = {
32
- /** Width of the legend shape in pixels. */
33
- width?: number;
34
- /** Height of the legend shape in pixels. */
35
- height?: number;
36
- /** Margin around the legend shape. */
37
- margin?: CSSProperties[ 'margin' ];
38
- };
39
-
40
16
  export type BaseLegendProps = VisxLegendProps & {
41
17
  items: BaseLegendItem[];
42
18
  orientation?: 'horizontal' | 'vertical';
43
- /**
44
- * TODO: Add 'left' | 'right' positioning support in future implementation
45
- */
46
- position?: 'top' | 'bottom';
19
+ position?: LegendPosition;
47
20
  alignment?: 'start' | 'center' | 'end';
48
21
  /** Additional CSS class name for legend items. */
49
22
  itemClassName?: string;
@@ -4,9 +4,9 @@ export { useXYChartTheme } from './use-xychart-theme';
4
4
  export { useChartDataTransform } from './use-chart-data-transform';
5
5
  export { useChartMargin } from './use-chart-margin';
6
6
  export { useElementSize } from './use-element-size';
7
- export { useHasLegendChild } from './use-has-legend-child';
8
7
  export { useTextTruncation } from './use-text-truncation';
9
8
  export { useZeroValueDisplay } from './use-zero-value-display';
9
+ export { useDataWithPercentages } from './use-data-with-percentages';
10
10
  export { useInteractiveLegendData } from './use-interactive-legend-data';
11
11
  export { usePrefersReducedMotion } from './use-prefers-reduced-motion';
12
12
  export { useTooltipPortalRelocator } from './use-tooltip-portal-relocator';
@@ -0,0 +1,24 @@
1
+ import { useMemo } from 'react';
2
+
3
+ interface DataPointWithValue {
4
+ value: number;
5
+ }
6
+
7
+ /**
8
+ * Hook to calculate percentages from values for chart data.
9
+ * Ensures percentages are always derived from values (single source of truth).
10
+ *
11
+ * @param data - Array of data points with values
12
+ * @return Data with calculated percentages
13
+ */
14
+ export const useDataWithPercentages = < T extends DataPointWithValue >(
15
+ data: T[]
16
+ ): ( T & { percentage: number } )[] => {
17
+ return useMemo( () => {
18
+ const totalValue = data.reduce( ( sum, segment ) => sum + segment.value, 0 );
19
+ return data.map( segment => ( {
20
+ ...segment,
21
+ percentage: totalValue > 0 ? ( segment.value / totalValue ) * 100 : 0,
22
+ } ) );
23
+ }, [ data ] );
24
+ };
@@ -2,7 +2,8 @@ import { useMemo } from 'react';
2
2
 
3
3
  /**
4
4
  * Data point interface for charts with interactive legends.
5
- * Requires label for series identification, value for calculations, and percentage for display.
5
+ * Requires label for series identification, value for calculations,
6
+ * and percentage (should be pre-calculated by the chart component).
6
7
  */
7
8
  interface DataPointWithPercentage {
8
9
  label: string;
@@ -14,7 +15,7 @@ interface DataPointWithPercentage {
14
15
  * Parameters for the useInteractiveLegendData hook.
15
16
  */
16
17
  interface UseInteractiveLegendDataParams< T extends DataPointWithPercentage > {
17
- /** The chart data to filter based on legend visibility */
18
+ /** The chart data with pre-calculated percentages */
18
19
  data: T[];
19
20
  /** Unique chart identifier, required for interactive legends */
20
21
  chartId: string | undefined;
@@ -33,9 +34,9 @@ interface UseInteractiveLegendDataResult< T extends DataPointWithPercentage > {
33
34
  /** Boolean indicating if all segments are hidden */
34
35
  allSegmentsHidden: boolean;
35
36
  /**
36
- * Legend data with recalculated percentages for visible items.
37
- * Uses original data for hidden items, but shows recalculated percentages for visible ones.
38
- * This ensures the legend displays accurate percentages while maintaining all entries.
37
+ * Legend data with stable percentage formatting.
38
+ * Hidden items keep their original percentage.
39
+ * Visible items show recalculated percentages that total 100%.
39
40
  */
40
41
  legendData: T[];
41
42
  }
@@ -85,8 +86,9 @@ export const useInteractiveLegendData = < T extends DataPointWithPercentage >( {
85
86
  isSeriesVisible,
86
87
  }: UseInteractiveLegendDataParams< T > ): UseInteractiveLegendDataResult< T > => {
87
88
  // Filter and recalculate data for interactive legends
89
+ // Note: data should already have percentages calculated by the chart component
88
90
  const visibleData = useMemo( () => {
89
- // If interactive mode is disabled or no chartId, return all data unchanged
91
+ // If interactive mode is disabled or no chartId, return data as-is
90
92
  if ( ! chartId || ! legendInteractive ) {
91
93
  return data;
92
94
  }
@@ -99,9 +101,8 @@ export const useInteractiveLegendData = < T extends DataPointWithPercentage >( {
99
101
  return [];
100
102
  }
101
103
 
102
- // Recalculate percentages so visible segments total 100%
104
+ // Recalculate percentages from values so visible segments total 100%
103
105
  const totalValue = filtered.reduce( ( sum, segment ) => sum + segment.value, 0 );
104
-
105
106
  return filtered.map( segment => ( {
106
107
  ...segment,
107
108
  percentage: totalValue > 0 ? ( segment.value / totalValue ) * 100 : 0,
@@ -113,24 +114,26 @@ export const useInteractiveLegendData = < T extends DataPointWithPercentage >( {
113
114
  return legendInteractive && visibleData.length === 0;
114
115
  }, [ legendInteractive, visibleData ] );
115
116
 
116
- // Prepare legend data with recalculated percentages for visible items
117
- // This maintains all legend entries but shows updated percentages for visible segments
117
+ // Prepare legend data with percentages
118
+ // Hidden items keep their original percentage (calculated from all values)
119
+ // Visible items show recalculated percentages (totaling 100%)
118
120
  const legendData = useMemo( () => {
119
121
  if ( ! legendInteractive || ! chartId ) {
120
122
  return data;
121
123
  }
122
124
 
123
- // Map original data to show recalculated percentages for visible items
125
+ // Build a Map for O(1) lookups instead of O(n) find() calls
126
+ const visibleDataMap = new Map( visibleData.map( d => [ d.label, d ] ) );
127
+
124
128
  return data.map( segment => {
125
129
  const isVisible = isSeriesVisible( chartId, segment.label );
126
130
  if ( ! isVisible ) {
127
- // Return original data for hidden items
131
+ // Hidden items keep original percentage
128
132
  return segment;
129
133
  }
130
134
 
131
- // For visible items, find the recalculated percentage from visibleData
132
- const recalculated = visibleData.find( d => d.label === segment.label );
133
- return recalculated || segment;
135
+ // For visible items, get the recalculated percentage from visibleData
136
+ return visibleDataMap.get( segment.label ) || segment;
134
137
  } );
135
138
  }, [ data, visibleData, legendInteractive, chartId, isSeriesVisible ] );
136
139