@aiready/components 0.1.3 → 0.1.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/dist/charts/ForceDirectedGraph.d.ts +2 -0
- package/dist/charts/ForceDirectedGraph.js +489 -162
- package/dist/charts/ForceDirectedGraph.js.map +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/hooks/useForceSimulation.d.ts +5 -0
- package/dist/hooks/useForceSimulation.js +181 -19
- package/dist/hooks/useForceSimulation.js.map +1 -1
- package/dist/index.js +489 -162
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/src/__tests__/smoke.test.js +4 -0
- package/src/charts/ForceDirectedGraph.tsx +281 -277
- package/src/charts/LinkItem.tsx +74 -0
- package/src/charts/NodeItem.tsx +70 -0
- package/src/hooks/useForceSimulation.ts +220 -36
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React2 from 'react';
|
|
2
|
-
import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
|
|
2
|
+
import React2__default, { forwardRef, useRef, useState, useEffect, useImperativeHandle, useCallback } from 'react';
|
|
3
3
|
import { cva } from 'class-variance-authority';
|
|
4
4
|
import { clsx } from 'clsx';
|
|
5
5
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -630,46 +630,183 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
630
630
|
height,
|
|
631
631
|
alphaDecay = 0.0228,
|
|
632
632
|
velocityDecay = 0.4,
|
|
633
|
-
|
|
633
|
+
alphaTarget = 0,
|
|
634
|
+
warmAlpha = 0.3,
|
|
635
|
+
alphaMin = 0.01,
|
|
636
|
+
// @ts-ignore allow extra option
|
|
637
|
+
stabilizeOnStop = true,
|
|
638
|
+
onTick,
|
|
639
|
+
// Optional throttle in milliseconds for tick updates (reduce React re-renders)
|
|
640
|
+
// Lower values = smoother but more CPU; default ~30ms (~33fps)
|
|
641
|
+
// @ts-ignore allow extra option
|
|
642
|
+
tickThrottleMs = 33,
|
|
643
|
+
// @ts-ignore allow extra option
|
|
644
|
+
maxSimulationTimeMs = 3e3
|
|
634
645
|
} = options;
|
|
635
646
|
const [nodes, setNodes] = useState(initialNodes);
|
|
636
647
|
const [links, setLinks] = useState(initialLinks);
|
|
637
648
|
const [isRunning, setIsRunning] = useState(false);
|
|
638
649
|
const [alpha, setAlpha] = useState(1);
|
|
639
650
|
const simulationRef = useRef(null);
|
|
651
|
+
const stopTimeoutRef = useRef(null);
|
|
652
|
+
const nodesKey = initialNodes.map((n) => n.id).join("|");
|
|
653
|
+
const linksKey = (initialLinks || []).map((l) => {
|
|
654
|
+
const s = typeof l.source === "string" ? l.source : l.source?.id;
|
|
655
|
+
const t = typeof l.target === "string" ? l.target : l.target?.id;
|
|
656
|
+
return `${s}->${t}:${l.type || ""}`;
|
|
657
|
+
}).join("|");
|
|
640
658
|
useEffect(() => {
|
|
641
659
|
const nodesCopy = initialNodes.map((node) => ({ ...node }));
|
|
642
660
|
const linksCopy = initialLinks.map((link) => ({ ...link }));
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
661
|
+
try {
|
|
662
|
+
nodesCopy.forEach((n, i) => {
|
|
663
|
+
const angle = i * 2 * Math.PI / nodesCopy.length;
|
|
664
|
+
const radius = Math.min(width, height) * 0.45;
|
|
665
|
+
n.x = width / 2 + radius * Math.cos(angle);
|
|
666
|
+
n.y = height / 2 + radius * Math.sin(angle);
|
|
667
|
+
n.vx = (Math.random() - 0.5) * 2;
|
|
668
|
+
n.vy = (Math.random() - 0.5) * 2;
|
|
669
|
+
});
|
|
670
|
+
} catch (e) {
|
|
671
|
+
nodesCopy.forEach((n) => {
|
|
672
|
+
n.x = Math.random() * width;
|
|
673
|
+
n.y = Math.random() * height;
|
|
674
|
+
n.vx = (Math.random() - 0.5) * 10;
|
|
675
|
+
n.vy = (Math.random() - 0.5) * 10;
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const simulation = d33.forceSimulation(nodesCopy);
|
|
679
|
+
try {
|
|
680
|
+
const linkForce = d33.forceLink(linksCopy);
|
|
681
|
+
linkForce.id((d) => d.id).distance((d) => d && d.distance != null ? d.distance : linkDistance).strength(linkStrength);
|
|
682
|
+
simulation.force("link", linkForce);
|
|
683
|
+
} catch (e) {
|
|
684
|
+
try {
|
|
685
|
+
simulation.force("link", d33.forceLink(linksCopy));
|
|
686
|
+
} catch (e2) {
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
simulation.force("charge", d33.forceManyBody().strength(chargeStrength));
|
|
691
|
+
simulation.force("center", d33.forceCenter(width / 2, height / 2).strength(centerStrength));
|
|
692
|
+
const collide = d33.forceCollide().radius((d) => {
|
|
649
693
|
const nodeSize = d && d.size ? d.size : 10;
|
|
650
694
|
return nodeSize + collisionRadius;
|
|
651
|
-
}).strength(collisionStrength)
|
|
652
|
-
|
|
695
|
+
}).strength(collisionStrength);
|
|
696
|
+
simulation.force("collision", collide);
|
|
697
|
+
simulation.force("x", d33.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5)));
|
|
698
|
+
simulation.force("y", d33.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)));
|
|
699
|
+
simulation.alphaDecay(alphaDecay);
|
|
700
|
+
simulation.velocityDecay(velocityDecay);
|
|
701
|
+
simulation.alphaMin(alphaMin);
|
|
702
|
+
try {
|
|
703
|
+
simulation.alphaTarget(alphaTarget);
|
|
704
|
+
} catch (e) {
|
|
705
|
+
}
|
|
706
|
+
try {
|
|
707
|
+
simulation.alpha(warmAlpha);
|
|
708
|
+
} catch (e) {
|
|
709
|
+
}
|
|
710
|
+
} catch (e) {
|
|
711
|
+
}
|
|
653
712
|
simulationRef.current = simulation;
|
|
654
|
-
|
|
713
|
+
if (stopTimeoutRef.current != null) {
|
|
714
|
+
try {
|
|
715
|
+
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
716
|
+
} catch (e) {
|
|
717
|
+
}
|
|
718
|
+
stopTimeoutRef.current = null;
|
|
719
|
+
}
|
|
720
|
+
if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
|
|
721
|
+
stopTimeoutRef.current = globalThis.setTimeout(() => {
|
|
722
|
+
try {
|
|
723
|
+
if (stabilizeOnStop) {
|
|
724
|
+
nodesCopy.forEach((n) => {
|
|
725
|
+
n.vx = 0;
|
|
726
|
+
n.vy = 0;
|
|
727
|
+
if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
|
|
728
|
+
if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
simulation.alpha(0);
|
|
732
|
+
simulation.stop();
|
|
733
|
+
} catch (e) {
|
|
734
|
+
}
|
|
735
|
+
setIsRunning(false);
|
|
736
|
+
setNodes([...nodesCopy]);
|
|
737
|
+
setLinks([...linksCopy]);
|
|
738
|
+
}, maxSimulationTimeMs);
|
|
739
|
+
}
|
|
740
|
+
let rafId = null;
|
|
741
|
+
let lastUpdate = 0;
|
|
742
|
+
const tickHandler = () => {
|
|
655
743
|
try {
|
|
656
744
|
if (typeof onTick === "function") onTick(nodesCopy, linksCopy, simulation);
|
|
657
745
|
} catch (e) {
|
|
658
746
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
747
|
+
try {
|
|
748
|
+
if (simulation.alpha() <= alphaMin) {
|
|
749
|
+
try {
|
|
750
|
+
if (stabilizeOnStop) {
|
|
751
|
+
nodesCopy.forEach((n) => {
|
|
752
|
+
n.vx = 0;
|
|
753
|
+
n.vy = 0;
|
|
754
|
+
if (typeof n.x === "number") n.x = Number(n.x.toFixed(3));
|
|
755
|
+
if (typeof n.y === "number") n.y = Number(n.y.toFixed(3));
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
simulation.stop();
|
|
759
|
+
} catch (e) {
|
|
760
|
+
}
|
|
761
|
+
setAlpha(simulation.alpha());
|
|
762
|
+
setIsRunning(false);
|
|
763
|
+
setNodes([...nodesCopy]);
|
|
764
|
+
setLinks([...linksCopy]);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
} catch (e) {
|
|
768
|
+
}
|
|
769
|
+
const now = Date.now();
|
|
770
|
+
const shouldUpdate = now - lastUpdate >= tickThrottleMs;
|
|
771
|
+
if (rafId == null && shouldUpdate) {
|
|
772
|
+
rafId = (globalThis.requestAnimationFrame || ((cb) => setTimeout(cb, 16)))(() => {
|
|
773
|
+
rafId = null;
|
|
774
|
+
lastUpdate = Date.now();
|
|
775
|
+
setNodes([...nodesCopy]);
|
|
776
|
+
setLinks([...linksCopy]);
|
|
777
|
+
setAlpha(simulation.alpha());
|
|
778
|
+
setIsRunning(simulation.alpha() > simulation.alphaMin());
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
simulation.on("tick", tickHandler);
|
|
664
783
|
simulation.on("end", () => {
|
|
665
784
|
setIsRunning(false);
|
|
666
785
|
});
|
|
667
786
|
return () => {
|
|
787
|
+
try {
|
|
788
|
+
simulation.on("tick", null);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
}
|
|
791
|
+
if (stopTimeoutRef.current != null) {
|
|
792
|
+
try {
|
|
793
|
+
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
794
|
+
} catch (e) {
|
|
795
|
+
}
|
|
796
|
+
stopTimeoutRef.current = null;
|
|
797
|
+
}
|
|
798
|
+
if (rafId != null) {
|
|
799
|
+
try {
|
|
800
|
+
(globalThis.cancelAnimationFrame || ((id) => clearTimeout(id)))(rafId);
|
|
801
|
+
} catch (e) {
|
|
802
|
+
}
|
|
803
|
+
rafId = null;
|
|
804
|
+
}
|
|
668
805
|
simulation.stop();
|
|
669
806
|
};
|
|
670
807
|
}, [
|
|
671
|
-
|
|
672
|
-
|
|
808
|
+
nodesKey,
|
|
809
|
+
linksKey,
|
|
673
810
|
chargeStrength,
|
|
674
811
|
linkDistance,
|
|
675
812
|
linkStrength,
|
|
@@ -680,12 +817,37 @@ function useForceSimulation(initialNodes, initialLinks, options) {
|
|
|
680
817
|
height,
|
|
681
818
|
alphaDecay,
|
|
682
819
|
velocityDecay,
|
|
683
|
-
|
|
820
|
+
alphaTarget,
|
|
821
|
+
alphaMin,
|
|
822
|
+
stabilizeOnStop,
|
|
823
|
+
tickThrottleMs,
|
|
824
|
+
maxSimulationTimeMs
|
|
684
825
|
]);
|
|
685
826
|
const restart = () => {
|
|
686
827
|
if (simulationRef.current) {
|
|
687
|
-
|
|
828
|
+
try {
|
|
829
|
+
simulationRef.current.alphaTarget(warmAlpha).restart();
|
|
830
|
+
} catch (e) {
|
|
831
|
+
simulationRef.current.restart();
|
|
832
|
+
}
|
|
688
833
|
setIsRunning(true);
|
|
834
|
+
if (stopTimeoutRef.current != null) {
|
|
835
|
+
try {
|
|
836
|
+
globalThis.clearTimeout(stopTimeoutRef.current);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
}
|
|
839
|
+
stopTimeoutRef.current = null;
|
|
840
|
+
}
|
|
841
|
+
if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {
|
|
842
|
+
stopTimeoutRef.current = globalThis.setTimeout(() => {
|
|
843
|
+
try {
|
|
844
|
+
simulationRef.current?.alpha(0);
|
|
845
|
+
simulationRef.current?.stop();
|
|
846
|
+
} catch (e) {
|
|
847
|
+
}
|
|
848
|
+
setIsRunning(false);
|
|
849
|
+
}, maxSimulationTimeMs);
|
|
850
|
+
}
|
|
689
851
|
}
|
|
690
852
|
};
|
|
691
853
|
const stop = () => {
|
|
@@ -746,6 +908,107 @@ function useDrag(simulation) {
|
|
|
746
908
|
onDragEnd: dragEnded
|
|
747
909
|
};
|
|
748
910
|
}
|
|
911
|
+
var NodeItem = ({
|
|
912
|
+
node,
|
|
913
|
+
isSelected,
|
|
914
|
+
isHovered,
|
|
915
|
+
pinned,
|
|
916
|
+
defaultNodeSize,
|
|
917
|
+
defaultNodeColor,
|
|
918
|
+
showLabel = true,
|
|
919
|
+
onClick,
|
|
920
|
+
onDoubleClick,
|
|
921
|
+
onMouseEnter,
|
|
922
|
+
onMouseLeave,
|
|
923
|
+
onMouseDown
|
|
924
|
+
}) => {
|
|
925
|
+
const nodeSize = node.size || defaultNodeSize;
|
|
926
|
+
const nodeColor = node.color || defaultNodeColor;
|
|
927
|
+
const x = node.x ?? 0;
|
|
928
|
+
const y = node.y ?? 0;
|
|
929
|
+
return /* @__PURE__ */ jsxs(
|
|
930
|
+
"g",
|
|
931
|
+
{
|
|
932
|
+
className: "cursor-pointer node",
|
|
933
|
+
"data-id": node.id,
|
|
934
|
+
transform: `translate(${x},${y})`,
|
|
935
|
+
onClick: () => onClick?.(node),
|
|
936
|
+
onDoubleClick: (e) => onDoubleClick?.(e, node),
|
|
937
|
+
onMouseEnter: () => onMouseEnter?.(node),
|
|
938
|
+
onMouseLeave: () => onMouseLeave?.(),
|
|
939
|
+
onMouseDown: (e) => onMouseDown?.(e, node),
|
|
940
|
+
children: [
|
|
941
|
+
/* @__PURE__ */ jsx(
|
|
942
|
+
"circle",
|
|
943
|
+
{
|
|
944
|
+
r: nodeSize,
|
|
945
|
+
fill: nodeColor,
|
|
946
|
+
stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
|
|
947
|
+
strokeWidth: pinned ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
|
|
948
|
+
opacity: isHovered || isSelected ? 1 : 0.9
|
|
949
|
+
}
|
|
950
|
+
),
|
|
951
|
+
pinned && /* @__PURE__ */ jsx("circle", { r: nodeSize + 4, fill: "none", stroke: "#ff6b6b", strokeWidth: 1, opacity: 0.5, className: "pointer-events-none" }),
|
|
952
|
+
showLabel && node.label && /* @__PURE__ */ jsx("text", { y: nodeSize + 15, fill: "#333", fontSize: "12", textAnchor: "middle", dominantBaseline: "middle", pointerEvents: "none", className: "select-none", children: node.label })
|
|
953
|
+
]
|
|
954
|
+
},
|
|
955
|
+
node.id
|
|
956
|
+
);
|
|
957
|
+
};
|
|
958
|
+
var NodeItem_default = NodeItem;
|
|
959
|
+
var LinkItem = ({ link, onClick, defaultWidth, showLabel = true, nodes = [] }) => {
|
|
960
|
+
const src = link.source?.id ?? (typeof link.source === "string" ? link.source : void 0);
|
|
961
|
+
const tgt = link.target?.id ?? (typeof link.target === "string" ? link.target : void 0);
|
|
962
|
+
const getNodePosition = (nodeOrId) => {
|
|
963
|
+
if (typeof nodeOrId === "object" && nodeOrId !== null) {
|
|
964
|
+
const node = nodeOrId;
|
|
965
|
+
return { x: node.x ?? 0, y: node.y ?? 0 };
|
|
966
|
+
} else if (typeof nodeOrId === "string") {
|
|
967
|
+
const found = nodes.find((n) => n.id === nodeOrId);
|
|
968
|
+
if (found) return { x: found.x ?? 0, y: found.y ?? 0 };
|
|
969
|
+
}
|
|
970
|
+
return null;
|
|
971
|
+
};
|
|
972
|
+
const sourcePos = getNodePosition(link.source);
|
|
973
|
+
const targetPos = getNodePosition(link.target);
|
|
974
|
+
if (!sourcePos || !targetPos) {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
const midX = (sourcePos.x + targetPos.x) / 2;
|
|
978
|
+
const midY = (sourcePos.y + targetPos.y) / 2;
|
|
979
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
980
|
+
/* @__PURE__ */ jsx(
|
|
981
|
+
"line",
|
|
982
|
+
{
|
|
983
|
+
x1: sourcePos.x,
|
|
984
|
+
y1: sourcePos.y,
|
|
985
|
+
x2: targetPos.x,
|
|
986
|
+
y2: targetPos.y,
|
|
987
|
+
"data-source": src,
|
|
988
|
+
"data-target": tgt,
|
|
989
|
+
stroke: link.color,
|
|
990
|
+
strokeWidth: link.width ?? defaultWidth ?? 1,
|
|
991
|
+
opacity: 0.6,
|
|
992
|
+
className: "cursor-pointer transition-opacity hover:opacity-100",
|
|
993
|
+
onClick: () => onClick?.(link)
|
|
994
|
+
}
|
|
995
|
+
),
|
|
996
|
+
showLabel && link.label && /* @__PURE__ */ jsx(
|
|
997
|
+
"text",
|
|
998
|
+
{
|
|
999
|
+
x: midX,
|
|
1000
|
+
y: midY,
|
|
1001
|
+
fill: "#666",
|
|
1002
|
+
fontSize: "10",
|
|
1003
|
+
textAnchor: "middle",
|
|
1004
|
+
dominantBaseline: "middle",
|
|
1005
|
+
pointerEvents: "none",
|
|
1006
|
+
children: link.label
|
|
1007
|
+
}
|
|
1008
|
+
)
|
|
1009
|
+
] });
|
|
1010
|
+
};
|
|
1011
|
+
var LinkItem_default = LinkItem;
|
|
749
1012
|
var ForceDirectedGraph = forwardRef(
|
|
750
1013
|
({
|
|
751
1014
|
nodes: initialNodes,
|
|
@@ -774,6 +1037,7 @@ var ForceDirectedGraph = forwardRef(
|
|
|
774
1037
|
const svgRef = useRef(null);
|
|
775
1038
|
const gRef = useRef(null);
|
|
776
1039
|
const [transform, setTransform] = useState({ k: 1, x: 0, y: 0 });
|
|
1040
|
+
const transformRef = useRef(transform);
|
|
777
1041
|
const dragNodeRef = useRef(null);
|
|
778
1042
|
const dragActiveRef = useRef(false);
|
|
779
1043
|
const [pinnedNodes, setPinnedNodes] = useState(/* @__PURE__ */ new Set());
|
|
@@ -781,73 +1045,177 @@ var ForceDirectedGraph = forwardRef(
|
|
|
781
1045
|
useEffect(() => {
|
|
782
1046
|
internalDragEnabledRef.current = enableDrag;
|
|
783
1047
|
}, [enableDrag]);
|
|
784
|
-
const onTick = (
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1048
|
+
const onTick = (_nodesCopy, _linksCopy, _sim) => {
|
|
1049
|
+
try {
|
|
1050
|
+
const boundsToUse = clusterBounds?.bounds ?? packageBounds;
|
|
1051
|
+
const nodeClusterMap = clusterBounds?.nodeToCluster ?? {};
|
|
1052
|
+
if (boundsToUse) {
|
|
1053
|
+
Object.values(nodesById).forEach((n) => {
|
|
1054
|
+
if (!n) return;
|
|
1055
|
+
const group = n.group ?? n.packageGroup;
|
|
1056
|
+
const clusterKey = nodeClusterMap[n.id];
|
|
1057
|
+
const key = clusterKey ?? (group ? `pkg:${group}` : void 0);
|
|
1058
|
+
if (!key) return;
|
|
1059
|
+
const center = boundsToUse[key];
|
|
1060
|
+
if (!center) return;
|
|
1061
|
+
const dx = center.x - (n.x ?? 0);
|
|
1062
|
+
const dy = center.y - (n.y ?? 0);
|
|
1063
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1064
|
+
const pullStrength = Math.min(0.5, 0.15 * (dist / (center.r || 200)) + 0.06);
|
|
1065
|
+
if (!isNaN(pullStrength) && isFinite(pullStrength)) {
|
|
1066
|
+
n.vx = (n.vx ?? 0) + dx / (dist || 1) * pullStrength;
|
|
1067
|
+
n.vy = (n.vy ?? 0) + dy / (dist || 1) * pullStrength;
|
|
794
1068
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const pack2 = d33.pack().size([width, height]).padding(30);
|
|
800
|
-
const packed = pack2(root);
|
|
801
|
-
const map = {};
|
|
802
|
-
if (packed.children) {
|
|
803
|
-
packed.children.forEach((c) => {
|
|
804
|
-
map[`pkg:${c.data.name}`] = { x: c.x, y: c.y, r: c.r * 0.95 };
|
|
805
|
-
});
|
|
806
|
-
effectiveBounds = map;
|
|
1069
|
+
if (center.r && dist > center.r) {
|
|
1070
|
+
const excess = (dist - center.r) / (dist || 1);
|
|
1071
|
+
n.vx = (n.vx ?? 0) - dx * 0.02 * excess;
|
|
1072
|
+
n.vy = (n.vy ?? 0) - dy * 0.02 * excess;
|
|
807
1073
|
}
|
|
808
|
-
}
|
|
809
|
-
} catch (e) {
|
|
1074
|
+
});
|
|
810
1075
|
}
|
|
1076
|
+
} catch (e) {
|
|
811
1077
|
}
|
|
812
|
-
|
|
1078
|
+
};
|
|
1079
|
+
const { packageAreas, localPositions } = React2__default.useMemo(() => {
|
|
813
1080
|
try {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
const
|
|
818
|
-
if (!
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1081
|
+
if (!initialNodes || !initialNodes.length) return { packageAreas: {}, localPositions: {} };
|
|
1082
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1083
|
+
initialNodes.forEach((n) => {
|
|
1084
|
+
const key = n.packageGroup || n.group || "root";
|
|
1085
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
1086
|
+
groups.get(key).push(n);
|
|
1087
|
+
});
|
|
1088
|
+
const groupKeys = Array.from(groups.keys());
|
|
1089
|
+
const children = groupKeys.map((k) => ({ name: k, value: Math.max(1, groups.get(k).length) }));
|
|
1090
|
+
const root = d33.hierarchy({ children });
|
|
1091
|
+
root.sum((d) => d.value);
|
|
1092
|
+
const pack2 = d33.pack().size([width, height]).padding(Math.max(20, Math.min(width, height) * 0.03));
|
|
1093
|
+
const packed = pack2(root);
|
|
1094
|
+
const packageAreas2 = {};
|
|
1095
|
+
if (packed.children) {
|
|
1096
|
+
packed.children.forEach((c) => {
|
|
1097
|
+
const name = c.data.name;
|
|
1098
|
+
packageAreas2[name] = { x: c.x, y: c.y, r: Math.max(40, c.r) };
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
const localPositions2 = {};
|
|
1102
|
+
groups.forEach((nodesInGroup, _key) => {
|
|
1103
|
+
if (!nodesInGroup || nodesInGroup.length === 0) return;
|
|
1104
|
+
const localNodes = nodesInGroup.map((n) => ({ id: n.id, x: Math.random() * 10 - 5, y: Math.random() * 10 - 5, size: n.size || 10 }));
|
|
1105
|
+
const localLinks = (initialLinks || []).filter((l) => {
|
|
1106
|
+
const s = typeof l.source === "string" ? l.source : l.source && l.source.id;
|
|
1107
|
+
const t = typeof l.target === "string" ? l.target : l.target && l.target.id;
|
|
1108
|
+
return localNodes.some((ln) => ln.id === s) && localNodes.some((ln) => ln.id === t);
|
|
1109
|
+
}).map((l) => ({ source: typeof l.source === "string" ? l.source : l.source.id, target: typeof l.target === "string" ? l.target : l.target.id }));
|
|
1110
|
+
if (localNodes.length === 1) {
|
|
1111
|
+
localPositions2[localNodes[0].id] = { x: 0, y: 0 };
|
|
1112
|
+
return;
|
|
832
1113
|
}
|
|
1114
|
+
const sim = d33.forceSimulation(localNodes).force("link", d33.forceLink(localLinks).id((d) => d.id).distance(30).strength(0.8)).force("charge", d33.forceManyBody().strength(-15)).force("collide", d33.forceCollide((d) => (d.size || 10) + 6).iterations(2)).stop();
|
|
1115
|
+
const ticks = 300;
|
|
1116
|
+
for (let i = 0; i < ticks; i++) sim.tick();
|
|
1117
|
+
localNodes.forEach((ln) => {
|
|
1118
|
+
localPositions2[ln.id] = { x: ln.x ?? 0, y: ln.y ?? 0 };
|
|
1119
|
+
});
|
|
833
1120
|
});
|
|
1121
|
+
return { packageAreas: packageAreas2, localPositions: localPositions2 };
|
|
834
1122
|
} catch (e) {
|
|
1123
|
+
return { packageAreas: {}, localPositions: {} };
|
|
835
1124
|
}
|
|
836
|
-
};
|
|
837
|
-
const
|
|
1125
|
+
}, [initialNodes, initialLinks, width, height]);
|
|
1126
|
+
const seededNodes = React2__default.useMemo(() => {
|
|
1127
|
+
if (!initialNodes || !Object.keys(packageAreas || {}).length) return initialNodes;
|
|
1128
|
+
return initialNodes.map((n) => {
|
|
1129
|
+
const key = n.packageGroup || n.group || "root";
|
|
1130
|
+
const area = packageAreas[key];
|
|
1131
|
+
const lp = localPositions[n.id];
|
|
1132
|
+
if (!area || !lp) return n;
|
|
1133
|
+
const scale = Math.max(0.5, area.r * 0.6 / (Math.max(1, Math.sqrt(lp.x * lp.x + lp.y * lp.y)) || 1));
|
|
1134
|
+
return { ...n, x: area.x + lp.x * scale, y: area.y + lp.y * scale };
|
|
1135
|
+
});
|
|
1136
|
+
}, [initialNodes, packageAreas, localPositions]);
|
|
1137
|
+
const { nodes, links, restart, stop, setForcesEnabled } = useForceSimulation(seededNodes || initialNodes, initialLinks, {
|
|
838
1138
|
width,
|
|
839
1139
|
height,
|
|
840
1140
|
chargeStrength: manualLayout ? 0 : void 0,
|
|
841
1141
|
onTick,
|
|
842
1142
|
...simulationOptions
|
|
843
1143
|
});
|
|
1144
|
+
const nodesById = React2__default.useMemo(() => {
|
|
1145
|
+
const m = {};
|
|
1146
|
+
(nodes || []).forEach((n) => {
|
|
1147
|
+
if (n && n.id) m[n.id] = n;
|
|
1148
|
+
});
|
|
1149
|
+
return m;
|
|
1150
|
+
}, [nodes]);
|
|
1151
|
+
const clusterBounds = React2__default.useMemo(() => {
|
|
1152
|
+
try {
|
|
1153
|
+
if (!links || !nodes) return null;
|
|
1154
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
1155
|
+
const adj = /* @__PURE__ */ new Map();
|
|
1156
|
+
nodes.forEach((n) => adj.set(n.id, /* @__PURE__ */ new Set()));
|
|
1157
|
+
links.forEach((l) => {
|
|
1158
|
+
const type = l.type || "reference";
|
|
1159
|
+
if (type !== "dependency") return;
|
|
1160
|
+
const s = typeof l.source === "string" ? l.source : l.source && l.source.id || null;
|
|
1161
|
+
const t = typeof l.target === "string" ? l.target : l.target && l.target.id || null;
|
|
1162
|
+
if (!s || !t) return;
|
|
1163
|
+
if (!nodeIds.has(s) || !nodeIds.has(t)) return;
|
|
1164
|
+
adj.get(s)?.add(t);
|
|
1165
|
+
adj.get(t)?.add(s);
|
|
1166
|
+
});
|
|
1167
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1168
|
+
const comps = [];
|
|
1169
|
+
for (const nid of nodeIds) {
|
|
1170
|
+
if (visited.has(nid)) continue;
|
|
1171
|
+
const stack = [nid];
|
|
1172
|
+
const comp = [];
|
|
1173
|
+
visited.add(nid);
|
|
1174
|
+
while (stack.length) {
|
|
1175
|
+
const cur = stack.pop();
|
|
1176
|
+
comp.push(cur);
|
|
1177
|
+
const neigh = adj.get(cur);
|
|
1178
|
+
if (!neigh) continue;
|
|
1179
|
+
for (const nb of neigh) {
|
|
1180
|
+
if (!visited.has(nb)) {
|
|
1181
|
+
visited.add(nb);
|
|
1182
|
+
stack.push(nb);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
comps.push(comp);
|
|
1187
|
+
}
|
|
1188
|
+
if (comps.length <= 1) return null;
|
|
1189
|
+
const children = comps.map((c, i) => ({ name: String(i), value: Math.max(1, c.length) }));
|
|
1190
|
+
d33.hierarchy({ children }).sum((d) => d.value).sort((a, b) => b.value - a.value);
|
|
1191
|
+
const num = comps.length;
|
|
1192
|
+
const cx = width / 2;
|
|
1193
|
+
const cy = height / 2;
|
|
1194
|
+
const base = Math.max(width, height);
|
|
1195
|
+
const circleRadius = base * Math.max(30, num * 20, Math.sqrt(num) * 12);
|
|
1196
|
+
const map = {};
|
|
1197
|
+
comps.forEach((c, i) => {
|
|
1198
|
+
const angle = 2 * Math.PI * i / num;
|
|
1199
|
+
const x = cx + Math.cos(angle) * circleRadius;
|
|
1200
|
+
const y = cy + Math.sin(angle) * circleRadius;
|
|
1201
|
+
const sizeBias = Math.sqrt(Math.max(1, c.length));
|
|
1202
|
+
const r = Math.max(200, 100 * sizeBias);
|
|
1203
|
+
map[`cluster:${i}`] = { x, y, r };
|
|
1204
|
+
});
|
|
1205
|
+
const nodeToCluster = {};
|
|
1206
|
+
comps.forEach((c, i) => c.forEach((nid) => nodeToCluster[nid] = `cluster:${i}`));
|
|
1207
|
+
return { bounds: map, nodeToCluster };
|
|
1208
|
+
} catch (e) {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
}, [nodes, links, width, height]);
|
|
844
1212
|
useEffect(() => {
|
|
845
|
-
if (!packageBounds) return;
|
|
1213
|
+
if (!packageBounds && !clusterBounds && (!packageAreas || Object.keys(packageAreas).length === 0)) return;
|
|
846
1214
|
try {
|
|
847
1215
|
restart();
|
|
848
1216
|
} catch (e) {
|
|
849
1217
|
}
|
|
850
|
-
}, [packageBounds, restart]);
|
|
1218
|
+
}, [packageBounds, clusterBounds, packageAreas, restart]);
|
|
851
1219
|
useEffect(() => {
|
|
852
1220
|
try {
|
|
853
1221
|
if (manualLayout || pinnedNodes.size > 0) setForcesEnabled(false);
|
|
@@ -935,6 +1303,7 @@ var ForceDirectedGraph = forwardRef(
|
|
|
935
1303
|
const g = d33.select(gRef.current);
|
|
936
1304
|
const zoom2 = d33.zoom().scaleExtent([0.1, 10]).on("zoom", (event) => {
|
|
937
1305
|
g.attr("transform", event.transform);
|
|
1306
|
+
transformRef.current = event.transform;
|
|
938
1307
|
setTransform(event.transform);
|
|
939
1308
|
});
|
|
940
1309
|
svg.call(zoom2);
|
|
@@ -942,6 +1311,26 @@ var ForceDirectedGraph = forwardRef(
|
|
|
942
1311
|
svg.on(".zoom", null);
|
|
943
1312
|
};
|
|
944
1313
|
}, [enableZoom]);
|
|
1314
|
+
useEffect(() => {
|
|
1315
|
+
if (!gRef.current) return;
|
|
1316
|
+
try {
|
|
1317
|
+
const g = d33.select(gRef.current);
|
|
1318
|
+
g.selectAll("g.node").each(function() {
|
|
1319
|
+
const datum = d33.select(this).datum();
|
|
1320
|
+
if (!datum) return;
|
|
1321
|
+
d33.select(this).attr("transform", `translate(${datum.x || 0},${datum.y || 0})`);
|
|
1322
|
+
});
|
|
1323
|
+
g.selectAll("line").each(function() {
|
|
1324
|
+
const l = d33.select(this).datum();
|
|
1325
|
+
if (!l) return;
|
|
1326
|
+
const s = typeof l.source === "object" ? l.source : nodes.find((n) => n.id === l.source) || l.source;
|
|
1327
|
+
const t = typeof l.target === "object" ? l.target : nodes.find((n) => n.id === l.target) || l.target;
|
|
1328
|
+
if (!s || !t) return;
|
|
1329
|
+
d33.select(this).attr("x1", s.x).attr("y1", s.y).attr("x2", t.x).attr("y2", t.y);
|
|
1330
|
+
});
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
}
|
|
1333
|
+
}, [nodes, links]);
|
|
945
1334
|
const handleDragStart = useCallback(
|
|
946
1335
|
(event, node) => {
|
|
947
1336
|
if (!enableDrag) return;
|
|
@@ -966,8 +1355,9 @@ var ForceDirectedGraph = forwardRef(
|
|
|
966
1355
|
const svg = svgRef.current;
|
|
967
1356
|
if (!svg) return;
|
|
968
1357
|
const rect = svg.getBoundingClientRect();
|
|
969
|
-
const
|
|
970
|
-
const
|
|
1358
|
+
const t = transformRef.current;
|
|
1359
|
+
const x = (event.clientX - rect.left - t.x) / t.k;
|
|
1360
|
+
const y = (event.clientY - rect.top - t.y) / t.k;
|
|
971
1361
|
dragNodeRef.current.fx = x;
|
|
972
1362
|
dragNodeRef.current.fy = y;
|
|
973
1363
|
};
|
|
@@ -994,7 +1384,7 @@ var ForceDirectedGraph = forwardRef(
|
|
|
994
1384
|
window.removeEventListener("mouseout", handleWindowLeave);
|
|
995
1385
|
window.removeEventListener("blur", handleWindowUp);
|
|
996
1386
|
};
|
|
997
|
-
}, [enableDrag
|
|
1387
|
+
}, [enableDrag]);
|
|
998
1388
|
useEffect(() => {
|
|
999
1389
|
if (!gRef.current || !enableDrag) return;
|
|
1000
1390
|
const g = d33.select(gRef.current);
|
|
@@ -1117,98 +1507,35 @@ var ForceDirectedGraph = forwardRef(
|
|
|
1117
1507
|
}
|
|
1118
1508
|
) }),
|
|
1119
1509
|
/* @__PURE__ */ jsxs("g", { ref: gRef, children: [
|
|
1120
|
-
links.map((link, i) =>
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
children: link.label
|
|
1150
|
-
}
|
|
1151
|
-
)
|
|
1152
|
-
] }, `link-${i}`);
|
|
1153
|
-
}),
|
|
1154
|
-
nodes.map((node) => {
|
|
1155
|
-
if (node.x == null || node.y == null) return null;
|
|
1156
|
-
const isSelected = selectedNodeId === node.id;
|
|
1157
|
-
const isHovered = hoveredNodeId === node.id;
|
|
1158
|
-
const nodeSize = node.size || defaultNodeSize;
|
|
1159
|
-
const nodeColor = node.color || defaultNodeColor;
|
|
1160
|
-
return /* @__PURE__ */ jsxs(
|
|
1161
|
-
"g",
|
|
1162
|
-
{
|
|
1163
|
-
transform: `translate(${node.x},${node.y})`,
|
|
1164
|
-
className: "cursor-pointer node",
|
|
1165
|
-
"data-id": node.id,
|
|
1166
|
-
onClick: () => handleNodeClick(node),
|
|
1167
|
-
onDoubleClick: (event) => handleNodeDoubleClick(event, node),
|
|
1168
|
-
onMouseEnter: () => handleNodeMouseEnter(node),
|
|
1169
|
-
onMouseLeave: handleNodeMouseLeave,
|
|
1170
|
-
onMouseDown: (e) => handleDragStart(e, node),
|
|
1171
|
-
children: [
|
|
1172
|
-
/* @__PURE__ */ jsx(
|
|
1173
|
-
"circle",
|
|
1174
|
-
{
|
|
1175
|
-
r: nodeSize,
|
|
1176
|
-
fill: nodeColor,
|
|
1177
|
-
stroke: isSelected ? "#000" : isHovered ? "#666" : "none",
|
|
1178
|
-
strokeWidth: pinnedNodes.has(node.id) ? 3 : isSelected ? 2.5 : isHovered ? 2 : 1.5,
|
|
1179
|
-
opacity: isHovered || isSelected ? 1 : 0.9,
|
|
1180
|
-
className: "transition-all"
|
|
1181
|
-
}
|
|
1182
|
-
),
|
|
1183
|
-
pinnedNodes.has(node.id) && /* @__PURE__ */ jsx(
|
|
1184
|
-
"circle",
|
|
1185
|
-
{
|
|
1186
|
-
r: nodeSize + 4,
|
|
1187
|
-
fill: "none",
|
|
1188
|
-
stroke: "#ff6b6b",
|
|
1189
|
-
strokeWidth: 1,
|
|
1190
|
-
opacity: 0.5,
|
|
1191
|
-
className: "pointer-events-none"
|
|
1192
|
-
}
|
|
1193
|
-
),
|
|
1194
|
-
showNodeLabels && node.label && /* @__PURE__ */ jsx(
|
|
1195
|
-
"text",
|
|
1196
|
-
{
|
|
1197
|
-
y: nodeSize + 15,
|
|
1198
|
-
fill: "#333",
|
|
1199
|
-
fontSize: "12",
|
|
1200
|
-
textAnchor: "middle",
|
|
1201
|
-
dominantBaseline: "middle",
|
|
1202
|
-
pointerEvents: "none",
|
|
1203
|
-
className: "select-none",
|
|
1204
|
-
children: node.label
|
|
1205
|
-
}
|
|
1206
|
-
)
|
|
1207
|
-
]
|
|
1208
|
-
},
|
|
1209
|
-
node.id
|
|
1210
|
-
);
|
|
1211
|
-
}),
|
|
1510
|
+
links.map((link, i) => /* @__PURE__ */ jsx(
|
|
1511
|
+
LinkItem_default,
|
|
1512
|
+
{
|
|
1513
|
+
link,
|
|
1514
|
+
onClick: handleLinkClick,
|
|
1515
|
+
defaultWidth: defaultLinkWidth,
|
|
1516
|
+
showLabel: showLinkLabels,
|
|
1517
|
+
nodes
|
|
1518
|
+
},
|
|
1519
|
+
`link-${i}`
|
|
1520
|
+
)),
|
|
1521
|
+
nodes.map((node) => /* @__PURE__ */ jsx(
|
|
1522
|
+
NodeItem_default,
|
|
1523
|
+
{
|
|
1524
|
+
node,
|
|
1525
|
+
isSelected: selectedNodeId === node.id,
|
|
1526
|
+
isHovered: hoveredNodeId === node.id,
|
|
1527
|
+
pinned: pinnedNodes.has(node.id),
|
|
1528
|
+
defaultNodeSize,
|
|
1529
|
+
defaultNodeColor,
|
|
1530
|
+
showLabel: showNodeLabels,
|
|
1531
|
+
onClick: handleNodeClick,
|
|
1532
|
+
onDoubleClick: handleNodeDoubleClick,
|
|
1533
|
+
onMouseEnter: handleNodeMouseEnter,
|
|
1534
|
+
onMouseLeave: handleNodeMouseLeave,
|
|
1535
|
+
onMouseDown: handleDragStart
|
|
1536
|
+
},
|
|
1537
|
+
node.id
|
|
1538
|
+
)),
|
|
1212
1539
|
packageBounds && Object.keys(packageBounds).length > 0 && /* @__PURE__ */ jsx("g", { className: "package-boundaries", pointerEvents: "none", children: Object.entries(packageBounds).map(([pid, b]) => /* @__PURE__ */ jsxs("g", { children: [
|
|
1213
1540
|
/* @__PURE__ */ jsx(
|
|
1214
1541
|
"circle",
|