@juspay/svelte-ui-components 2.106.2 → 2.107.1

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.
@@ -78,6 +78,7 @@
78
78
  stackNormalize = false,
79
79
  scrollable = false,
80
80
  minBandWidth = 48,
81
+ marginX,
81
82
  tooltipSnippet,
82
83
  empty,
83
84
  renderOverlay,
@@ -309,7 +310,19 @@
309
310
  base: { top: 20, left: showYAxis ? 50 : 28, right: 28, bottom: showXAxis ? 40 : 8 }
310
311
  });
311
312
  });
312
- let dims = $derived(layout);
313
+ let dims = $derived.by(() => {
314
+ if (marginX == null) {
315
+ return layout;
316
+ }
317
+ // Fixed horizontal inset: overrides the auto layout's tick-label padding
318
+ // so the plot (and its edge bars) runs to the requested inset.
319
+ const inset = Math.max(0, marginX);
320
+ return {
321
+ ...layout,
322
+ margin: { ...layout.margin, left: inset, right: inset },
323
+ innerWidth: Math.max(0, chartWidth - inset * 2)
324
+ };
325
+ });
313
326
 
314
327
  let catScale = $derived(
315
328
  createBandScale(labels, isVertical ? [0, dims.innerWidth] : [0, dims.innerHeight], barPadding)
@@ -610,6 +623,38 @@
610
623
  }
611
624
  }
612
625
 
626
+ // Grouped/single bars round only their value end (design-system bar spec):
627
+ // a vertical bar rounds its top (bottom when the value is negative), a
628
+ // horizontal bar its right (left when negative). The old all-corner rect
629
+ // rounding let the backdrop/track behind a bar peek through the notches at
630
+ // its baseline corners. Floating [low, high] range bars round both ends.
631
+ function valueEndBarPath(bar: BarRect): string {
632
+ if (barRadius <= 0) {
633
+ return roundedRectPath(bar.x, bar.y, bar.width, bar.height, 0, 0, 0, 0);
634
+ }
635
+ if (bar.isFloating) {
636
+ return roundedRectPath(
637
+ bar.x,
638
+ bar.y,
639
+ bar.width,
640
+ bar.height,
641
+ barRadius,
642
+ barRadius,
643
+ barRadius,
644
+ barRadius
645
+ );
646
+ }
647
+ const isNegative = (bar.dataPoint.value ?? 0) < 0;
648
+ if (isVertical) {
649
+ return isNegative
650
+ ? roundedRectPath(bar.x, bar.y, bar.width, bar.height, 0, 0, barRadius, barRadius)
651
+ : roundedRectPath(bar.x, bar.y, bar.width, bar.height, barRadius, barRadius, 0, 0);
652
+ }
653
+ return isNegative
654
+ ? roundedRectPath(bar.x, bar.y, bar.width, bar.height, barRadius, 0, 0, barRadius)
655
+ : roundedRectPath(bar.x, bar.y, bar.width, bar.height, 0, barRadius, barRadius, 0);
656
+ }
657
+
613
658
  // ── Fill attribute helper ──────────────────────────────────────
614
659
 
615
660
  function barFillAttr(bar: BarRect): string {
@@ -918,7 +963,7 @@
918
963
  onclick={() => handleClick(bar)}
919
964
  />
920
965
  {:else}
921
- <rect
966
+ <path
922
967
  class="bar"
923
968
  class:hovered={hovered?.si === bar.si && hovered?.pi === bar.pi}
924
969
  class:highlighted={effectiveHighlightedIndex !== null &&
@@ -927,12 +972,7 @@
927
972
  (hovered.si !== bar.si || hovered.pi !== bar.pi)) ||
928
973
  (effectiveHighlightedIndex !== null &&
929
974
  effectiveHighlightedIndex !== bar.pi)}
930
- x={bar.x}
931
- y={bar.y}
932
- width={bar.width}
933
- height={bar.height}
934
- rx={barRadius}
935
- ry={barRadius}
975
+ d={valueEndBarPath(bar)}
936
976
  fill={barFillAttr(bar)}
937
977
  data-pw={`bar-${i}`}
938
978
  tabindex="0"
