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