Skip to content

Commit d505f8e

Browse files
authored
New ArcLabel component (#817)
* feat(ArcLabel): New component for positioning text labels on arc segments. Resolves #7 * Add labels to Partition/sunburst example
1 parent 81757bf commit d505f8e

25 files changed

Lines changed: 1007 additions & 120 deletions

.changeset/arclabel-component.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'layerchart': minor
3+
---
4+
5+
feat(ArcLabel): New component for positioning text labels on arc segments
6+
7+
`ArcLabel` is a new marking component for placing text (and optional leader lines) relative to an arc. It's used internally by `PieChart` and `ArcChart` when the `labels` prop is set, but can also be rendered directly inside an `Arc` children snippet.
8+
9+
Supported placements:
10+
11+
- `centroid` — at the arc centroid (horizontal text, default)
12+
- `centroid-rotated` — at the centroid, rotated to follow the arc tangent, flipped where needed so text stays upright
13+
- `centroid-radial` — at the centroid, rotated to read along the radial direction (center → outer edge)
14+
- `inner` / `middle` / `outer` — along the inner, medial, or outer arc path (centered via `startOffset: '50%'` by default)
15+
- `callout` — outside the arc with a leader line that bends horizontally to the label
16+
17+
`ArcLabel` accepts a single `offset` prop that is routed to the placement-appropriate radial padding (centroid offset, `innerPadding`/`outerPadding`, or `calloutLineLength`), plus `calloutLineLength` / `calloutLabelOffset` / `calloutPadding` for fine-grained control of callout leader lines. The leader line renders via the `Path` primitive, so it works in both SVG and Canvas chart layers.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'layerchart': minor
3+
---
4+
5+
breaking(Arc): Center arc text along path by default for `inner`/`middle`/`outer` positions
6+
7+
`getArcTextProps('inner' | 'middle' | 'outer')` now defaults to `startOffset: '50%'` with `textAnchor: 'middle'`, centering the text along the arc path rather than anchoring it at the arc start. When an explicit `startOffset` is provided, the anchor falls back to `'start'` so the text begins at that position (matching prior behavior for callers that set a start offset).
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'layerchart': minor
3+
---
4+
5+
feat(Arc): Add `innerPadding` option to `getArcTextProps` / `getTrackTextProps`
6+
7+
`ArcTextOptions` now supports an `innerPadding` option, symmetric to the existing `outerPadding`. Positive values shrink the inner radius used to build the `inner`/`middle` arc text paths, moving text inward (toward the chart center). Previously, offsetting an `inner`-placed arc label away from the arc edge required overriding the path manually; now it works the same as `outerPadding` does for `outer` text.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'layerchart': minor
3+
---
4+
5+
feat(PieChart/ArcChart): Add top-level `labels` prop
6+
7+
`PieChart` and `ArcChart` now accept a `labels` prop that renders text labels on each arc without requiring a custom `arc` snippet. Pass `true` to enable defaults (centroid placement, default value accessor), or an object to configure any `ArcLabel` props — placement, offset, value accessor, callout line lengths, leader line style, text class, etc.
8+
9+
```svelte
10+
<PieChart
11+
{data}
12+
labels={{ placement: 'callout', value: 'fruit' }}
13+
/>
14+
```

docs/src/content/components/ArcChart.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ related: [Chart, Pie]
99

1010
:example{ name="basic" showCode }
1111

12+
### Labels
13+
14+
Enable arc labels via the `labels` prop. Pass `true` for defaults, or an object to configure placement and other [`ArcLabel`](/docs/components/ArcLabel) props.
15+
16+
:example{ name="labels" }
17+
1218
<!-- ## Examples
1319
1420
### Basic
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
description: Marking component which positions text labels on pie and arc chart segments, including along the arc path, at the centroid (horizontal, tangent-rotated, or radially-rotated), outside the arc, and with callout leader lines.
3+
category: marks
4+
layers: [svg]
5+
related: [Arc, Pie, PieChart, ArcChart, Labels]
6+
---
7+
8+
## Usage
9+
10+
`ArcLabel` positions a text label (and optional leader line) relative to an arc. It's used by `PieChart` and `ArcChart` internally when the `labels` prop is set, but can also be used directly inside an `Arc` children snippet for full control.
11+
12+
:example{ component="PieChart" name="labels" showCode }
13+
14+
### Placements
15+
16+
- `centroid` — at the arc centroid (horizontal text, default).
17+
- `centroid-rotated` — at the centroid, rotated to follow the arc tangent. The rotation is flipped where needed so text stays upright.
18+
- `centroid-radial` — at the centroid, rotated to read along the radial direction (center → outer edge). Useful for sunburst-style labels.
19+
- `inner` — along the inner arc path.
20+
- `middle` — along the medial arc path (midway between inner and outer).
21+
- `outer` — along the outer arc path.
22+
- `callout` — outside the arc with a polyline leader line that bends horizontally toward the label.
23+
24+
### Offsets
25+
26+
Depending on the placement, different offset props apply:
27+
28+
- `outerPadding` — adds padding to the outer radius used for `inner` / `middle` / `outer` arc text paths.
29+
- `startOffset` — percentage along the arc path where the text starts. Defaults to `'50%'` (centered).
30+
- `calloutLineLength` — length of the radial portion of the `callout` leader line.
31+
- `calloutLabelOffset` — length of the horizontal portion of the `callout` leader line after the bend.
32+
- `calloutPadding` — gap between the end of the leader line and the label text.
33+
34+
### Callout leader lines
35+
36+
`placement="callout"` draws a line from the outer arc edge to the label with a single bend. The line is rendered via the [`Path`](/docs/components/Path) component so it works in both SVG and Canvas chart layers.
37+
38+
Three props control the geometry:
39+
40+
- `calloutLineLength` — length of the radial (first) segment, from the outer arc edge out to the bend.
41+
- `calloutLabelOffset` — length of the horizontal (second) segment, from the bend to the label.
42+
- `calloutPadding` — gap between the end of the line and the label text.
43+
44+
:example{ component="PieChart" name="labels-callout" }
45+
46+
Customize the line itself via the `line` prop, which forwards props to `<Path>`:
47+
48+
```svelte
49+
<PieChart
50+
{data}
51+
labels={{
52+
placement: 'callout',
53+
value: 'fruit',
54+
line: { stroke: 'currentColor', strokeWidth: 1.5, class: 'opacity-50' }
55+
}}
56+
/>
57+
```
58+
59+
### Using `ArcLabel` directly
60+
61+
When using `Arc` directly (outside of `PieChart`/`ArcChart`), render `ArcLabel` inside the `Arc` children snippet. The snippet exposes `centroid`, `startAngle`, `endAngle`, `innerRadius`, `outerRadius`, and `getArcTextProps` — all of which `ArcLabel` accepts.
62+
63+
```svelte
64+
<Arc {...arcProps}>
65+
{#snippet children({ centroid, startAngle, endAngle, innerRadius, outerRadius, getArcTextProps })}
66+
<ArcLabel
67+
{centroid}
68+
{startAngle}
69+
{endAngle}
70+
{innerRadius}
71+
{outerRadius}
72+
{getArcTextProps}
73+
placement="centroid-radial"
74+
value="Label text"
75+
/>
76+
{/snippet}
77+
</Arc>
78+
```

docs/src/content/components/PieChart.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ related: [Chart, Pie]
99

1010
:example{ name="basic" showCode }
1111

12+
### Labels
13+
14+
Enable arc labels via the `labels` prop. Pass `true` for defaults, or an object to configure placement and other [`ArcLabel`](/docs/components/ArcLabel) props.
15+
16+
:example{ name="labels" }
17+
1218
<!-- ## Examples
1319
1420
### Basic
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<script lang="ts">
2+
import { ArcChart, type ArcLabelPlacement } from 'layerchart';
3+
import { MenuField, RangeField } from 'svelte-ux';
4+
import { fruitColors } from '$lib/utils/fruitColors';
5+
import { longData } from '$lib/utils/data';
6+
7+
const placements: { label: string; value: ArcLabelPlacement }[] = [
8+
{ label: 'Centroid', value: 'centroid' },
9+
{ label: 'Centroid (rotated)', value: 'centroid-rotated' },
10+
{ label: 'Centroid (radial)', value: 'centroid-radial' },
11+
{ label: 'Inner', value: 'inner' },
12+
{ label: 'Middle', value: 'middle' },
13+
{ label: 'Outer', value: 'outer' },
14+
{ label: 'Callout', value: 'callout' }
15+
];
16+
17+
let placement: ArcLabelPlacement = $state('centroid');
18+
let offset = $state(0);
19+
20+
const data = longData.filter((d) => d.year === 2019);
21+
export { data };
22+
</script>
23+
24+
<div class="grid grid-cols-[1fr_1fr] gap-2 mb-4">
25+
<MenuField
26+
label="Placement"
27+
options={placements}
28+
bind:value={placement}
29+
stepper
30+
classes={{ menuIcon: 'hidden' }}
31+
/>
32+
<RangeField label="Offset" bind:value={offset} min={-40} max={60} />
33+
</div>
34+
35+
<ArcChart
36+
{data}
37+
key="fruit"
38+
value="value"
39+
cRange={fruitColors}
40+
outerRadius={-60}
41+
innerRadius={-20}
42+
cornerRadius={10}
43+
padding={{ top: 24, bottom: 24, left: 80, right: 80 }}
44+
labels={{
45+
placement,
46+
offset,
47+
value: 'fruit',
48+
class: 'text-xs fill-surface-content'
49+
}}
50+
height={400}
51+
/>

docs/src/examples/components/ArcChart/series-labels.svelte

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { Arc, ArcChart, Text } from 'layerchart';
2+
import { ArcChart } from 'layerchart';
33
44
const data = [
55
{ key: 'move', value: 400, maxValue: 1000, color: '#ef4444' },
@@ -23,18 +23,11 @@
2323
outerRadius={-25}
2424
innerRadius={-20}
2525
cornerRadius={10}
26+
labels={{
27+
placement: 'middle',
28+
startOffset: '0%',
29+
value: 'key',
30+
class: 'fill-black pointer-events-none text-xs'
31+
}}
2632
height={180}
27-
>
28-
{#snippet arc({ context, props, seriesIndex })}
29-
<Arc {...props}>
30-
{#snippet children({ getArcTextProps })}
31-
<Text
32-
{...getArcTextProps('middle')}
33-
value={context.series.visibleSeries[seriesIndex].key}
34-
class="fill-black pointer-events-none"
35-
font-size="12px"
36-
/>
37-
{/snippet}
38-
</Arc>
39-
{/snippet}
40-
</ArcChart>
33+
/>

docs/src/examples/components/Chord/basic.svelte

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { scaleOrdinal } from 'd3-scale';
33
import { schemeTableau10 } from 'd3-scale-chromatic';
4-
import { Chart, Layer, Chord, Ribbon, Arc, Group, Text } from 'layerchart';
4+
import { Chart, Layer, Chord, Ribbon, Arc, ArcLabel } from 'layerchart';
55
66
// Population migration between regions (in thousands)
77
const names = ['Asia', 'Europe', 'Africa', 'Americas', 'Oceania'];
@@ -41,21 +41,17 @@
4141
{outerRadius}
4242
fill={color(names[group.index])}
4343
stroke="none"
44-
/>
45-
<Group
46-
x={(outerRadius + 6) * Math.cos((group.startAngle + group.endAngle) / 2 - Math.PI / 2)}
47-
y={(outerRadius + 6) * Math.sin((group.startAngle + group.endAngle) / 2 - Math.PI / 2)}
4844
>
49-
<Text
50-
value={names[group.index]}
51-
textAnchor={(group.startAngle + group.endAngle) / 2 > Math.PI ? 'end' : 'start'}
52-
verticalAnchor="middle"
53-
class="text-xs font-medium"
54-
transform="rotate({(((group.startAngle + group.endAngle) / 2) * 180) / Math.PI -
55-
90 +
56-
((group.startAngle + group.endAngle) / 2 > Math.PI ? 180 : 0)})"
57-
/>
58-
</Group>
45+
{#snippet children(arcProps)}
46+
<ArcLabel
47+
{...arcProps}
48+
placement="centroid-rotated"
49+
offset={(outerRadius - innerRadius) / 2 + 6}
50+
value={names[group.index]}
51+
class="text-xs font-medium"
52+
/>
53+
{/snippet}
54+
</Arc>
5955
{/each}
6056
{/snippet}
6157
</Chord>

0 commit comments

Comments
 (0)