@automattic/charts 4.11.0 → 4.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.cjs +856 -236
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.css +19 -13
  5. package/dist/index.d.cts +43 -4
  6. package/dist/index.d.ts +43 -4
  7. package/dist/index.js +855 -237
  8. package/dist/index.js.map +1 -1
  9. package/package.json +10 -10
  10. package/src/charts/bar-chart/bar-chart.tsx +134 -138
  11. package/src/charts/bar-chart/index.ts +3 -0
  12. package/src/charts/bar-chart/private/band-tooltip.tsx +41 -15
  13. package/src/charts/bar-chart/private/bar-layer.tsx +132 -0
  14. package/src/charts/bar-chart/private/comparison-bars-geometry.ts +22 -34
  15. package/src/charts/bar-chart/private/comparison-bars.tsx +9 -18
  16. package/src/charts/bar-chart/private/comparison-constants.ts +4 -10
  17. package/src/charts/bar-chart/private/grouped-bars.ts +68 -0
  18. package/src/charts/bar-chart/private/index.ts +7 -1
  19. package/src/charts/bar-chart/private/overlap-bars.ts +183 -0
  20. package/src/charts/bar-chart/private/selected-bar-ring.tsx +84 -0
  21. package/src/charts/bar-chart/private/slot-tooltip-members.tsx +29 -0
  22. package/src/charts/bar-chart/private/slotted-bar-series.tsx +84 -0
  23. package/src/charts/bar-chart/private/test/comparison-bars-geometry.test.ts +4 -11
  24. package/src/charts/bar-chart/private/test/comparison-bars.test.tsx +2 -3
  25. package/src/charts/bar-chart/private/test/grouped-bars.test.ts +87 -0
  26. package/src/charts/bar-chart/private/test/overlap-bars.test.ts +159 -0
  27. package/src/charts/bar-chart/private/use-bar-stops.ts +89 -0
  28. package/src/charts/bar-chart/private/use-overlap-slots.ts +194 -0
  29. package/src/charts/bar-chart/private/value-domain.ts +0 -10
  30. package/src/charts/bar-chart/test/bar-chart-overlap.test.tsx +347 -0
  31. package/src/charts/bar-chart/test/bar-chart.test.tsx +66 -19
  32. package/src/charts/bar-chart/test/use-bar-legend-items.test.tsx +81 -0
  33. package/src/charts/bar-chart/types.ts +3 -0
  34. package/src/charts/bar-chart/use-bar-colors.ts +46 -0
  35. package/src/charts/bar-chart/use-bar-legend-items.ts +87 -0
  36. package/src/charts/line-chart/line-chart.tsx +22 -17
  37. package/src/charts/line-chart/test/line-chart.test.tsx +165 -1
  38. package/src/charts/pie-chart/pie-chart.tsx +7 -2
  39. package/src/charts/pie-semi-circle-chart/pie-semi-circle-chart.tsx +7 -2
  40. package/src/charts/private/pie-keyboard-navigation/index.ts +0 -1
  41. package/src/charts/private/{pie-keyboard-navigation/pie-keyboard-navigation.module.scss → selected-ring/selected-ring.module.scss} +11 -6
  42. package/src/charts/private/selected-ring/selected-ring.tsx +73 -0
  43. package/src/charts/private/{pie-keyboard-navigation/test/selected-segment-ring.test.ts → selected-ring/test/selected-ring.test.ts} +1 -1
  44. package/src/components/legend/hooks/test/use-chart-legend-items.test.tsx +20 -0
  45. package/src/components/legend/hooks/use-chart-legend-items.ts +27 -3
  46. package/src/components/tooltip/xy-chart-tooltip.tsx +3 -2
  47. package/src/index.ts +8 -2
  48. package/src/providers/chart-context/global-charts-provider.tsx +2 -0
  49. package/src/providers/chart-context/test/chart-context.test.tsx +0 -13
  50. package/src/providers/chart-context/themes.ts +0 -1
  51. package/src/providers/chart-context/types.ts +2 -0
  52. package/src/test-utils/selected-bar-ring.ts +14 -0
  53. package/src/types.ts +10 -2
  54. package/src/charts/private/pie-keyboard-navigation/selected-segment-ring.tsx +0 -47
@@ -31,10 +31,10 @@ import { pickLabelTextColor, resolveLabelRoles } from '../private/label-text-col
31
31
  import {
32
32
  orderArcsForNavigation,
33
33
  PieSelectionOutput,
34
- SelectedSegmentRing,
35
34
  usePieKeyboardNavigation,
36
35
  } from '../private/pie-keyboard-navigation';