@@ -161,6 +161,14 @@ export type OptionalBarChartProperties = {
161
161
  * is `false`. Default is `48`.
162
162
  */
163
163
  minBandWidth?: number;
164
+ /**
165
+ * Fixed horizontal inset (px) between the svg edges and the plot, overriding
166
+ * the auto-computed left/right margins. Use for edge-to-edge funnels where
167
+ * the auto layout's tick-label padding leaves dead space beside the first and
168
+ * last bars. Category tick labels near the edges may clip if they are wider
169
+ * than their band, so pair small values with short labels.
170
+ */
171
+ marginX?: number;
164
172
  tooltipSnippet?: Snippet<[BarChartDataPoint, number]>;
165
173
  empty?: Snippet;
166
174
  /**
@@ -145,30 +145,49 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
145
145
  }
146
146
  const colWidth = maxCol === 0 ? 0 : (width - nodeWidth) / maxCol;
147
147
  const columnPadding = new Map();
148
- // Initialize y positions
148
+ // Global px-per-value scale (d3-sankey's `ky`): the tightest column — least
149
+ // height left after node gaps — sets one scale for the whole diagram, so a
150
+ // given value renders the same height in every column. The previous layout
151
+ // stretched every column to fill the full plot height, which gave each
152
+ // column its own scale; link widths (sized at the source column's scale)
153
+ // then overflowed target nodes laid out at a smaller scale, and the ribbon
154
+ // stacks spilled below the bottom node row.
155
+ let pxPerValue = Number.POSITIVE_INFINITY;
156
+ for (const ids of columnGroups.values()) {
157
+ const totalValue = ids.reduce((s, id) => s + (nodeValues.get(id) ?? 0), 0);
158
+ const availableHeight = height - (ids.length - 1) * nodePadding;
159
+ if (totalValue > 0 && availableHeight > 0) {
160
+ pxPerValue = Math.min(pxPerValue, availableHeight / totalValue);
161
+ }
162
+ }
163
+ if (!Number.isFinite(pxPerValue)) {
164
+ // All-zero data: nothing carries volume, every bar collapses to the minimum.
165
+ pxPerValue = 0;
166
+ }
167
+ const linkRenderWidth = (value) => Math.max(minLinkWidth, value * pxPerValue);
168
+ // Initialize y positions. A node must be at least as tall as its thicker
169
+ // side's rendered link stack: every ribbon is clamped to minLinkWidth, so a
170
+ // node fanning into many near-zero links would otherwise be shorter than the
171
+ // inflated stack attached to it.
149
172
  const nodeY = new Map();
150
173
  const nodeH = new Map();
151
174
  for (const [col, ids] of columnGroups) {
152
- const totalValue = ids.reduce((s, id) => s + (nodeValues.get(id) ?? 0), 0);
153
175
  const gapCount = ids.length - 1;
154
- const availableHeight = height - gapCount * nodePadding;
155
176
  const renderedHeights = ids.map((id) => {
156
- const val = nodeValues.get(id) ?? 0;
157
- const h = totalValue > 0 ? (val / totalValue) * availableHeight : availableHeight / ids.length;
158
- return Math.max(minLinkWidth, h);
177
+ const outStack = (outgoing.get(id) ?? []).reduce((s, link) => s + linkRenderWidth(link.value), 0);
178
+ const inStack = (incoming.get(id) ?? []).reduce((s, link) => s + linkRenderWidth(link.value), 0);
179
+ return Math.max(minLinkWidth, (nodeValues.get(id) ?? 0) * pxPerValue, outStack, inStack);
159
180
  });
160
181
  const sumRendered = renderedHeights.reduce((s, h) => s + h, 0);
161
182
  const paddingBudget = gapCount > 0 ? (height - sumRendered) / gapCount : 0;
162
183
  const effectivePadding = Math.max(0, Math.min(nodePadding, paddingBudget));
163
- const scale = sumRendered > height && sumRendered > 0 ? height / sumRendered : 1;
164
184
  columnPadding.set(col, effectivePadding);
165
185
  let y = 0;
166
186
  for (let index = 0; index < ids.length; index++) {
167
187
  const id = ids[index];
168
- const renderedH = renderedHeights[index] * scale;
169
188
  nodeY.set(id, y);
170
- nodeH.set(id, renderedH);
171
- y += renderedH + effectivePadding;
189
+ nodeH.set(id, renderedHeights[index]);
190
+ y += renderedHeights[index] + effectivePadding;
172
191
  }
173
192
  }
174
193
  // Iterative relaxation (upstream pass)
@@ -245,11 +264,11 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
245
264
  }));
246
265
  const nodeById = new Map(computedNodes.map((n) => [n.id, n]));
247
266
  const linkKey = (l) => `${l.source}${l.target}`;
267
+ // Link widths use the same global scale as node heights, so each node's link
268
+ // stack fills its bar exactly and never runs past its bottom edge.
248
269
  const linkWidths = new Map();
249
270
  for (const l of links) {
250
- const sVal = nodeValues.get(l.source) ?? 1;
251
- const sourceH = nodeH.get(l.source) ?? 0;
252
- linkWidths.set(linkKey(l), Math.max(minLinkWidth, (l.value / Math.max(sVal, 1)) * sourceH));
271
+ linkWidths.set(linkKey(l), linkRenderWidth(l.value));
253
272
  }
254
273
  const linkSy = new Map();
255
274
  const linkTy = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.106.2",
3
+ "version": "2.107.1",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",