@simplex-viz/svg 0.1.3 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { rectToConvexPolygon, getHullBoundingRadius, SimplexGraphEngine, computeConvexHull, regularPolygon, starConvexHull } from '@simplex-viz/core';
1
+ import { rectToConvexPolygon, computeConvexHull, regularPolygon, starConvexHull, getHullBoundingRadius, SimplexGraphEngine } from '@simplex-viz/core';
2
+ export { computeConvexHull, rectToConvexPolygon, regularPolygon, solveSimplexSnapshot, starConvexHull } from '@simplex-viz/core';
2
3
 
3
4
  // src/hullMeasure.ts
4
5
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -89,6 +90,490 @@ function measureVisualWithSvgView(view, visual, cacheKey, rootSvg, measureBin, c
89
90
  cache.set(cacheKey, hull);
90
91
  return hull;
91
92
  }
93
+
94
+ // src/views/BadgeClusterSvgView.ts
95
+ var BadgeClusterSvgView = class {
96
+ mountOrUpdate(groupEl, visual, state) {
97
+ const doc = groupEl.ownerDocument;
98
+ while (groupEl.firstChild) {
99
+ groupEl.removeChild(groupEl.firstChild);
100
+ }
101
+ const isLight = visual.theme === "light";
102
+ const cardFill = isLight ? "#ffffff" : "#0d1320";
103
+ const hoverStroke = isLight ? "#0f172a" : "#e2e8f0";
104
+ const titleFill = isLight ? "#0f172a" : "#f1f5f9";
105
+ const metaFill = isLight ? "#475569" : "#94a3b8";
106
+ const titleLen = visual.title.length;
107
+ const metaLen = visual.metaLine.length;
108
+ const estWidth = Math.max(122, Math.max(titleLen * 6.8 + 44, metaLen * 5.9 + 24));
109
+ const height = 42;
110
+ const halfW = estWidth / 2;
111
+ const bg = doc.createElementNS(SVG_NS, "rect");
112
+ bg.setAttribute("x", String(-halfW));
113
+ bg.setAttribute("y", String(-21));
114
+ bg.setAttribute("width", String(estWidth));
115
+ bg.setAttribute("height", String(height));
116
+ bg.setAttribute("rx", "8");
117
+ bg.setAttribute("fill", cardFill);
118
+ bg.setAttribute("fill-opacity", isLight ? "0.97" : "0.94");
119
+ bg.setAttribute(
120
+ "stroke",
121
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered || state.interaction.isHighlighted ? hoverStroke : visual.colorHex
122
+ );
123
+ bg.setAttribute(
124
+ "stroke-width",
125
+ state.interaction.isHovered || state.interaction.isHighlighted ? "2.2" : "1.4"
126
+ );
127
+ groupEl.appendChild(bg);
128
+ const swatch = doc.createElementNS(SVG_NS, "circle");
129
+ swatch.setAttribute("cx", String(-halfW + 13));
130
+ swatch.setAttribute("cy", String(-7));
131
+ swatch.setAttribute("r", "4");
132
+ swatch.setAttribute("fill", visual.colorHex);
133
+ groupEl.appendChild(swatch);
134
+ const codeText = doc.createElementNS(SVG_NS, "text");
135
+ codeText.setAttribute("x", String(-halfW + 22));
136
+ codeText.setAttribute("y", String(-3));
137
+ codeText.setAttribute("fill", isLight ? "#0f172a" : visual.colorHex);
138
+ codeText.setAttribute("font-size", "10");
139
+ codeText.setAttribute("font-weight", "600");
140
+ codeText.setAttribute("font-family", "IBM Plex Mono, monospace");
141
+ codeText.textContent = visual.code;
142
+ groupEl.appendChild(codeText);
143
+ const titleText = doc.createElementNS(SVG_NS, "text");
144
+ titleText.setAttribute("x", String(-halfW + 44));
145
+ titleText.setAttribute("y", String(-3));
146
+ titleText.setAttribute("fill", titleFill);
147
+ titleText.setAttribute("font-size", "11");
148
+ titleText.setAttribute("font-weight", "600");
149
+ titleText.setAttribute("font-family", "Plus Jakarta Sans, sans-serif");
150
+ titleText.textContent = visual.title;
151
+ groupEl.appendChild(titleText);
152
+ const subText = doc.createElementNS(SVG_NS, "text");
153
+ subText.setAttribute("x", "0");
154
+ subText.setAttribute("y", "13");
155
+ subText.setAttribute("text-anchor", "middle");
156
+ subText.setAttribute("fill", metaFill);
157
+ subText.setAttribute("font-size", "9.5");
158
+ subText.setAttribute("font-family", "IBM Plex Mono, monospace");
159
+ subText.textContent = visual.metaLine;
160
+ groupEl.appendChild(subText);
161
+ }
162
+ };
163
+
164
+ // src/views/ClusterAnchorSvgView.ts
165
+ var ClusterAnchorSvgView = class {
166
+ measureConvexHull(visual) {
167
+ return { kind: "circle", radius: visual.radius + 5 };
168
+ }
169
+ mountOrUpdate(groupEl, visual, state) {
170
+ const doc = groupEl.ownerDocument;
171
+ while (groupEl.firstChild) {
172
+ groupEl.removeChild(groupEl.firstChild);
173
+ }
174
+ const isLight = visual.theme === "light";
175
+ const outerFill = isLight ? "#ffffff" : "#07090E";
176
+ const hoverStroke = isLight ? "#0f172a" : "#f8fafc";
177
+ const codeFill = isLight ? "#0f172a" : "#f8fafc";
178
+ const r = visual.radius;
179
+ const outer = doc.createElementNS(SVG_NS, "circle");
180
+ outer.setAttribute("cx", "0");
181
+ outer.setAttribute("cy", "0");
182
+ outer.setAttribute("r", String(r + 3));
183
+ outer.setAttribute("fill", outerFill);
184
+ outer.setAttribute("fill-opacity", isLight ? "0.96" : "0.88");
185
+ outer.setAttribute(
186
+ "stroke",
187
+ state.interaction.isHovered || state.interaction.isHighlighted ? hoverStroke : visual.colorHex
188
+ );
189
+ outer.setAttribute("stroke-width", state.interaction.isHovered ? "2.4" : "1.6");
190
+ outer.setAttribute("stroke-dasharray", "4 2.5");
191
+ groupEl.appendChild(outer);
192
+ const inner = doc.createElementNS(SVG_NS, "circle");
193
+ inner.setAttribute("cx", "0");
194
+ inner.setAttribute("cy", "0");
195
+ inner.setAttribute("r", String(r - 2));
196
+ inner.setAttribute("fill", visual.colorHex);
197
+ inner.setAttribute("fill-opacity", isLight ? "0.14" : "0.18");
198
+ inner.setAttribute("stroke", visual.colorHex);
199
+ inner.setAttribute("stroke-width", "1.4");
200
+ groupEl.appendChild(inner);
201
+ const codeEl = doc.createElementNS(SVG_NS, "text");
202
+ codeEl.setAttribute("x", "0");
203
+ codeEl.setAttribute("y", "-2");
204
+ codeEl.setAttribute("text-anchor", "middle");
205
+ codeEl.setAttribute("fill", codeFill);
206
+ codeEl.setAttribute("font-size", "11");
207
+ codeEl.setAttribute("font-weight", "600");
208
+ codeEl.setAttribute("font-family", "IBM Plex Mono, monospace");
209
+ codeEl.textContent = visual.code;
210
+ groupEl.appendChild(codeEl);
211
+ const countEl = doc.createElementNS(SVG_NS, "text");
212
+ countEl.setAttribute("x", "0");
213
+ countEl.setAttribute("y", "11");
214
+ countEl.setAttribute("text-anchor", "middle");
215
+ countEl.setAttribute("fill", isLight ? "#334155" : visual.colorHex);
216
+ countEl.setAttribute("font-size", "9");
217
+ countEl.setAttribute("font-weight", isLight ? "600" : "400");
218
+ countEl.setAttribute("font-family", "IBM Plex Mono, monospace");
219
+ countEl.textContent = `${visual.memberCount}n`;
220
+ groupEl.appendChild(countEl);
221
+ }
222
+ };
223
+ var MORPH_SAMPLES = 64;
224
+ function sampleShapePerimeter(shape, radius) {
225
+ const r = Math.max(3, radius);
226
+ const pts = [];
227
+ if (shape === "circle") {
228
+ for (let i = 0; i < MORPH_SAMPLES; i++) {
229
+ const a = i / MORPH_SAMPLES * Math.PI * 2 - Math.PI / 2;
230
+ pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
231
+ }
232
+ return pts;
233
+ }
234
+ const polyVerts = [];
235
+ if (shape === "diamond") {
236
+ const d = r * 1.12;
237
+ polyVerts.push(
238
+ { x: 0, y: -d },
239
+ { x: d * 0.88, y: 0 },
240
+ { x: 0, y: d },
241
+ { x: -d * 0.88, y: 0 }
242
+ );
243
+ } else if (shape === "hexagon") {
244
+ for (let i = 0; i < 6; i++) {
245
+ const a = -Math.PI / 2 + i * Math.PI * 2 / 6;
246
+ polyVerts.push({ x: Math.cos(a) * r * 1.05, y: Math.sin(a) * r * 1.05 });
247
+ }
248
+ } else {
249
+ const outer = r * 1.24;
250
+ const inner = r * 0.56;
251
+ for (let i = 0; i < 10; i++) {
252
+ const a = -Math.PI / 2 + i * Math.PI / 5;
253
+ const rad = i % 2 === 0 ? outer : inner;
254
+ polyVerts.push({ x: Math.cos(a) * rad, y: Math.sin(a) * rad });
255
+ }
256
+ }
257
+ const edgeLens = [];
258
+ let totalLen = 0;
259
+ for (let i = 0; i < polyVerts.length; i++) {
260
+ const a = polyVerts[i];
261
+ const b = polyVerts[(i + 1) % polyVerts.length];
262
+ const len = Math.hypot(b.x - a.x, b.y - a.y);
263
+ edgeLens.push(len);
264
+ totalLen += len;
265
+ }
266
+ for (let s = 0; s < MORPH_SAMPLES; s++) {
267
+ const targetDist = s / MORPH_SAMPLES * totalLen;
268
+ let accum = 0;
269
+ for (let i = 0; i < polyVerts.length; i++) {
270
+ const len = edgeLens[i];
271
+ if (accum + len >= targetDist || i === polyVerts.length - 1) {
272
+ const localT = len > 1e-6 ? (targetDist - accum) / len : 0;
273
+ const a = polyVerts[i];
274
+ const b = polyVerts[(i + 1) % polyVerts.length];
275
+ pts.push({
276
+ x: a.x + (b.x - a.x) * localT,
277
+ y: a.y + (b.y - a.y) * localT
278
+ });
279
+ break;
280
+ }
281
+ accum += len;
282
+ }
283
+ }
284
+ return pts;
285
+ }
286
+ function pointsToSvgPath(pts) {
287
+ if (pts.length === 0) return "";
288
+ let d = `M ${pts[0].x.toFixed(2)} ${pts[0].y.toFixed(2)}`;
289
+ for (let i = 1; i < pts.length; i++) {
290
+ const p = pts[i];
291
+ d += ` L ${p.x.toFixed(2)} ${p.y.toFixed(2)}`;
292
+ }
293
+ return `${d} Z`;
294
+ }
295
+ function parseHexRgb(hex) {
296
+ const clean = hex.startsWith("#") ? hex.slice(1) : hex;
297
+ if (clean.length < 6) return [148, 163, 184];
298
+ return [
299
+ parseInt(clean.slice(0, 2), 16) || 0,
300
+ parseInt(clean.slice(2, 4), 16) || 0,
301
+ parseInt(clean.slice(4, 6), 16) || 0
302
+ ];
303
+ }
304
+ function lerpHex(fromHex, toHex, t) {
305
+ const a = parseHexRgb(fromHex);
306
+ const b = parseHexRgb(toHex);
307
+ const r = Math.round(a[0] + (b[0] - a[0]) * t);
308
+ const g = Math.round(a[1] + (b[1] - a[1]) * t);
309
+ const bl = Math.round(a[2] + (b[2] - a[2]) * t);
310
+ return `rgb(${r}, ${g}, ${bl})`;
311
+ }
312
+ var GeometricShapeSvgView = class {
313
+ measureConvexHull(visual) {
314
+ const r = visual.radius + (visual.strokeWidth ?? 1.5);
315
+ if (visual.shape === "circle") {
316
+ return { kind: "circle", radius: r };
317
+ }
318
+ if (visual.shape === "diamond") {
319
+ const d = r * 1.12;
320
+ return {
321
+ kind: "polygon",
322
+ vertices: computeConvexHull([
323
+ { x: 0, y: -d },
324
+ { x: d * 0.88, y: 0 },
325
+ { x: 0, y: d },
326
+ { x: -d * 0.88, y: 0 }
327
+ ])
328
+ };
329
+ }
330
+ if (visual.shape === "hexagon") {
331
+ return regularPolygon(6, r * 1.05);
332
+ }
333
+ return starConvexHull(5, r * 1.24);
334
+ }
335
+ ensureElements(groupEl) {
336
+ let pathEl = groupEl.querySelector("path.sv-geom-shape");
337
+ let ringEl = groupEl.querySelector("path.sv-geom-ring");
338
+ if (!pathEl || !ringEl) {
339
+ while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
340
+ const doc = groupEl.ownerDocument;
341
+ ringEl = doc.createElementNS(SVG_NS, "path");
342
+ ringEl.setAttribute("class", "sv-geom-ring");
343
+ ringEl.setAttribute("fill", "none");
344
+ pathEl = doc.createElementNS(SVG_NS, "path");
345
+ pathEl.setAttribute("class", "sv-geom-shape");
346
+ groupEl.appendChild(ringEl);
347
+ groupEl.appendChild(pathEl);
348
+ }
349
+ return { pathEl, ringEl };
350
+ }
351
+ mountOrUpdate(groupEl, visual, state) {
352
+ const { pathEl, ringEl } = this.ensureElements(groupEl);
353
+ const pts = sampleShapePerimeter(visual.shape, visual.radius);
354
+ const d = pointsToSvgPath(pts);
355
+ const isLight = visual.theme === "light";
356
+ const defaultStroke = isLight ? "rgba(15, 23, 42, 0.25)" : "rgba(15, 23, 42, 0.8)";
357
+ const hoverStroke = isLight ? "#0f172a" : "#f8fafc";
358
+ const accentRing = isLight ? "#0891b2" : "#22d3ee";
359
+ pathEl.setAttribute("d", d);
360
+ pathEl.setAttribute("fill", visual.fillHex);
361
+ pathEl.setAttribute(
362
+ "stroke",
363
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? hoverStroke : visual.strokeHex ?? defaultStroke
364
+ );
365
+ pathEl.setAttribute(
366
+ "stroke-width",
367
+ String(state.interaction.isHovered || state.interaction.isPinned ? 2.2 : visual.strokeWidth ?? 1.4)
368
+ );
369
+ if (state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
370
+ const outerPts = sampleShapePerimeter(visual.shape, visual.radius + 4.5);
371
+ ringEl.setAttribute("d", pointsToSvgPath(outerPts));
372
+ ringEl.setAttribute("stroke", state.interaction.isPinned ? "#f59e0b" : accentRing);
373
+ ringEl.setAttribute("stroke-width", "1.4");
374
+ ringEl.setAttribute("stroke-opacity", "0.9");
375
+ } else {
376
+ ringEl.setAttribute("d", "");
377
+ }
378
+ }
379
+ morph(groupEl, from, to, progress, state) {
380
+ const { pathEl, ringEl } = this.ensureElements(groupEl);
381
+ const ptsA = sampleShapePerimeter(from.shape, from.radius);
382
+ const ptsB = sampleShapePerimeter(to.shape, to.radius);
383
+ const morphed = [];
384
+ for (let i = 0; i < MORPH_SAMPLES; i++) {
385
+ const a = ptsA[i];
386
+ const b = ptsB[i];
387
+ morphed.push({
388
+ x: a.x + (b.x - a.x) * progress,
389
+ y: a.y + (b.y - a.y) * progress
390
+ });
391
+ }
392
+ const isLight = to.theme === "light";
393
+ pathEl.setAttribute("d", pointsToSvgPath(morphed));
394
+ pathEl.setAttribute("fill", lerpHex(from.fillHex, to.fillHex, progress));
395
+ pathEl.setAttribute(
396
+ "stroke",
397
+ state.interaction.isHovered ? isLight ? "#0f172a" : "#f8fafc" : to.strokeHex ?? (isLight ? "rgba(15, 23, 42, 0.25)" : "rgba(15, 23, 42, 0.8)")
398
+ );
399
+ pathEl.setAttribute("stroke-width", String(to.strokeWidth ?? 1.4));
400
+ ringEl.setAttribute("d", "");
401
+ return true;
402
+ }
403
+ };
404
+
405
+ // src/views/MultiSectorGlyphSvgView.ts
406
+ function describeDonutSectorPath(innerR, outerR, startAngle, endAngle) {
407
+ const sweep = endAngle - startAngle;
408
+ if (sweep >= Math.PI * 2 - 1e-4) {
409
+ return [
410
+ `M ${outerR} 0`,
411
+ `A ${outerR} ${outerR} 0 1 0 ${-outerR} 0`,
412
+ `A ${outerR} ${outerR} 0 1 0 ${outerR} 0`,
413
+ `M ${innerR} 0`,
414
+ `A ${innerR} ${innerR} 0 1 1 ${-innerR} 0`,
415
+ `A ${innerR} ${innerR} 0 1 1 ${innerR} 0`,
416
+ "Z"
417
+ ].join(" ");
418
+ }
419
+ const largeArc = sweep > Math.PI ? 1 : 0;
420
+ const x1o = Math.cos(startAngle) * outerR;
421
+ const y1o = Math.sin(startAngle) * outerR;
422
+ const x2o = Math.cos(endAngle) * outerR;
423
+ const y2o = Math.sin(endAngle) * outerR;
424
+ const x1i = Math.cos(endAngle) * innerR;
425
+ const y1i = Math.sin(endAngle) * innerR;
426
+ const x2i = Math.cos(startAngle) * innerR;
427
+ const y2i = Math.sin(startAngle) * innerR;
428
+ return [
429
+ `M ${x1o.toFixed(2)} ${y1o.toFixed(2)}`,
430
+ `A ${outerR.toFixed(2)} ${outerR.toFixed(2)} 0 ${largeArc} 1 ${x2o.toFixed(2)} ${y2o.toFixed(2)}`,
431
+ `L ${x1i.toFixed(2)} ${y1i.toFixed(2)}`,
432
+ `A ${innerR.toFixed(2)} ${innerR.toFixed(2)} 0 ${largeArc} 0 ${x2i.toFixed(2)} ${y2i.toFixed(2)}`,
433
+ "Z"
434
+ ].join(" ");
435
+ }
436
+ var MultiSectorGlyphSvgView = class {
437
+ measureConvexHull(visual) {
438
+ return { kind: "circle", radius: visual.outerRadius + 2 };
439
+ }
440
+ mountOrUpdate(groupEl, visual, state) {
441
+ const doc = groupEl.ownerDocument;
442
+ while (groupEl.firstChild) {
443
+ groupEl.removeChild(groupEl.firstChild);
444
+ }
445
+ const isLight = visual.theme === "light";
446
+ const dividerStroke = isLight ? "#ffffff" : "#07090E";
447
+ const hoverStroke = isLight ? "#0f172a" : "#f8fafc";
448
+ const defaultAccent = isLight ? "#0891b2" : "#22d3ee";
449
+ const totalWeight = visual.sectors.reduce((s, sec) => s + Math.max(0, sec.weight), 0) || 1;
450
+ let angle = -Math.PI / 2;
451
+ for (const sec of visual.sectors) {
452
+ const frac = Math.max(0, sec.weight) / totalWeight;
453
+ if (frac <= 5e-3) continue;
454
+ const nextAngle = angle + frac * Math.PI * 2;
455
+ const path = doc.createElementNS(SVG_NS, "path");
456
+ path.setAttribute(
457
+ "d",
458
+ describeDonutSectorPath(visual.innerRadius, visual.outerRadius, angle, nextAngle)
459
+ );
460
+ path.setAttribute("fill", sec.colorHex);
461
+ path.setAttribute("stroke", dividerStroke);
462
+ path.setAttribute("stroke-width", visual.sectors.length > 1 ? "1.1" : "0.8");
463
+ groupEl.appendChild(path);
464
+ angle = nextAngle;
465
+ }
466
+ const core = doc.createElementNS(SVG_NS, "circle");
467
+ core.setAttribute("cx", "0");
468
+ core.setAttribute("cy", "0");
469
+ core.setAttribute("r", String(Math.max(2, visual.innerRadius - 1)));
470
+ core.setAttribute("fill", visual.centerColorHex);
471
+ groupEl.appendChild(core);
472
+ if (visual.accentRingHex || state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
473
+ const ring = doc.createElementNS(SVG_NS, "circle");
474
+ ring.setAttribute("cx", "0");
475
+ ring.setAttribute("cy", "0");
476
+ ring.setAttribute("r", String(visual.outerRadius + 3));
477
+ ring.setAttribute("fill", "none");
478
+ ring.setAttribute(
479
+ "stroke",
480
+ state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? hoverStroke : visual.accentRingHex ?? defaultAccent
481
+ );
482
+ ring.setAttribute("stroke-width", state.interaction.isHovered ? "2" : "1.2");
483
+ groupEl.appendChild(ring);
484
+ }
485
+ }
486
+ };
487
+
488
+ // src/presets.ts
489
+ function extractDefaultItemLabel(item, fallbackId, getLabel) {
490
+ if (getLabel) {
491
+ const custom = getLabel(item);
492
+ if (custom.length > 0) return custom;
493
+ }
494
+ if (typeof item === "object" && item !== null) {
495
+ if ("label" in item && typeof item.label === "string" && item.label.length > 0) {
496
+ return item.label;
497
+ }
498
+ if ("title" in item && typeof item.title === "string" && item.title.length > 0) {
499
+ return item.title;
500
+ }
501
+ if ("headline" in item && typeof item.headline === "string" && item.headline.length > 0) {
502
+ return item.headline;
503
+ }
504
+ if ("name" in item && typeof item.name === "string" && item.name.length > 0) {
505
+ return item.name;
506
+ }
507
+ }
508
+ return fallbackId;
509
+ }
510
+ function createPresetViews(preset) {
511
+ const contentView = preset === "glyphs" ? new MultiSectorGlyphSvgView() : new GeometricShapeSvgView();
512
+ const clusterView = preset === "badges" ? new BadgeClusterSvgView() : new ClusterAnchorSvgView();
513
+ return { contentView, clusterView };
514
+ }
515
+ function createPresetVisualResolver(preset, getLabel, theme = "dark") {
516
+ return {
517
+ resolveContentVisual: (item, ctx) => {
518
+ if (preset === "glyphs") {
519
+ return {
520
+ outerRadius: 11.5,
521
+ innerRadius: 4.5,
522
+ sectors: ctx.unitClusters.map((uc) => ({
523
+ colorHex: uc.cluster.colorHex,
524
+ weight: uc.weight
525
+ })),
526
+ centerColorHex: ctx.mixedColorHex,
527
+ theme
528
+ };
529
+ }
530
+ const rank = ctx.unitClusters.length;
531
+ return {
532
+ shape: rank >= 3 ? "star" : rank === 2 ? "diamond" : "circle",
533
+ radius: 9.5,
534
+ fillHex: ctx.mixedColorHex,
535
+ strokeHex: theme === "light" ? "#ffffff" : "#07090E",
536
+ strokeWidth: 1.4,
537
+ label: extractDefaultItemLabel(item, ctx.id, getLabel),
538
+ theme
539
+ };
540
+ },
541
+ resolveClusterVisual: (cluster) => {
542
+ if (preset === "badges") {
543
+ const count = cluster.members.length;
544
+ return {
545
+ code: cluster.code,
546
+ title: cluster.defaultLabel,
547
+ metaLine: `${count} ${count === 1 ? "item" : "items"}`,
548
+ colorHex: cluster.colorHex,
549
+ theme
550
+ };
551
+ }
552
+ return {
553
+ code: cluster.code,
554
+ label: cluster.defaultLabel,
555
+ memberCount: cluster.members.length,
556
+ colorHex: cluster.colorHex,
557
+ radius: 22,
558
+ theme
559
+ };
560
+ },
561
+ getVisualCacheKey: (visual) => {
562
+ const t = visual.theme ?? "dark";
563
+ if ("sectors" in visual) {
564
+ const secKey = visual.sectors.map((s) => `${s.colorHex}:${s.weight.toFixed(2)}`).join("|");
565
+ return `glyph:${t}:${visual.outerRadius}:${visual.centerColorHex}:${secKey}`;
566
+ }
567
+ if ("shape" in visual) {
568
+ return `geom:${t}:${visual.shape}:${visual.radius}:${visual.fillHex}`;
569
+ }
570
+ if ("metaLine" in visual) {
571
+ return `badge:${t}:${visual.code}:${visual.title}:${visual.metaLine}:${visual.colorHex}`;
572
+ }
573
+ return `anchor:${t}:${visual.code}:${visual.memberCount}:${visual.colorHex}`;
574
+ }
575
+ };
576
+ }
92
577
  function interactionSignature(state) {
93
578
  const { isHovered, isPinned, isDimmed, isHighlighted } = state.interaction;
94
579
  return `${isHovered ? 1 : 0}${isPinned ? 1 : 0}${isDimmed ? 1 : 0}${isHighlighted ? 1 : 0}`;
@@ -211,7 +696,7 @@ var SvgRendererAdapter = class {
211
696
  }
212
697
  while (this.edgeLines.length < snapshot.edges.length) {
213
698
  const line = this.doc.createElementNS(SVG_NS, "line");
214
- line.setAttribute("stroke", "rgba(148, 163, 184, 0.16)");
699
+ line.setAttribute("stroke", "rgba(100, 116, 139, 0.26)");
215
700
  line.setAttribute("stroke-width", "1");
216
701
  line.setAttribute("stroke-dasharray", "3 4");
217
702
  this.edgesGroup.appendChild(line);
@@ -443,7 +928,24 @@ var SvgRendererAdapter = class {
443
928
  this.svgEl.removeChild(this.svgEl.firstChild);
444
929
  }
445
930
  }
446
- };
931
+ };
932
+ function resolveContainerElement(target) {
933
+ if (typeof target !== "string") {
934
+ return target;
935
+ }
936
+ if (typeof document === "undefined") {
937
+ throw new Error(
938
+ `SimplexSvgController: cannot resolve selector "${target}" outside a DOM environment.`
939
+ );
940
+ }
941
+ const el = document.querySelector(target);
942
+ if (!el) {
943
+ throw new Error(
944
+ `SimplexSvgController: no element matched container selector "${target}".`
945
+ );
946
+ }
947
+ return el;
948
+ }
447
949
  var SimplexSvgController = class _SimplexSvgController {
448
950
  containerEl;
449
951
  svgEl;
@@ -451,6 +953,15 @@ var SimplexSvgController = class _SimplexSvgController {
451
953
  engine;
452
954
  resizeObserver = null;
453
955
  onFpsUpdate;
956
+ focusOnClusterClick;
957
+ getIdExtractor;
958
+ getLabelExtractor;
959
+ tooltipOption;
960
+ tooltipEl = null;
961
+ focusedClusterUidState = null;
962
+ currentPreset;
963
+ currentTheme;
964
+ usesCustomVisualResolver;
454
965
  rafId = null;
455
966
  lastFrameTs = 0;
456
967
  fpsSamples = 0;
@@ -462,29 +973,44 @@ var SimplexSvgController = class _SimplexSvgController {
462
973
  dragStartY = 0;
463
974
  dragMoved = false;
464
975
  constructor(containerEl, options) {
465
- this.containerEl = containerEl;
976
+ this.containerEl = resolveContainerElement(containerEl);
466
977
  this.onFpsUpdate = options.onFpsUpdate;
467
- const doc = containerEl.ownerDocument;
978
+ this.focusOnClusterClick = options.focusOnClusterClick ?? true;
979
+ this.getLabelExtractor = options.getLabel;
980
+ this.tooltipOption = options.tooltip ?? false;
981
+ this.getIdExtractor = options.getId ?? ((item) => typeof item === "object" && item !== null && "id" in item ? String(item.id) : "");
982
+ this.currentPreset = options.preset ?? "geometric";
983
+ this.currentTheme = options.theme ?? "dark";
984
+ this.usesCustomVisualResolver = options.visualResolver !== void 0;
985
+ const presetViews = createPresetViews(this.currentPreset);
986
+ const contentView = options.contentView ?? presetViews.contentView;
987
+ const clusterView = options.clusterView ?? presetViews.clusterView;
988
+ const visualResolver = options.visualResolver ?? createPresetVisualResolver(
989
+ this.currentPreset,
990
+ this.getLabelExtractor,
991
+ this.currentTheme
992
+ );
993
+ const doc = this.containerEl.ownerDocument;
468
994
  this.svgEl = doc.createElementNS(SVG_NS, "svg");
469
995
  this.svgEl.setAttribute("width", "100%");
470
996
  this.svgEl.setAttribute("height", "100%");
471
997
  this.svgEl.style.display = "block";
472
998
  this.svgEl.style.touchAction = "none";
473
999
  this.svgEl.style.userSelect = "none";
474
- containerEl.appendChild(this.svgEl);
475
- const rect = containerEl.getBoundingClientRect();
1000
+ this.containerEl.appendChild(this.svgEl);
1001
+ const rect = this.containerEl.getBoundingClientRect();
476
1002
  const initialW = rect.width > 40 ? rect.width : 960;
477
1003
  const initialH = rect.height > 40 ? rect.height : 620;
478
1004
  this.adapter = new SvgRendererAdapter(this.svgEl, {
479
- contentView: options.contentView,
480
- clusterView: options.clusterView
1005
+ contentView,
1006
+ clusterView
481
1007
  });
482
1008
  this.engine = SimplexGraphEngine.createFromResolved({
483
1009
  items: options.items,
484
1010
  getId: options.getId,
485
1011
  getTags: options.getTags,
486
1012
  deriveClusterMeta: options.deriveClusterMeta,
487
- visualResolver: options.visualResolver,
1013
+ visualResolver,
488
1014
  renderer: this.adapter,
489
1015
  viewportWidth: initialW,
490
1016
  viewportHeight: initialH,
@@ -493,7 +1019,7 @@ var SimplexSvgController = class _SimplexSvgController {
493
1019
  meshOptions: options.meshOptions,
494
1020
  layoutOptions: options.layoutOptions,
495
1021
  showHulls: options.showHulls,
496
- interactions: options.interactions,
1022
+ interactions: this.wrapInteractions(options.interactions),
497
1023
  onMetricsChange: options.onMetricsChange,
498
1024
  onClustersChange: options.onClustersChange
499
1025
  });
@@ -509,13 +1035,125 @@ var SimplexSvgController = class _SimplexSvgController {
509
1035
  }
510
1036
  }
511
1037
  });
512
- this.resizeObserver.observe(containerEl);
1038
+ this.resizeObserver.observe(this.containerEl);
513
1039
  }
514
1040
  this.ensureLoopRunning();
515
1041
  }
