@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks/useForceSimulation.ts"],"names":[],"mappings":"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","file":"useForceSimulation.js","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport * as d3 from 'd3';\n\nexport interface SimulationNode extends d3.SimulationNodeDatum {\n id: string;\n [key: string]: any;\n}\n\nexport interface SimulationLink extends d3.SimulationLinkDatum<SimulationNode> {\n source: string | SimulationNode;\n target: string | SimulationNode;\n [key: string]: any;\n}\n\nexport interface ForceSimulationOptions {\n /**\n * Strength of the charge force (repulsion between nodes)\n * @default -300\n */\n chargeStrength?: number;\n\n /**\n * Distance for links between nodes\n * @default 100\n */\n linkDistance?: number;\n\n /**\n * Strength of the link force\n * @default 1\n */\n linkStrength?: number;\n\n /**\n * Strength of collision detection\n * @default 1\n */\n collisionStrength?: number;\n\n /**\n * Radius for collision detection (node size)\n * @default 10\n */\n collisionRadius?: number;\n\n /**\n * Strength of centering force\n * @default 0.1\n */\n centerStrength?: number;\n\n /**\n * Width of the simulation space\n */\n width: number;\n\n /**\n * Height of the simulation space\n */\n height: number;\n\n /**\n * Alpha decay rate (how quickly the simulation cools down)\n * @default 0.0228\n */\n alphaDecay?: number;\n\n /**\n * Velocity decay (friction)\n * @default 0.4\n */\n velocityDecay?: number;\n\n /**\n * Optional tick callback invoked on each simulation tick with current nodes/links and the simulation instance\n */\n onTick?: (nodes: SimulationNode[], links: SimulationLink[], sim: d3.Simulation<SimulationNode, SimulationLink>) => void;\n}\n\nexport interface UseForceSimulationReturn {\n /**\n * Current nodes with positions\n */\n nodes: SimulationNode[];\n\n /**\n * Current links\n */\n links: SimulationLink[];\n\n /**\n * Restart the simulation\n */\n restart: () => void;\n\n /**\n * Stop the simulation\n */\n stop: () => void;\n\n /**\n * Whether the simulation is currently running\n */\n isRunning: boolean;\n\n /**\n * Current alpha value (simulation heat)\n */\n alpha: number;\n}\n\n/**\n * Hook for managing d3-force simulations\n * Automatically handles simulation lifecycle, tick updates, and cleanup\n *\n * @param initialNodes - Initial nodes for the simulation\n * @param initialLinks - Initial links for the simulation\n * @param options - Configuration options for the force simulation\n * @returns Simulation state and control functions\n *\n * @example\n * ```tsx\n * function NetworkGraph() {\n * const nodes = [\n * { id: 'node1', name: 'Node 1' },\n * { id: 'node2', name: 'Node 2' },\n * { id: 'node3', name: 'Node 3' },\n * ];\n *\n * const links = [\n * { source: 'node1', target: 'node2' },\n * { source: 'node2', target: 'node3' },\n * ];\n *\n * const { nodes: simulatedNodes, links: simulatedLinks, restart } = useForceSimulation(\n * nodes,\n * links,\n * {\n * width: 800,\n * height: 600,\n * chargeStrength: -500,\n * linkDistance: 150,\n * }\n * );\n *\n * return (\n * <svg width={800} height={600}>\n * {simulatedLinks.map((link, i) => (\n * <line\n * key={i}\n * x1={(link.source as SimulationNode).x}\n * y1={(link.source as SimulationNode).y}\n * x2={(link.target as SimulationNode).x}\n * y2={(link.target as SimulationNode).y}\n * stroke=\"#999\"\n * />\n * ))}\n * {simulatedNodes.map((node) => (\n * <circle\n * key={node.id}\n * cx={node.x}\n * cy={node.y}\n * r={10}\n * fill=\"#69b3a2\"\n * />\n * ))}\n * </svg>\n * );\n * }\n * ```\n */\nexport function useForceSimulation(\n initialNodes: SimulationNode[],\n initialLinks: SimulationLink[],\n options: ForceSimulationOptions\n): UseForceSimulationReturn & { setForcesEnabled: (enabled: boolean) => void } {\n const {\n chargeStrength = -300,\n linkDistance = 100,\n linkStrength = 1,\n collisionStrength = 1,\n collisionRadius = 10,\n centerStrength = 0.1,\n width,\n height,\n alphaDecay = 0.0228,\n velocityDecay = 0.4,\n onTick,\n } = options;\n\n const [nodes, setNodes] = useState<SimulationNode[]>(initialNodes);\n const [links, setLinks] = useState<SimulationLink[]>(initialLinks);\n const [isRunning, setIsRunning] = useState(false);\n const [alpha, setAlpha] = useState(1);\n\n const simulationRef = useRef<d3.Simulation<SimulationNode, SimulationLink> | null>(null);\n\n useEffect(() => {\n // Create a copy of nodes and links to avoid mutating the original data\n const nodesCopy = initialNodes.map((node) => ({ ...node }));\n const linksCopy = initialLinks.map((link) => ({ ...link }));\n\n // Create the simulation\n const simulation = d3\n .forceSimulation<SimulationNode>(nodesCopy)\n .force(\n 'link',\n d3\n .forceLink<SimulationNode, SimulationLink>(linksCopy)\n .id((d) => d.id)\n .distance((d: any) => (d && d.distance != null ? d.distance : linkDistance))\n .strength(linkStrength)\n )\n .force('charge', d3.forceManyBody().strength(chargeStrength))\n .force('center', d3.forceCenter(width / 2, height / 2).strength(centerStrength))\n .force(\n 'collision',\n d3\n .forceCollide<SimulationNode>()\n .radius((d: any) => {\n // Use node-specific size when available and add configured padding to ensure minimum spacing\n const nodeSize = (d && d.size) ? d.size : 10;\n return nodeSize + collisionRadius;\n })\n .strength(collisionStrength)\n )\n .alphaDecay(alphaDecay)\n .velocityDecay(velocityDecay);\n\n simulationRef.current = simulation;\n\n // Update state on each tick\n simulation.on('tick', () => {\n try {\n if (typeof onTick === 'function') onTick(nodesCopy, linksCopy, simulation);\n } catch (e) {\n // ignore user tick errors\n }\n setNodes([...nodesCopy]);\n setLinks([...linksCopy]);\n setAlpha(simulation.alpha());\n setIsRunning(simulation.alpha() > simulation.alphaMin());\n });\n\n simulation.on('end', () => {\n setIsRunning(false);\n });\n\n // Cleanup on unmount\n return () => {\n simulation.stop();\n };\n }, [\n initialNodes,\n initialLinks,\n chargeStrength,\n linkDistance,\n linkStrength,\n collisionStrength,\n collisionRadius,\n centerStrength,\n width,\n height,\n alphaDecay,\n velocityDecay,\n onTick,\n ]);\n\n const restart = () => {\n if (simulationRef.current) {\n simulationRef.current.alpha(1).restart();\n setIsRunning(true);\n }\n };\n\n const stop = () => {\n if (simulationRef.current) {\n simulationRef.current.stop();\n setIsRunning(false);\n }\n };\n\n const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });\n const forcesEnabledRef = useRef(true);\n\n const setForcesEnabled = (enabled: boolean) => {\n const sim = simulationRef.current;\n if (!sim) return;\n // avoid repeated updates\n if (forcesEnabledRef.current === enabled) return;\n forcesEnabledRef.current = enabled;\n\n try {\n // Only toggle charge and link forces to avoid collapse; keep collision/centering\n const charge: any = sim.force('charge');\n if (charge && typeof charge.strength === 'function') {\n charge.strength(enabled ? originalForcesRef.current.charge : 0);\n }\n\n const link: any = sim.force('link');\n if (link && typeof link.strength === 'function') {\n link.strength(enabled ? originalForcesRef.current.link : 0);\n }\n } catch (e) {\n // ignore\n }\n };\n\n return {\n nodes,\n links,\n restart,\n stop,\n isRunning,\n alpha,\n setForcesEnabled,\n };\n}\n\n/**\n * Hook for creating a draggable force simulation\n * Provides drag handlers that can be attached to node elements\n *\n * @param simulation - The d3 force simulation instance\n * @returns Drag behavior that can be applied to nodes\n *\n * @example\n * ```tsx\n * function DraggableNetworkGraph() {\n * const simulation = useRef<d3.Simulation<SimulationNode, SimulationLink>>();\n * const drag = useDrag(simulation.current);\n *\n * return (\n * <svg>\n * {nodes.map((node) => (\n * <circle\n * key={node.id}\n * {...drag}\n * cx={node.x}\n * cy={node.y}\n * r={10}\n * />\n * ))}\n * </svg>\n * );\n * }\n * ```\n */\nexport function useDrag(simulation: d3.Simulation<SimulationNode, any> | null | undefined) {\n const dragStarted = (event: any, node: SimulationNode) => {\n if (!simulation) return;\n if (!event.active) simulation.alphaTarget(0.3).restart();\n node.fx = node.x;\n node.fy = node.y;\n };\n\n const dragged = (event: any, node: SimulationNode) => {\n node.fx = event.x;\n node.fy = event.y;\n };\n\n const dragEnded = (event: any, node: SimulationNode) => {\n if (!simulation) return;\n if (!event.active) simulation.alphaTarget(0);\n node.fx = null;\n node.fy = null;\n };\n\n return {\n onDragStart: dragStarted,\n onDrag: dragged,\n onDragEnd: dragEnded,\n };\n}"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks/useForceSimulation.ts"],"names":["e"],"mappings":"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","file":"useForceSimulation.js","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport * as d3 from 'd3';\n\nexport interface SimulationNode extends d3.SimulationNodeDatum {\n id: string;\n [key: string]: any;\n}\n\nexport interface SimulationLink extends d3.SimulationLinkDatum<SimulationNode> {\n source: string | SimulationNode;\n target: string | SimulationNode;\n [key: string]: any;\n}\n\nexport interface ForceSimulationOptions {\n /**\n * Strength of the charge force (repulsion between nodes)\n * @default -300\n */\n chargeStrength?: number;\n\n /**\n * Distance for links between nodes\n * @default 100\n */\n linkDistance?: number;\n\n /**\n * Strength of the link force\n * @default 1\n */\n linkStrength?: number;\n\n /**\n * Strength of collision detection\n * @default 1\n */\n collisionStrength?: number;\n\n /**\n * Radius for collision detection (node size)\n * @default 10\n */\n collisionRadius?: number;\n\n /**\n * Strength of centering force\n * @default 0.1\n */\n centerStrength?: number;\n\n /**\n * Width of the simulation space\n */\n width: number;\n\n /**\n * Height of the simulation space\n */\n height: number;\n\n /**\n * Alpha decay rate (how quickly the simulation cools down)\n * @default 0.0228\n */\n alphaDecay?: number;\n\n /**\n * Alpha target controls the resting energy of the simulation. When set to 0\n * the simulation will cool and stop moving once forces settle. Increase to\n * keep the graph more dynamic.\n * @default 0\n */\n alphaTarget?: number;\n\n /**\n * Warm alpha used when (re)starting the simulation to give it a small amount\n * of energy. This mirrors the Observable example which sets a modest\n * alphaTarget when dragging instead of forcing alpha to 1.\n * @default 0.3\n */\n warmAlpha?: number;\n\n /**\n * Minimum alpha threshold below which the simulation is considered cooled\n * and will stop. Increasing this makes the simulation stop earlier.\n * @default 0.01\n */\n alphaMin?: number;\n\n /**\n * When true, zero node velocities and snap positions when the simulation\n * stops to reduce residual jitter.\n * @default true\n */\n stabilizeOnStop?: boolean;\n\n /**\n * Maximum time (ms) to allow the simulation to run after creation/restart.\n * If the simulation hasn't cooled by this time, it will be force-stopped\n * to prevent indefinite animation. Set to 0 to disable.\n * @default 3000\n */\n maxSimulationTimeMs?: number;\n\n /**\n * Velocity decay (friction)\n * @default 0.4\n */\n velocityDecay?: number;\n\n /**\n * Optional tick callback invoked on each simulation tick with current nodes/links and the simulation instance\n */\n onTick?: (nodes: SimulationNode[], links: SimulationLink[], sim: d3.Simulation<SimulationNode, SimulationLink>) => void;\n}\n\nexport interface UseForceSimulationReturn {\n /**\n * Current nodes with positions\n */\n nodes: SimulationNode[];\n\n /**\n * Current links\n */\n links: SimulationLink[];\n\n /**\n * Restart the simulation\n */\n restart: () => void;\n\n /**\n * Stop the simulation\n */\n stop: () => void;\n\n /**\n * Whether the simulation is currently running\n */\n isRunning: boolean;\n\n /**\n * Current alpha value (simulation heat)\n */\n alpha: number;\n}\n\n/**\n * Hook for managing d3-force simulations\n * Automatically handles simulation lifecycle, tick updates, and cleanup\n *\n * @param initialNodes - Initial nodes for the simulation\n * @param initialLinks - Initial links for the simulation\n * @param options - Configuration options for the force simulation\n * @returns Simulation state and control functions\n *\n * @example\n * ```tsx\n * function NetworkGraph() {\n * const nodes = [\n * { id: 'node1', name: 'Node 1' },\n * { id: 'node2', name: 'Node 2' },\n * { id: 'node3', name: 'Node 3' },\n * ];\n *\n * const links = [\n * { source: 'node1', target: 'node2' },\n * { source: 'node2', target: 'node3' },\n * ];\n *\n * const { nodes: simulatedNodes, links: simulatedLinks, restart } = useForceSimulation(\n * nodes,\n * links,\n * {\n * width: 800,\n * height: 600,\n * chargeStrength: -500,\n * linkDistance: 150,\n * }\n * );\n *\n * return (\n * <svg width={800} height={600}>\n * {simulatedLinks.map((link, i) => (\n * <line\n * key={i}\n * x1={(link.source as SimulationNode).x}\n * y1={(link.source as SimulationNode).y}\n * x2={(link.target as SimulationNode).x}\n * y2={(link.target as SimulationNode).y}\n * stroke=\"#999\"\n * />\n * ))}\n * {simulatedNodes.map((node) => (\n * <circle\n * key={node.id}\n * cx={node.x}\n * cy={node.y}\n * r={10}\n * fill=\"#69b3a2\"\n * />\n * ))}\n * </svg>\n * );\n * }\n * ```\n */\nexport function useForceSimulation(\n initialNodes: SimulationNode[],\n initialLinks: SimulationLink[],\n options: ForceSimulationOptions\n): UseForceSimulationReturn & { setForcesEnabled: (enabled: boolean) => void } {\n const {\n chargeStrength = -300,\n linkDistance = 100,\n linkStrength = 1,\n collisionStrength = 1,\n collisionRadius = 10,\n centerStrength = 0.1,\n width,\n height,\n alphaDecay = 0.0228,\n velocityDecay = 0.4,\n alphaTarget = 0,\n warmAlpha = 0.3,\n alphaMin = 0.01,\n // @ts-ignore allow extra option\n stabilizeOnStop = true,\n onTick,\n // Optional throttle in milliseconds for tick updates (reduce React re-renders)\n // Lower values = smoother but more CPU; default ~30ms (~33fps)\n // @ts-ignore allow extra option\n tickThrottleMs = 33,\n // @ts-ignore allow extra option\n maxSimulationTimeMs = 3000,\n } = options;\n\n const [nodes, setNodes] = useState<SimulationNode[]>(initialNodes);\n const [links, setLinks] = useState<SimulationLink[]>(initialLinks);\n const [isRunning, setIsRunning] = useState(false);\n const [alpha, setAlpha] = useState(1);\n\n const simulationRef = useRef<d3.Simulation<SimulationNode, SimulationLink> | null>(null);\n const stopTimeoutRef = useRef<number | null>(null);\n\n // Create lightweight keys for nodes/links so we only recreate the simulation\n // when the actual identity/content of inputs change (not when parent passes\n // new array references on each render).\n const nodesKey = initialNodes.map((n) => n.id).join('|');\n const linksKey = (initialLinks || []).map((l) => {\n const s = typeof l.source === 'string' ? l.source : (l.source as any)?.id;\n const t = typeof l.target === 'string' ? l.target : (l.target as any)?.id;\n return `${s}->${t}:${(l as any).type || ''}`;\n }).join('|');\n\n useEffect(() => {\n // Create a copy of nodes and links to avoid mutating the original data\n const nodesCopy = initialNodes.map((node) => ({ ...node }));\n const linksCopy = initialLinks.map((link) => ({ ...link }));\n\n // ALWAYS seed initial positions to ensure nodes don't stack at origin\n // This is critical for force-directed graphs to work properly\n try {\n // Always seed positions for all nodes when simulation is created\n // This ensures nodes start spread out even if they have coordinates\n nodesCopy.forEach((n, i) => {\n // Use deterministic but more widely spread positions based on index\n const angle = (i * 2 * Math.PI) / nodesCopy.length;\n // Larger seed radius to encourage an initial spread\n const radius = Math.min(width, height) * 0.45;\n n.x = width / 2 + radius * Math.cos(angle);\n n.y = height / 2 + radius * Math.sin(angle);\n // Add very small random velocity to avoid large initial motion\n (n as any).vx = (Math.random() - 0.5) * 2;\n (n as any).vy = (Math.random() - 0.5) * 2;\n });\n } catch (e) {\n // If error, fall back to random positions\n nodesCopy.forEach((n) => {\n n.x = Math.random() * width;\n n.y = Math.random() * height;\n (n as any).vx = (Math.random() - 0.5) * 10;\n (n as any).vy = (Math.random() - 0.5) * 10;\n });\n }\n\n // Create the simulation\n const simulation = (d3.forceSimulation(nodesCopy as any) as unknown) as d3.Simulation<SimulationNode, SimulationLink>;\n\n // Configure link force separately to avoid using generic type args on d3 helpers\n try {\n const linkForce = (d3.forceLink(linksCopy as any) as unknown) as d3.ForceLink<SimulationNode, SimulationLink>;\n linkForce.id((d: any) => d.id).distance((d: any) => (d && d.distance != null ? d.distance : linkDistance)).strength(linkStrength);\n simulation.force('link', linkForce as any);\n } catch (e) {\n // fallback: attach a plain link force\n try { simulation.force('link', d3.forceLink(linksCopy as any) as any); } catch (e) {}\n }\n ;\n\n try {\n simulation.force('charge', d3.forceManyBody().strength(chargeStrength) as any);\n simulation.force('center', d3.forceCenter(width / 2, height / 2).strength(centerStrength) as any);\n const collide = d3.forceCollide().radius((d: any) => {\n const nodeSize = (d && d.size) ? d.size : 10;\n return nodeSize + collisionRadius;\n }).strength(collisionStrength as any) as any;\n simulation.force('collision', collide);\n simulation.force('x', d3.forceX(width / 2).strength(Math.max(0.02, centerStrength * 0.5)) as any);\n simulation.force('y', d3.forceY(height / 2).strength(Math.max(0.02, centerStrength * 0.5)) as any);\n simulation.alphaDecay(alphaDecay);\n simulation.velocityDecay(velocityDecay);\n simulation.alphaMin(alphaMin);\n try { simulation.alphaTarget(alphaTarget); } catch (e) {}\n try { simulation.alpha(warmAlpha); } catch (e) {}\n } catch (e) {\n // ignore force configuration errors\n }\n\n simulationRef.current = simulation;\n\n // Force-stop timeout to ensure simulation doesn't run forever.\n if (stopTimeoutRef.current != null) {\n try { (globalThis.clearTimeout as any)(stopTimeoutRef.current); } catch (e) {}\n stopTimeoutRef.current = null;\n }\n if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {\n stopTimeoutRef.current = (globalThis.setTimeout as any)(() => {\n try {\n if (stabilizeOnStop) {\n nodesCopy.forEach((n) => {\n (n as any).vx = 0;\n (n as any).vy = 0;\n if (typeof n.x === 'number') n.x = Number(n.x.toFixed(3));\n if (typeof n.y === 'number') n.y = Number(n.y.toFixed(3));\n });\n }\n simulation.alpha(0);\n simulation.stop();\n } catch (e) {}\n setIsRunning(false);\n setNodes([...nodesCopy]);\n setLinks([...linksCopy]);\n }, maxSimulationTimeMs) as unknown as number;\n }\n\n // Update state on each tick. Batch updates via requestAnimationFrame to avoid\n // excessive React re-renders which can cause visual flicker.\n let rafId: number | null = null;\n let lastUpdate = 0;\n const tickHandler = () => {\n try {\n if (typeof onTick === 'function') onTick(nodesCopy, linksCopy, simulation);\n } catch (e) {\n // ignore user tick errors\n }\n\n // If simulation alpha has cooled below the configured minimum, stop it to\n // ensure nodes don't drift indefinitely (acts as a hard-stop safeguard).\n try {\n if (simulation.alpha() <= (alphaMin as number)) {\n try {\n if (stabilizeOnStop) {\n nodesCopy.forEach((n) => {\n (n as any).vx = 0;\n (n as any).vy = 0;\n if (typeof n.x === 'number') n.x = Number(n.x.toFixed(3));\n if (typeof n.y === 'number') n.y = Number(n.y.toFixed(3));\n });\n }\n simulation.stop();\n } catch (e) {}\n setAlpha(simulation.alpha());\n setIsRunning(false);\n setNodes([...nodesCopy]);\n setLinks([...linksCopy]);\n return;\n }\n } catch (e) {\n // ignore\n }\n\n const now = Date.now();\n const shouldUpdate = now - lastUpdate >= (tickThrottleMs as number);\n if (rafId == null && shouldUpdate) {\n rafId = (globalThis.requestAnimationFrame || ((cb: FrameRequestCallback) => setTimeout(cb, 16)))(() => {\n rafId = null;\n lastUpdate = Date.now();\n setNodes([...nodesCopy]);\n setLinks([...linksCopy]);\n setAlpha(simulation.alpha());\n setIsRunning(simulation.alpha() > simulation.alphaMin());\n }) as unknown as number;\n }\n };\n\n simulation.on('tick', tickHandler);\n\n simulation.on('end', () => {\n setIsRunning(false);\n });\n\n // Cleanup on unmount\n return () => {\n try {\n simulation.on('tick', null as any);\n } catch (e) {}\n if (stopTimeoutRef.current != null) {\n try { (globalThis.clearTimeout as any)(stopTimeoutRef.current); } catch (e) {}\n stopTimeoutRef.current = null;\n }\n if (rafId != null) {\n try { (globalThis.cancelAnimationFrame || ((id: number) => clearTimeout(id)))(rafId); } catch (e) {}\n rafId = null;\n }\n simulation.stop();\n };\n }, [\n nodesKey,\n linksKey,\n chargeStrength,\n linkDistance,\n linkStrength,\n collisionStrength,\n collisionRadius,\n centerStrength,\n width,\n height,\n alphaDecay,\n velocityDecay,\n alphaTarget,\n alphaMin,\n stabilizeOnStop,\n tickThrottleMs,\n maxSimulationTimeMs,\n ]);\n\n const restart = () => {\n if (simulationRef.current) {\n // Reheat the simulation to a modest alpha target rather than forcing\n // full heat; this matches the Observable pattern and helps stability.\n try { simulationRef.current.alphaTarget(warmAlpha).restart(); } catch (e) { simulationRef.current.restart(); }\n setIsRunning(true);\n // Reset safety timeout when simulation is manually restarted\n if (stopTimeoutRef.current != null) {\n try { (globalThis.clearTimeout as any)(stopTimeoutRef.current); } catch (e) {}\n stopTimeoutRef.current = null;\n }\n if (maxSimulationTimeMs && maxSimulationTimeMs > 0) {\n stopTimeoutRef.current = (globalThis.setTimeout as any)(() => {\n try { simulationRef.current?.alpha(0); simulationRef.current?.stop(); } catch (e) {}\n setIsRunning(false);\n }, maxSimulationTimeMs) as unknown as number;\n }\n }\n };\n\n const stop = () => {\n if (simulationRef.current) {\n simulationRef.current.stop();\n setIsRunning(false);\n }\n };\n\n const originalForcesRef = useRef({ charge: chargeStrength, link: linkStrength, collision: collisionStrength });\n const forcesEnabledRef = useRef(true);\n\n const setForcesEnabled = (enabled: boolean) => {\n const sim = simulationRef.current;\n if (!sim) return;\n // avoid repeated updates\n if (forcesEnabledRef.current === enabled) return;\n forcesEnabledRef.current = enabled;\n\n try {\n // Only toggle charge and link forces to avoid collapse; keep collision/centering\n const charge: any = sim.force('charge');\n if (charge && typeof charge.strength === 'function') {\n charge.strength(enabled ? originalForcesRef.current.charge : 0);\n }\n\n const link: any = sim.force('link');\n if (link && typeof link.strength === 'function') {\n link.strength(enabled ? originalForcesRef.current.link : 0);\n }\n } catch (e) {\n // ignore\n }\n };\n\n return {\n nodes,\n links,\n restart,\n stop,\n isRunning,\n alpha,\n setForcesEnabled,\n };\n}\n\n/**\n * Hook for creating a draggable force simulation\n * Provides drag handlers that can be attached to node elements\n *\n * @param simulation - The d3 force simulation instance\n * @returns Drag behavior that can be applied to nodes\n *\n * @example\n * ```tsx\n * function DraggableNetworkGraph() {\n * const simulation = useRef<d3.Simulation<SimulationNode, SimulationLink>>();\n * const drag = useDrag(simulation.current);\n *\n * return (\n * <svg>\n * {nodes.map((node) => (\n * <circle\n * key={node.id}\n * {...drag}\n * cx={node.x}\n * cy={node.y}\n * r={10}\n * />\n * ))}\n * </svg>\n * );\n * }\n * ```\n */\nexport function useDrag(simulation: d3.Simulation<SimulationNode, any> | null | undefined) {\n const dragStarted = (event: any, node: SimulationNode) => {\n if (!simulation) return;\n if (!event.active) simulation.alphaTarget(0.3).restart();\n node.fx = node.x;\n node.fy = node.y;\n };\n\n const dragged = (event: any, node: SimulationNode) => {\n node.fx = event.x;\n node.fy = event.y;\n };\n\n const dragEnded = (event: any, node: SimulationNode) => {\n if (!simulation) return;\n if (!event.active) simulation.alphaTarget(0);\n node.fx = null;\n node.fy = null;\n };\n\n return {\n onDragStart: dragStarted,\n onDrag: dragged,\n onDragEnd: dragEnded,\n };\n}"]}
|