layerchart 2.0.0-next.50 → 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/Axis.svelte +25 -0
- package/dist/components/Axis.svelte.d.ts +10 -0
- package/dist/components/Circle.svelte +82 -59
- package/dist/components/CircleLegend.svelte +389 -0
- package/dist/components/CircleLegend.svelte.d.ts +114 -0
- package/dist/components/Ellipse.svelte +83 -64
- package/dist/components/GeoLegend.svelte +404 -0
- package/dist/components/GeoLegend.svelte.d.ts +106 -0
- package/dist/components/GeoRaster.svelte +311 -0
- package/dist/components/GeoRaster.svelte.d.ts +61 -0
- package/dist/components/Grid.svelte +15 -0
- package/dist/components/Grid.svelte.d.ts +5 -0
- package/dist/components/Image.svelte +2 -2
- 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/Line.svelte +82 -62
- package/dist/components/Points.svelte +2 -2
- package/dist/components/Polygon.svelte +92 -56
- package/dist/components/Rect.svelte +113 -64
- package/dist/components/Rule.svelte +2 -0
- package/dist/components/Sankey.svelte +0 -2
- package/dist/components/Text.svelte +83 -52
- 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 +40 -2
- package/dist/components/charts/PieChart.svelte.d.ts +10 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +8 -0
- package/dist/components/layers/Canvas.svelte +65 -48
- package/dist/components/layers/Canvas.svelte.d.ts +10 -0
- package/dist/contexts/canvas.d.ts +3 -0
- package/dist/server/ContextCapture.svelte +30 -0
- package/dist/server/ContextCapture.svelte.d.ts +8 -0
- package/dist/server/ServerChart.svelte +26 -0
- package/dist/server/ServerChart.svelte.d.ts +11 -0
- package/dist/server/TestBarChart.svelte +35 -0
- package/dist/server/TestBarChart.svelte.d.ts +14 -0
- package/dist/server/TestLineChart.svelte +35 -0
- package/dist/server/TestLineChart.svelte.d.ts +14 -0
- package/dist/server/captureStore.d.ts +8 -0
- package/dist/server/captureStore.js +18 -0
- package/dist/server/index.d.ts +137 -0
- package/dist/server/index.js +141 -0
- package/dist/server/renderChart.ssr.test.d.ts +1 -0
- package/dist/server/renderChart.ssr.test.js +205 -0
- package/dist/server/renderTree.d.ts +8 -0
- package/dist/server/renderTree.js +29 -0
- package/dist/states/__screenshots__/chart.svelte.test.ts/ChartState-geo-projection-skips-markInfo-should-not-derive-x-y-accessors-from-marks-when-geo-projection-is-active-1.png +0 -0
- package/dist/states/__screenshots__/chart.svelte.test.ts/ChartState-geo-projection-skips-markInfo-should-not-derive-x-y-accessors-from-marks-when-geo-projection-is-active-2.png +0 -0
- package/dist/states/chart.svelte.d.ts +5 -1
- package/dist/states/chart.svelte.js +18 -3
- package/dist/states/chart.svelte.test.js +110 -0
- package/dist/states/geo.svelte.d.ts +5 -1
- package/dist/states/geo.svelte.js +80 -68
- package/dist/utils/arcText.svelte.d.ts +7 -1
- package/dist/utils/arcText.svelte.js +8 -4
- package/dist/utils/canvas.js +29 -10
- package/dist/utils/canvas.svelte.test.js +2 -2
- package/dist/utils/motion.svelte.js +14 -0
- package/package.json +7 -1
|
@@ -101,6 +101,18 @@
|
|
|
101
101
|
*/
|
|
102
102
|
scale?: any;
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Stroke color for axis rule, grid lines, and tick marks.
|
|
106
|
+
* Useful for server-side rendering where CSS variables are not available.
|
|
107
|
+
*/
|
|
108
|
+
stroke?: string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fill color for tick labels and axis label.
|
|
112
|
+
* Useful for server-side rendering where CSS variables are not available.
|
|
113
|
+
*/
|
|
114
|
+
fill?: string;
|
|
115
|
+
|
|
104
116
|
/**
|
|
105
117
|
* Classes for styling various parts of the axis
|
|
106
118
|
* @default {}
|
|
@@ -156,6 +168,8 @@
|
|
|
156
168
|
tickMarks = true,
|
|
157
169
|
format,
|
|
158
170
|
tickLabelProps,
|
|
171
|
+
stroke,
|
|
172
|
+
fill,
|
|
159
173
|
motion,
|
|
160
174
|
transitionIn,
|
|
161
175
|
transitionInParams,
|
|
@@ -168,6 +182,8 @@
|
|
|
168
182
|
|
|
169
183
|
const ctx = getChartContext();
|
|
170
184
|
|
|
185
|
+
ctx.registerComponent({ name: 'Axis', kind: 'composite-mark' });
|
|
186
|
+
|
|
171
187
|
const orientation = $derived(
|
|
172
188
|
placement === 'angle'
|
|
173
189
|
? 'angle'
|
|
@@ -465,6 +481,8 @@
|
|
|
465
481
|
// complement 10px text (until Text supports custom styles)
|
|
466
482
|
capHeight: '7px',
|
|
467
483
|
lineHeight: '11px',
|
|
484
|
+
fill,
|
|
485
|
+
stroke,
|
|
468
486
|
...labelProps,
|
|
469
487
|
class: cls('lc-axis-label', classes.label, labelProps?.class),
|
|
470
488
|
}) satisfies ComponentProps<typeof Text>;
|
|
@@ -479,6 +497,7 @@
|
|
|
479
497
|
<Rule
|
|
480
498
|
x={placement === 'left' ? '$left' : placement === 'right' ? '$right' : placement === 'angle'}
|
|
481
499
|
y={placement === 'top' ? '$top' : placement === 'bottom' ? '$bottom' : placement === 'radius'}
|
|
500
|
+
{stroke}
|
|
482
501
|
{motion}
|
|
483
502
|
{...extractLayerProps(rule, 'lc-axis-rule', classes.rule ?? '')}
|
|
484
503
|
/>
|
|
@@ -506,6 +525,8 @@
|
|
|
506
525
|
// complement 10px text (until Text supports custom styles)
|
|
507
526
|
capHeight: '7px',
|
|
508
527
|
lineHeight: '11px',
|
|
528
|
+
fill,
|
|
529
|
+
stroke,
|
|
509
530
|
...tickLabelProps,
|
|
510
531
|
class: cls('lc-axis-tick-label', classes.tickLabel, tickLabelProps?.class),
|
|
511
532
|
}}
|
|
@@ -515,6 +536,7 @@
|
|
|
515
536
|
<Rule
|
|
516
537
|
x={orientation === 'horizontal' || orientation === 'angle' ? tick : false}
|
|
517
538
|
y={orientation === 'vertical' || orientation === 'radius' ? tick : false}
|
|
539
|
+
{stroke}
|
|
518
540
|
{motion}
|
|
519
541
|
{...extractLayerProps(grid, 'lc-axis-grid', classes.rule ?? '')}
|
|
520
542
|
/>
|
|
@@ -528,6 +550,7 @@
|
|
|
528
550
|
y1={tickCoords.y}
|
|
529
551
|
x2={tickCoords.x}
|
|
530
552
|
y2={tickCoords.y + (placement === 'top' ? -tickLength : tickLength)}
|
|
553
|
+
{stroke}
|
|
531
554
|
{motion}
|
|
532
555
|
class={tickClasses}
|
|
533
556
|
/>
|
|
@@ -537,6 +560,7 @@
|
|
|
537
560
|
y1={tickCoords.y}
|
|
538
561
|
x2={tickCoords.x + (placement === 'left' ? -tickLength : tickLength)}
|
|
539
562
|
y2={tickCoords.y}
|
|
563
|
+
{stroke}
|
|
540
564
|
{motion}
|
|
541
565
|
class={tickClasses}
|
|
542
566
|
/>
|
|
@@ -546,6 +570,7 @@
|
|
|
546
570
|
y1={radialTickCoordsY}
|
|
547
571
|
x2={radialTickMarkCoordsX}
|
|
548
572
|
y2={radialTickMarkCoordsY}
|
|
573
|
+
{stroke}
|
|
549
574
|
{motion}
|
|
550
575
|
class={tickClasses}
|
|
551
576
|
/>
|
|
@@ -87,6 +87,16 @@ export type AxisPropsWithoutHTML<In extends Transition = Transition> = {
|
|
|
87
87
|
* Override scale for the axis
|
|
88
88
|
*/
|
|
89
89
|
scale?: any;
|
|
90
|
+
/**
|
|
91
|
+
* Stroke color for axis rule, grid lines, and tick marks.
|
|
92
|
+
* Useful for server-side rendering where CSS variables are not available.
|
|
93
|
+
*/
|
|
94
|
+
stroke?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Fill color for tick labels and axis label.
|
|
97
|
+
* Useful for server-side rendering where CSS variables are not available.
|
|
98
|
+
*/
|
|
99
|
+
fill?: string;
|
|
90
100
|
/**
|
|
91
101
|
* Classes for styling various parts of the axis
|
|
92
102
|
* @default {}
|
|
@@ -95,7 +95,13 @@
|
|
|
95
95
|
import { untrack } from 'svelte';
|
|
96
96
|
import { createMotion, createDataMotionMap, type MotionProp } from '../utils/motion.svelte.js';
|
|
97
97
|
import { renderCircle, type ComputedStylesOptions } from '../utils/canvas.js';
|
|
98
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
hasAnyDataProp,
|
|
100
|
+
resolveDataProp,
|
|
101
|
+
resolveColorProp,
|
|
102
|
+
resolveGeoDataPair,
|
|
103
|
+
resolveStyleProp,
|
|
104
|
+
} from '../utils/dataProp.js';
|
|
99
105
|
import { getGeoContext } from '../contexts/geo.js';
|
|
100
106
|
import { chartDataArray } from '../utils/common.js';
|
|
101
107
|
import type { SVGAttributes } from 'svelte/elements';
|
|
@@ -130,9 +136,7 @@
|
|
|
130
136
|
const geo = getGeoContext();
|
|
131
137
|
|
|
132
138
|
// Data to iterate over in data mode
|
|
133
|
-
const resolvedData: any[] = $derived(
|
|
134
|
-
dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []
|
|
135
|
-
);
|
|
139
|
+
const resolvedData: any[] = $derived(dataMode ? (dataProp ?? chartDataArray(chartCtx.data)) : []);
|
|
136
140
|
|
|
137
141
|
// Resolve a single data item to pixel coordinates
|
|
138
142
|
function resolveCircle(d: any) {
|
|
@@ -201,21 +205,16 @@
|
|
|
201
205
|
|
|
202
206
|
const layerCtx = getLayerContext();
|
|
203
207
|
|
|
204
|
-
const motionCx = createMotion(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
);
|
|
214
|
-
const motionR = createMotion(
|
|
215
|
-
initialR,
|
|
216
|
-
() => (typeof r === 'number' ? r : 1),
|
|
217
|
-
motion
|
|
218
|
-
);
|
|
208
|
+
const motionCx = createMotion(initialCx, () => (typeof cx === 'number' ? cx : 0), motion);
|
|
209
|
+
const motionCy = createMotion(initialCy, () => (typeof cy === 'number' ? cy : 0), motion);
|
|
210
|
+
const motionR = createMotion(initialR, () => (typeof r === 'number' ? r : 1), motion);
|
|
211
|
+
|
|
212
|
+
const staticFill = $derived(typeof fill === 'string' ? fill : undefined);
|
|
213
|
+
const staticFillOpacity = $derived(typeof fillOpacity === 'number' ? fillOpacity : undefined);
|
|
214
|
+
const staticStroke = $derived(typeof stroke === 'string' ? stroke : undefined);
|
|
215
|
+
const staticStrokeWidth = $derived(typeof strokeWidth === 'number' ? strokeWidth : undefined);
|
|
216
|
+
const staticOpacity = $derived(typeof opacity === 'number' ? opacity : undefined);
|
|
217
|
+
const staticClassName = $derived(typeof className === 'string' ? className : undefined);
|
|
219
218
|
|
|
220
219
|
// Style options (shared between pixel and data mode)
|
|
221
220
|
function getStyleOptions(
|
|
@@ -228,16 +227,29 @@
|
|
|
228
227
|
itemClass?: string | undefined
|
|
229
228
|
) {
|
|
230
229
|
return styleOverrides
|
|
231
|
-
? merge(
|
|
230
|
+
? merge(
|
|
231
|
+
{
|
|
232
|
+
styles: {
|
|
233
|
+
strokeWidth:
|
|
234
|
+
itemStrokeWidth ?? (typeof strokeWidth === 'number' ? strokeWidth : undefined),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
styleOverrides
|
|
238
|
+
)
|
|
232
239
|
: {
|
|
233
240
|
styles: {
|
|
234
241
|
fill: itemFill ?? fill,
|
|
235
|
-
fillOpacity:
|
|
242
|
+
fillOpacity:
|
|
243
|
+
itemFillOpacity ?? (typeof fillOpacity === 'number' ? fillOpacity : undefined),
|
|
236
244
|
stroke: itemStroke ?? stroke,
|
|
237
|
-
strokeWidth:
|
|
245
|
+
strokeWidth:
|
|
246
|
+
itemStrokeWidth ?? (typeof strokeWidth === 'number' ? strokeWidth : undefined),
|
|
238
247
|
opacity: itemOpacity ?? (typeof opacity === 'number' ? opacity : undefined),
|
|
239
248
|
},
|
|
240
|
-
classes: cls(
|
|
249
|
+
classes: cls(
|
|
250
|
+
'lc-circle',
|
|
251
|
+
itemClass ?? (typeof className === 'string' ? className : undefined)
|
|
252
|
+
),
|
|
241
253
|
style: restProps.style as string | undefined,
|
|
242
254
|
};
|
|
243
255
|
}
|
|
@@ -254,7 +266,15 @@
|
|
|
254
266
|
const resolvedStrokeWidth = resolveStyleProp(strokeWidth, item.d);
|
|
255
267
|
const resolvedOpacity = resolveStyleProp(opacity, item.d);
|
|
256
268
|
const resolvedClass = resolveStyleProp(className, item.d);
|
|
257
|
-
const styleOpts = getStyleOptions(
|
|
269
|
+
const styleOpts = getStyleOptions(
|
|
270
|
+
styleOverrides,
|
|
271
|
+
resolvedFill,
|
|
272
|
+
resolvedStroke,
|
|
273
|
+
resolvedFillOpacity,
|
|
274
|
+
resolvedStrokeWidth,
|
|
275
|
+
resolvedOpacity,
|
|
276
|
+
resolvedClass
|
|
277
|
+
);
|
|
258
278
|
renderCircle(ctx, item, styleOpts);
|
|
259
279
|
}
|
|
260
280
|
} else {
|
|
@@ -284,30 +304,33 @@
|
|
|
284
304
|
color: typeof fill === 'string' ? fill : undefined,
|
|
285
305
|
};
|
|
286
306
|
},
|
|
287
|
-
canvasRender:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
canvasRender:
|
|
308
|
+
layerCtx === 'canvas'
|
|
309
|
+
? {
|
|
310
|
+
render,
|
|
311
|
+
events: {
|
|
312
|
+
click: restProps.onclick,
|
|
313
|
+
pointerdown: restProps.onpointerdown,
|
|
314
|
+
pointerenter: restProps.onpointerenter,
|
|
315
|
+
pointermove: restProps.onpointermove,
|
|
316
|
+
pointerleave: restProps.onpointerleave,
|
|
317
|
+
},
|
|
318
|
+
deps: () => [
|
|
319
|
+
dataMode,
|
|
320
|
+
dataMode ? resolvedItems : null,
|
|
321
|
+
motionCx.current,
|
|
322
|
+
motionCy.current,
|
|
323
|
+
motionR.current,
|
|
324
|
+
fillKey!.current,
|
|
325
|
+
fillOpacity,
|
|
326
|
+
strokeKey!.current,
|
|
327
|
+
strokeWidth,
|
|
328
|
+
opacity,
|
|
329
|
+
className,
|
|
330
|
+
restProps.style,
|
|
331
|
+
],
|
|
332
|
+
}
|
|
333
|
+
: undefined,
|
|
311
334
|
});
|
|
312
335
|
</script>
|
|
313
336
|
|
|
@@ -339,12 +362,12 @@
|
|
|
339
362
|
cx={motionCx.current}
|
|
340
363
|
cy={motionCy.current}
|
|
341
364
|
r={motionR.current}
|
|
342
|
-
fill={
|
|
343
|
-
fill-opacity={
|
|
344
|
-
stroke={
|
|
345
|
-
stroke-width={
|
|
346
|
-
opacity={
|
|
347
|
-
class={cls('lc-circle',
|
|
365
|
+
fill={staticFill}
|
|
366
|
+
fill-opacity={staticFillOpacity}
|
|
367
|
+
stroke={staticStroke}
|
|
368
|
+
stroke-width={staticStrokeWidth}
|
|
369
|
+
opacity={staticOpacity}
|
|
370
|
+
class={cls('lc-circle', staticClassName)}
|
|
348
371
|
{...restProps}
|
|
349
372
|
/>
|
|
350
373
|
{/if}
|
|
@@ -382,13 +405,13 @@
|
|
|
382
405
|
style:width="{motionR.current * 2}px"
|
|
383
406
|
style:height="{motionR.current * 2}px"
|
|
384
407
|
style:border-radius="50%"
|
|
385
|
-
style:background-color={
|
|
386
|
-
style:opacity={
|
|
387
|
-
style:border-width={
|
|
388
|
-
style:border-color={
|
|
408
|
+
style:background-color={staticFill}
|
|
409
|
+
style:opacity={staticOpacity}
|
|
410
|
+
style:border-width={staticStrokeWidth}
|
|
411
|
+
style:border-color={staticStroke}
|
|
389
412
|
style:border-style="solid"
|
|
390
413
|
style:transform="translate(-50%, -50%)"
|
|
391
|
-
class={cls('lc-circle',
|
|
414
|
+
class={cls('lc-circle', staticClassName)}
|
|
392
415
|
{...restProps}
|
|
393
416
|
>
|
|
394
417
|
{@render children?.()}
|
|
@@ -0,0 +1,389 @@
|
|
|
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 CircleLegendPropsWithoutHTML = {
|
|
6
|
+
/**
|
|
7
|
+
* The scale to use for the legend. Defaults to the chart's `rScale`.
|
|
8
|
+
*/
|
|
9
|
+
scale?: AnyScale;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The title of the legend.
|
|
13
|
+
*
|
|
14
|
+
* @default ''
|
|
15
|
+
*/
|
|
16
|
+
title?: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The number of ticks to show.
|
|
20
|
+
*
|
|
21
|
+
* @default 4
|
|
22
|
+
*/
|
|
23
|
+
ticks?: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Explicit tick values to show. Overrides `ticks`.
|
|
27
|
+
*/
|
|
28
|
+
tickValues?: number[];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format for the tick labels.
|
|
32
|
+
*/
|
|
33
|
+
tickFormat?: FormatType | FormatConfig;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The font size of the tick labels.
|
|
37
|
+
*
|
|
38
|
+
* @default 10
|
|
39
|
+
*/
|
|
40
|
+
tickFontSize?: number;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The font size of the title.
|
|
44
|
+
*
|
|
45
|
+
* @default 10
|
|
46
|
+
*/
|
|
47
|
+
titleFontSize?: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Width reserved for the tick labels next to the circles.
|
|
51
|
+
*
|
|
52
|
+
* @default 40
|
|
53
|
+
*/
|
|
54
|
+
labelWidth?: number;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gap between the top of each circle and the leader line/label.
|
|
58
|
+
*
|
|
59
|
+
* @default 4
|
|
60
|
+
*/
|
|
61
|
+
labelGap?: number;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Where to render the tick labels.
|
|
65
|
+
* - `'right'` / `'left'`: outside the circles with a leader line
|
|
66
|
+
* - `'inline'`: centered inside each circle, near the top
|
|
67
|
+
*
|
|
68
|
+
* @default 'right'
|
|
69
|
+
*/
|
|
70
|
+
labelPlacement?: 'left' | 'right' | 'inline';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The placement of the legend.
|
|
74
|
+
*/
|
|
75
|
+
placement?: Placement;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The fill color of the circles.
|
|
79
|
+
*
|
|
80
|
+
* @default 'none'
|
|
81
|
+
*/
|
|
82
|
+
fill?: string;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The stroke color of the circles and leader lines.
|
|
86
|
+
*
|
|
87
|
+
* @default 'currentColor'
|
|
88
|
+
*/
|
|
89
|
+
stroke?: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The stroke width of the circles.
|
|
93
|
+
*
|
|
94
|
+
* @default 1
|
|
95
|
+
*/
|
|
96
|
+
strokeWidth?: number;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Value to indicate on the legend (e.g. the currently hovered data point).
|
|
100
|
+
* When set, a 50%-opacity filled circle is drawn at this value's radius.
|
|
101
|
+
* Defaults to auto-detecting from `ctx.tooltip.data` via the chart's
|
|
102
|
+
* radius accessor (`ctx.r`).
|
|
103
|
+
*/
|
|
104
|
+
value?: number | null;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Classes to apply to the elements.
|
|
108
|
+
*
|
|
109
|
+
* @default {}
|
|
110
|
+
*/
|
|
111
|
+
classes?: {
|
|
112
|
+
root?: string;
|
|
113
|
+
title?: string;
|
|
114
|
+
circle?: string;
|
|
115
|
+
tick?: string;
|
|
116
|
+
label?: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A bindable reference to the wrapping `<div>` element.
|
|
121
|
+
*
|
|
122
|
+
* @bindable
|
|
123
|
+
*/
|
|
124
|
+
ref?: HTMLElement;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type CircleLegendProps = CircleLegendPropsWithoutHTML &
|
|
128
|
+
Without<HTMLAttributes<HTMLElement>, CircleLegendPropsWithoutHTML>;
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<script lang="ts">
|
|
132
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
133
|
+
import { format, type FormatType, type FormatConfig } from '@layerstack/utils';
|
|
134
|
+
|
|
135
|
+
import { cls } from '@layerstack/tailwind';
|
|
136
|
+
import type { AnyScale } from '../utils/scales.svelte.js';
|
|
137
|
+
import { getChartContext } from '../contexts/chart.js';
|
|
138
|
+
|
|
139
|
+
let {
|
|
140
|
+
scale: scaleProp,
|
|
141
|
+
title = '',
|
|
142
|
+
ticks = 4,
|
|
143
|
+
tickValues: tickValuesProp,
|
|
144
|
+
tickFormat: tickFormatProp,
|
|
145
|
+
tickFontSize = 10,
|
|
146
|
+
titleFontSize = 10,
|
|
147
|
+
labelWidth = 40,
|
|
148
|
+
labelGap = 4,
|
|
149
|
+
labelPlacement = 'right',
|
|
150
|
+
placement,
|
|
151
|
+
fill = 'none',
|
|
152
|
+
stroke = 'currentColor',
|
|
153
|
+
strokeWidth = 1,
|
|
154
|
+
value: valueProp,
|
|
155
|
+
classes = {},
|
|
156
|
+
ref: refProp = $bindable(),
|
|
157
|
+
class: className,
|
|
158
|
+
...restProps
|
|
159
|
+
}: CircleLegendProps = $props();
|
|
160
|
+
|
|
161
|
+
let ref = $state<HTMLElement>();
|
|
162
|
+
$effect.pre(() => {
|
|
163
|
+
refProp = ref;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const ctx = getChartContext();
|
|
167
|
+
|
|
168
|
+
const scale = $derived(scaleProp ?? ctx.rScale);
|
|
169
|
+
|
|
170
|
+
const tickValues = $derived.by(() => {
|
|
171
|
+
if (tickValuesProp) return tickValuesProp;
|
|
172
|
+
if (!scale) return [] as number[];
|
|
173
|
+
|
|
174
|
+
// Prefer scale.ticks (continuous scales) and pick the largest `ticks` positive values
|
|
175
|
+
if (typeof (scale as any).ticks === 'function') {
|
|
176
|
+
const all = ((scale as any).ticks(ticks) as number[]).filter((v) => Number(scale(v)) > 0);
|
|
177
|
+
if (all.length >= 2) {
|
|
178
|
+
return all.slice(-ticks);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fallback: derive evenly spaced values from the domain extent
|
|
183
|
+
const domain = scale.domain() as number[];
|
|
184
|
+
const min = Number(domain[0]);
|
|
185
|
+
const max = Number(domain[domain.length - 1]);
|
|
186
|
+
const n = Math.max(2, ticks);
|
|
187
|
+
return Array.from({ length: n }, (_, i) => min + ((max - min) * (i + 1)) / n);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const items = $derived.by(() => {
|
|
191
|
+
if (!scale) return [] as Array<{ value: number; radius: number }>;
|
|
192
|
+
return tickValues
|
|
193
|
+
.map((value) => ({ value, radius: Number(scale(value)) }))
|
|
194
|
+
.filter((d) => Number.isFinite(d.radius) && d.radius > 0)
|
|
195
|
+
.sort((a, b) => b.radius - a.radius);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const maxRadius = $derived(items[0]?.radius ?? 0);
|
|
199
|
+
|
|
200
|
+
// Indicator for the currently hovered value. If `value` is explicitly
|
|
201
|
+
// provided, use it; otherwise fall back to `ctx.tooltip.data` piped through
|
|
202
|
+
// the chart's radius accessor (`ctx.r`).
|
|
203
|
+
const indicatorRadius = $derived.by(() => {
|
|
204
|
+
if (!scale) return null;
|
|
205
|
+
let value: any = valueProp;
|
|
206
|
+
if (value == null) {
|
|
207
|
+
const data = ctx.tooltip?.data;
|
|
208
|
+
if (data == null) return null;
|
|
209
|
+
value = ctx.r?.(data);
|
|
210
|
+
}
|
|
211
|
+
if (value == null) return null;
|
|
212
|
+
const r = Number(scale(value));
|
|
213
|
+
if (!Number.isFinite(r) || r <= 0) return null;
|
|
214
|
+
return r;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const padding = $derived(Math.ceil(strokeWidth / 2));
|
|
218
|
+
const titleHeight = $derived(title ? titleFontSize + 6 : 0);
|
|
219
|
+
const width = $derived(
|
|
220
|
+
labelPlacement === 'inline'
|
|
221
|
+
? maxRadius * 2 + padding * 2
|
|
222
|
+
: maxRadius * 2 + padding * 2 + labelGap + labelWidth
|
|
223
|
+
);
|
|
224
|
+
const svgHeight = $derived(maxRadius * 2 + padding * 2 + titleHeight);
|
|
225
|
+
const cx = $derived(
|
|
226
|
+
labelPlacement === 'left' ? labelWidth + labelGap + maxRadius + padding : maxRadius + padding
|
|
227
|
+
);
|
|
228
|
+
const baseY = $derived(maxRadius * 2 + padding + titleHeight);
|
|
229
|
+
|
|
230
|
+
// Leader line / label x positions (only used for left/right placement)
|
|
231
|
+
const labelLineX = $derived(
|
|
232
|
+
labelPlacement === 'left' ? cx - maxRadius - labelGap : cx + maxRadius + labelGap
|
|
233
|
+
);
|
|
234
|
+
const labelTextX = $derived(
|
|
235
|
+
labelPlacement === 'inline' ? cx : labelPlacement === 'left' ? labelLineX - 2 : labelLineX + 2
|
|
236
|
+
);
|
|
237
|
+
const labelTextAnchor = $derived(
|
|
238
|
+
labelPlacement === 'inline' ? 'middle' : labelPlacement === 'left' ? 'end' : 'start'
|
|
239
|
+
);
|
|
240
|
+
</script>
|
|
241
|
+
|
|
242
|
+
<div
|
|
243
|
+
bind:this={ref}
|
|
244
|
+
{...restProps}
|
|
245
|
+
data-placement={placement}
|
|
246
|
+
class={cls('lc-circle-legend-container', className, classes.root)}
|
|
247
|
+
>
|
|
248
|
+
{#if items.length}
|
|
249
|
+
<svg {width} height={svgHeight} viewBox="0 0 {width} {svgHeight}" class="lc-circle-legend-svg">
|
|
250
|
+
{#if title}
|
|
251
|
+
<text
|
|
252
|
+
x={cx}
|
|
253
|
+
y={titleFontSize}
|
|
254
|
+
text-anchor="middle"
|
|
255
|
+
style:font-size={titleFontSize}
|
|
256
|
+
class={cls('lc-circle-legend-title', classes.title)}
|
|
257
|
+
>
|
|
258
|
+
{title}
|
|
259
|
+
</text>
|
|
260
|
+
{/if}
|
|
261
|
+
<g class="lc-circle-legend-g">
|
|
262
|
+
{#if indicatorRadius != null}
|
|
263
|
+
<circle
|
|
264
|
+
{cx}
|
|
265
|
+
cy={baseY - indicatorRadius}
|
|
266
|
+
r={indicatorRadius}
|
|
267
|
+
fill={stroke}
|
|
268
|
+
fill-opacity="0.5"
|
|
269
|
+
class={cls('lc-circle-legend-indicator')}
|
|
270
|
+
/>
|
|
271
|
+
{/if}
|
|
272
|
+
{#each items as item (item.value)}
|
|
273
|
+
<circle
|
|
274
|
+
{cx}
|
|
275
|
+
cy={baseY - item.radius}
|
|
276
|
+
r={item.radius}
|
|
277
|
+
{fill}
|
|
278
|
+
{stroke}
|
|
279
|
+
stroke-width={strokeWidth}
|
|
280
|
+
class={cls('lc-circle-legend-circle', classes.circle)}
|
|
281
|
+
/>
|
|
282
|
+
{#if labelPlacement !== 'inline'}
|
|
283
|
+
<line
|
|
284
|
+
x1={cx}
|
|
285
|
+
y1={baseY - item.radius * 2}
|
|
286
|
+
x2={labelLineX}
|
|
287
|
+
y2={baseY - item.radius * 2}
|
|
288
|
+
{stroke}
|
|
289
|
+
stroke-dasharray="2,2"
|
|
290
|
+
class={cls('lc-circle-legend-tick', classes.tick)}
|
|
291
|
+
/>
|
|
292
|
+
{/if}
|
|
293
|
+
<text
|
|
294
|
+
x={labelTextX}
|
|
295
|
+
y={labelPlacement === 'inline'
|
|
296
|
+
? baseY - item.radius * 2 + tickFontSize
|
|
297
|
+
: baseY - item.radius * 2}
|
|
298
|
+
text-anchor={labelTextAnchor}
|
|
299
|
+
dominant-baseline={labelPlacement === 'inline' ? 'auto' : 'middle'}
|
|
300
|
+
style:font-size={tickFontSize}
|
|
301
|
+
class={cls('lc-circle-legend-label', classes.label)}
|
|
302
|
+
>
|
|
303
|
+
{tickFormatProp ? format(item.value, asAny(tickFormatProp)) : item.value}
|
|
304
|
+
</text>
|
|
305
|
+
{/each}
|
|
306
|
+
</g>
|
|
307
|
+
</svg>
|
|
308
|
+
{/if}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<style>
|
|
312
|
+
@layer components {
|
|
313
|
+
:where(.lc-circle-legend-container) {
|
|
314
|
+
display: inline-block;
|
|
315
|
+
z-index: 1;
|
|
316
|
+
|
|
317
|
+
&[data-placement] {
|
|
318
|
+
position: absolute;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
&[data-placement='top-left'] {
|
|
322
|
+
top: 0;
|
|
323
|
+
left: 0;
|
|
324
|
+
}
|
|
325
|
+
&[data-placement='top'] {
|
|
326
|
+
top: 0;
|
|
327
|
+
left: 50%;
|
|
328
|
+
transform: translateX(-50%);
|
|
329
|
+
}
|
|
330
|
+
&[data-placement='top-right'] {
|
|
331
|
+
top: 0;
|
|
332
|
+
right: 0;
|
|
333
|
+
}
|
|
334
|
+
&[data-placement='left'] {
|
|
335
|
+
top: 50%;
|
|
336
|
+
left: 0;
|
|
337
|
+
transform: translateY(-50%);
|
|
338
|
+
}
|
|
339
|
+
&[data-placement='center'] {
|
|
340
|
+
top: 50%;
|
|
341
|
+
left: 50%;
|
|
342
|
+
transform: translate(-50%, -50%);
|
|
343
|
+
}
|
|
344
|
+
&[data-placement='right'] {
|
|
345
|
+
top: 50%;
|
|
346
|
+
right: 0;
|
|
347
|
+
transform: translateY(-50%);
|
|
348
|
+
}
|
|
349
|
+
&[data-placement='bottom-left'] {
|
|
350
|
+
bottom: 0;
|
|
351
|
+
left: 0;
|
|
352
|
+
}
|
|
353
|
+
&[data-placement='bottom'] {
|
|
354
|
+
bottom: 0;
|
|
355
|
+
left: 50%;
|
|
356
|
+
transform: translateX(-50%);
|
|
357
|
+
}
|
|
358
|
+
&[data-placement='bottom-right'] {
|
|
359
|
+
bottom: 0;
|
|
360
|
+
right: 0;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:where(.lc-circle-legend-title) {
|
|
365
|
+
font-weight: 600;
|
|
366
|
+
fill: var(--color-surface-content, currentColor);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
:where(.lc-circle-legend-svg) {
|
|
370
|
+
overflow: visible;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
:where(.lc-circle-legend-label) {
|
|
374
|
+
fill: var(--color-surface-content, currentColor);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
:where(.lc-circle-legend-tick) {
|
|
378
|
+
stroke: var(--color-surface-content, currentColor);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
:where(.lc-circle-legend-circle) {
|
|
382
|
+
stroke: var(--color-surface-content, currentColor);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
:where(.lc-circle-legend-indicator) {
|
|
386
|
+
fill: var(--color-surface-content, currentColor);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
</style>
|