516
1042
  static createFromResolved(containerEl, options) {
517
1043
  return new _SimplexSvgController(containerEl, options);
518
1044
  }
1045
+ wrapInteractions(userInteractions) {
1046
+ return {
1047
+ onContentHover: (ev) => {
1048
+ userInteractions?.onContentHover?.(ev);
1049
+ if (ev) {
1050
+ this.showTooltip(
1051
+ ev.screenPosition.x,
1052
+ ev.screenPosition.y,
1053
+ this.formatContentTooltip(ev)
1054
+ );
1055
+ } else {
1056
+ this.hideTooltip();
1057
+ }
1058
+ },
1059
+ onClusterHover: (ev) => {
1060
+ userInteractions?.onClusterHover?.(ev);
1061
+ if (ev) {
1062
+ this.showTooltip(
1063
+ ev.screenPosition.x,
1064
+ ev.screenPosition.y,
1065
+ this.formatClusterTooltip(ev)
1066
+ );
1067
+ } else {
1068
+ this.hideTooltip();
1069
+ }
1070
+ },
1071
+ onLeftClick: (target, rawEvent) => {
1072
+ if (userInteractions?.onLeftClick) {
1073
+ userInteractions.onLeftClick(target, rawEvent);
1074
+ return;
1075
+ }
1076
+ if (!this.focusOnClusterClick) return;
1077
+ if (target.kind === "cluster") {
1078
+ const next = this.focusedClusterUidState === target.cluster.clusterUid ? null : target.cluster.clusterUid;
1079
+ this.setFocusedClusterUid(next);
1080
+ } else if (target.kind === "background" && this.focusedClusterUidState !== null) {
1081
+ this.setFocusedClusterUid(null);
1082
+ }
1083
+ },
1084
+ onRightClick: userInteractions?.onRightClick,
1085
+ onWheel: userInteractions?.onWheel ?? "zoom-to-cursor"
1086
+ };
1087
+ }
1088
+ formatContentTooltip(ev) {
1089
+ if (typeof this.tooltipOption === "function") {
1090
+ return this.tooltipOption({ kind: "content", ...ev });
1091
+ }
1092
+ const label = extractDefaultItemLabel(
1093
+ ev.item,
1094
+ ev.layoutContext.id,
1095
+ this.getLabelExtractor
1096
+ );
1097
+ const tagList = ev.layoutContext.tags.map((t) => `#${t.tag}`).slice(0, 5).join(" \xB7 ");
1098
+ return tagList ? `${label}
1099
+ ${tagList}` : label;
1100
+ }
1101
+ formatClusterTooltip(ev) {
1102
+ if (typeof this.tooltipOption === "function") {
1103
+ return this.tooltipOption({ kind: "cluster", ...ev });
1104
+ }
1105
+ const count = ev.cluster.members.length;
1106
+ return `${ev.cluster.code} \xB7 ${ev.cluster.defaultLabel}
1107
+ ${count} ${count === 1 ? "item" : "items"}`;
1108
+ }
1109
+ applyTooltipThemeStyles(el) {
1110
+ const isLight = this.currentTheme === "light";
1111
+ el.style.background = isLight ? "rgba(255, 255, 255, 0.96)" : "rgba(9, 13, 22, 0.94)";
1112
+ el.style.border = isLight ? "1px solid rgba(203, 213, 225, 0.95)" : "1px solid rgba(51, 65, 85, 0.85)";
1113
+ el.style.color = isLight ? "#0f172a" : "#f1f5f9";
1114
+ el.style.boxShadow = isLight ? "0 10px 24px rgba(15, 23, 42, 0.12)" : "0 10px 24px rgba(0, 0, 0, 0.45)";
1115
+ }
1116
+ ensureTooltipElement() {
1117
+ if (!this.tooltipOption) return null;
1118
+ if (this.tooltipEl) return this.tooltipEl;
1119
+ const doc = this.containerEl.ownerDocument;
1120
+ const computedPos = typeof getComputedStyle !== "undefined" ? getComputedStyle(this.containerEl).position : this.containerEl.style.position;
1121
+ if (!computedPos || computedPos === "static") {
1122
+ this.containerEl.style.position = "relative";
1123
+ }
1124
+ const el = doc.createElement("div");
1125
+ el.className = "sv-tooltip";
1126
+ el.style.position = "absolute";
1127
+ el.style.pointerEvents = "none";
1128
+ el.style.zIndex = "20";
1129
+ el.style.display = "none";
1130
+ el.style.maxWidth = "240px";
1131
+ el.style.padding = "7px 10px";
1132
+ el.style.borderRadius = "8px";
1133
+ el.style.fontFamily = "Plus Jakarta Sans, ui-sans-serif, system-ui, sans-serif";
1134
+ el.style.fontSize = "12px";
1135
+ el.style.lineHeight = "1.4";
1136
+ el.style.whiteSpace = "pre-line";
1137
+ this.applyTooltipThemeStyles(el);
1138
+ this.containerEl.appendChild(el);
1139
+ this.tooltipEl = el;
1140
+ return el;
1141
+ }
1142
+ showTooltip(screenX, screenY, text) {
1143
+ const el = this.ensureTooltipElement();
1144
+ if (!el || !text) return;
1145
+ el.textContent = text;
1146
+ el.style.display = "block";
1147
+ const maxLeft = Math.max(8, (this.containerEl.clientWidth || 960) - 256);
1148
+ const maxTop = Math.max(8, (this.containerEl.clientHeight || 600) - 72);
1149
+ el.style.left = `${Math.min(Math.max(8, Math.round(screenX + 14)), maxLeft)}px`;
1150
+ el.style.top = `${Math.min(Math.max(8, Math.round(screenY + 14)), maxTop)}px`;
1151
+ }
1152
+ hideTooltip() {
1153
+ if (this.tooltipEl) {
1154
+ this.tooltipEl.style.display = "none";
1155
+ }
1156
+ }
519
1157
  get camera() {
520
1158
  return this.engine.camera;
521
1159
  }
