layerchart 2.0.0-next.50 → 2.0.0-next.52

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 (70) hide show
  1. package/dist/components/Arc.svelte +12 -4
  2. package/dist/components/Arc.svelte.d.ts +4 -0
  3. package/dist/components/ArcLabel.svelte +259 -0
  4. package/dist/components/ArcLabel.svelte.d.ts +73 -0
  5. package/dist/components/ArcLabel.svelte.test.d.ts +1 -0
  6. package/dist/components/ArcLabel.svelte.test.js +235 -0
  7. package/dist/components/Axis.svelte +25 -0
  8. package/dist/components/Axis.svelte.d.ts +10 -0
  9. package/dist/components/Circle.svelte +82 -59
  10. package/dist/components/CircleLegend.svelte +389 -0
  11. package/dist/components/CircleLegend.svelte.d.ts +114 -0
  12. package/dist/components/Ellipse.svelte +83 -64
  13. package/dist/components/GeoLegend.svelte +404 -0
  14. package/dist/components/GeoLegend.svelte.d.ts +106 -0
  15. package/dist/components/GeoRaster.svelte +311 -0
  16. package/dist/components/GeoRaster.svelte.d.ts +61 -0
  17. package/dist/components/Grid.svelte +15 -0
  18. package/dist/components/Grid.svelte.d.ts +5 -0
  19. package/dist/components/Image.svelte +2 -2
  20. package/dist/components/Labels.svelte +46 -11
  21. package/dist/components/Labels.svelte.d.ts +7 -3
  22. package/dist/components/Legend.svelte +58 -3
  23. package/dist/components/Legend.svelte.d.ts +7 -0
  24. package/dist/components/Line.svelte +82 -62
  25. package/dist/components/Points.svelte +2 -2
  26. package/dist/components/Polygon.svelte +92 -56
  27. package/dist/components/Rect.svelte +113 -64
  28. package/dist/components/Rule.svelte +2 -0
  29. package/dist/components/Sankey.svelte +0 -2
  30. package/dist/components/Text.svelte +83 -52
  31. package/dist/components/__screenshots__/ArcLabel.svelte.test.ts/ArcLabel-defaults-placement-to-centroid--x-y-at-the-centroid--middle-anchors--1.png +0 -0
  32. package/dist/components/__screenshots__/ArcLabel.svelte.test.ts/ArcLabel-defaults-placement-to-centroid--x-y-at-the-centroid--middle-anchors--2.png +0 -0
  33. package/dist/components/charts/ArcChart.svelte +39 -2
  34. package/dist/components/charts/ArcChart.svelte.d.ts +12 -1
  35. package/dist/components/charts/PieChart.svelte +40 -2
  36. package/dist/components/charts/PieChart.svelte.d.ts +10 -0
  37. package/dist/components/index.d.ts +8 -0
  38. package/dist/components/index.js +8 -0
  39. package/dist/components/layers/Canvas.svelte +65 -48
  40. package/dist/components/layers/Canvas.svelte.d.ts +10 -0
  41. package/dist/contexts/canvas.d.ts +3 -0
  42. package/dist/server/ContextCapture.svelte +30 -0
  43. package/dist/server/ContextCapture.svelte.d.ts +8 -0
  44. package/dist/server/ServerChart.svelte +26 -0
  45. package/dist/server/ServerChart.svelte.d.ts +11 -0
  46. package/dist/server/TestBarChart.svelte +35 -0
  47. package/dist/server/TestBarChart.svelte.d.ts +14 -0
  48. package/dist/server/TestLineChart.svelte +35 -0
  49. package/dist/server/TestLineChart.svelte.d.ts +14 -0
  50. package/dist/server/captureStore.d.ts +8 -0
  51. package/dist/server/captureStore.js +18 -0
  52. package/dist/server/index.d.ts +137 -0
  53. package/dist/server/index.js +141 -0
  54. package/dist/server/renderChart.ssr.test.d.ts +1 -0
  55. package/dist/server/renderChart.ssr.test.js +205 -0
  56. package/dist/server/renderTree.d.ts +8 -0
  57. package/dist/server/renderTree.js +29 -0
  58. package/dist/states/__screenshots__/chart.svelte.test.ts/ChartState-geo-projection-skips-markInfo-should-not-derive-x-y-accessors-from-marks-when-geo-projection-is-active-1.png +0 -0
  59. package/dist/states/__screenshots__/chart.svelte.test.ts/ChartState-geo-projection-skips-markInfo-should-not-derive-x-y-accessors-from-marks-when-geo-projection-is-active-2.png +0 -0
  60. package/dist/states/chart.svelte.d.ts +5 -1
  61. package/dist/states/chart.svelte.js +18 -3
  62. package/dist/states/chart.svelte.test.js +110 -0
  63. package/dist/states/geo.svelte.d.ts +5 -1
  64. package/dist/states/geo.svelte.js +80 -68
  65. package/dist/utils/arcText.svelte.d.ts +7 -1
  66. package/dist/utils/arcText.svelte.js +8 -4
  67. package/dist/utils/canvas.js +29 -10
  68. package/dist/utils/canvas.svelte.test.js +2 -2
  69. package/dist/utils/motion.svelte.js +14 -0
  70. package/package.json +7 -1
