layerchart 2.0.0-next.51 → 2.0.0-next.53
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.
- package/dist/components/Arc.svelte +12 -4
- package/dist/components/Arc.svelte.d.ts +4 -0
- package/dist/components/ArcLabel.svelte +259 -0
- package/dist/components/ArcLabel.svelte.d.ts +73 -0
- package/dist/components/ArcLabel.svelte.test.d.ts +1 -0
- package/dist/components/ArcLabel.svelte.test.js +235 -0
- package/dist/components/CircleLegend.svelte +389 -0
- package/dist/components/CircleLegend.svelte.d.ts +114 -0
- package/dist/components/GeoLegend.svelte +435 -0
- package/dist/components/GeoLegend.svelte.d.ts +117 -0
- package/dist/components/Labels.svelte +46 -11
- package/dist/components/Labels.svelte.d.ts +7 -3
- package/dist/components/Legend.svelte +58 -3
- package/dist/components/Legend.svelte.d.ts +7 -0
- package/dist/components/__screenshots__/ArcLabel.svelte.test.ts/ArcLabel-defaults-placement-to-centroid--x-y-at-the-centroid--middle-anchors--1.png +0 -0
- package/dist/components/__screenshots__/ArcLabel.svelte.test.ts/ArcLabel-defaults-placement-to-centroid--x-y-at-the-centroid--middle-anchors--2.png +0 -0
- package/dist/components/charts/ArcChart.svelte +39 -2
- package/dist/components/charts/ArcChart.svelte.d.ts +12 -1
- package/dist/components/charts/PieChart.svelte +38 -0
- package/dist/components/charts/PieChart.svelte.d.ts +10 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +6 -0
- package/dist/utils/arcText.svelte.d.ts +7 -1
- package/dist/utils/arcText.svelte.js +8 -4
- package/package.json +1 -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
|
|
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
|
|
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
|
+
});
|