@fortemi/graph 2026.7.3 → 2026.7.5
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 +57 -3
- package/dist/controller-m13D5PbF.d.ts +148 -0
- package/dist/controller.d.ts +2 -0
- package/dist/controller.js +184 -0
- package/dist/controller.js.map +1 -0
- package/dist/index.d.ts +219 -139
- package/dist/index.js +453 -164
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { GraphRepository, CommunitiesRepository } from '@fortemi/core';
|
|
2
|
-
|
|
3
1
|
// src/degree.ts
|
|
4
2
|
function computeDegrees(graph) {
|
|
5
3
|
const degree = /* @__PURE__ */ new Map();
|
|
@@ -77,6 +75,11 @@ function mulberry32(seed) {
|
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
// src/layout.ts
|
|
78
|
+
function readPosition(map, id) {
|
|
79
|
+
if (!map) return void 0;
|
|
80
|
+
if (map instanceof Map) return map.get(id);
|
|
81
|
+
return Object.prototype.hasOwnProperty.call(map, id) ? map[id] : void 0;
|
|
82
|
+
}
|
|
80
83
|
var DEFAULTS = {
|
|
81
84
|
width: 760,
|
|
82
85
|
height: 460,
|
|
@@ -141,6 +144,29 @@ function layoutCommunityGraph(graph, options = {}) {
|
|
|
141
144
|
y[index] = centerY + Math.sin(angle) * (algorithm === "radial" ? radius : localRadius * 0.72);
|
|
142
145
|
}
|
|
143
146
|
});
|
|
147
|
+
const pinnedIndex = /* @__PURE__ */ new Map();
|
|
148
|
+
const seededIndex = /* @__PURE__ */ new Set();
|
|
149
|
+
graph.nodes.forEach((node, index) => {
|
|
150
|
+
const seed = readPosition(options.initialPositions, node.id);
|
|
151
|
+
if (seed) {
|
|
152
|
+
x[index] = seed.x;
|
|
153
|
+
y[index] = seed.y;
|
|
154
|
+
seededIndex.add(index);
|
|
155
|
+
}
|
|
156
|
+
const pin = readPosition(options.pinned, node.id);
|
|
157
|
+
if (pin) {
|
|
158
|
+
x[index] = pin.x;
|
|
159
|
+
y[index] = pin.y;
|
|
160
|
+
pinnedIndex.set(index, pin);
|
|
161
|
+
seededIndex.add(index);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
const holdPinned = () => {
|
|
165
|
+
for (const [index, pos] of pinnedIndex) {
|
|
166
|
+
x[index] = pos.x;
|
|
167
|
+
y[index] = pos.y;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
144
170
|
if (algorithm === "force" && n > 1) {
|
|
145
171
|
const ticks = Math.max(0, Math.floor(options.ticks ?? DEFAULTS.ticks));
|
|
146
172
|
const seed = options.seed ?? DEFAULTS.seed;
|
|
@@ -151,6 +177,7 @@ function layoutCommunityGraph(graph, options = {}) {
|
|
|
151
177
|
const communityStrength = options.communityStrength ?? DEFAULTS.communityStrength;
|
|
152
178
|
const rng = mulberry32(seed);
|
|
153
179
|
for (let i = 0; i < n; i++) {
|
|
180
|
+
if (seededIndex.has(i)) continue;
|
|
154
181
|
x[i] += (rng() - 0.5) * 8;
|
|
155
182
|
y[i] += (rng() - 0.5) * 8;
|
|
156
183
|
}
|
|
@@ -250,6 +277,7 @@ function layoutCommunityGraph(graph, options = {}) {
|
|
|
250
277
|
x[i] = clampToRange(x[i], boundsPadding + r[i], width - boundsPadding - r[i]);
|
|
251
278
|
y[i] = clampToRange(y[i], boundsPadding + r[i], height - boundsPadding - r[i]);
|
|
252
279
|
}
|
|
280
|
+
holdPinned();
|
|
253
281
|
alpha *= cooling;
|
|
254
282
|
}
|
|
255
283
|
} else {
|
|
@@ -257,6 +285,7 @@ function layoutCommunityGraph(graph, options = {}) {
|
|
|
257
285
|
x[i] = clampToRange(x[i], boundsPadding + r[i], width - boundsPadding - r[i]);
|
|
258
286
|
y[i] = clampToRange(y[i], boundsPadding + r[i], height - boundsPadding - r[i]);
|
|
259
287
|
}
|
|
288
|
+
holdPinned();
|
|
260
289
|
}
|
|
261
290
|
const nodes = graph.nodes.map((node, index) => ({
|
|
262
291
|
...node,
|
|
@@ -430,187 +459,447 @@ function deserializeGraphSnapshot(input) {
|
|
|
430
459
|
}
|
|
431
460
|
return graph;
|
|
432
461
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
462
|
+
|
|
463
|
+
// src/render-prep.ts
|
|
464
|
+
var GREYSCALE_COMMUNITY_RAMP = [
|
|
465
|
+
"#2B2824",
|
|
466
|
+
"#43403A",
|
|
467
|
+
"#585149",
|
|
468
|
+
"#6E665A",
|
|
469
|
+
"#837A6B",
|
|
470
|
+
"#968C7C"
|
|
471
|
+
];
|
|
472
|
+
function positionGetter(positions) {
|
|
473
|
+
if (!positions) return () => void 0;
|
|
474
|
+
if (positions instanceof Map) return (id) => positions.get(id);
|
|
475
|
+
return (id) => Object.prototype.hasOwnProperty.call(positions, id) ? positions[id] : void 0;
|
|
476
|
+
}
|
|
477
|
+
function communityRanks(graph) {
|
|
478
|
+
const rank = /* @__PURE__ */ new Map();
|
|
479
|
+
[...graph.communities].sort((a, b) => b.nodes.length - a.nodes.length || a.id.localeCompare(b.id)).forEach((community, index) => rank.set(community.id, index));
|
|
480
|
+
return rank;
|
|
481
|
+
}
|
|
482
|
+
function colorForRank(communityId, rank, palette) {
|
|
483
|
+
if (palette === "community") return colorForCommunity(communityId);
|
|
484
|
+
const ramp = palette === "greyscale" ? GREYSCALE_COMMUNITY_RAMP : palette;
|
|
485
|
+
if (ramp.length === 0) return UNASSIGNED_COMMUNITY_COLOR;
|
|
486
|
+
if (rank < 0) return ramp[ramp.length - 1];
|
|
487
|
+
return ramp[rank % ramp.length];
|
|
488
|
+
}
|
|
489
|
+
function mapCommunityGraph(graph, options = {}) {
|
|
490
|
+
const palette = options.palette ?? "community";
|
|
491
|
+
const labelFor = options.labelFor ?? ((id) => id);
|
|
492
|
+
const getPos = positionGetter(options.positions);
|
|
493
|
+
const sizeFor = options.sizeFor ?? ((degree) => nodeRadius(degree, options.radius));
|
|
494
|
+
const degrees = computeDegrees(graph);
|
|
495
|
+
const rankByComm = communityRanks(graph);
|
|
496
|
+
const commOfNode = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const community of graph.communities) {
|
|
498
|
+
for (const id of community.nodes) commOfNode.set(id, community.id);
|
|
447
499
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
layout: { ...DEFAULT_LAYOUT, ...options.layout },
|
|
460
|
-
status: { loading: false, error: null, freshness: null, cache: null },
|
|
461
|
-
transition: void 0
|
|
500
|
+
const nodes = graph.nodes.map((node) => {
|
|
501
|
+
const communityId = commOfNode.get(node.id);
|
|
502
|
+
const rank = communityId != null ? rankByComm.get(communityId) ?? -1 : -1;
|
|
503
|
+
const degree = degrees.get(node.id) ?? 0;
|
|
504
|
+
const pos = getPos(node.id);
|
|
505
|
+
const rendered = {
|
|
506
|
+
id: node.id,
|
|
507
|
+
label: labelFor(node.id),
|
|
508
|
+
size: sizeFor(degree, node.id),
|
|
509
|
+
color: colorForRank(communityId, rank, palette),
|
|
510
|
+
communityRank: rank
|
|
462
511
|
};
|
|
512
|
+
if (pos) {
|
|
513
|
+
rendered.x = pos.x;
|
|
514
|
+
rendered.y = pos.y;
|
|
515
|
+
}
|
|
516
|
+
return rendered;
|
|
517
|
+
});
|
|
518
|
+
const links = graph.edges.map((edge) => {
|
|
519
|
+
const link = { source: edge.source, target: edge.target, weight: edge.weight };
|
|
520
|
+
if (edge.kind !== void 0) link.kind = edge.kind;
|
|
521
|
+
return link;
|
|
522
|
+
});
|
|
523
|
+
return { nodes, links, clusters: graph.communities.length };
|
|
524
|
+
}
|
|
525
|
+
function bakeRenderGraph(graph, options = {}) {
|
|
526
|
+
const positioned = layoutCommunityGraph(graph, options.layout);
|
|
527
|
+
const positions = /* @__PURE__ */ new Map();
|
|
528
|
+
for (const node of positioned.nodes) positions.set(node.id, { x: node.x, y: node.y });
|
|
529
|
+
const mapOptions = { positions };
|
|
530
|
+
if (options.labelFor) mapOptions.labelFor = options.labelFor;
|
|
531
|
+
if (options.palette) mapOptions.palette = options.palette;
|
|
532
|
+
if (options.radius) mapOptions.radius = options.radius;
|
|
533
|
+
if (options.sizeFor) mapOptions.sizeFor = options.sizeFor;
|
|
534
|
+
return mapCommunityGraph(graph, mapOptions);
|
|
535
|
+
}
|
|
536
|
+
function stringifyRenderGraph(graph) {
|
|
537
|
+
const nodes = [...graph.nodes].sort((a, b) => a.id.localeCompare(b.id));
|
|
538
|
+
const links = [...graph.links].sort(
|
|
539
|
+
(a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target) || (a.kind ?? "").localeCompare(b.kind ?? "")
|
|
540
|
+
);
|
|
541
|
+
return JSON.stringify({ nodes, links, clusters: graph.clusters });
|
|
542
|
+
}
|
|
543
|
+
function isRenderGraph(value) {
|
|
544
|
+
if (!value || typeof value !== "object") return false;
|
|
545
|
+
const candidate = value;
|
|
546
|
+
return Array.isArray(candidate.nodes) && candidate.nodes.length > 0 && Array.isArray(candidate.links) && candidate.nodes.every(
|
|
547
|
+
(n) => n && typeof n.id === "string" && typeof n.size === "number" && typeof n.color === "string"
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
function hasBakedPositions(graph) {
|
|
551
|
+
return graph.nodes.every((n) => typeof n.x === "number" && typeof n.y === "number");
|
|
552
|
+
}
|
|
553
|
+
async function loadRenderSnapshot(source, options = {}) {
|
|
554
|
+
const requirePositions = options.requirePositions ?? true;
|
|
555
|
+
try {
|
|
556
|
+
let data;
|
|
557
|
+
if (typeof source === "string") {
|
|
558
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
559
|
+
if (!fetchImpl) return null;
|
|
560
|
+
const res = await fetchImpl(source);
|
|
561
|
+
if (!res.ok) return null;
|
|
562
|
+
data = await res.json();
|
|
563
|
+
} else if (typeof source === "function") {
|
|
564
|
+
data = await source();
|
|
565
|
+
} else {
|
|
566
|
+
data = source;
|
|
567
|
+
}
|
|
568
|
+
if (!isRenderGraph(data)) return null;
|
|
569
|
+
if (requirePositions && !hasBakedPositions(data)) return null;
|
|
570
|
+
return data;
|
|
571
|
+
} catch {
|
|
572
|
+
return null;
|
|
463
573
|
}
|
|
464
|
-
|
|
465
|
-
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/contract.ts
|
|
577
|
+
function applyControlFilters(graph, filters) {
|
|
578
|
+
const base = filterCommunityGraph(graph, filters);
|
|
579
|
+
const minDegree = filters?.minDegree ?? 0;
|
|
580
|
+
if (minDegree <= 0) return base;
|
|
581
|
+
const degree = computeDegrees(base);
|
|
582
|
+
const keep = base.nodes.filter((n) => (degree.get(n.id) ?? 0) >= minDegree).map((n) => n.id);
|
|
583
|
+
if (keep.length === base.nodes.length) return base;
|
|
584
|
+
return filterCommunityGraph(base, { nodeIds: keep });
|
|
585
|
+
}
|
|
586
|
+
function communityLegend(graph, colors) {
|
|
587
|
+
if (!graph) return [];
|
|
588
|
+
return graph.communities.map((c) => ({
|
|
589
|
+
communityId: c.id,
|
|
590
|
+
color: colorForCommunity(c.id, colors),
|
|
591
|
+
count: c.nodes.length
|
|
592
|
+
})).sort((a, b) => b.count - a.count || a.communityId.localeCompare(b.communityId));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/render-dom.ts
|
|
596
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
597
|
+
function el(tag, attrs) {
|
|
598
|
+
const node = document.createElementNS(SVG_NS, tag);
|
|
599
|
+
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
|
|
600
|
+
return node;
|
|
601
|
+
}
|
|
602
|
+
function clamp(v, lo, hi) {
|
|
603
|
+
return Math.max(lo, Math.min(hi, v));
|
|
604
|
+
}
|
|
605
|
+
function renderCommunityGraph(container, graph, options = {}) {
|
|
606
|
+
const width = options.width ?? 760;
|
|
607
|
+
const height = options.height ?? 460;
|
|
608
|
+
const background = options.background ?? "#fafafa";
|
|
609
|
+
const interactive = options.interactive ?? true;
|
|
610
|
+
const minScale = options.minScale ?? 0.4;
|
|
611
|
+
const maxScale = options.maxScale ?? 3;
|
|
612
|
+
let currentGraph = graph;
|
|
613
|
+
let filters = options.filters;
|
|
614
|
+
let algorithm = options.algorithm ?? "force";
|
|
615
|
+
let selectedNodeId = options.selectedNodeId ?? null;
|
|
616
|
+
let labelFor = options.labelFor ?? ((id) => id);
|
|
617
|
+
let visible = applyControlFilters(currentGraph, filters);
|
|
618
|
+
let positioned = layoutCommunityGraph(visible, {
|
|
619
|
+
algorithm,
|
|
620
|
+
width,
|
|
621
|
+
height,
|
|
622
|
+
...options.layoutOptions
|
|
623
|
+
});
|
|
624
|
+
let adjacency = buildAdjacency(visible);
|
|
625
|
+
const transform = { scale: 1, offsetX: 0, offsetY: 0 };
|
|
626
|
+
if (getComputedPosition(container) === "static") container.style.position = "relative";
|
|
627
|
+
const svg = el("svg", {
|
|
628
|
+
role: "img",
|
|
629
|
+
"aria-label": "Community graph",
|
|
630
|
+
viewBox: `0 0 ${width} ${height}`,
|
|
631
|
+
width: "100%"
|
|
632
|
+
});
|
|
633
|
+
svg.style.display = "block";
|
|
634
|
+
svg.style.background = background;
|
|
635
|
+
svg.style.touchAction = "none";
|
|
636
|
+
svg.style.aspectRatio = `${width} / ${height}`;
|
|
637
|
+
const viewport = el("g", {});
|
|
638
|
+
const edgesLayer = el("g", {});
|
|
639
|
+
const nodesLayer = el("g", {});
|
|
640
|
+
viewport.appendChild(edgesLayer);
|
|
641
|
+
viewport.appendChild(nodesLayer);
|
|
642
|
+
svg.appendChild(viewport);
|
|
643
|
+
container.appendChild(svg);
|
|
644
|
+
const popup = document.createElement("div");
|
|
645
|
+
popup.setAttribute("data-fortemi-graph-popup", "");
|
|
646
|
+
Object.assign(popup.style, {
|
|
647
|
+
position: "absolute",
|
|
648
|
+
display: "none",
|
|
649
|
+
pointerEvents: "none",
|
|
650
|
+
padding: "2px 6px",
|
|
651
|
+
font: "12px system-ui, sans-serif",
|
|
652
|
+
background: "#111",
|
|
653
|
+
color: "#fff",
|
|
654
|
+
borderRadius: "4px",
|
|
655
|
+
transform: "translate(-50%, -130%)",
|
|
656
|
+
whiteSpace: "nowrap",
|
|
657
|
+
zIndex: "2"
|
|
658
|
+
});
|
|
659
|
+
container.appendChild(popup);
|
|
660
|
+
const circles = /* @__PURE__ */ new Map();
|
|
661
|
+
const lines = [];
|
|
662
|
+
function applyTransform() {
|
|
663
|
+
viewport.setAttribute(
|
|
664
|
+
"transform",
|
|
665
|
+
`translate(${transform.offsetX} ${transform.offsetY}) scale(${transform.scale})`
|
|
666
|
+
);
|
|
466
667
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
668
|
+
function fitToViewport() {
|
|
669
|
+
const bounds = computeGraphBounds(positioned.nodes);
|
|
670
|
+
const fit = fitGraphToViewport(bounds, { width, height }, {
|
|
671
|
+
padding: 24,
|
|
672
|
+
minScale,
|
|
673
|
+
maxScale
|
|
674
|
+
});
|
|
675
|
+
transform.scale = fit.scale;
|
|
676
|
+
transform.offsetX = fit.offsetX;
|
|
677
|
+
transform.offsetY = fit.offsetY;
|
|
678
|
+
applyTransform();
|
|
472
679
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
680
|
+
function styleNode(id) {
|
|
681
|
+
const circle = circles.get(id);
|
|
682
|
+
if (!circle) return;
|
|
683
|
+
const node = positioned.nodeIndex.get(id);
|
|
684
|
+
if (!node) return;
|
|
685
|
+
const selected = id === selectedNodeId;
|
|
686
|
+
circle.setAttribute("r", String(selected ? node.r + 3 : node.r));
|
|
687
|
+
circle.setAttribute("stroke", selected ? "#111" : "#fff");
|
|
688
|
+
circle.setAttribute("stroke-width", selected ? "3" : "1.5");
|
|
476
689
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
690
|
+
function showPopup(id) {
|
|
691
|
+
const node = positioned.nodeIndex.get(id);
|
|
692
|
+
if (!node) {
|
|
693
|
+
popup.style.display = "none";
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const px = transform.offsetX + node.x * transform.scale;
|
|
697
|
+
const py = transform.offsetY + node.y * transform.scale;
|
|
698
|
+
const rect = svg.getBoundingClientRect?.();
|
|
699
|
+
const scaleToPx = rect && rect.width ? rect.width / width : 1;
|
|
700
|
+
popup.textContent = labelFor(id);
|
|
701
|
+
popup.style.left = `${px * scaleToPx}px`;
|
|
702
|
+
popup.style.top = `${py * scaleToPx}px`;
|
|
703
|
+
popup.style.display = "block";
|
|
480
704
|
}
|
|
481
|
-
|
|
482
|
-
|
|
705
|
+
function setSelected(id, emit) {
|
|
706
|
+
const prev = selectedNodeId;
|
|
707
|
+
selectedNodeId = id;
|
|
708
|
+
if (prev && prev !== id) styleNode(prev);
|
|
709
|
+
if (id) {
|
|
710
|
+
styleNode(id);
|
|
711
|
+
showPopup(id);
|
|
712
|
+
if (emit) options.onSelectNode?.(id);
|
|
713
|
+
} else {
|
|
714
|
+
popup.style.display = "none";
|
|
715
|
+
}
|
|
483
716
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
717
|
+
function setHover(id) {
|
|
718
|
+
if (!interactive) return;
|
|
719
|
+
if (!id) {
|
|
720
|
+
for (const [, c] of circles) c.style.opacity = "1";
|
|
721
|
+
for (const { line } of lines) line.style.opacity = "0.55";
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const keep = new Set(adjacency.get(id) ?? []);
|
|
725
|
+
keep.add(id);
|
|
726
|
+
for (const [nid, c] of circles) c.style.opacity = keep.has(nid) ? "1" : "0.15";
|
|
727
|
+
for (const { line, source, target } of lines) {
|
|
728
|
+
line.style.opacity = source === id || target === id ? "0.9" : "0.06";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function build() {
|
|
732
|
+
edgesLayer.replaceChildren();
|
|
733
|
+
nodesLayer.replaceChildren();
|
|
734
|
+
circles.clear();
|
|
735
|
+
lines.length = 0;
|
|
736
|
+
for (const edge of positioned.edges) {
|
|
737
|
+
const s = positioned.nodeIndex.get(edge.source);
|
|
738
|
+
const t = positioned.nodeIndex.get(edge.target);
|
|
739
|
+
if (!s || !t) continue;
|
|
740
|
+
const line = el("line", {
|
|
741
|
+
x1: s.x,
|
|
742
|
+
y1: s.y,
|
|
743
|
+
x2: t.x,
|
|
744
|
+
y2: t.y,
|
|
745
|
+
stroke: "#9aa0a6",
|
|
746
|
+
"stroke-width": clamp(edge.weight, 1, 5),
|
|
747
|
+
opacity: 0.55
|
|
748
|
+
});
|
|
749
|
+
edgesLayer.appendChild(line);
|
|
750
|
+
lines.push({ line, source: edge.source, target: edge.target });
|
|
751
|
+
}
|
|
752
|
+
for (const node of positioned.nodes) {
|
|
753
|
+
const circle = el("circle", {
|
|
754
|
+
cx: node.x,
|
|
755
|
+
cy: node.y,
|
|
756
|
+
r: node.r,
|
|
757
|
+
fill: colorForCommunity(node.communityId, options.colors),
|
|
758
|
+
stroke: "#fff",
|
|
759
|
+
"stroke-width": 1.5,
|
|
760
|
+
tabindex: 0,
|
|
761
|
+
role: "button",
|
|
762
|
+
"data-node-id": node.id,
|
|
763
|
+
"aria-label": `Graph node ${labelFor(node.id)}`
|
|
764
|
+
});
|
|
765
|
+
circle.style.cursor = "pointer";
|
|
766
|
+
circle.style.outline = "none";
|
|
767
|
+
const title = el("title", {});
|
|
768
|
+
title.textContent = labelFor(node.id);
|
|
769
|
+
circle.appendChild(title);
|
|
770
|
+
if (interactive) {
|
|
771
|
+
circle.addEventListener("click", (e) => {
|
|
772
|
+
e.stopPropagation();
|
|
773
|
+
setSelected(node.id, true);
|
|
524
774
|
});
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const assignments = await this.communityRepo.previewDynamicCommunity(this.state.filters ?? {});
|
|
529
|
-
this.setState({
|
|
530
|
-
graph: {
|
|
531
|
-
nodes: assignments.map((assignment) => ({ id: assignment.noteId })),
|
|
532
|
-
edges: [],
|
|
533
|
-
communities: [{ id: "dynamic-preview", nodes: assignments.map((assignment) => assignment.noteId) }]
|
|
534
|
-
},
|
|
535
|
-
graphSource: { id: "dynamic-preview", name: "Dynamic preview" },
|
|
536
|
-
status: { loading: false, error: null, freshness: "unknown", cache: null }
|
|
775
|
+
circle.addEventListener("dblclick", (e) => {
|
|
776
|
+
e.stopPropagation();
|
|
777
|
+
options.onNavigate?.(node.id);
|
|
537
778
|
});
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
edges: [],
|
|
547
|
-
communities: [{ id: activeCommunity.id, nodes: assignments.map((assignment) => assignment.noteId) }]
|
|
548
|
-
},
|
|
549
|
-
graphSource: { id: activeCommunity.graphSourceId ?? activeCommunity.id, name: activeCommunity.name },
|
|
550
|
-
status: { loading: false, error: null, freshness: activeCommunity.freshness ?? "unknown", cache: null }
|
|
779
|
+
circle.addEventListener("mouseenter", () => setHover(node.id));
|
|
780
|
+
circle.addEventListener("mouseleave", () => setHover(null));
|
|
781
|
+
circle.addEventListener("keydown", (e) => {
|
|
782
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
if (selectedNodeId === node.id) options.onNavigate?.(node.id);
|
|
785
|
+
else setSelected(node.id, true);
|
|
786
|
+
}
|
|
551
787
|
});
|
|
552
788
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.setState({ status: { ...this.state.status, loading: false, error: e } });
|
|
556
|
-
throw e;
|
|
789
|
+
nodesLayer.appendChild(circle);
|
|
790
|
+
circles.set(node.id, circle);
|
|
557
791
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
setEmbeddingSetSelector(selector) {
|
|
565
|
-
this.beginTransition("topics", "embedding-set-change");
|
|
566
|
-
this.setState({ embeddingSetSelector: selector, mode: "topics" });
|
|
567
|
-
void this.refresh();
|
|
568
|
-
}
|
|
569
|
-
setCommunitySource(sourceId) {
|
|
570
|
-
this.communitySourceId = sourceId;
|
|
571
|
-
this.beginTransition(this.state.mode, "community-source-change");
|
|
572
|
-
void this.refresh();
|
|
573
|
-
}
|
|
574
|
-
/** Apply dynamic-search filters and switch into that mode (state only). */
|
|
575
|
-
applyFilters(nextFilters) {
|
|
576
|
-
this.beginTransition("dynamic-search", "filter-change");
|
|
577
|
-
this.setState({ filters: nextFilters, mode: "dynamic-search" });
|
|
578
|
-
}
|
|
579
|
-
setFilters(nextFilters) {
|
|
580
|
-
this.applyFilters(nextFilters);
|
|
581
|
-
void this.refresh();
|
|
582
|
-
}
|
|
583
|
-
async recompute() {
|
|
584
|
-
this.beginTransition(this.state.mode, "recompute");
|
|
585
|
-
if (this.state.mode === "topics" && this.state.embeddingSetSelector) {
|
|
586
|
-
const result = await this.graphRepo.buildOrLoadSimilarityGraph({
|
|
587
|
-
selector: this.state.embeddingSetSelector,
|
|
588
|
-
source: "live-only"
|
|
589
|
-
});
|
|
590
|
-
this.setState({
|
|
591
|
-
graph: result.graph,
|
|
592
|
-
graphSource: result.graphSource,
|
|
593
|
-
status: { loading: false, error: null, freshness: result.freshness, cache: result.cache }
|
|
594
|
-
});
|
|
595
|
-
return;
|
|
792
|
+
if (selectedNodeId && circles.has(selectedNodeId)) {
|
|
793
|
+
styleNode(selectedNodeId);
|
|
794
|
+
showPopup(selectedNodeId);
|
|
795
|
+
} else {
|
|
796
|
+
selectedNodeId = null;
|
|
797
|
+
popup.style.display = "none";
|
|
596
798
|
}
|
|
597
|
-
await this.refresh();
|
|
598
799
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
800
|
+
function relayout() {
|
|
801
|
+
visible = applyControlFilters(currentGraph, filters);
|
|
802
|
+
positioned = layoutCommunityGraph(visible, {
|
|
803
|
+
algorithm,
|
|
804
|
+
width,
|
|
805
|
+
height,
|
|
806
|
+
...options.layoutOptions
|
|
807
|
+
});
|
|
808
|
+
adjacency = buildAdjacency(visible);
|
|
809
|
+
build();
|
|
810
|
+
fitToViewport();
|
|
602
811
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
812
|
+
const onWheel = (e) => {
|
|
813
|
+
e.preventDefault();
|
|
814
|
+
const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
|
|
815
|
+
transform.scale = clamp(transform.scale * factor, minScale, maxScale);
|
|
816
|
+
applyTransform();
|
|
817
|
+
if (selectedNodeId) showPopup(selectedNodeId);
|
|
818
|
+
};
|
|
819
|
+
let panning = null;
|
|
820
|
+
const onDown = (e) => {
|
|
821
|
+
if (e.target === svg || e.target === viewport) {
|
|
822
|
+
panning = { x: e.clientX, y: e.clientY };
|
|
823
|
+
setSelected(null, true);
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const onMove = (e) => {
|
|
827
|
+
if (!panning) return;
|
|
828
|
+
transform.offsetX += e.clientX - panning.x;
|
|
829
|
+
transform.offsetY += e.clientY - panning.y;
|
|
830
|
+
panning = { x: e.clientX, y: e.clientY };
|
|
831
|
+
applyTransform();
|
|
832
|
+
};
|
|
833
|
+
const onUp = () => {
|
|
834
|
+
panning = null;
|
|
835
|
+
};
|
|
836
|
+
if (interactive) {
|
|
837
|
+
svg.addEventListener("wheel", onWheel, { passive: false });
|
|
838
|
+
svg.addEventListener("mousedown", onDown);
|
|
839
|
+
svg.addEventListener("mousemove", onMove);
|
|
840
|
+
svg.addEventListener("mouseup", onUp);
|
|
841
|
+
svg.addEventListener("mouseleave", onUp);
|
|
608
842
|
}
|
|
609
|
-
|
|
843
|
+
build();
|
|
844
|
+
fitToViewport();
|
|
845
|
+
return {
|
|
846
|
+
element: svg,
|
|
847
|
+
update(next) {
|
|
848
|
+
let needsRelayout = false;
|
|
849
|
+
if (next.graph && next.graph !== currentGraph) {
|
|
850
|
+
currentGraph = next.graph;
|
|
851
|
+
needsRelayout = true;
|
|
852
|
+
}
|
|
853
|
+
if ("filters" in next) {
|
|
854
|
+
filters = next.filters;
|
|
855
|
+
needsRelayout = true;
|
|
856
|
+
}
|
|
857
|
+
if (next.algorithm && next.algorithm !== algorithm) {
|
|
858
|
+
algorithm = next.algorithm;
|
|
859
|
+
needsRelayout = true;
|
|
860
|
+
}
|
|
861
|
+
if (next.labelFor) labelFor = next.labelFor;
|
|
862
|
+
if (needsRelayout) relayout();
|
|
863
|
+
if ("selectedNodeId" in next) setSelected(next.selectedNodeId ?? null, false);
|
|
864
|
+
},
|
|
865
|
+
focus(nodeId) {
|
|
866
|
+
if (!nodeId) {
|
|
867
|
+
setSelected(null, false);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const node = positioned.nodeIndex.get(nodeId);
|
|
871
|
+
if (!node) return;
|
|
872
|
+
transform.offsetX = width / 2 - node.x * transform.scale;
|
|
873
|
+
transform.offsetY = height / 2 - node.y * transform.scale;
|
|
874
|
+
applyTransform();
|
|
875
|
+
setSelected(nodeId, false);
|
|
876
|
+
},
|
|
877
|
+
destroy() {
|
|
878
|
+
if (interactive) {
|
|
879
|
+
svg.removeEventListener("wheel", onWheel);
|
|
880
|
+
svg.removeEventListener("mousedown", onDown);
|
|
881
|
+
svg.removeEventListener("mousemove", onMove);
|
|
882
|
+
svg.removeEventListener("mouseup", onUp);
|
|
883
|
+
svg.removeEventListener("mouseleave", onUp);
|
|
884
|
+
}
|
|
885
|
+
svg.remove();
|
|
886
|
+
popup.remove();
|
|
887
|
+
circles.clear();
|
|
888
|
+
lines.length = 0;
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function getComputedPosition(elm) {
|
|
893
|
+
const inline = elm.style.position;
|
|
894
|
+
if (inline) return inline;
|
|
895
|
+
const view = elm.ownerDocument?.defaultView;
|
|
896
|
+
if (view?.getComputedStyle) return view.getComputedStyle(elm).position || "static";
|
|
897
|
+
return "static";
|
|
898
|
+
}
|
|
610
899
|
|
|
611
900
|
// src/index.ts
|
|
612
|
-
var VERSION = "2026.7.
|
|
901
|
+
var VERSION = "2026.7.5";
|
|
613
902
|
|
|
614
|
-
export { COMMUNITY_COLORS, GRAPH_SNAPSHOT_VERSION,
|
|
903
|
+
export { COMMUNITY_COLORS, GRAPH_SNAPSHOT_VERSION, GREYSCALE_COMMUNITY_RAMP, UNASSIGNED_COMMUNITY_COLOR, VERSION, applyControlFilters, bakeRenderGraph, buildAdjacency, colorForCommunity, communityLegend, communityRanks, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, hasBakedPositions, isRenderGraph, layoutCommunityGraph, loadRenderSnapshot, mapCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, renderCommunityGraph, serializeGraphSnapshot, stringifyGraphSnapshot, stringifyRenderGraph, subgraphForNodes };
|
|
615
904
|
//# sourceMappingURL=index.js.map
|
|
616
905
|
//# sourceMappingURL=index.js.map
|