@@ -149,6 +149,10 @@
149
149
  centroid: [number, number];
150
150
  boundingBox: DOMRect;
151
151
  value: number;
152
+ startAngle: number;
153
+ endAngle: number;
154
+ innerRadius: number;
155
+ outerRadius: number;
152
156
  getTrackTextProps: GetArcTextProps;
153
157
  getArcTextProps: GetArcTextProps;
154
158
  },
@@ -370,8 +374,8 @@
370
374
  {
371
375
  startAngle: () => trackStartAngle,
372
376
  endAngle: () => trackEndAngle,
373
- outerRadius: () => trackOuterRadius + (opts.outerPadding ? opts.outerPadding : 0),
374
- innerRadius: () => trackInnerRadius,
377
+ outerRadius: () => trackOuterRadius + (opts.outerPadding ?? 0),
378
+ innerRadius: () => trackInnerRadius - (opts.innerPadding ?? 0),
375
379
  cornerRadius: () => trackCornerRadius,
376
380
  centroid: () => trackArcCentroid,
377
381
  },
@@ -385,8 +389,8 @@
385
389
  {
386
390
  startAngle: () => startAngle,
387
391
  endAngle: () => arcEndAngle,
388
- outerRadius: () => outerRadius + (opts.outerPadding ? opts.outerPadding : 0),
389
- innerRadius: () => innerRadius,
392
+ outerRadius: () => outerRadius + (opts.outerPadding ?? 0),
393
+ innerRadius: () => innerRadius - (opts.innerPadding ?? 0),
390
394
  cornerRadius: () => cornerRadius,
391
395
  centroid: () => trackArcCentroid,
392
396
  },
@@ -432,6 +436,10 @@
432
436
  centroid: trackArcCentroid,
433
437
  boundingBox,
434
438
  value: motionEndAngle.current,
439
+ startAngle,
440
+ endAngle: arcEndAngle,
441
+ innerRadius,
442
+ outerRadius,
435
443
  getTrackTextProps: getTrackTextProps,
436
444
  getArcTextProps: getArcTextProps,
437
445
  })}
@@ -124,6 +124,10 @@ export type ArcPropsWithoutHTML = {
124
124
  centroid: [number, number];
125
125
  boundingBox: DOMRect;
126
126
  value: number;
127
+ startAngle: number;
128
+ endAngle: number;
129
+ innerRadius: number;
130
+ outerRadius: number;
127
131
  getTrackTextProps: GetArcTextProps;
128
132
  getArcTextProps: GetArcTextProps;
129
133
  }