@@ -543,6 +1181,57 @@ var SimplexSvgController = class _SimplexSvgController {
543
1181
  this.engine.setItems(items);
544
1182
  this.ensureLoopRunning();
545
1183
  }
1184
+ /**
1185
+ * Switches between built-in presets (`'geometric'`, `'glyphs'`, `'badges'`) with smooth
1186
+ * morph/crossfade transitions.
1187
+ */
1188
+ setPreset(preset, getLabel) {
1189
+ this.currentPreset = preset;
1190
+ if (getLabel !== void 0) {
1191
+ this.getLabelExtractor = getLabel;
1192
+ }
1193
+ const views = createPresetViews(preset);
1194
+ const resolver = createPresetVisualResolver(
1195
+ preset,
1196
+ this.getLabelExtractor,
1197
+ this.currentTheme
1198
+ );
1199
+ this.adapter.setViews({
1200
+ contentView: views.contentView,
1201
+ clusterView: views.clusterView
1202
+ });
1203
+ this.engine.setVisualResolver(
1204
+ resolver
1205
+ );
1206
+ this.ensureLoopRunning();
1207
+ }
1208
+ /**
1209
+ * Switches built-in preset & tooltip theme between `'dark'` and `'light'`.
1210
+ */
1211
+ setTheme(theme) {
1212
+ if (this.currentTheme === theme) return;
1213
+ this.currentTheme = theme;
1214
+ if (this.tooltipEl) {
1215
+ this.applyTooltipThemeStyles(this.tooltipEl);
1216
+ }
1217
+ if (!this.usesCustomVisualResolver) {
1218
+ const resolver = createPresetVisualResolver(
1219
+ this.currentPreset,
1220
+ this.getLabelExtractor,
1221
+ theme
1222
+ );
1223
+ this.engine.setVisualResolver(
1224
+ resolver
1225
+ );
1226
+ this.ensureLoopRunning();
1227
+ }
1228
+ }
1229
+ setTooltip(tooltip) {
1230
+ this.tooltipOption = tooltip;
1231
+ if (!tooltip) {
1232
+ this.hideTooltip();
1233
+ }
1234
+ }
546
1235
  setVisualResolver(resolver) {
547
1236
  this.engine.setVisualResolver(resolver);
548
1237
  this.ensureLoopRunning();
@@ -559,7 +1248,7 @@ var SimplexSvgController = class _SimplexSvgController {
559
1248
  this.ensureLoopRunning();
560
1249
  }
561
1250
  setInteractions(interactions) {
562
- this.engine.setInteractions(interactions);
1251
+ this.engine.setInteractions(this.wrapInteractions(interactions));
563
1252
  }
564
1253
  setPinnedIds(ids) {
565
1254
  this.engine.setPinnedIds(ids);
@@ -570,6 +1259,7 @@ var SimplexSvgController = class _SimplexSvgController {
570
1259
  this.ensureLoopRunning();
571
1260
  }
572
1261
  setFocusedClusterUid(clusterUid) {
1262
+ this.focusedClusterUidState = clusterUid;
573
1263
  this.engine.setFocusedClusterUid(clusterUid);
574
1264
  this.ensureLoopRunning();
575
1265
  }
@@ -611,6 +1301,7 @@ var SimplexSvgController = class _SimplexSvgController {
611
1301
  this.dragStartX = e.clientX;
612
1302
  this.dragStartY = e.clientY;
613
1303
  this.engine.camera.panBy(dx, dy);
1304
+ this.hideTooltip();
614
1305
  this.ensureLoopRunning();
615
1306
  }
616
1307
  return;
@@ -628,6 +1319,7 @@ var SimplexSvgController = class _SimplexSvgController {
628
1319
  };
629
1320
  onPointerLeave = () => {
630
1321
  this.isDraggingPan = false;
1322
+ this.hideTooltip();
631
1323
  this.engine.handlePointerLeave();
632
1324
  this.ensureLoopRunning();
633
1325
  };
@@ -678,444 +1370,89 @@ var SimplexSvgController = class _SimplexSvgController {
678
1370
  e.preventDefault();
679
1371
  this.ensureLoopRunning();
680
1372
  }
681
- };
682
- attachSvgListeners() {
683
- this.svgEl.addEventListener("pointerdown", this.onPointerDown);
684
- this.svgEl.addEventListener("pointermove", this.onPointerMove);
685
- this.svgEl.addEventListener("pointerup", this.onPointerUp);
686
- this.svgEl.addEventListener("pointerleave", this.onPointerLeave);
687
- this.svgEl.addEventListener("click", this.onClick);
688
- this.svgEl.addEventListener("contextmenu", this.onContextMenu);
689
- this.svgEl.addEventListener("wheel", this.onWheel, { passive: false });
690
- }
691
- detachSvgListeners() {
692
- this.svgEl.removeEventListener("pointerdown", this.onPointerDown);
693
- this.svgEl.removeEventListener("pointermove", this.onPointerMove);
694
- this.svgEl.removeEventListener("pointerup", this.onPointerUp);
695
- this.svgEl.removeEventListener("pointerleave", this.onPointerLeave);
696
- this.svgEl.removeEventListener("click", this.onClick);
697
- this.svgEl.removeEventListener("contextmenu", this.onContextMenu);
698
- this.svgEl.removeEventListener("wheel", this.onWheel);
699
- }
700
- ensureLoopRunning() {
701
- if (this.destroyed || this.rafId !== null) return;
702
- if (typeof requestAnimationFrame === "undefined") {
703
- this.engine.renderNow();
704
- return;
705
- }
706
- this.lastFrameTs = performance.now();
707
- const tick = (now) => {
708
- if (this.destroyed) return;
709
- const dtMs = Math.max(1, Math.min(50, now - this.lastFrameTs));
710
- this.lastFrameTs = now;
711
- this.fpsSamples += 1;
712
- this.fpsAccumMs += dtMs;
713
- if (this.fpsAccumMs >= 320) {
714
- const fps = Math.round(this.fpsSamples * 1e3 / this.fpsAccumMs);
715
- this.onFpsUpdate?.(fps);
716
- this.fpsSamples = 0;
717
- this.fpsAccumMs = 0;
718
- }
719
- const settled = this.engine.step(dtMs);
720
- if (!settled) {
721
- this.rafId = requestAnimationFrame(tick);
722
- } else {
723
- this.rafId = null;
724
- }
725
- };
726
- this.rafId = requestAnimationFrame(tick);
727
- }
728
- destroy() {
729
- if (this.destroyed) return;
730
- this.destroyed = true;
731
- if (this.rafId !== null && typeof cancelAnimationFrame !== "undefined") {
732
- cancelAnimationFrame(this.rafId);
733
- this.rafId = null;
734
- }
735
- this.resizeObserver?.disconnect();
736
- this.detachSvgListeners();
737
- this.engine.destroy();
738
- if (this.svgEl.parentElement === this.containerEl) {
739
- this.containerEl.removeChild(this.svgEl);
740
- }
741
- }
742
- };
743
- var MORPH_SAMPLES = 64;
744
- function sampleShapePerimeter(shape, radius) {
745
- const r = Math.max(3, radius);
746
- const pts = [];
747
- if (shape === "circle") {
748
- for (let i = 0; i < MORPH_SAMPLES; i++) {
749
- const a = i / MORPH_SAMPLES * Math.PI * 2 - Math.PI / 2;
750
- pts.push({ x: Math.cos(a) * r, y: Math.sin(a) * r });
751
- }
752
- return pts;
753
- }
754
- const polyVerts = [];
755
- if (shape === "diamond") {
756
- const d = r * 1.12;
757
- polyVerts.push(
758
- { x: 0, y: -d },
759
- { x: d * 0.88, y: 0 },
760
- { x: 0, y: d },
761
- { x: -d * 0.88, y: 0 }
762
- );
763
- } else if (shape === "hexagon") {
764
- for (let i = 0; i < 6; i++) {
765
- const a = -Math.PI / 2 + i * Math.PI * 2 / 6;
766
- polyVerts.push({ x: Math.cos(a) * r * 1.05, y: Math.sin(a) * r * 1.05 });
767
- }
768
- } else {
769
- const outer = r * 1.24;
770
- const inner = r * 0.56;
771
- for (let i = 0; i < 10; i++) {
772
- const a = -Math.PI / 2 + i * Math.PI / 5;
773
- const rad = i % 2 === 0 ? outer : inner;
774
- polyVerts.push({ x: Math.cos(a) * rad, y: Math.sin(a) * rad });
775
- }
776
- }
777
- const edgeLens = [];
778
- let totalLen = 0;
779
- for (let i = 0; i < polyVerts.length; i++) {
780
- const a = polyVerts[i];
781
- const b = polyVerts[(i + 1) % polyVerts.length];
782
- const len = Math.hypot(b.x - a.x, b.y - a.y);
783
- edgeLens.push(len);
784
- totalLen += len;
785
- }
786
- for (let s = 0; s < MORPH_SAMPLES; s++) {
787
- const targetDist = s / MORPH_SAMPLES * totalLen;
788
- let accum = 0;
789
- for (let i = 0; i < polyVerts.length; i++) {
790
- const len = edgeLens[i];
791
- if (accum + len >= targetDist || i === polyVerts.length - 1) {
792
- const localT = len > 1e-6 ? (targetDist - accum) / len : 0;
793
- const a = polyVerts[i];
794
- const b = polyVerts[(i + 1) % polyVerts.length];
795
- pts.push({
796
- x: a.x + (b.x - a.x) * localT,
797
- y: a.y + (b.y - a.y) * localT
798
- });
799
- break;
800
- }
801
- accum += len;
802
- }
803
- }
804
- return pts;
805
- }
806
- function pointsToSvgPath(pts) {
807
- if (pts.length === 0) return "";
808
- let d = `M ${pts[0].x.toFixed(2)} ${pts[0].y.toFixed(2)}`;
809
- for (let i = 1; i < pts.length; i++) {
810
- const p = pts[i];
811
- d += ` L ${p.x.toFixed(2)} ${p.y.toFixed(2)}`;
812
- }
813
- return `${d} Z`;
814
- }
815
- function parseHexRgb(hex) {
816
- const clean = hex.startsWith("#") ? hex.slice(1) : hex;
817
- if (clean.length < 6) return [148, 163, 184];
818
- return [
819
- parseInt(clean.slice(0, 2), 16) || 0,
820
- parseInt(clean.slice(2, 4), 16) || 0,
821
- parseInt(clean.slice(4, 6), 16) || 0
822
- ];
823
- }
824
- function lerpHex(fromHex, toHex, t) {
825
- const a = parseHexRgb(fromHex);
826
- const b = parseHexRgb(toHex);
827
- const r = Math.round(a[0] + (b[0] - a[0]) * t);
828
- const g = Math.round(a[1] + (b[1] - a[1]) * t);
829
- const bl = Math.round(a[2] + (b[2] - a[2]) * t);
830
- return `rgb(${r}, ${g}, ${bl})`;
831
- }
832
- var GeometricShapeSvgView = class {
833
- measureConvexHull(visual) {
834
- const r = visual.radius + (visual.strokeWidth ?? 1.5);
835
- if (visual.shape === "circle") {
836
- return { kind: "circle", radius: r };
837
- }
838
- if (visual.shape === "diamond") {
839
- const d = r * 1.12;
840
- return {
841
- kind: "polygon",
842
- vertices: computeConvexHull([
843
- { x: 0, y: -d },
844
- { x: d * 0.88, y: 0 },
845
- { x: 0, y: d },
846
- { x: -d * 0.88, y: 0 }
847
- ])
848
- };
849
- }
850
- if (visual.shape === "hexagon") {
851
- return regularPolygon(6, r * 1.05);
852
- }
853
- return starConvexHull(5, r * 1.24);
854
- }
855
- ensureElements(groupEl) {
856
- let pathEl = groupEl.querySelector("path.sv-geom-shape");
857
- let ringEl = groupEl.querySelector("path.sv-geom-ring");
858
- if (!pathEl || !ringEl) {
859
- while (groupEl.firstChild) groupEl.removeChild(groupEl.firstChild);
860
- const doc = groupEl.ownerDocument;
861
- ringEl = doc.createElementNS(SVG_NS, "path");
862
- ringEl.setAttribute("class", "sv-geom-ring");
863
- ringEl.setAttribute("fill", "none");
864
- pathEl = doc.createElementNS(SVG_NS, "path");
865
- pathEl.setAttribute("class", "sv-geom-shape");
866
- groupEl.appendChild(ringEl);
867
- groupEl.appendChild(pathEl);
868
- }
869
- return { pathEl, ringEl };
870
- }
871
- mountOrUpdate(groupEl, visual, state) {
872
- const { pathEl, ringEl } = this.ensureElements(groupEl);
873
- const pts = sampleShapePerimeter(visual.shape, visual.radius);
874
- const d = pointsToSvgPath(pts);
875
- pathEl.setAttribute("d", d);
876
- pathEl.setAttribute("fill", visual.fillHex);
877
- pathEl.setAttribute(
878
- "stroke",
879
- state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? "#f8fafc" : visual.strokeHex ?? "rgba(15, 23, 42, 0.8)"
880
- );
881
- pathEl.setAttribute(
882
- "stroke-width",
883
- String(state.interaction.isHovered || state.interaction.isPinned ? 2.2 : visual.strokeWidth ?? 1.4)
884
- );
885
- if (state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
886
- const outerPts = sampleShapePerimeter(visual.shape, visual.radius + 4.5);
887
- ringEl.setAttribute("d", pointsToSvgPath(outerPts));
888
- ringEl.setAttribute("stroke", state.interaction.isPinned ? "#f59e0b" : "#22d3ee");
889
- ringEl.setAttribute("stroke-width", "1.4");
890
- ringEl.setAttribute("stroke-opacity", "0.9");
891
- } else {
892
- ringEl.setAttribute("d", "");
893
- }
1373
+ };
1374
+ attachSvgListeners() {
1375
+ this.svgEl.addEventListener("pointerdown", this.onPointerDown);
1376
+ this.svgEl.addEventListener("pointermove", this.onPointerMove);
1377
+ this.svgEl.addEventListener("pointerup", this.onPointerUp);
1378
+ this.svgEl.addEventListener("pointerleave", this.onPointerLeave);
1379
+ this.svgEl.addEventListener("click", this.onClick);
1380
+ this.svgEl.addEventListener("contextmenu", this.onContextMenu);
1381
+ this.svgEl.addEventListener("wheel", this.onWheel, { passive: false });
894
1382
  }
895
- morph(groupEl, from, to, progress, state) {
896
- const { pathEl, ringEl } = this.ensureElements(groupEl);
897
- const ptsA = sampleShapePerimeter(from.shape, from.radius);
898
- const ptsB = sampleShapePerimeter(to.shape, to.radius);
899
- const morphed = [];
900
- for (let i = 0; i < MORPH_SAMPLES; i++) {
901
- const a = ptsA[i];
902
- const b = ptsB[i];
903
- morphed.push({
904
- x: a.x + (b.x - a.x) * progress,
905
- y: a.y + (b.y - a.y) * progress
906
- });
907
- }
908
- pathEl.setAttribute("d", pointsToSvgPath(morphed));
909
- pathEl.setAttribute("fill", lerpHex(from.fillHex, to.fillHex, progress));
910
- pathEl.setAttribute(
911
- "stroke",
912
- state.interaction.isHovered ? "#f8fafc" : to.strokeHex ?? "rgba(15, 23, 42, 0.8)"
913
- );
914
- pathEl.setAttribute("stroke-width", String(to.strokeWidth ?? 1.4));
915
- ringEl.setAttribute("d", "");
916
- return true;
1383
+ detachSvgListeners() {
1384
+ this.svgEl.removeEventListener("pointerdown", this.onPointerDown);
1385
+ this.svgEl.removeEventListener("pointermove", this.onPointerMove);
1386
+ this.svgEl.removeEventListener("pointerup", this.onPointerUp);
1387
+ this.svgEl.removeEventListener("pointerleave", this.onPointerLeave);
1388
+ this.svgEl.removeEventListener("click", this.onClick);
1389
+ this.svgEl.removeEventListener("contextmenu", this.onContextMenu);
1390
+ this.svgEl.removeEventListener("wheel", this.onWheel);
917
1391
  }
918
- };
919
-
920
- // src/views/MultiSectorGlyphSvgView.ts
921
- function describeDonutSectorPath(innerR, outerR, startAngle, endAngle) {
922
- const sweep = endAngle - startAngle;
923
- if (sweep >= Math.PI * 2 - 1e-4) {
924
- return [
925
- `M ${outerR} 0`,
926
- `A ${outerR} ${outerR} 0 1 0 ${-outerR} 0`,
927
- `A ${outerR} ${outerR} 0 1 0 ${outerR} 0`,
928
- `M ${innerR} 0`,
929
- `A ${innerR} ${innerR} 0 1 1 ${-innerR} 0`,
930
- `A ${innerR} ${innerR} 0 1 1 ${innerR} 0`,
931
- "Z"
932
- ].join(" ");
1392
+ ensureLoopRunning() {
1393
+ if (this.destroyed || this.rafId !== null) return;
1394
+ if (typeof requestAnimationFrame === "undefined") {
1395
+ this.engine.renderNow();
1396
+ return;
1397
+ }
1398
+ this.lastFrameTs = performance.now();
1399
+ const tick = (now) => {
1400
+ if (this.destroyed) return;
1401
+ const dtMs = Math.max(1, Math.min(50, now - this.lastFrameTs));
1402
+ this.lastFrameTs = now;
1403
+ this.fpsSamples += 1;
1404
+ this.fpsAccumMs += dtMs;
1405
+ if (this.fpsAccumMs >= 320) {
1406
+ const fps = Math.round(this.fpsSamples * 1e3 / this.fpsAccumMs);
1407
+ this.onFpsUpdate?.(fps);
1408
+ this.fpsSamples = 0;
1409
+ this.fpsAccumMs = 0;
1410
+ }
1411
+ const settled = this.engine.step(dtMs);
1412
+ if (!settled) {
1413
+ this.rafId = requestAnimationFrame(tick);
1414
+ } else {
1415
+ this.rafId = null;
1416
+ }
1417
+ };
1418
+ this.rafId = requestAnimationFrame(tick);
933
1419
  }
934
- const largeArc = sweep > Math.PI ? 1 : 0;
935
- const x1o = Math.cos(startAngle) * outerR;
936
- const y1o = Math.sin(startAngle) * outerR;
937
- const x2o = Math.cos(endAngle) * outerR;
938
- const y2o = Math.sin(endAngle) * outerR;
939
- const x1i = Math.cos(endAngle) * innerR;
940
- const y1i = Math.sin(endAngle) * innerR;
941
- const x2i = Math.cos(startAngle) * innerR;
942
- const y2i = Math.sin(startAngle) * innerR;
943
- return [
944
- `M ${x1o.toFixed(2)} ${y1o.toFixed(2)}`,
945
- `A ${outerR.toFixed(2)} ${outerR.toFixed(2)} 0 ${largeArc} 1 ${x2o.toFixed(2)} ${y2o.toFixed(2)}`,
946
- `L ${x1i.toFixed(2)} ${y1i.toFixed(2)}`,
947
- `A ${innerR.toFixed(2)} ${innerR.toFixed(2)} 0 ${largeArc} 0 ${x2i.toFixed(2)} ${y2i.toFixed(2)}`,
948
- "Z"
949
- ].join(" ");
950
- }
951
- var MultiSectorGlyphSvgView = class {
952
- measureConvexHull(visual) {
953
- return { kind: "circle", radius: visual.outerRadius + 2 };
1420
+ /** Helper accessor matching `getId` when external callers inspect items. */
1421
+ getItemId(item) {
1422
+ return this.getIdExtractor(item);
954
1423
  }
955
- mountOrUpdate(groupEl, visual, state) {
956
- const doc = groupEl.ownerDocument;
957
- while (groupEl.firstChild) {
958
- groupEl.removeChild(groupEl.firstChild);
959
- }
960
- const totalWeight = visual.sectors.reduce((s, sec) => s + Math.max(0, sec.weight), 0) || 1;
961
- let angle = -Math.PI / 2;
962
- for (const sec of visual.sectors) {
963
- const frac = Math.max(0, sec.weight) / totalWeight;
964
- if (frac <= 5e-3) continue;
965
- const nextAngle = angle + frac * Math.PI * 2;
966
- const path = doc.createElementNS(SVG_NS, "path");
967
- path.setAttribute(
968
- "d",
969
- describeDonutSectorPath(visual.innerRadius, visual.outerRadius, angle, nextAngle)
970
- );
971
- path.setAttribute("fill", sec.colorHex);
972
- path.setAttribute("stroke", "#07090E");
973
- path.setAttribute("stroke-width", visual.sectors.length > 1 ? "1.1" : "0.8");
974
- groupEl.appendChild(path);
975
- angle = nextAngle;
1424
+ destroy() {
1425
+ if (this.destroyed) return;
1426
+ this.destroyed = true;
1427
+ if (this.rafId !== null && typeof cancelAnimationFrame !== "undefined") {
1428
+ cancelAnimationFrame(this.rafId);
1429
+ this.rafId = null;
976
1430
  }
977
- const core = doc.createElementNS(SVG_NS, "circle");
978
- core.setAttribute("cx", "0");
979
- core.setAttribute("cy", "0");
980
- core.setAttribute("r", String(Math.max(2, visual.innerRadius - 1)));
981
- core.setAttribute("fill", visual.centerColorHex);
982
- groupEl.appendChild(core);
983
- if (visual.accentRingHex || state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
984
- const ring = doc.createElementNS(SVG_NS, "circle");
985
- ring.setAttribute("cx", "0");
986
- ring.setAttribute("cy", "0");
987
- ring.setAttribute("r", String(visual.outerRadius + 3));
988
- ring.setAttribute("fill", "none");
989
- ring.setAttribute(
990
- "stroke",
991
- state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? "#f8fafc" : visual.accentRingHex ?? "#22d3ee"
992
- );
993
- ring.setAttribute("stroke-width", state.interaction.isHovered ? "2" : "1.2");
994
- groupEl.appendChild(ring);
1431
+ this.resizeObserver?.disconnect();
1432
+ this.detachSvgListeners();
1433
+ this.engine.destroy();
1434
+ if (this.tooltipEl && this.tooltipEl.parentElement === this.containerEl) {
1435
+ this.containerEl.removeChild(this.tooltipEl);
1436
+ this.tooltipEl = null;
995
1437
  }
996
- }
997
- };
998
-
999
- // src/views/BadgeClusterSvgView.ts
1000
- var BadgeClusterSvgView = class {
1001
- mountOrUpdate(groupEl, visual, state) {
1002
- const doc = groupEl.ownerDocument;
1003
- while (groupEl.firstChild) {
1004
- groupEl.removeChild(groupEl.firstChild);
1438
+ if (this.svgEl.parentElement === this.containerEl) {
1439
+ this.containerEl.removeChild(this.svgEl);
1005
1440
  }
1006
- const titleLen = visual.title.length;
1007
- const metaLen = visual.metaLine.length;
1008
- const estWidth = Math.max(122, Math.max(titleLen * 6.8 + 44, metaLen * 5.9 + 24));
1009
- const height = 42;
1010
- const halfW = estWidth / 2;
1011
- const bg = doc.createElementNS(SVG_NS, "rect");
1012
- bg.setAttribute("x", String(-halfW));
1013
- bg.setAttribute("y", String(-21));
1014
- bg.setAttribute("width", String(estWidth));
1015
- bg.setAttribute("height", String(height));
1016
- bg.setAttribute("rx", "8");
1017
- bg.setAttribute("fill", "#0d1320");
1018
- bg.setAttribute("fill-opacity", "0.94");
1019
- bg.setAttribute(
1020
- "stroke",
1021
- state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered || state.interaction.isHighlighted ? "#e2e8f0" : visual.colorHex
1022
- );
1023
- bg.setAttribute(
1024
- "stroke-width",
1025
- state.interaction.isHovered || state.interaction.isHighlighted ? "2.2" : "1.4"
1026
- );
1027
- groupEl.appendChild(bg);
1028
- const swatch = doc.createElementNS(SVG_NS, "circle");
1029
- swatch.setAttribute("cx", String(-halfW + 13));
1030
- swatch.setAttribute("cy", String(-7));
1031
- swatch.setAttribute("r", "4");
1032
- swatch.setAttribute("fill", visual.colorHex);
1033
- groupEl.appendChild(swatch);
1034
- const codeText = doc.createElementNS(SVG_NS, "text");
1035
- codeText.setAttribute("x", String(-halfW + 22));
1036
- codeText.setAttribute("y", String(-3));
1037
- codeText.setAttribute("fill", visual.colorHex);
1038
- codeText.setAttribute("font-size", "10");
1039
- codeText.setAttribute("font-weight", "600");
1040
- codeText.setAttribute("font-family", "IBM Plex Mono, monospace");
1041
- codeText.textContent = visual.code;
1042
- groupEl.appendChild(codeText);
1043
- const titleText = doc.createElementNS(SVG_NS, "text");
1044
- titleText.setAttribute("x", String(-halfW + 44));
1045
- titleText.setAttribute("y", String(-3));
1046
- titleText.setAttribute("fill", "#f1f5f9");
1047
- titleText.setAttribute("font-size", "11");
1048
- titleText.setAttribute("font-weight", "600");
1049
- titleText.setAttribute("font-family", "Plus Jakarta Sans, sans-serif");
1050
- titleText.textContent = visual.title;
1051
- groupEl.appendChild(titleText);
1052
- const subText = doc.createElementNS(SVG_NS, "text");
1053
- subText.setAttribute("x", "0");
1054
- subText.setAttribute("y", "13");
1055
- subText.setAttribute("text-anchor", "middle");
1056
- subText.setAttribute("fill", "#94a3b8");
1057
- subText.setAttribute("font-size", "9.5");
1058
- subText.setAttribute("font-family", "IBM Plex Mono, monospace");
1059
- subText.textContent = visual.metaLine;
1060
- groupEl.appendChild(subText);
1061
1441
  }
1062
1442
  };
1063
-
1064
- // src/views/ClusterAnchorSvgView.ts
1065
- var ClusterAnchorSvgView = class {
1066
- measureConvexHull(visual) {
1067
- return { kind: "circle", radius: visual.radius + 5 };
1068
- }
1069
- mountOrUpdate(groupEl, visual, state) {
1070
- const doc = groupEl.ownerDocument;
1071
- while (groupEl.firstChild) {
1072
- groupEl.removeChild(groupEl.firstChild);
1073
- }
1074
- const r = visual.radius;
1075
- const outer = doc.createElementNS(SVG_NS, "circle");
1076
- outer.setAttribute("cx", "0");
1077
- outer.setAttribute("cy", "0");
1078
- outer.setAttribute("r", String(r + 3));
1079
- outer.setAttribute("fill", "#07090E");
1080
- outer.setAttribute("fill-opacity", "0.88");
1081
- outer.setAttribute(
1082
- "stroke",
1083
- state.interaction.isHovered || state.interaction.isHighlighted ? "#f8fafc" : visual.colorHex
1084
- );
1085
- outer.setAttribute("stroke-width", state.interaction.isHovered ? "2.4" : "1.6");
1086
- outer.setAttribute("stroke-dasharray", "4 2.5");
1087
- groupEl.appendChild(outer);
1088
- const inner = doc.createElementNS(SVG_NS, "circle");
1089
- inner.setAttribute("cx", "0");
1090
- inner.setAttribute("cy", "0");
1091
- inner.setAttribute("r", String(r - 2));
1092
- inner.setAttribute("fill", visual.colorHex);
1093
- inner.setAttribute("fill-opacity", "0.18");
1094
- inner.setAttribute("stroke", visual.colorHex);
1095
- inner.setAttribute("stroke-width", "1.4");
1096
- groupEl.appendChild(inner);
1097
- const codeEl = doc.createElementNS(SVG_NS, "text");
1098
- codeEl.setAttribute("x", "0");
1099
- codeEl.setAttribute("y", "-2");
1100
- codeEl.setAttribute("text-anchor", "middle");
1101
- codeEl.setAttribute("fill", "#f8fafc");
1102
- codeEl.setAttribute("font-size", "11");
1103
- codeEl.setAttribute("font-weight", "600");
1104
- codeEl.setAttribute("font-family", "IBM Plex Mono, monospace");
1105
- codeEl.textContent = visual.code;
1106
- groupEl.appendChild(codeEl);
1107
- const countEl = doc.createElementNS(SVG_NS, "text");
1108
- countEl.setAttribute("x", "0");
1109
- countEl.setAttribute("y", "11");
1110
- countEl.setAttribute("text-anchor", "middle");
1111
- countEl.setAttribute("fill", visual.colorHex);
1112
- countEl.setAttribute("font-size", "9");
1113
- countEl.setAttribute("font-family", "IBM Plex Mono, monospace");
1114
- countEl.textContent = `${visual.memberCount}n`;
1115
- groupEl.appendChild(countEl);
1443
+ function createSimplexSvg(target, itemsOrOptions) {
1444
+ if (Array.isArray(itemsOrOptions)) {
1445
+ return SimplexSvgController.createFromResolved(target, {
1446
+ items: itemsOrOptions,
1447
+ tooltip: true
1448
+ });
1116
1449
  }
1117
- };
1450
+ return new SimplexSvgController(
1451
+ target,
1452
+ itemsOrOptions
1453
+ );
1454
+ }
1118
1455
 
1119
- export { BadgeClusterSvgView, ClusterAnchorSvgView, GeometricShapeSvgView, MultiSectorGlyphSvgView, SVG_NS, SimplexSvgController, SvgRendererAdapter, measureVisualWithSvgView };
1456
+ export { BadgeClusterSvgView, ClusterAnchorSvgView, GeometricShapeSvgView, MultiSectorGlyphSvgView, SVG_NS, SimplexSvgController, SvgRendererAdapter, createPresetViews, createPresetVisualResolver, createSimplexSvg, extractDefaultItemLabel, measureVisualWithSvgView };
1120
1457
  //# sourceMappingURL=index.js.map
1121
1458
  //# sourceMappingURL=index.js.map