@webmcp-auto-ui/ui 2.5.32 → 2.5.34
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/package.json +16 -2
- package/src/agent/DiagnosticModal.svelte +126 -50
- package/src/agent/EphemeralBubble.svelte +13 -3
- package/src/agent/MCPserversList.svelte +147 -0
- package/src/agent/McpConnector.svelte +10 -1
- package/src/agent/RecipeBrowser.svelte +384 -0
- package/src/agent/RemoteMCPserversDemo.svelte +5 -121
- package/src/agent/ToolBrowser.svelte +133 -0
- package/src/agent/WebMCPserversList.svelte +2 -0
- package/src/agent/useAgentLoop.svelte.ts +396 -0
- package/src/base/chat-inline.svelte +68 -0
- package/src/base/dialog-content.svelte +3 -1
- package/src/base/dialog-trigger.svelte +3 -2
- package/src/components/HeaderControls.svelte +78 -0
- package/src/index.ts +13 -35
- package/src/stores/canvas.svelte.ts +0 -6
- package/src/widgets/SafeImage.svelte +67 -0
- package/src/widgets/WidgetRenderer.svelte +153 -78
- package/src/widgets/notebook/executors/index.ts +0 -1
- package/src/widgets/notebook/executors/sql.ts +32 -182
- package/src/widgets/notebook/import-modal-api.ts +237 -0
- package/src/widgets/notebook/import-modal.svelte +738 -0
- package/src/widgets/notebook/left-pane.ts +1 -1
- package/src/widgets/notebook/notebook.svelte +75 -0
- package/src/widgets/notebook/notebook.ts +38 -73
- package/src/widgets/notebook/prose.ts +6 -3
- package/src/widgets/notebook/shared.ts +68 -49
- package/src/widgets/rich/cards.svelte +74 -0
- package/src/widgets/rich/carousel.svelte +126 -0
- package/src/widgets/rich/chart-rich.svelte +221 -0
- package/src/widgets/rich/chat-input.svelte +51 -0
- package/src/widgets/rich/data-table.svelte +132 -0
- package/src/widgets/rich/gallery.svelte +115 -0
- package/src/widgets/rich/grid-data.svelte +85 -0
- package/src/widgets/rich/hemicycle.svelte +95 -0
- package/src/widgets/rich/js-sandbox.svelte +67 -0
- package/src/widgets/rich/json-viewer.svelte +82 -0
- package/src/widgets/rich/log.svelte +62 -0
- package/src/widgets/rich/profile.svelte +91 -0
- package/src/widgets/rich/sankey.svelte +73 -0
- package/src/widgets/rich/stat-card.svelte +60 -0
- package/src/widgets/rich/timeline.svelte +95 -0
- package/src/widgets/rich/trombinoscope.svelte +87 -0
- package/src/widgets/simple/actions.svelte +36 -0
- package/src/widgets/simple/alert.svelte +52 -0
- package/src/widgets/simple/chart.svelte +38 -0
- package/src/widgets/simple/code.svelte +30 -0
- package/src/widgets/simple/kv.svelte +31 -0
- package/src/widgets/simple/list.svelte +35 -0
- package/src/widgets/simple/stat.svelte +36 -0
- package/src/widgets/simple/tags.svelte +34 -0
- package/src/widgets/simple/text.svelte +130 -0
- package/src/widgets/helpers/safe-image.ts +0 -78
- package/src/widgets/notebook/import-modals.ts +0 -560
- package/src/widgets/notebook/recipe-browser.ts +0 -350
- package/src/widgets/rich/cards.ts +0 -181
- package/src/widgets/rich/carousel.ts +0 -319
- package/src/widgets/rich/chart-rich.ts +0 -386
- package/src/widgets/rich/d3.ts +0 -503
- package/src/widgets/rich/data-table.ts +0 -342
- package/src/widgets/rich/gallery.ts +0 -350
- package/src/widgets/rich/grid-data.ts +0 -173
- package/src/widgets/rich/hemicycle.ts +0 -313
- package/src/widgets/rich/js-sandbox.ts +0 -122
- package/src/widgets/rich/json-viewer.ts +0 -202
- package/src/widgets/rich/log.ts +0 -143
- package/src/widgets/rich/map.ts +0 -218
- package/src/widgets/rich/profile.ts +0 -256
- package/src/widgets/rich/sankey.ts +0 -257
- package/src/widgets/rich/stat-card.ts +0 -125
- package/src/widgets/rich/timeline.ts +0 -179
- package/src/widgets/rich/trombinoscope.ts +0 -246
- package/src/widgets/simple/actions.ts +0 -89
- package/src/widgets/simple/alert.ts +0 -100
- package/src/widgets/simple/chart.ts +0 -189
- package/src/widgets/simple/code.ts +0 -79
- package/src/widgets/simple/kv.ts +0 -68
- package/src/widgets/simple/list.ts +0 -89
- package/src/widgets/simple/stat.ts +0 -58
- package/src/widgets/simple/tags.ts +0 -125
- package/src/widgets/simple/text.ts +0 -198
package/src/widgets/rich/d3.ts
DELETED
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
// Vanilla renderer for the D3 widget.
|
|
2
|
-
// Mirrors packages/ui/src/widgets/rich/D3Widget.svelte (4 presets).
|
|
3
|
-
//
|
|
4
|
-
// Contract: export function render(container, spec): () => void
|
|
5
|
-
// - `spec` is the D3Spec (same shape as the Svelte version).
|
|
6
|
-
// - D3 is lazy-loaded.
|
|
7
|
-
// - Force preset simulation is stopped on cleanup.
|
|
8
|
-
// - container.__exportPng(scale) serialises the SVG and rasterises it.
|
|
9
|
-
|
|
10
|
-
export interface D3Spec {
|
|
11
|
-
title?: string;
|
|
12
|
-
preset?: 'hex-heatmap' | 'radial' | 'treemap' | 'force';
|
|
13
|
-
data: unknown;
|
|
14
|
-
config?: Record<string, unknown>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type D3Module = typeof import('d3');
|
|
18
|
-
|
|
19
|
-
// ── Colour helpers ───────────────────────────────────────────────────────────
|
|
20
|
-
function cssVar(name: string, fallback: string): string {
|
|
21
|
-
if (typeof document === 'undefined') return fallback;
|
|
22
|
-
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ── Chrome (outer card) ──────────────────────────────────────────────────────
|
|
26
|
-
function buildChrome(container: HTMLElement, title?: string): HTMLDivElement {
|
|
27
|
-
container.innerHTML = '';
|
|
28
|
-
const card = document.createElement('div');
|
|
29
|
-
card.className = 'bg-surface border border-border rounded-lg p-3 md:p-4 font-sans';
|
|
30
|
-
if (title) {
|
|
31
|
-
const h = document.createElement('h3');
|
|
32
|
-
h.className = 'text-sm font-semibold text-text1 mb-3';
|
|
33
|
-
h.textContent = title;
|
|
34
|
-
card.appendChild(h);
|
|
35
|
-
}
|
|
36
|
-
const slot = document.createElement('div');
|
|
37
|
-
slot.className = 'w-full min-h-[200px]';
|
|
38
|
-
card.appendChild(slot);
|
|
39
|
-
container.appendChild(card);
|
|
40
|
-
return slot;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function showLoading(slot: HTMLElement) {
|
|
44
|
-
slot.innerHTML = '<div class="flex items-center justify-center h-48 text-text2 text-sm">Chargement D3...</div>';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function showError(slot: HTMLElement, msg: string) {
|
|
48
|
-
slot.innerHTML = '';
|
|
49
|
-
const d = document.createElement('div');
|
|
50
|
-
d.className = 'text-accent2 text-sm';
|
|
51
|
-
d.textContent = msg;
|
|
52
|
-
slot.appendChild(d);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function placeholder(slot: HTMLElement, msg: string) {
|
|
56
|
-
slot.innerHTML = `<p class="text-xs text-text2 p-2">${msg}</p>`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Presets ──────────────────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
function renderHexHeatmap(d3: D3Module, el: HTMLElement, d: D3Spec) {
|
|
62
|
-
const values = (d.data as { values?: number[][] })?.values;
|
|
63
|
-
if (!values || values.length === 0) {
|
|
64
|
-
placeholder(el, 'No data (values missing)');
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const cfg = d.config ?? {};
|
|
68
|
-
const cellSize: number = (cfg.cellSize as number) ?? 14;
|
|
69
|
-
const colorScale: string[] = (cfg.colorScale as string[]) ?? [
|
|
70
|
-
cssVar('--color-surface2', '#f0f0f6'),
|
|
71
|
-
cssVar('--color-accent', '#6c5ce7'),
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
const rows = values.length;
|
|
75
|
-
const cols = Math.max(...values.map((r) => r.length));
|
|
76
|
-
const gap = 2;
|
|
77
|
-
const hex = cellSize;
|
|
78
|
-
const hx = hex * Math.sqrt(3);
|
|
79
|
-
const hy = hex * 1.5;
|
|
80
|
-
const svgW = cols * hx + hex * 0.5 + gap * cols;
|
|
81
|
-
const svgH = rows * hy + hex * 0.5 + gap * rows;
|
|
82
|
-
|
|
83
|
-
const allVals = values.flat();
|
|
84
|
-
const minVal = d3.min(allVals) ?? 0;
|
|
85
|
-
const maxVal = d3.max(allVals) ?? 1;
|
|
86
|
-
|
|
87
|
-
const colorInterp = d3.scaleLinear<string>()
|
|
88
|
-
.domain([minVal, maxVal])
|
|
89
|
-
.range(colorScale as [string, string]);
|
|
90
|
-
|
|
91
|
-
const svg = d3
|
|
92
|
-
.select(el)
|
|
93
|
-
.append('svg')
|
|
94
|
-
.attr('viewBox', `0 0 ${svgW} ${svgH}`)
|
|
95
|
-
.attr('width', '100%')
|
|
96
|
-
.attr('height', svgH);
|
|
97
|
-
|
|
98
|
-
function hexPath(cx: number, cy: number, r: number): string {
|
|
99
|
-
const pts = d3.range(6).map((i) => {
|
|
100
|
-
const angle = (Math.PI / 180) * (60 * i - 30);
|
|
101
|
-
return [cx + r * Math.cos(angle), cy + r * Math.sin(angle)];
|
|
102
|
-
});
|
|
103
|
-
return `M${pts.map((p) => p.join(',')).join('L')}Z`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
values.forEach((row, ri) => {
|
|
107
|
-
row.forEach((val, ci) => {
|
|
108
|
-
const cx = ci * (hx + gap) + (ri % 2 === 1 ? hx / 2 : 0) + hx / 2;
|
|
109
|
-
const cy = ri * (hy + gap) + hex;
|
|
110
|
-
svg
|
|
111
|
-
.append('path')
|
|
112
|
-
.attr('d', hexPath(cx, cy, hex * 0.9))
|
|
113
|
-
.attr('fill', colorInterp(val))
|
|
114
|
-
.attr('stroke', 'none')
|
|
115
|
-
.append('title')
|
|
116
|
-
.text(String(val));
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function renderRadial(d3: D3Module, el: HTMLElement, d: D3Spec, width: number) {
|
|
122
|
-
const segments = (d.data as { segments?: { label: string; value: number; color?: string }[] })?.segments;
|
|
123
|
-
if (!segments || segments.length === 0) {
|
|
124
|
-
placeholder(el, 'No data (segments missing)');
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const cfg = d.config ?? {};
|
|
128
|
-
const innerRatio: number = (cfg.innerRadius as number) ?? 0.4;
|
|
129
|
-
const size = Math.min(width || 300, 300);
|
|
130
|
-
const outerR = size / 2 - 10;
|
|
131
|
-
const innerR = outerR * innerRatio;
|
|
132
|
-
|
|
133
|
-
const defaultColors = [
|
|
134
|
-
cssVar('--color-accent', '#6c5ce7'),
|
|
135
|
-
cssVar('--color-accent2', '#e17055'),
|
|
136
|
-
'#00b894', '#fdcb6e', '#0984e3', '#e84393',
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
const svg = d3
|
|
140
|
-
.select(el)
|
|
141
|
-
.append('svg')
|
|
142
|
-
.attr('viewBox', `0 0 ${size} ${size}`)
|
|
143
|
-
.attr('width', '100%')
|
|
144
|
-
.attr('height', size);
|
|
145
|
-
|
|
146
|
-
const g = svg.append('g').attr('transform', `translate(${size / 2},${size / 2})`);
|
|
147
|
-
|
|
148
|
-
const pie = d3.pie<{ label: string; value: number; color?: string }>()
|
|
149
|
-
.value((seg) => seg.value)
|
|
150
|
-
.sort(null);
|
|
151
|
-
|
|
152
|
-
const arc = d3.arc<d3.PieArcDatum<{ label: string; value: number; color?: string }>>()
|
|
153
|
-
.innerRadius(innerR)
|
|
154
|
-
.outerRadius(outerR);
|
|
155
|
-
|
|
156
|
-
const arcs = pie(segments);
|
|
157
|
-
|
|
158
|
-
g.selectAll('path')
|
|
159
|
-
.data(arcs)
|
|
160
|
-
.enter()
|
|
161
|
-
.append('path')
|
|
162
|
-
.attr('d', arc)
|
|
163
|
-
.attr('fill', (_, i) => arcs[i].data.color ?? defaultColors[i % defaultColors.length])
|
|
164
|
-
.attr('stroke', 'var(--color-surface, #fff)')
|
|
165
|
-
.attr('stroke-width', 2)
|
|
166
|
-
.append('title')
|
|
167
|
-
.text((arcDatum) => `${arcDatum.data.label}: ${arcDatum.data.value}`);
|
|
168
|
-
|
|
169
|
-
const labelArc = d3.arc<d3.PieArcDatum<{ label: string; value: number; color?: string }>>()
|
|
170
|
-
.innerRadius(outerR * 0.7)
|
|
171
|
-
.outerRadius(outerR * 0.7);
|
|
172
|
-
|
|
173
|
-
g.selectAll('text')
|
|
174
|
-
.data(arcs)
|
|
175
|
-
.enter()
|
|
176
|
-
.append('text')
|
|
177
|
-
.attr('transform', (arcDatum) => `translate(${labelArc.centroid(arcDatum)})`)
|
|
178
|
-
.attr('text-anchor', 'middle')
|
|
179
|
-
.attr('font-size', '10px')
|
|
180
|
-
.attr('fill', 'var(--color-text1, #111)')
|
|
181
|
-
.text((arcDatum) => arcDatum.data.label.length > 8 ? arcDatum.data.label.slice(0, 7) + '…' : arcDatum.data.label);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function renderTreemap(d3: D3Module, el: HTMLElement, d: D3Spec, width: number) {
|
|
185
|
-
type TreeNode = { name: string; value?: number; children?: TreeNode[] };
|
|
186
|
-
const rawData = d.data as { children?: TreeNode[] };
|
|
187
|
-
if (!rawData?.children || rawData.children.length === 0) {
|
|
188
|
-
placeholder(el, 'No data (children missing)');
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const cfg = d.config ?? {};
|
|
192
|
-
const padding: number = (cfg.padding as number) ?? 2;
|
|
193
|
-
const w = width || 400;
|
|
194
|
-
const h = Math.max(200, Math.round(w * 0.6));
|
|
195
|
-
|
|
196
|
-
const accent = cssVar('--color-accent', '#6c5ce7');
|
|
197
|
-
const colorScale = d3.scaleSequential(d3.interpolateBlues).domain([0, 10]);
|
|
198
|
-
|
|
199
|
-
const root = d3
|
|
200
|
-
.hierarchy<TreeNode>({ name: 'root', children: rawData.children })
|
|
201
|
-
.sum((node) => node.value ?? 1)
|
|
202
|
-
.sort((a, b) => (b.value ?? 0) - (a.value ?? 0));
|
|
203
|
-
|
|
204
|
-
d3.treemap<TreeNode>()
|
|
205
|
-
.size([w, h])
|
|
206
|
-
.padding(padding)(root);
|
|
207
|
-
|
|
208
|
-
const svg = d3
|
|
209
|
-
.select(el)
|
|
210
|
-
.append('svg')
|
|
211
|
-
.attr('viewBox', `0 0 ${w} ${h}`)
|
|
212
|
-
.attr('width', '100%')
|
|
213
|
-
.attr('height', h);
|
|
214
|
-
|
|
215
|
-
const leaves = root.leaves() as (d3.HierarchyRectangularNode<TreeNode>)[];
|
|
216
|
-
|
|
217
|
-
const cell = svg
|
|
218
|
-
.selectAll('g')
|
|
219
|
-
.data(leaves)
|
|
220
|
-
.enter()
|
|
221
|
-
.append('g')
|
|
222
|
-
.attr('transform', (node) => `translate(${node.x0},${node.y0})`);
|
|
223
|
-
|
|
224
|
-
cell
|
|
225
|
-
.append('rect')
|
|
226
|
-
.attr('width', (node) => Math.max(0, node.x1 - node.x0))
|
|
227
|
-
.attr('height', (node) => Math.max(0, node.y1 - node.y0))
|
|
228
|
-
.attr('fill', (_, i) => colorScale(i % 11))
|
|
229
|
-
.attr('stroke', accent)
|
|
230
|
-
.attr('stroke-width', 1)
|
|
231
|
-
.append('title')
|
|
232
|
-
.text((node) => `${node.data.name}: ${node.value}`);
|
|
233
|
-
|
|
234
|
-
cell
|
|
235
|
-
.append('text')
|
|
236
|
-
.attr('x', 4)
|
|
237
|
-
.attr('y', 14)
|
|
238
|
-
.attr('font-size', '11px')
|
|
239
|
-
.attr('fill', '#fff')
|
|
240
|
-
.text((node) => {
|
|
241
|
-
const w2 = node.x1 - node.x0;
|
|
242
|
-
const label = node.data.name;
|
|
243
|
-
return w2 > 30 ? (label.length > 12 ? label.slice(0, 11) + '…' : label) : '';
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Returns the simulation so the caller can stop it on cleanup.
|
|
248
|
-
function renderForce(
|
|
249
|
-
d3: D3Module,
|
|
250
|
-
el: HTMLElement,
|
|
251
|
-
d: D3Spec,
|
|
252
|
-
width: number,
|
|
253
|
-
): { simulation?: ReturnType<D3Module['forceSimulation']> } {
|
|
254
|
-
type FNode = { id: string; label?: string; group?: number };
|
|
255
|
-
type FLink = { source: string; target: string; value?: number };
|
|
256
|
-
const rawData = d.data as { nodes?: FNode[]; links?: FLink[] };
|
|
257
|
-
if (!rawData?.nodes || rawData.nodes.length === 0) {
|
|
258
|
-
placeholder(el, 'No data (nodes missing)');
|
|
259
|
-
return {};
|
|
260
|
-
}
|
|
261
|
-
const nodes: FNode[] = rawData.nodes.map((n) => ({ ...n }));
|
|
262
|
-
const links: FLink[] = (rawData.links ?? []).map((l) => ({ ...l }));
|
|
263
|
-
const w = width || 400;
|
|
264
|
-
const h = Math.max(250, Math.round(w * 0.65));
|
|
265
|
-
|
|
266
|
-
const maxChars = Math.max(10, Math.floor(w / 120));
|
|
267
|
-
const truncate = (text: string): string =>
|
|
268
|
-
text.length > maxChars ? text.slice(0, Math.max(1, maxChars - 1)) + '…' : text;
|
|
269
|
-
|
|
270
|
-
const accent = cssVar('--color-accent', '#6c5ce7');
|
|
271
|
-
const accent2 = cssVar('--color-accent2', '#e17055');
|
|
272
|
-
const groups = Array.from(new Set(nodes.map((n) => n.group ?? 0)));
|
|
273
|
-
const colorMap = d3.scaleOrdinal<number, string>()
|
|
274
|
-
.domain(groups)
|
|
275
|
-
.range([accent, accent2, '#00b894', '#fdcb6e', '#0984e3', '#e84393']);
|
|
276
|
-
|
|
277
|
-
const svg = d3
|
|
278
|
-
.select(el)
|
|
279
|
-
.append('svg')
|
|
280
|
-
.attr('viewBox', `0 0 ${w} ${h}`)
|
|
281
|
-
.attr('width', '100%')
|
|
282
|
-
.attr('height', h);
|
|
283
|
-
|
|
284
|
-
const sim = d3
|
|
285
|
-
.forceSimulation(nodes as d3.SimulationNodeDatum[])
|
|
286
|
-
.force('link', d3.forceLink(links).id((n: d3.SimulationNodeDatum) => (n as FNode).id).distance(60))
|
|
287
|
-
.force('charge', d3.forceManyBody().strength(-120))
|
|
288
|
-
.force('center', d3.forceCenter(w / 2, h / 2))
|
|
289
|
-
.force('collision', d3.forceCollide(18));
|
|
290
|
-
|
|
291
|
-
const link = svg
|
|
292
|
-
.append('g')
|
|
293
|
-
.attr('stroke', 'var(--color-border, #ccc)')
|
|
294
|
-
.attr('stroke-opacity', 0.6)
|
|
295
|
-
.selectAll('line')
|
|
296
|
-
.data(links)
|
|
297
|
-
.enter()
|
|
298
|
-
.append('line')
|
|
299
|
-
.attr('stroke-width', (l) => Math.sqrt(l.value ?? 1));
|
|
300
|
-
|
|
301
|
-
const node = svg
|
|
302
|
-
.append('g')
|
|
303
|
-
.selectAll('g')
|
|
304
|
-
.data(nodes)
|
|
305
|
-
.enter()
|
|
306
|
-
.append('g')
|
|
307
|
-
.call(
|
|
308
|
-
d3.drag<SVGGElement, FNode>()
|
|
309
|
-
.on('start', (event, n) => {
|
|
310
|
-
if (!event.active) sim.alphaTarget(0.3).restart();
|
|
311
|
-
(n as d3.SimulationNodeDatum).fx = (n as d3.SimulationNodeDatum).x;
|
|
312
|
-
(n as d3.SimulationNodeDatum).fy = (n as d3.SimulationNodeDatum).y;
|
|
313
|
-
})
|
|
314
|
-
.on('drag', (event, n) => {
|
|
315
|
-
(n as d3.SimulationNodeDatum).fx = event.x;
|
|
316
|
-
(n as d3.SimulationNodeDatum).fy = event.y;
|
|
317
|
-
})
|
|
318
|
-
.on('end', (event, n) => {
|
|
319
|
-
if (!event.active) sim.alphaTarget(0);
|
|
320
|
-
(n as d3.SimulationNodeDatum).fx = null;
|
|
321
|
-
(n as d3.SimulationNodeDatum).fy = null;
|
|
322
|
-
})
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
node
|
|
326
|
-
.append('circle')
|
|
327
|
-
.attr('r', 10)
|
|
328
|
-
.attr('fill', (n) => colorMap(n.group ?? 0))
|
|
329
|
-
.attr('stroke', '#fff')
|
|
330
|
-
.attr('stroke-width', 1.5);
|
|
331
|
-
|
|
332
|
-
node
|
|
333
|
-
.append('text')
|
|
334
|
-
.attr('x', 14)
|
|
335
|
-
.attr('y', 4)
|
|
336
|
-
.attr('font-size', '10px')
|
|
337
|
-
.attr('fill', 'var(--color-text1, #111)')
|
|
338
|
-
.text((n) => truncate(String(n.label ?? n.id)));
|
|
339
|
-
|
|
340
|
-
node.append('title').text((n) => String(n.label ?? n.id));
|
|
341
|
-
|
|
342
|
-
sim.on('tick', () => {
|
|
343
|
-
link
|
|
344
|
-
.attr('x1', (l) => (l.source as d3.SimulationNodeDatum).x ?? 0)
|
|
345
|
-
.attr('y1', (l) => (l.source as d3.SimulationNodeDatum).y ?? 0)
|
|
346
|
-
.attr('x2', (l) => (l.target as d3.SimulationNodeDatum).x ?? 0)
|
|
347
|
-
.attr('y2', (l) => (l.target as d3.SimulationNodeDatum).y ?? 0);
|
|
348
|
-
node.attr('transform', (n) => `translate(${(n as d3.SimulationNodeDatum).x ?? 0},${(n as d3.SimulationNodeDatum).y ?? 0})`);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
return { simulation: sim };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ── __exportPng: SVG → canvas → blob ─────────────────────────────────────────
|
|
355
|
-
async function exportPng(container: HTMLElement, scale = 2): Promise<Blob> {
|
|
356
|
-
const svgEl = container.querySelector('svg') as SVGSVGElement | null;
|
|
357
|
-
if (!svgEl) throw new Error('No SVG to export');
|
|
358
|
-
|
|
359
|
-
// Serialise
|
|
360
|
-
const clone = svgEl.cloneNode(true) as SVGSVGElement;
|
|
361
|
-
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
362
|
-
const serializer = new XMLSerializer();
|
|
363
|
-
const svgString = serializer.serializeToString(clone);
|
|
364
|
-
|
|
365
|
-
// Dimensions from viewBox (preferred) or bounding rect
|
|
366
|
-
let w = 0;
|
|
367
|
-
let h = 0;
|
|
368
|
-
const vb = svgEl.getAttribute('viewBox');
|
|
369
|
-
if (vb) {
|
|
370
|
-
const parts = vb.split(/\s+/).map(Number);
|
|
371
|
-
if (parts.length === 4) { w = parts[2]; h = parts[3]; }
|
|
372
|
-
}
|
|
373
|
-
if (!w || !h) {
|
|
374
|
-
const rect = svgEl.getBoundingClientRect();
|
|
375
|
-
w = rect.width; h = rect.height;
|
|
376
|
-
}
|
|
377
|
-
if (!w || !h) throw new Error('Cannot determine SVG dimensions');
|
|
378
|
-
|
|
379
|
-
const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|
380
|
-
const url = URL.createObjectURL(svgBlob);
|
|
381
|
-
try {
|
|
382
|
-
const img = new Image();
|
|
383
|
-
img.crossOrigin = 'anonymous';
|
|
384
|
-
await new Promise<void>((resolve, reject) => {
|
|
385
|
-
img.onload = () => resolve();
|
|
386
|
-
img.onerror = () => reject(new Error('SVG image load failed'));
|
|
387
|
-
img.src = url;
|
|
388
|
-
});
|
|
389
|
-
const canvas = document.createElement('canvas');
|
|
390
|
-
canvas.width = Math.max(1, Math.round(w * scale));
|
|
391
|
-
canvas.height = Math.max(1, Math.round(h * scale));
|
|
392
|
-
const ctx = canvas.getContext('2d');
|
|
393
|
-
if (!ctx) throw new Error('2D context unavailable');
|
|
394
|
-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
395
|
-
return await new Promise<Blob>((resolve, reject) => {
|
|
396
|
-
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('toBlob failed'))), 'image/png');
|
|
397
|
-
});
|
|
398
|
-
} finally {
|
|
399
|
-
URL.revokeObjectURL(url);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// ── Main render dispatcher ───────────────────────────────────────────────────
|
|
404
|
-
export function render(container: HTMLElement, data: D3Spec): () => void {
|
|
405
|
-
const spec = data;
|
|
406
|
-
if (!spec || !spec.preset) {
|
|
407
|
-
const slot = buildChrome(container, spec?.title);
|
|
408
|
-
slot.innerHTML = '<div class="widget-empty" style="padding:1em;opacity:.6">No data</div>';
|
|
409
|
-
return () => { container.innerHTML = ''; };
|
|
410
|
-
}
|
|
411
|
-
const slot = buildChrome(container, spec.title);
|
|
412
|
-
showLoading(slot);
|
|
413
|
-
|
|
414
|
-
let cancelled = false;
|
|
415
|
-
let simulation: ReturnType<D3Module['forceSimulation']> | undefined;
|
|
416
|
-
let ro: ResizeObserver | undefined;
|
|
417
|
-
let currentWidth = 0;
|
|
418
|
-
let lastRenderedWidth = -1;
|
|
419
|
-
|
|
420
|
-
const paint = (d3: D3Module) => {
|
|
421
|
-
if (cancelled) return;
|
|
422
|
-
// Stop any previous simulation before repainting.
|
|
423
|
-
simulation?.stop();
|
|
424
|
-
simulation = undefined;
|
|
425
|
-
slot.innerHTML = '';
|
|
426
|
-
|
|
427
|
-
if (!spec?.preset) {
|
|
428
|
-
placeholder(slot, 'No preset specified.');
|
|
429
|
-
return;
|
|
430
|
-
}
|
|
431
|
-
switch (spec.preset) {
|
|
432
|
-
case 'hex-heatmap':
|
|
433
|
-
renderHexHeatmap(d3, slot, spec);
|
|
434
|
-
break;
|
|
435
|
-
case 'radial':
|
|
436
|
-
renderRadial(d3, slot, spec, currentWidth);
|
|
437
|
-
break;
|
|
438
|
-
case 'treemap':
|
|
439
|
-
renderTreemap(d3, slot, spec, currentWidth);
|
|
440
|
-
break;
|
|
441
|
-
case 'force': {
|
|
442
|
-
const { simulation: sim } = renderForce(d3, slot, spec, currentWidth);
|
|
443
|
-
simulation = sim;
|
|
444
|
-
break;
|
|
445
|
-
}
|
|
446
|
-
default:
|
|
447
|
-
placeholder(slot, `Preset inconnu : ${spec.preset}`);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Emit a bubbling CustomEvent so hosts can react to re-renders.
|
|
451
|
-
try {
|
|
452
|
-
container.dispatchEvent(new CustomEvent('d3:rendered', {
|
|
453
|
-
bubbles: true,
|
|
454
|
-
detail: { preset: spec.preset },
|
|
455
|
-
}));
|
|
456
|
-
} catch { /* noop */ }
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
(async () => {
|
|
460
|
-
let d3: D3Module;
|
|
461
|
-
try {
|
|
462
|
-
d3 = await import('d3');
|
|
463
|
-
} catch (e) {
|
|
464
|
-
if (cancelled) return;
|
|
465
|
-
showError(slot, e instanceof Error ? e.message : 'D3 load failed');
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
if (cancelled) return;
|
|
469
|
-
|
|
470
|
-
// ResizeObserver: repaint on meaningful width changes (radial/treemap/force depend on it).
|
|
471
|
-
ro = new ResizeObserver((entries) => {
|
|
472
|
-
const r = entries[0]?.contentRect;
|
|
473
|
-
if (!r) return;
|
|
474
|
-
currentWidth = r.width;
|
|
475
|
-
if (lastRenderedWidth < 0 || Math.abs(currentWidth - lastRenderedWidth) > 4) {
|
|
476
|
-
lastRenderedWidth = currentWidth;
|
|
477
|
-
paint(d3);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
ro.observe(slot);
|
|
481
|
-
|
|
482
|
-
// Initial paint (ResizeObserver also fires synchronously after observe, but
|
|
483
|
-
// kick it in case the element has zero size at mount time).
|
|
484
|
-
currentWidth = slot.getBoundingClientRect().width;
|
|
485
|
-
lastRenderedWidth = currentWidth;
|
|
486
|
-
paint(d3);
|
|
487
|
-
})();
|
|
488
|
-
|
|
489
|
-
// Attach PNG export.
|
|
490
|
-
(container as HTMLElement & { __exportPng?: (scale?: number) => Promise<Blob> }).__exportPng =
|
|
491
|
-
(scale?: number) => exportPng(slot, scale ?? 2);
|
|
492
|
-
|
|
493
|
-
return () => {
|
|
494
|
-
cancelled = true;
|
|
495
|
-
ro?.disconnect();
|
|
496
|
-
simulation?.stop();
|
|
497
|
-
simulation = undefined;
|
|
498
|
-
container.innerHTML = '';
|
|
499
|
-
try {
|
|
500
|
-
delete (container as HTMLElement & { __exportPng?: unknown }).__exportPng;
|
|
501
|
-
} catch { /* noop */ }
|
|
502
|
-
};
|
|
503
|
-
}
|