@@ -0,0 +1,259 @@
1
+ <script lang="ts" module>
2
+ import type { PathProps } from './Path.svelte';
3
+ import type { TextProps } from './Text.svelte';
4
+ import type { GetArcTextProps, ArcTextOptions } from '../utils/arcText.svelte.js';
5
+
6
+ /**
7
+ * Placement options for `ArcLabel`.
8
+ * - `centroid`: at the arc centroid (horizontal text)
9
+ * - `centroid-rotated`: at the arc centroid, rotated to follow the arc tangent
10
+ * - `centroid-radial`: at the arc centroid, rotated to read radially (center → outside)
11
+ * - `inner` / `middle` / `outer`: along the inner / middle / outer arc path
12
+ * - `callout`: outside the arc connected by a polyline with a bend
13
+ */
14
+ export type ArcLabelPlacement =
15
+ | 'centroid'
16
+ | 'centroid-rotated'
17
+ | 'centroid-radial'
18
+ | 'inner'
19
+ | 'middle'
20
+ | 'outer'
21
+ | 'callout';
22
+
23
+ export type ArcLabelConfig = {
24
+ /**
25
+ * The placement of the label.
26
+ * @default 'centroid'
27
+ */
28
+ placement?: ArcLabelPlacement;
29
+
30
+ /**
31
+ * Length of the radial portion of the callout leader line (from the outer
32
+ * arc edge outward to the bend point).
33
+ * @default 16
34
+ */
35
+ calloutLineLength?: number;
36
+
37
+ /**
38
+ * Length of the horizontal portion of the callout leader line after the
39
+ * bend before the label text.
40
+ * @default 12
41
+ */
42
+ calloutLabelOffset?: number;
43
+
44
+ /**
45
+ * Padding between the bend point on the leader line and the label text.
46
+ * @default 4
47
+ */
48
+ calloutPadding?: number;
49
+
50
+ /**
51
+ * Props applied to the leader line `<Path>` when using `callout` placement.
52
+ * Because `<Path>` is used (instead of a raw `<polyline>`), the leader line
53
+ * renders in SVG and Canvas chart layers alike.
54
+ */
55
+ line?: Omit<PathProps, 'pathData'>;
56
+
57
+ /**
58
+ * Radial offset for the label, interpreted per-placement:
59
+ * - `centroid` / `centroid-rotated` / `centroid-radial`: shifts the label
60
+ * radially from the arc centroid (positive = outward).
61
+ * - `inner` / `middle` / `outer`: added to `outerPadding` (padding of the
62
+ * arc path the text runs along).
63
+ * - `callout`: added to `calloutLineLength` (radial portion of the leader
64
+ * line).
65
+ * @default 0
66
+ */
67
+ offset?: number;
68
+ } & ArcTextOptions &
69
+ Omit<TextProps, 'path'>;
70
+
71
+ export type ArcLabelProps = {
72
+ /**
73
+ * Function returned from the `Arc` children snippet used to position the
74
+ * label for `inner`, `middle`, `outer`, and `outer-radial` placements.
75
+ */
76
+ getArcTextProps?: GetArcTextProps;
77
+
78
+ /** Centroid `[x, y]` of the arc (from `Arc` children snippet) */
79
+ centroid?: [number, number];
80
+
81
+ /** Arc start angle in radians (from `Arc` children snippet) */
82
+ startAngle?: number;
83
+
84
+ /** Arc end angle in radians (from `Arc` children snippet) */
85
+ endAngle?: number;
86
+
87
+ /** Arc inner radius (from `Arc` children snippet) */
88
+ innerRadius?: number;
89
+
90
+ /** Arc outer radius (from `Arc` children snippet) */
91
+ outerRadius?: number;
92
+ } & ArcLabelConfig;
93
+ </script>
94
+
95
+ <script lang="ts">
96
+ import Path from './Path.svelte';
97
+ import Text from './Text.svelte';
98
+ import { radiansToDegrees } from '../utils/math.js';
99
+
100
+ let {
101
+ getArcTextProps,
102
+ centroid,
103
+ startAngle,
104
+ endAngle,
105
+ innerRadius,
106
+ outerRadius,
107
+ placement = 'centroid',
108
+ startOffset,
109
+ outerPadding,
110
+ calloutLineLength = 16,
111
+ calloutLabelOffset = 12,
112
+ calloutPadding = 4,
113
+ line,
114
+ offset = 0,
115
+ ...restProps
116
+ }: ArcLabelProps = $props();
117
+
118
+ const midAngle = $derived(
119
+ startAngle != null && endAngle != null ? (startAngle + endAngle) / 2 : 0
120
+ );
121
+
122
+ // Offset the centroid radially along the mid-angle direction.
123
+ // Used by the `centroid` / `centroid-rotated` / `centroid-radial` placements.
124
+ const offsetCentroid = $derived.by(() => {
125
+ if (!centroid) return centroid;
126
+ if (!offset || startAngle == null || endAngle == null) return centroid;
127
+ const angle = midAngle - Math.PI / 2;
128
+ return [centroid[0] + Math.cos(angle) * offset, centroid[1] + Math.sin(angle) * offset] as [
129
+ number,
130
+ number,
131
+ ];
132
+ });
133
+
134
+ // Route the shared `offset` to the placement-appropriate radial padding:
135
+ // - `outer`: outward from the outer edge (`outerPadding`)
136
+ // - `inner`: inward from the inner edge (`innerPadding`)
137
+ // - `middle`: both, so the middle path shifts by half in each direction
138
+ const effectiveOuterPadding = $derived.by(() => {
139
+ const base = outerPadding ?? 0;
140
+ if (placement === 'outer') return base + offset;
141
+ if (placement === 'middle') return base + offset;
142
+ return base;
143
+ });
144
+ const effectiveInnerPadding = $derived.by(() => {
145
+ if (placement === 'inner') return offset;
146
+ if (placement === 'middle') return offset;
147
+ return 0;
148
+ });
149
+ // `calloutLineLength` for `callout` placement gets the shared `offset` added
150
+ // on top of the default/explicit length.
151
+ const effectiveCalloutLineLength = $derived(calloutLineLength + offset);
152
+
153
+ // Rotation in degrees to apply to the text at the centroid.
154
+ // - `centroid-rotated`: follow the arc tangent direction
155
+ // - `centroid-radial`: read radially outward (center → outer edge)
156
+ const centroidRotation = $derived.by(() => {
157
+ if (startAngle == null || endAngle == null) return 0;
158
+ let deg = radiansToDegrees(midAngle);
159
+ if (placement === 'centroid-radial') {
160
+ // Rotate so text reads along the radial direction
161
+ deg = deg - 90;
162
+ } else if (placement !== 'centroid-rotated') {
163
+ return 0;
164
+ }
165
+ // Normalize to [-180, 180]
166
+ deg = ((deg + 180) % 360) - 180;
167
+ // Flip text on the side where it would be upside-down so it remains readable
168
+ if (deg > 90) deg -= 180;
169
+ else if (deg < -90) deg += 180;
170
+ return deg;
171
+ });
172
+
173
+ const calloutGeometry = $derived.by(() => {
174
+ if (placement !== 'callout' || startAngle == null || endAngle == null || outerRadius == null) {
175
+ return null;
176
+ }
177
+
178
+ // Match d3-shape arc convention: 0 radians = 12 o'clock, increasing clockwise.
179
+ const angle = midAngle - Math.PI / 2;
180
+ const cos = Math.cos(angle);
181
+ const sin = Math.sin(angle);
182
+
183
+ // Point on the outer arc edge at the mid-angle
184
+ const x0 = cos * outerRadius;
185
+ const y0 = sin * outerRadius;
186
+
187
+ // Bend point: extend radially outward from the edge
188
+ const bendRadius = outerRadius + effectiveCalloutLineLength;
189
+ const x1 = cos * bendRadius;
190
+ const y1 = sin * bendRadius;
191
+
192
+ // Label point: extend horizontally toward the chart side the arc lives on
193
+ const onRightSide = cos >= 0;
194
+ const x2 = x1 + (onRightSide ? calloutLabelOffset : -calloutLabelOffset);
195
+ const y2 = y1;
196
+
197
+ return {
198
+ pathData: `M${x0},${y0}L${x1},${y1}L${x2},${y2}`,
199
+ labelX: x2 + (onRightSide ? calloutPadding : -calloutPadding),
200
+ labelY: y2,
201
+ textAnchor: (onRightSide ? 'start' : 'end') as 'start' | 'end',
202
+ };
203
+ });
204
+
205
+ const arcTextProps = $derived.by(() => {
206
+ if (placement === 'centroid') {
207
+ if (offsetCentroid) {
208
+ return {
209
+ x: offsetCentroid[0],
210
+ y: offsetCentroid[1],
211
+ textAnchor: 'middle' as const,
212
+ verticalAnchor: 'middle' as const,
213
+ };
214
+ }
215
+ return getArcTextProps?.('centroid', { startOffset, outerPadding }) ?? {};
216
+ }
217
+
218
+ if (placement === 'centroid-rotated' || placement === 'centroid-radial') {
219
+ if (offsetCentroid) {
220
+ return {
221
+ x: offsetCentroid[0],
222
+ y: offsetCentroid[1],
223
+ textAnchor: 'middle' as const,
224
+ verticalAnchor: 'middle' as const,
225
+ transform: `rotate(${centroidRotation}, ${offsetCentroid[0]}, ${offsetCentroid[1]})`,
226
+ };
227
+ }
228
+ return getArcTextProps?.('centroid', { startOffset, outerPadding }) ?? {};
229
+ }
230
+
231
+ if (placement === 'callout') {
232
+ const g = calloutGeometry;
233
+ if (g) {
234
+ return {
235
+ x: g.labelX,
236
+ y: g.labelY,
237
+ textAnchor: g.textAnchor,
238
+ verticalAnchor: 'middle' as const,
239
+ };
240
+ }
241
+ return {};
242
+ }
243
+
244
+ // inner / middle / outer
245
+ return (
246
+ getArcTextProps?.(placement, {
247
+ startOffset,
248
+ outerPadding: effectiveOuterPadding,
249
+ innerPadding: effectiveInnerPadding,
250
+ }) ?? {}
251
+ );
252
+ });
253
+ </script>
254
+
255
+ {#if placement === 'callout' && calloutGeometry}
256
+ <Path pathData={calloutGeometry.pathData} {...line} />
257
+ {/if}
258
+
259
+ <Text {...arcTextProps} {...restProps} />
@@ -0,0 +1,73 @@
1
+ import type { PathProps } from './Path.svelte';
2
+ import type { TextProps } from './Text.svelte';
3
+ import type { GetArcTextProps, ArcTextOptions } from '../utils/arcText.svelte.js';
4
+ /**
5
+ * Placement options for `ArcLabel`.
6
+ * - `centroid`: at the arc centroid (horizontal text)
7
+ * - `centroid-rotated`: at the arc centroid, rotated to follow the arc tangent
8
+ * - `centroid-radial`: at the arc centroid, rotated to read radially (center → outside)
9
+ * - `inner` / `middle` / `outer`: along the inner / middle / outer arc path
10
+ * - `callout`: outside the arc connected by a polyline with a bend
11
+ */
12
+ export type ArcLabelPlacement = 'centroid' | 'centroid-rotated' | 'centroid-radial' | 'inner' | 'middle' | 'outer' | 'callout';
13
+ export type ArcLabelConfig = {
14
+ /**
15
+ * The placement of the label.
16
+ * @default 'centroid'
17
+ */
18
+ placement?: ArcLabelPlacement;
19
+ /**
20
+ * Length of the radial portion of the callout leader line (from the outer
21
+ * arc edge outward to the bend point).
22
+ * @default 16
23
+ */
24
+ calloutLineLength?: number;
25
+ /**
26
+ * Length of the horizontal portion of the callout leader line after the
27
+ * bend before the label text.
28
+ * @default 12
29
+ */
30
+ calloutLabelOffset?: number;
31
+ /**
32
+ * Padding between the bend point on the leader line and the label text.
33
+ * @default 4
34
+ */
35
+ calloutPadding?: number;
36
+ /**
37
+ * Props applied to the leader line `<Path>` when using `callout` placement.
38
+ * Because `<Path>` is used (instead of a raw `<polyline>`), the leader line
39
+ * renders in SVG and Canvas chart layers alike.
40
+ */
41
+ line?: Omit<PathProps, 'pathData'>;
42
+ /**
43
+ * Radial offset for the label, interpreted per-placement:
44
+ * - `centroid` / `centroid-rotated` / `centroid-radial`: shifts the label
45
+ * radially from the arc centroid (positive = outward).
46
+ * - `inner` / `middle` / `outer`: added to `outerPadding` (padding of the
47
+ * arc path the text runs along).
48
+ * - `callout`: added to `calloutLineLength` (radial portion of the leader
49
+ * line).
50
+ * @default 0
51
+ */
52
+ offset?: number;
53
+ } & ArcTextOptions & Omit<TextProps, 'path'>;
54
+ export type ArcLabelProps = {
55
+ /**
56
+ * Function returned from the `Arc` children snippet used to position the
57
+ * label for `inner`, `middle`, `outer`, and `outer-radial` placements.
58
+ */
59
+ getArcTextProps?: GetArcTextProps;
60
+ /** Centroid `[x, y]` of the arc (from `Arc` children snippet) */
61
+ centroid?: [number, number];
62
+ /** Arc start angle in radians (from `Arc` children snippet) */
63
+ startAngle?: number;
64
+ /** Arc end angle in radians (from `Arc` children snippet) */
65
+ endAngle?: number;
66
+ /** Arc inner radius (from `Arc` children snippet) */
67
+ innerRadius?: number;
68
+ /** Arc outer radius (from `Arc` children snippet) */
69
+ outerRadius?: number;
70
+ } & ArcLabelConfig;
71
+ declare const ArcLabel: import("svelte").Component<ArcLabelProps, {}, "">;
72
+ type ArcLabel = ReturnType<typeof ArcLabel>;
73
+ export default ArcLabel;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,235 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { render } from 'vitest-browser-svelte';
3
+ import { page } from 'vitest/browser';
4
+ import TestHarness, { componentTestId } from './tests/TestHarness.svelte';
5
+ import Arc from './Arc.svelte';
6
+ import ArcLabel from './ArcLabel.svelte';
7
+ const defaultArcProps = {
8
+ fill: 'currentColor',
9
+ value: 50,
10
+ innerRadius: 70,
11
+ outerRadius: 140,
12
+ };
13
+ describe('ArcLabel', () => {
14
+ it('renders a text element with the supplied value at the centroid', async () => {
15
+ render(TestHarness, {
16
+ chartProps: { height: 400, padding: '50' },
17
+ component: Arc,
18
+ componentProps: defaultArcProps,
19
+ childComponents: [
20
+ {
21
+ component: ArcLabel,
22
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
23
+ centroid,
24
+ startAngle,
25
+ endAngle,
26
+ innerRadius,
27
+ outerRadius,
28
+ getArcTextProps,
29
+ value: 'hello',
30
+ 'data-testid': 'arc-label',
31
+ }),
32
+ },
33
+ ],
34
+ });
35
+ const arc = page.getByTestId(componentTestId);
36
+ await expect.element(arc).toBeInTheDocument();
37
+ const label = page.getByTestId('arc-label');
38
+ await expect.element(label).toBeInTheDocument();
39
+ await expect.element(label).toHaveTextContent('hello');
40
+ });
41
+ it('does not render a polyline for non-callout placements', async () => {
42
+ render(TestHarness, {
43
+ chartProps: { height: 400, padding: '50' },
44
+ component: Arc,
45
+ componentProps: defaultArcProps,
46
+ childComponents: [
47
+ {
48
+ component: ArcLabel,
49
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
50
+ centroid,
51
+ startAngle,
52
+ endAngle,
53
+ innerRadius,
54
+ outerRadius,
55
+ getArcTextProps,
56
+ value: 'Centroid',
57
+ 'data-testid': 'arc-label',
58
+ }),
59
+ },
60
+ ],
61
+ });
62
+ const text = page.getByTestId('arc-label');
63
+ await expect.element(text).toBeInTheDocument();
64
+ const chart = page.getByTestId('test-lc-chart');
65
+ expect(chart.element().querySelector('polyline')).toBeNull();
66
+ });
67
+ it('applies a rotation transform for centroid-rotated placement', async () => {
68
+ render(TestHarness, {
69
+ chartProps: { height: 400, padding: '50' },
70
+ component: Arc,
71
+ componentProps: {
72
+ ...defaultArcProps,
73
+ // 90° slice on the right side so midAngle = 45°
74
+ startAngle: 0,
75
+ endAngle: Math.PI / 2,
76
+ },
77
+ childComponents: [
78
+ {
79
+ component: ArcLabel,
80
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
81
+ centroid,
82
+ startAngle,
83
+ endAngle,
84
+ innerRadius,
85
+ outerRadius,
86
+ getArcTextProps,
87
+ placement: 'centroid-rotated',
88
+ value: 'Rotated',
89
+ 'data-testid': 'arc-label',
90
+ }),
91
+ },
92
+ ],
93
+ });
94
+ const el = page.getByTestId('arc-label');
95
+ await expect.element(el).toBeInTheDocument();
96
+ const transform = el.element().getAttribute('transform') ?? '';
97
+ // midAngle = 45°, tangent rotation → 45°
98
+ expect(transform).toMatch(/rotate\(45/);
99
+ });
100
+ it('applies a rotation transform for centroid-radial placement', async () => {
101
+ render(TestHarness, {
102
+ chartProps: { height: 400, padding: '50' },
103
+ component: Arc,
104
+ componentProps: {
105
+ ...defaultArcProps,
106
+ startAngle: 0,
107
+ endAngle: Math.PI / 2,
108
+ },
109
+ childComponents: [
110
+ {
111
+ component: ArcLabel,
112
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
113
+ centroid,
114
+ startAngle,
115
+ endAngle,
116
+ innerRadius,
117
+ outerRadius,
118
+ getArcTextProps,
119
+ placement: 'centroid-radial',
120
+ value: 'Radial',
121
+ 'data-testid': 'arc-label',
122
+ }),
123
+ },
124
+ ],
125
+ });
126
+ const el = page.getByTestId('arc-label');
127
+ await expect.element(el).toBeInTheDocument();
128
+ const transform = el.element().getAttribute('transform') ?? '';
129
+ // midAngle = 45°, radial rotation = midAngle - 90 = -45°
130
+ expect(transform).toMatch(/rotate\(-45/);
131
+ });
132
+ it('renders a polyline leader line for callout placement', async () => {
133
+ render(TestHarness, {
134
+ chartProps: { height: 400, padding: '50' },
135
+ component: Arc,
136
+ componentProps: {
137
+ ...defaultArcProps,
138
+ startAngle: 0,
139
+ endAngle: Math.PI / 2,
140
+ },
141
+ childComponents: [
142
+ {
143
+ component: ArcLabel,
144
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
145
+ centroid,
146
+ startAngle,
147
+ endAngle,
148
+ innerRadius,
149
+ outerRadius,
150
+ getArcTextProps,
151
+ placement: 'callout',
152
+ value: 'Callout',
153
+ 'data-testid': 'arc-label',
154
+ }),
155
+ },
156
+ ],
157
+ });
158
+ const label = page.getByTestId('arc-label');
159
+ await expect.element(label).toBeInTheDocument();
160
+ // The leader line is a <path> with two line segments (edge → bend → label)
161
+ const chart = page.getByTestId('test-lc-chart');
162
+ const paths = Array.from(chart.element().querySelectorAll('path'));
163
+ const leader = paths.find((p) => {
164
+ const d = p.getAttribute('d') ?? '';
165
+ // Leader-line pathData has the form `M${x0},${y0}L${x1},${y1}L${x2},${y2}`
166
+ // with two `L` segments. The arc `<path>` has `A` arc commands.
167
+ return /^M[^A]*L[^A]*L[^A]*$/.test(d);
168
+ });
169
+ expect(leader).toBeDefined();
170
+ });
171
+ it('does not render a polyline element for callout placement', async () => {
172
+ render(TestHarness, {
173
+ chartProps: { height: 400, padding: '50' },
174
+ component: Arc,
175
+ componentProps: {
176
+ ...defaultArcProps,
177
+ startAngle: 0,
178
+ endAngle: Math.PI / 2,
179
+ },
180
+ childComponents: [
181
+ {
182
+ component: ArcLabel,
183
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
184
+ centroid,
185
+ startAngle,
186
+ endAngle,
187
+ innerRadius,
188
+ outerRadius,
189
+ getArcTextProps,
190
+ placement: 'callout',
191
+ value: 'NoPolyline',
192
+ 'data-testid': 'arc-label',
193
+ }),
194
+ },
195
+ ],
196
+ });
197
+ await expect.element(page.getByTestId('arc-label')).toBeInTheDocument();
198
+ const chart = page.getByTestId('test-lc-chart');
199
+ expect(chart.element().querySelector('polyline')).toBeNull();
200
+ });
201
+ it('delegates inner/middle/outer placements to getArcTextProps (text on path)', async () => {
202
+ render(TestHarness, {
203
+ chartProps: { height: 400, padding: '50' },
204
+ component: Arc,
205
+ componentProps: {
206
+ ...defaultArcProps,
207
+ startAngle: 0,
208
+ endAngle: Math.PI / 2,
209
+ },
210
+ childComponents: [
211
+ {
212
+ component: ArcLabel,
213
+ props: ({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps }) => ({
214
+ centroid,
215
+ startAngle,
216
+ endAngle,
217
+ innerRadius,
218
+ outerRadius,
219
+ getArcTextProps,
220
+ placement: 'middle',
221
+ value: 'Middle',
222
+ 'data-testid': 'arc-label',
223
+ }),
224
+ },
225
+ ],
226
+ });
227
+ const label = page.getByTestId('arc-label');
228
+ await expect.element(label).toBeInTheDocument();
229
+ // Text along a path is rendered via <textPath href="#..."> inside the text element
230
+ const textPath = label.element().querySelector('textPath');
231
+ expect(textPath).not.toBeNull();
232
+ // Default startOffset should be 50% (centered along the arc)
233
+ expect(textPath?.getAttribute('startOffset')).toBe('50%');
234
+ });
235
+ });