37
36
  import { RadialWipeAnimation } from '../private/radial-wipe-animation/';
37
+ import { SelectedRing } from '../private/selected-ring/selected-ring';
38
38
  import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
39
39
  import { withResponsive, ResponsiveConfig } from '../private/with-responsive';
40
40
  import styles from './pie-chart.module.scss';
@@ -503,7 +503,12 @@ const PieChartInternal = ( {
503
503
  { /* Unmasked so the entry wipe cannot hide the focus indicator. */ }
504
504
  { selectedArc && (
505
505
  <Group top={ centerY } left={ centerX }>
506
- <SelectedSegmentRing chartId={ chartId } d={ path( selectedArc ) || '' } />
506
+ <SelectedRing
507
+ chartId={ chartId }
508
+ d={ path( selectedArc ) || '' }
509
+ side="inside"
510
+ testId="pie-selected-ring"
511
+ />
507
512
  </Group>
508
513
  ) }
509
514
  { ! allSegmentsHidden && (
@@ -26,10 +26,10 @@ import { ChartLayout } from '../private/chart-layout';
26
26
  import {
27
27
  orderArcsForNavigation,
28
28
  PieSelectionOutput,
29
- SelectedSegmentRing,
30
29
  usePieKeyboardNavigation,
31
30
  } from '../private/pie-keyboard-navigation';
32
31
  import { RadialWipeAnimation } from '../private/radial-wipe-animation';
32
+ import { SelectedRing } from '../private/selected-ring/selected-ring';
33
33
  import { getAllHiddenMessage, SvgEmptyState } from '../private/svg-empty-state';
34
34
  import { withResponsive } from '../private/with-responsive';
35
35
  import styles from './pie-semi-circle-chart.module.scss';
@@ -429,7 +429,12 @@ const PieSemiCircleChartInternal: FC< PieSemiCircleChartProps > = ( {
429
429
  { /* Unmasked so the entry wipe cannot hide the focus indicator. */ }
430
430
  { selectedArc && (
431
431
  <Group top={ height } left={ width / 2 }>
432
- <SelectedSegmentRing chartId={ chartId } d={ path( selectedArc ) || '' } />
432
+ <SelectedRing
433
+ chartId={ chartId }
434
+ d={ path( selectedArc ) || '' }
435
+ side="inside"
436
+ testId="pie-selected-ring"
437
+ />
433
438
  </Group>
434
439
  ) }
435
440
  { ! allSegmentsHidden && (
@@ -1,4 +1,3 @@
1
1
  export { orderArcsForNavigation } from './order-arcs-for-navigation';
2
2
  export { PieSelectionOutput } from './pie-selection-output';
3
- export { SelectedSegmentRing } from './selected-segment-ring';
4
3
  export { usePieKeyboardNavigation } from './use-pie-keyboard-navigation';
@@ -1,16 +1,21 @@
1
- // The clip to the segment removes the outer half of each stroke,
2
- // so widths are doubled.
3
- .selected-ring__contrast,
4
- .selected-ring__focus {
1
+ // The clip removes one half of each stroke, so widths are doubled.
2
+ .selected-ring__far,
3
+ .selected-ring__edge {
5
4
  fill: none;
6
5
  }
7
6
 
7
+ .selected-ring__far {
8
+ stroke-width: calc(4 * var(--wpds-border-width-focus));
9
+ }
10
+
11
+ .selected-ring__edge {
12
+ stroke-width: calc(2 * var(--wpds-border-width-focus));
13
+ }
14
+
8
15
  .selected-ring__contrast {
9
16
  stroke: var(--a8c-charts-color-background, #fff);
10
- stroke-width: calc(4 * var(--wpds-border-width-focus));
11
17
  }
12
18
 
13
19
  .selected-ring__focus {
14
20
  stroke: var(--wpds-color-stroke-focus);
15
- stroke-width: calc(2 * var(--wpds-border-width-focus));
16
21
  }
@@ -0,0 +1,73 @@
1
+ import clsx from 'clsx';
2
+ import styles from './selected-ring.module.scss';
3
+
4
+ type Tone = 'contrast' | 'focus';
5
+
6
+ /** Which side of the shape's edge the ring is drawn on. */
7
+ export type SelectedRingSide = 'inside' | 'outside';
8
+
9
+ interface SelectedRingProps {
10
+ /** The chart's id, which keeps the clip path unique on a page with several charts. */
11
+ chartId: string;
12
+ /** The selected shape's path. */
13
+ d: string;
14
+ /** `inside` keeps the ring clear of the SVG edge; `outside` leaves the shape's fill uncovered. */
15
+ side: SelectedRingSide;
16
+ testId?: string;
17
+ }
18
+
19
+ // The tone at the edge is drawn last, over the wider tone beyond it.
20
+ const TONES: Record< SelectedRingSide, [ far: Tone, edge: Tone ] > = {
21
+ inside: [ 'contrast', 'focus' ],
22
+ outside: [ 'focus', 'contrast' ],
23
+ };
24
+
25
+ const EVERYWHERE = 'M-1e6 -1e6H1e6V1e6H-1e6Z';
26
+
27
+ /**
28
+ * Encodes `value` for an SVG id, losslessly so distinct chart ids never share a clip path.
29
+ *
30
+ * @param value - Any string.
31
+ * @return `value` with each character outside `[A-Za-z0-9-]` written as `_<hex code point>_`.
32
+ */
33
+ export const toSvgId = ( value: string ) =>
34
+ Array.from( value, character =>
35
+ /[A-Za-z0-9-]/.test( character )
36
+ ? character
37
+ : `_${ character.codePointAt( 0 )?.toString( 16 ) }_`
38
+ ).join( '' );
39
+
40
+ /**
41
+ * Marks a selected shape with a focus-colored ring and a contrast ring, so it reads against any fill.
42
+ *
43
+ * @param {SelectedRingProps} props - Component props
44
+ * @return {JSX.Element} The ring
45
+ */
46
+ export const SelectedRing = ( { chartId, d, side, testId }: SelectedRingProps ) => {
47
+ const clipId = `selected-ring-clip-${ toSvgId( chartId ) }`;
48
+ const clipPath = `url(#${ clipId })`;
49
+ const [ far, edge ] = TONES[ side ];
50
+
51
+ return (
52
+ <g pointerEvents="none" aria-hidden="true">
53
+ <clipPath id={ clipId }>
54
+ { side === 'inside' ? (
55
+ <path d={ d } />
56
+ ) : (
57
+ <path d={ `${ EVERYWHERE } ${ d }` } clipRule="evenodd" />
58
+ ) }
59
+ </clipPath>
60
+ <path
61
+ d={ d }
62
+ clipPath={ clipPath }
63
+ className={ clsx( styles[ 'selected-ring__far' ], styles[ `selected-ring__${ far }` ] ) }
64
+ />
65
+ <path
66
+ d={ d }
67
+ clipPath={ clipPath }
68
+ className={ clsx( styles[ 'selected-ring__edge' ], styles[ `selected-ring__${ edge }` ] ) }
69
+ data-testid={ testId }
70
+ />
71
+ </g>
72
+ );
73
+ };
@@ -1,4 +1,4 @@
1
- import { toSvgId } from '../selected-segment-ring';
1
+ import { toSvgId } from '../selected-ring';
2
2
 
3
3
  describe( 'toSvgId', () => {
4
4
  it.each( [
@@ -421,6 +421,26 @@ describe( 'useChartLegendItems', () => {
421
421
  expect( result.current[ 2 ].color ).toBe( result.current[ 0 ].color );
422
422
  } );
423
423
 
424
+ test( 'colors each swatch, comparison items included, from seriesColors by series index', () => {
425
+ const seriesColors = [ '#111111', '#222222', '#333333', '#444444' ];
426
+ const { result } = renderHook(
427
+ () =>
428
+ useChartLegendItems( twoMetricData, {
429
+ collapseGroups: true,
430
+ comparisonSeriesItems: perMetricLabel,
431
+ seriesColors,
432
+ } ),
433
+ { wrapper }
434
+ );
435
+
436
+ expect( result.current.map( item => item.color ) ).toEqual( [
437
+ '#111111',
438
+ '#333333',
439
+ '#222222',
440
+ '#444444',
441
+ ] );
442
+ } );
443
+
424
444
  test( 'adds nothing when no series is a comparison', () => {
425
445
  const { result } = renderHook(
426
446
  () => useChartLegendItems( [ comparisonData[ 0 ] ], { comparisonItem: true } ),
@@ -41,6 +41,25 @@ export interface ChartLegendOptions {
41
41
  comparisonItem?: SeriesChartLegendConfig[ 'comparisonItem' ];
42
42
  /** One static item per comparison series instead; see `SeriesChartLegendConfig`. */
43
43
  comparisonSeriesItems?: SeriesChartLegendConfig[ 'comparisonSeriesItems' ];
44
+ /** Each series' swatch color, by index, in place of its series color. */
45
+ seriesColors?: string[];
46
+ }
47
+
48
+ /**
49
+ * Wraps `getElementStyles` so a swatch takes the color given for its series.
50
+ * @param getElementStyles - Function to get element styles
51
+ * @param seriesColors - Each series' swatch color, by index
52
+ * @return Element styles with the given color
53
+ */
54
+ function withSeriesColors(
55
+ getElementStyles: ( params: GetElementStylesParams ) => ElementStyles,
56
+ seriesColors: string[]
57
+ ): ( params: GetElementStylesParams ) => ElementStyles {
58
+ return params => {
59
+ const styles = getElementStyles( params );
60
+
61
+ return { ...styles, color: seriesColors[ params.index ] ?? styles.color };
62
+ };
44
63
  }
45
64
 
46
65
  /**
@@ -386,9 +405,10 @@ export function useChartLegendItems<
386
405
  collapseGroups = false,
387
406
  comparisonItem = false,
388
407
  comparisonSeriesItems,
408
+ seriesColors,
389
409
  renderGlyph,
390
410
  } = options;
391
- const { getElementStyles } = useGlobalChartsContext();
411
+ const { getElementStyles: getSeriesStyles } = useGlobalChartsContext();
392
412
 
393
413
  return useMemo( () => {
394
414
  if ( ! data || ! Array.isArray( data ) || data.length === 0 ) {
@@ -398,6 +418,9 @@ export function useChartLegendItems<
398
418
  // Handle SeriesData (multiple series with data points)
399
419
  if ( 'data' in data[ 0 ] ) {
400
420
  const seriesData = data as SeriesData[];
421
+ const getElementStyles = seriesColors
422
+ ? withSeriesColors( getSeriesStyles, seriesColors )
423
+ : getSeriesStyles;
401
424
  const items = processSeriesData(
402
425
  seriesData,
403
426
  getElementStyles,
@@ -440,7 +463,7 @@ export function useChartLegendItems<
440
463
  // Handle DataPointDate or DataPointPercentageCalculated (single data points)
441
464
  return processPointData(
442
465
  data as ( DataPointDate | DataPointPercentageCalculated )[],
443
- getElementStyles,
466
+ getSeriesStyles,
444
467
  showValues,
445
468
  legendValueDisplay,
446
469
  withGlyph,
@@ -450,7 +473,7 @@ export function useChartLegendItems<
450
473
  );
451
474
  }, [
452
475
  data,
453
- getElementStyles,
476
+ getSeriesStyles,
454
477
  showValues,
455
478
  legendValueDisplay,
456
479
  withGlyph,
@@ -458,6 +481,7 @@ export function useChartLegendItems<
458
481
  collapseGroups,
459
482
  comparisonItem,
460
483
  comparisonSeriesItems,
484
+ seriesColors,
461
485
  renderGlyph,
462
486
  legendShape,
463
487
  ] );
@@ -152,7 +152,8 @@ export const XYChartTooltip: React.FC< XYChartTooltipProps > = ( {
152
152
  hasKeyboardSelection.current = true;
153
153
 
154
154
  if ( mode === 'group' ) {
155
- // Show all series at the selected data point index in single tooltip.
155
+ // visx merges each shown datum into the previous tooltip data, so start empty or a series dropped from `series` keeps its row.
156
+ closeTooltipNow();
156
157
  series.forEach( ( s, index ) => {
157
158
  if ( selectedIndex < s.data.length ) {
158
159
  const datum = s.data[ selectedIndex ];
@@ -292,7 +293,7 @@ export const useKeyboardNavigation = ( {
292
293
  const previousTotalPoints = useRef( totalPoints );
293
294
  const previousVisibleSeriesKey = useRef( visibleSeriesKey );
294
295
 
295
- // Hiding or showing a series moves every index onto a different bar. The count alone misses a
296
+ // Hiding or showing a series re-points every index. The count alone misses a
296
297
  // swap that keeps it the same, such as hiding one series while revealing another of equal length.
297
298
  // Clear rather than clamp once focus has left the chart: a new tooltip would pull focus back.
298
299
  useEffect( () => {
package/src/index.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  // Charts
2
2
  export { AreaChart, AreaChartUnresponsive } from './charts/area-chart';
3
- export { BarChart, BarChartUnresponsive } from './charts/bar-chart';
3
+ export {
4
+ BarChart,
5
+ BarChartUnresponsive,
6
+ useBarColors,
7
+ useBarLegendItems,
8
+ } from './charts/bar-chart';
4
9
  export { BarListChart, BarListChartUnresponsive } from './charts/bar-list-chart';
5
10
  export { ConversionFunnelChart } from './charts/conversion-funnel-chart';
6
11
  export { GeoChart, GeoChartUnresponsive } from './charts/geo-chart';
@@ -76,6 +81,7 @@ export type {
76
81
  LegendShape,
77
82
  LegendShapeLabel,
78
83
  LegendShapeRenderProps,
84
+ BarChartLayout,
79
85
  } from './types';
80
86
  export type * from './visx/types';
81
87
  export type { PieChartProps, PieChartRenderTooltipParams } from './charts/pie-chart';
@@ -109,7 +115,7 @@ export type { LegendProps, BaseLegendProps, ChartLegendOptions } from './compone
109
115
 
110
116
  // Previously available via '@automattic/charts/bar-chart', '@automattic/charts/line-chart', etc.
111
117
  export type { AreaChartProps } from './charts/area-chart';
112
- export type { BarChartProps, BandHighlightSelection } from './charts/bar-chart';
118
+ export type { BarChartProps, BandHighlightSelection, BarLegendOptions } from './charts/bar-chart';
113
119
  export type {
114
120
  BarListChartProps,
115
121
  RenderLabelProps,
@@ -371,6 +371,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
371
371
  theme: providerTheme,
372
372
  formatting,
373
373
  getElementStyles,
374
+ backgroundColor: colorCache.background,
374
375
  toggleSeriesVisibility,
375
376
  setSeriesVisibility,
376
377
  setChartHiddenSeries,
@@ -388,6 +389,7 @@ export const GlobalChartsProvider: FC< GlobalChartsProviderProps > = ( {
388
389
  providerTheme,
389
390
  formatting,
390
391
  getElementStyles,
392
+ colorCache.background,
391
393
  toggleSeriesVisibility,
392
394
  setSeriesVisibility,
393
395
  setChartHiddenSeries,
@@ -5,7 +5,6 @@ import { useChartId } from '../hooks/use-chart-id';
5
5
  import { useChartRegistration } from '../hooks/use-chart-registration';
6
6
  import { useGlobalChartsContext } from '../hooks/use-global-charts-context';
7
7
  import { createPaletteGenerator } from '../private/palette-generator';
8
- import { defaultTheme } from '../themes';
9
8
  import type { BaseLegendItem } from '../../../components/legend';
10
9
  import type { SeriesData } from '../../../types';
11
10
  import type { GlobalChartsContextValue } from '../types';
@@ -1168,12 +1167,9 @@ describe( 'ChartContext', () => {
1168
1167
  legendShape: 'rect',
1169
1168
  } );
1170
1169
 
1171
- // Should get theme legend shape styles (not line styles), with the comparison bar
1172
- // opacity layered on so the swatch matches the translucent comparison bar.
1173
1170
  expect( styles.shapeStyles ).toEqual( {
1174
1171
  fill: '#LEGEND1',
1175
1172
  stroke: '#BORDER1',
1176
- opacity: 0.5,
1177
1173
  } );
1178
1174
  } );
1179
1175
  } );
@@ -2144,13 +2140,4 @@ describe( 'ChartContext', () => {
2144
2140
  } );
2145
2141
  } );
2146
2142
  } );
2147
-
2148
- describe( 'defaultTheme', () => {
2149
- it( 'exposes default barChart comparison styles', () => {
2150
- expect( defaultTheme.barChart.barStyles.comparison ).toEqual( {
2151
- widthFactor: 1.5,
2152
- opacity: 0.5,
2153
- } );
2154
- } );
2155
- } );
2156
2143
  } );
@@ -49,7 +49,6 @@ const defaultTheme: CompleteChartTheme = {
49
49
  barStyles: {
50
50
  comparison: {
51
51
  widthFactor: 1.5,
52
- opacity: 0.5,
53
52
  },
54
53
  },
55
54
  },
@@ -41,6 +41,8 @@ export interface GlobalChartsContextValue {
41
41
  /** Locale and time zone every date the charts print is rendered in. */
42
42
  formatting: ChartFormatting;
43
43
  getElementStyles: ( params: GetElementStylesParams ) => ElementStyles;
44
+ /** The chart background as an opaque hex, for colors that must keep contrast against it. Optional so a hand-built context value still type-checks. */
45
+ backgroundColor?: string;
44
46
  // Series visibility management shared by charts, legends, and programmatic controls.
45
47
  toggleSeriesVisibility: ( chartId: string, seriesLabel: string ) => void;
46
48
  setSeriesVisibility: ( chartId: string, seriesLabel: string, visible: boolean ) => void;
@@ -0,0 +1,14 @@
1
+ import { screen } from '@testing-library/react';
2
+
3
+ const NUMBER = /-?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/gi;
4
+
5
+ /**
6
+ * The box the bar chart's selected ring outlines, read from its path.
7
+ *
8
+ * @return The ring's x, y, width and height.
9
+ */
10
+ export const selectedBarRingBounds = () => {
11
+ const d = screen.getByTestId( 'bar-chart-selected-ring' ).getAttribute( 'd' ) ?? '';
12
+ const [ x, y, width, height ] = ( d.match( NUMBER ) ?? [] ).map( Number );
13
+ return { x, y, width, height };
14
+ };
package/src/types.ts CHANGED
@@ -306,14 +306,22 @@ export type SeriesData = {
306
306
 
307
307
  /**
308
308
  * Visual styling for a bar series of a given semantic type (e.g. 'comparison').
309
- * `widthFactor` is the bar width relative to the primary bar slot (1.5 = 150%);
310
- * `opacity` sets the shadow translucency.
309
+ * `widthFactor` is the bar width relative to the primary bar slot (1.5 = 150%).
311
310
  */
312
311
  export type BarStyles = {
313
312
  widthFactor?: number;
313
+ /**
314
+ * Draws the comparison bar translucent, in its primary's color, instead of the opaque tones the chart picks.
315
+ * @deprecated Comparison bars are opaque. Leave this unset; set the comparison series' `options.stroke` for a color of its own.
316
+ */
314
317
  opacity?: number;
315
318
  };
316
319
 
320
+ /**
321
+ * How a bar chart places its series in a category. `grouped` puts each primary series in a slot of its own; `overlap` puts every primary in one slot and every comparison in a second, and overlaps the `group`s within each slot.
322
+ */
323
+ export type BarChartLayout = 'grouped' | 'overlap';
324
+
317
325
  export type MultipleDataPointsDate = {
318
326
  label: string;
319
327
  data: DataPointDate[];
@@ -1,47 +0,0 @@
1
- import styles from './pie-keyboard-navigation.module.scss';
2
-
3
- interface SelectedSegmentRingProps {
4
- /** The chart's id, which keeps the clip path unique on a page with several charts. */
5
- chartId: string;
6
- /** The selected segment's path, from the same arc generator that draws the segment. */
7
- d: string;
8
- }
9
-
10
- /**
11
- * Encodes `value` for an SVG id, losslessly so distinct chart ids never share a clip path.
12
- *
13
- * @param value - Any string.
14
- * @return `value` with each character outside `[A-Za-z0-9-]` written as `_<hex code point>_`.
15
- */
16
- export const toSvgId = ( value: string ) =>
17
- Array.from( value, character =>
18
- /[A-Za-z0-9-]/.test( character )
19
- ? character
20
- : `_${ character.codePointAt( 0 )?.toString( 16 ) }_`
21
- ).join( '' );
22
-
23
- /**
24
- * Marks the selected segment with a two-tone ring drawn inside its edge, so the SVG edge cannot cut it off.
25
- *
26
- * @param {SelectedSegmentRingProps} props - Component props
27
- * @return {JSX.Element} The ring
28
- */
29
- export const SelectedSegmentRing = ( { chartId, d }: SelectedSegmentRingProps ) => {
30
- const clipId = `pie-selected-ring-clip-${ toSvgId( chartId ) }`;
31
- const clipPath = `url(#${ clipId })`;
32
-
33
- return (
34
- <g pointerEvents="none" aria-hidden="true">
35
- <clipPath id={ clipId }>
36
- <path d={ d } />
37
- </clipPath>
38
- <path d={ d } clipPath={ clipPath } className={ styles[ 'selected-ring__contrast' ] } />
39
- <path
40
- d={ d }
41
- clipPath={ clipPath }
42
- className={ styles[ 'selected-ring__focus' ] }
43
- data-testid="pie-selected-ring"
44
- />
45
- </g>
46
- );
47
- };