@juspay/svelte-ui-components 2.104.0 → 2.105.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.
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
minLinkWidth = 1,
|
|
38
38
|
dataLabelOffsetX = 0,
|
|
39
39
|
disableDimOnHover = false,
|
|
40
|
-
firstColumnLabelSide = 'left'
|
|
40
|
+
firstColumnLabelSide = 'left',
|
|
41
|
+
lastColumnLabelSide = 'right',
|
|
42
|
+
marginX = 40
|
|
41
43
|
}: SankeyChartProperties = $props();
|
|
42
44
|
|
|
43
45
|
// ── State ──────────────────────────────────────────────────────
|
|
@@ -99,8 +101,11 @@
|
|
|
99
101
|
// used to run past the svg edge and clip. Reserve a capped gutter sized from the
|
|
100
102
|
// sink-node labels (sinks are what land in the final column) and lay the diagram
|
|
101
103
|
// out in the remaining width instead.
|
|
104
|
+
// With `lastColumnLabelSide: 'left'` sink labels render inside the plot over
|
|
105
|
+
// the incoming ribbons (mirroring the first column's 'right' mode), so no
|
|
106
|
+
// right gutter is reserved and the diagram runs to the right edge.
|
|
102
107
|
let lastColumnLabelGutter = $derived.by(() => {
|
|
103
|
-
if (!showLabels || nodes.length === 0 || chartWidth <= 0) {
|
|
108
|
+
if (lastColumnLabelSide === 'left' || !showLabels || nodes.length === 0 || chartWidth <= 0) {
|
|
104
109
|
return 0;
|
|
105
110
|
}
|
|
106
111
|
const sourceIds = new Set(links.map((link) => link.source));
|
|
@@ -150,7 +155,7 @@
|
|
|
150
155
|
});
|
|
151
156
|
|
|
152
157
|
let plotWidth = $derived(
|
|
153
|
-
Math.max(0, chartWidth -
|
|
158
|
+
Math.max(0, chartWidth - marginX * 2 - firstColumnLabelGutter - lastColumnLabelGutter)
|
|
154
159
|
);
|
|
155
160
|
let layout = $derived(
|
|
156
161
|
computeSankeyLayout(
|
|
@@ -202,6 +207,29 @@
|
|
|
202
207
|
return truncateToWidth(text, Math.max(0, colWidth - 6), colLabelFont);
|
|
203
208
|
};
|
|
204
209
|
|
|
210
|
+
// Column headers centre on their column's bar; at tight horizontal margins an
|
|
211
|
+
// edge header (e.g. a long last-column title) would otherwise paint past the
|
|
212
|
+
// svg boundary. Clamp each header's centre so its rendered text stays on the
|
|
213
|
+
// canvas — a no-op at the default margins.
|
|
214
|
+
const columnLabelX = (columnIndex: number, label: string): number => {
|
|
215
|
+
const center = columnIndex * colWidth + nodeWidth / 2;
|
|
216
|
+
const textWidth = measureText(truncateColumnLabel(label), colLabelFont).width;
|
|
217
|
+
const canvasLeft = -(marginX + firstColumnLabelGutter) + 2;
|
|
218
|
+
const canvasRight = plotWidth + lastColumnLabelGutter + marginX - 2;
|
|
219
|
+
const min = canvasLeft + textWidth / 2;
|
|
220
|
+
const max = canvasRight - textWidth / 2;
|
|
221
|
+
// Text wider than the whole canvas keeps its centre; the colWidth-based
|
|
222
|
+
// truncation budget makes that unreachable in practice.
|
|
223
|
+
return max < min ? center : Math.min(Math.max(center, min), max);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Which side of its bar a node's label renders on. First and last columns are
|
|
227
|
+
// configurable (`firstColumnLabelSide` / `lastColumnLabelSide`); in a
|
|
228
|
+
// single-column chart the first-column setting wins.
|
|
229
|
+
const labelOnLeft = (column: number): boolean =>
|
|
230
|
+
(column === 0 && firstColumnLabelSide === 'left') ||
|
|
231
|
+
(column === columnCount - 1 && column !== 0 && lastColumnLabelSide === 'left');
|
|
232
|
+
|
|
205
233
|
const truncateLabel = (text: string, column: number): string => {
|
|
206
234
|
// Middle columns must budget for dataLabelOffsetX too: the label starts at
|
|
207
235
|
// node.x + nodeWidth + 6 + dataLabelOffsetX, so the room before the next
|
|
@@ -213,9 +241,9 @@
|
|
|
213
241
|
// symmetric with the last column's sink-gutter budget below.
|
|
214
242
|
const available =
|
|
215
243
|
column === 0 && firstColumnLabelSide === 'left'
|
|
216
|
-
? Math.max(0, firstColumnLabelGutter +
|
|
217
|
-
: column === columnCount - 1
|
|
218
|
-
? Math.max(0, lastColumnLabelGutter +
|
|
244
|
+
? Math.max(0, firstColumnLabelGutter + marginX - 6 - dataLabelOffsetX)
|
|
245
|
+
: column === columnCount - 1 && lastColumnLabelSide === 'right'
|
|
246
|
+
? Math.max(0, lastColumnLabelGutter + marginX - 6 - dataLabelOffsetX)
|
|
219
247
|
: Math.max(0, colWidth - nodeWidth - 12 - dataLabelOffsetX);
|
|
220
248
|
// No usable room — hide the label rather than force text that would overflow;
|
|
221
249
|
// the full text is still reachable via the node's <title> on hover.
|
|
@@ -453,12 +481,12 @@
|
|
|
453
481
|
<div class="chart-empty">{@render empty()}</div>
|
|
454
482
|
{:else}
|
|
455
483
|
<ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio} {maxHeight}>
|
|
456
|
-
<g transform="translate({
|
|
484
|
+
<g transform="translate({marginX + firstColumnLabelGutter}, {MARGIN})">
|
|
457
485
|
{#if columnLabels != null && columnLabels.length > 0}
|
|
458
486
|
{#each columnLabels.slice(0, columnCount) as label, ci (ci)}
|
|
459
487
|
<text
|
|
460
488
|
class="sankey-col-label"
|
|
461
|
-
x={ci
|
|
489
|
+
x={columnLabelX(ci, label)}
|
|
462
490
|
y={-8}
|
|
463
491
|
text-anchor="middle"
|
|
464
492
|
dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
|
|
@@ -518,11 +546,11 @@
|
|
|
518
546
|
<text
|
|
519
547
|
class="sankey-label"
|
|
520
548
|
class:node-dimmed={dimmed}
|
|
521
|
-
x={node.column
|
|
549
|
+
x={labelOnLeft(node.column)
|
|
522
550
|
? node.x - 6 - dataLabelOffsetX
|
|
523
551
|
: node.x + node.width + 6 + dataLabelOffsetX}
|
|
524
552
|
y={node.y + node.height / 2}
|
|
525
|
-
text-anchor={node.column
|
|
553
|
+
text-anchor={labelOnLeft(node.column) ? 'end' : 'start'}
|
|
526
554
|
dominant-baseline="middle"
|
|
527
555
|
>{truncateLabel(
|
|
528
556
|
showValues ? `${node.label} (${format(node.value)})` : node.label,
|
|
@@ -92,6 +92,21 @@ export type OptionalSankeyChartProperties = {
|
|
|
92
92
|
* diagram gains that width.
|
|
93
93
|
*/
|
|
94
94
|
firstColumnLabelSide?: 'left' | 'right';
|
|
95
|
+
/**
|
|
96
|
+
* Which side of a last-column (sink) node its label renders on. `'right'` (default)
|
|
97
|
+
* anchors the label into a reserved right gutter outside the diagram. `'left'` renders it
|
|
98
|
+
* inside the plot — left of the bar, over the incoming ribbons (pair with the
|
|
99
|
+
* `--sankey-label-halo-*` tokens); no right gutter is reserved, so the diagram runs to the
|
|
100
|
+
* right edge. In a single-column chart `firstColumnLabelSide` wins.
|
|
101
|
+
*/
|
|
102
|
+
lastColumnLabelSide?: 'left' | 'right';
|
|
103
|
+
/**
|
|
104
|
+
* Horizontal inset in pixels between the svg edges and the diagram (plus any label
|
|
105
|
+
* gutters). Defaults to `40`, matching the fixed vertical margin. Lower it for
|
|
106
|
+
* edge-to-edge funnels when both first/last column labels render inside the plot;
|
|
107
|
+
* column headers clamp to the canvas so they never paint past the svg.
|
|
108
|
+
*/
|
|
109
|
+
marginX?: number;
|
|
95
110
|
};
|
|
96
111
|
export type SankeyChartEventProperties = {
|
|
97
112
|
onnodeclick?: (event: {
|