UNPKG

reagraph

Version:

WebGL Node-based Graph for React

1,666 lines 164 kB
(function() { "use strict"; try { if (typeof document != "undefined") { var elementStyle = document.createElement("style"); elementStyle.appendChild(document.createTextNode("._canvas_670zp_1 {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n}\n._container_155l7_1 {\n transform-origin: bottom right;\n overflow: hidden;\n position: absolute;\n border: solid 1px var(--radial-menu-border);\n}\n\n ._container_155l7_1._disabled_155l7_7 {\n opacity: 0.6;\n }\n\n ._container_155l7_1._disabled_155l7_7 ._contentContainer_155l7_10 {\n cursor: not-allowed;\n }\n\n ._container_155l7_1:not(._disabled_155l7_7) ._contentContainer_155l7_10 {\n cursor: pointer;\n }\n\n ._container_155l7_1:not(._disabled_155l7_7) ._contentContainer_155l7_10:hover {\n color: var(--radial-menu-active-color);\n background: var(--radial-menu-active-background);\n }\n\n ._container_155l7_1 ._contentContainer_155l7_10 {\n width: 200%;\n height: 200%;\n transform-origin: 50% 50%;\n border-radius: 50%;\n outline: none;\n transition: background 150ms ease-in-out;\n color: var(--radial-menu-color);\n }\n\n ._container_155l7_1 ._contentContainer_155l7_10 ._contentInner_155l7_35 {\n position: absolute;\n width: 100%;\n text-align: center;\n }\n\n ._container_155l7_1 ._contentContainer_155l7_10 ._contentInner_155l7_35 ._content_155l7_10 {\n display: inline-block;\n }\n\n ._container_155l7_1 svg {\n margin: 0 auto;\n fill: var(--radial-menu-active-color);\n height: 25px;\n width: 25px;\n display: block;\n }\n._container_x9hyx_1 {\n border-radius: 50%;\n z-index: 9;\n position: relative;\n height: 175px;\n width: 175px;\n border: solid 5px var(--radial-menu-border);\n overflow: hidden;\n background: var(--radial-menu-background);\n}\n\n ._container_x9hyx_1:before {\n content: ' ';\n background: var(--radial-menu-border);\n border-radius: 50%;\n height: 25px;\n width: 25px;\n position: absolute;\n z-index: 9;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n }")); document.head.appendChild(elementStyle); } } catch (e) { console.error("vite-plugin-css-injected-by-js", e); } })(); import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import React, { createContext, useContext, useCallback, useRef, useEffect, useMemo, useState, useLayoutEffect, forwardRef, useImperativeHandle, Fragment as Fragment$1, Suspense } from "react"; import { useThree, useFrame, extend, Canvas } from "@react-three/fiber"; import { forceRadial as forceRadial$1, forceSimulation, forceX, forceY, forceCollide, forceManyBody, forceLink, forceCenter, forceZ } from "d3-force-3d"; import { treemap, hierarchy, stratify, tree } from "d3-hierarchy"; import circular from "graphology-layout/circular.js"; import noverlapLayout from "graphology-layout-noverlap"; import forceAtlas2Layout from "graphology-layout-forceatlas2"; import random from "graphology-layout/random.js"; import pagerank from "graphology-metrics/centrality/pagerank.js"; import { degreeCentrality } from "graphology-metrics/centrality/degree.js"; import { scaleLinear } from "d3-scale"; import { create, useStore as useStore$1 } from "zustand"; import { useShallow } from "zustand/shallow"; import { Vector3, QuadraticBezierCurve3, LineCurve3, Color, Plane, Vector2, DoubleSide, ShaderMaterial, TubeGeometry, Euler, BoxGeometry, CylinderGeometry, Quaternion, BufferAttribute, Mesh, TextureLoader, LinearFilter, Box3, MathUtils, Raycaster, Sphere as Sphere$1, Spherical, Matrix4, Vector4, MOUSE, Scene } from "three"; import { bidirectional } from "graphology-shortest-path"; import Graph from "graphology"; import { useSpring, a } from "@react-spring/three"; import { Billboard, RoundedBox, Text, useCursor, Html, Svg as Svg$1 } from "@react-three/drei"; import ellipsize from "ellipsize"; import { useGesture } from "@use-gesture/react"; import { mergeBufferGeometries, SelectionBox } from "three-stdlib"; import ThreeCameraControls from "camera-controls"; import * as holdEvent from "hold-event"; import classNames from "classnames"; function tick(layout) { return new Promise((resolve, _reject) => { let stable; function run() { if (!stable) { stable = layout.step(); run(); } else { resolve(stable); } } run(); }); } function buildNodeEdges(graph) { const nodes = []; const edges = []; graph.forEachNode((id, n) => { nodes.push({ ...n, id, // This is for the clustering radius: n.size || 1 }); }); graph.forEachEdge((id, l) => { edges.push({ ...l, id }); }); return { nodes, edges }; } function concentricLayout({ graph, radius = 40, drags, getNodePosition, concentricSpacing = 100 }) { const { nodes, edges } = buildNodeEdges(graph); const layout = {}; const getNodesInLevel = (level) => { const circumference = 2 * Math.PI * (radius + level * concentricSpacing); const minNodeSpacing = 40; return Math.floor(circumference / minNodeSpacing); }; const fixedLevelMap = /* @__PURE__ */ new Map(); const dynamicNodes = []; for (const node of nodes) { const data = graph.getNodeAttribute(node.id, "data"); const level = data?.level; if (typeof level === "number" && level >= 0) { if (!fixedLevelMap.has(level)) { fixedLevelMap.set(level, []); } fixedLevelMap.get(level).push(node.id); } else { dynamicNodes.push({ id: node.id, metric: graph.degree(node.id) }); } } dynamicNodes.sort((a2, b) => b.metric - a2.metric); for (const [level, nodeIds] of fixedLevelMap.entries()) { const count = nodeIds.length; const r = radius + level * concentricSpacing; for (let i2 = 0; i2 < count; i2++) { const angle = 2 * Math.PI * i2 / count; layout[nodeIds[i2]] = { x: r * Math.cos(angle), y: r * Math.sin(angle) }; } } const occupiedLevels = new Set(fixedLevelMap.keys()); let dynamicLevel = 0; let i = 0; while (i < dynamicNodes.length) { while (occupiedLevels.has(dynamicLevel)) { dynamicLevel++; } const nodesInLevel = getNodesInLevel(dynamicLevel); const r = radius + dynamicLevel * concentricSpacing; for (let j = 0; j < nodesInLevel && i < dynamicNodes.length; j++) { const angle = 2 * Math.PI * j / nodesInLevel; layout[dynamicNodes[i].id] = { x: r * Math.cos(angle), y: r * Math.sin(angle) }; i++; } dynamicLevel++; } return { step() { return true; }, getNodePosition(id) { if (getNodePosition) { const pos = getNodePosition(id, { graph, drags, nodes, edges }); if (pos) return pos; } if (drags?.[id]?.position) { return drags[id].position; } return layout[id]; } }; } function traverseGraph(nodes, nodeStack = []) { const currentDepth = nodeStack.length; for (const node of nodes) { const idx = nodeStack.indexOf(node); if (idx > -1) { const loop = [...nodeStack.slice(idx), node].map((d) => d.data.id); throw new Error( `Invalid Graph: Circular node path detected: ${loop.join(" -> ")}.` ); } if (currentDepth > node.depth) { node.depth = currentDepth; traverseGraph(node.out, [...nodeStack, node]); } } } function getNodeDepth(nodes, links) { let invalid = false; const graph = nodes.reduce( (acc, cur) => ({ ...acc, [cur.id]: { data: cur, out: [], depth: -1, ins: [] } }), {} ); try { for (const link of links) { const from = link.source; const to = link.target; if (!graph.hasOwnProperty(from)) { throw new Error(`Missing source Node ${from}`); } if (!graph.hasOwnProperty(to)) { throw new Error(`Missing target Node ${to}`); } const sourceNode = graph[from]; const targetNode = graph[to]; targetNode.ins.push(sourceNode); sourceNode.out.push(targetNode); } traverseGraph(Object.values(graph)); } catch (e) { invalid = true; } const allDepths = Object.keys(graph).map((id) => graph[id].depth); const maxDepth = Math.max(...allDepths); return { invalid, depths: graph, maxDepth: maxDepth || 1 }; } const RADIALS = ["radialin", "radialout"]; function forceRadial({ nodes, edges, mode = "lr", nodeLevelRatio = 2 }) { const { depths, maxDepth, invalid } = getNodeDepth(nodes, edges); if (invalid) { return null; } const modeDistance = RADIALS.includes(mode) ? 1 : 5; const dagLevelDistance = nodes.length / maxDepth * nodeLevelRatio * modeDistance; if (mode) { const getFFn = (fix, invert) => (node) => !fix ? void 0 : (depths[node.id].depth - maxDepth / 2) * dagLevelDistance * (invert ? -1 : 1); const fxFn = getFFn(["lr", "rl"].includes(mode), mode === "rl"); const fyFn = getFFn(["td", "bu"].includes(mode), mode === "td"); const fzFn = getFFn(["zin", "zout"].includes(mode), mode === "zout"); nodes.forEach((node) => { node.fx = fxFn(node); node.fy = fyFn(node); node.fz = fzFn(node); }); } return RADIALS.includes(mode) ? forceRadial$1((node) => { const nodeDepth = depths[node.id]; const depth = mode === "radialin" ? maxDepth - nodeDepth.depth : nodeDepth.depth; return depth * dagLevelDistance; }).strength(1) : null; } function forceInABox() { const constant = (_) => () => _; const index = (d) => d.index; let id = index; let nodes = []; let links = []; let clusters; let tree2; let size = [100, 100]; let forceNodeSize = constant(1); let forceCharge = constant(-1); let forceLinkDistance = constant(100); let forceLinkStrength = constant(0.1); let foci = {}; let linkStrengthIntraCluster = 0.1; let linkStrengthInterCluster = 1e-3; let templateNodes = []; let offset = [0, 0]; let templateForce; let groupBy = (d) => d.cluster; let template = "treemap"; let enableGrouping = true; let strength = 0.1; function force(alpha) { if (!enableGrouping) { return force; } if (template === "force") { templateForce.tick(); getFocisFromTemplate(); } for (let i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) { node = nodes[i]; node.vx += (foci[groupBy(node)].x - node.x) * k; node.vy += (foci[groupBy(node)].y - node.y) * k; } } function initialize() { if (!nodes) { return; } if (template === "treemap") { initializeWithTreemap(); } else { initializeWithForce(); } } force.initialize = function(_) { nodes = _; initialize(); }; function getLinkKey(l) { let sourceID = groupBy(l.source), targetID = groupBy(l.target); return sourceID <= targetID ? sourceID + "~" + targetID : targetID + "~" + sourceID; } function computeClustersNodeCounts(nodes2) { let clustersCounts = /* @__PURE__ */ new Map(), tmpCount = {}; nodes2.forEach(function(d) { if (!clustersCounts.has(groupBy(d))) { clustersCounts.set(groupBy(d), { count: 0, sumforceNodeSize: 0 }); } }); nodes2.forEach(function(d) { tmpCount = clustersCounts.get(groupBy(d)); tmpCount.count = tmpCount.count + 1; tmpCount.sumforceNodeSize = tmpCount.sumforceNodeSize + // @ts-ignore Math.PI * (forceNodeSize(d) * forceNodeSize(d)) * 1.3; clustersCounts.set(groupBy(d), tmpCount); }); return clustersCounts; } function computeClustersLinkCounts(links2) { let dClusterLinks = /* @__PURE__ */ new Map(), clusterLinks = []; links2.forEach(function(l) { let key = getLinkKey(l), count; if (dClusterLinks.has(key)) { count = dClusterLinks.get(key); } else { count = 0; } count += 1; dClusterLinks.set(key, count); }); dClusterLinks.forEach(function(value, key) { let source, target; source = key.split("~")[0]; target = key.split("~")[1]; if (source !== void 0 && target !== void 0) { clusterLinks.push({ source, target, count: value }); } }); return clusterLinks; } function getGroupsGraph() { let gnodes = []; let glinks = []; let dNodes = /* @__PURE__ */ new Map(); let c; let i; let cc; let clustersCounts; let clustersLinks; clustersCounts = computeClustersNodeCounts(nodes); clustersLinks = computeClustersLinkCounts(links); for (c of clustersCounts.keys()) { cc = clustersCounts.get(c); gnodes.push({ id: c, size: cc.count, r: Math.sqrt(cc.sumforceNodeSize / Math.PI) }); dNodes.set(c, i); } clustersLinks.forEach(function(l) { let source = dNodes.get(l.source), target = dNodes.get(l.target); if (source !== void 0 && target !== void 0) { glinks.push({ source, target, count: l.count }); } }); return { nodes: gnodes, links: glinks }; } function getGroupsTree() { let children = []; let c; let cc; let clustersCounts; clustersCounts = computeClustersNodeCounts(force.nodes()); for (c of clustersCounts.keys()) { cc = clustersCounts.get(c); children.push({ id: c, size: cc.count }); } return { id: "clustersTree", children }; } function getFocisFromTemplate() { foci.none = { x: 0, y: 0 }; templateNodes.forEach(function(d) { if (template === "treemap") { foci[d.data.id] = { x: d.x0 + (d.x1 - d.x0) / 2 - offset[0], y: d.y0 + (d.y1 - d.y0) / 2 - offset[1] }; } else { foci[d.id] = { x: d.x - offset[0], y: d.y - offset[1] }; } }); return foci; } function initializeWithTreemap() { let sim = treemap().size(force.size()); tree2 = hierarchy(getGroupsTree()).sum((d) => d.radius).sort(function(a2, b) { return b.height - a2.height || b.value - a2.value; }); templateNodes = sim(tree2).leaves(); getFocisFromTemplate(); } function checkLinksAsObjects() { let linkCount = 0; if (nodes.length === 0) return; links.forEach(function(link) { let source, target; if (!nodes) { return; } source = link.source; target = link.target; if (typeof link.source !== "object") { source = nodes.find((n) => n.id === link.source); } if (typeof link.target !== "object") { target = nodes.find((n) => n.id === link.target); } if (source === void 0 || target === void 0) { throw Error( "Error setting links, couldnt find nodes for a link (see it on the console)" ); } link.source = source; link.target = target; link.index = linkCount++; }); } function initializeWithForce() { let net; if (!nodes || !nodes.length) { return; } checkLinksAsObjects(); net = getGroupsGraph(); if (clusters.size > 0) { net.nodes.forEach((n) => { n.fx = clusters.get(n.id)?.position?.x; n.fy = clusters.get(n.id)?.position?.y; }); } templateForce = forceSimulation(net.nodes).force("x", forceX(size[0] / 2).strength(0.1)).force("y", forceY(size[1] / 2).strength(0.1)).force("collide", forceCollide((d) => d.r).iterations(4)).force("charge", forceManyBody().strength(forceCharge)).force( "links", forceLink(net.nodes.length ? net.links : []).distance(forceLinkDistance).strength(forceLinkStrength) ); templateNodes = templateForce.nodes(); getFocisFromTemplate(); } force.template = function(x) { if (!arguments.length) { return template; } template = x; initialize(); return force; }; force.groupBy = function(x) { if (!arguments.length) { return groupBy; } if (typeof x === "string") { groupBy = function(d) { return d[x]; }; return force; } groupBy = x; return force; }; force.enableGrouping = function(x) { if (!arguments.length) { return enableGrouping; } enableGrouping = x; return force; }; force.strength = function(x) { if (!arguments.length) { return strength; } strength = x; return force; }; force.getLinkStrength = function(e) { if (enableGrouping) { if (groupBy(e.source) === groupBy(e.target)) { if (typeof linkStrengthIntraCluster === "function") { return linkStrengthIntraCluster(e); } else { return linkStrengthIntraCluster; } } else { if (typeof linkStrengthInterCluster === "function") { return linkStrengthInterCluster(e); } else { return linkStrengthInterCluster; } } } else { if (typeof linkStrengthIntraCluster === "function") { return linkStrengthIntraCluster(e); } else { return linkStrengthIntraCluster; } } }; force.id = function(_) { return arguments.length ? (id = _, force) : id; }; force.size = function(_) { return arguments.length ? (size = _, force) : size; }; force.linkStrengthInterCluster = function(_) { return arguments.length ? (linkStrengthInterCluster = _, force) : linkStrengthInterCluster; }; force.linkStrengthIntraCluster = function(_) { return arguments.length ? (linkStrengthIntraCluster = _, force) : linkStrengthIntraCluster; }; force.nodes = function(_) { return arguments.length ? (nodes = _, force) : nodes; }; force.links = function(_) { if (!arguments.length) { return links; } if (_ === null) { links = []; } else { links = _; } initialize(); return force; }; force.template = function(x) { if (!arguments.length) { return template; } template = x; initialize(); return force; }; force.forceNodeSize = function(_) { return arguments.length ? (forceNodeSize = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceNodeSize; }; force.nodeSize = force.forceNodeSize; force.forceCharge = function(_) { return arguments.length ? (forceCharge = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceCharge; }; force.forceLinkDistance = function(_) { return arguments.length ? (forceLinkDistance = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceLinkDistance; }; force.forceLinkStrength = function(_) { return arguments.length ? (forceLinkStrength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : forceLinkStrength; }; force.offset = function(_) { return arguments.length ? (offset = typeof _ === "function" ? _ : constant(+_), force) : offset; }; force.getFocis = getFocisFromTemplate; force.setClusters = function(value) { clusters = value; return force; }; return force; } function forceDirected({ graph, nodeLevelRatio = 2, mode = null, dimensions = 2, nodeStrength = -250, linkDistance = 50, clusterStrength = 0.5, linkStrengthInterCluster = 0.01, linkStrengthIntraCluster = 0.5, forceLinkDistance = 100, forceLinkStrength = 0.1, clusterType = "force", forceCharge = -700, getNodePosition, drags, clusters, clusterAttribute, forceLayout }) { const { nodes, edges } = buildNodeEdges(graph); const is2d = dimensions === 2; const nodeStrengthAdjustment = is2d && edges.length > 25 ? nodeStrength * 2 : nodeStrength; let forceX$1; let forceY$1; if (forceLayout === "forceDirected2d") { forceX$1 = forceX(); forceY$1 = forceY(); } else { forceX$1 = forceX(600).strength(0.05); forceY$1 = forceY(600).strength(0.05); } const sim = forceSimulation().force("center", forceCenter(0, 0)).force("link", forceLink()).force("charge", forceManyBody().strength(nodeStrengthAdjustment)).force("x", forceX$1).force("y", forceY$1).force("z", forceZ()).force( "collide", forceCollide((d) => d.radius + 10) ).force( "dagRadial", forceRadial({ nodes, edges, mode, nodeLevelRatio }) ).stop(); let groupingForce; if (clusterAttribute) { let forceChargeAdjustment = forceCharge; if (nodes?.length) { const adjustmentFactor = Math.ceil(nodes.length / 200); forceChargeAdjustment = forceCharge * adjustmentFactor; } groupingForce = forceInABox().setClusters(clusters).strength(clusterStrength).template(clusterType).groupBy((d) => d.data[clusterAttribute]).links(edges).size([100, 100]).linkStrengthInterCluster(linkStrengthInterCluster).linkStrengthIntraCluster(linkStrengthIntraCluster).forceLinkDistance(forceLinkDistance).forceLinkStrength(forceLinkStrength).forceCharge(forceChargeAdjustment).forceNodeSize((d) => d.radius); } let layout = sim.numDimensions(dimensions).nodes(nodes); if (groupingForce) { layout = layout.force("group", groupingForce); } if (linkDistance) { let linkForce = layout.force("link"); if (linkForce) { linkForce.id((d) => d.id).links(edges).distance(linkDistance); if (groupingForce) { linkForce = linkForce.strength(groupingForce?.getLinkStrength ?? 0.1); } } } const nodeMap = new Map(nodes.map((n) => [n.id, n])); return { step() { while (sim.alpha() > 0.01) { sim.tick(); } return true; }, getNodePosition(id) { if (getNodePosition) { const pos = getNodePosition(id, { graph, drags, nodes, edges }); if (pos) { return pos; } } if (drags?.[id]?.position) { return drags?.[id]?.position; } return nodeMap.get(id); } }; } function circular2d({ graph, radius, drags, getNodePosition }) { const layout = circular(graph, { scale: radius }); const { nodes, edges } = buildNodeEdges(graph); return { step() { return true; }, getNodePosition(id) { if (getNodePosition) { const pos = getNodePosition(id, { graph, drags, nodes, edges }); if (pos) { return pos; } } if (drags?.[id]?.position) { return drags?.[id]?.position; } return layout?.[id]; } }; } const DIRECTION_MAP = { td: { x: "x", y: "y", factor: -1 }, lr: { x: "y", y: "x", factor: 1 } }; function hierarchical({ graph, drags, mode = "td", nodeSeparation = 1, nodeSize = [50, 50], getNodePosition }) { const { nodes, edges } = buildNodeEdges(graph); const { depths } = getNodeDepth(nodes, edges); const rootNodes = Object.keys(depths).map((d) => depths[d]); const root = stratify().id((d) => d.data.id).parentId((d) => d.ins?.[0]?.data?.id)(rootNodes); const treeRoot = tree().separation(() => nodeSeparation).nodeSize(nodeSize)(hierarchy(root)); const treeNodes = treeRoot.descendants(); const path = DIRECTION_MAP[mode]; const mappedNodes = new Map( nodes.map((n) => { const { x, y } = treeNodes.find((t) => t.data.id === n.id); return [ n.id, { ...n, [path.x]: x * path.factor, [path.y]: y * path.factor, z: 0 } ]; }) ); return { step() { return true; }, getNodePosition(id) { if (getNodePosition) { const pos = getNodePosition(id, { graph, drags, nodes, edges }); if (pos) { return pos; } } if (drags?.[id]?.position) { return drags?.[id]?.position; } return mappedNodes.get(id); } }; } function nooverlap({ graph, margin, drags, getNodePosition, ratio, gridSize, maxIterations }) { const { nodes, edges } = buildNodeEdges(graph); const layout = noverlapLayout(graph, { maxIterations, inputReducer: (_key, attr) => ({ ...attr, // Have to specify defaults for the engine x: attr.x || 0, y: attr.y || 0 }), settings: { ratio, margin, gridSize } }); return { step() { return true; }, getNodePosition(id) { if (getNodePosition) { const pos = getNodePosition(id, { graph, drags, nodes, edges }); if (pos) { return pos; } } if (drags?.[id]?.position) { return drags?.[id]?.position; } return layout?.[id]; } }; } function forceAtlas2({ graph, drags, iterations, ...rest }) { random.assign(graph); const layout = forceAtlas2Layout(graph, { iterations, settings: rest }); return { step() { return true; }, getNodePosition(id) { return drags?.[id]?.position || layout?.[id]; } }; } function custom({ graph, drags, getNodePosition }) { const { nodes, edges } = buildNodeEdges(graph); return { step() { return true; }, getNodePosition(id) { return getNodePosition(id, { graph, drags, nodes, edges }); } }; } const FORCE_LAYOUTS = [ "forceDirected2d", "treeTd2d", "treeLr2d", "radialOut2d", "treeTd3d", "treeLr3d", "radialOut3d", "forceDirected3d" ]; function layoutProvider({ type, ...rest }) { if (FORCE_LAYOUTS.includes(type)) { const { nodeStrength, linkDistance, nodeLevelRatio } = rest; if (type === "forceDirected2d") { return forceDirected({ ...rest, dimensions: 2, nodeLevelRatio: nodeLevelRatio || 2, nodeStrength: nodeStrength || -250, linkDistance, forceLayout: type }); } else if (type === "treeTd2d") { return forceDirected({ ...rest, mode: "td", dimensions: 2, nodeLevelRatio: nodeLevelRatio || 5, nodeStrength: nodeStrength || -250, linkDistance: linkDistance || 50, forceLayout: type }); } else if (type === "treeLr2d") { return forceDirected({ ...rest, mode: "lr", dimensions: 2, nodeLevelRatio: nodeLevelRatio || 5, nodeStrength: nodeStrength || -250, linkDistance: linkDistance || 50, forceLayout: type }); } else if (type === "radialOut2d") { return forceDirected({ ...rest, mode: "radialout", dimensions: 2, nodeLevelRatio: nodeLevelRatio || 5, nodeStrength: nodeStrength || -500, linkDistance: linkDistance || 100, forceLayout: type }); } else if (type === "treeTd3d") { return forceDirected({ ...rest, mode: "td", dimensions: 3, nodeLevelRatio: nodeLevelRatio || 2, nodeStrength: nodeStrength || -500, linkDistance: linkDistance || 50 }); } else if (type === "treeLr3d") { return forceDirected({ ...rest, mode: "lr", dimensions: 3, nodeLevelRatio: nodeLevelRatio || 2, nodeStrength: nodeStrength || -500, linkDistance: linkDistance || 50, forceLayout: type }); } else if (type === "radialOut3d") { return forceDirected({ ...rest, mode: "radialout", dimensions: 3, nodeLevelRatio: nodeLevelRatio || 2, nodeStrength: nodeStrength || -500, linkDistance: linkDistance || 100, forceLayout: type }); } else if (type === "forceDirected3d") { return forceDirected({ ...rest, dimensions: 3, nodeLevelRatio: nodeLevelRatio || 2, nodeStrength: nodeStrength || -250, linkDistance, forceLayout: type }); } } else if (type === "circular2d") { const { radius } = rest; return circular2d({ ...rest, radius: radius || 300 }); } else if (type === "concentric2d") { return concentricLayout(rest); } else if (type === "hierarchicalTd") { return hierarchical({ ...rest, mode: "td" }); } else if (type === "hierarchicalLr") { return hierarchical({ ...rest, mode: "lr" }); } else if (type === "nooverlap") { const { graph, maxIterations, ratio, margin, gridSize, ...settings } = rest; return nooverlap({ graph, margin: margin || 10, maxIterations: maxIterations || 50, ratio: ratio || 10, gridSize: gridSize || 20, ...settings }); } else if (type === "forceatlas2") { const { graph, iterations, gravity, scalingRatio, ...settings } = rest; return forceAtlas2({ type: "forceatlas2", graph, ...settings, scalingRatio: scalingRatio || 100, gravity: gravity || 10, iterations: iterations || 50 }); } else if (type === "custom") { return custom({ ...rest }); } throw new Error(`Layout ${type} not found.`); } function recommendLayout(nodes, edges) { const { invalid } = getNodeDepth(nodes, edges); const nodeCount = nodes.length; if (!invalid) { if (nodeCount > 100) { return "radialOut2d"; } else { return "treeTd2d"; } } return "forceDirected2d"; } function calcLabelVisibility({ nodePosition, labelType, camera }) { return (shape, size) => { const isAlwaysVisible = labelType === "all" || labelType === "nodes" && shape === "node" || labelType === "edges" && shape === "edge"; if (!isAlwaysVisible && camera && nodePosition && camera?.position?.z / camera?.zoom - nodePosition?.z > 6e3) { return false; } if (isAlwaysVisible) { return true; } else if (labelType === "auto" && shape === "node") { if (size > 7) { return true; } else if (camera && nodePosition && camera.position.z / camera.zoom - nodePosition.z < 3e3) { return true; } } return false; }; } function getLabelOffsetByType(offset, position) { switch (position) { case "above": return offset; case "below": return -offset; case "inline": case "natural": default: return 0; } } const isServerRender = typeof window === "undefined"; function pageRankSizing({ graph }) { const ranks = pagerank(graph); return { ranks, getSizeForNode: (nodeID) => ranks[nodeID] * 80 }; } function centralitySizing({ graph }) { const ranks = degreeCentrality(graph); return { ranks, getSizeForNode: (nodeID) => ranks[nodeID] * 20 }; } function attributeSizing({ graph, attribute, defaultSize }) { const map = /* @__PURE__ */ new Map(); if (attribute) { graph.forEachNode((id, node) => { const size = node.data?.[attribute]; if (isNaN(size)) { console.warn(`Attribute ${size} is not a number for node ${node.id}`); } map.set(id, size || 0); }); } else { console.warn("Attribute sizing configured but no attribute provided"); } return { getSizeForNode: (nodeId) => { if (!attribute || !map) { return defaultSize; } return map.get(nodeId); } }; } const providers = { pagerank: pageRankSizing, centrality: centralitySizing, attribute: attributeSizing, none: ({ defaultSize }) => ({ getSizeForNode: (_id) => defaultSize }) }; function nodeSizeProvider({ type, ...rest }) { const provider = providers[type]?.(rest); if (!provider && type !== "default") { throw new Error(`Unknown sizing strategy: ${type}`); } const { graph, minSize, maxSize } = rest; const sizes = /* @__PURE__ */ new Map(); let min; let max; graph.forEachNode((id, node) => { let size; if (type === "default") { size = node.size || rest.defaultSize; } else { size = provider.getSizeForNode(id); } if (min === void 0 || size < min) { min = size; } if (max === void 0 || size > max) { max = size; } sizes.set(id, size); }); if (type !== "none") { const scale = scaleLinear().domain([min, max]).rangeRound([minSize, maxSize]); for (const [nodeId, size] of sizes) { sizes.set(nodeId, scale(size)); } } return sizes; } function buildGraph(graph, nodes, edges) { graph.clear(); const addedNodes = /* @__PURE__ */ new Set(); for (const node of nodes) { try { if (!addedNodes.has(node.id)) { graph.addNode(node.id, node); addedNodes.add(node.id); } } catch (e) { console.error(`[Graph] Error adding node '${node.id}`, e); } } for (const edge of edges) { if (!addedNodes.has(edge.source) || !addedNodes.has(edge.target)) { continue; } try { graph.addEdge(edge.source, edge.target, edge); } catch (e) { console.error( `[Graph] Error adding edge '${edge.source} -> ${edge.target}`, e ); } } return graph; } function transformGraph({ graph, layout, sizingType, labelType, sizingAttribute, defaultNodeSize, minNodeSize, maxNodeSize, clusterAttribute }) { const nodes = []; const edges = []; const map = /* @__PURE__ */ new Map(); const sizes = nodeSizeProvider({ graph, type: sizingType, attribute: sizingAttribute, minSize: minNodeSize, maxSize: maxNodeSize, defaultSize: defaultNodeSize }); graph.nodes().length; const checkVisibility = calcLabelVisibility({ labelType }); graph.forEachNode((id, node) => { const position = layout.getNodePosition(id); const { data, fill, icon, label, size, ...rest } = node; const nodeSize = sizes.get(node.id); const labelVisible = checkVisibility("node", nodeSize); const nodeLinks = graph.inboundNeighbors(node.id) || []; const parents = nodeLinks.map((n2) => graph.getNodeAttributes(n2)); const n = { ...node, size: nodeSize, labelVisible, label, icon, fill, cluster: clusterAttribute ? data[clusterAttribute] : void 0, parents, data: { ...rest, ...data ?? {} }, position: { ...position, x: position.x || 0, y: position.y || 0, z: position.z || 1 } }; map.set(node.id, n); nodes.push(n); }); graph.forEachEdge((_id, link) => { const from = map.get(link.source); const to = map.get(link.target); if (from && to) { const { data, id, label, size, ...rest } = link; const labelVisible = checkVisibility("edge", size); edges.push({ ...link, id, label, labelVisible, size, data: { ...rest, id, ...data || {} } }); } }); return { nodes, edges }; } const animationConfig = { mass: 10, tension: 1e3, friction: 300, // Decreasing precision to improve performance from 0.00001 precision: 0.1 }; function getArrowVectors(placement, curve, arrowLength) { const curveLength = curve.getLength(); const absSize = placement === "end" ? curveLength : curveLength / 2; const offset = placement === "end" ? arrowLength / 2 : 0; const u = (absSize - offset) / curveLength; const position = curve.getPointAt(u); const rotation = curve.getTangentAt(u); return [position, rotation]; } function getArrowSize(size) { return [size + 6, 2 + size / 1.5]; } const MULTI_EDGE_OFFSET_FACTOR = 0.7; function getMidPoint(from, to, offset = 0) { const fromVector = new Vector3(from.x, from.y || 0, from.z || 0); const toVector = new Vector3(to.x, to.y || 0, to.z || 0); const midVector = new Vector3().addVectors(fromVector, toVector).divideScalar(2); return midVector.setLength(midVector.length() + offset); } function getCurvePoints(from, to, offset = -1) { const fromVector = from.clone(); const toVector = to.clone(); const v = new Vector3().subVectors(toVector, fromVector); const vlen = v.length(); const vn = v.clone().normalize(); const vv = new Vector3().subVectors(toVector, fromVector).divideScalar(2); const k = Math.abs(vn.x) % 1; const b = new Vector3(-vn.y, vn.x - k * vn.z, k * vn.y).normalize(); const vm = new Vector3().add(fromVector).add(vv).add(b.multiplyScalar(vlen / 4).multiplyScalar(offset)); return [from, vm, to]; } function getCurve(from, fromOffset, to, toOffset, curved, curveOffset) { const offsetFrom = getPointBetween(from, to, fromOffset); const offsetTo = getPointBetween(to, from, toOffset); return curved ? new QuadraticBezierCurve3( ...getCurvePoints(offsetFrom, offsetTo, curveOffset) ) : new LineCurve3(offsetFrom, offsetTo); } function getVector(node) { return new Vector3(node.position.x, node.position.y, node.position.z || 0); } function getPointBetween(from, to, offset) { const distance = from.distanceTo(to); return from.clone().add( to.clone().sub(from).multiplyScalar(offset / distance) ); } function updateNodePosition(node, offset) { return { ...node, position: { ...node.position, x: node.position.x + offset.x, y: node.position.y + offset.y, z: node.position.z + offset.z } }; } function calculateEdgeCurveOffset({ edge, edges, curved }) { let updatedCurved = curved; let curveOffset; const parallelEdges = edges.filter((e) => e.target === edge.target && e.source === edge.source).map((e) => e.id); if (parallelEdges.length > 1) { updatedCurved = true; const edgeIndex = parallelEdges.indexOf(edge.id); const offsetMultiplier = edgeIndex === 0 ? 1 : 1 + edgeIndex * 0.8; const side = edgeIndex % 2 === 0 ? 1 : -1; const magnitude = MULTI_EDGE_OFFSET_FACTOR * offsetMultiplier; curveOffset = side * magnitude; } if (edge.data?.isAggregated && edges.length > 1) { const edgeIndex = parallelEdges.indexOf(edge.id); return { curved: true, curveOffset: edgeIndex === 0 ? MULTI_EDGE_OFFSET_FACTOR : -MULTI_EDGE_OFFSET_FACTOR }; } return { curved: updatedCurved, curveOffset }; } function calculateSubLabelOffset(fromPosition, toPosition, subLabelPlacement) { const dx = toPosition.x - fromPosition.x; const dy = toPosition.y - fromPosition.y; const angle = Math.atan2(dy, dx); const perpAngle = subLabelPlacement === "above" ? dx >= 0 ? angle + Math.PI / 2 : angle - Math.PI / 2 : dx >= 0 ? angle - Math.PI / 2 : angle + Math.PI / 2; const offsetDistance = 7; const offsetX = Math.cos(perpAngle) * offsetDistance; const offsetY = Math.sin(perpAngle) * offsetDistance; return { x: offsetX, y: offsetY, z: 0 }; } function getLayoutCenter(nodes) { let minX = Number.POSITIVE_INFINITY; let maxX = Number.NEGATIVE_INFINITY; let minY = Number.POSITIVE_INFINITY; let maxY = Number.NEGATIVE_INFINITY; let minZ = Number.POSITIVE_INFINITY; let maxZ = Number.NEGATIVE_INFINITY; for (let node of nodes) { minX = Math.min(minX, node.position.x); maxX = Math.max(maxX, node.position.x); minY = Math.min(minY, node.position.y); maxY = Math.max(maxY, node.position.y); minZ = Math.min(minZ, node.position.z); maxZ = Math.max(maxZ, node.position.z); } return { height: maxY - minY, width: maxX - minX, minX, maxX, minY, maxY, minZ, maxZ, x: (maxX + minX) / 2, y: (maxY + minY) / 2, z: (maxZ + minZ) / 2 }; } function buildClusterGroups(nodes, clusterAttribute) { if (!clusterAttribute) { return /* @__PURE__ */ new Map(); } return nodes.reduce((entryMap, e) => { const val = e.data[clusterAttribute]; if (val) { entryMap.set(val, [...entryMap.get(val) || [], e]); } return entryMap; }, /* @__PURE__ */ new Map()); } function calculateClusters({ nodes, clusterAttribute }) { const result = /* @__PURE__ */ new Map(); if (clusterAttribute) { const groups = buildClusterGroups(nodes, clusterAttribute); for (const [key, nodes2] of groups) { const position = getLayoutCenter(nodes2); result.set(key, { label: key, nodes: nodes2, position }); } } return result; } function findPath(graph, source, target) { return bidirectional(graph, source, target); } const isNotEditableElement = (element) => { return element.tagName !== "INPUT" && element.tagName !== "SELECT" && element.tagName !== "TEXTAREA" && !element.isContentEditable; }; const createStore = ({ actives = [], selections = [], collapsedNodeIds = [], theme }) => create((set) => ({ theme: { ...theme, edge: { ...theme?.edge, label: { ...theme?.edge?.label, fontSize: theme?.edge?.label?.fontSize ?? 6 } } }, edges: [], nodes: [], collapsedNodeIds, clusters: /* @__PURE__ */ new Map(), panning: false, draggingIds: [], actives, edgeContextMenus: /* @__PURE__ */ new Set(), edgeMeshes: [], selections, hoveredNodeId: null, drags: {}, graph: new Graph({ multi: true }), setTheme: (theme2) => set((state) => ({ ...state, theme: theme2 })), setClusters: (clusters) => set((state) => ({ ...state, clusters })), setEdgeContextMenus: (edgeContextMenus) => set((state) => ({ ...state, edgeContextMenus })), setEdgeMeshes: (edgeMeshes) => set((state) => ({ ...state, edgeMeshes })), setPanning: (panning) => set((state) => ({ ...state, panning })), setDrags: (drags) => set((state) => ({ ...state, drags })), addDraggingId: (id) => set((state) => ({ ...state, draggingIds: [...state.draggingIds, id] })), removeDraggingId: (id) => set((state) => ({ ...state, draggingIds: state.draggingIds.filter((drag) => drag !== id) })), setActives: (actives2) => set((state) => ({ ...state, actives: actives2 })), setSelections: (selections2) => set((state) => ({ ...state, selections: selections2 })), setHoveredNodeId: (hoveredNodeId) => set((state) => ({ ...state, hoveredNodeId })), setNodes: (nodes) => set((state) => ({ ...state, nodes, centerPosition: getLayoutCenter(nodes) })), setEdges: (edges) => set((state) => ({ ...state, edges })), setNodePosition: (id, position) => set((state) => { const node = state.nodes.find((n) => n.id === id); const originalVector = getVector(node); const newVector = new Vector3(position.x, position.y, position.z); const offset = newVector.sub(originalVector); const nodes = [...state.nodes]; if (state.selections?.includes(id)) { state.selections?.forEach((id2) => { const node2 = state.nodes.find((n) => n.id === id2); if (node2) { const nodeIndex = state.nodes.indexOf(node2); nodes[nodeIndex] = updateNodePosition(node2, offset); } }); } else { const nodeIndex = state.nodes.indexOf(node); nodes[nodeIndex] = updateNodePosition(node, offset); } return { ...state, drags: { ...state.drags, [id]: node }, nodes }; }), setCollapsedNodeIds: (nodeIds = []) => set((state) => ({ ...state, collapsedNodeIds: nodeIds })), // Update the position of a cluster with nodes inside it setClusterPosition: (id, position) => set((state) => { const clusters = new Map(state.clusters); const cluster = clusters.get(id); if (cluster) { const oldPos = cluster.position; const offset = new Vector3( position.x - oldPos.x, position.y - oldPos.y, position.z - (oldPos.z ?? 0) ); const nodes = [...state.nodes]; const drags = { ...state.drags }; nodes.forEach((node, index) => { if (node.cluster === id) { nodes[index] = { ...node, position: { ...node.position, x: node.position.x + offset.x, y: node.position.y + offset.y, z: node.position.z + (offset.z ?? 0) } }; drags[node.id] = node; } }); const clusterNodes = nodes.filter( (node) => node.cluster === id ); const newClusterPosition = getLayoutCenter(clusterNodes); clusters.set(id, { ...cluster, position: newClusterPosition }); return { ...state, drags: { ...drags, [id]: cluster }, clusters, nodes }; } return state; }) })); const defaultStore = createStore({}); const StoreContext = isServerRender ? null : createContext(defaultStore); const Provider = ({ children, store = defaultStore }) => { if (isServerRender) { return children; } return React.createElement(StoreContext.Provider, { value: store }, children); }; const useStore = (selector) => { const store = useContext(StoreContext); return useStore$1(store, useShallow(selector)); }; function getHiddenChildren({ nodeId, nodes, edges, currentHiddenNodes, currentHiddenEdges }) { const hiddenNodes = []; const hiddenEdges = []; const curHiddenNodeIds = currentHiddenNodes.map((n) => n.id); const curHiddenEdgeIds = currentHiddenEdges.map((e) => e.id); const outboundEdges = edges.filter((l) => l.source === nodeId); const outboundEdgeNodeIds = outboundEdges.map((l) => l.target); hiddenEdges.push(...outboundEdges); for (const outboundEdgeNodeId of outboundEdgeNodeIds) { const incomingEdges = edges.filter( (l) => l.target === outboundEdgeNodeId && l.source !== nodeId ); let hideNode = false; if (incomingEdges.length === 0) { hideNode = true; } else if (incomingEdges.length > 0 && !curHiddenNodeIds.includes(outboundEdgeNodeId)) { const inboundNodeLinkIds = incomingEdges.map((l) => l.id); if (inboundNodeLinkIds.every((i) => curHiddenEdgeIds.includes(i))) { hideNode = true; } } if (hideNode) { const node = nodes.find((n) => n.id === outboundEdgeNodeId); if (node) { hiddenNodes.push(node); } const nested = getHiddenChildren({ nodeId: outboundEdgeNodeId, nodes, edges, currentHiddenEdges: hiddenEdges, currentHiddenNodes: hiddenNodes }); hiddenEdges.push(...nested.hiddenEdges); hiddenNodes.push(...nested.hiddenNodes); } } const uniqueEdges = Object.values( hiddenEdges.reduce( (acc, next) => ({ ...acc, [next.id]: next }), {} ) ); const uniqueNodes = Object.values( hiddenNodes.reduce( (acc, next) => ({ ...acc, [next.id]: next }), {} ) ); return { hiddenEdges: uniqueEdges, hiddenNodes: uniqueNodes }; } const getVisibleEntities = ({ collapsedIds, nodes, edges }) => { const curHiddenNodes = []; const curHiddenEdges = []; for (const collapsedId of collapsedIds) { const { hiddenEdges, hiddenNodes } = getHiddenChildren({ nodeId: collapsedId, nodes, edges, currentHiddenEdges: curHiddenEdges, currentHiddenNodes: curHiddenNodes }); curHiddenNodes.push(...hiddenNodes); curHiddenEdges.push(...hiddenEdges); } const hiddenNodeIds = curHiddenNodes.map((n) => n.id); const hiddenEdgeIds = curHiddenEdges.map((e) => e.id); const visibleNodes = nodes.filter((n) => !hiddenNodeIds.includes(n.id)); const visibleEdges = edges.filter((e) => !hiddenEdgeIds.includes(e.id)); return { visibleNodes, visibleEdges }; }; const getExpandPath = ({ nodeId, edges, visibleEdgeIds }) => { const parentIds = []; const inboundEdges = edges.filter((l) => l.target === nodeId); const inboundEdgeIds = inboundEdges.map((e) => e.id); const hasVisibleInboundEdge = inboundEdgeIds.some( (id) => visibleEdgeIds.includes(id) ); if (hasVisibleInboundEdge) { return parentIds; } const inboundEdgeNodeIds = inboundEdges.map((l) => l.source); let addedParent = false; for (const inboundNodeId of inboundEdgeNodeIds) { if (!addedParent) { parentIds.push( ...[ inboundNodeId, ...getExpandPath({ nodeId: inboundNodeId, edges, visibleEdgeIds }) ] ); addedParent = true; } } return parentIds; }; const useCollapse = ({ collapsedNodeIds = [], nodes = [], edges = [] }) => { const getIsCollapsed = useCallback( (nodeId) => { const { visibleNodes } = getVisibleEntities({ nodes, edges, collapsedIds: collapsedNodeIds }); const visibleNodeIds = visibleNodes.map((n) => n.id); return !visibleNodeIds.includes(nodeId); }, [collapsedNodeIds, edges, nodes] ); const getExpandPathIds = useCallback( (nodeId) => { const { visibleEdges } = getVisibleEntities({ nodes, edges, collapsedIds: collapsedNodeIds }); const visibleEdgeIds = visibleEdges.map((e) => e.id); return getExpandPath({ nodeId, edges, visibleEdgeIds }); }, [collapsedNodeIds, edges, nodes] ); return { getIsCollapsed, getExpandPathIds }; }; const useGraph = ({ layoutType, sizingType, labelType, sizingAttribute, clusterAttribute, selections, nodes, edges, actives, collapsedNodeIds, defaultNodeSize, maxNodeSize, minNodeSize, layoutOverrides, constrainDragging }) => { const graph = useStore((state) => state.graph); const clusters = useStore((state) => state.clusters); const storedNodes = useStore((state) => state.nodes); const setClusters = useStore((state) => state.setClusters); const stateCollapsedNodeIds = useStore((state) => state.collapsedNodeIds); const setEdges = useStore((state) => state.setEdges); const stateNodes = useStore((state) => state.nodes); const setNodes = useStore((state) => state.setNodes); const setSelections = useStore((state) => state.setSelections); const setActives = useStore((state) => state.setActives); const drags = useStore((state) => state.drags); const setDrags = useStore((state) => state.setDrags); const setCollapsedNodeIds = useStore((state) => state.setCollapsedNodeIds); const layoutMounted = useRef(false); const layout = useRef(null); const camera = useThree((state) => s