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.
@@ -0,0 +1,435 @@
1
+ <script lang="ts" module>
2
+ import type { Placement } from './types.js';
3
+ import { asAny, type Without } from '../utils/types.js';
4
+
5
+ export type GeoLegendUnits = 'km' | 'mi';
6
+
7
+ export type GeoLegendVariant = 'bracket' | 'alternating';
8
+
9
+ export type GeoLegendPropsWithoutHTML = {
10
+ /**
11
+ * Units to display.
12
+ *
13
+ * @default 'mi'
14
+ */
15
+ units?: GeoLegendUnits;
16
+
17
+ /**
18
+ * Visual style of the bar.
19
+ * - `'bracket'`: top rule with downward brackets at each tick (default)
20
+ * - `'alternating'`: alternating filled/unfilled segments between ticks
21
+ *
22
+ * @default 'bracket'
23
+ */
24
+ variant?: GeoLegendVariant;
25
+
26
+ /**
27
+ * Explicit distance to represent (in `units`). When omitted, a "nice" round
28
+ * value is chosen so the bar covers roughly 25% of the chart width.
29
+ */
30
+ distance?: number;
31
+
32
+ /**
33
+ * Number of tick subdivisions of the bar.
34
+ *
35
+ * @default 4
36
+ */
37
+ ticks?: number;
38
+
39
+ /**
40
+ * Where to place the tick labels relative to the bar. Useful for stacking
41
+ * two legends (e.g. kilometers + miles) tightly.
42
+ *
43
+ * @default 'bottom'
44
+ */
45
+ labelPlacement?: 'top' | 'bottom';
46
+
47
+ /**
48
+ * Format for the tick labels. Receives the distance in `units`.
49
+ */
50
+ tickFormat?: FormatType | FormatConfig | ((value: number) => string);
51
+
52
+ /**
53
+ * The font size of the tick labels.
54
+ *
55
+ * @default 10
56
+ */
57
+ tickFontSize?: number;
58
+
59
+ /**
60
+ * The font size of the title.
61
+ *
62
+ * @default 10
63
+ */
64
+ titleFontSize?: number;
65
+
66
+ /**
67
+ * The thickness of the bar.
68
+ *
69
+ * @default 4
70
+ */
71
+ height?: number;
72
+
73
+ /**
74
+ * The title of the legend.
75
+ *
76
+ * @default ''
77
+ */
78
+ title?: string;
79
+
80
+ /**
81
+ * Reference point in chart pixel coordinates used to compute the
82
+ * pixels-per-distance ratio. Defaults to the center of the chart's plot
83
+ * area, which is generally a reasonable approximation away from the poles.
84
+ */
85
+ referencePoint?: [number, number];
86
+
87
+ /**
88
+ * Scale of the projection originally used to pre-project the data, for
89
+ * charts that render pre-projected topologies via `geoIdentity`. For
90
+ * example, the `us-atlas` `counties-albers-10m` / `states-albers-10m`
91
+ * topologies are pre-projected with `geoAlbersUsa().scale(1300)`, so pass
92
+ * `referenceScale={1300}`. When provided, pixels-per-distance is derived
93
+ * directly from the chart's `geoIdentity` fit scale and this reference
94
+ * scale, bypassing the `projection.invert` + `geoDistance` path which
95
+ * does not work for pre-projected data.
96
+ */
97
+ referenceScale?: number;
98
+
99
+ /**
100
+ * The placement of the legend.
101
+ */
102
+ placement?: Placement;
103
+
104
+ /**
105
+ * The fill/stroke color of the bar.
106
+ *
107
+ * @default 'currentColor'
108
+ */
109
+ color?: string;
110
+
111
+ /**
112
+ * Classes to apply to the elements.
113
+ *
114
+ * @default {}
115
+ */
116
+ classes?: {
117
+ root?: string;
118
+ title?: string;
119
+ bar?: string;
120
+ tick?: string;
121
+ label?: string;
122
+ };
123
+
124
+ /**
125
+ * A bindable reference to the wrapping `<div>` element.
126
+ *
127
+ * @bindable
128
+ */
129
+ ref?: HTMLElement;
130
+ };
131
+
132
+ export type GeoLegendProps = GeoLegendPropsWithoutHTML &
133
+ Without<HTMLAttributes<HTMLElement>, GeoLegendPropsWithoutHTML>;
134
+ </script>
135
+
136
+ <script lang="ts">
137
+ import type { HTMLAttributes } from 'svelte/elements';
138
+ import { geoDistance } from 'd3-geo';
139
+ import { format, type FormatType, type FormatConfig } from '@layerstack/utils';
140
+
141
+ import { cls } from '@layerstack/tailwind';
142
+ import { getChartContext } from '../contexts/chart.js';
143
+
144
+ let {
145
+ units = 'mi',
146
+ variant = 'bracket',
147
+ distance: distanceProp,
148
+ ticks = 4,
149
+ labelPlacement = 'bottom',
150
+ tickFormat: tickFormatProp,
151
+ tickFontSize = 10,
152
+ titleFontSize = 10,
153
+ height = 4,
154
+ title = '',
155
+ referencePoint,
156
+ referenceScale,
157
+ placement,
158
+ color = 'currentColor',
159
+ classes = {},
160
+ ref: refProp = $bindable(),
161
+ class: className,
162
+ ...restProps
163
+ }: GeoLegendProps = $props();
164
+
165
+ let ref = $state<HTMLElement>();
166
+ $effect.pre(() => {
167
+ refProp = ref;
168
+ });
169
+
170
+ const ctx = getChartContext();
171
+
172
+ // Earth radius in the selected units
173
+ const earthRadius = $derived(units === 'mi' ? 3958.8 : 6371);
174
+
175
+ // Pixels per unit at the reference point on the current projection.
176
+ // `null` if no projection or invert is unavailable (or numerically degenerate).
177
+ const pixelsPerUnit = $derived.by(() => {
178
+ const projection = ctx.geo?.projection;
179
+ if (!projection) return null;
180
+
181
+ let pxPerUnit: number;
182
+
183
+ if (referenceScale != null) {
184
+ // Pre-projected data path (e.g. `geoIdentity` + us-atlas
185
+ // `counties-albers-10m`): `projection.invert` returns topology pixel
186
+ // coordinates, not lon/lat, so `geoDistance` can't be used. Instead,
187
+ // combine the chart's fit scale with the known base projection scale:
188
+ // topology units per chart px = 1 / fitScale
189
+ // radians per topology unit = 1 / referenceScale
190
+ // units (mi/km) per radian = earthRadius
191
+ // => px per unit = (fitScale * referenceScale) / earthRadius
192
+ const fitScale = typeof projection.scale === 'function' ? projection.scale() : null;
193
+ if (fitScale == null || !Number.isFinite(fitScale) || fitScale === 0) return null;
194
+ pxPerUnit = (fitScale * referenceScale) / earthRadius;
195
+ } else {
196
+ if (typeof projection.invert !== 'function') return null;
197
+
198
+ const refPx: [number, number] = referencePoint ?? [ctx.width / 2, ctx.height / 2];
199
+ const a = projection.invert(refPx);
200
+ const b = projection.invert([refPx[0] + 1, refPx[1]]);
201
+ if (!a || !b) return null;
202
+ if (!Number.isFinite(a[0]) || !Number.isFinite(b[0])) return null;
203
+
204
+ const radiansPerPx = geoDistance(a, b);
205
+ if (!Number.isFinite(radiansPerPx) || radiansPerPx === 0) return null;
206
+
207
+ const unitsPerPx = radiansPerPx * earthRadius;
208
+ pxPerUnit = 1 / unitsPerPx;
209
+ }
210
+
211
+ // In `canvas` transform mode the projection itself is not re-scaled — the
212
+ // rendered output is visually scaled by `ctx.transform.scale`, so we need
213
+ // to multiply to keep the bar consistent with what the user sees.
214
+ if (ctx.transform?.mode === 'canvas') {
215
+ pxPerUnit *= ctx.transform.scale ?? 1;
216
+ }
217
+
218
+ return pxPerUnit;
219
+ });
220
+
221
+ function niceDistance(d: number) {
222
+ if (!(d > 0)) return 0;
223
+ const exp = Math.floor(Math.log10(d));
224
+ const base = Math.pow(10, exp);
225
+ const mantissa = d / base;
226
+ let nice;
227
+ if (mantissa < 1.5) nice = 1;
228
+ else if (mantissa < 3) nice = 2;
229
+ else if (mantissa < 7) nice = 5;
230
+ else nice = 10;
231
+ return nice * base;
232
+ }
233
+
234
+ const distance = $derived.by(() => {
235
+ if (distanceProp != null) return distanceProp;
236
+ if (pixelsPerUnit == null) return 0;
237
+ const viewportUnits = ctx.width / pixelsPerUnit;
238
+ return niceDistance(viewportUnits * 0.25);
239
+ });
240
+
241
+ const barWidth = $derived(pixelsPerUnit && distance > 0 ? distance * pixelsPerUnit : 0);
242
+
243
+ const tickValues = $derived.by(() => {
244
+ if (distance <= 0) return [] as number[];
245
+ return Array.from({ length: ticks + 1 }, (_, i) => (distance * i) / ticks);
246
+ });
247
+
248
+ function formatTick(value: number) {
249
+ if (typeof tickFormatProp === 'function') return tickFormatProp(value);
250
+ if (tickFormatProp) return format(value, asAny(tickFormatProp));
251
+ // Default: append unit on the last tick only
252
+ return value === distance ? `${value} ${units}` : String(value);
253
+ }
254
+
255
+ const padding = 2;
256
+ const titleHeight = $derived(title ? titleFontSize + 6 : 0);
257
+ const tickLabelHeight = $derived(tickFontSize + 3);
258
+ const width = $derived(Math.ceil(barWidth) + padding * 2);
259
+ const svgHeight = $derived(titleHeight + height + tickLabelHeight + padding * 2 + 3);
260
+ const barY = $derived(
261
+ labelPlacement === 'top'
262
+ ? titleHeight + padding + tickLabelHeight
263
+ : titleHeight + padding
264
+ );
265
+ const tickLabelY = $derived(
266
+ labelPlacement === 'top'
267
+ ? titleHeight + padding + tickFontSize
268
+ : barY + height + 3 + tickFontSize
269
+ );
270
+
271
+ // Single path for the `bracket` variant: outer bracket as one continuous
272
+ // polyline (so corners join cleanly) plus interior ticks. The top rule sits
273
+ // on the opposite side of the labels so the bracket "opens" toward them.
274
+ const bracketPath = $derived.by(() => {
275
+ if (barWidth <= 0) return '';
276
+ const x0 = padding;
277
+ const x1 = padding + barWidth;
278
+ const yRule = labelPlacement === 'top' ? barY + height : barY;
279
+ const yTicks = labelPlacement === 'top' ? barY : barY + height;
280
+ let d = `M${x0},${yTicks}L${x0},${yRule}L${x1},${yRule}L${x1},${yTicks}`;
281
+ for (let i = 1; i < ticks; i++) {
282
+ const tx = padding + (barWidth * i) / ticks;
283
+ d += `M${tx},${yRule}L${tx},${yTicks}`;
284
+ }
285
+ return d;
286
+ });
287
+ </script>
288
+
289
+ <div
290
+ bind:this={ref}
291
+ {...restProps}
292
+ data-placement={placement}
293
+ class={cls('lc-geo-legend-container', className, classes.root)}
294
+ >
295
+ {#if barWidth > 0}
296
+ <svg {width} height={svgHeight} viewBox="0 0 {width} {svgHeight}" class="lc-geo-legend-svg">
297
+ {#if title}
298
+ <text
299
+ x={padding}
300
+ y={titleFontSize}
301
+ style:font-size={titleFontSize}
302
+ class={cls('lc-geo-legend-title', classes.title)}
303
+ >
304
+ {title}
305
+ </text>
306
+ {/if}
307
+ {#if variant === 'bracket'}
308
+ <path
309
+ d={bracketPath}
310
+ style:fill="none"
311
+ stroke={color}
312
+ stroke-linecap="round"
313
+ stroke-linejoin="round"
314
+ class={cls('lc-geo-legend-bar', classes.bar)}
315
+ />
316
+ {:else if variant === 'alternating'}
317
+ <!-- Outline + alternating filled segments between consecutive ticks -->
318
+
319
+ {#each Array.from({ length: ticks }) as _, i}
320
+ {#if i % 2 === 0}
321
+ {@const x1 = padding + (barWidth * i) / ticks}
322
+ {@const x2 = padding + (barWidth * (i + 1)) / ticks}
323
+ <rect
324
+ x={x1}
325
+ y={barY}
326
+ width={x2 - x1}
327
+ {height}
328
+ fill={color}
329
+ class={cls('lc-geo-legend-bar', classes.bar)}
330
+ />
331
+ {/if}
332
+ {/each}
333
+ <rect
334
+ x={padding}
335
+ y={barY}
336
+ width={barWidth}
337
+ {height}
338
+ style:fill="none"
339
+ stroke={color}
340
+ class={cls('lc-geo-legend-bar', classes.bar)}
341
+ />
342
+ {/if}
343
+ <g class="lc-geo-legend-ticks">
344
+ {#each tickValues as value, i}
345
+ {@const x = padding + (barWidth * i) / ticks}
346
+ <text
347
+ {x}
348
+ y={tickLabelY}
349
+ text-anchor="middle"
350
+ style:font-size={tickFontSize}
351
+ class={cls('lc-geo-legend-label', classes.label)}
352
+ >
353
+ {formatTick(value)}
354
+ </text>
355
+ {/each}
356
+ </g>
357
+ </svg>
358
+ {/if}
359
+ </div>
360
+
361
+ <style>
362
+ @layer components {
363
+ :where(.lc-geo-legend-container) {
364
+ display: inline-block;
365
+ z-index: 1;
366
+
367
+ &[data-placement] {
368
+ position: absolute;
369
+ }
370
+
371
+ &[data-placement='top-left'] {
372
+ top: 0;
373
+ left: 0;
374
+ }
375
+ &[data-placement='top'] {
376
+ top: 0;
377
+ left: 50%;
378
+ transform: translateX(-50%);
379
+ }
380
+ &[data-placement='top-right'] {
381
+ top: 0;
382
+ right: 0;
383
+ }
384
+ &[data-placement='left'] {
385
+ top: 50%;
386
+ left: 0;
387
+ transform: translateY(-50%);
388
+ }
389
+ &[data-placement='center'] {
390
+ top: 50%;
391
+ left: 50%;
392
+ transform: translate(-50%, -50%);
393
+ }
394
+ &[data-placement='right'] {
395
+ top: 50%;
396
+ right: 0;
397
+ transform: translateY(-50%);
398
+ }
399
+ &[data-placement='bottom-left'] {
400
+ bottom: 0;
401
+ left: 0;
402
+ }
403
+ &[data-placement='bottom'] {
404
+ bottom: 0;
405
+ left: 50%;
406
+ transform: translateX(-50%);
407
+ }
408
+ &[data-placement='bottom-right'] {
409
+ bottom: 0;
410
+ right: 0;
411
+ }
412
+ }
413
+
414
+ :where(.lc-geo-legend-svg) {
415
+ overflow: visible;
416
+ }
417
+
418
+ :where(.lc-geo-legend-title) {
419
+ font-weight: 600;
420
+ fill: var(--color-surface-content, currentColor);
421
+ }
422
+
423
+ :where(.lc-geo-legend-bar) {
424
+ fill: var(--color-surface-content, currentColor);
425
+ }
426
+
427
+ :where(.lc-geo-legend-tick) {
428
+ stroke: var(--color-surface-content, currentColor);
429
+ }
430
+
431
+ :where(.lc-geo-legend-label) {
432
+ fill: var(--color-surface-content, currentColor);
433
+ }
434
+ }
435
+ </style>
@@ -0,0 +1,117 @@
1
+ import type { Placement } from './types.js';
2
+ import { type Without } from '../utils/types.js';
3
+ export type GeoLegendUnits = 'km' | 'mi';
4
+ export type GeoLegendVariant = 'bracket' | 'alternating';
5
+ export type GeoLegendPropsWithoutHTML = {
6
+ /**
7
+ * Units to display.
8
+ *
9
+ * @default 'mi'
10
+ */
11
+ units?: GeoLegendUnits;
12
+ /**
13
+ * Visual style of the bar.
14
+ * - `'bracket'`: top rule with downward brackets at each tick (default)
15
+ * - `'alternating'`: alternating filled/unfilled segments between ticks
16
+ *
17
+ * @default 'bracket'
18
+ */
19
+ variant?: GeoLegendVariant;
20
+ /**
21
+ * Explicit distance to represent (in `units`). When omitted, a "nice" round
22
+ * value is chosen so the bar covers roughly 25% of the chart width.
23
+ */
24
+ distance?: number;
25
+ /**
26
+ * Number of tick subdivisions of the bar.
27
+ *
28
+ * @default 4
29
+ */
30
+ ticks?: number;
31
+ /**
32
+ * Where to place the tick labels relative to the bar. Useful for stacking
33
+ * two legends (e.g. kilometers + miles) tightly.
34
+ *
35
+ * @default 'bottom'
36
+ */
37
+ labelPlacement?: 'top' | 'bottom';
38
+ /**
39
+ * Format for the tick labels. Receives the distance in `units`.
40
+ */
41
+ tickFormat?: FormatType | FormatConfig | ((value: number) => string);
42
+ /**
43
+ * The font size of the tick labels.
44
+ *
45
+ * @default 10
46
+ */
47
+ tickFontSize?: number;
48
+ /**
49
+ * The font size of the title.
50
+ *
51
+ * @default 10
52
+ */
53
+ titleFontSize?: number;
54
+ /**
55
+ * The thickness of the bar.
56
+ *
57
+ * @default 4
58
+ */
59
+ height?: number;
60
+ /**
61
+ * The title of the legend.
62
+ *
63
+ * @default ''
64
+ */
65
+ title?: string;
66
+ /**
67
+ * Reference point in chart pixel coordinates used to compute the
68
+ * pixels-per-distance ratio. Defaults to the center of the chart's plot
69
+ * area, which is generally a reasonable approximation away from the poles.
70
+ */
71
+ referencePoint?: [number, number];
72
+ /**
73
+ * Scale of the projection originally used to pre-project the data, for
74
+ * charts that render pre-projected topologies via `geoIdentity`. For
75
+ * example, the `us-atlas` `counties-albers-10m` / `states-albers-10m`
76
+ * topologies are pre-projected with `geoAlbersUsa().scale(1300)`, so pass
77
+ * `referenceScale={1300}`. When provided, pixels-per-distance is derived
78
+ * directly from the chart's `geoIdentity` fit scale and this reference
79
+ * scale, bypassing the `projection.invert` + `geoDistance` path which
80
+ * does not work for pre-projected data.
81
+ */
82
+ referenceScale?: number;
83
+ /**
84
+ * The placement of the legend.
85
+ */
86
+ placement?: Placement;
87
+ /**
88
+ * The fill/stroke color of the bar.
89
+ *
90
+ * @default 'currentColor'
91
+ */
92
+ color?: string;
93
+ /**
94
+ * Classes to apply to the elements.
95
+ *
96
+ * @default {}
97
+ */
98
+ classes?: {
99
+ root?: string;
100
+ title?: string;
101
+ bar?: string;
102
+ tick?: string;
103
+ label?: string;
104
+ };
105
+ /**
106
+ * A bindable reference to the wrapping `<div>` element.
107
+ *
108
+ * @bindable
109
+ */
110
+ ref?: HTMLElement;
111
+ };
112
+ export type GeoLegendProps = GeoLegendPropsWithoutHTML & Without<HTMLAttributes<HTMLElement>, GeoLegendPropsWithoutHTML>;
113
+ import type { HTMLAttributes } from 'svelte/elements';
114
+ import { type FormatType, type FormatConfig } from '@layerstack/utils';
115
+ declare const GeoLegend: import("svelte").Component<GeoLegendProps, {}, "ref">;
116
+ type GeoLegend = ReturnType<typeof GeoLegend>;
117
+ export default GeoLegend;
@@ -42,15 +42,19 @@
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,6 +85,7 @@
81
85
  import { getChartContext } from '../contexts/chart.js';
82
86
  import Group from './Group.svelte';
83
87
  import { extractLayerProps } from '../utils/attributes.js';
88
+ import { createDimensionGetter } from '../utils/rect.svelte.js';
84
89
 
85
90
  const ctx = getChartContext();
86
91
 
@@ -94,7 +99,7 @@
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,6 +109,9 @@
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,14 +154,27 @@
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,14 +185,27 @@
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,7 +215,7 @@
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,7 +227,7 @@
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,7 +305,8 @@
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
  }