@juspay/svelte-ui-components 4.50.0 → 4.52.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/Animations/tokenizedFly.js +9 -2
- package/dist/AreaChart/AreaChart.svelte +41 -13
- package/dist/AreaChart/properties.d.ts +12 -0
- package/dist/AttachmentChipRow/AttachmentChipRow.svelte +7 -6
- package/dist/BarChart/BarChart.svelte +54 -6
- package/dist/BarChart/properties.d.ts +12 -0
- package/dist/Calendar/Calendar.svelte +124 -51
- package/dist/Card/Card.svelte +5 -0
- package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -0
- package/dist/CheckListItem/CheckListItem.svelte +27 -5
- package/dist/ColorPicker/ColorPicker.svelte +40 -9
- package/dist/ColorPicker/properties.d.ts +8 -0
- package/dist/Combobox/Combobox.svelte +34 -2
- package/dist/CommandMenu/CommandMenu.svelte +35 -8
- package/dist/CommandMenu/properties.d.ts +6 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +5 -5
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +41 -7
- package/dist/DualAxisBarChart/properties.d.ts +4 -0
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +104 -10
- package/dist/FileDropzoneTrigger/properties.d.ts +14 -4
- package/dist/FileInput/FileInput.svelte +38 -9
- package/dist/FileInput/context.d.ts +20 -0
- package/dist/FileInput/context.js +8 -0
- package/dist/FileInput/properties.d.ts +25 -1
- package/dist/FunnelChart/FunnelChart.svelte +20 -1
- package/dist/FunnelChart/properties.d.ts +4 -0
- package/dist/Gallery/Gallery.svelte +72 -27
- package/dist/HITL/HITL.svelte +12 -3
- package/dist/LineChart/LineChart.svelte +105 -33
- package/dist/LineChart/properties.d.ts +12 -0
- package/dist/MediaPlayer/MediaPlayer.svelte +172 -2
- package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
- package/dist/MediaPlayer/properties.d.ts +31 -0
- package/dist/Menu/Menu.svelte +6 -4
- package/dist/Modal/Modal.svelte +15 -12
- package/dist/Modal/properties.d.ts +29 -1
- package/dist/Pagination/Pagination.svelte +1 -0
- package/dist/PieChart/PieChart.svelte +51 -29
- package/dist/PieChart/properties.d.ts +4 -0
- package/dist/Pill/Pill.svelte +5 -0
- package/dist/Radio/Radio.svelte +11 -2
- package/dist/Radio/properties.d.ts +7 -2
- package/dist/SankeyChart/SankeyChart.svelte +339 -211
- package/dist/SankeyChart/properties.d.ts +4 -0
- package/dist/Scroller/Scroller.svelte +247 -4
- package/dist/Scroller/properties.d.ts +8 -0
- package/dist/Select/Select.svelte +43 -5
- package/dist/Select/properties.d.ts +17 -1
- package/dist/Sheet/Sheet.svelte +31 -4
- package/dist/Sheet/properties.d.ts +6 -0
- package/dist/SplitInput/SplitInput.svelte +17 -0
- package/dist/SplitInput/field-name.d.ts +29 -0
- package/dist/SplitInput/field-name.js +50 -0
- package/dist/SplitInput/properties.d.ts +18 -4
- package/dist/Status/Status.svelte +24 -12
- package/dist/Status/properties.d.ts +11 -0
- package/dist/Table/BuiltinCell.svelte +13 -5
- package/dist/Table/BuiltinCell.svelte.d.ts +1 -0
- package/dist/Table/Table.svelte +78 -20
- package/dist/Table/a11y.d.ts +7 -0
- package/dist/Table/a11y.js +71 -0
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +13 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +48 -9
- package/dist/Toast/Toast.svelte +40 -4
- package/dist/Toggle/Toggle.svelte +2 -0
- package/dist/Toggle/properties.d.ts +2 -0
- package/dist/_chart/Axis.svelte +10 -0
- package/dist/_chart/ChartContainer.svelte +32 -1
- package/dist/_chart/ChartTooltip.svelte +18 -3
- package/dist/_chart/LabelPlate.svelte +114 -0
- package/dist/_chart/LabelPlate.svelte.d.ts +7 -0
- package/dist/_chart/a11y.d.ts +29 -0
- package/dist/_chart/a11y.js +44 -0
- package/dist/_chart/measure.d.ts +2 -0
- package/dist/_chart/measure.js +4 -0
- package/dist/_chart/tooltipPosition.d.ts +6 -0
- package/dist/_chart/tooltipPosition.js +30 -2
- package/dist/_chart/types.d.ts +22 -0
- package/dist/_interaction/focusable.d.ts +47 -0
- package/dist/_interaction/focusable.js +139 -0
- package/dist/assets/captions-on.svg +3 -0
- package/dist/assets/captions.svg +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/theme-dark.css +6 -1
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +18 -2
- package/dist/utils.js +73 -28
- package/dist-codemod/codemod/migrate-audit.js +9 -7
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +9 -1
- package/dist-codemod/codemod/migration-assets-layout.js +61 -0
- package/dist-wc/index.js +5653 -4148
- package/package.json +4 -2
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SankeyChartProperties, SankeyTooltipContext } from './properties';
|
|
3
3
|
import ChartContainer from '../_chart/ChartContainer.svelte';
|
|
4
|
+
import LabelPlate from '../_chart/LabelPlate.svelte';
|
|
5
|
+
import { onMount } from 'svelte';
|
|
6
|
+
import { defaultChartName, resolveChartName } from '../_chart/a11y';
|
|
4
7
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
5
8
|
import { computeSankeyLayout } from '../_chart/geometry';
|
|
6
9
|
import { getColor } from '../_chart/colors';
|
|
7
10
|
import { formatNumber } from '../_chart/format';
|
|
8
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
measureText,
|
|
13
|
+
readCssVarPx,
|
|
14
|
+
invalidateTextMeasurements,
|
|
15
|
+
type FontSpec
|
|
16
|
+
} from '../_chart/measure';
|
|
9
17
|
import { pointerPositionIn, dismissOnOutsidePointerDown } from '../_chart/interactions';
|
|
10
18
|
import { truncateToWidth } from '../_chart/labels';
|
|
11
19
|
import {
|
|
@@ -35,6 +43,8 @@
|
|
|
35
43
|
onlinkclick,
|
|
36
44
|
onnodehover,
|
|
37
45
|
onlinkhover,
|
|
46
|
+
ariaLabel,
|
|
47
|
+
ariaDescription,
|
|
38
48
|
testId,
|
|
39
49
|
classes,
|
|
40
50
|
columnLabels,
|
|
@@ -81,45 +91,161 @@
|
|
|
81
91
|
|
|
82
92
|
let format = $derived(valueFormat ?? formatNumber);
|
|
83
93
|
let isEmpty = $derived(nodes.length === 0);
|
|
94
|
+
let chartName = $derived(
|
|
95
|
+
resolveChartName(
|
|
96
|
+
ariaLabel,
|
|
97
|
+
defaultChartName('sankey', {
|
|
98
|
+
seriesNames: nodes.map((node) => node.label ?? node.id)
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
let hasMarks = $derived.by(() => layout.nodes.length > 0 || layout.links.length > 0);
|
|
84
103
|
const MARGIN = 40;
|
|
104
|
+
const LABEL_NODE_GAP = 10;
|
|
105
|
+
|
|
106
|
+
// The SVG's loaded font is the source of truth. Canvas widths are useful
|
|
107
|
+
// for truncation, but a cached fallback font and an estimated line height
|
|
108
|
+
// cannot decide whether two Firefox labels collide.
|
|
109
|
+
let fontMetrics: {
|
|
110
|
+
label: FontSpec;
|
|
111
|
+
column: FontSpec;
|
|
112
|
+
height: number;
|
|
113
|
+
columnWidths: number[];
|
|
114
|
+
} | null = $state(null);
|
|
115
|
+
let fontRevision = $state(0);
|
|
116
|
+
let labelFont = $derived.by(() => {
|
|
117
|
+
void fontRevision;
|
|
118
|
+
return fontMetrics === null ? { size: 12, weight: 400 } : { ...fontMetrics.label };
|
|
119
|
+
});
|
|
120
|
+
let colLabelFont = $derived.by(() => {
|
|
121
|
+
void fontRevision;
|
|
122
|
+
return fontMetrics === null ? { size: 11, weight: 400 } : { ...fontMetrics.column };
|
|
123
|
+
});
|
|
124
|
+
// Include both plate padding and a clear gap between adjacent label plates.
|
|
125
|
+
let labelLinePx = $derived.by(() => (fontMetrics?.height ?? 16) + 8);
|
|
126
|
+
let effectiveNodePadding = $derived(
|
|
127
|
+
showLabels ? Math.max(nodePadding, labelLinePx) : nodePadding
|
|
128
|
+
);
|
|
85
129
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
onMount(() => {
|
|
131
|
+
if (containerEl === null) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const root = containerEl;
|
|
135
|
+
let mounted = true;
|
|
136
|
+
let frame = 0;
|
|
137
|
+
const observed = new SvelteSet<SVGTextElement>();
|
|
138
|
+
const resize = new ResizeObserver(schedule);
|
|
139
|
+
const mutation = new MutationObserver(schedule);
|
|
140
|
+
const fontOf = (text: SVGTextElement): FontSpec => {
|
|
141
|
+
const style = getComputedStyle(text);
|
|
142
|
+
return {
|
|
143
|
+
size: parseFloat(style.fontSize),
|
|
144
|
+
weight: style.fontWeight,
|
|
145
|
+
family: style.fontFamily
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
function measure(): void {
|
|
149
|
+
if (!mounted) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const labels = Array.from(root.querySelectorAll<SVGTextElement>('.sankey-label'));
|
|
153
|
+
const columns = Array.from(root.querySelectorAll<SVGTextElement>('.sankey-col-label'));
|
|
154
|
+
const texts = new Set([...labels, ...columns]);
|
|
155
|
+
for (const text of observed) {
|
|
156
|
+
if (!texts.has(text)) {
|
|
157
|
+
resize.unobserve(text);
|
|
158
|
+
observed.delete(text);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const text of texts) {
|
|
162
|
+
if (!observed.has(text)) {
|
|
163
|
+
resize.observe(text);
|
|
164
|
+
observed.add(text);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const first = labels[0] ?? columns[0];
|
|
168
|
+
if (!first || typeof first.getBBox !== 'function') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const label = labels[0]
|
|
172
|
+
? fontOf(labels[0])
|
|
173
|
+
: {
|
|
174
|
+
size: readCssVarPx(root, '--sankey-label-font-size', 12),
|
|
175
|
+
weight: readCssVarPx(root, '--sankey-label-font-weight', 400),
|
|
176
|
+
family: fontOf(first).family
|
|
177
|
+
};
|
|
178
|
+
const column = columns[0]
|
|
179
|
+
? fontOf(columns[0])
|
|
180
|
+
: {
|
|
181
|
+
size: readCssVarPx(root, '--sankey-col-label-font-size', 11),
|
|
182
|
+
weight: readCssVarPx(root, '--sankey-col-label-font-weight', 400),
|
|
183
|
+
family: label.family
|
|
184
|
+
};
|
|
185
|
+
const rawHeight =
|
|
186
|
+
labels.length > 0
|
|
187
|
+
? Math.max(
|
|
188
|
+
...labels.map((text) => {
|
|
189
|
+
const box = text.getBBox();
|
|
190
|
+
const rect = text.getBoundingClientRect();
|
|
191
|
+
const scaleY = Math.abs(text.getScreenCTM()?.d ?? 1) || 1;
|
|
192
|
+
// Gecko's native painted rectangle can exceed getBBox's font box.
|
|
193
|
+
// Convert it back to SVG units and keep the larger real measurement.
|
|
194
|
+
return Math.max(box.height, rect.height / scaleY);
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
: (fontMetrics?.height ?? 16);
|
|
198
|
+
// Round upward to a subpixel grid so viewport-coordinate rounding never
|
|
199
|
+
// feeds tiny height oscillations back into the responsive layout.
|
|
200
|
+
const height = Math.ceil(rawHeight * 64) / 64;
|
|
201
|
+
// Native painted bounds can exceed the canvas advance width in Gecko.
|
|
202
|
+
// Keep those real widths for edge clamps as well as the loaded font spec.
|
|
203
|
+
const columnWidths = columns.map((text) => {
|
|
204
|
+
const scaleX = Math.abs(text.getScreenCTM()?.a ?? 1) || 1;
|
|
205
|
+
const width = Math.max(text.getBBox().width, text.getBoundingClientRect().width / scaleX);
|
|
206
|
+
return Math.ceil(width * 64) / 64;
|
|
207
|
+
});
|
|
208
|
+
const next = { label, column, height, columnWidths };
|
|
209
|
+
if (JSON.stringify(next) !== JSON.stringify(fontMetrics)) {
|
|
210
|
+
invalidateTextMeasurements();
|
|
211
|
+
fontMetrics = next;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function schedule(): void {
|
|
215
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
216
|
+
cancelAnimationFrame(frame);
|
|
217
|
+
frame = requestAnimationFrame(measure);
|
|
218
|
+
} else {
|
|
219
|
+
measure();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function fontsChanged(): void {
|
|
223
|
+
if (mounted) {
|
|
224
|
+
invalidateTextMeasurements();
|
|
225
|
+
fontRevision += 1;
|
|
226
|
+
schedule();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
mutation.observe(root, {
|
|
230
|
+
subtree: true,
|
|
231
|
+
childList: true,
|
|
232
|
+
characterData: true,
|
|
233
|
+
attributes: true,
|
|
234
|
+
attributeFilter: ['class', 'style']
|
|
104
235
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
family: chartFontFamily
|
|
236
|
+
measure();
|
|
237
|
+
document.fonts?.ready.then(fontsChanged);
|
|
238
|
+
document.fonts?.addEventListener('loadingdone', fontsChanged);
|
|
239
|
+
return () => {
|
|
240
|
+
mounted = false;
|
|
241
|
+
if (typeof cancelAnimationFrame === 'function') {
|
|
242
|
+
cancelAnimationFrame(frame);
|
|
243
|
+
}
|
|
244
|
+
resize.disconnect();
|
|
245
|
+
mutation.disconnect();
|
|
246
|
+
document.fonts?.removeEventListener('loadingdone', fontsChanged);
|
|
247
|
+
};
|
|
118
248
|
});
|
|
119
|
-
// A label's rendered line box measures ≈1.33em across common font stacks;
|
|
120
|
-
// two label centres closer than this overlap visibly. Derived from the
|
|
121
|
-
// tokened size so consumers that scale labels keep honest de-collision.
|
|
122
|
-
let labelLinePx = $derived(Math.ceil(labelFont.size * 1.33));
|
|
123
249
|
|
|
124
250
|
// Final-column labels render to the RIGHT of their node; the bare 40px margin is
|
|
125
251
|
// nowhere near enough for real funnel labels ("PARTIALLY_FAILED (1,234)"), so they
|
|
@@ -143,7 +269,7 @@
|
|
|
143
269
|
const longestPx =
|
|
144
270
|
Math.max(...sinkLabels.map((label) => measureText(label, labelFont).width)) +
|
|
145
271
|
(showValues ? measureText(' (999,999)', labelFont).width : 0);
|
|
146
|
-
const wanted = longestPx +
|
|
272
|
+
const wanted = longestPx + LABEL_NODE_GAP + 4 + dataLabelOffsetX;
|
|
147
273
|
// Cap the reservation so labels can never squeeze the diagram below 3/4 width,
|
|
148
274
|
// and floor at 0 — a negative dataLabelOffsetX must not inflate the plot
|
|
149
275
|
// past the right margin.
|
|
@@ -173,7 +299,7 @@
|
|
|
173
299
|
const longestPx =
|
|
174
300
|
Math.max(...sourceLabels.map((label) => measureText(label, labelFont).width)) +
|
|
175
301
|
(showValues ? measureText(' (999,999)', labelFont).width : 0);
|
|
176
|
-
const wanted = longestPx +
|
|
302
|
+
const wanted = longestPx + LABEL_NODE_GAP + 4 + dataLabelOffsetX;
|
|
177
303
|
// Same 25% cap and 0 floor as the sink gutter so the two together can never
|
|
178
304
|
// starve the diagram, and a negative dataLabelOffsetX can't push past the margin.
|
|
179
305
|
return Math.max(0, Math.min(chartWidth * 0.25, wanted));
|
|
@@ -190,7 +316,7 @@
|
|
|
190
316
|
plotWidth,
|
|
191
317
|
plotHeight,
|
|
192
318
|
nodeWidth,
|
|
193
|
-
|
|
319
|
+
effectiveNodePadding,
|
|
194
320
|
iterations,
|
|
195
321
|
minLinkWidth
|
|
196
322
|
)
|
|
@@ -239,7 +365,10 @@
|
|
|
239
365
|
// canvas — a no-op at the default margins.
|
|
240
366
|
const columnLabelX = (columnIndex: number, label: string): number => {
|
|
241
367
|
const center = columnIndex * colWidth + nodeWidth / 2;
|
|
242
|
-
const textWidth =
|
|
368
|
+
const textWidth = Math.max(
|
|
369
|
+
measureText(truncateColumnLabel(label), colLabelFont).width,
|
|
370
|
+
fontMetrics?.columnWidths[columnIndex] ?? 0
|
|
371
|
+
);
|
|
243
372
|
const canvasLeft = -(marginX + firstColumnLabelGutter) + 2;
|
|
244
373
|
const canvasRight = plotWidth + lastColumnLabelGutter + marginX - 2;
|
|
245
374
|
const min = canvasLeft + textWidth / 2;
|
|
@@ -258,66 +387,43 @@
|
|
|
258
387
|
|
|
259
388
|
const truncateLabel = (text: string, column: number): string => {
|
|
260
389
|
// Middle columns must budget for dataLabelOffsetX too: the label starts at
|
|
261
|
-
// node.x + nodeWidth +
|
|
390
|
+
// node.x + nodeWidth + LABEL_NODE_GAP + dataLabelOffsetX, so the room before the next
|
|
262
391
|
// column's bar shrinks by the same offset. Omitting it let "fitting"
|
|
263
392
|
// labels run under the neighbouring column's node rect.
|
|
264
|
-
// First-column labels anchor `end` at node.x -
|
|
393
|
+
// First-column labels anchor `end` at node.x - LABEL_NODE_GAP - offset with node.x = 0.
|
|
265
394
|
// The diagram is shifted right by firstColumnLabelGutter, so the room to the
|
|
266
395
|
// SVG's left edge is that gutter plus the base margin, minus the inset —
|
|
267
396
|
// symmetric with the last column's sink-gutter budget below.
|
|
268
397
|
const available =
|
|
269
398
|
column === 0 && firstColumnLabelSide === 'left'
|
|
270
|
-
? Math.max(0, firstColumnLabelGutter + marginX -
|
|
399
|
+
? Math.max(0, firstColumnLabelGutter + marginX - LABEL_NODE_GAP - dataLabelOffsetX)
|
|
271
400
|
: column === columnCount - 1 && lastColumnLabelSide === 'right'
|
|
272
|
-
? Math.max(0, lastColumnLabelGutter + marginX -
|
|
273
|
-
: Math.max(0, colWidth - nodeWidth -
|
|
401
|
+
? Math.max(0, lastColumnLabelGutter + marginX - LABEL_NODE_GAP - dataLabelOffsetX)
|
|
402
|
+
: Math.max(0, colWidth - nodeWidth - LABEL_NODE_GAP * 2 - dataLabelOffsetX);
|
|
274
403
|
// No usable room — hide the label rather than force text that would overflow;
|
|
275
404
|
// the full text is still reachable via the node's <title> on hover.
|
|
276
405
|
return truncateToWidth(text, available, labelFont);
|
|
277
406
|
};
|
|
278
407
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// line — the hidden label's text stays reachable via the node's <title>.
|
|
284
|
-
let collidingLabels = $derived.by(() => {
|
|
285
|
-
const hidden = new SvelteSet<string>();
|
|
286
|
-
if (!showLabels) {
|
|
287
|
-
return hidden;
|
|
288
|
-
}
|
|
289
|
-
const byColumn = new SvelteMap<number, typeof layout.nodes>();
|
|
408
|
+
// At narrow widths the aspect-ratio height may not fit all label rows.
|
|
409
|
+
// Reserve a font-sized minimum, while honoring the caller's maximum height.
|
|
410
|
+
let maxColumnNodes = $derived.by(() => {
|
|
411
|
+
const counts = new SvelteMap<number, number>();
|
|
290
412
|
for (const node of layout.nodes) {
|
|
291
|
-
|
|
292
|
-
if (bucket) {
|
|
293
|
-
bucket.push(node);
|
|
294
|
-
} else {
|
|
295
|
-
byColumn.set(node.column, [node]);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
for (const columnNodes of byColumn.values()) {
|
|
299
|
-
const sorted = [...columnNodes].sort((a, b) => a.y + a.height / 2 - (b.y + b.height / 2));
|
|
300
|
-
let lastKept: (typeof sorted)[number] | null = null;
|
|
301
|
-
for (const node of sorted) {
|
|
302
|
-
if (lastKept === null) {
|
|
303
|
-
lastKept = node;
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
const centerGap = node.y + node.height / 2 - (lastKept.y + lastKept.height / 2);
|
|
307
|
-
if (centerGap < labelLinePx) {
|
|
308
|
-
if (node.value > lastKept.value) {
|
|
309
|
-
hidden.add(lastKept.id);
|
|
310
|
-
lastKept = node;
|
|
311
|
-
} else {
|
|
312
|
-
hidden.add(node.id);
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
lastKept = node;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
413
|
+
counts.set(node.column, (counts.get(node.column) ?? 0) + 1);
|
|
318
414
|
}
|
|
319
|
-
return
|
|
415
|
+
return Math.max(0, ...counts.values());
|
|
320
416
|
});
|
|
417
|
+
let minimumLabelHeight = $derived(
|
|
418
|
+
showLabels ? Math.min(maxHeight, MARGIN * 2 + maxColumnNodes * (labelLinePx + 4)) : 0
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// Keep a usable label slot in every column when the viewport narrows. The
|
|
422
|
+
// outer chart scrolls horizontally instead of rendering empty label text.
|
|
423
|
+
// Explicit --chart-min-width overrides retain their existing precedence.
|
|
424
|
+
let minimumDiagramWidth = $derived(
|
|
425
|
+
showLabels ? Math.max(160, columnCount * Math.max(140, labelFont.size * 12) + marginX * 2) : 160
|
|
426
|
+
);
|
|
321
427
|
|
|
322
428
|
// ── Helpers ────────────────────────────────────────────────────
|
|
323
429
|
|
|
@@ -659,147 +765,169 @@
|
|
|
659
765
|
|
|
660
766
|
<div
|
|
661
767
|
class="sankey-chart {classes ?? ''}"
|
|
662
|
-
bind:this={containerEl}
|
|
663
768
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
664
769
|
testID={typeof testId === 'string' ? testId : null}
|
|
665
770
|
>
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
771
|
+
<div
|
|
772
|
+
class="sankey-layout"
|
|
773
|
+
bind:this={containerEl}
|
|
774
|
+
style:min-width={`var(--chart-min-width, ${minimumDiagramWidth}px)`}
|
|
775
|
+
>
|
|
776
|
+
{#if isEmpty && typeof empty === 'function'}
|
|
777
|
+
<div class="chart-empty">{@render empty()}</div>
|
|
778
|
+
{:else}
|
|
779
|
+
<ChartContainer
|
|
780
|
+
bind:width={chartWidth}
|
|
781
|
+
bind:height={chartHeight}
|
|
782
|
+
{aspectRatio}
|
|
783
|
+
{maxHeight}
|
|
784
|
+
minHeight={minimumLabelHeight}
|
|
785
|
+
ariaLabel={chartName}
|
|
786
|
+
{ariaDescription}
|
|
787
|
+
interactive={hasMarks}
|
|
788
|
+
>
|
|
789
|
+
<g transform="translate({marginX + firstColumnLabelGutter}, {MARGIN})">
|
|
790
|
+
<!-- Optional backdrop panel behind the diagram (--sankey-plot-background,
|
|
672
791
|
transparent by default). Spans exactly the plot: first column's bars
|
|
673
792
|
to last column's bars, header row excluded. -->
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
793
|
+
<rect class="sankey-plot-bg" x={0} y={0} width={plotWidth} height={plotHeight} />
|
|
794
|
+
{#if columnLabels != null && columnLabels.length > 0}
|
|
795
|
+
{#each columnLabels.slice(0, columnCount) as label, ci (ci)}
|
|
796
|
+
<LabelPlate>
|
|
797
|
+
<text
|
|
798
|
+
class="sankey-col-label"
|
|
799
|
+
x={columnLabelX(ci, label)}
|
|
800
|
+
y={-8}
|
|
801
|
+
text-anchor="middle"
|
|
802
|
+
dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
|
|
803
|
+
>
|
|
804
|
+
</LabelPlate>
|
|
805
|
+
{/each}
|
|
806
|
+
{/if}
|
|
807
|
+
|
|
808
|
+
{#each layout.links as link, i (i)}
|
|
809
|
+
{@const highlighted = isLinkHighlighted(link.source, link.target)}
|
|
810
|
+
{@const dimmed =
|
|
811
|
+
!disableDimOnHover && (activeNode !== null || activeLink !== null) && !highlighted}
|
|
812
|
+
<path
|
|
813
|
+
class="sankey-link"
|
|
814
|
+
data-link-source={link.source}
|
|
815
|
+
data-link-target={link.target}
|
|
816
|
+
d={link.path}
|
|
817
|
+
fill="none"
|
|
818
|
+
stroke={link.color ?? nodeColorMap.get(link.source) ?? getColor(0)}
|
|
819
|
+
stroke-width={Math.max(minLinkWidth, link.width)}
|
|
820
|
+
stroke-opacity={highlighted ? 0.7 : dimmed ? 0.08 : 0.4}
|
|
821
|
+
tabindex="0"
|
|
822
|
+
role="button"
|
|
823
|
+
aria-label="{resolveNodeLabel(link.source)} to {resolveNodeLabel(
|
|
824
|
+
link.target
|
|
825
|
+
)}: {format(link.value)}"
|
|
826
|
+
onmouseenter={(e) => handleLinkEnter(e, link.source, link.target)}
|
|
827
|
+
onmousemove={trackMouse}
|
|
828
|
+
onmouseleave={handleLinkLeave}
|
|
829
|
+
onfocus={() => handleLinkFocus(link.source, link.target)}
|
|
830
|
+
onblur={handleLinkBlur}
|
|
831
|
+
onkeydown={(e) => handleLinkKeydown(e, link.source, link.target)}
|
|
832
|
+
onclick={() => handleLinkClick(link.source, link.target)}
|
|
833
|
+
/>
|
|
684
834
|
{/each}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
{#each layout.links as link, i (i)}
|
|
688
|
-
{@const highlighted = isLinkHighlighted(link.source, link.target)}
|
|
689
|
-
{@const dimmed =
|
|
690
|
-
!disableDimOnHover && (activeNode !== null || activeLink !== null) && !highlighted}
|
|
691
|
-
<path
|
|
692
|
-
class="sankey-link"
|
|
693
|
-
data-link-source={link.source}
|
|
694
|
-
data-link-target={link.target}
|
|
695
|
-
d={link.path}
|
|
696
|
-
fill="none"
|
|
697
|
-
stroke={link.color ?? nodeColorMap.get(link.source) ?? getColor(0)}
|
|
698
|
-
stroke-width={Math.max(minLinkWidth, link.width)}
|
|
699
|
-
stroke-opacity={highlighted ? 0.7 : dimmed ? 0.08 : 0.4}
|
|
700
|
-
tabindex="0"
|
|
701
|
-
role="button"
|
|
702
|
-
aria-label="{resolveNodeLabel(link.source)} to {resolveNodeLabel(link.target)}: {format(
|
|
703
|
-
link.value
|
|
704
|
-
)}"
|
|
705
|
-
onmouseenter={(e) => handleLinkEnter(e, link.source, link.target)}
|
|
706
|
-
onmousemove={trackMouse}
|
|
707
|
-
onmouseleave={handleLinkLeave}
|
|
708
|
-
onfocus={() => handleLinkFocus(link.source, link.target)}
|
|
709
|
-
onblur={handleLinkBlur}
|
|
710
|
-
onkeydown={(e) => handleLinkKeydown(e, link.source, link.target)}
|
|
711
|
-
onclick={() => handleLinkClick(link.source, link.target)}
|
|
712
|
-
/>
|
|
713
|
-
{/each}
|
|
714
|
-
|
|
715
|
-
{#each layout.nodes as node, ni (ni)}
|
|
716
|
-
{@const color = nodeColorMap.get(node.id) ?? getColor(ni)}
|
|
717
|
-
{@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
|
|
718
|
-
<rect
|
|
719
|
-
class="sankey-node"
|
|
720
|
-
class:node-dimmed={dimmed}
|
|
721
|
-
data-node-id={node.id}
|
|
722
|
-
x={node.x}
|
|
723
|
-
y={node.y}
|
|
724
|
-
width={node.width}
|
|
725
|
-
height={node.height}
|
|
726
|
-
rx={radius}
|
|
727
|
-
ry={radius}
|
|
728
|
-
fill={color}
|
|
729
|
-
tabindex="0"
|
|
730
|
-
role="button"
|
|
731
|
-
aria-label="{node.label}: {format(node.value)}"
|
|
732
|
-
onmouseenter={(e) => handleNodeEnter(e, node.id)}
|
|
733
|
-
onmousemove={trackMouse}
|
|
734
|
-
onmouseleave={handleNodeLeave}
|
|
735
|
-
onfocus={() => handleNodeFocus(node.id)}
|
|
736
|
-
onblur={handleNodeBlur}
|
|
737
|
-
onkeydown={(e) => handleNodeKeydown(e, node.id)}
|
|
738
|
-
onclick={() => handleNodeClick(node.id)}
|
|
739
|
-
/>
|
|
740
|
-
{/each}
|
|
741
|
-
|
|
742
|
-
<!-- Labels render in a second pass, after every node rect: within one
|
|
743
|
-
interleaved loop a label could be over-painted by a later column's
|
|
744
|
-
bar whenever the width estimate ran short. -->
|
|
745
|
-
{#if showLabels}
|
|
835
|
+
|
|
746
836
|
{#each layout.nodes as node, ni (ni)}
|
|
837
|
+
{@const color = nodeColorMap.get(node.id) ?? getColor(ni)}
|
|
747
838
|
{@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
839
|
+
<rect
|
|
840
|
+
class="sankey-node"
|
|
841
|
+
class:node-dimmed={dimmed}
|
|
842
|
+
data-node-id={node.id}
|
|
843
|
+
x={node.x}
|
|
844
|
+
y={node.y}
|
|
845
|
+
width={node.width}
|
|
846
|
+
height={node.height}
|
|
847
|
+
rx={radius}
|
|
848
|
+
ry={radius}
|
|
849
|
+
fill={color}
|
|
850
|
+
tabindex="0"
|
|
851
|
+
role="button"
|
|
852
|
+
aria-label="{node.label}: {format(node.value)}"
|
|
853
|
+
onmouseenter={(e) => handleNodeEnter(e, node.id)}
|
|
854
|
+
onmousemove={trackMouse}
|
|
855
|
+
onmouseleave={handleNodeLeave}
|
|
856
|
+
onfocus={() => handleNodeFocus(node.id)}
|
|
857
|
+
onblur={handleNodeBlur}
|
|
858
|
+
onkeydown={(e) => handleNodeKeydown(e, node.id)}
|
|
859
|
+
onclick={() => handleNodeClick(node.id)}
|
|
860
|
+
/>
|
|
765
861
|
{/each}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
862
|
+
|
|
863
|
+
<!-- Labels render in a second pass, after every node rect: within one
|
|
864
|
+
interleaved loop a label could be over-painted by a later column's
|
|
865
|
+
bar whenever the width estimate ran short. -->
|
|
866
|
+
{#if showLabels}
|
|
867
|
+
{#each layout.nodes as node, ni (ni)}
|
|
868
|
+
{@const dimmed = connectedNodes !== null && !connectedNodes.has(node.id)}
|
|
869
|
+
<LabelPlate>
|
|
870
|
+
<text
|
|
871
|
+
class="sankey-label"
|
|
872
|
+
aria-hidden="true"
|
|
873
|
+
class:node-dimmed={dimmed}
|
|
874
|
+
x={labelOnLeft(node.column)
|
|
875
|
+
? node.x - LABEL_NODE_GAP - dataLabelOffsetX
|
|
876
|
+
: node.x + node.width + LABEL_NODE_GAP + dataLabelOffsetX}
|
|
877
|
+
y={node.y + node.height / 2}
|
|
878
|
+
text-anchor={labelOnLeft(node.column) ? 'end' : 'start'}
|
|
879
|
+
dominant-baseline="middle"
|
|
880
|
+
>{truncateLabel(
|
|
881
|
+
showValues ? `${node.label} (${format(node.value)})` : node.label,
|
|
882
|
+
node.column
|
|
883
|
+
)}<title
|
|
884
|
+
>{showValues ? `${node.label} (${format(node.value)})` : node.label}</title
|
|
885
|
+
></text
|
|
886
|
+
>
|
|
887
|
+
</LabelPlate>
|
|
888
|
+
{/each}
|
|
783
889
|
{/if}
|
|
784
|
-
|
|
785
|
-
</
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
890
|
+
</g>
|
|
891
|
+
</ChartContainer>
|
|
892
|
+
|
|
893
|
+
{#if typeof tooltipSnippet === 'function'}
|
|
894
|
+
<ChartTooltip
|
|
895
|
+
data={tooltipData}
|
|
896
|
+
{mouseX}
|
|
897
|
+
{mouseY}
|
|
898
|
+
{anchor}
|
|
899
|
+
portal={tooltipPortal}
|
|
900
|
+
originEl={containerEl}
|
|
901
|
+
unstyled
|
|
902
|
+
>
|
|
903
|
+
{#snippet content()}
|
|
904
|
+
{#if tooltipContext !== null}
|
|
905
|
+
{@render tooltipSnippet(tooltipContext)}
|
|
906
|
+
{/if}
|
|
907
|
+
{/snippet}
|
|
908
|
+
</ChartTooltip>
|
|
909
|
+
{:else}
|
|
910
|
+
<ChartTooltip
|
|
911
|
+
data={tooltipData}
|
|
912
|
+
{mouseX}
|
|
913
|
+
{mouseY}
|
|
914
|
+
{anchor}
|
|
915
|
+
portal={tooltipPortal}
|
|
916
|
+
originEl={containerEl}
|
|
917
|
+
/>
|
|
918
|
+
{/if}
|
|
795
919
|
{/if}
|
|
796
|
-
|
|
920
|
+
</div>
|
|
797
921
|
</div>
|
|
798
922
|
|
|
799
923
|
<style>
|
|
800
924
|
.sankey-chart {
|
|
801
925
|
width: 100%;
|
|
802
926
|
position: relative;
|
|
927
|
+
overflow-x: auto;
|
|
928
|
+
}
|
|
929
|
+
.sankey-layout {
|
|
930
|
+
position: relative;
|
|
803
931
|
}
|
|
804
932
|
.sankey-plot-bg {
|
|
805
933
|
fill: var(--sankey-plot-background, transparent);
|
|
@@ -829,7 +957,7 @@
|
|
|
829
957
|
opacity: var(--sankey-dimmed-opacity, 0.15);
|
|
830
958
|
}
|
|
831
959
|
.sankey-label {
|
|
832
|
-
fill: var(--sankey-label-color, #333);
|
|
960
|
+
fill: var(--sankey-label-color, light-dark(#333, #e5e7eb));
|
|
833
961
|
font-size: var(--sankey-label-font-size, 12px);
|
|
834
962
|
font-weight: var(--sankey-label-font-weight, 400);
|
|
835
963
|
font-family: var(--chart-font-family, inherit);
|
|
@@ -846,14 +974,14 @@
|
|
|
846
974
|
var(--chart-transition-easing, var(--motion-easing, ease));
|
|
847
975
|
}
|
|
848
976
|
.sankey-col-label {
|
|
849
|
-
fill: var(--sankey-col-label-color, #666);
|
|
977
|
+
fill: var(--sankey-col-label-color, light-dark(#666, #9ca3af));
|
|
850
978
|
font-size: var(--sankey-col-label-font-size, 11px);
|
|
851
979
|
font-weight: var(--sankey-col-label-font-weight, 400);
|
|
852
980
|
font-family: var(--chart-font-family, inherit);
|
|
853
981
|
pointer-events: none;
|
|
854
982
|
}
|
|
855
983
|
.sankey-label.node-dimmed {
|
|
856
|
-
opacity: var(--sankey-dimmed-opacity,
|
|
984
|
+
opacity: var(--sankey-label-dimmed-opacity, 1);
|
|
857
985
|
}
|
|
858
986
|
.chart-empty {
|
|
859
987
|
padding: var(--chart-empty-padding, 32px 24px);
|