@juspay/svelte-ui-components 2.31.0 → 2.32.1
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/_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 {};
|
|
@@ -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>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChartTooltipProperties } from './types';
|
|
3
|
+
|
|
4
|
+
let { data, mouseX = 0, mouseY = 0, customSnippet, classes }: ChartTooltipProperties = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
{#if data !== null}
|
|
8
|
+
<div class="chart-tooltip {classes ?? ''}" style="left: {mouseX + 12}px; top: {mouseY - 12}px;">
|
|
9
|
+
{#if typeof customSnippet === 'function'}
|
|
10
|
+
{@render customSnippet(data)}
|
|
11
|
+
{:else}
|
|
12
|
+
{#if data.title}
|
|
13
|
+
<div class="tooltip-title">{data.title}</div>
|
|
14
|
+
{/if}
|
|
15
|
+
{#each data.items as item, i (i)}
|
|
16
|
+
<div class="tooltip-item">
|
|
17
|
+
{#if item.color}
|
|
18
|
+
<span class="tooltip-swatch" style="background: {item.color}"></span>
|
|
19
|
+
{/if}
|
|
20
|
+
<span class="tooltip-label">{item.label}</span>
|
|
21
|
+
<span class="tooltip-value">{item.value}</span>
|
|
22
|
+
</div>
|
|
23
|
+
{/each}
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
26
|
+
{/if}
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.chart-tooltip {
|
|
30
|
+
position: absolute;
|
|
31
|
+
z-index: var(--chart-tooltip-z-index, 10);
|
|
32
|
+
background: var(--chart-tooltip-background, rgba(0, 0, 0, 0.85));
|
|
33
|
+
color: var(--chart-tooltip-color, #fff);
|
|
34
|
+
font-size: var(--chart-tooltip-font-size, 12px);
|
|
35
|
+
font-family: var(--chart-font-family, inherit);
|
|
36
|
+
padding: var(--chart-tooltip-padding, 8px 12px);
|
|
37
|
+
border-radius: var(--chart-tooltip-border-radius, 4px);
|
|
38
|
+
box-shadow: var(--chart-tooltip-shadow, 0 2px 8px rgba(0, 0, 0, 0.2));
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
max-width: var(--chart-tooltip-max-width, 280px);
|
|
41
|
+
width: fit-content;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.tooltip-title {
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
margin-bottom: 4px;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
text-overflow: ellipsis;
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.tooltip-item {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: 6px;
|
|
56
|
+
padding: 1px 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.tooltip-swatch {
|
|
60
|
+
display: inline-block;
|
|
61
|
+
width: 8px;
|
|
62
|
+
height: 8px;
|
|
63
|
+
border-radius: 2px;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.tooltip-label {
|
|
68
|
+
opacity: 0.8;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
text-overflow: ellipsis;
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
min-width: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.tooltip-value {
|
|
76
|
+
font-weight: 600;
|
|
77
|
+
margin-left: auto;
|
|
78
|
+
padding-left: 12px;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
</style>
|