@simplex-viz/svg 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -3
- package/dist/SimplexSvgController.d.ts +32 -2
- package/dist/SimplexSvgController.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +242 -47
- package/dist/index.js.map +1 -1
- package/dist/presets.d.ts +2 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/views/BadgeClusterSvgView.d.ts +1 -0
- package/dist/views/BadgeClusterSvgView.d.ts.map +1 -1
- package/dist/views/ClusterAnchorSvgView.d.ts +1 -0
- package/dist/views/ClusterAnchorSvgView.d.ts.map +1 -1
- package/dist/views/GeometricShapeSvgView.d.ts +1 -0
- package/dist/views/GeometricShapeSvgView.d.ts.map +1 -1
- package/dist/views/MultiSectorGlyphSvgView.d.ts +1 -0
- package/dist/views/MultiSectorGlyphSvgView.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { rectToConvexPolygon, computeConvexHull, regularPolygon, starConvexHull, getHullBoundingRadius, SimplexGraphEngine } from '@simplex-viz/core';
|
|
2
|
-
export { computeConvexHull, rectToConvexPolygon, regularPolygon, solveSimplexSnapshot, starConvexHull } from '@simplex-viz/core';
|
|
2
|
+
export { computeConvexHull, createAdaptiveHexMeshStrategy, createFixedLatticeMeshStrategy, createIncrementalAnchorClusteringStrategy, createJaccardAnnealingClusteringStrategy, createLocalAdaptiveLayoutStrategy, createOklabContrastColorStrategy, createPaletteColorStrategy, createPbdUniformLayoutStrategy, createThemeAdaptiveOklabColorStrategy, createWarmStartPbdLayoutStrategy, rectToConvexPolygon, regularPolygon, solveSimplexSnapshot, starConvexHull } from '@simplex-viz/core';
|
|
3
3
|
|
|
4
4
|
// src/hullMeasure.ts
|
|
5
5
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
@@ -98,6 +98,11 @@ var BadgeClusterSvgView = class {
|
|
|
98
98
|
while (groupEl.firstChild) {
|
|
99
99
|
groupEl.removeChild(groupEl.firstChild);
|
|
100
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";
|
|
101
106
|
const titleLen = visual.title.length;
|
|
102
107
|
const metaLen = visual.metaLine.length;
|
|
103
108
|
const estWidth = Math.max(122, Math.max(titleLen * 6.8 + 44, metaLen * 5.9 + 24));
|
|
@@ -109,11 +114,11 @@ var BadgeClusterSvgView = class {
|
|
|
109
114
|
bg.setAttribute("width", String(estWidth));
|
|
110
115
|
bg.setAttribute("height", String(height));
|
|
111
116
|
bg.setAttribute("rx", "8");
|
|
112
|
-
bg.setAttribute("fill",
|
|
113
|
-
bg.setAttribute("fill-opacity", "0.94");
|
|
117
|
+
bg.setAttribute("fill", cardFill);
|
|
118
|
+
bg.setAttribute("fill-opacity", isLight ? "0.97" : "0.94");
|
|
114
119
|
bg.setAttribute(
|
|
115
120
|
"stroke",
|
|
116
|
-
state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered || state.interaction.isHighlighted ?
|
|
121
|
+
state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered || state.interaction.isHighlighted ? hoverStroke : visual.colorHex
|
|
117
122
|
);
|
|
118
123
|
bg.setAttribute(
|
|
119
124
|
"stroke-width",
|
|
@@ -129,7 +134,7 @@ var BadgeClusterSvgView = class {
|
|
|
129
134
|
const codeText = doc.createElementNS(SVG_NS, "text");
|
|
130
135
|
codeText.setAttribute("x", String(-halfW + 22));
|
|
131
136
|
codeText.setAttribute("y", String(-3));
|
|
132
|
-
codeText.setAttribute("fill", visual.colorHex);
|
|
137
|
+
codeText.setAttribute("fill", isLight ? "#0f172a" : visual.colorHex);
|
|
133
138
|
codeText.setAttribute("font-size", "10");
|
|
134
139
|
codeText.setAttribute("font-weight", "600");
|
|
135
140
|
codeText.setAttribute("font-family", "IBM Plex Mono, monospace");
|
|
@@ -138,7 +143,7 @@ var BadgeClusterSvgView = class {
|
|
|
138
143
|
const titleText = doc.createElementNS(SVG_NS, "text");
|
|
139
144
|
titleText.setAttribute("x", String(-halfW + 44));
|
|
140
145
|
titleText.setAttribute("y", String(-3));
|
|
141
|
-
titleText.setAttribute("fill",
|
|
146
|
+
titleText.setAttribute("fill", titleFill);
|
|
142
147
|
titleText.setAttribute("font-size", "11");
|
|
143
148
|
titleText.setAttribute("font-weight", "600");
|
|
144
149
|
titleText.setAttribute("font-family", "Plus Jakarta Sans, sans-serif");
|
|
@@ -148,7 +153,7 @@ var BadgeClusterSvgView = class {
|
|
|
148
153
|
subText.setAttribute("x", "0");
|
|
149
154
|
subText.setAttribute("y", "13");
|
|
150
155
|
subText.setAttribute("text-anchor", "middle");
|
|
151
|
-
subText.setAttribute("fill",
|
|
156
|
+
subText.setAttribute("fill", metaFill);
|
|
152
157
|
subText.setAttribute("font-size", "9.5");
|
|
153
158
|
subText.setAttribute("font-family", "IBM Plex Mono, monospace");
|
|
154
159
|
subText.textContent = visual.metaLine;
|
|
@@ -166,16 +171,20 @@ var ClusterAnchorSvgView = class {
|
|
|
166
171
|
while (groupEl.firstChild) {
|
|
167
172
|
groupEl.removeChild(groupEl.firstChild);
|
|
168
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";
|
|
169
178
|
const r = visual.radius;
|
|
170
179
|
const outer = doc.createElementNS(SVG_NS, "circle");
|
|
171
180
|
outer.setAttribute("cx", "0");
|
|
172
181
|
outer.setAttribute("cy", "0");
|
|
173
182
|
outer.setAttribute("r", String(r + 3));
|
|
174
|
-
outer.setAttribute("fill",
|
|
175
|
-
outer.setAttribute("fill-opacity", "0.88");
|
|
183
|
+
outer.setAttribute("fill", outerFill);
|
|
184
|
+
outer.setAttribute("fill-opacity", isLight ? "0.96" : "0.88");
|
|
176
185
|
outer.setAttribute(
|
|
177
186
|
"stroke",
|
|
178
|
-
state.interaction.isHovered || state.interaction.isHighlighted ?
|
|
187
|
+
state.interaction.isHovered || state.interaction.isHighlighted ? hoverStroke : visual.colorHex
|
|
179
188
|
);
|
|
180
189
|
outer.setAttribute("stroke-width", state.interaction.isHovered ? "2.4" : "1.6");
|
|
181
190
|
outer.setAttribute("stroke-dasharray", "4 2.5");
|
|
@@ -185,7 +194,7 @@ var ClusterAnchorSvgView = class {
|
|
|
185
194
|
inner.setAttribute("cy", "0");
|
|
186
195
|
inner.setAttribute("r", String(r - 2));
|
|
187
196
|
inner.setAttribute("fill", visual.colorHex);
|
|
188
|
-
inner.setAttribute("fill-opacity", "0.18");
|
|
197
|
+
inner.setAttribute("fill-opacity", isLight ? "0.14" : "0.18");
|
|
189
198
|
inner.setAttribute("stroke", visual.colorHex);
|
|
190
199
|
inner.setAttribute("stroke-width", "1.4");
|
|
191
200
|
groupEl.appendChild(inner);
|
|
@@ -193,7 +202,7 @@ var ClusterAnchorSvgView = class {
|
|
|
193
202
|
codeEl.setAttribute("x", "0");
|
|
194
203
|
codeEl.setAttribute("y", "-2");
|
|
195
204
|
codeEl.setAttribute("text-anchor", "middle");
|
|
196
|
-
codeEl.setAttribute("fill",
|
|
205
|
+
codeEl.setAttribute("fill", codeFill);
|
|
197
206
|
codeEl.setAttribute("font-size", "11");
|
|
198
207
|
codeEl.setAttribute("font-weight", "600");
|
|
199
208
|
codeEl.setAttribute("font-family", "IBM Plex Mono, monospace");
|
|
@@ -203,8 +212,9 @@ var ClusterAnchorSvgView = class {
|
|
|
203
212
|
countEl.setAttribute("x", "0");
|
|
204
213
|
countEl.setAttribute("y", "11");
|
|
205
214
|
countEl.setAttribute("text-anchor", "middle");
|
|
206
|
-
countEl.setAttribute("fill", visual.colorHex);
|
|
215
|
+
countEl.setAttribute("fill", isLight ? "#334155" : visual.colorHex);
|
|
207
216
|
countEl.setAttribute("font-size", "9");
|
|
217
|
+
countEl.setAttribute("font-weight", isLight ? "600" : "400");
|
|
208
218
|
countEl.setAttribute("font-family", "IBM Plex Mono, monospace");
|
|
209
219
|
countEl.textContent = `${visual.memberCount}n`;
|
|
210
220
|
groupEl.appendChild(countEl);
|
|
@@ -342,11 +352,15 @@ var GeometricShapeSvgView = class {
|
|
|
342
352
|
const { pathEl, ringEl } = this.ensureElements(groupEl);
|
|
343
353
|
const pts = sampleShapePerimeter(visual.shape, visual.radius);
|
|
344
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";
|
|
345
359
|
pathEl.setAttribute("d", d);
|
|
346
360
|
pathEl.setAttribute("fill", visual.fillHex);
|
|
347
361
|
pathEl.setAttribute(
|
|
348
362
|
"stroke",
|
|
349
|
-
state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ?
|
|
363
|
+
state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? hoverStroke : visual.strokeHex ?? defaultStroke
|
|
350
364
|
);
|
|
351
365
|
pathEl.setAttribute(
|
|
352
366
|
"stroke-width",
|
|
@@ -355,7 +369,7 @@ var GeometricShapeSvgView = class {
|
|
|
355
369
|
if (state.interaction.isHovered || state.interaction.isPinned || state.interaction.isHighlighted) {
|
|
356
370
|
const outerPts = sampleShapePerimeter(visual.shape, visual.radius + 4.5);
|
|
357
371
|
ringEl.setAttribute("d", pointsToSvgPath(outerPts));
|
|
358
|
-
ringEl.setAttribute("stroke", state.interaction.isPinned ? "#f59e0b" :
|
|
372
|
+
ringEl.setAttribute("stroke", state.interaction.isPinned ? "#f59e0b" : accentRing);
|
|
359
373
|
ringEl.setAttribute("stroke-width", "1.4");
|
|
360
374
|
ringEl.setAttribute("stroke-opacity", "0.9");
|
|
361
375
|
} else {
|
|
@@ -375,11 +389,12 @@ var GeometricShapeSvgView = class {
|
|
|
375
389
|
y: a.y + (b.y - a.y) * progress
|
|
376
390
|
});
|
|
377
391
|
}
|
|
392
|
+
const isLight = to.theme === "light";
|
|
378
393
|
pathEl.setAttribute("d", pointsToSvgPath(morphed));
|
|
379
394
|
pathEl.setAttribute("fill", lerpHex(from.fillHex, to.fillHex, progress));
|
|
380
395
|
pathEl.setAttribute(
|
|
381
396
|
"stroke",
|
|
382
|
-
state.interaction.isHovered ? "#f8fafc" : to.strokeHex ?? "rgba(15, 23, 42, 0.8)"
|
|
397
|
+
state.interaction.isHovered ? isLight ? "#0f172a" : "#f8fafc" : to.strokeHex ?? (isLight ? "rgba(15, 23, 42, 0.25)" : "rgba(15, 23, 42, 0.8)")
|
|
383
398
|
);
|
|
384
399
|
pathEl.setAttribute("stroke-width", String(to.strokeWidth ?? 1.4));
|
|
385
400
|
ringEl.setAttribute("d", "");
|
|
@@ -427,6 +442,10 @@ var MultiSectorGlyphSvgView = class {
|
|
|
427
442
|
while (groupEl.firstChild) {
|
|
428
443
|
groupEl.removeChild(groupEl.firstChild);
|
|
429
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";
|
|
430
449
|
const totalWeight = visual.sectors.reduce((s, sec) => s + Math.max(0, sec.weight), 0) || 1;
|
|
431
450
|
let angle = -Math.PI / 2;
|
|
432
451
|
for (const sec of visual.sectors) {
|
|
@@ -439,7 +458,7 @@ var MultiSectorGlyphSvgView = class {
|
|
|
439
458
|
describeDonutSectorPath(visual.innerRadius, visual.outerRadius, angle, nextAngle)
|
|
440
459
|
);
|
|
441
460
|
path.setAttribute("fill", sec.colorHex);
|
|
442
|
-
path.setAttribute("stroke",
|
|
461
|
+
path.setAttribute("stroke", dividerStroke);
|
|
443
462
|
path.setAttribute("stroke-width", visual.sectors.length > 1 ? "1.1" : "0.8");
|
|
444
463
|
groupEl.appendChild(path);
|
|
445
464
|
angle = nextAngle;
|
|
@@ -458,7 +477,7 @@ var MultiSectorGlyphSvgView = class {
|
|
|
458
477
|
ring.setAttribute("fill", "none");
|
|
459
478
|
ring.setAttribute(
|
|
460
479
|
"stroke",
|
|
461
|
-
state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ?
|
|
480
|
+
state.interaction.isPinned ? "#f59e0b" : state.interaction.isHovered ? hoverStroke : visual.accentRingHex ?? defaultAccent
|
|
462
481
|
);
|
|
463
482
|
ring.setAttribute("stroke-width", state.interaction.isHovered ? "2" : "1.2");
|
|
464
483
|
groupEl.appendChild(ring);
|
|
@@ -493,7 +512,7 @@ function createPresetViews(preset) {
|
|
|
493
512
|
const clusterView = preset === "badges" ? new BadgeClusterSvgView() : new ClusterAnchorSvgView();
|
|
494
513
|
return { contentView, clusterView };
|
|
495
514
|
}
|
|
496
|
-
function createPresetVisualResolver(preset, getLabel) {
|
|
515
|
+
function createPresetVisualResolver(preset, getLabel, theme = "dark") {
|
|
497
516
|
return {
|
|
498
517
|
resolveContentVisual: (item, ctx) => {
|
|
499
518
|
if (preset === "glyphs") {
|
|
@@ -504,7 +523,8 @@ function createPresetVisualResolver(preset, getLabel) {
|
|
|
504
523
|
colorHex: uc.cluster.colorHex,
|
|
505
524
|
weight: uc.weight
|
|
506
525
|
})),
|
|
507
|
-
centerColorHex: ctx.mixedColorHex
|
|
526
|
+
centerColorHex: ctx.mixedColorHex,
|
|
527
|
+
theme
|
|
508
528
|
};
|
|
509
529
|
}
|
|
510
530
|
const rank = ctx.unitClusters.length;
|
|
@@ -512,9 +532,10 @@ function createPresetVisualResolver(preset, getLabel) {
|
|
|
512
532
|
shape: rank >= 3 ? "star" : rank === 2 ? "diamond" : "circle",
|
|
513
533
|
radius: 9.5,
|
|
514
534
|
fillHex: ctx.mixedColorHex,
|
|
515
|
-
strokeHex: "#07090E",
|
|
535
|
+
strokeHex: theme === "light" ? "#ffffff" : "#07090E",
|
|
516
536
|
strokeWidth: 1.4,
|
|
517
|
-
label: extractDefaultItemLabel(item, ctx.id, getLabel)
|
|
537
|
+
label: extractDefaultItemLabel(item, ctx.id, getLabel),
|
|
538
|
+
theme
|
|
518
539
|
};
|
|
519
540
|
},
|
|
520
541
|
resolveClusterVisual: (cluster) => {
|
|
@@ -524,7 +545,8 @@ function createPresetVisualResolver(preset, getLabel) {
|
|
|
524
545
|
code: cluster.code,
|
|
525
546
|
title: cluster.defaultLabel,
|
|
526
547
|
metaLine: `${count} ${count === 1 ? "item" : "items"}`,
|
|
527
|
-
colorHex: cluster.colorHex
|
|
548
|
+
colorHex: cluster.colorHex,
|
|
549
|
+
theme
|
|
528
550
|
};
|
|
529
551
|
}
|
|
530
552
|
return {
|
|
@@ -532,21 +554,23 @@ function createPresetVisualResolver(preset, getLabel) {
|
|
|
532
554
|
label: cluster.defaultLabel,
|
|
533
555
|
memberCount: cluster.members.length,
|
|
534
556
|
colorHex: cluster.colorHex,
|
|
535
|
-
radius: 22
|
|
557
|
+
radius: 22,
|
|
558
|
+
theme
|
|
536
559
|
};
|
|
537
560
|
},
|
|
538
561
|
getVisualCacheKey: (visual) => {
|
|
562
|
+
const t = visual.theme ?? "dark";
|
|
539
563
|
if ("sectors" in visual) {
|
|
540
564
|
const secKey = visual.sectors.map((s) => `${s.colorHex}:${s.weight.toFixed(2)}`).join("|");
|
|
541
|
-
return `glyph:${visual.outerRadius}:${visual.centerColorHex}:${secKey}`;
|
|
565
|
+
return `glyph:${t}:${visual.outerRadius}:${visual.centerColorHex}:${secKey}`;
|
|
542
566
|
}
|
|
543
567
|
if ("shape" in visual) {
|
|
544
|
-
return `geom:${visual.shape}:${visual.radius}:${visual.fillHex}`;
|
|
568
|
+
return `geom:${t}:${visual.shape}:${visual.radius}:${visual.fillHex}`;
|
|
545
569
|
}
|
|
546
570
|
if ("metaLine" in visual) {
|
|
547
|
-
return `badge:${visual.code}:${visual.title}:${visual.metaLine}:${visual.colorHex}`;
|
|
571
|
+
return `badge:${t}:${visual.code}:${visual.title}:${visual.metaLine}:${visual.colorHex}`;
|
|
548
572
|
}
|
|
549
|
-
return `anchor:${visual.code}:${visual.memberCount}:${visual.colorHex}`;
|
|
573
|
+
return `anchor:${t}:${visual.code}:${visual.memberCount}:${visual.colorHex}`;
|
|
550
574
|
}
|
|
551
575
|
};
|
|
552
576
|
}
|
|
@@ -672,7 +696,7 @@ var SvgRendererAdapter = class {
|
|
|
672
696
|
}
|
|
673
697
|
while (this.edgeLines.length < snapshot.edges.length) {
|
|
674
698
|
const line = this.doc.createElementNS(SVG_NS, "line");
|
|
675
|
-
line.setAttribute("stroke", "rgba(
|
|
699
|
+
line.setAttribute("stroke", "rgba(100, 116, 139, 0.26)");
|
|
676
700
|
line.setAttribute("stroke-width", "1");
|
|
677
701
|
line.setAttribute("stroke-dasharray", "3 4");
|
|
678
702
|
this.edgesGroup.appendChild(line);
|
|
@@ -935,16 +959,23 @@ var SimplexSvgController = class _SimplexSvgController {
|
|
|
935
959
|
tooltipOption;
|
|
936
960
|
tooltipEl = null;
|
|
937
961
|
focusedClusterUidState = null;
|
|
962
|
+
currentPreset;
|
|
963
|
+
currentTheme;
|
|
964
|
+
usesCustomVisualResolver;
|
|
965
|
+
cameraProxy;
|
|
938
966
|
rafId = null;
|
|
939
967
|
lastFrameTs = 0;
|
|
940
968
|
fpsSamples = 0;
|
|
941
969
|
fpsAccumMs = 0;
|
|
942
970
|
destroyed = false;
|
|
943
|
-
// Centralized drag-to-pan state on root <svg>
|
|
971
|
+
// Centralized drag-to-pan and multi-touch pinch-zoom state on root <svg>
|
|
944
972
|
isDraggingPan = false;
|
|
945
973
|
dragStartX = 0;
|
|
946
974
|
dragStartY = 0;
|
|
947
975
|
dragMoved = false;
|
|
976
|
+
activePointers = /* @__PURE__ */ new Map();
|
|
977
|
+
lastPinchDist = null;
|
|
978
|
+
lastPinchMid = null;
|
|
948
979
|
constructor(containerEl, options) {
|
|
949
980
|
this.containerEl = resolveContainerElement(containerEl);
|
|
950
981
|
this.onFpsUpdate = options.onFpsUpdate;
|
|
@@ -952,13 +983,16 @@ var SimplexSvgController = class _SimplexSvgController {
|
|
|
952
983
|
this.getLabelExtractor = options.getLabel;
|
|
953
984
|
this.tooltipOption = options.tooltip ?? false;
|
|
954
985
|
this.getIdExtractor = options.getId ?? ((item) => typeof item === "object" && item !== null && "id" in item ? String(item.id) : "");
|
|
955
|
-
|
|
956
|
-
|
|
986
|
+
this.currentPreset = options.preset ?? "geometric";
|
|
987
|
+
this.currentTheme = options.theme ?? "dark";
|
|
988
|
+
this.usesCustomVisualResolver = options.visualResolver !== void 0;
|
|
989
|
+
const presetViews = createPresetViews(this.currentPreset);
|
|
957
990
|
const contentView = options.contentView ?? presetViews.contentView;
|
|
958
991
|
const clusterView = options.clusterView ?? presetViews.clusterView;
|
|
959
992
|
const visualResolver = options.visualResolver ?? createPresetVisualResolver(
|
|
960
|
-
|
|
961
|
-
this.getLabelExtractor
|
|
993
|
+
this.currentPreset,
|
|
994
|
+
this.getLabelExtractor,
|
|
995
|
+
this.currentTheme
|
|
962
996
|
);
|
|
963
997
|
const doc = this.containerEl.ownerDocument;
|
|
964
998
|
this.svgEl = doc.createElementNS(SVG_NS, "svg");
|
|
@@ -988,11 +1022,42 @@ var SimplexSvgController = class _SimplexSvgController {
|
|
|
988
1022
|
resolveTagMeta: options.resolveTagMeta,
|
|
989
1023
|
meshOptions: options.meshOptions,
|
|
990
1024
|
layoutOptions: options.layoutOptions,
|
|
1025
|
+
meshStrategy: options.meshStrategy,
|
|
1026
|
+
clusteringStrategy: options.clusteringStrategy,
|
|
1027
|
+
colorStrategy: options.colorStrategy,
|
|
1028
|
+
layoutStrategy: options.layoutStrategy,
|
|
1029
|
+
theme: this.currentTheme,
|
|
991
1030
|
showHulls: options.showHulls,
|
|
992
1031
|
interactions: this.wrapInteractions(options.interactions),
|
|
993
1032
|
onMetricsChange: options.onMetricsChange,
|
|
994
1033
|
onClustersChange: options.onClustersChange
|
|
995
1034
|
});
|
|
1035
|
+
const baseCamera = this.engine.camera;
|
|
1036
|
+
const syncCameraRender = () => {
|
|
1037
|
+
if (this.destroyed) return;
|
|
1038
|
+
this.engine.renderNow();
|
|
1039
|
+
};
|
|
1040
|
+
this.cameraProxy = {
|
|
1041
|
+
getState: () => baseCamera.getState(),
|
|
1042
|
+
setState: (next) => {
|
|
1043
|
+
baseCamera.setState(next);
|
|
1044
|
+
syncCameraRender();
|
|
1045
|
+
},
|
|
1046
|
+
zoomToCursor: (screenX, screenY, zoomFactor) => {
|
|
1047
|
+
baseCamera.zoomToCursor(screenX, screenY, zoomFactor);
|
|
1048
|
+
syncCameraRender();
|
|
1049
|
+
},
|
|
1050
|
+
panBy: (dx, dy) => {
|
|
1051
|
+
baseCamera.panBy(dx, dy);
|
|
1052
|
+
syncCameraRender();
|
|
1053
|
+
},
|
|
1054
|
+
reset: () => {
|
|
1055
|
+
baseCamera.reset();
|
|
1056
|
+
syncCameraRender();
|
|
1057
|
+
},
|
|
1058
|
+
screenToWorld: (screenX, screenY) => baseCamera.screenToWorld(screenX, screenY),
|
|
1059
|
+
worldToScreen: (worldX, worldY) => baseCamera.worldToScreen(worldX, worldY)
|
|
1060
|
+
};
|
|
996
1061
|
this.attachSvgListeners();
|
|
997
1062
|
if (typeof ResizeObserver !== "undefined") {
|
|
998
1063
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
@@ -1076,6 +1141,13 @@ ${tagList}` : label;
|
|
|
1076
1141
|
return `${ev.cluster.code} \xB7 ${ev.cluster.defaultLabel}
|
|
1077
1142
|
${count} ${count === 1 ? "item" : "items"}`;
|
|
1078
1143
|
}
|
|
1144
|
+
applyTooltipThemeStyles(el) {
|
|
1145
|
+
const isLight = this.currentTheme === "light";
|
|
1146
|
+
el.style.background = isLight ? "rgba(255, 255, 255, 0.96)" : "rgba(9, 13, 22, 0.94)";
|
|
1147
|
+
el.style.border = isLight ? "1px solid rgba(203, 213, 225, 0.95)" : "1px solid rgba(51, 65, 85, 0.85)";
|
|
1148
|
+
el.style.color = isLight ? "#0f172a" : "#f1f5f9";
|
|
1149
|
+
el.style.boxShadow = isLight ? "0 10px 24px rgba(15, 23, 42, 0.12)" : "0 10px 24px rgba(0, 0, 0, 0.45)";
|
|
1150
|
+
}
|
|
1079
1151
|
ensureTooltipElement() {
|
|
1080
1152
|
if (!this.tooltipOption) return null;
|
|
1081
1153
|
if (this.tooltipEl) return this.tooltipEl;
|
|
@@ -1093,14 +1165,11 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1093
1165
|
el.style.maxWidth = "240px";
|
|
1094
1166
|
el.style.padding = "7px 10px";
|
|
1095
1167
|
el.style.borderRadius = "8px";
|
|
1096
|
-
el.style.background = "rgba(9, 13, 22, 0.94)";
|
|
1097
|
-
el.style.border = "1px solid rgba(51, 65, 85, 0.85)";
|
|
1098
|
-
el.style.color = "#f1f5f9";
|
|
1099
1168
|
el.style.fontFamily = "Plus Jakarta Sans, ui-sans-serif, system-ui, sans-serif";
|
|
1100
1169
|
el.style.fontSize = "12px";
|
|
1101
1170
|
el.style.lineHeight = "1.4";
|
|
1102
1171
|
el.style.whiteSpace = "pre-line";
|
|
1103
|
-
|
|
1172
|
+
this.applyTooltipThemeStyles(el);
|
|
1104
1173
|
this.containerEl.appendChild(el);
|
|
1105
1174
|
this.tooltipEl = el;
|
|
1106
1175
|
return el;
|
|
@@ -1121,7 +1190,7 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1121
1190
|
}
|
|
1122
1191
|
}
|
|
1123
1192
|
get camera() {
|
|
1124
|
-
return this.
|
|
1193
|
+
return this.cameraProxy;
|
|
1125
1194
|
}
|
|
1126
1195
|
getSvgElement() {
|
|
1127
1196
|
return this.svgEl;
|
|
@@ -1152,13 +1221,15 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1152
1221
|
* morph/crossfade transitions.
|
|
1153
1222
|
*/
|
|
1154
1223
|
setPreset(preset, getLabel) {
|
|
1224
|
+
this.currentPreset = preset;
|
|
1155
1225
|
if (getLabel !== void 0) {
|
|
1156
1226
|
this.getLabelExtractor = getLabel;
|
|
1157
1227
|
}
|
|
1158
1228
|
const views = createPresetViews(preset);
|
|
1159
1229
|
const resolver = createPresetVisualResolver(
|
|
1160
1230
|
preset,
|
|
1161
|
-
this.getLabelExtractor
|
|
1231
|
+
this.getLabelExtractor,
|
|
1232
|
+
this.currentTheme
|
|
1162
1233
|
);
|
|
1163
1234
|
this.adapter.setViews({
|
|
1164
1235
|
contentView: views.contentView,
|
|
@@ -1169,6 +1240,64 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1169
1240
|
);
|
|
1170
1241
|
this.ensureLoopRunning();
|
|
1171
1242
|
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Switches built-in preset & tooltip theme between `'dark'` and `'light'`.
|
|
1245
|
+
*/
|
|
1246
|
+
setTheme(theme) {
|
|
1247
|
+
if (this.currentTheme === theme) return;
|
|
1248
|
+
this.currentTheme = theme;
|
|
1249
|
+
if (this.tooltipEl) {
|
|
1250
|
+
this.applyTooltipThemeStyles(this.tooltipEl);
|
|
1251
|
+
}
|
|
1252
|
+
this.engine.setTheme(theme);
|
|
1253
|
+
if (!this.usesCustomVisualResolver) {
|
|
1254
|
+
const resolver = createPresetVisualResolver(
|
|
1255
|
+
this.currentPreset,
|
|
1256
|
+
this.getLabelExtractor,
|
|
1257
|
+
theme
|
|
1258
|
+
);
|
|
1259
|
+
this.engine.setVisualResolver(
|
|
1260
|
+
resolver
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
this.ensureLoopRunning();
|
|
1264
|
+
}
|
|
1265
|
+
setMeshStrategy(strategy) {
|
|
1266
|
+
this.engine.setMeshStrategy(strategy);
|
|
1267
|
+
this.ensureLoopRunning();
|
|
1268
|
+
}
|
|
1269
|
+
setClusteringStrategy(strategy) {
|
|
1270
|
+
this.engine.setClusteringStrategy(strategy);
|
|
1271
|
+
this.ensureLoopRunning();
|
|
1272
|
+
}
|
|
1273
|
+
setColorStrategy(strategy) {
|
|
1274
|
+
this.engine.setColorStrategy(strategy);
|
|
1275
|
+
this.ensureLoopRunning();
|
|
1276
|
+
}
|
|
1277
|
+
setLayoutStrategy(strategy) {
|
|
1278
|
+
this.engine.setLayoutStrategy(strategy);
|
|
1279
|
+
this.ensureLoopRunning();
|
|
1280
|
+
}
|
|
1281
|
+
getMeshStrategy() {
|
|
1282
|
+
return this.engine.getMeshStrategy();
|
|
1283
|
+
}
|
|
1284
|
+
getClusteringStrategy() {
|
|
1285
|
+
return this.engine.getClusteringStrategy();
|
|
1286
|
+
}
|
|
1287
|
+
getColorStrategy() {
|
|
1288
|
+
return this.engine.getColorStrategy();
|
|
1289
|
+
}
|
|
1290
|
+
getLayoutStrategy() {
|
|
1291
|
+
return this.engine.getLayoutStrategy();
|
|
1292
|
+
}
|
|
1293
|
+
setMeshOptions(options) {
|
|
1294
|
+
this.engine.setMeshOptions(options);
|
|
1295
|
+
this.ensureLoopRunning();
|
|
1296
|
+
}
|
|
1297
|
+
setLayoutOptions(options) {
|
|
1298
|
+
this.engine.setLayoutOptions(options);
|
|
1299
|
+
this.ensureLoopRunning();
|
|
1300
|
+
}
|
|
1172
1301
|
setTooltip(tooltip) {
|
|
1173
1302
|
this.tooltipOption = tooltip;
|
|
1174
1303
|
if (!tooltip) {
|
|
@@ -1217,9 +1346,25 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1217
1346
|
y: clientY - rect.top
|
|
1218
1347
|
};
|
|
1219
1348
|
}
|
|
1349
|
+
resetTouchPinchState() {
|
|
1350
|
+
this.lastPinchDist = null;
|
|
1351
|
+
this.lastPinchMid = null;
|
|
1352
|
+
}
|
|
1220
1353
|
onPointerDown = (e) => {
|
|
1221
|
-
if (e.button !== 0) return;
|
|
1354
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
1222
1355
|
const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
|
|
1356
|
+
this.activePointers.set(e.pointerId, { x, y });
|
|
1357
|
+
if (this.activePointers.size >= 2) {
|
|
1358
|
+
this.isDraggingPan = false;
|
|
1359
|
+
this.dragMoved = true;
|
|
1360
|
+
const pts = [...this.activePointers.values()];
|
|
1361
|
+
const p0 = pts[0];
|
|
1362
|
+
const p1 = pts[1];
|
|
1363
|
+
this.lastPinchDist = Math.hypot(p1.x - p0.x, p1.y - p0.y);
|
|
1364
|
+
this.lastPinchMid = { x: (p0.x + p1.x) * 0.5, y: (p0.y + p1.y) * 0.5 };
|
|
1365
|
+
this.hideTooltip();
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1223
1368
|
const hit = this.engine.hitTest(x, y);
|
|
1224
1369
|
if (hit.kind === "background") {
|
|
1225
1370
|
this.isDraggingPan = true;
|
|
@@ -1234,10 +1379,34 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1234
1379
|
};
|
|
1235
1380
|
onPointerMove = (e) => {
|
|
1236
1381
|
const { x, y } = this.toLocalCoords(e.clientX, e.clientY);
|
|
1382
|
+
if (this.activePointers.has(e.pointerId)) {
|
|
1383
|
+
this.activePointers.set(e.pointerId, { x, y });
|
|
1384
|
+
}
|
|
1385
|
+
if (this.activePointers.size >= 2) {
|
|
1386
|
+
const pts = [...this.activePointers.values()];
|
|
1387
|
+
const p0 = pts[0];
|
|
1388
|
+
const p1 = pts[1];
|
|
1389
|
+
const dist = Math.hypot(p1.x - p0.x, p1.y - p0.y);
|
|
1390
|
+
const mid = { x: (p0.x + p1.x) * 0.5, y: (p0.y + p1.y) * 0.5 };
|
|
1391
|
+
if (this.lastPinchDist !== null && this.lastPinchMid !== null && this.lastPinchDist > 8 && dist > 8) {
|
|
1392
|
+
const dx = mid.x - this.lastPinchMid.x;
|
|
1393
|
+
const dy = mid.y - this.lastPinchMid.y;
|
|
1394
|
+
const zoomFactor = dist / this.lastPinchDist;
|
|
1395
|
+
this.engine.camera.panBy(dx, dy);
|
|
1396
|
+
this.engine.camera.zoomToCursor(mid.x, mid.y, zoomFactor);
|
|
1397
|
+
this.dragMoved = true;
|
|
1398
|
+
this.hideTooltip();
|
|
1399
|
+
this.engine.renderNow();
|
|
1400
|
+
}
|
|
1401
|
+
this.lastPinchDist = dist;
|
|
1402
|
+
this.lastPinchMid = mid;
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1237
1405
|
if (this.isDraggingPan) {
|
|
1238
1406
|
const dx = e.clientX - this.dragStartX;
|
|
1239
1407
|
const dy = e.clientY - this.dragStartY;
|
|
1240
|
-
|
|
1408
|
+
const dragThreshold = e.pointerType === "touch" ? 6 : 3;
|
|
1409
|
+
if (Math.hypot(dx, dy) > dragThreshold) {
|
|
1241
1410
|
this.dragMoved = true;
|
|
1242
1411
|
}
|
|
1243
1412
|
if (this.dragMoved) {
|
|
@@ -1245,23 +1414,47 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1245
1414
|
this.dragStartY = e.clientY;
|
|
1246
1415
|
this.engine.camera.panBy(dx, dy);
|
|
1247
1416
|
this.hideTooltip();
|
|
1248
|
-
this.
|
|
1417
|
+
this.engine.renderNow();
|
|
1249
1418
|
}
|
|
1250
1419
|
return;
|
|
1251
1420
|
}
|
|
1421
|
+
if (e.pointerType === "touch") {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1252
1424
|
this.engine.handlePointerMove(x, y);
|
|
1253
1425
|
const hit = this.engine.hitTest(x, y);
|
|
1254
1426
|
this.svgEl.style.cursor = hit.kind === "background" ? "grab" : "pointer";
|
|
1255
1427
|
this.ensureLoopRunning();
|
|
1256
1428
|
};
|
|
1257
|
-
onPointerUp = () => {
|
|
1258
|
-
|
|
1429
|
+
onPointerUp = (e) => {
|
|
1430
|
+
this.activePointers.delete(e.pointerId);
|
|
1431
|
+
if (this.activePointers.size < 2) {
|
|
1432
|
+
this.resetTouchPinchState();
|
|
1433
|
+
}
|
|
1434
|
+
if (this.activePointers.size === 0 && this.isDraggingPan) {
|
|
1259
1435
|
this.isDraggingPan = false;
|
|
1260
1436
|
this.svgEl.style.cursor = "grab";
|
|
1261
1437
|
}
|
|
1262
1438
|
};
|
|
1263
|
-
|
|
1264
|
-
this.
|
|
1439
|
+
onPointerCancel = (e) => {
|
|
1440
|
+
this.activePointers.delete(e.pointerId);
|
|
1441
|
+
if (this.activePointers.size < 2) {
|
|
1442
|
+
this.resetTouchPinchState();
|
|
1443
|
+
}
|
|
1444
|
+
if (this.activePointers.size === 0) {
|
|
1445
|
+
this.isDraggingPan = false;
|
|
1446
|
+
this.dragMoved = false;
|
|
1447
|
+
this.svgEl.style.cursor = "grab";
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
onPointerLeave = (e) => {
|
|
1451
|
+
this.activePointers.delete(e.pointerId);
|
|
1452
|
+
if (this.activePointers.size < 2) {
|
|
1453
|
+
this.resetTouchPinchState();
|
|
1454
|
+
}
|
|
1455
|
+
if (this.activePointers.size === 0) {
|
|
1456
|
+
this.isDraggingPan = false;
|
|
1457
|
+
}
|
|
1265
1458
|
this.hideTooltip();
|
|
1266
1459
|
this.engine.handlePointerLeave();
|
|
1267
1460
|
this.ensureLoopRunning();
|
|
@@ -1318,6 +1511,7 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1318
1511
|
this.svgEl.addEventListener("pointerdown", this.onPointerDown);
|
|
1319
1512
|
this.svgEl.addEventListener("pointermove", this.onPointerMove);
|
|
1320
1513
|
this.svgEl.addEventListener("pointerup", this.onPointerUp);
|
|
1514
|
+
this.svgEl.addEventListener("pointercancel", this.onPointerCancel);
|
|
1321
1515
|
this.svgEl.addEventListener("pointerleave", this.onPointerLeave);
|
|
1322
1516
|
this.svgEl.addEventListener("click", this.onClick);
|
|
1323
1517
|
this.svgEl.addEventListener("contextmenu", this.onContextMenu);
|
|
@@ -1327,6 +1521,7 @@ ${count} ${count === 1 ? "item" : "items"}`;
|
|
|
1327
1521
|
this.svgEl.removeEventListener("pointerdown", this.onPointerDown);
|
|
1328
1522
|
this.svgEl.removeEventListener("pointermove", this.onPointerMove);
|
|
1329
1523
|
this.svgEl.removeEventListener("pointerup", this.onPointerUp);
|
|
1524
|
+
this.svgEl.removeEventListener("pointercancel", this.onPointerCancel);
|
|
1330
1525
|
this.svgEl.removeEventListener("pointerleave", this.onPointerLeave);
|
|
1331
1526
|
this.svgEl.removeEventListener("click", this.onClick);
|
|
1332
1527
|
this.svgEl.removeEventListener("contextmenu", this.onContextMenu);
|