|
42 | 42 |
|
43 | 43 | /** |
44 | 44 | * The placement of the label relative to the point. |
45 | | - * `smart` dynamically positions labels based on neighboring point values (peak, trough, rising, falling). |
| 45 | + * - `outside`: outside the bar/point. |
| 46 | + * - `inside`: inside the bar/point near the value edge. |
| 47 | + * - `middle`: aligned to the value edge with a middle anchor. |
| 48 | + * - `center`: centered within the bar body (between the value edge and baseline). |
| 49 | + * - `smart`: dynamically positions labels based on neighboring point values (peak, trough, rising, falling). |
46 | 50 | * @default 'outside' |
47 | 51 | */ |
48 | | - placement?: 'inside' | 'outside' | 'center' | 'smart'; |
| 52 | + placement?: 'inside' | 'outside' | 'middle' | 'center' | 'smart'; |
49 | 53 |
|
50 | 54 | /** |
51 | 55 | * The offset of the label from the point |
52 | 56 | * |
53 | | - * @default placement === 'center' ? 0 : 4 |
| 57 | + * @default placement === 'center' || placement === 'middle' ? 0 : 4 |
54 | 58 | */ |
55 | 59 | offset?: number; |
56 | 60 |
|
|
81 | 85 | import { getChartContext } from '$lib/contexts/chart.js'; |
82 | 86 | import Group from './Group.svelte'; |
83 | 87 | import { extractLayerProps } from '$lib/utils/attributes.js'; |
| 88 | + import { createDimensionGetter } from '$lib/utils/rect.svelte.js'; |
84 | 89 |
|
85 | 90 | const ctx = getChartContext(); |
86 | 91 |
|
|
94 | 99 | y, |
95 | 100 | seriesKey, |
96 | 101 | placement = 'outside', |
97 | | - offset = placement === 'center' ? 0 : 4, |
| 102 | + offset = placement === 'center' || placement === 'middle' ? 0 : 4, |
98 | 103 | format, |
99 | 104 | key = (_: any, i: number) => i, |
100 | 105 | children: childrenProp, |
|
104 | 109 | ...restProps |
105 | 110 | }: LabelsProps<TData> = $props(); |
106 | 111 |
|
| 112 | + // Used to compute the bar's bounding rect for `center` placement |
| 113 | + const getDimensions = $derived(createDimensionGetter(ctx, () => ({ x, y }))); |
| 114 | +
|
107 | 115 | // TODO: Should we let `Points` handle opacity for children snippet as well? |
108 | 116 | let series = $derived(ctx.series.series.find((s) => s.key === seriesKey)); |
109 | 117 | let derivedOpacity = $derived( |
|
146 | 154 |
|
147 | 155 | if (isScaleBand(ctx.yScale)) { |
148 | 156 | // Position label left/right on horizontal bars |
149 | | - if (isLowEdge) { |
| 157 | + if (placement === 'center') { |
| 158 | + // Center within the bar body |
| 159 | + const dims = getDimensions(point.data) ?? { x: point.x, y: point.y, width: 0, height: 0 }; |
| 160 | + result = { |
| 161 | + value: formattedValue, |
| 162 | + fill: fillValue, |
| 163 | + x: dims.x + dims.width / 2, |
| 164 | + y: dims.y + dims.height / 2, |
| 165 | + textAnchor: 'middle', |
| 166 | + verticalAnchor: 'middle', |
| 167 | + capHeight: '.6rem', |
| 168 | + }; |
| 169 | + } else if (isLowEdge) { |
150 | 170 | // left |
151 | 171 | result = { |
152 | 172 | value: formattedValue, |
153 | 173 | fill: fillValue, |
154 | 174 | x: point.x + (placement === 'outside' ? -offset : offset), |
155 | 175 | y: point.y, |
156 | | - textAnchor: placement === 'outside' ? 'end' : 'start', |
| 176 | + textAnchor: |
| 177 | + placement === 'middle' ? 'middle' : placement === 'outside' ? 'end' : 'start', |
157 | 178 | verticalAnchor: 'middle', |
158 | 179 | capHeight: '.6rem', |
159 | 180 | }; |
|
164 | 185 | fill: fillValue, |
165 | 186 | x: point.x + (placement === 'outside' ? offset : -offset), |
166 | 187 | y: point.y, |
167 | | - textAnchor: placement === 'outside' ? 'start' : 'end', |
| 188 | + textAnchor: |
| 189 | + placement === 'middle' ? 'middle' : placement === 'outside' ? 'start' : 'end', |
168 | 190 | verticalAnchor: 'middle', |
169 | 191 | capHeight: '.6rem', |
170 | 192 | }; |
171 | 193 | } |
172 | 194 | } else { |
173 | 195 | // Position label top/bottom on vertical bars |
174 | | - if (isLowEdge) { |
| 196 | + if (placement === 'center') { |
| 197 | + // Center within the bar body |
| 198 | + const dims = getDimensions(point.data) ?? { x: point.x, y: point.y, width: 0, height: 0 }; |
| 199 | + result = { |
| 200 | + value: formattedValue, |
| 201 | + fill: fillValue, |
| 202 | + x: dims.x + dims.width / 2, |
| 203 | + y: dims.y + dims.height / 2, |
| 204 | + capHeight: '.6rem', |
| 205 | + textAnchor: 'middle', |
| 206 | + verticalAnchor: 'middle', |
| 207 | + }; |
| 208 | + } else if (isLowEdge) { |
175 | 209 | // bottom |
176 | 210 | result = { |
177 | 211 | value: formattedValue, |
|
181 | 215 | capHeight: '.6rem', |
182 | 216 | textAnchor: 'middle', |
183 | 217 | verticalAnchor: |
184 | | - placement === 'center' ? 'middle' : placement === 'outside' ? 'start' : 'end', |
| 218 | + placement === 'middle' ? 'middle' : placement === 'outside' ? 'start' : 'end', |
185 | 219 | }; |
186 | 220 | } else { |
187 | 221 | // top |
|
193 | 227 | capHeight: '.6rem', |
194 | 228 | textAnchor: 'middle', |
195 | 229 | verticalAnchor: |
196 | | - placement === 'center' ? 'middle' : placement === 'outside' ? 'end' : 'start', |
| 230 | + placement === 'middle' ? 'middle' : placement === 'outside' ? 'end' : 'start', |
197 | 231 | }; |
198 | 232 | } |
199 | 233 | } |
|
271 | 305 | --fill-color: var(--color-surface-content, currentColor); |
272 | 306 | --stroke-color: var(--color-surface-100, light-dark(white, black)); |
273 | 307 |
|
274 | | - &[data-placement='inside'] { |
| 308 | + &[data-placement='inside'], |
| 309 | + &[data-placement='center'] { |
275 | 310 | --fill-color: var(--color-surface-100, light-dark(white, black)); |
276 | 311 | --stroke-color: var(--color-surface-content, currentColor); |
277 | 312 | } |
|
0 commit comments