@juspay/svelte-ui-components 2.30.0 → 2.32.0
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/AreaChart/AreaChart.svelte +421 -0
- package/dist/AreaChart/AreaChart.svelte.d.ts +4 -0
- package/dist/AreaChart/properties.d.ts +62 -0
- package/dist/AreaChart/properties.js +1 -0
- package/dist/BarChart/BarChart.svelte +372 -0
- package/dist/BarChart/BarChart.svelte.d.ts +4 -0
- package/dist/BarChart/properties.d.ts +44 -0
- package/dist/BarChart/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +361 -0
- package/dist/LineChart/LineChart.svelte.d.ts +4 -0
- package/dist/LineChart/properties.d.ts +59 -0
- package/dist/LineChart/properties.js +1 -0
- package/dist/PieChart/PieChart.svelte +236 -0
- package/dist/PieChart/PieChart.svelte.d.ts +4 -0
- package/dist/PieChart/properties.d.ts +36 -0
- package/dist/PieChart/properties.js +1 -0
- package/dist/SankeyChart/SankeyChart.svelte +312 -0
- package/dist/SankeyChart/SankeyChart.svelte.d.ts +4 -0
- package/dist/SankeyChart/properties.d.ts +52 -0
- package/dist/SankeyChart/properties.js +1 -0
- package/dist/Select/Select.svelte +43 -24
- package/dist/Select/properties.d.ts +5 -0
- package/dist/_chart/Axis.svelte +143 -0
- package/dist/_chart/Axis.svelte.d.ts +4 -0
- package/dist/_chart/ChartContainer.svelte +81 -0
- package/dist/_chart/ChartContainer.svelte.d.ts +4 -0
- package/dist/_chart/ChartTooltip.svelte +81 -0
- package/dist/_chart/ChartTooltip.svelte.d.ts +4 -0
- package/dist/_chart/Legend.svelte +59 -0
- package/dist/_chart/Legend.svelte.d.ts +4 -0
- package/dist/_chart/colors.d.ts +4 -0
- package/dist/_chart/colors.js +36 -0
- package/dist/_chart/format.d.ts +3 -0
- package/dist/_chart/format.js +28 -0
- package/dist/_chart/geometry.d.ts +24 -0
- package/dist/_chart/geometry.js +204 -0
- package/dist/_chart/paths.d.ts +4 -0
- package/dist/_chart/paths.js +138 -0
- package/dist/_chart/scales.d.ts +5 -0
- package/dist/_chart/scales.js +77 -0
- package/dist/_chart/types.d.ts +128 -0
- package/dist/_chart/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SankeyChartProperties, SankeyTooltipContext } from './properties';
|
|
3
|
+
import ChartContainer from '../_chart/ChartContainer.svelte';
|
|
4
|
+
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
5
|
+
import { computeSankeyLayout } from '../_chart/geometry';
|
|
6
|
+
import { getColor } from '../_chart/colors';
|
|
7
|
+
import { formatNumber } from '../_chart/format';
|
|
8
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
9
|
+
|
|
10
|
+
// ── Props ──────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
let {
|
|
13
|
+
nodes,
|
|
14
|
+
links,
|
|
15
|
+
nodeWidth = 16,
|
|
16
|
+
nodePadding = 8,
|
|
17
|
+
iterations = 6,
|
|
18
|
+
showValues = false,
|
|
19
|
+
showLabels = true,
|
|
20
|
+
aspectRatio = 16 / 9,
|
|
21
|
+
valueFormat,
|
|
22
|
+
tooltipSnippet,
|
|
23
|
+
empty,
|
|
24
|
+
onnodeclick,
|
|
25
|
+
onlinkclick,
|
|
26
|
+
onnodehover,
|
|
27
|
+
onlinkhover,
|
|
28
|
+
testId,
|
|
29
|
+
classes
|
|
30
|
+
}: SankeyChartProperties = $props();
|
|
31
|
+
|
|
32
|
+
// ── State ──────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
35
|
+
let chartWidth = $state(0);
|
|
36
|
+
let chartHeight = $state(0);
|
|
37
|
+
let hoveredNode = $state<string | null>(null);
|
|
38
|
+
let hoveredLink = $state<{ source: string; target: string } | null>(null);
|
|
39
|
+
let mouseX = $state(0);
|
|
40
|
+
let mouseY = $state(0);
|
|
41
|
+
|
|
42
|
+
// ── Layout ─────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
let format = $derived(valueFormat ?? formatNumber);
|
|
45
|
+
let isEmpty = $derived(nodes.length === 0);
|
|
46
|
+
const MARGIN = 40;
|
|
47
|
+
let layout = $derived(
|
|
48
|
+
computeSankeyLayout(
|
|
49
|
+
nodes,
|
|
50
|
+
links,
|
|
51
|
+
Math.max(0, chartWidth - MARGIN * 2),
|
|
52
|
+
Math.max(0, chartHeight - MARGIN * 2),
|
|
53
|
+
nodeWidth,
|
|
54
|
+
nodePadding,
|
|
55
|
+
iterations
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
let connectedNodes = $derived.by(() => {
|
|
60
|
+
if (hoveredNode !== null) {
|
|
61
|
+
const connected = new SvelteSet<string>([hoveredNode]);
|
|
62
|
+
for (const l of links) {
|
|
63
|
+
if (l.source === hoveredNode || l.target === hoveredNode) {
|
|
64
|
+
connected.add(l.source);
|
|
65
|
+
connected.add(l.target);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return connected;
|
|
69
|
+
}
|
|
70
|
+
if (hoveredLink !== null) {
|
|
71
|
+
return new SvelteSet([hoveredLink.source, hoveredLink.target]);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── Tooltip ────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
let tooltipData = $derived.by(() => {
|
|
79
|
+
if (hoveredNode !== null) {
|
|
80
|
+
const n = layout.nodes.find((nd) => nd.id === hoveredNode);
|
|
81
|
+
if (!n) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
title: n.label,
|
|
86
|
+
items: [
|
|
87
|
+
{
|
|
88
|
+
label: 'Total flow',
|
|
89
|
+
value: format(n.value),
|
|
90
|
+
color: n.color ?? getColor(layout.nodes.indexOf(n))
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (hoveredLink !== null) {
|
|
96
|
+
const l = links.find(
|
|
97
|
+
(lk) => lk.source === hoveredLink!.source && lk.target === hoveredLink!.target
|
|
98
|
+
);
|
|
99
|
+
if (!l) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
title: `${nodes.find((n) => n.id === l.source)?.label ?? l.source} → ${nodes.find((n) => n.id === l.target)?.label ?? l.target}`,
|
|
104
|
+
items: [{ label: 'Flow', value: format(l.value) }]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let tooltipContext = $derived.by<SankeyTooltipContext | null>(() => {
|
|
111
|
+
if (hoveredNode !== null) {
|
|
112
|
+
const n = findNode(hoveredNode);
|
|
113
|
+
const computed = layout.nodes.find((nd) => nd.id === hoveredNode);
|
|
114
|
+
if (!n || !computed) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return { type: 'node', node: n, value: computed.value };
|
|
118
|
+
}
|
|
119
|
+
if (hoveredLink !== null) {
|
|
120
|
+
const l = findLink(hoveredLink.source, hoveredLink.target);
|
|
121
|
+
if (!l) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return { type: 'link', link: l };
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── Interactions ───────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
function trackMouse(e: MouseEvent) {
|
|
132
|
+
if (containerEl === null) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const rect = containerEl.getBoundingClientRect();
|
|
136
|
+
mouseX = e.clientX - rect.left;
|
|
137
|
+
mouseY = e.clientY - rect.top;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isLinkHighlighted(source: string, target: string): boolean {
|
|
141
|
+
if (hoveredLink !== null) {
|
|
142
|
+
return source === hoveredLink.source && target === hoveredLink.target;
|
|
143
|
+
}
|
|
144
|
+
if (hoveredNode !== null) {
|
|
145
|
+
return source === hoveredNode || target === hoveredNode;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findNode(id: string) {
|
|
151
|
+
return nodes.find((n) => n.id === id);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findLink(source: string, target: string) {
|
|
155
|
+
return links.find((l) => l.source === source && l.target === target);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleNodeEnter(e: MouseEvent, id: string) {
|
|
159
|
+
hoveredNode = id;
|
|
160
|
+
trackMouse(e);
|
|
161
|
+
const orig = findNode(id);
|
|
162
|
+
if (orig) {
|
|
163
|
+
onnodehover?.({ node: orig });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleNodeLeave() {
|
|
168
|
+
hoveredNode = null;
|
|
169
|
+
onnodehover?.(null);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function handleNodeClick(id: string) {
|
|
173
|
+
const orig = findNode(id);
|
|
174
|
+
if (orig) {
|
|
175
|
+
onnodeclick?.({ node: orig });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function handleLinkEnter(e: MouseEvent, source: string, target: string) {
|
|
180
|
+
hoveredLink = { source, target };
|
|
181
|
+
trackMouse(e);
|
|
182
|
+
const orig = findLink(source, target);
|
|
183
|
+
if (orig) {
|
|
184
|
+
onlinkhover?.({ link: orig });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handleLinkLeave() {
|
|
189
|
+
hoveredLink = null;
|
|
190
|
+
onlinkhover?.(null);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleLinkClick(source: string, target: string) {
|
|
194
|
+
const orig = findLink(source, target);
|
|
195
|
+
if (orig) {
|
|
196
|
+
onlinkclick?.({ link: orig });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<div
|
|
202
|
+
class="sankey-chart {classes ?? ''}"
|
|
203
|
+
bind:this={containerEl}
|
|
204
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
205
|
+
>
|
|
206
|
+
{#if isEmpty && typeof empty === 'function'}
|
|
207
|
+
<div class="chart-empty">{@render empty()}</div>
|
|
208
|
+
{:else}
|
|
209
|
+
<ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
|
|
210
|
+
<g transform="translate({MARGIN}, {MARGIN})">
|
|
211
|
+
{#each layout.links as link, i (i)}
|
|
212
|
+
{@const highlighted = isLinkHighlighted(link.source, link.target)}
|
|
213
|
+
{@const dimmed = (hoveredNode !== null || hoveredLink !== null) && !highlighted}
|
|
214
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
215
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
216
|
+
<path
|
|
217
|
+
class="sankey-link"
|
|
218
|
+
d={link.path}
|
|
219
|
+
fill="none"
|
|
220
|
+
stroke={link.color ?? getColor(layout.nodes.findIndex((n) => n.id === link.source))}
|
|
221
|
+
stroke-width={Math.max(1, link.width)}
|
|
222
|
+
stroke-opacity={highlighted ? 0.7 : dimmed ? 0.08 : 0.4}
|
|
223
|
+
onmouseenter={(e) => handleLinkEnter(e, link.source, link.target)}
|
|
224
|
+
onmousemove={trackMouse}
|
|
225
|
+
onmouseleave={handleLinkLeave}
|
|
226
|
+
onclick={() => handleLinkClick(link.source, link.target)}
|
|
227
|
+
/>
|
|
228
|
+
{/each}
|
|
229
|
+
|
|
230
|
+
{#each layout.nodes as node, ni (ni)}
|
|
231
|
+
{@const color = node.color ?? getColor(ni)}
|
|
232
|
+
{@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
|
|
233
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
234
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
235
|
+
<rect
|
|
236
|
+
class="sankey-node"
|
|
237
|
+
class:node-dimmed={dimmed}
|
|
238
|
+
x={node.x}
|
|
239
|
+
y={node.y}
|
|
240
|
+
width={node.width}
|
|
241
|
+
height={node.height}
|
|
242
|
+
rx={2}
|
|
243
|
+
ry={2}
|
|
244
|
+
fill={color}
|
|
245
|
+
onmouseenter={(e) => handleNodeEnter(e, node.id)}
|
|
246
|
+
onmousemove={trackMouse}
|
|
247
|
+
onmouseleave={handleNodeLeave}
|
|
248
|
+
onclick={() => handleNodeClick(node.id)}
|
|
249
|
+
/>
|
|
250
|
+
{#if showLabels}
|
|
251
|
+
<text
|
|
252
|
+
class="sankey-label"
|
|
253
|
+
class:node-dimmed={dimmed}
|
|
254
|
+
x={node.column === 0 ? node.x - 6 : node.x + node.width + 6}
|
|
255
|
+
y={node.y + node.height / 2}
|
|
256
|
+
text-anchor={node.column === 0 ? 'end' : 'start'}
|
|
257
|
+
dominant-baseline="middle"
|
|
258
|
+
>{node.label}{#if showValues}
|
|
259
|
+
({format(node.value)}){/if}</text
|
|
260
|
+
>
|
|
261
|
+
{/if}
|
|
262
|
+
{/each}
|
|
263
|
+
</g>
|
|
264
|
+
</ChartContainer>
|
|
265
|
+
|
|
266
|
+
{#if typeof tooltipSnippet === 'function' && tooltipContext !== null}
|
|
267
|
+
<div class="chart-tooltip-slot" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
|
|
268
|
+
{@render tooltipSnippet(tooltipContext)}
|
|
269
|
+
</div>
|
|
270
|
+
{:else}
|
|
271
|
+
<ChartTooltip data={tooltipData} {mouseX} {mouseY} />
|
|
272
|
+
{/if}
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<style>
|
|
277
|
+
.sankey-chart {
|
|
278
|
+
width: 100%;
|
|
279
|
+
position: relative;
|
|
280
|
+
}
|
|
281
|
+
.sankey-link {
|
|
282
|
+
transition: stroke-opacity var(--chart-transition-duration, 0.2s) ease;
|
|
283
|
+
cursor: pointer;
|
|
284
|
+
}
|
|
285
|
+
.sankey-node {
|
|
286
|
+
transition: opacity var(--chart-transition-duration, 0.2s) ease;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
}
|
|
289
|
+
.sankey-node.node-dimmed {
|
|
290
|
+
opacity: var(--sankey-dimmed-opacity, 0.15);
|
|
291
|
+
}
|
|
292
|
+
.sankey-label {
|
|
293
|
+
fill: var(--sankey-label-color, #333);
|
|
294
|
+
font-size: var(--sankey-label-font-size, 12px);
|
|
295
|
+
font-family: var(--chart-font-family, inherit);
|
|
296
|
+
pointer-events: none;
|
|
297
|
+
transition: opacity var(--chart-transition-duration, 0.2s) ease;
|
|
298
|
+
}
|
|
299
|
+
.sankey-label.node-dimmed {
|
|
300
|
+
opacity: var(--sankey-dimmed-opacity, 0.15);
|
|
301
|
+
}
|
|
302
|
+
.chart-tooltip-slot {
|
|
303
|
+
position: absolute;
|
|
304
|
+
z-index: 10;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
}
|
|
307
|
+
.chart-empty {
|
|
308
|
+
padding: var(--chart-empty-padding, 32px 24px);
|
|
309
|
+
color: var(--chart-empty-color, #9ca3af);
|
|
310
|
+
text-align: center;
|
|
311
|
+
}
|
|
312
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type SankeyNode = {
|
|
3
|
+
id: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
};
|
|
7
|
+
export type SankeyLink = {
|
|
8
|
+
source: string;
|
|
9
|
+
target: string;
|
|
10
|
+
value: number;
|
|
11
|
+
color?: string;
|
|
12
|
+
};
|
|
13
|
+
export type SankeyTooltipContext = {
|
|
14
|
+
type: 'node';
|
|
15
|
+
node: SankeyNode;
|
|
16
|
+
value: number;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'link';
|
|
19
|
+
link: SankeyLink;
|
|
20
|
+
};
|
|
21
|
+
export type SankeyChartProperties = MandatorySankeyChartProperties & OptionalSankeyChartProperties & SankeyChartEventProperties;
|
|
22
|
+
export type MandatorySankeyChartProperties = {
|
|
23
|
+
nodes: SankeyNode[];
|
|
24
|
+
links: SankeyLink[];
|
|
25
|
+
};
|
|
26
|
+
export type OptionalSankeyChartProperties = {
|
|
27
|
+
nodeWidth?: number;
|
|
28
|
+
nodePadding?: number;
|
|
29
|
+
iterations?: number;
|
|
30
|
+
showValues?: boolean;
|
|
31
|
+
showLabels?: boolean;
|
|
32
|
+
aspectRatio?: number;
|
|
33
|
+
valueFormat?: (value: number) => string;
|
|
34
|
+
tooltipSnippet?: Snippet<[SankeyTooltipContext]>;
|
|
35
|
+
empty?: Snippet;
|
|
36
|
+
testId?: string;
|
|
37
|
+
classes?: string;
|
|
38
|
+
};
|
|
39
|
+
export type SankeyChartEventProperties = {
|
|
40
|
+
onnodeclick?: (event: {
|
|
41
|
+
node: SankeyNode;
|
|
42
|
+
}) => void;
|
|
43
|
+
onlinkclick?: (event: {
|
|
44
|
+
link: SankeyLink;
|
|
45
|
+
}) => void;
|
|
46
|
+
onnodehover?: (event: {
|
|
47
|
+
node: SankeyNode;
|
|
48
|
+
} | null) => void;
|
|
49
|
+
onlinkhover?: (event: {
|
|
50
|
+
link: SankeyLink;
|
|
51
|
+
} | null) => void;
|
|
52
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
disabled = false,
|
|
14
14
|
bottomContent,
|
|
15
15
|
optionIndicator,
|
|
16
|
+
triggerSummary,
|
|
16
17
|
testId,
|
|
17
18
|
itemTestId,
|
|
18
19
|
onchange,
|
|
@@ -256,30 +257,48 @@
|
|
|
256
257
|
tabindex={disabled ? -1 : searchable ? -1 : 0}
|
|
257
258
|
>
|
|
258
259
|
{#if multiple}
|
|
259
|
-
{#
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
260
|
+
{#if typeof triggerSummary === 'function'}
|
|
261
|
+
{@render triggerSummary({ value, items })}
|
|
262
|
+
{#if searchable}
|
|
263
|
+
<input
|
|
264
|
+
class="select-search"
|
|
265
|
+
type="text"
|
|
266
|
+
value={query}
|
|
267
|
+
oninput={handleSearchInput}
|
|
268
|
+
onfocus={handleSearchFocus}
|
|
269
|
+
bind:this={searchInputEl}
|
|
270
|
+
placeholder={value.length === 0 ? placeholder : ''}
|
|
271
|
+
{disabled}
|
|
272
|
+
autocomplete="off"
|
|
273
|
+
tabindex={disabled ? -1 : 0}
|
|
274
|
+
/>
|
|
275
|
+
{/if}
|
|
276
|
+
{:else}
|
|
277
|
+
{#each value as id (id)}
|
|
278
|
+
<Pill
|
|
279
|
+
text={getLabel(id)}
|
|
280
|
+
dismissible
|
|
281
|
+
{disabled}
|
|
282
|
+
ondismiss={() => removeItem(id)}
|
|
283
|
+
{...typeof testId === 'string' ? { testId: `${testId}-pill-${id}` } : {}}
|
|
284
|
+
/>
|
|
285
|
+
{/each}
|
|
286
|
+
{#if searchable}
|
|
287
|
+
<input
|
|
288
|
+
class="select-search"
|
|
289
|
+
type="text"
|
|
290
|
+
value={query}
|
|
291
|
+
oninput={handleSearchInput}
|
|
292
|
+
onfocus={handleSearchFocus}
|
|
293
|
+
bind:this={searchInputEl}
|
|
294
|
+
placeholder={value.length === 0 ? placeholder : ''}
|
|
295
|
+
{disabled}
|
|
296
|
+
autocomplete="off"
|
|
297
|
+
tabindex={disabled ? -1 : 0}
|
|
298
|
+
/>
|
|
299
|
+
{:else if value.length === 0}
|
|
300
|
+
<span class="select-placeholder">{placeholder}</span>
|
|
301
|
+
{/if}
|
|
283
302
|
{/if}
|
|
284
303
|
{:else if searchable}
|
|
285
304
|
<input
|
|
@@ -27,6 +27,11 @@ export type OptionalSelectProperties = {
|
|
|
27
27
|
open?: boolean;
|
|
28
28
|
/** Horizontal anchor of the dropdown panel. `'left'` (default) anchors to the trigger's left edge; `'right'` anchors to the right edge so a content-wider panel hangs leftward instead of overflowing. */
|
|
29
29
|
dropdownAlign?: 'left' | 'right';
|
|
30
|
+
/** Snippet for rendering a compact trigger summary in multiple mode instead of one Pill per selected value. Receives `{ value, items }` so the consumer can compute e.g. "All" / "3 selected". When not provided the default Pill-per-value behaviour is used. */
|
|
31
|
+
triggerSummary?: import('svelte').Snippet<[{
|
|
32
|
+
value: string[];
|
|
33
|
+
items: SelectItem[];
|
|
34
|
+
}]>;
|
|
30
35
|
};
|
|
31
36
|
export type SelectEventProperties = {
|
|
32
37
|
onchange?: (value: string[]) => void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { AxisProperties } from './types';
|
|
3
|
+
import { defaultTickFormat } from './format';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
orientation,
|
|
7
|
+
scale,
|
|
8
|
+
tickCount = 5,
|
|
9
|
+
tickFormat,
|
|
10
|
+
showGridlines = false,
|
|
11
|
+
gridlineLength = 0,
|
|
12
|
+
label,
|
|
13
|
+
classes
|
|
14
|
+
}: AxisProperties = $props();
|
|
15
|
+
|
|
16
|
+
let format = $derived(tickFormat ?? defaultTickFormat);
|
|
17
|
+
|
|
18
|
+
let isHorizontal = $derived(orientation === 'top' || orientation === 'bottom');
|
|
19
|
+
|
|
20
|
+
let tickValues = $derived.by(() => {
|
|
21
|
+
if ('ticks' in scale && typeof scale.ticks === 'function') {
|
|
22
|
+
return scale.ticks(tickCount);
|
|
23
|
+
}
|
|
24
|
+
if ('domain' in scale && Array.isArray(scale.domain)) {
|
|
25
|
+
return scale.domain;
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
let isBand = $derived('bandwidth' in scale);
|
|
31
|
+
let bandOffset = $derived(isBand && 'bandwidth' in scale ? scale.bandwidth / 2 : 0);
|
|
32
|
+
|
|
33
|
+
function positionTick(tick: number | string): number {
|
|
34
|
+
if ('bandwidth' in scale) {
|
|
35
|
+
return scale(String(tick)) + bandOffset;
|
|
36
|
+
}
|
|
37
|
+
return scale(Number(tick)) + bandOffset;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TICK_SIZE = 6;
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<g class="axis axis-{orientation} {classes ?? ''}">
|
|
44
|
+
{#if isHorizontal}
|
|
45
|
+
<line class="axis-line" x1={scale.range[0]} x2={scale.range[1]} y1={0} y2={0} />
|
|
46
|
+
{#each tickValues as tick, i (i)}
|
|
47
|
+
{@const x = positionTick(tick)}
|
|
48
|
+
<g class="tick" transform="translate({x}, 0)">
|
|
49
|
+
<line class="tick-mark" y2={orientation === 'bottom' ? TICK_SIZE : -TICK_SIZE} />
|
|
50
|
+
<text
|
|
51
|
+
class="tick-label"
|
|
52
|
+
y={orientation === 'bottom' ? TICK_SIZE + 4 : -(TICK_SIZE + 4)}
|
|
53
|
+
text-anchor="middle"
|
|
54
|
+
dominant-baseline={orientation === 'bottom' ? 'hanging' : 'auto'}
|
|
55
|
+
>
|
|
56
|
+
{format(tick)}
|
|
57
|
+
</text>
|
|
58
|
+
{#if showGridlines && gridlineLength > 0}
|
|
59
|
+
<line
|
|
60
|
+
class="gridline"
|
|
61
|
+
y1={0}
|
|
62
|
+
y2={orientation === 'bottom' ? -gridlineLength : gridlineLength}
|
|
63
|
+
/>
|
|
64
|
+
{/if}
|
|
65
|
+
</g>
|
|
66
|
+
{/each}
|
|
67
|
+
{#if label}
|
|
68
|
+
<text
|
|
69
|
+
class="axis-label"
|
|
70
|
+
x={(scale.range[0] + scale.range[1]) / 2}
|
|
71
|
+
y={orientation === 'bottom' ? 36 : -30}
|
|
72
|
+
text-anchor="middle"
|
|
73
|
+
>
|
|
74
|
+
{label}
|
|
75
|
+
</text>
|
|
76
|
+
{/if}
|
|
77
|
+
{:else}
|
|
78
|
+
<line class="axis-line" y1={scale.range[0]} y2={scale.range[1]} x1={0} x2={0} />
|
|
79
|
+
{#each tickValues as tick, i (i)}
|
|
80
|
+
{@const y = positionTick(tick)}
|
|
81
|
+
<g class="tick" transform="translate(0, {y})">
|
|
82
|
+
<line class="tick-mark" x2={orientation === 'left' ? -TICK_SIZE : TICK_SIZE} />
|
|
83
|
+
<text
|
|
84
|
+
class="tick-label"
|
|
85
|
+
x={orientation === 'left' ? -(TICK_SIZE + 4) : TICK_SIZE + 4}
|
|
86
|
+
text-anchor={orientation === 'left' ? 'end' : 'start'}
|
|
87
|
+
dominant-baseline="middle"
|
|
88
|
+
>
|
|
89
|
+
{format(tick)}
|
|
90
|
+
</text>
|
|
91
|
+
{#if showGridlines && gridlineLength > 0}
|
|
92
|
+
<line
|
|
93
|
+
class="gridline"
|
|
94
|
+
x1={0}
|
|
95
|
+
x2={orientation === 'left' ? gridlineLength : -gridlineLength}
|
|
96
|
+
/>
|
|
97
|
+
{/if}
|
|
98
|
+
</g>
|
|
99
|
+
{/each}
|
|
100
|
+
{#if label}
|
|
101
|
+
<text
|
|
102
|
+
class="axis-label"
|
|
103
|
+
transform="rotate(-90)"
|
|
104
|
+
x={-(scale.range[0] + scale.range[1]) / 2}
|
|
105
|
+
y={orientation === 'left' ? -40 : 40}
|
|
106
|
+
text-anchor="middle"
|
|
107
|
+
>
|
|
108
|
+
{label}
|
|
109
|
+
</text>
|
|
110
|
+
{/if}
|
|
111
|
+
{/if}
|
|
112
|
+
</g>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
.axis-line {
|
|
116
|
+
stroke: var(--chart-axis-color, #666);
|
|
117
|
+
stroke-width: var(--chart-axis-stroke-width, 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.tick-mark {
|
|
121
|
+
stroke: var(--chart-axis-color, #666);
|
|
122
|
+
stroke-width: var(--chart-axis-stroke-width, 1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.tick-label {
|
|
126
|
+
fill: var(--chart-axis-color, #666);
|
|
127
|
+
font-size: var(--chart-axis-font-size, 11px);
|
|
128
|
+
font-family: var(--chart-axis-font-family, inherit);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.axis-label {
|
|
132
|
+
fill: var(--chart-axis-label-color, #333);
|
|
133
|
+
font-size: var(--chart-axis-label-font-size, 12px);
|
|
134
|
+
font-family: var(--chart-axis-font-family, inherit);
|
|
135
|
+
font-weight: 500;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.gridline {
|
|
139
|
+
stroke: var(--chart-gridline-color, #e0e0e0);
|
|
140
|
+
stroke-opacity: var(--chart-gridline-opacity, 0.5);
|
|
141
|
+
stroke-dasharray: var(--chart-gridline-dash, 4 4);
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChartContainerProperties } from './types';
|
|
3
|
+
import { onMount } from 'svelte';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
width = $bindable(0),
|
|
7
|
+
height = $bindable(0),
|
|
8
|
+
aspectRatio = 16 / 9,
|
|
9
|
+
minHeight = 0,
|
|
10
|
+
testId,
|
|
11
|
+
classes,
|
|
12
|
+
children
|
|
13
|
+
}: ChartContainerProperties = $props();
|
|
14
|
+
|
|
15
|
+
let containerEl: HTMLDivElement | null = $state(null);
|
|
16
|
+
|
|
17
|
+
onMount(() => {
|
|
18
|
+
if (containerEl === null) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function measure() {
|
|
23
|
+
if (containerEl === null) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const rect = containerEl.getBoundingClientRect();
|
|
27
|
+
const w = Math.round(rect.width);
|
|
28
|
+
width = w;
|
|
29
|
+
height = Math.max(minHeight, Math.round(w / aspectRatio));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
measure();
|
|
33
|
+
|
|
34
|
+
// Coalesce bursts of resize events into a single measure per frame, always
|
|
35
|
+
// using the latest size (a leading-edge debounce would drop the final size).
|
|
36
|
+
let frame = 0;
|
|
37
|
+
const observer = new ResizeObserver(() => {
|
|
38
|
+
cancelAnimationFrame(frame);
|
|
39
|
+
frame = requestAnimationFrame(measure);
|
|
40
|
+
});
|
|
41
|
+
observer.observe(containerEl);
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
cancelAnimationFrame(frame);
|
|
45
|
+
observer.disconnect();
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<div
|
|
51
|
+
class="chart-container {classes ?? ''}"
|
|
52
|
+
bind:this={containerEl}
|
|
53
|
+
data-pw={typeof testId === 'string' ? testId : null}
|
|
54
|
+
>
|
|
55
|
+
{#if width > 0 && height > 0}
|
|
56
|
+
<svg
|
|
57
|
+
viewBox="0 0 {width} {height}"
|
|
58
|
+
preserveAspectRatio="xMidYMid meet"
|
|
59
|
+
role="img"
|
|
60
|
+
{width}
|
|
61
|
+
{height}
|
|
62
|
+
>
|
|
63
|
+
{@render children()}
|
|
64
|
+
</svg>
|
|
65
|
+
{/if}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.chart-container {
|
|
70
|
+
width: 100%;
|
|
71
|
+
background: var(--chart-background, transparent);
|
|
72
|
+
font-family: var(--chart-font-family, inherit);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
svg {
|
|
76
|
+
display: block;
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: auto;
|
|
79
|
+
overflow: visible;
|
|
80
|
+
}
|
|
81
|
+
</style>
|