@juspay/svelte-ui-components 2.104.0 → 2.106.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,14 +155,15 @@
150
155
  });
151
156
 
152
157
  let plotWidth = $derived(
153
- Math.max(0, chartWidth - MARGIN * 2 - firstColumnLabelGutter - lastColumnLabelGutter)
158
+ Math.max(0, chartWidth - marginX * 2 - firstColumnLabelGutter - lastColumnLabelGutter)
154
159
  );
160
+ let plotHeight = $derived(Math.max(0, chartHeight - MARGIN * 2));
155
161
  let layout = $derived(
156
162
  computeSankeyLayout(
157
163
  nodes,
158
164
  links,
159
165
  plotWidth,
160
- Math.max(0, chartHeight - MARGIN * 2),
166
+ plotHeight,
161
167
  nodeWidth,
162
168
  nodePadding,
163
169
  iterations,
@@ -202,6 +208,29 @@
202
208
  return truncateToWidth(text, Math.max(0, colWidth - 6), colLabelFont);
203
209
  };
204
210
 
211
+ // Column headers centre on their column's bar; at tight horizontal margins an
212
+ // edge header (e.g. a long last-column title) would otherwise paint past the
213
+ // svg boundary. Clamp each header's centre so its rendered text stays on the
214
+ // canvas — a no-op at the default margins.
215
+ const columnLabelX = (columnIndex: number, label: string): number => {
216
+ const center = columnIndex * colWidth + nodeWidth / 2;
217
+ const textWidth = measureText(truncateColumnLabel(label), colLabelFont).width;
218
+ const canvasLeft = -(marginX + firstColumnLabelGutter) + 2;
219
+ const canvasRight = plotWidth + lastColumnLabelGutter + marginX - 2;
220
+ const min = canvasLeft + textWidth / 2;
221
+ const max = canvasRight - textWidth / 2;
222
+ // Text wider than the whole canvas keeps its centre; the colWidth-based
223
+ // truncation budget makes that unreachable in practice.
224
+ return max < min ? center : Math.min(Math.max(center, min), max);
225
+ };
226
+
227
+ // Which side of its bar a node's label renders on. First and last columns are
228
+ // configurable (`firstColumnLabelSide` / `lastColumnLabelSide`); in a
229
+ // single-column chart the first-column setting wins.
230
+ const labelOnLeft = (column: number): boolean =>
231
+ (column === 0 && firstColumnLabelSide === 'left') ||
232
+ (column === columnCount - 1 && column !== 0 && lastColumnLabelSide === 'left');
233
+
205
234
  const truncateLabel = (text: string, column: number): string => {
206
235
  // Middle columns must budget for dataLabelOffsetX too: the label starts at
207
236
  // node.x + nodeWidth + 6 + dataLabelOffsetX, so the room before the next
@@ -213,9 +242,9 @@
213
242
  // symmetric with the last column's sink-gutter budget below.
214
243
  const available =
215
244
  column === 0 && firstColumnLabelSide === 'left'
216
- ? Math.max(0, firstColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
217
- : column === columnCount - 1
218
- ? Math.max(0, lastColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
245
+ ? Math.max(0, firstColumnLabelGutter + marginX - 6 - dataLabelOffsetX)
246
+ : column === columnCount - 1 && lastColumnLabelSide === 'right'
247
+ ? Math.max(0, lastColumnLabelGutter + marginX - 6 - dataLabelOffsetX)
219
248
  : Math.max(0, colWidth - nodeWidth - 12 - dataLabelOffsetX);
220
249
  // No usable room — hide the label rather than force text that would overflow;
221
250
  // the full text is still reachable via the node's <title> on hover.
@@ -453,12 +482,16 @@
453
482
  <div class="chart-empty">{@render empty()}</div>
454
483
  {:else}
455
484
  <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio} {maxHeight}>
456
- <g transform="translate({MARGIN + firstColumnLabelGutter}, {MARGIN})">
485
+ <g transform="translate({marginX + firstColumnLabelGutter}, {MARGIN})">
486
+ <!-- Optional backdrop panel behind the diagram (--sankey-plot-background,
487
+ transparent by default). Spans exactly the plot: first column's bars
488
+ to last column's bars, header row excluded. -->
489
+ <rect class="sankey-plot-bg" x={0} y={0} width={plotWidth} height={plotHeight} />
457
490
  {#if columnLabels != null && columnLabels.length > 0}
458
491
  {#each columnLabels.slice(0, columnCount) as label, ci (ci)}
459
492
  <text
460
493
  class="sankey-col-label"
461
- x={ci * colWidth + nodeWidth / 2}
494
+ x={columnLabelX(ci, label)}
462
495
  y={-8}
463
496
  text-anchor="middle"
464
497
  dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
@@ -518,11 +551,11 @@
518
551
  <text
519
552
  class="sankey-label"
520
553
  class:node-dimmed={dimmed}
521
- x={node.column === 0 && firstColumnLabelSide === 'left'
554
+ x={labelOnLeft(node.column)
522
555
  ? node.x - 6 - dataLabelOffsetX
523
556
  : node.x + node.width + 6 + dataLabelOffsetX}
524
557
  y={node.y + node.height / 2}
525
- text-anchor={node.column === 0 && firstColumnLabelSide === 'left' ? 'end' : 'start'}
558
+ text-anchor={labelOnLeft(node.column) ? 'end' : 'start'}
526
559
  dominant-baseline="middle"
527
560
  >{truncateLabel(
528
561
  showValues ? `${node.label} (${format(node.value)})` : node.label,
@@ -551,6 +584,10 @@
551
584
  width: 100%;
552
585
  position: relative;
553
586
  }
587
+ .sankey-plot-bg {
588
+ fill: var(--sankey-plot-background, transparent);
589
+ pointer-events: none;
590
+ }
554
591
  .sankey-link {
555
592
  transition: stroke-opacity var(--chart-transition-duration, 0.2s) ease;
556
593
  cursor: pointer;
@@ -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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.104.0",
3
+ "version": "2.106.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",