@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.
Files changed (95) hide show
  1. package/dist/Animations/tokenizedFly.js +9 -2
  2. package/dist/AreaChart/AreaChart.svelte +41 -13
  3. package/dist/AreaChart/properties.d.ts +12 -0
  4. package/dist/AttachmentChipRow/AttachmentChipRow.svelte +7 -6
  5. package/dist/BarChart/BarChart.svelte +54 -6
  6. package/dist/BarChart/properties.d.ts +12 -0
  7. package/dist/Calendar/Calendar.svelte +124 -51
  8. package/dist/Card/Card.svelte +5 -0
  9. package/dist/ChatSuggestions/ChatSuggestions.svelte +1 -0
  10. package/dist/CheckListItem/CheckListItem.svelte +27 -5
  11. package/dist/ColorPicker/ColorPicker.svelte +40 -9
  12. package/dist/ColorPicker/properties.d.ts +8 -0
  13. package/dist/Combobox/Combobox.svelte +34 -2
  14. package/dist/CommandMenu/CommandMenu.svelte +35 -8
  15. package/dist/CommandMenu/properties.d.ts +6 -0
  16. package/dist/DateRangePicker/DateRangePicker.svelte +5 -5
  17. package/dist/DualAxisBarChart/DualAxisBarChart.svelte +41 -7
  18. package/dist/DualAxisBarChart/properties.d.ts +4 -0
  19. package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +104 -10
  20. package/dist/FileDropzoneTrigger/properties.d.ts +14 -4
  21. package/dist/FileInput/FileInput.svelte +38 -9
  22. package/dist/FileInput/context.d.ts +20 -0
  23. package/dist/FileInput/context.js +8 -0
  24. package/dist/FileInput/properties.d.ts +25 -1
  25. package/dist/FunnelChart/FunnelChart.svelte +20 -1
  26. package/dist/FunnelChart/properties.d.ts +4 -0
  27. package/dist/Gallery/Gallery.svelte +72 -27
  28. package/dist/HITL/HITL.svelte +12 -3
  29. package/dist/LineChart/LineChart.svelte +105 -33
  30. package/dist/LineChart/properties.d.ts +12 -0
  31. package/dist/MediaPlayer/MediaPlayer.svelte +172 -2
  32. package/dist/MediaPlayer/MediaPlayer.svelte.d.ts +1 -1
  33. package/dist/MediaPlayer/properties.d.ts +31 -0
  34. package/dist/Menu/Menu.svelte +6 -4
  35. package/dist/Modal/Modal.svelte +15 -12
  36. package/dist/Modal/properties.d.ts +29 -1
  37. package/dist/Pagination/Pagination.svelte +1 -0
  38. package/dist/PieChart/PieChart.svelte +51 -29
  39. package/dist/PieChart/properties.d.ts +4 -0
  40. package/dist/Pill/Pill.svelte +5 -0
  41. package/dist/Radio/Radio.svelte +11 -2
  42. package/dist/Radio/properties.d.ts +7 -2
  43. package/dist/SankeyChart/SankeyChart.svelte +339 -211
  44. package/dist/SankeyChart/properties.d.ts +4 -0
  45. package/dist/Scroller/Scroller.svelte +247 -4
  46. package/dist/Scroller/properties.d.ts +8 -0
  47. package/dist/Select/Select.svelte +43 -5
  48. package/dist/Select/properties.d.ts +17 -1
  49. package/dist/Sheet/Sheet.svelte +31 -4
  50. package/dist/Sheet/properties.d.ts +6 -0
  51. package/dist/SplitInput/SplitInput.svelte +17 -0
  52. package/dist/SplitInput/field-name.d.ts +29 -0
  53. package/dist/SplitInput/field-name.js +50 -0
  54. package/dist/SplitInput/properties.d.ts +18 -4
  55. package/dist/Status/Status.svelte +24 -12
  56. package/dist/Status/properties.d.ts +11 -0
  57. package/dist/Table/BuiltinCell.svelte +13 -5
  58. package/dist/Table/BuiltinCell.svelte.d.ts +1 -0
  59. package/dist/Table/Table.svelte +78 -20
  60. package/dist/Table/a11y.d.ts +7 -0
  61. package/dist/Table/a11y.js +71 -0
  62. package/dist/Table/cellData.js +3 -0
  63. package/dist/Table/properties.d.ts +13 -5
  64. package/dist/ThemeSwitcher/ThemeSwitcher.svelte +4 -0
  65. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +48 -9
  66. package/dist/Toast/Toast.svelte +40 -4
  67. package/dist/Toggle/Toggle.svelte +2 -0
  68. package/dist/Toggle/properties.d.ts +2 -0
  69. package/dist/_chart/Axis.svelte +10 -0
  70. package/dist/_chart/ChartContainer.svelte +32 -1
  71. package/dist/_chart/ChartTooltip.svelte +18 -3
  72. package/dist/_chart/LabelPlate.svelte +114 -0
  73. package/dist/_chart/LabelPlate.svelte.d.ts +7 -0
  74. package/dist/_chart/a11y.d.ts +29 -0
  75. package/dist/_chart/a11y.js +44 -0
  76. package/dist/_chart/measure.d.ts +2 -0
  77. package/dist/_chart/measure.js +4 -0
  78. package/dist/_chart/tooltipPosition.d.ts +6 -0
  79. package/dist/_chart/tooltipPosition.js +30 -2
  80. package/dist/_chart/types.d.ts +22 -0
  81. package/dist/_interaction/focusable.d.ts +47 -0
  82. package/dist/_interaction/focusable.js +139 -0
  83. package/dist/assets/captions-on.svg +3 -0
  84. package/dist/assets/captions.svg +5 -0
  85. package/dist/index.d.ts +1 -1
  86. package/dist/index.js +1 -1
  87. package/dist/styles/theme-dark.css +6 -1
  88. package/dist/types.d.ts +2 -0
  89. package/dist/utils.d.ts +18 -2
  90. package/dist/utils.js +73 -28
  91. package/dist-codemod/codemod/migrate-audit.js +9 -7
  92. package/dist-codemod/codemod/migration-assets/legacy-palette.css +9 -1
  93. package/dist-codemod/codemod/migration-assets-layout.js +61 -0
  94. package/dist-wc/index.js +5653 -4148
  95. 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 { measureText, readCssVarPx } from '../_chart/measure';
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
- // Real text measurement via the shared canvas-backed helper (exact on the
87
- // client, 0.6em/char heuristic under SSR/tests). Character estimates used
88
- // to both over-reserve the right label gutter (dead canvas) and under-budget
89
- // uppercase-heavy labels (text sliding under the next column's bars).
90
- // Weight and family are part of the spec: a 700-weight label is measurably
91
- // wider than a 400 one, and the app's rendered font rarely matches the
92
- // measurement default — budgets must be computed at the rendered style or
93
- // borderline labels truncate (or overflow) for no visible reason.
94
- // Family only enters the spec once the document's fonts have loaded: measuring
95
- // a not-yet-loaded webfont silently measures its fallback (usually wider) and
96
- // the width cache would pin that stale value under the loaded font's key.
97
- // Until then the measurement default applies — same behaviour as before.
98
- // One-shot promise subscription at init (browser only) — nothing to tear
99
- // down, and the $state write re-derives every measurement consumer.
100
- let fontsLoaded = $state(typeof document !== 'undefined' && document.fonts?.status === 'loaded');
101
- if (typeof document !== 'undefined' && document.fonts?.status !== 'loaded') {
102
- document.fonts?.ready.then(() => {
103
- fontsLoaded = true;
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
- let chartFontFamily = $derived(
107
- fontsLoaded && containerEl ? getComputedStyle(containerEl).fontFamily : null
108
- );
109
- let labelFont = $derived({
110
- size: containerEl ? readCssVarPx(containerEl, '--sankey-label-font-size', 12) : 12,
111
- weight: containerEl ? readCssVarPx(containerEl, '--sankey-label-font-weight', 400) : 400,
112
- family: chartFontFamily
113
- });
114
- let colLabelFont = $derived({
115
- size: containerEl ? readCssVarPx(containerEl, '--sankey-col-label-font-size', 11) : 11,
116
- weight: containerEl ? readCssVarPx(containerEl, '--sankey-col-label-font-weight', 400) : 400,
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 + 10 + dataLabelOffsetX;
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 + 10 + dataLabelOffsetX;
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
- nodePadding,
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 = measureText(truncateColumnLabel(label), colLabelFont).width;
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 + 6 + dataLabelOffsetX, so the room before the next
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 - 6 - offset with node.x = 0.
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 - 6 - dataLabelOffsetX)
399
+ ? Math.max(0, firstColumnLabelGutter + marginX - LABEL_NODE_GAP - dataLabelOffsetX)
271
400
  : column === columnCount - 1 && lastColumnLabelSide === 'right'
272
- ? Math.max(0, lastColumnLabelGutter + marginX - 6 - dataLabelOffsetX)
273
- : Math.max(0, colWidth - nodeWidth - 12 - dataLabelOffsetX);
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
- // Vertical label de-collision: labels sit at each node's centre-y, so two
280
- // small stacked nodes in a crowded column render their 12px labels on top of
281
- // each other. Per column, walk labels top-to-bottom and drop the label of
282
- // the smaller-value node whenever two centres come closer than one label
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
- const bucket = byColumn.get(node.column);
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 hidden;
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
- {#if isEmpty && typeof empty === 'function'}
667
- <div class="chart-empty">{@render empty()}</div>
668
- {:else}
669
- <ChartContainer bind:width={chartWidth} bind:height={chartHeight} {aspectRatio} {maxHeight}>
670
- <g transform="translate({marginX + firstColumnLabelGutter}, {MARGIN})">
671
- <!-- Optional backdrop panel behind the diagram (--sankey-plot-background,
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
- <rect class="sankey-plot-bg" x={0} y={0} width={plotWidth} height={plotHeight} />
675
- {#if columnLabels != null && columnLabels.length > 0}
676
- {#each columnLabels.slice(0, columnCount) as label, ci (ci)}
677
- <text
678
- class="sankey-col-label"
679
- x={columnLabelX(ci, label)}
680
- y={-8}
681
- text-anchor="middle"
682
- dominant-baseline="auto">{truncateColumnLabel(label)}<title>{label}</title></text
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
- {/if}
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
- {#if !collidingLabels.has(node.id)}
749
- <text
750
- class="sankey-label"
751
- class:node-dimmed={dimmed}
752
- x={labelOnLeft(node.column)
753
- ? node.x - 6 - dataLabelOffsetX
754
- : node.x + node.width + 6 + dataLabelOffsetX}
755
- y={node.y + node.height / 2}
756
- text-anchor={labelOnLeft(node.column) ? 'end' : 'start'}
757
- dominant-baseline="middle"
758
- >{truncateLabel(
759
- showValues ? `${node.label} (${format(node.value)})` : node.label,
760
- node.column
761
- )}<title>{showValues ? `${node.label} (${format(node.value)})` : node.label}</title
762
- ></text
763
- >
764
- {/if}
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
- {/if}
767
- </g>
768
- </ChartContainer>
769
-
770
- {#if typeof tooltipSnippet === 'function'}
771
- <ChartTooltip
772
- data={tooltipData}
773
- {mouseX}
774
- {mouseY}
775
- {anchor}
776
- portal={tooltipPortal}
777
- originEl={containerEl}
778
- unstyled
779
- >
780
- {#snippet content()}
781
- {#if tooltipContext !== null}
782
- {@render tooltipSnippet(tooltipContext)}
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
- {/snippet}
785
- </ChartTooltip>
786
- {:else}
787
- <ChartTooltip
788
- data={tooltipData}
789
- {mouseX}
790
- {mouseY}
791
- {anchor}
792
- portal={tooltipPortal}
793
- originEl={containerEl}
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
- {/if}
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, 0.15);
984
+ opacity: var(--sankey-label-dimmed-opacity, 1);
857
985
  }
858
986
  .chart-empty {
859
987
  padding: var(--chart-empty-padding, 32px 24px);