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.
- 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 +404 -0
- package/dist/components/GeoLegend.svelte.d.ts +106 -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
|
@@ -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
|
-
* `
|
|
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 (
|
|
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:
|
|
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:
|
|
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 (
|
|
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 === '
|
|
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 === '
|
|
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
|
-
* `
|
|
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
|
/**
|