@automattic/charts 0.53.0 → 0.53.2

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 (122) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/charts/bar-chart/index.cjs +5 -5
  3. package/dist/charts/bar-chart/index.d.cts +1 -1
  4. package/dist/charts/bar-chart/index.d.ts +1 -1
  5. package/dist/charts/bar-chart/index.js +4 -4
  6. package/dist/charts/bar-list-chart/index.cjs +6 -6
  7. package/dist/charts/bar-list-chart/index.d.cts +1 -1
  8. package/dist/charts/bar-list-chart/index.d.ts +1 -1
  9. package/dist/charts/bar-list-chart/index.js +5 -5
  10. package/dist/charts/conversion-funnel-chart/index.cjs +4 -4
  11. package/dist/charts/conversion-funnel-chart/index.d.cts +1 -1
  12. package/dist/charts/conversion-funnel-chart/index.d.ts +1 -1
  13. package/dist/charts/conversion-funnel-chart/index.js +3 -3
  14. package/dist/charts/geo-chart/index.cjs +13 -0
  15. package/dist/charts/geo-chart/index.cjs.map +1 -0
  16. package/dist/charts/geo-chart/index.css +8 -0
  17. package/dist/charts/geo-chart/index.css.map +1 -0
  18. package/dist/charts/geo-chart/index.d.cts +62 -0
  19. package/dist/charts/geo-chart/index.d.ts +62 -0
  20. package/dist/charts/geo-chart/index.js +13 -0
  21. package/dist/charts/geo-chart/index.js.map +1 -0
  22. package/dist/charts/leaderboard-chart/index.cjs +5 -5
  23. package/dist/charts/leaderboard-chart/index.d.cts +2 -2
  24. package/dist/charts/leaderboard-chart/index.d.ts +2 -2
  25. package/dist/charts/leaderboard-chart/index.js +4 -4
  26. package/dist/charts/line-chart/index.cjs +5 -5
  27. package/dist/charts/line-chart/index.d.cts +1 -1
  28. package/dist/charts/line-chart/index.d.ts +1 -1
  29. package/dist/charts/line-chart/index.js +4 -4
  30. package/dist/charts/pie-chart/index.cjs +6 -6
  31. package/dist/charts/pie-chart/index.d.cts +1 -1
  32. package/dist/charts/pie-chart/index.d.ts +1 -1
  33. package/dist/charts/pie-chart/index.js +5 -5
  34. package/dist/charts/pie-semi-circle-chart/index.cjs +6 -6
  35. package/dist/charts/pie-semi-circle-chart/index.d.cts +1 -1
  36. package/dist/charts/pie-semi-circle-chart/index.d.ts +1 -1
  37. package/dist/charts/pie-semi-circle-chart/index.js +5 -5
  38. package/dist/{chunk-ERGEUE7R.cjs → chunk-4KEE36W3.cjs} +19 -19
  39. package/dist/chunk-4KEE36W3.cjs.map +1 -0
  40. package/dist/{chunk-4RYV2TII.js → chunk-6RKPV3UP.js} +10 -10
  41. package/dist/{chunk-VER6S543.js → chunk-ALDWCNLH.js} +3 -3
  42. package/dist/chunk-CLSMJQCO.cjs +121 -0
  43. package/dist/chunk-CLSMJQCO.cjs.map +1 -0
  44. package/dist/{chunk-2FRTJVQ3.js → chunk-DCZ47KPZ.js} +6 -6
  45. package/dist/chunk-DCZ47KPZ.js.map +1 -0
  46. package/dist/{chunk-GWNXOI4M.cjs → chunk-DDV5726Q.cjs} +25 -25
  47. package/dist/{chunk-GWNXOI4M.cjs.map → chunk-DDV5726Q.cjs.map} +1 -1
  48. package/dist/{chunk-O2BJMTIS.js → chunk-DYMJWNYM.js} +2 -2
  49. package/dist/{chunk-A7X3CNEO.cjs → chunk-FVWTBK44.cjs} +6 -6
  50. package/dist/{chunk-A7X3CNEO.cjs.map → chunk-FVWTBK44.cjs.map} +1 -1
  51. package/dist/{chunk-ZN7KVU4R.cjs → chunk-GX4XTD6V.cjs} +76 -76
  52. package/dist/{chunk-ZN7KVU4R.cjs.map → chunk-GX4XTD6V.cjs.map} +1 -1
  53. package/dist/{chunk-VTMJWCCW.js → chunk-H6XRINJE.js} +10 -10
  54. package/dist/{chunk-CQPKK55N.js → chunk-HPJ5XSZM.js} +60 -60
  55. package/dist/{chunk-CQPKK55N.js.map → chunk-HPJ5XSZM.js.map} +1 -1
  56. package/dist/{chunk-6QCSXXDY.cjs → chunk-JWMWOBAX.cjs} +23 -23
  57. package/dist/{chunk-6QCSXXDY.cjs.map → chunk-JWMWOBAX.cjs.map} +1 -1
  58. package/dist/{chunk-DY7IVYWP.js → chunk-KBORJZKC.js} +8 -8
  59. package/dist/{chunk-W4ZYJ74Q.js → chunk-KORG7ITC.js} +3 -3
  60. package/dist/{chunk-AHUSYMYS.cjs → chunk-LNLHCZ6F.cjs} +28 -28
  61. package/dist/{chunk-AHUSYMYS.cjs.map → chunk-LNLHCZ6F.cjs.map} +1 -1
  62. package/dist/chunk-RDJ5PKC5.js +121 -0
  63. package/dist/chunk-RDJ5PKC5.js.map +1 -0
  64. package/dist/{chunk-P3QEXFTA.js → chunk-TE63Y5PX.js} +9 -1
  65. package/dist/chunk-TE63Y5PX.js.map +1 -0
  66. package/dist/{chunk-O23EGQ3H.cjs → chunk-UWAZGLHG.cjs} +22 -22
  67. package/dist/{chunk-O23EGQ3H.cjs.map → chunk-UWAZGLHG.cjs.map} +1 -1
  68. package/dist/{chunk-W3H42XRV.cjs → chunk-WKN6C4ZE.cjs} +10 -10
  69. package/dist/{chunk-W3H42XRV.cjs.map → chunk-WKN6C4ZE.cjs.map} +1 -1
  70. package/dist/{chunk-RHQIACQT.js → chunk-Z34VYZGR.js} +9 -9
  71. package/dist/{chunk-NYZFVI2P.cjs → chunk-ZPJHWKEK.cjs} +9 -9
  72. package/dist/{chunk-NYZFVI2P.cjs.map → chunk-ZPJHWKEK.cjs.map} +1 -1
  73. package/dist/{chunk-DAKYGZG6.cjs → chunk-ZVGEDXDP.cjs} +10 -2
  74. package/dist/chunk-ZVGEDXDP.cjs.map +1 -0
  75. package/dist/components/legend/index.cjs +4 -4
  76. package/dist/components/legend/index.d.cts +1 -1
  77. package/dist/components/legend/index.d.ts +1 -1
  78. package/dist/components/legend/index.js +3 -3
  79. package/dist/components/tooltip/index.d.cts +1 -1
  80. package/dist/components/tooltip/index.d.ts +1 -1
  81. package/dist/hooks/index.cjs +3 -3
  82. package/dist/hooks/index.d.cts +1 -1
  83. package/dist/hooks/index.d.ts +1 -1
  84. package/dist/hooks/index.js +2 -2
  85. package/dist/index.cjs +26 -126
  86. package/dist/index.cjs.map +1 -1
  87. package/dist/index.d.cts +15 -55
  88. package/dist/index.d.ts +15 -55
  89. package/dist/index.js +38 -138
  90. package/dist/index.js.map +1 -1
  91. package/dist/{leaderboard-chart-3dKYMfoP.d.cts → leaderboard-chart-CN80sJmQ.d.cts} +1 -1
  92. package/dist/{leaderboard-chart-BqH8BLiG.d.ts → leaderboard-chart-DPi2ueOg.d.ts} +1 -1
  93. package/dist/providers/index.cjs +3 -3
  94. package/dist/providers/index.d.cts +2 -2
  95. package/dist/providers/index.d.ts +2 -2
  96. package/dist/providers/index.js +2 -2
  97. package/dist/{themes-ChB_VjWt.d.ts → themes-AriuFXQ2.d.ts} +2 -2
  98. package/dist/{themes-DijSDhqQ.d.cts → themes-Dzg0wE3B.d.cts} +2 -2
  99. package/dist/{types-sQ20gAeB.d.cts → types-D1lTxRyg.d.cts} +1 -1
  100. package/dist/{types-sQ20gAeB.d.ts → types-D1lTxRyg.d.ts} +1 -1
  101. package/dist/utils/index.cjs +4 -2
  102. package/dist/utils/index.cjs.map +1 -1
  103. package/dist/utils/index.d.cts +11 -2
  104. package/dist/utils/index.d.ts +11 -2
  105. package/dist/utils/index.js +3 -1
  106. package/package.json +31 -23
  107. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx +2 -5
  108. package/src/charts/sparkline/sparkline.tsx +2 -0
  109. package/src/charts/sparkline/types.ts +8 -0
  110. package/src/utils/color-utils.ts +19 -0
  111. package/src/utils/test/color-utils.test.ts +232 -0
  112. package/dist/chunk-2FRTJVQ3.js.map +0 -1
  113. package/dist/chunk-DAKYGZG6.cjs.map +0 -1
  114. package/dist/chunk-ERGEUE7R.cjs.map +0 -1
  115. package/dist/chunk-P3QEXFTA.js.map +0 -1
  116. /package/dist/{chunk-4RYV2TII.js.map → chunk-6RKPV3UP.js.map} +0 -0
  117. /package/dist/{chunk-VER6S543.js.map → chunk-ALDWCNLH.js.map} +0 -0
  118. /package/dist/{chunk-O2BJMTIS.js.map → chunk-DYMJWNYM.js.map} +0 -0
  119. /package/dist/{chunk-VTMJWCCW.js.map → chunk-H6XRINJE.js.map} +0 -0
  120. /package/dist/{chunk-DY7IVYWP.js.map → chunk-KBORJZKC.js.map} +0 -0
  121. /package/dist/{chunk-W4ZYJ74Q.js.map → chunk-KORG7ITC.js.map} +0 -0
  122. /package/dist/{chunk-RHQIACQT.js.map → chunk-Z34VYZGR.js.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  export { M as MetricValueType, f as formatMetricValue } from '../format-metric-value-MXm5DtQ_.js';
2
2
  import { TickFormatter } from '@visx/axis';
3
3
  import { AnyD3Scale, ScaleInput } from '@visx/scale';
4
- import { S as SeriesData, C as ChartTheme, f as CompleteChartTheme } from '../types-sQ20gAeB.js';
4
+ import { d as SeriesData, C as ChartTheme, f as CompleteChartTheme } from '../types-D1lTxRyg.js';
5
5
  import { LegendShape } from '@visx/legend/lib/types';
6
6
  import { LineStyles } from '@visx/xychart';
7
7
  import '@visx/annotation/lib/components/CircleSubject';
@@ -157,6 +157,15 @@ declare const isValidHexColor: (hex: unknown) => hex is string;
157
157
  * @throws {Error} if hex string is malformed
158
158
  */
159
159
  declare const validateHexColor: (hex: unknown) => void;
160
+ /**
161
+ * Convert hex color to rgba with specified opacity.
162
+ * This is genuinely reusable across chart components.
163
+ * @param hex - The hex color string (e.g., '#ff0000')
164
+ * @param alpha - The opacity value. Values outside the [0, 1] range will be clamped by the underlying d3 color library.
165
+ * @return The rgba color string (e.g., 'rgba(255, 0, 0, 0.5)')
166
+ * @throws {Error} if hex string is malformed or alpha is not a valid number
167
+ */
168
+ declare const hexToRgba: (hex: string, alpha: number) => string;
160
169
  /**
161
170
  * Calculate the perceptual distance between two HSL colors
162
171
  * @param hsl1 - first color in HSL format [h, s, l]
@@ -213,4 +222,4 @@ declare const lightenHexColor: (hex: string, blend: number) => string;
213
222
  */
214
223
  declare const resolveCssVariable: (value: string, element?: HTMLElement | null) => string | null;
215
224
 
216
- export { attachSubComponents, formatPercentage, getColorDistance, getItemShapeStyles, getLongestTickWidth, getSeriesLineStyles, getSeriesStroke, isSafari, isValidHexColor, lightenHexColor, mergeThemes, normalizeColorToHex, parseAsLocalDate, parseHslString, parseRgbString, resolveCssVariable, validateHexColor };
225
+ export { attachSubComponents, formatPercentage, getColorDistance, getItemShapeStyles, getLongestTickWidth, getSeriesLineStyles, getSeriesStroke, hexToRgba, isSafari, isValidHexColor, lightenHexColor, mergeThemes, normalizeColorToHex, parseAsLocalDate, parseHslString, parseRgbString, resolveCssVariable, validateHexColor };
@@ -7,6 +7,7 @@ import {
7
7
  getLongestTickWidth,
8
8
  getSeriesLineStyles,
9
9
  getSeriesStroke,
10
+ hexToRgba,
10
11
  isSafari,
11
12
  isValidHexColor,
12
13
  lightenHexColor,
@@ -17,7 +18,7 @@ import {
17
18
  parseRgbString,
18
19
  resolveCssVariable,
19
20
  validateHexColor
20
- } from "../chunk-P3QEXFTA.js";
21
+ } from "../chunk-TE63Y5PX.js";
21
22
  import "../chunk-G3PMV62Z.js";
22
23
  export {
23
24
  attachSubComponents,
@@ -28,6 +29,7 @@ export {
28
29
  getLongestTickWidth,
29
30
  getSeriesLineStyles,
30
31
  getSeriesStroke,
32
+ hexToRgba,
31
33
  isSafari,
32
34
  isValidHexColor,
33
35
  lightenHexColor,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "0.53.0",
3
+ "version": "0.53.2",
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": {
@@ -38,6 +38,11 @@
38
38
  "require": "./dist/charts/conversion-funnel-chart/index.cjs"
39
39
  },
40
40
  "./conversion-funnel-chart/style.css": "./dist/charts/conversion-funnel-chart/index.css",
41
+ "./geo-chart": {
42
+ "import": "./dist/charts/geo-chart/index.js",
43
+ "require": "./dist/charts/geo-chart/index.cjs"
44
+ },
45
+ "./geo-chart/style.css": "./dist/charts/geo-chart/index.css",
41
46
  "./hooks": {
42
47
  "import": "./dist/hooks/index.js",
43
48
  "require": "./dist/hooks/index.cjs"
@@ -111,45 +116,48 @@
111
116
  ".": [
112
117
  "./dist/index.d.ts"
113
118
  ],
114
- "line-chart": [
115
- "./dist/charts/line-chart/index.d.ts"
116
- ],
117
119
  "bar-chart": [
118
120
  "./dist/charts/bar-chart/index.d.ts"
119
121
  ],
120
- "pie-chart": [
121
- "./dist/charts/pie-chart/index.d.ts"
122
- ],
123
- "pie-semi-circle-chart": [
124
- "./dist/charts/pie-semi-circle-chart/index.d.ts"
125
- ],
126
122
  "bar-list-chart": [
127
123
  "./dist/charts/bar-list-chart/index.d.ts"
128
124
  ],
129
- "leaderboard-chart": [
130
- "./dist/charts/leaderboard-chart/index.d.ts"
131
- ],
132
125
  "conversion-funnel-chart": [
133
126
  "./dist/charts/conversion-funnel-chart/index.d.ts"
134
127
  ],
135
- "tooltip": [
136
- "./dist/components/tooltip/index.d.ts"
128
+ "geo-chart": [
129
+ "./dist/charts/geo-chart/index.d.ts"
130
+ ],
131
+ "hooks": [
132
+ "./dist/hooks/index.d.ts"
133
+ ],
134
+ "leaderboard-chart": [
135
+ "./dist/charts/leaderboard-chart/index.d.ts"
137
136
  ],
138
137
  "legend": [
139
138
  "./dist/components/legend/index.d.ts"
140
139
  ],
141
- "sparkline": [
142
- "./dist/components/sparkline/index.d.ts"
140
+ "line-chart": [
141
+ "./dist/charts/line-chart/index.d.ts"
143
142
  ],
144
- "trend-indicator": [
145
- "./dist/components/trend-indicator/index.d.ts"
143
+ "pie-chart": [
144
+ "./dist/charts/pie-chart/index.d.ts"
146
145
  ],
147
- "hooks": [
148
- "./dist/hooks/index.d.ts"
146
+ "pie-semi-circle-chart": [
147
+ "./dist/charts/pie-semi-circle-chart/index.d.ts"
149
148
  ],
150
149
  "providers": [
151
150
  "./dist/providers/index.d.ts"
152
151
  ],
152
+ "sparkline": [
153
+ "./dist/components/sparkline/index.d.ts"
154
+ ],
155
+ "tooltip": [
156
+ "./dist/components/tooltip/index.d.ts"
157
+ ],
158
+ "trend-indicator": [
159
+ "./dist/components/trend-indicator/index.d.ts"
160
+ ],
153
161
  "utils": [
154
162
  "./dist/utils/index.d.ts"
155
163
  ],
@@ -214,8 +222,8 @@
214
222
  "@types/react": "18.3.26",
215
223
  "@types/react-dom": "18.3.7",
216
224
  "@visx/glyph": "3.12.0",
217
- "@wordpress/components": "30.9.0",
218
- "@wordpress/element": "6.36.0",
225
+ "@wordpress/components": "31.0.0",
226
+ "@wordpress/element": "6.37.0",
219
227
  "babel-jest": "30.2.0",
220
228
  "esbuild": "0.25.9",
221
229
  "esbuild-sass-plugin": "^3.1.0",
@@ -1,6 +1,5 @@
1
1
  import { localPoint } from '@visx/event';
2
2
  import { useTooltip, useTooltipInPortal } from '@visx/tooltip';
3
- import { color as d3Color } from '@visx/vendor/d3-color';
4
3
  import clsx from 'clsx';
5
4
  import { type FC, useRef, useMemo, useEffect, useCallback, useContext } from 'react';
6
5
  import { usePrefersReducedMotion } from '../../hooks';
@@ -12,7 +11,7 @@ import {
12
11
  useGlobalChartsTheme,
13
12
  useGlobalChartsContext,
14
13
  } from '../../providers';
15
- import { formatPercentage } from '../../utils';
14
+ import { formatPercentage, hexToRgba } from '../../utils';
16
15
  import styles from './conversion-funnel-chart.module.scss';
17
16
  import { useFunnelSelection } from './private';
18
17
  import type { FunnelStep, ConversionFunnelChartProps } from './types';
@@ -226,9 +225,7 @@ const ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {
226
225
 
227
226
  // Create light background version of primary color if not set
228
227
  const barBackgroundColor =
229
- backgroundColor ||
230
- d3Color( barColor )?.copy( { opacity: 0.08 } ).formatRgb() ||
231
- 'rgba(0, 0, 0, 0.08)';
228
+ backgroundColor || hexToRgba( barColor, 0.08 ) || 'rgba(0, 0, 0, 0.08)';
232
229
 
233
230
  // Default main metric rendering function
234
231
  const renderDefaultMainMetric = () => (
@@ -55,6 +55,7 @@ const SparklineComponent = forwardRef< HTMLDivElement, SparklineProps >(
55
55
  className,
56
56
  chartId,
57
57
  margin: marginProp,
58
+ animation,
58
59
  },
59
60
  ref
60
61
  ) => {
@@ -170,6 +171,7 @@ const SparklineComponent = forwardRef< HTMLDivElement, SparklineProps >(
170
171
  },
171
172
  } }
172
173
  curveType="monotone"
174
+ animation={ animation }
173
175
  />
174
176
  </div>
175
177
  );
@@ -89,4 +89,12 @@ export interface SparklineProps {
89
89
  bottom?: number;
90
90
  left?: number;
91
91
  };
92
+
93
+ /**
94
+ * Enable entry animation on initial render
95
+ * Creates a rising effect where the line scales up from the bottom.
96
+ * Automatically respects user's prefers-reduced-motion system setting.
97
+ * @default false
98
+ */
99
+ animation?: boolean;
92
100
  }
@@ -32,6 +32,25 @@ export const validateHexColor = ( hex: unknown ): void => {
32
32
  throw new Error( 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed' );
33
33
  };
34
34
 
35
+ /**
36
+ * Convert hex color to rgba with specified opacity.
37
+ * This is genuinely reusable across chart components.
38
+ * @param hex - The hex color string (e.g., '#ff0000')
39
+ * @param alpha - The opacity value. Values outside the [0, 1] range will be clamped by the underlying d3 color library.
40
+ * @return The rgba color string (e.g., 'rgba(255, 0, 0, 0.5)')
41
+ * @throws {Error} if hex string is malformed or alpha is not a valid number
42
+ */
43
+ export const hexToRgba = ( hex: string, alpha: number ): string => {
44
+ validateHexColor( hex );
45
+
46
+ if ( typeof alpha !== 'number' || isNaN( alpha ) ) {
47
+ throw new Error( 'Alpha must be a number' );
48
+ }
49
+
50
+ // Safe to use non-null assertion since validateHexColor ensures valid hex
51
+ return d3Color( hex )!.copy( { opacity: alpha } ).formatRgb();
52
+ };
53
+
35
54
  /**
36
55
  * Calculate the perceptual distance between two HSL colors
37
56
  * @param hsl1 - first color in HSL format [h, s, l]
@@ -3,6 +3,7 @@ import {
3
3
  getColorDistance,
4
4
  lightenHexColor,
5
5
  isValidHexColor,
6
+ hexToRgba,
6
7
  validateHexColor,
7
8
  parseHslString,
8
9
  parseRgbString,
@@ -290,6 +291,237 @@ describe( 'getColorDistance', () => {
290
291
  } );
291
292
  } );
292
293
 
294
+ describe( 'hexToRgba', () => {
295
+ describe( 'Valid hex colors', () => {
296
+ it( 'converts 6-digit hex to rgb with full opacity', () => {
297
+ const result = hexToRgba( '#ff0000', 1 );
298
+ expect( result ).toBe( 'rgb(255, 0, 0)' );
299
+ } );
300
+
301
+ it( 'converts 6-digit hex to rgba with partial opacity', () => {
302
+ const result = hexToRgba( '#00ff00', 0.5 );
303
+ expect( result ).toBe( 'rgba(0, 255, 0, 0.5)' );
304
+ } );
305
+
306
+ it( 'converts 6-digit hex to rgba with zero opacity', () => {
307
+ const result = hexToRgba( '#0000ff', 0 );
308
+ expect( result ).toBe( 'rgba(0, 0, 255, 0)' );
309
+ } );
310
+
311
+ it( 'handles lowercase hex colors', () => {
312
+ const result = hexToRgba( '#abcdef', 0.8 );
313
+ expect( result ).toBe( 'rgba(171, 205, 239, 0.8)' );
314
+ } );
315
+
316
+ it( 'handles uppercase hex colors', () => {
317
+ const result = hexToRgba( '#ABCDEF', 0.8 );
318
+ expect( result ).toBe( 'rgba(171, 205, 239, 0.8)' );
319
+ } );
320
+
321
+ it( 'handles mixed case hex colors', () => {
322
+ const result = hexToRgba( '#AbCdEf', 0.3 );
323
+ expect( result ).toBe( 'rgba(171, 205, 239, 0.3)' );
324
+ } );
325
+ } );
326
+
327
+ describe( 'Edge cases', () => {
328
+ it( 'handles black color', () => {
329
+ const result = hexToRgba( '#000000', 1 );
330
+ expect( result ).toBe( 'rgb(0, 0, 0)' );
331
+ } );
332
+
333
+ it( 'handles white color', () => {
334
+ const result = hexToRgba( '#ffffff', 1 );
335
+ expect( result ).toBe( 'rgb(255, 255, 255)' );
336
+ } );
337
+
338
+ it( 'handles high precision alpha values', () => {
339
+ const result = hexToRgba( '#ff0000', 0.123456 );
340
+ expect( result ).toBe( 'rgba(255, 0, 0, 0.123456)' );
341
+ } );
342
+
343
+ // Function now validates hex input format
344
+ it( 'throws error for hex without # prefix', () => {
345
+ expect( () => hexToRgba( 'ff0000', 1 ) ).toThrow( 'Hex color must start with #' );
346
+ } );
347
+ } );
348
+
349
+ describe( 'Input validation', () => {
350
+ describe( 'Invalid hex format', () => {
351
+ it( 'throws error for non-string input', () => {
352
+ expect( () => hexToRgba( 123 as unknown as string, 1 ) ).toThrow(
353
+ 'Hex color must be a string'
354
+ );
355
+ expect( () => hexToRgba( null as unknown as string, 1 ) ).toThrow(
356
+ 'Hex color must be a string'
357
+ );
358
+ expect( () => hexToRgba( undefined as unknown as string, 1 ) ).toThrow(
359
+ 'Hex color must be a string'
360
+ );
361
+ } );
362
+
363
+ it( 'throws error for hex without # prefix', () => {
364
+ expect( () => hexToRgba( 'ff0000', 1 ) ).toThrow( 'Hex color must start with #' );
365
+ expect( () => hexToRgba( '000000', 1 ) ).toThrow( 'Hex color must start with #' );
366
+ } );
367
+
368
+ it( 'throws error for wrong length hex strings', () => {
369
+ expect( () => hexToRgba( '#ff', 1 ) ).toThrow(
370
+ 'Hex color must be 7 characters long (e.g., #ff0000)'
371
+ );
372
+ expect( () => hexToRgba( '#fff', 1 ) ).toThrow(
373
+ 'Hex color must be 7 characters long (e.g., #ff0000)'
374
+ );
375
+ expect( () => hexToRgba( '#ffff', 1 ) ).toThrow(
376
+ 'Hex color must be 7 characters long (e.g., #ff0000)'
377
+ );
378
+ expect( () => hexToRgba( '#fffff', 1 ) ).toThrow(
379
+ 'Hex color must be 7 characters long (e.g., #ff0000)'
380
+ );
381
+ expect( () => hexToRgba( '#ff00000', 1 ) ).toThrow(
382
+ 'Hex color must be 7 characters long (e.g., #ff0000)'
383
+ );
384
+ expect( () => hexToRgba( '#', 1 ) ).toThrow(
385
+ 'Hex color must be 7 characters long (e.g., #ff0000)'
386
+ );
387
+ } );
388
+
389
+ it( 'throws error for invalid hex characters', () => {
390
+ expect( () => hexToRgba( '#gggggg', 1 ) ).toThrow(
391
+ 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed'
392
+ );
393
+ expect( () => hexToRgba( '#ff00gg', 1 ) ).toThrow(
394
+ 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed'
395
+ );
396
+ expect( () => hexToRgba( '#zz0000', 1 ) ).toThrow(
397
+ 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed'
398
+ );
399
+ expect( () => hexToRgba( '#ff@000', 1 ) ).toThrow(
400
+ 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed'
401
+ );
402
+ expect( () => hexToRgba( '#ff 000', 1 ) ).toThrow(
403
+ 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed'
404
+ );
405
+ } );
406
+
407
+ it( 'throws error for empty string', () => {
408
+ expect( () => hexToRgba( '', 1 ) ).toThrow( 'Hex color must start with #' );
409
+ } );
410
+ } );
411
+
412
+ describe( 'Invalid alpha values', () => {
413
+ it( 'throws error for non-number alpha', () => {
414
+ expect( () => hexToRgba( '#ff0000', 'invalid' as unknown as number ) ).toThrow(
415
+ 'Alpha must be a number'
416
+ );
417
+ expect( () => hexToRgba( '#ff0000', null as unknown as number ) ).toThrow(
418
+ 'Alpha must be a number'
419
+ );
420
+ expect( () => hexToRgba( '#ff0000', undefined as unknown as number ) ).toThrow(
421
+ 'Alpha must be a number'
422
+ );
423
+ expect( () => hexToRgba( '#ff0000', {} as unknown as number ) ).toThrow(
424
+ 'Alpha must be a number'
425
+ );
426
+ } );
427
+
428
+ it( 'throws error for NaN alpha', () => {
429
+ expect( () => hexToRgba( '#ff0000', NaN ) ).toThrow( 'Alpha must be a number' );
430
+ } );
431
+
432
+ it( 'accepts negative and greater than 1 alpha values without throwing (d3 color formatRgb() clamps them)', () => {
433
+ // These should not throw - CSS allows alpha values outside 0-1 range
434
+ expect( () => hexToRgba( '#ff0000', -0.5 ) ).not.toThrow();
435
+ expect( () => hexToRgba( '#ff0000', 1.5 ) ).not.toThrow();
436
+ expect( () => hexToRgba( '#ff0000', 2 ) ).not.toThrow();
437
+ } );
438
+ } );
439
+ } );
440
+
441
+ describe( 'Real-world color examples', () => {
442
+ it( 'converts primary blue color', () => {
443
+ const result = hexToRgba( '#4f46e5', 0.08 );
444
+ expect( result ).toBe( 'rgba(79, 70, 229, 0.08)' );
445
+ } );
446
+
447
+ it( 'converts success green color', () => {
448
+ const result = hexToRgba( '#10b981', 0.15 );
449
+ expect( result ).toBe( 'rgba(16, 185, 129, 0.15)' );
450
+ } );
451
+
452
+ it( 'converts error red color', () => {
453
+ const result = hexToRgba( '#ef4444', 0.2 );
454
+ expect( result ).toBe( 'rgba(239, 68, 68, 0.2)' );
455
+ } );
456
+
457
+ it( 'converts gray color', () => {
458
+ const result = hexToRgba( '#6b7280', 0.6 );
459
+ expect( result ).toBe( 'rgba(107, 114, 128, 0.6)' );
460
+ } );
461
+ } );
462
+
463
+ describe( 'Boundary alpha values', () => {
464
+ it( 'handles alpha value of 0', () => {
465
+ const result = hexToRgba( '#ff0000', 0 );
466
+ expect( result ).toBe( 'rgba(255, 0, 0, 0)' );
467
+ } );
468
+
469
+ it( 'handles alpha value of 1 (returns rgb format)', () => {
470
+ const result = hexToRgba( '#ff0000', 1 );
471
+ expect( result ).toBe( 'rgb(255, 0, 0)' );
472
+ } );
473
+
474
+ it( 'clamps negative alpha values to 0', () => {
475
+ const result = hexToRgba( '#ff0000', -0.5 );
476
+ expect( result ).toBe( 'rgba(255, 0, 0, 0)' );
477
+ } );
478
+
479
+ it( 'clamps alpha values greater than 1 (returns rgb format)', () => {
480
+ const result = hexToRgba( '#ff0000', 1.5 );
481
+ expect( result ).toBe( 'rgb(255, 0, 0)' );
482
+ } );
483
+ } );
484
+
485
+ describe( 'Color component extraction', () => {
486
+ it( 'correctly extracts red component', () => {
487
+ const result = hexToRgba( '#ff0000', 1 );
488
+ expect( result ).toContain( '255, 0, 0' );
489
+ } );
490
+
491
+ it( 'correctly extracts green component', () => {
492
+ const result = hexToRgba( '#00ff00', 1 );
493
+ expect( result ).toContain( '0, 255, 0' );
494
+ } );
495
+
496
+ it( 'correctly extracts blue component', () => {
497
+ const result = hexToRgba( '#0000ff', 1 );
498
+ expect( result ).toContain( '0, 0, 255' );
499
+ } );
500
+
501
+ it( 'correctly extracts all components for mixed color', () => {
502
+ const result = hexToRgba( '#8a2be2', 1 ); // BlueViolet
503
+ expect( result ).toBe( 'rgb(138, 43, 226)' );
504
+ } );
505
+ } );
506
+
507
+ describe( 'Typical usage patterns', () => {
508
+ it( 'works with common CSS background opacity', () => {
509
+ const result = hexToRgba( '#4f46e5', 0.08 );
510
+ expect( result ).toBe( 'rgba(79, 70, 229, 0.08)' );
511
+ } );
512
+
513
+ it( 'works with hover state opacity', () => {
514
+ const result = hexToRgba( '#4f46e5', 0.15 );
515
+ expect( result ).toBe( 'rgba(79, 70, 229, 0.15)' );
516
+ } );
517
+
518
+ it( 'works with disabled state opacity', () => {
519
+ const result = hexToRgba( '#4f46e5', 0.3 );
520
+ expect( result ).toBe( 'rgba(79, 70, 229, 0.3)' );
521
+ } );
522
+ } );
523
+ } );
524
+
293
525
  describe( 'lightenHexColor', () => {
294
526
  describe( 'Valid inputs', () => {
295
527
  it( 'returns original color with blend of 0', () => {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/charts/conversion-funnel-chart/conversion-funnel-chart.tsx","../src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss","../src/charts/conversion-funnel-chart/private/use-funnel-selection.ts"],"sourcesContent":["import { localPoint } from '@visx/event';\nimport { useTooltip, useTooltipInPortal } from '@visx/tooltip';\nimport { color as d3Color } from '@visx/vendor/d3-color';\nimport clsx from 'clsx';\nimport { type FC, useRef, useMemo, useEffect, useCallback, useContext } from 'react';\nimport { usePrefersReducedMotion } from '../../hooks';\nimport {\n\tGlobalChartsProvider,\n\tGlobalChartsContext,\n\tuseChartId,\n\tuseChartRegistration,\n\tuseGlobalChartsTheme,\n\tuseGlobalChartsContext,\n} from '../../providers';\nimport { formatPercentage } from '../../utils';\nimport styles from './conversion-funnel-chart.module.scss';\nimport { useFunnelSelection } from './private';\nimport type { FunnelStep, ConversionFunnelChartProps } from './types';\n\n/**\n * Internal ConversionFunnelChart component with chart registration\n *\n * @param props - Component props\n * @param props.chartId - Optional unique identifier for the chart\n * @param props.mainRate - Main conversion rate to highlight\n * @param props.changeIndicator - Change indicator (e.g., +2%, -1.5%)\n * @param props.steps - Array of funnel steps\n * @param props.loading - Whether the chart is in loading state\n * @param props.animation - Whether to show chart animation on initial render or not\n * @param props.className - Additional CSS class name\n * @param props.style - Custom styling\n * @param props.renderStepLabel - Custom render function for step labels\n * @param props.renderStepRate - Custom render function for step rates\n * @param props.renderMainMetric - Custom render function for the entire main metric section\n * @param props.renderTooltip - Custom render function for tooltip content\n * @return JSX element representing the conversion funnel chart\n */\nconst ConversionFunnelChartInternal: FC< ConversionFunnelChartProps > = ( {\n\tmainRate,\n\tchangeIndicator,\n\tsteps,\n\tloading = false,\n\tanimation,\n\tclassName,\n\tchartId: providedChartId,\n\tstyle,\n\trenderStepLabel,\n\trenderStepRate,\n\trenderMainMetric,\n\trenderTooltip,\n} ) => {\n\tconst chartId = useChartId( providedChartId );\n\tconst { conversionFunnelChart: conversionFunnelChartSettings } = useGlobalChartsTheme();\n\tconst { getElementStyles } = useGlobalChartsContext();\n\tconst chartRef = useRef< HTMLDivElement >( null );\n\tconst selectedBarRef = useRef< HTMLDivElement | null >( null );\n\n\t// Use @visx/tooltip hooks for tooltip positioning\n\tconst { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip } =\n\t\tuseTooltip();\n\n\t// Use custom hook for selection management\n\tconst { handleBarClick, handleBarKeyDown, clearSelection, getStepState } =\n\t\tuseFunnelSelection( hideTooltip );\n\tconst { containerRef: portalContainerRef, TooltipInPortal } = useTooltipInPortal( {\n\t\t// use TooltipWithBounds for boundary detection\n\t\tdetectBounds: true,\n\t\t// when tooltip containers are scrolled, this will correctly update the Tooltip position\n\t\tscroll: true,\n\t} );\n\n\t// Wrapper to clear selectedBarRef after clearing selection\n\tconst clearSelectionAndRef = useCallback( () => {\n\t\tclearSelection();\n\t\tselectedBarRef.current = null;\n\t\thideTooltip();\n\t}, [ clearSelection, hideTooltip ] );\n\n\t// Helper function to show tooltip at specific coordinates\n\tconst showTooltipAt = useCallback(\n\t\t( step: FunnelStep, x: number, y: number ) => {\n\t\t\tshowTooltip( {\n\t\t\t\ttooltipData: step,\n\t\t\t\ttooltipLeft: x,\n\t\t\t\ttooltipTop: y - 10,\n\t\t\t} );\n\t\t},\n\t\t[ showTooltip ]\n\t);\n\n\t// Helper function to get tooltip coordinates for mouse events\n\tconst getMouseTooltipCoords = useCallback( ( event: React.MouseEvent ) => {\n\t\tconst containerElement = chartRef.current;\n\t\tif ( containerElement ) {\n\t\t\tconst coords = localPoint( containerElement, event.nativeEvent );\n\t\t\tif ( coords ) {\n\t\t\t\treturn { x: coords.x, y: coords.y };\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}, [] );\n\n\t// Helper function to get tooltip coordinates for keyboard events\n\tconst getKeyboardTooltipCoords = useCallback( ( event: React.KeyboardEvent ) => {\n\t\tconst rect = event.currentTarget.getBoundingClientRect();\n\t\tconst containerElement = chartRef.current;\n\t\tif ( containerElement ) {\n\t\t\tconst containerRect = containerElement.getBoundingClientRect();\n\t\t\tconst x = rect.left + rect.width / 2 - containerRect.left;\n\t\t\tconst y = rect.top - containerRect.top;\n\t\t\treturn { x, y };\n\t\t}\n\t\treturn null;\n\t}, [] );\n\n\t// Helper function to handle step interaction (both click and keyboard)\n\tconst handleStepInteraction = useCallback(\n\t\t(\n\t\t\tstep: FunnelStep,\n\t\t\tevent: React.MouseEvent | React.KeyboardEvent,\n\t\t\tinteractionType: 'click' | 'keyboard'\n\t\t) => {\n\t\t\t// Store reference to the interacted element\n\t\t\tselectedBarRef.current = event.currentTarget as HTMLDivElement;\n\n\t\t\t// Check if deselecting the same step\n\t\t\tconst { isClicked } = getStepState( step.id );\n\t\t\tif ( isClicked ) {\n\t\t\t\t// Deselecting - clear selection (tooltip will be hidden by hook)\n\t\t\t\tif ( interactionType === 'click' ) {\n\t\t\t\t\thandleBarClick( step.id );\n\t\t\t\t} else {\n\t\t\t\t\thandleBarKeyDown( step.id, event as React.KeyboardEvent );\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Selecting - handle selection and show tooltip\n\t\t\tif ( interactionType === 'click' ) {\n\t\t\t\thandleBarClick( step.id );\n\t\t\t\tconst coords = getMouseTooltipCoords( event as React.MouseEvent );\n\t\t\t\tif ( coords ) {\n\t\t\t\t\tshowTooltipAt( step, coords.x, coords.y );\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thandleBarKeyDown( step.id, event as React.KeyboardEvent );\n\t\t\t\tconst coords = getKeyboardTooltipCoords( event as React.KeyboardEvent );\n\t\t\t\tif ( coords ) {\n\t\t\t\t\tshowTooltipAt( step, coords.x, coords.y );\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t[\n\t\t\tgetStepState,\n\t\t\thandleBarClick,\n\t\t\thandleBarKeyDown,\n\t\t\tshowTooltipAt,\n\t\t\tgetMouseTooltipCoords,\n\t\t\tgetKeyboardTooltipCoords,\n\t\t]\n\t);\n\n\t// Create handler factories to avoid arrow functions in JSX\n\tconst stepHandlers = useMemo( () => {\n\t\tconst handlers = new Map<\n\t\t\tstring,\n\t\t\t{\n\t\t\t\tonClick: ( event: React.MouseEvent ) => void;\n\t\t\t\tonKeyDown: ( event: React.KeyboardEvent ) => void;\n\t\t\t}\n\t\t>();\n\n\t\tsteps.forEach( step => {\n\t\t\tconst onClick = ( event: React.MouseEvent ) => {\n\t\t\t\tevent.stopPropagation();\n\t\t\t\thandleStepInteraction( step, event, 'click' );\n\t\t\t};\n\n\t\t\tconst onKeyDown = ( event: React.KeyboardEvent ) => {\n\t\t\t\tif ( event.key === 'Enter' || event.key === ' ' ) {\n\t\t\t\t\thandleStepInteraction( step, event, 'keyboard' );\n\t\t\t\t} else {\n\t\t\t\t\t// For other keys (like Escape), just handle the selection\n\t\t\t\t\tselectedBarRef.current = event.currentTarget as HTMLDivElement;\n\t\t\t\t\thandleBarKeyDown( step.id, event );\n\t\t\t\t}\n\t\t\t};\n\n\t\t\thandlers.set( step.id, { onClick, onKeyDown } );\n\t\t} );\n\n\t\treturn handlers;\n\t}, [ steps, handleStepInteraction, handleBarKeyDown ] );\n\n\t// Handle document-level click to clear selection when clicking outside selected bar\n\tuseEffect( () => {\n\t\tconst handleDocumentClick = ( event: MouseEvent ) => {\n\t\t\t// Only clear selection if there's an active selection and click is outside the selected bar\n\t\t\tif ( selectedBarRef.current && ! selectedBarRef.current.contains( event.target as Node ) ) {\n\t\t\t\tclearSelectionAndRef();\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener( 'mousedown', handleDocumentClick );\n\n\t\treturn () => {\n\t\t\tdocument.removeEventListener( 'mousedown', handleDocumentClick );\n\t\t};\n\t}, [ clearSelectionAndRef ] );\n\n\t// Get component settings from theme with fallbacks\n\tconst { primaryColor, backgroundColor, positiveChangeColor, negativeChangeColor } =\n\t\tconversionFunnelChartSettings;\n\n\t// Resolve bar color using getElementStyles with primaryColor as override\n\tconst { color: barColor } = getElementStyles\n\t\t? getElementStyles( {\n\t\t\t\tindex: 0,\n\t\t\t\toverrideColor: primaryColor,\n\t\t } )\n\t\t: { color: primaryColor || '#000000' };\n\n\t// Determine change indicator color\n\tconst isPositiveChange = changeIndicator?.startsWith( '+' );\n\tconst changeColor = isPositiveChange ? positiveChangeColor : negativeChangeColor;\n\n\t// Create light background version of primary color if not set\n\tconst barBackgroundColor =\n\t\tbackgroundColor ||\n\t\td3Color( barColor )?.copy( { opacity: 0.08 } ).formatRgb() ||\n\t\t'rgba(0, 0, 0, 0.08)';\n\n\t// Default main metric rendering function\n\tconst renderDefaultMainMetric = () => (\n\t\t<>\n\t\t\t<span className={ styles[ 'main-rate' ] }>{ formatPercentage( mainRate ) }</span>\n\t\t\t{ changeIndicator && (\n\t\t\t\t<span className={ styles[ 'change-indicator' ] } style={ { color: changeColor } }>\n\t\t\t\t\t{ changeIndicator }\n\t\t\t\t</span>\n\t\t\t) }\n\t\t</>\n\t);\n\n\t// Default tooltip rendering function\n\tconst renderDefaultTooltip = ( step: FunnelStep ) => (\n\t\t<>\n\t\t\t<div className={ styles[ 'tooltip-title' ] }>{ step.label }</div>\n\t\t\t<div className={ styles[ 'tooltip-content' ] }>\n\t\t\t\t{ formatPercentage( step.rate ) }\n\t\t\t\t{ ` • ${ step.count ?? 'no' } items` }\n\t\t\t</div>\n\t\t</>\n\t);\n\n\t// Validate data\n\tconst isDataValid = Boolean( steps && steps.length > 0 );\n\n\t// Memoize metadata to prevent unnecessary re-registration\n\tconst chartMetadata = useMemo(\n\t\t() => ( {\n\t\t\tmainRate,\n\t\t\tchangeIndicator,\n\t\t\tstepsCount: steps?.length || 0,\n\t\t} ),\n\t\t[ mainRate, changeIndicator, steps?.length ]\n\t);\n\n\tuseChartRegistration( {\n\t\tchartId,\n\t\tlegendItems: [],\n\t\tchartType: 'conversion-funnel',\n\t\tisDataValid,\n\t\tmetadata: chartMetadata,\n\t} );\n\n\tconst prefersReducedMotion = usePrefersReducedMotion();\n\n\t// Handle empty or undefined data\n\tif ( ! isDataValid ) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName={ clsx( styles.conversionFunnelChart, loading && styles.loading, className ) }\n\t\t\t\tstyle={ style }\n\t\t\t>\n\t\t\t\t<div className={ styles[ 'empty-state' ] }>\n\t\t\t\t\t{ loading ? 'Loading...' : 'No data available' }\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t}\n\n\t// Calculate bar heights relative to the maximum (first step)\n\tconst maxRate = Math.max( ...steps.map( step => step.rate ) );\n\n\treturn (\n\t\t<>\n\t\t\t<div\n\t\t\t\tref={ node => {\n\t\t\t\t\t// Set containerRef for @visx coordinate system\n\t\t\t\t\tportalContainerRef( node );\n\t\t\t\t\tchartRef.current = node;\n\t\t\t\t} }\n\t\t\t\tclassName={ clsx( styles.conversionFunnelChart, loading && styles.loading, className ) }\n\t\t\t\tstyle={ style }\n\t\t\t>\n\t\t\t\t{ /* Main Metric */ }\n\t\t\t\t{ renderMainMetric ? (\n\t\t\t\t\trenderMainMetric( {\n\t\t\t\t\t\tmainRate,\n\t\t\t\t\t\tchangeIndicator,\n\t\t\t\t\t\tclassName: styles[ 'main-metric' ],\n\t\t\t\t\t\tchangeColor,\n\t\t\t\t\t} )\n\t\t\t\t) : (\n\t\t\t\t\t<div className={ styles[ 'main-metric' ] }>{ renderDefaultMainMetric() }</div>\n\t\t\t\t) }\n\n\t\t\t\t{ /* Funnel Steps */ }\n\t\t\t\t<div className={ styles[ 'funnel-container' ] }>\n\t\t\t\t\t{ steps.map( ( step, index ) => {\n\t\t\t\t\t\tconst barHeight = ( step.rate / maxRate ) * 100;\n\t\t\t\t\t\tconst { isBlurred } = getStepState( step.id );\n\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={ step.id }\n\t\t\t\t\t\t\t\tclassName={ clsx( styles[ 'funnel-step' ], isBlurred && styles.blurred ) }\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{ /* Step Label and Rate */ }\n\t\t\t\t\t\t\t\t<div className={ styles[ 'step-header' ] }>\n\t\t\t\t\t\t\t\t\t{ renderStepLabel ? (\n\t\t\t\t\t\t\t\t\t\trenderStepLabel( {\n\t\t\t\t\t\t\t\t\t\t\tstep,\n\t\t\t\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t\t\t\t\tclassName: styles[ 'step-label' ],\n\t\t\t\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<span className={ styles[ 'step-label' ] }>{ step.label }</span>\n\t\t\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t\t\t\t{ renderStepRate ? (\n\t\t\t\t\t\t\t\t\t\trenderStepRate( {\n\t\t\t\t\t\t\t\t\t\t\tstep,\n\t\t\t\t\t\t\t\t\t\t\tindex,\n\t\t\t\t\t\t\t\t\t\t\tclassName: styles[ 'step-rate' ],\n\t\t\t\t\t\t\t\t\t\t} )\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<span className={ styles[ 'step-rate' ] }>\n\t\t\t\t\t\t\t\t\t\t\t{ formatPercentage( step.rate ) }\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t) }\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t{ /* Funnel Bar */ }\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName={ clsx( styles[ 'bar-container' ], isBlurred && styles.disabled ) }\n\t\t\t\t\t\t\t\t\tonClick={ stepHandlers.get( step.id )?.onClick }\n\t\t\t\t\t\t\t\t\tonKeyDown={ stepHandlers.get( step.id )?.onKeyDown }\n\t\t\t\t\t\t\t\t\trole=\"button\"\n\t\t\t\t\t\t\t\t\ttabIndex={ isBlurred ? -1 : 0 }\n\t\t\t\t\t\t\t\t\taria-label={ step.label }\n\t\t\t\t\t\t\t\t\tstyle={ { backgroundColor: barBackgroundColor } }\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tclassName={ clsx( styles[ 'funnel-bar' ], {\n\t\t\t\t\t\t\t\t\t\t\t[ styles[ 'funnel-bar--animated' ] ]:\n\t\t\t\t\t\t\t\t\t\t\t\tanimation && ! loading && ! prefersReducedMotion,\n\t\t\t\t\t\t\t\t\t\t} ) }\n\t\t\t\t\t\t\t\t\t\tstyle={ {\n\t\t\t\t\t\t\t\t\t\t\theight: `${ barHeight }%`,\n\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: barColor,\n\t\t\t\t\t\t\t\t\t\t} }\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t} ) }\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{ /* Tooltip Portal */ }\n\t\t\t{ tooltipOpen &&\n\t\t\t\ttooltipData &&\n\t\t\t\t( () => {\n\t\t\t\t\tconst tooltipContent = renderTooltip\n\t\t\t\t\t\t? renderTooltip( {\n\t\t\t\t\t\t\t\tstep: tooltipData as FunnelStep,\n\t\t\t\t\t\t\t\tindex: steps.findIndex( s => s.id === ( tooltipData as FunnelStep ).id ),\n\t\t\t\t\t\t\t\ttop: tooltipTop,\n\t\t\t\t\t\t\t\tleft: tooltipLeft,\n\t\t\t\t\t\t\t\tclassName: styles[ 'tooltip-wrapper' ],\n\t\t\t\t\t\t } )\n\t\t\t\t\t\t: renderDefaultTooltip( tooltipData as FunnelStep );\n\n\t\t\t\t\t// Don't render tooltip if renderTooltip returns falsy\n\t\t\t\t\tif ( ! tooltipContent ) return null;\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<TooltipInPortal\n\t\t\t\t\t\t\t// set this to random so it correctly updates with parent bounds\n\t\t\t\t\t\t\tkey={ Math.random() }\n\t\t\t\t\t\t\ttop={ tooltipTop }\n\t\t\t\t\t\t\tleft={ tooltipLeft }\n\t\t\t\t\t\t\tclassName={ styles[ 'tooltip-wrapper' ] }\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{ tooltipContent }\n\t\t\t\t\t\t</TooltipInPortal>\n\t\t\t\t\t);\n\t\t\t\t} )() }\n\t\t</>\n\t);\n};\n\n/**\n * ConversionFunnelChart component with provider wrapper\n *\n * @param props - Component props\n * @return JSX element representing the conversion funnel chart\n */\nconst ConversionFunnelChartWithProvider: FC< ConversionFunnelChartProps > = props => {\n\tconst existingContext = useContext( GlobalChartsContext );\n\n\t// If we're already in a GlobalChartsProvider context, don't create a new one\n\tif ( existingContext ) {\n\t\treturn <ConversionFunnelChartInternal { ...props } />;\n\t}\n\n\t// Otherwise, create our own GlobalChartsProvider\n\treturn (\n\t\t<GlobalChartsProvider>\n\t\t\t<ConversionFunnelChartInternal { ...props } />\n\t\t</GlobalChartsProvider>\n\t);\n};\n\nConversionFunnelChartWithProvider.displayName = 'ConversionFunnelChart';\n\nexport { ConversionFunnelChartWithProvider as default };\n","import 'css-chunk:src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss';export default {\n \"conversionFunnelChart\": \"a8ccharts-lK-YNK\",\n \"loading\": \"a8ccharts-DbHKK5\",\n \"main-metric\": \"a8ccharts-61WPYr\",\n \"main-rate\": \"a8ccharts-RRRI6x\",\n \"change-indicator\": \"a8ccharts-661iwx\",\n \"funnel-container\": \"a8ccharts-Z7EGnW\",\n \"funnel-step\": \"a8ccharts-VqFY0l\",\n \"blurred\": \"a8ccharts-7dTRBs\",\n \"step-header\": \"a8ccharts-2JsQiV\",\n \"step-label\": \"a8ccharts-6OabC4\",\n \"step-rate\": \"a8ccharts-9wSZ6n\",\n \"bar-container\": \"a8ccharts-sSmCTi\",\n \"disabled\": \"a8ccharts-PLWVAW\",\n \"funnel-bar\": \"a8ccharts-EzczI-\",\n \"selected\": \"a8ccharts-wNpZEu\",\n \"funnel-bar--animated\": \"a8ccharts-68HQJl\",\n \"stretch\": \"a8ccharts-CmtieZ\",\n \"tooltip-wrapper\": \"a8ccharts-2TeoCn\",\n \"tooltip-title\": \"a8ccharts-jkRitH\",\n \"tooltip-content\": \"a8ccharts-8jgT-3\",\n \"empty-state\": \"a8ccharts-Ml6MMr\"\n};","import { useCallback, useState } from 'react';\n\n/**\n * Custom hook to manage funnel bar selection state and interactions\n * @param hideTooltip - Function to hide tooltip when selection is cleared\n * @return Object containing selection state and event handlers\n */\nexport const useFunnelSelection = ( hideTooltip?: () => void ) => {\n\tconst [ clickedStep, setClickedStep ] = useState< string | null >( null );\n\n\t// Handle bar click\n\tconst handleBarClick = useCallback(\n\t\t( stepId: string ) => {\n\t\t\tif ( clickedStep === stepId ) {\n\t\t\t\t// If clicking the same step, deselect it\n\t\t\t\tsetClickedStep( null );\n\t\t\t\thideTooltip?.();\n\t\t\t} else {\n\t\t\t\t// Otherwise, select this step\n\t\t\t\tsetClickedStep( stepId );\n\t\t\t}\n\t\t},\n\t\t[ clickedStep, hideTooltip ]\n\t);\n\n\t// Handle bar keydown\n\tconst handleBarKeyDown = useCallback(\n\t\t( stepId: string, event: React.KeyboardEvent ) => {\n\t\t\tif ( event.key === 'Enter' || event.key === ' ' ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tif ( clickedStep === stepId ) {\n\t\t\t\t\tsetClickedStep( null );\n\t\t\t\t\thideTooltip?.();\n\t\t\t\t} else {\n\t\t\t\t\tsetClickedStep( stepId );\n\t\t\t\t}\n\t\t\t} else if ( event.key === 'Escape' ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tsetClickedStep( null );\n\t\t\t\thideTooltip?.();\n\t\t\t}\n\t\t},\n\t\t[ clickedStep, hideTooltip ]\n\t);\n\n\t// Clear selection (for chart-level click)\n\tconst clearSelection = useCallback( () => {\n\t\tsetClickedStep( null );\n\t\thideTooltip?.();\n\t}, [ hideTooltip ] );\n\n\t// Get step state helpers\n\tconst getStepState = useCallback(\n\t\t( stepId: string ) => ( {\n\t\t\tisClicked: clickedStep === stepId,\n\t\t\tisBlurred: clickedStep !== null && clickedStep !== stepId,\n\t\t} ),\n\t\t[ clickedStep ]\n\t);\n\n\treturn {\n\t\tclickedStep,\n\t\thandleBarClick,\n\t\thandleBarKeyDown,\n\t\tclearSelection,\n\t\tgetStepState,\n\t};\n};\n"],"mappings":"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","names":["useCallback","useCallback"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/jetpack/jetpack/projects/js-packages/charts/dist/chunk-DAKYGZG6.cjs","../src/utils/create-composition.ts","../src/utils/date-parsing.ts","../src/utils/format-metric-value.ts","../src/utils/format-percentage.ts","../src/utils/get-longest-tick-width.ts","../src/utils/get-styles.ts","../src/utils/is-safari.ts","../src/utils/merge-themes.ts","../src/utils/color-utils.ts","../src/utils/resolve-css-var.ts"],"names":["formatNumber","varName"],"mappings":"AAAA;ACUO,SAAS,mBAAA,CACf,KAAA,EACA,aAAA,EAC0B;AAC1B,EAAA,OAAO,MAAA,CAAO,MAAA,CAAQ,KAAA,EAAO,aAAc,CAAA;AAC5C;ADXA;AACA;AEiCA,mCAAyC;AAOzC,IAAM,YAAA,EAAc,CAAE,UAAA,EAAA,GAAiC;AACtD,EAAA,OAAO,8BAAA,CAA+B,IAAA,CAAM,UAAW,CAAA;AACxD,CAAA;AAqBO,IAAM,iBAAA,EAAmB,CAAE,UAAA,EAAA,GAA8B;AAC/D,EAAA,MAAM,cAAA,EAAgB,UAAA,CAAW,IAAA,CAAK,CAAA;AAGtC,EAAA,GAAA,CAAK,WAAA,CAAa,aAAc,CAAA,EAAI;AACnC,IAAA,MAAM,QAAA,EAAU,+BAAA,aAAwB,CAAA;AAExC,IAAA,GAAA,CAAK,CAAE,8BAAA,OAAiB,CAAA,EAAI;AAC3B,MAAA,uBAAO,IAAI,IAAA,CAAM,GAAI,CAAA;AAAA,IACtB;AAGA,IAAA,OAAO,OAAA;AAAA,EACR;AAGA,EAAA,MAAM,QAAA,EAAU;AAAA,IACf,YAAA;AAAA;AAAA,IACA,qBAAA;AAAA;AAAA,IACA,kBAAA;AAAA;AAAA,IACA,uBAAA;AAAA;AAAA,IACA,2BAAA;AAAA;AAAA,IACA;AAAA;AAAA,EACD,CAAA;AAEA,EAAA,IAAA,CAAA,MAAY,OAAA,GAAU,OAAA,EAAU;AAC/B,IAAA,MAAM,OAAA,EAAS,4BAAA,aAAO,EAAe,MAAA,kBAAQ,IAAI,IAAA,CAAK,CAAE,CAAA;AACxD,IAAA,GAAA,CAAK,8BAAA,MAAgB,CAAA,EAAI;AACxB,MAAA,OAAO,MAAA;AAAA,IACR;AAAA,EACD;AAGA,EAAA,uBAAO,IAAI,IAAA,CAAM,GAAI,CAAA;AACtB,CAAA;AF7DA;AACA;AG1CA,iEAAkD;AAyB3C,IAAM,kBAAA,EAAoB,CAChC,KAAA,EACA,KAAA,EAAwB,QAAA,EACxB,EAAE,QAAA,EAAU,eAAA,EAAiB,KAAA,EAAO,YAAY,EAAA,EAA8B,CAAC,CAAA,EAAA,GACnE;AACZ,EAAA,GAAA,CAAK,MAAA,IAAU,KAAA,GAAQ,MAAA,IAAU,KAAA,CAAA,EAAY;AAC5C,IAAA,OAAO,EAAA;AAAA,EACR;AAEA,EAAA,MAAM,aAAA,EAAe,MAAA,CAAQ,KAAM,CAAA;AACnC,EAAA,GAAA,CAAK,KAAA,CAAO,YAAa,CAAA,EAAI;AAC5B,IAAA,OAAO,EAAA;AAAA,EACR;AAEA,EAAA,OAAA,CAAS,IAAA,EAAO;AAAA,IACf,KAAK,UAAA,EAAY;AAEhB,MAAA,MAAM,UAAA,EAAY,eAAA,EACf,mDAAA,YAAqB,EAAc;AAAA,QACnC,QAAA,mBAAU,QAAA,UAAY,GAAA;AAAA,QACtB,mBAAA,EAAqB;AAAA,UACpB,qBAAA,mBAAuB,QAAA,UAAY,GAAA;AAAA,UACnC;AAAA,QACD;AAAA,MACA,CAAE,EAAA,EACF,4CAAA,YAAc,EAAc;AAAA,QAC5B,QAAA,mBAAU,QAAA,UAAY,GAAA;AAAA,QACtB,mBAAA,EAAqB;AAAA,UACpB;AAAA,QACD;AAAA,MACA,CAAE,CAAA;AACL,MAAA,OAAO,CAAA,CAAA,EAAK,SAAU,CAAA,CAAA;AACvB,IAAA;AAEgB,IAAA;AACS,MAAA;AAChB,QAAA;AACR,MAAA;AAEqB,MAAA;AACV,QAAA;AACV,QAAA;AACQ,UAAA;AACM,UAAA;AACd,QAAA;AACC,MAAA;AACH,IAAA;AAEK,IAAA;AACI,IAAA;AACD,MAAA;AAEM,QAAA;AACV,QAAA;AACC,UAAA;AACA,UAAA;AACD,QAAA;AAEc,MAAA;AACJ,QAAA;AACV,QAAA;AACC,UAAA;AACD,QAAA;AACE,MAAA;AACN,IAAA;AACD,EAAA;AACD;AHM2B;AACA;AIlGlBA;AAUyB;AAEZ,EAAA;AACC,IAAA;AACb,MAAA;AACP,MAAA;AACA,MAAA;AACD,IAAA;AACC,EAAA;AACH;AJ0F2B;AACA;AK9GlB;AAaR;AAIuB,EAAA;AACH,EAAA;AACO,IAAA;AACR,IAAA;AACnB,EAAA;AAEuB,EAAA;AACxB;ALgG2B;AACA;AM5GX;AAMT,EAAA;AAGA,EAAA;AAKM,EAAA;AAEb;AAWC;AAIkB,EAAA;AACnB;AAWgB;AAMT,EAAA;AACa,EAAA;AACM,EAAA;AAED,EAAA;AACpB,IAAA;AACA,IAAA;AACJ,EAAA;AAIgB,EAAA;AACK,IAAA;AAEnB,EAAA;AACM,IAAA;AACR,EAAA;AAGO,EAAA;AACR;AN+D2B;AACA;AOlJG;AACH,EAAA;AAClB,IAAA;AACR,EAAA;AACO,EAAA;AACR;APoJ2B;AACA;AQ1JL;AAoBrB;AAIkB,EAAA;AAA0B;AAE7B,IAAA;AACb,EAAA;AACH;ARsI2B;AACA;ASnKA;AAOM;AACV,EAAA;AACvB;AAOkC;AACP,EAAA;AACzB,IAAA;AACD,EAAA;AAGoB,EAAA;AACF,IAAA;AAClB,EAAA;AAC2B,EAAA;AACT,IAAA;AAClB,EAAA;AACwB,EAAA;AACN,IAAA;AAClB,EAAA;AACiB,EAAA;AAClB;AASC;AAGuB,EAAA;AACA,EAAA;AAGC,EAAA;AACJ,EAAA;AAGF,EAAA;AACM,EAAA;AACC,EAAA;AAEb,EAAA;AACS,IAAA;AAGrB,EAAA;AACD;AAQgC;AACP,EAAA;AAGC,EAAA;AACjB,IAAA;AACR,EAAA;AAEsB,EAAA;AAGI,EAAA;AAClB,IAAA;AACR,EAAA;AAG0B,EAAA;AAGH,EAAA;AACxB;AAQgC;AACP,EAAA;AAGC,EAAA;AACjB,IAAA;AACR,EAAA;AAEwB,EAAA;AAGR,EAAA;AACR,IAAA;AACR,EAAA;AAGwB,EAAA;AACzB;AAYC;AAIuB,EAAA;AACf,IAAA;AACR,EAAA;AAGyB,EAAA;AACjB,IAAA;AACR,EAAA;AAEsB,EAAA;AAGC,EAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACS,IAAA;AAC/B,EAAA;AAGyB,EAAA;AACN,IAAA;AACA,MAAA;AACD,MAAA;AAER,QAAA;AACR,MAAA;AACD,IAAA;AAEO,IAAA;AACR,EAAA;AAGyB,EAAA;AAEC,IAAA;AACjB,MAAA;AACR,IAAA;AACwB,IAAA;AACV,IAAA;AACC,MAAA;AACf,IAAA;AACO,IAAA;AACR,EAAA;AAGO,EAAA;AACR;AAWiC;AACV,EAAA;AAEE,EAAA;AACA,EAAA;AACA,EAAA;AAGC,EAAA;AACA,EAAA;AACA,EAAA;AAEE,EAAA;AAG5B;AT4D2B;AACA;AUrQE;AAe5B;AAGe,EAAA;AACP,IAAA;AACR,EAAA;AAIuB,EAAA;AACP,IAAA;AAED,IAAA;AACI,MAAA;AAEE,MAAA;AACpB,IAAA;AACD,EAAA;AAG4B,EAAA;AACpB,IAAA;AACR,EAAA;AAGO,EAAA;AACR;AASS;AAEkB,EAAA;AAED,EAAA;AACjB,IAAA;AACR,EAAA;AAGyB,EAAA;AAEA,EAAA;AAEF,IAAA;AAEf,IAAA;AACC,MAAA;AACR,IAAA;AAESC,IAAAA;AACV,EAAA;AAGsB,EAAA;AAGf,EAAA;AACC,IAAA;AACR,EAAA;AAEuB,EAAA;AAEL,EAAA;AACnB;AASS;AACe,EAAA;AACf,IAAA;AACR,EAAA;AAEI,EAAA;AACmB,IAAA;AACA,IAAA;AAEf,IAAA;AACA,EAAA;AAEA,IAAA;AACR,EAAA;AACD;AV6M2B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/jetpack/jetpack/projects/js-packages/charts/dist/chunk-DAKYGZG6.cjs","sourcesContent":[null,"/**\n * Utility function to create chart components with composition API.\n *\n * This function attaches subcomponents to a chart component to enable\n * dot notation access like <Chart.Legend />, <Chart.Tooltip />, etc.\n *\n * @param Chart - The main chart component\n * @param subComponents - Object containing subcomponents to attach\n * @return Chart component with attached subcomponents\n */\nexport function attachSubComponents< TChart, TSubComponents extends Record< string, unknown > >(\n\tChart: TChart,\n\tsubComponents: TSubComponents\n): TChart & TSubComponents {\n\treturn Object.assign( Chart, subComponents );\n}\n","/**\n * @file Date parsing utilities using date-fns for local timezone handling\n *\n * This module provides utilities for parsing various date string formats and converting\n * them to local timezone dates using the battle-tested date-fns library. For formats\n * without timezone info, they're treated as local. For formats with timezone info,\n * they're converted to the equivalent local time.\n *\n * Note: And specifically it prevents format `YYYY-MM-DD` being parsed as UTC date.\n *\n * Key Features:\n * - All parsed dates are in local timezone\n * - Converts timezone-aware strings to local equivalent\n * - Robust input validation and error handling using date-fns\n * - TypeScript type safety\n * - Much smaller codebase than custom parsing\n *\n * Supported Formats:\n * - YYYY-MM-DD (treated as local)\n * - YYYY-MM-DD HH:mm:ss (treated as local)\n * - YYYY-MM-DD HH:mm (treated as local)\n * - YYYY-MM-DDTHH:mm:ss (treated as local)\n * - YYYY-MM-DDTHH:mm:ss.SSS (treated as local)\n * - YYYY-MM-DDTHH:mm (treated as local)\n * - YYYY-MM-DDTHH:mm:ssZ (converted to local)\n * - YYYY-MM-DDTHH:mm:ss±HH:mm (converted to local)\n *\n * @example\n * ```typescript\n * parseAsLocalDate(\"2025-01-01\"); // Local timezone\n * parseAsLocalDate(\"2025-01-01 14:30:00\"); // Local timezone\n * parseAsLocalDate(\"2025-01-01 14:30\"); // Local timezone\n * parseAsLocalDate(\"2025-01-01T14:30:45.123\"); // Local timezone\n * parseAsLocalDate(\"2025-01-01T14:30:00Z\"); // UTC 14:30 → Local equivalent\n * parseAsLocalDate(\"2025-01-01T14:30:00+05:00\"); // +05:00 14:30 → Local equivalent\n * ```\n */\n\nimport { parse, parseISO, isValid } from 'date-fns';\n\n/**\n * Checks if a date string contains timezone information\n * @param {string} dateString - The date string to check for timezone information\n * @return {boolean} True if the date string contains timezone information, false otherwise\n */\nconst hasTimezone = ( dateString: string ): boolean => {\n\treturn /T.*[Z]$|T.*[+-]\\d{2}:?\\d{2}$/.test( dateString );\n};\n\n/**\n * Parses any supported date string format and returns a local timezone date\n *\n * Uses date-fns for robust parsing and validation. For strings without timezone\n * info, treats as local timezone. For strings with timezone info, converts to\n * local timezone equivalent.\n *\n * Supports:\n * - YYYY-MM-DD (local)\n * - YYYY-MM-DD HH:mm:ss (local)\n * - YYYY-MM-DD HH:mm (local)\n * - YYYY-MM-DDTHH:mm:ss (local)\n * - YYYY-MM-DDTHH:mm:ss.SSS (local)\n * - YYYY-MM-DDTHH:mm (local)\n * - YYYY-MM-DDTHH:mm:ssZ (UTC → local)\n * - YYYY-MM-DDTHH:mm:ss±HH:mm (offset → local)\n * @param {string} dateString - The date string to parse into a local timezone date\n * @return {Date} A Date object representing the parsed date in local timezone, or an invalid Date if parsing fails\n */\nexport const parseAsLocalDate = ( dateString: string ): Date => {\n\tconst trimmedString = dateString.trim();\n\n\t// If it has timezone information, parse as ISO and convert to local\n\tif ( hasTimezone( trimmedString ) ) {\n\t\tconst isoDate = parseISO( trimmedString );\n\n\t\tif ( ! isValid( isoDate ) ) {\n\t\t\treturn new Date( NaN );\n\t\t}\n\n\t\t// parseISO automatically converts to local timezone\n\t\treturn isoDate;\n\t}\n\n\t// For naive strings, try different local formats\n\tconst formats = [\n\t\t'yyyy-MM-dd', // 2025-01-01\n\t\t'yyyy-MM-dd HH:mm:ss', // 2025-01-01 14:30:45\n\t\t'yyyy-MM-dd HH:mm', // 2025-01-01 14:30\n\t\t\"yyyy-MM-dd'T'HH:mm:ss\", // 2025-01-01T14:30:45\n\t\t\"yyyy-MM-dd'T'HH:mm:ss.SSS\", // 2025-01-01T14:30:45.123\n\t\t\"yyyy-MM-dd'T'HH:mm\", // 2025-01-01T14:30\n\t];\n\n\tfor ( const format of formats ) {\n\t\tconst result = parse( trimmedString, format, new Date() );\n\t\tif ( isValid( result ) ) {\n\t\t\treturn result;\n\t\t}\n\t}\n\n\t// If no format matched, return invalid date\n\treturn new Date( NaN );\n};\n","import { formatNumberCompact, formatNumber } from '@automattic/number-formatters';\n\n/**\n * Types for formatMetricValue\n */\nexport type MetricValueType = 'number' | 'average' | 'currency';\n\ntype FormatMetricValueOptions = {\n\tdecimals?: number;\n\tuseMultipliers?: boolean;\n\tsignDisplay?: Intl.NumberFormatOptions[ 'signDisplay' ];\n};\n\n/**\n * Format a numeric metric value based on type, precision and scale.\n * Supports currency, number and percentage, using `@automattic/number-formatters`.\n *\n * @param value - The value to format\n * @param type - The type of formatting to apply\n * @param options - Formatting options\n * @param options.decimals - Number of decimal places to show\n * @param options.useMultipliers - Whether to use K, M, B suffixes for large numbers\n * @param options.signDisplay - Controls when to display the sign (auto, always, never, exceptZero)\n * @return Formatted string\n */\nexport const formatMetricValue = (\n\tvalue: string | number,\n\ttype: MetricValueType = 'number',\n\t{ decimals, useMultipliers = false, signDisplay }: FormatMetricValueOptions = {}\n): string => {\n\tif ( value === null || value === undefined ) {\n\t\treturn '';\n\t}\n\n\tconst numericValue = Number( value );\n\tif ( isNaN( numericValue ) ) {\n\t\treturn '';\n\t}\n\n\tswitch ( type ) {\n\t\tcase 'currency': {\n\t\t\t// Basic currency formatting - can be enhanced with full currency support\n\t\t\tconst formatted = useMultipliers\n\t\t\t\t? formatNumberCompact( numericValue, {\n\t\t\t\t\t\tdecimals: decimals ?? 2,\n\t\t\t\t\t\tnumberFormatOptions: {\n\t\t\t\t\t\t\tmaximumFractionDigits: decimals ?? 2,\n\t\t\t\t\t\t\tsignDisplay,\n\t\t\t\t\t\t},\n\t\t\t\t } )\n\t\t\t\t: formatNumber( numericValue, {\n\t\t\t\t\t\tdecimals: decimals ?? 2,\n\t\t\t\t\t\tnumberFormatOptions: {\n\t\t\t\t\t\t\tsignDisplay,\n\t\t\t\t\t\t},\n\t\t\t\t } );\n\t\t\treturn `$${ formatted }`;\n\t\t}\n\n\t\tcase 'average': {\n\t\t\tif ( ! Number.isFinite( numericValue ) ) {\n\t\t\t\treturn '—';\n\t\t\t}\n\n\t\t\treturn formatNumber( numericValue, {\n\t\t\t\tdecimals: decimals ?? 0,\n\t\t\t\tnumberFormatOptions: {\n\t\t\t\t\tstyle: 'percent',\n\t\t\t\t\tsignDisplay: signDisplay ?? 'exceptZero',\n\t\t\t\t},\n\t\t\t} );\n\t\t}\n\n\t\tcase 'number':\n\t\tdefault: {\n\t\t\treturn useMultipliers\n\t\t\t\t? formatNumberCompact( numericValue, {\n\t\t\t\t\t\tdecimals: decimals ?? 0,\n\t\t\t\t\t\tnumberFormatOptions: {\n\t\t\t\t\t\t\tmaximumFractionDigits: decimals ?? 0,\n\t\t\t\t\t\t\tsignDisplay,\n\t\t\t\t\t\t},\n\t\t\t\t } )\n\t\t\t\t: formatNumber( numericValue, {\n\t\t\t\t\t\tdecimals: decimals ?? 0,\n\t\t\t\t\t\tnumberFormatOptions: {\n\t\t\t\t\t\t\tsignDisplay,\n\t\t\t\t\t\t},\n\t\t\t\t } );\n\t\t}\n\t}\n};\n","import { formatNumber } from '@automattic/number-formatters';\n\n/**\n * Format a percentage value with smart decimal handling.\n * Uses `@automattic/number-formatters` for consistent formatting.\n * Removes unnecessary trailing zeros and caps at 2 decimal places.\n *\n * @param value - The percentage value (0-100 range)\n * @return Formatted percentage string (e.g., \"30%\", \"30.1%\", \"30.25%\")\n */\nexport const formatPercentage = ( value: number ): string => {\n\t// Use formatNumber with percentage style, but convert from 0-100 range to 0-1 range\n\treturn formatNumber( value / 100, {\n\t\tnumberFormatOptions: {\n\t\t\tstyle: 'percent',\n\t\t\tminimumFractionDigits: 0,\n\t\t\tmaximumFractionDigits: 2,\n\t\t},\n\t} );\n};\n","import { getStringWidth } from '@visx/text';\nimport type { TickFormatter } from '@visx/axis';\nimport type { AnyD3Scale, ScaleInput } from '@visx/scale';\n\n/**\n * Returns the width of the longest tick.\n *\n * @param ticks - Ticks to get the width of.\n * @param formatTick - Function to format the tick.\n * @param {object} labelStyle - Style object for the label.\n * @return {number} - Width of the longest tick.\n */\nexport const getLongestTickWidth = < T extends AnyD3Scale >(\n\tticks: ScaleInput< T >[],\n\tformatTick: TickFormatter< ScaleInput< T > >,\n\tlabelStyle?: object\n) => {\n\tconst formattedTicks = ticks.map( tick => formatTick( tick, 0, [] ) );\n\tconst longestTick = formattedTicks.reduce(\n\t\t( longest, current ) => ( longest.length >= current.length ? longest : current ),\n\t\tformattedTicks[ 0 ]\n\t);\n\n\treturn getStringWidth( longestTick, labelStyle );\n};\n","import type { ChartTheme, SeriesData } from '../types';\nimport type { LegendShape } from '@visx/legend/lib/types';\nimport type { LineStyles } from '@visx/xychart';\n\n/**\n * Utility function to get consolidated line styles for a series\n * This consolidates the logic used by both LineChart and Legend components\n *\n * @param {SeriesData} seriesData - The series data containing styling options\n * @param {number} index - The index of the series in the data array\n * @param {ChartTheme} providerTheme - The chart theme configuration\n * @return {LineStyles} The consolidated line styles for the series\n */\nexport function getSeriesLineStyles(\n\tseriesData: SeriesData,\n\tindex: number,\n\tproviderTheme: ChartTheme\n): LineStyles {\n\t// Get theme-based line styles for line type\n\tconst themeSemanticLineStyle = providerTheme?.lineChart?.lineStyles?.[ seriesData.options?.type ];\n\n\t// Get theme-based line styles for index of series data\n\tconst themeSeriesLineStyle =\n\t\tproviderTheme?.seriesLineStyles?.[ index % providerTheme.seriesLineStyles.length ];\n\n\t// Priority order: custom series style > theme line type style > default theme series style\n\treturn (\n\t\tseriesData.options?.seriesLineStyle ?? themeSemanticLineStyle ?? themeSeriesLineStyle ?? {}\n\t);\n}\n\n/**\n * Utility function to get stroke color for a series\n *\n * @param {SeriesData} seriesData - The series data containing styling options\n * @param {number} index - The index of the series in the data array\n * @param {string[]} themeColors - Array of theme colors\n * @return {string} The stroke color for the series\n */\nexport function getSeriesStroke(\n\tseriesData: SeriesData,\n\tindex: number,\n\tthemeColors: string[]\n): string {\n\treturn seriesData.options?.stroke ?? themeColors[ index % themeColors.length ];\n}\n\n/**\n * Utility function to get shape styles for a legend item\n *\n * @param {SeriesData} series - The series data containing styling options\n * @param {number} index - The index of the series in the data array\n * @param {ChartTheme} theme - The chart theme configuration\n * @param {LegendShape} legendShape - The shape to use for the item (optional)\n * @return {Record< string, unknown >} The shape styles for the item\n */\nexport function getItemShapeStyles(\n\tseries: SeriesData,\n\tindex: number,\n\ttheme: ChartTheme,\n\tlegendShape?: LegendShape< SeriesData[], number >\n): Record< string, unknown > {\n\tconst seriesShapeStyles = series.options?.legendShapeStyle ?? {};\n\tconst lineStyles = legendShape === 'line' ? getSeriesLineStyles( series, index, theme ) : {};\n\tconst themeShapeStyles = theme.legendShapeStyles?.[ index ];\n\n\tconst itemShapeStyles = {\n\t\t...seriesShapeStyles,\n\t\t...lineStyles,\n\t};\n\n\t// Return item shape styles if they are not empty\n\tif (\n\t\tObject.values( itemShapeStyles ).some(\n\t\t\tvalue => value !== undefined && value !== null && value !== ''\n\t\t)\n\t) {\n\t\treturn itemShapeStyles;\n\t}\n\n\t// Fallback to theme shape styles if defined\n\treturn themeShapeStyles ?? {};\n}\n","export const isSafari = () => {\n\tif ( typeof navigator !== 'undefined' && navigator.userAgent ) {\n\t\treturn /^((?!chrome|android).)*safari/i.test( navigator.userAgent );\n\t}\n\treturn false;\n};\n","import deepmerge from 'deepmerge';\nimport type { ChartTheme, CompleteChartTheme } from '../types';\n\n/**\n * Merges chart themes with proper precedence.\n * The second theme (override) takes precedence over the first theme (base).\n *\n * @param baseTheme - Base theme object\n * @param overrideTheme - Theme to override base with (takes precedence)\n * @return Merged theme with overrideTheme values taking precedence\n */\nexport function mergeThemes(\n\tbaseTheme: CompleteChartTheme,\n\toverrideTheme: Partial< ChartTheme >\n): CompleteChartTheme;\nexport function mergeThemes(\n\tbaseTheme: ChartTheme,\n\toverrideTheme: Partial< ChartTheme >\n): ChartTheme;\nexport function mergeThemes(\n\tbaseTheme: ChartTheme,\n\toverrideTheme: Partial< ChartTheme >\n): ChartTheme {\n\t// Use deepmerge to properly merge nested objects, with overrideTheme taking precedence\n\treturn deepmerge( baseTheme, overrideTheme, {\n\t\t// Ensure arrays are replaced rather than concatenated\n\t\tarrayMerge: ( _destinationArray, sourceArray ) => sourceArray,\n\t} ) as ChartTheme;\n}\n","import { color as d3Color, hsl as d3Hsl } from '@visx/vendor/d3-color';\n\n/**\n * Check if a value is a valid 6-digit hex color\n * @param hex - The value to check\n * @return true if valid hex color format (e.g., '#ff0000')\n */\nexport const isValidHexColor = ( hex: unknown ): hex is string => {\n\treturn typeof hex === 'string' && /^#[0-9a-fA-F]{6}$/.test( hex );\n};\n\n/**\n * Validate hex color format, throwing descriptive errors if invalid\n * @param hex - The hex color string to validate\n * @throws {Error} if hex string is malformed\n */\nexport const validateHexColor = ( hex: unknown ): void => {\n\tif ( isValidHexColor( hex ) ) {\n\t\treturn;\n\t}\n\n\t// Provide specific error messages for common issues\n\tif ( typeof hex !== 'string' ) {\n\t\tthrow new Error( 'Hex color must be a string' );\n\t}\n\tif ( ! hex.startsWith( '#' ) ) {\n\t\tthrow new Error( 'Hex color must start with #' );\n\t}\n\tif ( hex.length !== 7 ) {\n\t\tthrow new Error( 'Hex color must be 7 characters long (e.g., #ff0000)' );\n\t}\n\tthrow new Error( 'Hex color contains invalid characters. Only 0-9, a-f, A-F are allowed' );\n};\n\n/**\n * Calculate the perceptual distance between two HSL colors\n * @param hsl1 - first color in HSL format [h, s, l]\n * @param hsl2 - second color in HSL format [h, s, l]\n * @return distance value (0-100+, lower means more similar)\n */\nexport const getColorDistance = (\n\thsl1: [ number, number, number ],\n\thsl2: [ number, number, number ]\n): number => {\n\tconst [ h1, s1, l1 ] = hsl1;\n\tconst [ h2, s2, l2 ] = hsl2;\n\n\t// Calculate hue difference, accounting for circular nature (0° = 360°)\n\tlet hueDiff = Math.abs( h1 - h2 );\n\thueDiff = Math.min( hueDiff, 360 - hueDiff );\n\n\t// Weight the differences: hue is most important, then lightness, then saturation\n\tconst hueWeight = 2;\n\tconst lightnessWeight = 1;\n\tconst saturationWeight = 0.5;\n\n\treturn Math.sqrt(\n\t\tMath.pow( hueDiff * hueWeight, 2 ) +\n\t\t\tMath.pow( ( l1 - l2 ) * lightnessWeight, 2 ) +\n\t\t\tMath.pow( ( s1 - s2 ) * saturationWeight, 2 )\n\t);\n};\n\n/**\n * Parse an HSL string like 'hsl(120, 50%, 50%)' into an HSL tuple.\n *\n * @param hslString - HSL color string\n * @return HSL tuple [h, s, l] or null if invalid\n */\nexport const parseHslString = ( hslString: string ): [ number, number, number ] | null => {\n\tconst lower = hslString.toLowerCase().trim();\n\n\t// Check prefix - d3-color handles the parsing\n\tif ( ! lower.startsWith( 'hsl(' ) ) {\n\t\treturn null;\n\t}\n\n\tconst parsed = d3Hsl( lower );\n\n\t// d3Hsl returns NaN values for invalid colors\n\tif ( isNaN( parsed.h ) && isNaN( parsed.s ) && isNaN( parsed.l ) ) {\n\t\treturn null;\n\t}\n\n\t// Normalize hue to 0-360 range (d3 may return NaN for achromatic colors)\n\tconst h = isNaN( parsed.h ) ? 0 : ( ( parsed.h % 360 ) + 360 ) % 360;\n\n\t// d3-color uses 0-1 scale, convert to 0-100\n\treturn [ h, parsed.s * 100, parsed.l * 100 ];\n};\n\n/**\n * Parse an RGB string like 'rgb(255, 0, 0)' into a hex color.\n *\n * @param rgbString - RGB color string\n * @return hex color string or null if invalid\n */\nexport const parseRgbString = ( rgbString: string ): string | null => {\n\tconst lower = rgbString.toLowerCase().trim();\n\n\t// Check prefix - only handle rgb(), not rgba()\n\tif ( ! lower.startsWith( 'rgb(' ) || lower.startsWith( 'rgba(' ) ) {\n\t\treturn null;\n\t}\n\n\tconst parsed = d3Color( lower );\n\n\t// d3Color returns null for invalid colors\n\tif ( ! parsed ) {\n\t\treturn null;\n\t}\n\n\t// d3-color clamps values automatically\n\treturn parsed.formatHex();\n};\n\n/**\n * Normalize any CSS color value to a hex color string.\n * Handles hex colors, HSL strings, RGB strings, and CSS variables.\n *\n * @param color - Any CSS color value\n * @param element - Optional DOM element for resolving CSS variables\n * @param resolveCss - Function to resolve CSS variables (injected for testability)\n * @return hex color string, or the original value if conversion fails\n */\nexport const normalizeColorToHex = (\n\tcolor: string,\n\telement?: HTMLElement | null,\n\tresolveCss?: ( value: string, el?: HTMLElement | null ) => string | null\n): string => {\n\tif ( ! color || typeof color !== 'string' ) {\n\t\treturn '';\n\t}\n\n\t// Already a valid hex color (6-digit format)\n\tif ( /^#[0-9a-fA-F]{6}$/.test( color ) ) {\n\t\treturn color;\n\t}\n\n\tconst trimmed = color.trim().toLowerCase();\n\n\t// Handle 3-digit hex colors - expand to 6-digit\n\tif ( /^#[0-9a-f]{3}$/i.test( trimmed ) ) {\n\t\tconst r = trimmed[ 1 ];\n\t\tconst g = trimmed[ 2 ];\n\t\tconst b = trimmed[ 3 ];\n\t\treturn `#${ r }${ r }${ g }${ g }${ b }${ b }`;\n\t}\n\n\t// Handle CSS variables - must be resolved before d3-color can parse\n\tif ( trimmed.startsWith( '--' ) || trimmed.startsWith( 'var(' ) ) {\n\t\tif ( resolveCss ) {\n\t\t\tconst resolved = resolveCss( color, element );\n\t\t\tif ( resolved ) {\n\t\t\t\t// Recursively normalize the resolved value\n\t\t\t\treturn normalizeColorToHex( resolved, element, resolveCss );\n\t\t\t}\n\t\t}\n\t\t// Can't resolve CSS variable, return original\n\t\treturn color;\n\t}\n\n\t// Handle HSL and RGB strings using d3-color\n\tif ( trimmed.startsWith( 'hsl(' ) || trimmed.startsWith( 'rgb(' ) ) {\n\t\t// Reject rgba() - we only handle rgb()\n\t\tif ( trimmed.startsWith( 'rgba(' ) ) {\n\t\t\treturn color;\n\t\t}\n\t\tconst parsed = d3Color( trimmed );\n\t\tif ( parsed ) {\n\t\t\treturn parsed.formatHex();\n\t\t}\n\t\treturn color;\n\t}\n\n\t// Unknown format, return as-is\n\treturn color;\n};\n\n/**\n * Lighten a hex color by blending it with white.\n * Useful for creating color gradients or lighter variants.\n *\n * @param hex - Hex color string (e.g., '#98C8DF')\n * @param blend - Blend amount with white (0 = original color, 1 = white)\n * @return Lightened hex color string (e.g., '#cce4ef')\n * @throws {Error} if hex string is malformed\n */\nexport const lightenHexColor = ( hex: string, blend: number ): string => {\n\tvalidateHexColor( hex );\n\n\tconst r = parseInt( hex.slice( 1, 3 ), 16 );\n\tconst g = parseInt( hex.slice( 3, 5 ), 16 );\n\tconst b = parseInt( hex.slice( 5, 7 ), 16 );\n\n\t// Blend with white (255, 255, 255)\n\tconst newR = Math.round( r + ( 255 - r ) * blend );\n\tconst newG = Math.round( g + ( 255 - g ) * blend );\n\tconst newB = Math.round( b + ( 255 - b ) * blend );\n\n\treturn `#${ newR.toString( 16 ).padStart( 2, '0' ) }${ newG\n\t\t.toString( 16 )\n\t\t.padStart( 2, '0' ) }${ newB.toString( 16 ).padStart( 2, '0' ) }`;\n};\n","/**\n * Pattern for valid CSS custom property names (e.g., '--my-color', '--jp-gray-10')\n */\nconst CSS_VAR_NAME_PATTERN = /^--[\\w-]+$/;\n\n/**\n * Resolves a CSS custom property (variable) to its computed value.\n * Handles multiple formats:\n * - Plain variable names: '--my-color'\n * - CSS var() syntax: 'var(--my-color)'\n * - CSS var() with fallback: 'var(--my-color, #ffffff)'\n * - Regular values (returned as-is): '#ffffff', 'red'\n *\n * @param value - A CSS variable name, var() expression, or regular value\n * @param element - Optional DOM element to resolve the variable from (defaults to document.documentElement)\n * @return The resolved value, fallback value, or null if unresolvable\n */\nexport const resolveCssVariable = (\n\tvalue: string,\n\telement?: HTMLElement | null\n): string | null => {\n\tif ( ! value ) {\n\t\treturn null;\n\t}\n\n\t// Check if it's a var() expression: var(--name) or var(--name, fallback)\n\t// Parse manually to avoid regex backtracking vulnerabilities\n\tif ( value.startsWith( 'var(' ) && value.endsWith( ')' ) ) {\n\t\tconst parsed = parseVarExpression( value );\n\n\t\tif ( parsed ) {\n\t\t\tconst resolved = resolveVariableName( parsed.varName, element );\n\n\t\t\treturn resolved || parsed.fallback;\n\t\t}\n\t}\n\n\t// Check if it's a plain variable name (starts with --)\n\tif ( value.startsWith( '--' ) ) {\n\t\treturn resolveVariableName( value, element );\n\t}\n\n\t// Return regular values as-is (e.g., '#ffffff', 'red')\n\treturn value;\n};\n\n/**\n * Parses a var() expression into its variable name and optional fallback.\n * Uses string manipulation instead of complex regex to avoid ReDoS.\n *\n * @param expr - A var() expression like 'var(--name)' or 'var(--name, fallback)'\n * @return Parsed result or null if invalid\n */\nfunction parseVarExpression( expr: string ): { varName: string; fallback: string | null } | null {\n\t// Remove 'var(' prefix and ')' suffix\n\tconst inner = expr.slice( 4, -1 ).trim();\n\n\tif ( ! inner.startsWith( '--' ) ) {\n\t\treturn null;\n\t}\n\n\t// Find the comma separator (if any)\n\tconst commaIndex = inner.indexOf( ',' );\n\n\tif ( commaIndex === -1 ) {\n\t\t// No fallback: var(--name)\n\t\tconst varName = inner.trim();\n\t\t// Validate variable name format\n\t\tif ( ! CSS_VAR_NAME_PATTERN.test( varName ) ) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { varName, fallback: null };\n\t}\n\n\t// Has fallback: var(--name, fallback)\n\tconst varName = inner.slice( 0, commaIndex ).trim();\n\n\t// Validate variable name format\n\tif ( ! CSS_VAR_NAME_PATTERN.test( varName ) ) {\n\t\treturn null;\n\t}\n\n\tconst fallback = inner.slice( commaIndex + 1 ).trim();\n\n\treturn { varName, fallback: fallback || null };\n}\n\n/**\n * Resolves a plain CSS variable name to its computed value.\n *\n * @param varName - A CSS variable name like '--my-color'\n * @param element - Optional DOM element to resolve from\n * @return The computed value or null\n */\nfunction resolveVariableName( varName: string, element?: HTMLElement | null ): string | null {\n\tif ( typeof window === 'undefined' || typeof document === 'undefined' ) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst targetElement = element || document.documentElement;\n\t\tconst computedValue = getComputedStyle( targetElement ).getPropertyValue( varName ).trim();\n\n\t\treturn computedValue || null;\n\t} catch {\n\t\t// Return null if getComputedStyle throws (e.g., detached element)\n\t\treturn null;\n\t}\n}\n"]}