layerchart 2.0.0-next.51 → 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.
@@ -0,0 +1,404 @@
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
+ * The placement of the legend.
89
+ */
90
+ placement?: Placement;
91
+
92
+ /**
93
+ * The fill/stroke color of the bar.
94
+ *
95
+ * @default 'currentColor'
96
+ */
97
+ color?: string;
98
+
99
+ /**
100
+ * Classes to apply to the elements.
101
+ *
102
+ * @default {}
103
+ */
104
+ classes?: {
105
+ root?: string;
106
+ title?: string;
107
+ bar?: string;
108
+ tick?: string;
109
+ label?: string;
110
+ };
111
+
112
+ /**
113
+ * A bindable reference to the wrapping `<div>` element.
114
+ *
115
+ * @bindable
116
+ */
117
+ ref?: HTMLElement;
118
+ };
119
+
120
+ export type GeoLegendProps = GeoLegendPropsWithoutHTML &
121
+ Without<HTMLAttributes<HTMLElement>, GeoLegendPropsWithoutHTML>;
122
+ </script>
123
+
124
+ <script lang="ts">
125
+ import type { HTMLAttributes } from 'svelte/elements';
126
+ import { geoDistance } from 'd3-geo';
127
+ import { format, type FormatType, type FormatConfig } from '@layerstack/utils';
128
+
129
+ import { cls } from '@layerstack/tailwind';
130
+ import { getChartContext } from '../contexts/chart.js';
131
+
132
+ let {
133
+ units = 'mi',
134
+ variant = 'bracket',
135
+ distance: distanceProp,
136
+ ticks = 4,
137
+ labelPlacement = 'bottom',
138
+ tickFormat: tickFormatProp,
139
+ tickFontSize = 10,
140
+ titleFontSize = 10,
141
+ height = 4,
142
+ title = '',
143
+ referencePoint,
144
+ placement,
145
+ color = 'currentColor',
146
+ classes = {},
147
+ ref: refProp = $bindable(),
148
+ class: className,
149
+ ...restProps
150
+ }: GeoLegendProps = $props();
151
+
152
+ let ref = $state<HTMLElement>();
153
+ $effect.pre(() => {
154
+ refProp = ref;
155
+ });
156
+
157
+ const ctx = getChartContext();
158
+
159
+ // Earth radius in the selected units
160
+ const earthRadius = $derived(units === 'mi' ? 3958.8 : 6371);
161
+
162
+ // Pixels per unit at the reference point on the current projection.
163
+ // `null` if no projection or invert is unavailable (or numerically degenerate).
164
+ const pixelsPerUnit = $derived.by(() => {
165
+ const projection = ctx.geo?.projection;
166
+ if (!projection || typeof projection.invert !== 'function') return null;
167
+
168
+ const refPx: [number, number] = referencePoint ?? [ctx.width / 2, ctx.height / 2];
169
+ const a = projection.invert(refPx);
170
+ const b = projection.invert([refPx[0] + 1, refPx[1]]);
171
+ if (!a || !b) return null;
172
+ if (!Number.isFinite(a[0]) || !Number.isFinite(b[0])) return null;
173
+
174
+ const radiansPerPx = geoDistance(a, b);
175
+ if (!Number.isFinite(radiansPerPx) || radiansPerPx === 0) return null;
176
+
177
+ const unitsPerPx = radiansPerPx * earthRadius;
178
+ let pxPerUnit = 1 / unitsPerPx;
179
+
180
+ // In `canvas` transform mode the projection itself is not re-scaled — the
181
+ // rendered output is visually scaled by `ctx.transform.scale`, so we need
182
+ // to multiply to keep the bar consistent with what the user sees.
183
+ if (ctx.transform?.mode === 'canvas') {
184
+ pxPerUnit *= ctx.transform.scale ?? 1;
185
+ }
186
+
187
+ return pxPerUnit;
188
+ });
189
+
190
+ function niceDistance(d: number) {
191
+ if (!(d > 0)) return 0;
192
+ const exp = Math.floor(Math.log10(d));
193
+ const base = Math.pow(10, exp);
194
+ const mantissa = d / base;
195
+ let nice;
196
+ if (mantissa < 1.5) nice = 1;
197
+ else if (mantissa < 3) nice = 2;
198
+ else if (mantissa < 7) nice = 5;
199
+ else nice = 10;
200
+ return nice * base;
201
+ }
202
+
203
+ const distance = $derived.by(() => {
204
+ if (distanceProp != null) return distanceProp;
205
+ if (pixelsPerUnit == null) return 0;
206
+ const viewportUnits = ctx.width / pixelsPerUnit;
207
+ return niceDistance(viewportUnits * 0.25);
208
+ });
209
+
210
+ const barWidth = $derived(pixelsPerUnit && distance > 0 ? distance * pixelsPerUnit : 0);
211
+
212
+ const tickValues = $derived.by(() => {
213
+ if (distance <= 0) return [] as number[];
214
+ return Array.from({ length: ticks + 1 }, (_, i) => (distance * i) / ticks);
215
+ });
216
+
217
+ function formatTick(value: number) {
218
+ if (typeof tickFormatProp === 'function') return tickFormatProp(value);
219
+ if (tickFormatProp) return format(value, asAny(tickFormatProp));
220
+ // Default: append unit on the last tick only
221
+ return value === distance ? `${value} ${units}` : String(value);
222
+ }
223
+
224
+ const padding = 2;
225
+ const titleHeight = $derived(title ? titleFontSize + 6 : 0);
226
+ const tickLabelHeight = $derived(tickFontSize + 3);
227
+ const width = $derived(Math.ceil(barWidth) + padding * 2);
228
+ const svgHeight = $derived(titleHeight + height + tickLabelHeight + padding * 2 + 3);
229
+ const barY = $derived(
230
+ labelPlacement === 'top'
231
+ ? titleHeight + padding + tickLabelHeight
232
+ : titleHeight + padding
233
+ );
234
+ const tickLabelY = $derived(
235
+ labelPlacement === 'top'
236
+ ? titleHeight + padding + tickFontSize
237
+ : barY + height + 3 + tickFontSize
238
+ );
239
+
240
+ // Single path for the `bracket` variant: outer bracket as one continuous
241
+ // polyline (so corners join cleanly) plus interior ticks. The top rule sits
242
+ // on the opposite side of the labels so the bracket "opens" toward them.
243
+ const bracketPath = $derived.by(() => {
244
+ if (barWidth <= 0) return '';
245
+ const x0 = padding;
246
+ const x1 = padding + barWidth;
247
+ const yRule = labelPlacement === 'top' ? barY + height : barY;
248
+ const yTicks = labelPlacement === 'top' ? barY : barY + height;
249
+ let d = `M${x0},${yTicks}L${x0},${yRule}L${x1},${yRule}L${x1},${yTicks}`;
250
+ for (let i = 1; i < ticks; i++) {
251
+ const tx = padding + (barWidth * i) / ticks;
252
+ d += `M${tx},${yRule}L${tx},${yTicks}`;
253
+ }
254
+ return d;
255
+ });
256
+ </script>
257
+
258
+ <div
259
+ bind:this={ref}
260
+ {...restProps}
261
+ data-placement={placement}
262
+ class={cls('lc-geo-legend-container', className, classes.root)}
263
+ >
264
+ {#if barWidth > 0}
265
+ <svg {width} height={svgHeight} viewBox="0 0 {width} {svgHeight}" class="lc-geo-legend-svg">
266
+ {#if title}
267
+ <text
268
+ x={padding}
269
+ y={titleFontSize}
270
+ style:font-size={titleFontSize}
271
+ class={cls('lc-geo-legend-title', classes.title)}
272
+ >
273
+ {title}
274
+ </text>
275
+ {/if}
276
+ {#if variant === 'bracket'}
277
+ <path
278
+ d={bracketPath}
279
+ style:fill="none"
280
+ stroke={color}
281
+ stroke-linecap="round"
282
+ stroke-linejoin="round"
283
+ class={cls('lc-geo-legend-bar', classes.bar)}
284
+ />
285
+ {:else if variant === 'alternating'}
286
+ <!-- Outline + alternating filled segments between consecutive ticks -->
287
+
288
+ {#each Array.from({ length: ticks }) as _, i}
289
+ {#if i % 2 === 0}
290
+ {@const x1 = padding + (barWidth * i) / ticks}
291
+ {@const x2 = padding + (barWidth * (i + 1)) / ticks}
292
+ <rect
293
+ x={x1}
294
+ y={barY}
295
+ width={x2 - x1}
296
+ {height}
297
+ fill={color}
298
+ class={cls('lc-geo-legend-bar', classes.bar)}
299
+ />
300
+ {/if}
301
+ {/each}
302
+ <rect
303
+ x={padding}
304
+ y={barY}
305
+ width={barWidth}
306
+ {height}
307
+ style:fill="none"
308
+ stroke={color}
309
+ class={cls('lc-geo-legend-bar', classes.bar)}
310
+ />
311
+ {/if}
312
+ <g class="lc-geo-legend-ticks">
313
+ {#each tickValues as value, i}
314
+ {@const x = padding + (barWidth * i) / ticks}
315
+ <text
316
+ {x}
317
+ y={tickLabelY}
318
+ text-anchor="middle"
319
+ style:font-size={tickFontSize}
320
+ class={cls('lc-geo-legend-label', classes.label)}
321
+ >
322
+ {formatTick(value)}
323
+ </text>
324
+ {/each}
325
+ </g>
326
+ </svg>
327
+ {/if}
328
+ </div>
329
+
330
+ <style>
331
+ @layer components {
332
+ :where(.lc-geo-legend-container) {
333
+ display: inline-block;
334
+ z-index: 1;
335
+
336
+ &[data-placement] {
337
+ position: absolute;
338
+ }
339
+
340
+ &[data-placement='top-left'] {
341
+ top: 0;
342
+ left: 0;
343
+ }
344
+ &[data-placement='top'] {
345
+ top: 0;
346
+ left: 50%;
347
+ transform: translateX(-50%);
348
+ }
349
+ &[data-placement='top-right'] {
350
+ top: 0;
351
+ right: 0;
352
+ }
353
+ &[data-placement='left'] {
354
+ top: 50%;
355
+ left: 0;
356
+ transform: translateY(-50%);
357
+ }
358
+ &[data-placement='center'] {
359
+ top: 50%;
360
+ left: 50%;
361
+ transform: translate(-50%, -50%);
362
+ }
363
+ &[data-placement='right'] {
364
+ top: 50%;
365
+ right: 0;
366
+ transform: translateY(-50%);
367
+ }
368
+ &[data-placement='bottom-left'] {
369
+ bottom: 0;
370
+ left: 0;
371
+ }
372
+ &[data-placement='bottom'] {
373
+ bottom: 0;
374
+ left: 50%;
375
+ transform: translateX(-50%);
376
+ }
377
+ &[data-placement='bottom-right'] {
378
+ bottom: 0;
379
+ right: 0;
380
+ }
381
+ }
382
+
383
+ :where(.lc-geo-legend-svg) {
384
+ overflow: visible;
385
+ }
386
+
387
+ :where(.lc-geo-legend-title) {
388
+ font-weight: 600;
389
+ fill: var(--color-surface-content, currentColor);
390
+ }
391
+
392
+ :where(.lc-geo-legend-bar) {
393
+ fill: var(--color-surface-content, currentColor);
394
+ }
395
+
396
+ :where(.lc-geo-legend-tick) {
397
+ stroke: var(--color-surface-content, currentColor);
398
+ }
399
+
400
+ :where(.lc-geo-legend-label) {
401
+ fill: var(--color-surface-content, currentColor);
402
+ }
403
+ }
404
+ </style>
@@ -0,0 +1,106 @@
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
+ * The placement of the legend.
74
+ */
75
+ placement?: Placement;
76
+ /**
77
+ * The fill/stroke color of the bar.
78
+ *
79
+ * @default 'currentColor'
80
+ */
81
+ color?: string;
82
+ /**
83
+ * Classes to apply to the elements.
84
+ *
85
+ * @default {}
86
+ */
87
+ classes?: {
88
+ root?: string;
89
+ title?: string;
90
+ bar?: string;
91
+ tick?: string;
92
+ label?: string;
93
+ };
94
+ /**
95
+ * A bindable reference to the wrapping `<div>` element.
96
+ *
97
+ * @bindable
98
+ */
99
+ ref?: HTMLElement;
100
+ };
101
+ export type GeoLegendProps = GeoLegendPropsWithoutHTML & Without<HTMLAttributes<HTMLElement>, GeoLegendPropsWithoutHTML>;
102
+ import type { HTMLAttributes } from 'svelte/elements';
103
+ import { type FormatType, type FormatConfig } from '@layerstack/utils';
104
+ declare const GeoLegend: import("svelte").Component<GeoLegendProps, {}, "ref">;
105
+ type GeoLegend = ReturnType<typeof GeoLegend>;
106
+ 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
  }
@@ -34,14 +34,18 @@ export type LabelsPropsWithoutHTML<T = any> = {
34
34
  seriesKey?: string;
35
35
  /**
36
36
  * The placement of the label relative to the point.
37
- * `smart` dynamically positions labels based on neighboring point values (peak, trough, rising, falling).
37
+ * - `outside`: outside the bar/point.
38
+ * - `inside`: inside the bar/point near the value edge.
39
+ * - `middle`: aligned to the value edge with a middle anchor.
40
+ * - `center`: centered within the bar body (between the value edge and baseline).
41
+ * - `smart`: dynamically positions labels based on neighboring point values (peak, trough, rising, falling).
38
42
  * @default 'outside'
39
43
  */
40
- placement?: 'inside' | 'outside' | 'center' | 'smart';
44
+ placement?: 'inside' | 'outside' | 'middle' | 'center' | 'smart';
41
45
  /**
42
46
  * The offset of the label from the point
43
47
  *
44
- * @default placement === 'center' ? 0 : 4
48
+ * @default placement === 'center' || placement === 'middle' ? 0 : 4
45
49
  */
46
50
  offset?: number;
47
51
  /**