@juspay/svelte-ui-components 2.56.0 → 2.58.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/Input/Input.svelte +178 -55
- package/dist/Input/properties.d.ts +17 -0
- package/dist/SankeyChart/SankeyChart.svelte +100 -19
- package/dist/SankeyChart/properties.d.ts +22 -0
- package/package.json +1 -1
package/dist/Input/Input.svelte
CHANGED
|
@@ -39,7 +39,15 @@
|
|
|
39
39
|
ariaExpanded,
|
|
40
40
|
ariaAutocomplete,
|
|
41
41
|
ariaControls,
|
|
42
|
-
ariaActivedescendant
|
|
42
|
+
ariaActivedescendant,
|
|
43
|
+
leftIcon,
|
|
44
|
+
rightIcon,
|
|
45
|
+
onLeftIconClick,
|
|
46
|
+
onRightIconClick,
|
|
47
|
+
leftIconLabel = 'Leading action',
|
|
48
|
+
rightIconLabel = 'Trailing action',
|
|
49
|
+
mandatory = false,
|
|
50
|
+
forceError = false
|
|
43
51
|
}: InputProperties = $props();
|
|
44
52
|
|
|
45
53
|
export function focus() {
|
|
@@ -90,6 +98,11 @@
|
|
|
90
98
|
});
|
|
91
99
|
|
|
92
100
|
const showErrorMessage = $derived(validationState === 'Invalid');
|
|
101
|
+
// forceError lets consumers drive the error border from server/runtime validation,
|
|
102
|
+
// independent of validationPattern.
|
|
103
|
+
const showError = $derived(showErrorMessage || forceError);
|
|
104
|
+
const hasLeftIcon = $derived(typeof leftIcon === 'function');
|
|
105
|
+
const hasRightIcon = $derived(typeof rightIcon === 'function');
|
|
93
106
|
|
|
94
107
|
function handleOnInput(event: Event) {
|
|
95
108
|
if (inputElement === null) {
|
|
@@ -192,68 +205,113 @@
|
|
|
192
205
|
}
|
|
193
206
|
</script>
|
|
194
207
|
|
|
195
|
-
<div class="input-container {classes ?? ''}" class:input-error={
|
|
208
|
+
<div class="input-container {classes ?? ''}" class:input-error={showError && !actionInput}>
|
|
196
209
|
{#if typeof label === 'string' && label !== '' && !actionInput}
|
|
197
210
|
<label class="label" for={name}>
|
|
198
|
-
{label}
|
|
211
|
+
{label}{#if mandatory}<span class="input-mandatory-asterisk" aria-hidden="true">*</span>{/if}
|
|
199
212
|
</label>
|
|
200
213
|
{/if}
|
|
201
214
|
|
|
202
|
-
{#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
215
|
+
{#snippet fieldElement()}
|
|
216
|
+
{#if useTextArea}
|
|
217
|
+
<textarea
|
|
218
|
+
{value}
|
|
219
|
+
{placeholder}
|
|
220
|
+
autocomplete={autoComplete}
|
|
221
|
+
{name}
|
|
222
|
+
{role}
|
|
223
|
+
aria-expanded={ariaExpanded}
|
|
224
|
+
aria-autocomplete={ariaAutocomplete}
|
|
225
|
+
aria-controls={ariaControls}
|
|
226
|
+
aria-activedescendant={ariaActivedescendant}
|
|
227
|
+
aria-required={mandatory || null}
|
|
228
|
+
required={mandatory || null}
|
|
229
|
+
onfocus={onFocus}
|
|
230
|
+
onfocusout={_onFocusOut}
|
|
231
|
+
oninput={handleOnInput}
|
|
232
|
+
onpaste={handleOnPaste}
|
|
233
|
+
onclick={onClick}
|
|
234
|
+
onkeydown={onKeyDown}
|
|
235
|
+
class:action-input={actionInput}
|
|
236
|
+
style="--focus-border: {addFocusColor ? 1 : 0}px;"
|
|
237
|
+
disabled={disable}
|
|
238
|
+
bind:this={inputElement}
|
|
239
|
+
maxlength={dataType === 'tel' ? null : maxLength}
|
|
240
|
+
minlength={minLength}
|
|
241
|
+
></textarea>
|
|
242
|
+
{:else}
|
|
243
|
+
<input
|
|
244
|
+
type={dataType}
|
|
245
|
+
{value}
|
|
246
|
+
{placeholder}
|
|
247
|
+
autocomplete={autoComplete}
|
|
248
|
+
{name}
|
|
249
|
+
{role}
|
|
250
|
+
aria-expanded={ariaExpanded}
|
|
251
|
+
aria-autocomplete={ariaAutocomplete}
|
|
252
|
+
aria-controls={ariaControls}
|
|
253
|
+
aria-activedescendant={ariaActivedescendant}
|
|
254
|
+
aria-required={mandatory || null}
|
|
255
|
+
required={mandatory || null}
|
|
256
|
+
onfocus={onFocus}
|
|
257
|
+
onfocusout={_onFocusOut}
|
|
258
|
+
oninput={handleOnInput}
|
|
259
|
+
onpaste={handleOnPaste}
|
|
260
|
+
onclick={onClick}
|
|
261
|
+
onkeydown={onKeyDown}
|
|
262
|
+
data-pw={testId}
|
|
263
|
+
class:action-input={actionInput}
|
|
264
|
+
disabled={disable}
|
|
265
|
+
bind:this={inputElement}
|
|
266
|
+
maxlength={dataType === 'tel' ? null : maxLength}
|
|
267
|
+
minlength={minLength}
|
|
268
|
+
{min}
|
|
269
|
+
{max}
|
|
270
|
+
/>
|
|
271
|
+
{/if}
|
|
272
|
+
{/snippet}
|
|
273
|
+
|
|
274
|
+
{#if hasLeftIcon || hasRightIcon}
|
|
275
|
+
<div
|
|
276
|
+
class="input-field-wrap"
|
|
277
|
+
class:has-left-icon={hasLeftIcon}
|
|
278
|
+
class:has-right-icon={hasRightIcon}
|
|
279
|
+
>
|
|
280
|
+
{#if hasLeftIcon}
|
|
281
|
+
{#if onLeftIconClick}
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
class="input-icon input-icon-left input-icon-button"
|
|
285
|
+
aria-label={leftIconLabel}
|
|
286
|
+
onclick={onLeftIconClick}
|
|
287
|
+
>
|
|
288
|
+
{@render leftIcon?.()}
|
|
289
|
+
</button>
|
|
290
|
+
{:else}
|
|
291
|
+
<span class="input-icon input-icon-left">{@render leftIcon?.()}</span>
|
|
292
|
+
{/if}
|
|
293
|
+
{/if}
|
|
294
|
+
{@render fieldElement()}
|
|
295
|
+
{#if hasRightIcon}
|
|
296
|
+
{#if onRightIconClick}
|
|
297
|
+
<button
|
|
298
|
+
type="button"
|
|
299
|
+
class="input-icon input-icon-right input-icon-button"
|
|
300
|
+
aria-label={rightIconLabel}
|
|
301
|
+
onclick={onRightIconClick}
|
|
302
|
+
>
|
|
303
|
+
{@render rightIcon?.()}
|
|
304
|
+
</button>
|
|
305
|
+
{:else}
|
|
306
|
+
<span class="input-icon input-icon-right">{@render rightIcon?.()}</span>
|
|
307
|
+
{/if}
|
|
308
|
+
{/if}
|
|
309
|
+
</div>
|
|
227
310
|
{:else}
|
|
228
|
-
|
|
229
|
-
type={dataType}
|
|
230
|
-
{value}
|
|
231
|
-
{placeholder}
|
|
232
|
-
autocomplete={autoComplete}
|
|
233
|
-
{name}
|
|
234
|
-
{role}
|
|
235
|
-
aria-expanded={ariaExpanded}
|
|
236
|
-
aria-autocomplete={ariaAutocomplete}
|
|
237
|
-
aria-controls={ariaControls}
|
|
238
|
-
aria-activedescendant={ariaActivedescendant}
|
|
239
|
-
onfocus={onFocus}
|
|
240
|
-
onfocusout={_onFocusOut}
|
|
241
|
-
oninput={handleOnInput}
|
|
242
|
-
onpaste={handleOnPaste}
|
|
243
|
-
onclick={onClick}
|
|
244
|
-
onkeydown={onKeyDown}
|
|
245
|
-
data-pw={testId}
|
|
246
|
-
class:action-input={actionInput}
|
|
247
|
-
disabled={disable}
|
|
248
|
-
bind:this={inputElement}
|
|
249
|
-
maxlength={dataType === 'tel' ? null : maxLength}
|
|
250
|
-
minlength={minLength}
|
|
251
|
-
{min}
|
|
252
|
-
{max}
|
|
253
|
-
/>
|
|
311
|
+
{@render fieldElement()}
|
|
254
312
|
{/if}
|
|
255
313
|
|
|
256
|
-
{#if onErrorMessage !== '' &&
|
|
314
|
+
{#if onErrorMessage !== '' && showError && !actionInput}
|
|
257
315
|
<div class="error-message">
|
|
258
316
|
{onErrorMessage}
|
|
259
317
|
</div>
|
|
@@ -327,6 +385,71 @@
|
|
|
327
385
|
padding: var(--input-label-msg-padding);
|
|
328
386
|
}
|
|
329
387
|
|
|
388
|
+
.input-mandatory-asterisk {
|
|
389
|
+
color: var(--input-mandatory-color, var(--input-error-msg-text-color, #fa1405));
|
|
390
|
+
margin-left: var(--input-mandatory-gap, 2px);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/* Icon wrapper: only rendered when leftIcon/rightIcon is supplied, so non-icon
|
|
394
|
+
consumers keep the exact prior DOM. The field's bottom margin moves to the
|
|
395
|
+
wrap so the absolutely-positioned icons centre on the field, not the margin. */
|
|
396
|
+
.input-field-wrap {
|
|
397
|
+
position: relative;
|
|
398
|
+
display: block;
|
|
399
|
+
margin: var(--input-margin, 0px 0px 12px 0px);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.input-field-wrap > :global(textarea),
|
|
403
|
+
.input-field-wrap > :global(input) {
|
|
404
|
+
margin: 0 !important;
|
|
405
|
+
width: var(--input-width, 100%);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.input-icon {
|
|
409
|
+
position: absolute;
|
|
410
|
+
top: 50%;
|
|
411
|
+
transform: translateY(-50%);
|
|
412
|
+
display: inline-flex;
|
|
413
|
+
align-items: center;
|
|
414
|
+
justify-content: center;
|
|
415
|
+
width: var(--input-icon-size, 20px);
|
|
416
|
+
height: var(--input-icon-size, 20px);
|
|
417
|
+
color: var(--input-icon-color, inherit);
|
|
418
|
+
pointer-events: none;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.input-icon-button {
|
|
422
|
+
background: none;
|
|
423
|
+
border: none;
|
|
424
|
+
padding: 0;
|
|
425
|
+
cursor: pointer;
|
|
426
|
+
pointer-events: auto;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.input-icon-button:focus-visible {
|
|
430
|
+
outline: var(--input-icon-focus-outline, 2px solid var(--input-focus-border-color, #005fcc));
|
|
431
|
+
outline-offset: var(--input-icon-focus-outline-offset, 2px);
|
|
432
|
+
border-radius: var(--input-icon-focus-radius, 2px);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.input-icon-left {
|
|
436
|
+
left: var(--input-icon-gap, 12px);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.input-icon-right {
|
|
440
|
+
right: var(--input-icon-gap, 12px);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.input-field-wrap.has-left-icon > :global(textarea),
|
|
444
|
+
.input-field-wrap.has-left-icon > :global(input) {
|
|
445
|
+
padding-left: calc(var(--input-icon-size, 20px) + var(--input-icon-gap, 12px) * 2);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.input-field-wrap.has-right-icon > :global(textarea),
|
|
449
|
+
.input-field-wrap.has-right-icon > :global(input) {
|
|
450
|
+
padding-right: calc(var(--input-icon-size, 20px) + var(--input-icon-gap, 12px) * 2);
|
|
451
|
+
}
|
|
452
|
+
|
|
330
453
|
.error-message {
|
|
331
454
|
font-weight: var(--input-error-msg-text-weight, 400);
|
|
332
455
|
font-size: var(--input-error-msg-text-size, 12px);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CustomValidator, InputDataType, TextTransformer, ValidationState } from '../types';
|
|
2
2
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
3
4
|
export type InputProperties = OptionalInputProperties & InputEventProperties & MandatoryInputProperties;
|
|
4
5
|
export type MandatoryInputProperties = {
|
|
5
6
|
value: string;
|
|
@@ -32,6 +33,22 @@ export type OptionalInputProperties = {
|
|
|
32
33
|
ariaAutocomplete?: 'none' | 'inline' | 'list' | 'both';
|
|
33
34
|
ariaControls?: string | null;
|
|
34
35
|
ariaActivedescendant?: string | null;
|
|
36
|
+
/** Passive/clickable icon rendered inside the field on the leading edge (e.g. a search icon). */
|
|
37
|
+
leftIcon?: Snippet;
|
|
38
|
+
/** Passive/clickable icon rendered inside the field on the trailing edge (e.g. a clear button). */
|
|
39
|
+
rightIcon?: Snippet;
|
|
40
|
+
/** When set, the leftIcon becomes a focusable button invoking this handler. */
|
|
41
|
+
onLeftIconClick?: () => void;
|
|
42
|
+
/** When set, the rightIcon becomes a focusable button invoking this handler. */
|
|
43
|
+
onRightIconClick?: () => void;
|
|
44
|
+
/** Accessible label for the clickable leftIcon button (defaults to a generic label). */
|
|
45
|
+
leftIconLabel?: string;
|
|
46
|
+
/** Accessible label for the clickable rightIcon button (defaults to a generic label). */
|
|
47
|
+
rightIconLabel?: string;
|
|
48
|
+
/** Appends a required asterisk beside the label and sets aria-required on the field. */
|
|
49
|
+
mandatory?: boolean;
|
|
50
|
+
/** Forces the error border independent of validationPattern (server/runtime-driven errors). */
|
|
51
|
+
forceError?: boolean;
|
|
35
52
|
};
|
|
36
53
|
export type InputEventProperties = {
|
|
37
54
|
onInput?: (value: string, event: Event) => void;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { computeSankeyLayout } from '../_chart/geometry';
|
|
6
6
|
import { getColor } from '../_chart/colors';
|
|
7
7
|
import { formatNumber } from '../_chart/format';
|
|
8
|
-
import { SvelteSet } from 'svelte/reactivity';
|
|
8
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
9
9
|
|
|
10
10
|
// ── Props ──────────────────────────────────────────────────────
|
|
11
11
|
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
onnodehover,
|
|
27
27
|
onlinkhover,
|
|
28
28
|
testId,
|
|
29
|
-
classes
|
|
29
|
+
classes,
|
|
30
|
+
columnLabels,
|
|
31
|
+
nodeColorResolver
|
|
30
32
|
}: SankeyChartProperties = $props();
|
|
31
33
|
|
|
32
34
|
// ── State ──────────────────────────────────────────────────────
|
|
@@ -56,6 +58,45 @@
|
|
|
56
58
|
)
|
|
57
59
|
);
|
|
58
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Pre-computed colour map for all nodes. Keyed by node id. Computed once per layout
|
|
63
|
+
* change rather than re-running find() + indexOf() for every link on every render.
|
|
64
|
+
*
|
|
65
|
+
* Note: nodeColorResolver also controls link stroke colours (links inherit source-node
|
|
66
|
+
* colour), not just node fill colours. See Props docs for full description.
|
|
67
|
+
*/
|
|
68
|
+
let nodeColorMap = $derived.by(() => {
|
|
69
|
+
const map = new SvelteMap<string, string>();
|
|
70
|
+
for (let ni = 0; ni < layout.nodes.length; ni++) {
|
|
71
|
+
const node = layout.nodes[ni];
|
|
72
|
+
const color = node.color ?? nodeColorResolver?.(node.id, node.label ?? null) ?? getColor(ni);
|
|
73
|
+
map.set(node.id, color);
|
|
74
|
+
}
|
|
75
|
+
return map;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Column count and width — used by columnLabels rendering
|
|
79
|
+
let columnCount = $derived(
|
|
80
|
+
layout.nodes.length > 0 ? Math.max(...layout.nodes.map((n) => n.column)) + 1 : 0
|
|
81
|
+
);
|
|
82
|
+
let colWidth = $derived(
|
|
83
|
+
columnCount <= 1
|
|
84
|
+
? Math.max(0, chartWidth - MARGIN * 2)
|
|
85
|
+
: (Math.max(0, chartWidth - MARGIN * 2) - nodeWidth) / (columnCount - 1)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/** Percentage of source node's total value carried by a link (0–100, 2 dp). */
|
|
91
|
+
const computeLinkPct = (sourceId: string, linkValue: number): number => {
|
|
92
|
+
const sourceNode = layout.nodes.find((nd) => nd.id === sourceId);
|
|
93
|
+
if (!sourceNode) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const sourceTotal = sourceNode.value;
|
|
97
|
+
return sourceTotal > 0 ? Math.round((linkValue / sourceTotal) * 10000) / 100 : 0;
|
|
98
|
+
};
|
|
99
|
+
|
|
59
100
|
let connectedNodes = $derived.by(() => {
|
|
60
101
|
if (hoveredNode !== null) {
|
|
61
102
|
const connected = new SvelteSet<string>([hoveredNode]);
|
|
@@ -75,6 +116,27 @@
|
|
|
75
116
|
|
|
76
117
|
// ── Tooltip ────────────────────────────────────────────────────
|
|
77
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Pre-computed link tooltip data for the currently hovered link. Computed once and shared
|
|
121
|
+
* by both `tooltipData` and `tooltipContext` to avoid running the O(n) percentage lookup
|
|
122
|
+
* twice on every hover state change.
|
|
123
|
+
*/
|
|
124
|
+
let hoveredLinkCache = $derived.by(() => {
|
|
125
|
+
if (hoveredLink === null) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const l = links.find(
|
|
129
|
+
(lk) => lk.source === hoveredLink!.source && lk.target === hoveredLink!.target
|
|
130
|
+
);
|
|
131
|
+
if (!l) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const sourceLabelText = nodes.find((nd) => nd.id === l.source)?.label ?? l.source;
|
|
135
|
+
const targetLabelText = nodes.find((nd) => nd.id === l.target)?.label ?? l.target;
|
|
136
|
+
const pct = computeLinkPct(l.source, l.value);
|
|
137
|
+
return { link: l, sourceLabel: sourceLabelText, targetLabel: targetLabelText, percentage: pct };
|
|
138
|
+
});
|
|
139
|
+
|
|
78
140
|
let tooltipData = $derived.by(() => {
|
|
79
141
|
if (hoveredNode !== null) {
|
|
80
142
|
const n = layout.nodes.find((nd) => nd.id === hoveredNode);
|
|
@@ -92,16 +154,14 @@
|
|
|
92
154
|
]
|
|
93
155
|
};
|
|
94
156
|
}
|
|
95
|
-
if (
|
|
96
|
-
const l =
|
|
97
|
-
(lk) => lk.source === hoveredLink!.source && lk.target === hoveredLink!.target
|
|
98
|
-
);
|
|
99
|
-
if (!l) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
157
|
+
if (hoveredLinkCache !== null) {
|
|
158
|
+
const { link: l, sourceLabel, targetLabel, percentage: pct } = hoveredLinkCache;
|
|
102
159
|
return {
|
|
103
|
-
title: `${
|
|
104
|
-
items: [
|
|
160
|
+
title: `${sourceLabel} → ${targetLabel}`,
|
|
161
|
+
items: [
|
|
162
|
+
{ label: 'Flow', value: format(l.value) },
|
|
163
|
+
{ label: 'of source', value: `${pct.toFixed(2)}%` }
|
|
164
|
+
]
|
|
105
165
|
};
|
|
106
166
|
}
|
|
107
167
|
return null;
|
|
@@ -116,12 +176,15 @@
|
|
|
116
176
|
}
|
|
117
177
|
return { type: 'node', node: n, value: computed.value };
|
|
118
178
|
}
|
|
119
|
-
if (
|
|
120
|
-
const l
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
179
|
+
if (hoveredLinkCache !== null) {
|
|
180
|
+
const { link: l, sourceLabel, targetLabel, percentage: pct } = hoveredLinkCache;
|
|
181
|
+
return {
|
|
182
|
+
type: 'link',
|
|
183
|
+
link: l,
|
|
184
|
+
sourceLabel,
|
|
185
|
+
targetLabel,
|
|
186
|
+
percentage: pct
|
|
187
|
+
};
|
|
125
188
|
}
|
|
126
189
|
return null;
|
|
127
190
|
});
|
|
@@ -208,6 +271,18 @@
|
|
|
208
271
|
{:else}
|
|
209
272
|
<ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio}>
|
|
210
273
|
<g transform="translate({MARGIN}, {MARGIN})">
|
|
274
|
+
{#if columnLabels != null && columnLabels.length > 0}
|
|
275
|
+
{#each columnLabels as label, ci (ci)}
|
|
276
|
+
<text
|
|
277
|
+
class="sankey-col-label"
|
|
278
|
+
x={ci * colWidth + nodeWidth / 2}
|
|
279
|
+
y={-8}
|
|
280
|
+
text-anchor="middle"
|
|
281
|
+
dominant-baseline="auto">{label}</text
|
|
282
|
+
>
|
|
283
|
+
{/each}
|
|
284
|
+
{/if}
|
|
285
|
+
|
|
211
286
|
{#each layout.links as link, i (i)}
|
|
212
287
|
{@const highlighted = isLinkHighlighted(link.source, link.target)}
|
|
213
288
|
{@const dimmed = (hoveredNode !== null || hoveredLink !== null) && !highlighted}
|
|
@@ -217,7 +292,7 @@
|
|
|
217
292
|
class="sankey-link"
|
|
218
293
|
d={link.path}
|
|
219
294
|
fill="none"
|
|
220
|
-
stroke={link.color ??
|
|
295
|
+
stroke={link.color ?? nodeColorMap.get(link.source) ?? getColor(0)}
|
|
221
296
|
stroke-width={Math.max(1, link.width)}
|
|
222
297
|
stroke-opacity={highlighted ? 0.7 : dimmed ? 0.08 : 0.4}
|
|
223
298
|
onmouseenter={(e) => handleLinkEnter(e, link.source, link.target)}
|
|
@@ -228,7 +303,7 @@
|
|
|
228
303
|
{/each}
|
|
229
304
|
|
|
230
305
|
{#each layout.nodes as node, ni (ni)}
|
|
231
|
-
{@const color = node.
|
|
306
|
+
{@const color = nodeColorMap.get(node.id) ?? getColor(ni)}
|
|
232
307
|
{@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
|
|
233
308
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
234
309
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
@@ -296,6 +371,12 @@
|
|
|
296
371
|
pointer-events: none;
|
|
297
372
|
transition: opacity var(--chart-transition-duration, 0.2s) ease;
|
|
298
373
|
}
|
|
374
|
+
.sankey-col-label {
|
|
375
|
+
fill: var(--sankey-col-label-color, #666);
|
|
376
|
+
font-size: var(--sankey-col-label-font-size, 11px);
|
|
377
|
+
font-family: var(--chart-font-family, inherit);
|
|
378
|
+
pointer-events: none;
|
|
379
|
+
}
|
|
299
380
|
.sankey-label.node-dimmed {
|
|
300
381
|
opacity: var(--sankey-dimmed-opacity, 0.15);
|
|
301
382
|
}
|
|
@@ -10,6 +10,14 @@ export type SankeyLink = {
|
|
|
10
10
|
value: number;
|
|
11
11
|
color?: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Context passed to the `tooltipSnippet` prop on each hover event.
|
|
15
|
+
*
|
|
16
|
+
* The `'link'` branch gained three new optional fields (`sourceLabel`, `targetLabel`,
|
|
17
|
+
* `percentage`) in this release. They are always populated by the chart — the fields are
|
|
18
|
+
* typed optional so that existing consumer code that typed a variable explicitly as
|
|
19
|
+
* `{ type: 'link'; link: SankeyLink }` continues to compile without changes.
|
|
20
|
+
*/
|
|
13
21
|
export type SankeyTooltipContext = {
|
|
14
22
|
type: 'node';
|
|
15
23
|
node: SankeyNode;
|
|
@@ -17,6 +25,12 @@ export type SankeyTooltipContext = {
|
|
|
17
25
|
} | {
|
|
18
26
|
type: 'link';
|
|
19
27
|
link: SankeyLink;
|
|
28
|
+
/** Human-readable label of the source node (falls back to node id when label is undefined). Always present at runtime. */
|
|
29
|
+
sourceLabel?: string;
|
|
30
|
+
/** Human-readable label of the target node (falls back to node id when label is undefined). Always present at runtime. */
|
|
31
|
+
targetLabel?: string;
|
|
32
|
+
/** link.value as a percentage of the source node's total outgoing value (0–100, rounded to 2 dp). Always present at runtime. */
|
|
33
|
+
percentage?: number;
|
|
20
34
|
};
|
|
21
35
|
export type SankeyChartProperties = MandatorySankeyChartProperties & OptionalSankeyChartProperties & SankeyChartEventProperties;
|
|
22
36
|
export type MandatorySankeyChartProperties = {
|
|
@@ -35,6 +49,14 @@ export type OptionalSankeyChartProperties = {
|
|
|
35
49
|
empty?: Snippet;
|
|
36
50
|
testId?: string;
|
|
37
51
|
classes?: string;
|
|
52
|
+
/** Labels rendered above each column, indexed by column position (0-based). */
|
|
53
|
+
columnLabels?: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Called for each node and also for link strokes (links inherit the resolved source-node
|
|
56
|
+
* colour). Return a CSS colour string to override the default palette, or `null` to fall
|
|
57
|
+
* through to the default palette colour.
|
|
58
|
+
*/
|
|
59
|
+
nodeColorResolver?: (id: string, label: string | null) => string | null;
|
|
38
60
|
};
|
|
39
61
|
export type SankeyChartEventProperties = {
|
|
40
62
|
onnodeclick?: (event: {
|