@juspay/svelte-ui-components 2.81.1 → 2.81.3
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.
- package/dist/BarChart/BarChart.svelte +57 -12
- package/dist/_chart/geometry.d.ts +20 -0
- package/dist/_chart/geometry.js +107 -24
- package/package.json +1 -1
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
14
14
|
import Legend from '../_chart/Legend.svelte';
|
|
15
15
|
import { createBandScale, createLinearScale, niceLinearDomain } from '../_chart/scales';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
computeChartDimensions,
|
|
18
|
+
computeHorizontalCategoryGutter,
|
|
19
|
+
measureTextWidth
|
|
20
|
+
} from '../_chart/geometry';
|
|
17
21
|
import { getColor } from '../_chart/colors';
|
|
18
22
|
import { formatNumber } from '../_chart/format';
|
|
19
23
|
import { roundedRectPath } from '../_chart/paths';
|
|
@@ -110,17 +114,6 @@
|
|
|
110
114
|
|
|
111
115
|
let format = $derived(valueFormat ?? formatNumber);
|
|
112
116
|
let isVertical = $derived(orientation === 'vertical');
|
|
113
|
-
// When an axis is hidden its gutter (Y = 50px for tick labels, X = 40px) is dead
|
|
114
|
-
// space that squeezes the plot into the centre. Collapse the tick-label gutter but
|
|
115
|
-
// keep a symmetric breathing-room inset so the edge bars (and their value labels)
|
|
116
|
-
// don't sit flush against the container edges.
|
|
117
|
-
let dims = $derived(
|
|
118
|
-
computeChartDimensions(chartWidth, chartHeight, {
|
|
119
|
-
left: showYAxis ? 50 : 28,
|
|
120
|
-
right: 28,
|
|
121
|
-
bottom: showXAxis ? 40 : 8
|
|
122
|
-
})
|
|
123
|
-
);
|
|
124
117
|
|
|
125
118
|
let isMulti = $derived(Array.isArray(series) && series.length > 0);
|
|
126
119
|
|
|
@@ -197,6 +190,58 @@
|
|
|
197
190
|
return first.map((d) => d.label);
|
|
198
191
|
});
|
|
199
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Widest category label in the axis tick font, for the horizontal-orientation
|
|
195
|
+
* gutter below. Horizontal charts put category text (not short numeric ticks)
|
|
196
|
+
* on the Y axis, so the gutter must fit real words like "Submitted Address" —
|
|
197
|
+
* a fixed gutter clips them. Resolved from the mounted container so CSS-var
|
|
198
|
+
* theming (--chart-axis-font-size / --chart-axis-font-family) is honoured.
|
|
199
|
+
* Null when unmeasurable (SSR, no canvas) — the gutter then keeps its legacy
|
|
200
|
+
* fixed width.
|
|
201
|
+
*/
|
|
202
|
+
let widestCategoryLabelWidth = $derived.by(() => {
|
|
203
|
+
if (isVertical || !showYAxis || labels.length === 0 || containerEl === null) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const containerStyle = getComputedStyle(containerEl);
|
|
207
|
+
const axisFontSize = containerStyle.getPropertyValue('--chart-axis-font-size').trim() || '11px';
|
|
208
|
+
const axisFontFamilyToken = containerStyle.getPropertyValue('--chart-axis-font-family').trim();
|
|
209
|
+
const axisFontFamily =
|
|
210
|
+
axisFontFamilyToken === '' || axisFontFamilyToken === 'inherit'
|
|
211
|
+
? containerStyle.fontFamily || 'sans-serif'
|
|
212
|
+
: axisFontFamilyToken;
|
|
213
|
+
const axisFont = `${axisFontSize} ${axisFontFamily}`;
|
|
214
|
+
let widest: number | null = null;
|
|
215
|
+
for (const label of labels) {
|
|
216
|
+
const labelWidth = measureTextWidth(label, axisFont);
|
|
217
|
+
if (labelWidth === null) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (widest === null || labelWidth > widest) {
|
|
221
|
+
widest = labelWidth;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return widest;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// When an axis is hidden its gutter (Y = 50px for tick labels, X = 40px) is dead
|
|
228
|
+
// space that squeezes the plot into the centre. Collapse the tick-label gutter but
|
|
229
|
+
// keep a symmetric breathing-room inset so the edge bars (and their value labels)
|
|
230
|
+
// don't sit flush against the container edges. In horizontal orientation the
|
|
231
|
+
// visible Y axis carries category text, so its gutter grows to fit the widest
|
|
232
|
+
// label (never shrinking below the legacy 50px, capped at 45% of the width).
|
|
233
|
+
let dims = $derived(
|
|
234
|
+
computeChartDimensions(chartWidth, chartHeight, {
|
|
235
|
+
left: showYAxis
|
|
236
|
+
? isVertical
|
|
237
|
+
? 50
|
|
238
|
+
: computeHorizontalCategoryGutter(widestCategoryLabelWidth, chartWidth)
|
|
239
|
+
: 28,
|
|
240
|
+
right: 28,
|
|
241
|
+
bottom: showXAxis ? 40 : 8
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
200
245
|
// ── Effective highlighted index: merge declarative + imperative ─
|
|
201
246
|
|
|
202
247
|
/**
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import type { Margin, ChartDimensions, PieSliceLayout, ComputedSankeyNode, ComputedSankeyLink, StackedPoint } from './types';
|
|
2
2
|
export declare function computeChartDimensions(width: number, height: number, margin?: Partial<Margin>): ChartDimensions;
|
|
3
|
+
/**
|
|
4
|
+
* Measures rendered text width via a shared offscreen canvas context.
|
|
5
|
+
* Returns null when measurement is unavailable (SSR, or the environment
|
|
6
|
+
* provides no working 2D canvas — e.g. jsdom) so callers can fall back to
|
|
7
|
+
* a fixed layout instead of acting on a bogus 0.
|
|
8
|
+
*/
|
|
9
|
+
export declare function measureTextWidth(text: string, font: string): number | null;
|
|
10
|
+
/**
|
|
11
|
+
* Left margin for a horizontal bar chart's category axis, sized to fit the
|
|
12
|
+
* widest category label. Category tick labels render right-aligned 10px left
|
|
13
|
+
* of the axis line (tick mark 6px + 4px gap), so any label wider than
|
|
14
|
+
* `margin.left - 10` bleeds out of the SVG and gets clipped by the page.
|
|
15
|
+
*
|
|
16
|
+
* - Never shrinks below `fallback` (the legacy fixed gutter), so charts whose
|
|
17
|
+
* labels already fit keep their exact current layout.
|
|
18
|
+
* - Caps at 45% of the chart width so one pathological label cannot crush the
|
|
19
|
+
* plot area; past the cap the label bleeds as before, but the plot survives.
|
|
20
|
+
* - `widestLabelWidth === null` (SSR / unmeasurable) keeps the legacy gutter.
|
|
21
|
+
*/
|
|
22
|
+
export declare function computeHorizontalCategoryGutter(widestLabelWidth: number | null, chartWidth: number, fallback?: number): number;
|
|
3
23
|
export declare function computePieLayout(data: Array<{
|
|
4
24
|
label: string;
|
|
5
25
|
value: number;
|
package/dist/_chart/geometry.js
CHANGED
|
@@ -13,6 +13,51 @@ export function computeChartDimensions(width, height, margin = {}) {
|
|
|
13
13
|
innerHeight: Math.max(0, height - m.top - m.bottom)
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
// ── Text measurement ────────────────────────────────────────────
|
|
17
|
+
let textMeasurementContext = null;
|
|
18
|
+
/**
|
|
19
|
+
* Measures rendered text width via a shared offscreen canvas context.
|
|
20
|
+
* Returns null when measurement is unavailable (SSR, or the environment
|
|
21
|
+
* provides no working 2D canvas — e.g. jsdom) so callers can fall back to
|
|
22
|
+
* a fixed layout instead of acting on a bogus 0.
|
|
23
|
+
*/
|
|
24
|
+
export function measureTextWidth(text, font) {
|
|
25
|
+
if (typeof document === 'undefined') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (textMeasurementContext === null) {
|
|
29
|
+
textMeasurementContext = document.createElement('canvas').getContext('2d');
|
|
30
|
+
}
|
|
31
|
+
if (textMeasurementContext === null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
textMeasurementContext.font = font;
|
|
35
|
+
const width = textMeasurementContext.measureText(text).width;
|
|
36
|
+
// jsdom's canvas stub reports 0 for any text; treat that as "cannot measure".
|
|
37
|
+
return width > 0 ? width : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Left margin for a horizontal bar chart's category axis, sized to fit the
|
|
41
|
+
* widest category label. Category tick labels render right-aligned 10px left
|
|
42
|
+
* of the axis line (tick mark 6px + 4px gap), so any label wider than
|
|
43
|
+
* `margin.left - 10` bleeds out of the SVG and gets clipped by the page.
|
|
44
|
+
*
|
|
45
|
+
* - Never shrinks below `fallback` (the legacy fixed gutter), so charts whose
|
|
46
|
+
* labels already fit keep their exact current layout.
|
|
47
|
+
* - Caps at 45% of the chart width so one pathological label cannot crush the
|
|
48
|
+
* plot area; past the cap the label bleeds as before, but the plot survives.
|
|
49
|
+
* - `widestLabelWidth === null` (SSR / unmeasurable) keeps the legacy gutter.
|
|
50
|
+
*/
|
|
51
|
+
export function computeHorizontalCategoryGutter(widestLabelWidth, chartWidth, fallback = 50) {
|
|
52
|
+
if (widestLabelWidth === null) {
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
const tickLabelInset = 10;
|
|
56
|
+
const breathingPad = 4;
|
|
57
|
+
const cap = Math.max(fallback, chartWidth * 0.45);
|
|
58
|
+
const fitted = widestLabelWidth + tickLabelInset + breathingPad;
|
|
59
|
+
return Math.round(Math.min(Math.max(fallback, fitted), cap));
|
|
60
|
+
}
|
|
16
61
|
// ── Pie layout ──────────────────────────────────────────────────
|
|
17
62
|
export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0) {
|
|
18
63
|
const total = data.reduce((sum, d) => sum + Math.max(0, d.value), 0);
|
|
@@ -94,25 +139,37 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
94
139
|
columnGroups.get(col).push(n.id);
|
|
95
140
|
}
|
|
96
141
|
const colWidth = maxCol === 0 ? 0 : (width - nodeWidth) / maxCol;
|
|
142
|
+
const columnPadding = new Map();
|
|
97
143
|
// Initialize y positions
|
|
98
144
|
const nodeY = new Map();
|
|
99
145
|
const nodeH = new Map();
|
|
100
|
-
for (const [, ids] of columnGroups) {
|
|
146
|
+
for (const [col, ids] of columnGroups) {
|
|
101
147
|
const totalValue = ids.reduce((s, id) => s + (nodeValues.get(id) ?? 0), 0);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
148
|
+
const gapCount = ids.length - 1;
|
|
149
|
+
const availableHeight = height - gapCount * nodePadding;
|
|
150
|
+
const renderedHeights = ids.map((id) => {
|
|
105
151
|
const val = nodeValues.get(id) ?? 0;
|
|
106
152
|
const h = totalValue > 0 ? (val / totalValue) * availableHeight : availableHeight / ids.length;
|
|
107
|
-
|
|
153
|
+
return Math.max(minLinkWidth, h);
|
|
154
|
+
});
|
|
155
|
+
const sumRendered = renderedHeights.reduce((s, h) => s + h, 0);
|
|
156
|
+
const paddingBudget = gapCount > 0 ? (height - sumRendered) / gapCount : 0;
|
|
157
|
+
const effectivePadding = Math.max(0, Math.min(nodePadding, paddingBudget));
|
|
158
|
+
const scale = sumRendered > height && sumRendered > 0 ? height / sumRendered : 1;
|
|
159
|
+
columnPadding.set(col, effectivePadding);
|
|
160
|
+
let y = 0;
|
|
161
|
+
for (let index = 0; index < ids.length; index++) {
|
|
162
|
+
const id = ids[index];
|
|
163
|
+
const renderedH = renderedHeights[index] * scale;
|
|
108
164
|
nodeY.set(id, y);
|
|
109
165
|
nodeH.set(id, renderedH);
|
|
110
|
-
y += renderedH +
|
|
166
|
+
y += renderedH + effectivePadding;
|
|
111
167
|
}
|
|
112
168
|
}
|
|
113
169
|
// Iterative relaxation (upstream pass)
|
|
114
170
|
for (let iter = 0; iter < iterations; iter++) {
|
|
115
|
-
for (const [, ids] of columnGroups) {
|
|
171
|
+
for (const [col, ids] of columnGroups) {
|
|
172
|
+
const padding = columnPadding.get(col) ?? nodePadding;
|
|
116
173
|
for (const id of ids) {
|
|
117
174
|
const deps = incoming.get(id) ?? [];
|
|
118
175
|
const totalDepValue = deps.reduce((s, d) => s + d.value, 0);
|
|
@@ -128,15 +185,13 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
128
185
|
nodeY.set(id, Math.max(0, weightedY - (nodeH.get(id) ?? 0) / 2));
|
|
129
186
|
}
|
|
130
187
|
}
|
|
131
|
-
// Resolve overlaps: push down from the top.
|
|
132
|
-
ids.sort((a, b) => (nodeY.get(a) ?? 0) - (nodeY.get(b) ?? 0));
|
|
133
188
|
let y = 0;
|
|
134
189
|
for (const id of ids) {
|
|
135
190
|
const cy = nodeY.get(id) ?? 0;
|
|
136
191
|
if (cy < y) {
|
|
137
192
|
nodeY.set(id, y);
|
|
138
193
|
}
|
|
139
|
-
y = (nodeY.get(id) ?? 0) + (nodeH.get(id) ?? 0) +
|
|
194
|
+
y = (nodeY.get(id) ?? 0) + (nodeH.get(id) ?? 0) + padding;
|
|
140
195
|
}
|
|
141
196
|
// Resolve overlaps: pull back up from the bottom. The push-down pass
|
|
142
197
|
// above only ever grows a column's block downward, so a fan-out whose
|
|
@@ -151,7 +206,7 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
151
206
|
if (nodeBottom > bottomBoundary) {
|
|
152
207
|
nodeY.set(id, bottomBoundary - (nodeH.get(id) ?? 0));
|
|
153
208
|
}
|
|
154
|
-
bottomBoundary = (nodeY.get(id) ?? 0) -
|
|
209
|
+
bottomBoundary = (nodeY.get(id) ?? 0) - padding;
|
|
155
210
|
}
|
|
156
211
|
// Re-centre the column's node group within [0, height]: once overlaps
|
|
157
212
|
// are resolved, anchor the block at the midpoint of its remaining
|
|
@@ -184,25 +239,53 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
184
239
|
column: columns.get(n.id) ?? 0
|
|
185
240
|
}));
|
|
186
241
|
const nodeById = new Map(computedNodes.map((n) => [n.id, n]));
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
242
|
+
const linkKey = (l) => `${l.source}${l.target}`;
|
|
243
|
+
const linkWidths = new Map();
|
|
244
|
+
for (const l of links) {
|
|
245
|
+
const sVal = nodeValues.get(l.source) ?? 1;
|
|
246
|
+
const sourceH = nodeH.get(l.source) ?? 0;
|
|
247
|
+
linkWidths.set(linkKey(l), Math.max(minLinkWidth, (l.value / Math.max(sVal, 1)) * sourceH));
|
|
248
|
+
}
|
|
249
|
+
const linkSy = new Map();
|
|
250
|
+
const linkTy = new Map();
|
|
251
|
+
const bySource = new Map();
|
|
252
|
+
const byTarget = new Map();
|
|
253
|
+
for (const l of links) {
|
|
254
|
+
if (!bySource.has(l.source)) {
|
|
255
|
+
bySource.set(l.source, []);
|
|
256
|
+
}
|
|
257
|
+
bySource.get(l.source).push(l);
|
|
258
|
+
if (!byTarget.has(l.target)) {
|
|
259
|
+
byTarget.set(l.target, []);
|
|
260
|
+
}
|
|
261
|
+
byTarget.get(l.target).push(l);
|
|
262
|
+
}
|
|
263
|
+
for (const [source, group] of bySource) {
|
|
264
|
+
group.sort((a, b) => (nodeY.get(a.target) ?? 0) - (nodeY.get(b.target) ?? 0));
|
|
265
|
+
let offset = nodeY.get(source) ?? 0;
|
|
266
|
+
for (const l of group) {
|
|
267
|
+
const w = linkWidths.get(linkKey(l)) ?? 0;
|
|
268
|
+
linkSy.set(linkKey(l), offset + w / 2);
|
|
269
|
+
offset += w;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const [target, group] of byTarget) {
|
|
273
|
+
group.sort((a, b) => (nodeY.get(a.source) ?? 0) - (nodeY.get(b.source) ?? 0));
|
|
274
|
+
let offset = nodeY.get(target) ?? 0;
|
|
275
|
+
for (const l of group) {
|
|
276
|
+
const w = linkWidths.get(linkKey(l)) ?? 0;
|
|
277
|
+
linkTy.set(linkKey(l), offset + w / 2);
|
|
278
|
+
offset += w;
|
|
279
|
+
}
|
|
193
280
|
}
|
|
194
281
|
const computedLinks = links.map((l) => {
|
|
195
282
|
const sourceNode = nodeById.get(l.source);
|
|
196
283
|
const targetNode = nodeById.get(l.target);
|
|
197
|
-
const
|
|
198
|
-
const sourceH = nodeH.get(l.source) ?? 0;
|
|
199
|
-
const linkWidth = Math.max(minLinkWidth, (l.value / Math.max(sVal, 1)) * sourceH);
|
|
284
|
+
const linkWidth = linkWidths.get(linkKey(l)) ?? minLinkWidth;
|
|
200
285
|
const sx = (sourceNode?.x ?? 0) + nodeWidth;
|
|
201
|
-
const sy =
|
|
286
|
+
const sy = linkSy.get(linkKey(l)) ?? 0;
|
|
202
287
|
const tx = targetNode?.x ?? 0;
|
|
203
|
-
const ty =
|
|
204
|
-
sourceOffsets.set(l.source, (sourceOffsets.get(l.source) ?? 0) + linkWidth);
|
|
205
|
-
targetOffsets.set(l.target, (targetOffsets.get(l.target) ?? 0) + linkWidth);
|
|
288
|
+
const ty = linkTy.get(linkKey(l)) ?? 0;
|
|
206
289
|
const midX = (sx + tx) / 2;
|
|
207
290
|
const path = `M ${sx} ${sy} C ${midX} ${sy}, ${midX} ${ty}, ${tx} ${ty}`;
|
|
208
291
|
return {
|