@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 - MARGIN * 2 - firstColumnLabelGutter - lastColumnLabelGutter)
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 + MARGIN - 6 - dataLabelOffsetX)
217
- : column === columnCount - 1
218
- ? Math.max(0, lastColumnLabelGutter + MARGIN - 6 - dataLabelOffsetX)
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({MARGIN + firstColumnLabelGutter}, {MARGIN})">
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 * colWidth + nodeWidth / 2}
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 === 0 && firstColumnLabelSide === 'left'
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 === 0 && firstColumnLabelSide === 'left' ? 'end' : 'start'}
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: {
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.105.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",