reagraph
Version:
WebGL Node-based Graph for React
1 lines • 374 kB
Source Map (JSON)
{"version":3,"file":"index.umd.cjs","sources":["../src/layout/layoutUtils.ts","../src/layout/concentric2d.ts","../src/layout/depthUtils.ts","../src/layout/forceUtils.ts","../src/layout/forceInABox.ts","../src/layout/forceDirected.ts","../src/layout/circular2d.ts","../src/layout/hierarchical.ts","../src/layout/nooverlap.ts","../src/layout/forceatlas2.ts","../src/layout/custom.ts","../src/layout/layoutProvider.ts","../src/layout/recommender.ts","../src/utils/visibility.ts","../src/sizing/pageRank.ts","../src/sizing/centrality.ts","../src/sizing/attribute.ts","../src/sizing/nodeSizeProvider.ts","../src/utils/graph.ts","../src/utils/animation.ts","../src/utils/arrow.ts","../src/utils/position.ts","../src/utils/layout.ts","../src/utils/cluster.ts","../src/utils/paths.ts","../src/utils/dom.ts","../src/store.ts","../src/collapse/utils.ts","../src/collapse/useCollapse.ts","../src/useGraph.ts","../src/symbols/Label.tsx","../src/utils/useDrag.ts","../src/utils/useHoverIntent.ts","../src/CameraControls/useCameraControls.ts","../src/symbols/clusters/Ring.tsx","../src/symbols/Cluster.tsx","../src/symbols/Arrow.tsx","../src/symbols/Line.tsx","../src/symbols/Edge.tsx","../src/symbols/edges/useEdgeGeometry.ts","../src/symbols/edges/useEdgeEvents.ts","../src/symbols/edges/useEdgeAnimations.ts","../src/symbols/edges/Edge.tsx","../src/symbols/edges/Edges.tsx","../src/symbols/Ring.tsx","../src/symbols/nodes/Sphere.tsx","../src/symbols/nodes/Icon.tsx","../src/symbols/nodes/SphereWithIcon.tsx","../src/symbols/nodes/Svg.tsx","../src/symbols/nodes/SphereWithSvg.tsx","../src/symbols/nodes/Badge.tsx","../src/symbols/Node.tsx","../src/CameraControls/utils.ts","../src/CameraControls/useCenterGraph.ts","../src/utils/aggregateEdges.ts","../src/GraphScene.tsx","../src/CameraControls/CameraControls.tsx","../src/themes/darkTheme.ts","../src/themes/lightTheme.ts","../src/selection/utils.ts","../src/selection/Lasso.tsx","../src/GraphCanvas/GraphCanvas.tsx","../src/selection/useSelection.ts","../src/RadialMenu/RadialSlice.tsx","../src/RadialMenu/utils.ts","../src/RadialMenu/RadialMenu.tsx"],"sourcesContent":["import Graph from 'graphology';\nimport { LayoutStrategy } from './types';\nimport { InternalGraphEdge, InternalGraphNode } from '../types';\n\n/**\n * Promise based tick helper.\n */\nexport function tick(layout: LayoutStrategy) {\n return new Promise((resolve, _reject) => {\n let stable: boolean | undefined;\n\n function run() {\n if (!stable) {\n stable = layout.step();\n run();\n } else {\n resolve(stable);\n }\n }\n\n run();\n });\n}\n\n/**\n * Helper function to turn the graph nodes/edges into an array for\n * easier manipulation.\n */\nexport function buildNodeEdges(graph: Graph) {\n const nodes: InternalGraphNode[] = [];\n const edges: InternalGraphEdge[] = [];\n\n graph.forEachNode((id, n: any) => {\n nodes.push({\n ...n,\n id,\n // This is for the clustering\n radius: n.size || 1\n });\n });\n\n graph.forEachEdge((id, l: any) => {\n edges.push({ ...l, id });\n });\n\n return { nodes, edges };\n}\n","import { LayoutFactoryProps } from './types';\nimport { buildNodeEdges } from './layoutUtils';\n\nexport interface ConcentricLayoutInputs extends LayoutFactoryProps {\n /**\n * Base radius of the innermost circle.\n */\n radius: number;\n /**\n * Distance between circles.\n */\n concentricSpacing?: number;\n}\n\nexport function concentricLayout({\n graph,\n radius = 40,\n drags,\n getNodePosition,\n concentricSpacing = 100\n}: ConcentricLayoutInputs) {\n const { nodes, edges } = buildNodeEdges(graph);\n\n const layout: Record<string, { x: number; y: number }> = {};\n\n const getNodesInLevel = (level: number) => {\n const circumference = 2 * Math.PI * (radius + level * concentricSpacing);\n const minNodeSpacing = 40;\n return Math.floor(circumference / minNodeSpacing);\n };\n\n const fixedLevelMap = new Map<number, string[]>();\n const dynamicNodes: { id: string; metric: number }[] = [];\n\n // Split nodes: fixed-level and dynamic\n for (const node of nodes) {\n const data = graph.getNodeAttribute(node.id, 'data');\n const level = data?.level;\n\n if (typeof level === 'number' && level >= 0) {\n if (!fixedLevelMap.has(level)) {\n fixedLevelMap.set(level, []);\n }\n fixedLevelMap.get(level)!.push(node.id);\n } else {\n dynamicNodes.push({ id: node.id, metric: graph.degree(node.id) });\n }\n }\n\n // Sort dynamic nodes by degree\n dynamicNodes.sort((a, b) => b.metric - a.metric);\n\n // Fill layout for fixed-level nodes\n for (const [level, nodeIds] of fixedLevelMap.entries()) {\n const count = nodeIds.length;\n const r = radius + level * concentricSpacing;\n\n for (let i = 0; i < count; i++) {\n const angle = (2 * Math.PI * i) / count;\n layout[nodeIds[i]] = {\n x: r * Math.cos(angle),\n y: r * Math.sin(angle)\n };\n }\n }\n\n // Determine which levels are partially used and which are available\n const occupiedLevels = new Set(fixedLevelMap.keys());\n let dynamicLevel = 0;\n\n let i = 0;\n while (i < dynamicNodes.length) {\n // Skip occupied levels\n while (occupiedLevels.has(dynamicLevel)) {\n dynamicLevel++;\n }\n\n const nodesInLevel = getNodesInLevel(dynamicLevel);\n const r = radius + dynamicLevel * concentricSpacing;\n\n for (let j = 0; j < nodesInLevel && i < dynamicNodes.length; j++) {\n const angle = (2 * Math.PI * j) / nodesInLevel;\n layout[dynamicNodes[i].id] = {\n x: r * Math.cos(angle),\n y: r * Math.sin(angle)\n };\n i++;\n }\n\n dynamicLevel++;\n }\n\n return {\n step() {\n return true;\n },\n getNodePosition(id: string) {\n if (getNodePosition) {\n const pos = getNodePosition(id, { graph, drags, nodes, edges });\n if (pos) return pos;\n }\n\n if (drags?.[id]?.position) {\n return drags[id].position as any;\n }\n\n return layout[id];\n }\n };\n}\n","import { InternalGraphEdge, InternalGraphNode } from '../types';\n\nexport interface DepthNode {\n data: InternalGraphNode;\n ins: DepthNode[];\n out: DepthNode[];\n depth: number;\n}\n\n/**\n * Traverse the graph and get the depth of each node.\n */\nfunction traverseGraph(nodes: DepthNode[], nodeStack: DepthNode[] = []) {\n const currentDepth = nodeStack.length;\n\n for (const node of nodes) {\n const idx = nodeStack.indexOf(node);\n if (idx > -1) {\n const loop = [...nodeStack.slice(idx), node].map(d => d.data.id);\n throw new Error(\n `Invalid Graph: Circular node path detected: ${loop.join(' -> ')}.`\n );\n }\n\n if (currentDepth > node.depth) {\n node.depth = currentDepth;\n traverseGraph(node.out, [...nodeStack, node]);\n }\n }\n}\n\n/**\n * Gets the depth of the graph's nodes. Used in the radial layout.\n */\nexport function getNodeDepth(\n nodes: InternalGraphNode[],\n links: InternalGraphEdge[]\n) {\n let invalid = false;\n\n const graph: { [key: string]: DepthNode } = nodes.reduce(\n (acc, cur) => ({\n ...acc,\n [cur.id]: {\n data: cur,\n out: [],\n depth: -1,\n ins: []\n }\n }),\n {}\n );\n\n try {\n for (const link of links) {\n const from = link.source;\n const to = link.target;\n\n if (!graph.hasOwnProperty(from)) {\n throw new Error(`Missing source Node ${from}`);\n }\n\n if (!graph.hasOwnProperty(to)) {\n throw new Error(`Missing target Node ${to}`);\n }\n\n const sourceNode = graph[from];\n const targetNode = graph[to];\n targetNode.ins.push(sourceNode);\n sourceNode.out.push(targetNode);\n }\n\n traverseGraph(Object.values(graph));\n } catch (e) {\n invalid = true;\n }\n\n const allDepths = Object.keys(graph).map(id => graph[id].depth);\n const maxDepth = Math.max(...allDepths);\n\n return {\n invalid,\n depths: graph,\n maxDepth: maxDepth || 1\n };\n}\n","import { forceRadial as d3ForceRadial } from 'd3-force-3d';\nimport { InternalGraphEdge, InternalGraphNode } from '../types';\nimport { getNodeDepth } from './depthUtils';\n\nconst RADIALS: DagMode[] = ['radialin', 'radialout'];\n\nexport type DagMode =\n | 'lr'\n | 'rl'\n | 'td'\n | 'but'\n | 'zout'\n | 'zin'\n | 'radialin'\n | 'radialout';\n\nexport interface ForceRadialInputs {\n nodes: InternalGraphNode[];\n edges: InternalGraphEdge[];\n mode: DagMode;\n nodeLevelRatio: number;\n}\n\n/**\n * Radial graph layout using D3 Force 3d.\n * Inspired by: https://github.com/vasturiano/three-forcegraph/blob/master/src/forcegraph-kapsule.js#L970-L1018\n */\nexport function forceRadial({\n nodes,\n edges,\n mode = 'lr',\n nodeLevelRatio = 2\n}: ForceRadialInputs) {\n const { depths, maxDepth, invalid } = getNodeDepth(nodes, edges);\n\n if (invalid) {\n return null;\n }\n\n const modeDistance = RADIALS.includes(mode) ? 1 : 5;\n const dagLevelDistance =\n (nodes.length / maxDepth) * nodeLevelRatio * modeDistance;\n\n if (mode) {\n const getFFn =\n (fix: boolean, invert: boolean) => (node: InternalGraphNode) =>\n !fix\n ? undefined\n : (depths[node.id].depth - maxDepth / 2) *\n dagLevelDistance *\n (invert ? -1 : 1);\n\n const fxFn = getFFn(['lr', 'rl'].includes(mode), mode === 'rl');\n const fyFn = getFFn(['td', 'bu'].includes(mode), mode === 'td');\n const fzFn = getFFn(['zin', 'zout'].includes(mode), mode === 'zout');\n\n nodes.forEach(node => {\n node.fx = fxFn(node);\n node.fy = fyFn(node);\n node.fz = fzFn(node);\n });\n }\n\n return RADIALS.includes(mode)\n ? d3ForceRadial(node => {\n const nodeDepth = depths[node.id];\n const depth =\n mode === 'radialin' ? maxDepth - nodeDepth.depth : nodeDepth.depth;\n return depth * dagLevelDistance;\n }).strength(1)\n : null;\n}\n","import {\n forceSimulation,\n forceX,\n forceY,\n forceLink,\n forceManyBody,\n forceCollide\n} from 'd3-force-3d';\nimport { treemap, hierarchy } from 'd3-hierarchy';\nimport { ClusterGroup } from '../utils/cluster';\n\n/**\n * Used for calculating clusterings of nodes.\n *\n * Modified version of: https://github.com/john-guerra/forceInABox\n *\n * Changes:\n * - Improved d3 import for tree shaking\n * - Fixed node lookup for edges using array\n * - Updated d3-force to use d3-force-3d\n * - Removed template logic\n */\nexport function forceInABox() {\n // d3 style\n const constant = (_: any) => () => _;\n const index = (d: any) => d.index;\n\n // Default values\n let id = index;\n let nodes = [];\n let links = []; // needed for the force version\n let clusters: Map<string, ClusterGroup>;\n let tree;\n let size = [100, 100];\n let forceNodeSize = constant(1); // The expected node size used for computing the cluster node\n let forceCharge = constant(-1);\n let forceLinkDistance = constant(100);\n let forceLinkStrength = constant(0.1);\n let foci = {};\n let linkStrengthIntraCluster = 0.1;\n let linkStrengthInterCluster = 0.001;\n let templateNodes = [];\n let offset = [0, 0];\n let templateForce;\n let groupBy = d => d.cluster;\n let template = 'treemap';\n let enableGrouping = true;\n let strength = 0.1;\n\n function force(alpha) {\n if (!enableGrouping) {\n return force;\n }\n\n if (template === 'force') {\n // Do the tick of the template force and get the new focis\n templateForce.tick();\n getFocisFromTemplate();\n }\n\n for (let i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {\n node = nodes[i];\n node.vx += (foci[groupBy(node)].x - node.x) * k;\n node.vy += (foci[groupBy(node)].y - node.y) * k;\n }\n }\n\n function initialize() {\n if (!nodes) {\n return;\n }\n\n if (template === 'treemap') {\n initializeWithTreemap();\n } else {\n initializeWithForce();\n }\n }\n\n force.initialize = function (_) {\n nodes = _;\n initialize();\n };\n\n function getLinkKey(l) {\n let sourceID = groupBy(l.source),\n targetID = groupBy(l.target);\n\n return sourceID <= targetID\n ? sourceID + '~' + targetID\n : targetID + '~' + sourceID;\n }\n\n function computeClustersNodeCounts(nodes) {\n let clustersCounts = new Map(),\n tmpCount: any = {};\n\n nodes.forEach(function (d) {\n if (!clustersCounts.has(groupBy(d))) {\n clustersCounts.set(groupBy(d), { count: 0, sumforceNodeSize: 0 });\n }\n });\n\n nodes.forEach(function (d) {\n tmpCount = clustersCounts.get(groupBy(d));\n tmpCount.count = tmpCount.count + 1;\n tmpCount.sumforceNodeSize =\n tmpCount.sumforceNodeSize +\n // @ts-ignore\n Math.PI * (forceNodeSize(d) * forceNodeSize(d)) * 1.3;\n clustersCounts.set(groupBy(d), tmpCount);\n });\n\n return clustersCounts;\n }\n\n //Returns\n function computeClustersLinkCounts(links) {\n let dClusterLinks = new Map(),\n clusterLinks = [];\n\n links.forEach(function (l) {\n let key = getLinkKey(l),\n count;\n if (dClusterLinks.has(key)) {\n count = dClusterLinks.get(key);\n } else {\n count = 0;\n }\n count += 1;\n dClusterLinks.set(key, count);\n });\n\n dClusterLinks.forEach(function (value, key) {\n let source, target;\n source = key.split('~')[0];\n target = key.split('~')[1];\n if (source !== undefined && target !== undefined) {\n clusterLinks.push({\n source: source,\n target: target,\n count: value\n });\n }\n });\n\n return clusterLinks;\n }\n\n //Returns the metagraph of the clusters\n function getGroupsGraph() {\n let gnodes = [];\n let glinks = [];\n let dNodes = new Map();\n let c;\n let i;\n let cc;\n let clustersCounts;\n let clustersLinks;\n\n clustersCounts = computeClustersNodeCounts(nodes);\n clustersLinks = computeClustersLinkCounts(links);\n\n for (c of clustersCounts.keys()) {\n cc = clustersCounts.get(c);\n gnodes.push({\n id: c,\n size: cc.count,\n r: Math.sqrt(cc.sumforceNodeSize / Math.PI)\n }); // Uses approx meta-node size\n dNodes.set(c, i);\n }\n\n clustersLinks.forEach(function (l) {\n let source = dNodes.get(l.source),\n target = dNodes.get(l.target);\n if (source !== undefined && target !== undefined) {\n glinks.push({\n source: source,\n target: target,\n count: l.count\n });\n }\n });\n\n return { nodes: gnodes, links: glinks };\n }\n\n function getGroupsTree() {\n let children = [];\n let c;\n let cc;\n let clustersCounts;\n\n // @ts-ignore\n clustersCounts = computeClustersNodeCounts(force.nodes());\n\n for (c of clustersCounts.keys()) {\n cc = clustersCounts.get(c);\n children.push({ id: c, size: cc.count });\n }\n return { id: 'clustersTree', children: children };\n }\n\n function getFocisFromTemplate() {\n //compute foci\n // @ts-ignore\n foci.none = { x: 0, y: 0 };\n templateNodes.forEach(function (d) {\n if (template === 'treemap') {\n foci[d.data.id] = {\n x: d.x0 + (d.x1 - d.x0) / 2 - offset[0],\n y: d.y0 + (d.y1 - d.y0) / 2 - offset[1]\n };\n } else {\n foci[d.id] = {\n x: d.x - offset[0],\n y: d.y - offset[1]\n };\n }\n });\n return foci;\n }\n\n function initializeWithTreemap() {\n // @ts-ignore\n let sim = treemap().size(force.size());\n\n tree = hierarchy(getGroupsTree())\n .sum((d: any) => d.radius)\n .sort(function (a, b) {\n return b.height - a.height || b.value - a.value;\n });\n\n templateNodes = sim(tree).leaves();\n getFocisFromTemplate();\n }\n\n function checkLinksAsObjects() {\n // Check if links come in the format of indexes instead of objects\n let linkCount = 0;\n if (nodes.length === 0) return;\n\n links.forEach(function (link) {\n let source, target;\n if (!nodes) {\n return;\n }\n\n source = link.source;\n target = link.target;\n\n if (typeof link.source !== 'object') {\n source = nodes.find(n => n.id === link.source);\n }\n\n if (typeof link.target !== 'object') {\n target = nodes.find(n => n.id === link.target);\n }\n\n if (source === undefined || target === undefined) {\n throw Error(\n 'Error setting links, couldnt find nodes for a link (see it on the console)'\n );\n }\n link.source = source;\n link.target = target;\n link.index = linkCount++;\n });\n }\n\n function initializeWithForce() {\n let net;\n\n if (!nodes || !nodes.length) {\n return;\n }\n\n checkLinksAsObjects();\n\n net = getGroupsGraph();\n\n // Use dragged clusters position if available\n if (clusters.size > 0) {\n net.nodes.forEach(n => {\n // Set fixed X position for cluster\n n.fx = clusters.get(n.id)?.position?.x;\n // Set fixed Y position for cluster\n n.fy = clusters.get(n.id)?.position?.y;\n });\n }\n\n templateForce = forceSimulation(net.nodes)\n .force('x', forceX(size[0] / 2).strength(0.1))\n .force('y', forceY(size[1] / 2).strength(0.1))\n .force('collide', forceCollide(d => d.r).iterations(4))\n .force('charge', forceManyBody().strength(forceCharge))\n .force(\n 'links',\n forceLink(net.nodes.length ? net.links : [])\n .distance(forceLinkDistance)\n .strength(forceLinkStrength)\n );\n\n templateNodes = templateForce.nodes();\n\n getFocisFromTemplate();\n }\n\n force.template = function (x) {\n if (!arguments.length) {\n return template;\n }\n\n template = x;\n initialize();\n return force;\n };\n\n force.groupBy = function (x) {\n if (!arguments.length) {\n return groupBy;\n }\n\n if (typeof x === 'string') {\n groupBy = function (d) {\n return d[x];\n };\n\n return force;\n }\n\n groupBy = x;\n\n return force;\n };\n\n force.enableGrouping = function (x) {\n if (!arguments.length) {\n return enableGrouping;\n }\n\n enableGrouping = x;\n\n return force;\n };\n\n force.strength = function (x) {\n if (!arguments.length) {\n return strength;\n }\n\n strength = x;\n\n return force as any;\n };\n\n force.getLinkStrength = function (e) {\n if (enableGrouping) {\n if (groupBy(e.source) === groupBy(e.target)) {\n if (typeof linkStrengthIntraCluster === 'function') {\n // @ts-ignore\n return linkStrengthIntraCluster(e);\n } else {\n return linkStrengthIntraCluster;\n }\n } else {\n if (typeof linkStrengthInterCluster === 'function') {\n // @ts-ignore\n return linkStrengthInterCluster(e);\n } else {\n return linkStrengthInterCluster;\n }\n }\n } else {\n // Not grouping return the intracluster\n if (typeof linkStrengthIntraCluster === 'function') {\n // @ts-ignore\n return linkStrengthIntraCluster(e);\n } else {\n return linkStrengthIntraCluster;\n }\n }\n };\n\n force.id = function (_) {\n return arguments.length ? ((id = _), force) : id;\n };\n\n force.size = function (_) {\n return arguments.length ? ((size = _), force) : size;\n };\n\n force.linkStrengthInterCluster = function (_) {\n return arguments.length\n ? ((linkStrengthInterCluster = _), force)\n : linkStrengthInterCluster;\n };\n\n force.linkStrengthIntraCluster = function (_) {\n return arguments.length\n ? ((linkStrengthIntraCluster = _), force)\n : linkStrengthIntraCluster;\n };\n\n force.nodes = function (_) {\n return arguments.length ? ((nodes = _), force) : nodes;\n };\n\n force.links = function (_) {\n if (!arguments.length) {\n return links;\n }\n\n if (_ === null) {\n links = [];\n } else {\n links = _;\n }\n\n initialize();\n\n return force;\n };\n\n force.template = function (x) {\n if (!arguments.length) {\n return template;\n }\n\n template = x;\n initialize();\n return force;\n };\n\n force.forceNodeSize = function (_) {\n return arguments.length\n ? ((forceNodeSize = typeof _ === 'function' ? _ : constant(+_)),\n initialize(),\n force)\n : forceNodeSize;\n };\n\n // Legacy support\n force.nodeSize = force.forceNodeSize;\n\n force.forceCharge = function (_) {\n return arguments.length\n ? ((forceCharge = typeof _ === 'function' ? _ : constant(+_)),\n initialize(),\n force)\n : forceCharge;\n };\n\n force.forceLinkDistance = function (_) {\n return arguments.length\n ? ((forceLinkDistance = typeof _ === 'function' ? _ : constant(+_)),\n initialize(),\n force)\n : forceLinkDistance;\n };\n\n force.forceLinkStrength = function (_) {\n return arguments.length\n ? ((forceLinkStrength = typeof _ === 'function' ? _ : constant(+_)),\n initialize(),\n force)\n : forceLinkStrength;\n };\n\n force.offset = function (_) {\n return arguments.length\n ? ((offset = typeof _ === 'function' ? _ : constant(+_)), force)\n : offset;\n };\n\n force.getFocis = getFocisFromTemplate;\n\n // Define the clusters to reuse positions from\n force.setClusters = function (value: any) {\n clusters = value;\n\n return force;\n };\n\n return force;\n}\n","import {\n forceSimulation as d3ForceSimulation,\n forceLink as d3ForceLink,\n forceCollide,\n forceManyBody as d3ForceManyBody,\n forceX as d3ForceX,\n forceY as d3ForceY,\n forceZ as d3ForceZ,\n forceCenter as d3ForceCenter\n} from 'd3-force-3d';\nimport { forceRadial, DagMode } from './forceUtils';\nimport { LayoutFactoryProps, LayoutStrategy } from './types';\nimport { buildNodeEdges } from './layoutUtils';\nimport { forceInABox } from './forceInABox';\nimport { FORCE_LAYOUTS } from './layoutProvider';\nimport { ClusterGroup } from '../utils/cluster';\n\nexport interface ForceDirectedLayoutInputs extends LayoutFactoryProps {\n /**\n * Center inertia for the layout. Default: 1.\n */\n centerInertia?: number;\n\n /**\n * Number of dimensions for the layout. 2d or 3d.\n */\n dimensions?: number;\n\n /**\n * Mode for the dag layout. Only applicable for dag layouts.\n */\n mode?: DagMode;\n\n /**\n * Distance between links.\n */\n linkDistance?: number;\n\n /**\n * Strength of the node repulsion.\n */\n nodeStrength?: number;\n\n /**\n * Strength of the cluster repulsion.\n */\n clusterStrength?: number;\n\n /**\n * The clusters dragged position to reuse for the layout.\n */\n clusters: Map<string, ClusterGroup>;\n\n /**\n * The type of clustering.\n */\n clusterType?: 'force' | 'treemap';\n\n /**\n * Ratio of the distance between nodes on the same level.\n */\n nodeLevelRatio?: number;\n\n /**\n * LinkStrength between nodes of different clusters\n */\n linkStrengthInterCluster?: number | ((d: any) => number);\n\n /**\n * LinkStrength between nodes of the same cluster\n */\n linkStrengthIntraCluster?: number | ((d: any) => number);\n\n /**\n * Charge between the meta-nodes (Force template only)\n */\n forceLinkDistance?: number;\n\n /**\n * Used to compute the template force nodes size (Force template only)\n */\n forceLinkStrength?: number;\n\n /**\n * Used to compute the template force nodes size (Force template only)\n */\n forceCharge?: number;\n\n /**\n * Used to determine the simulation forceX and forceY values\n */\n forceLayout: (typeof FORCE_LAYOUTS)[number];\n}\n\nexport function forceDirected({\n graph,\n nodeLevelRatio = 2,\n mode = null,\n dimensions = 2,\n nodeStrength = -250,\n linkDistance = 50,\n clusterStrength = 0.5,\n linkStrengthInterCluster = 0.01,\n linkStrengthIntraCluster = 0.5,\n forceLinkDistance = 100,\n forceLinkStrength = 0.1,\n clusterType = 'force',\n forceCharge = -700,\n getNodePosition,\n drags,\n clusters,\n clusterAttribute,\n forceLayout\n}: ForceDirectedLayoutInputs): LayoutStrategy {\n const { nodes, edges } = buildNodeEdges(graph);\n\n // Dynamically adjust node strength based on the number of edges\n const is2d = dimensions === 2;\n const nodeStrengthAdjustment =\n is2d && edges.length > 25 ? nodeStrength * 2 : nodeStrength;\n\n let forceX;\n let forceY;\n if (forceLayout === 'forceDirected2d') {\n forceX = d3ForceX();\n forceY = d3ForceY();\n } else {\n forceX = d3ForceX(600).strength(0.05);\n forceY = d3ForceY(600).strength(0.05);\n }\n\n // Create the simulation\n const sim = d3ForceSimulation()\n .force('center', d3ForceCenter(0, 0))\n .force('link', d3ForceLink())\n .force('charge', d3ForceManyBody().strength(nodeStrengthAdjustment))\n .force('x', forceX)\n .force('y', forceY)\n .force('z', d3ForceZ())\n // Handles nodes not overlapping each other ( most relevant in clustering )\n .force(\n 'collide',\n forceCollide(d => d.radius + 10)\n )\n .force(\n 'dagRadial',\n forceRadial({\n nodes,\n edges,\n mode,\n nodeLevelRatio\n })\n )\n .stop();\n\n let groupingForce;\n if (clusterAttribute) {\n // Dynamically adjust cluster force charge based on the number of nodes\n let forceChargeAdjustment = forceCharge;\n if (nodes?.length) {\n const adjustmentFactor = Math.ceil(nodes.length / 200);\n forceChargeAdjustment = forceCharge * adjustmentFactor;\n }\n\n groupingForce = forceInABox()\n // The clusters dragged position to reuse for the layout\n .setClusters(clusters)\n // Strength to foci\n .strength(clusterStrength)\n // Either treemap or force\n .template(clusterType)\n // Node attribute to group\n .groupBy(d => d.data[clusterAttribute])\n // The graph links. Must be called after setting the grouping attribute\n .links(edges)\n // Size of the chart\n .size([100, 100])\n // linkStrength between nodes of different clusters\n .linkStrengthInterCluster(linkStrengthInterCluster)\n // linkStrength between nodes of the same cluster\n .linkStrengthIntraCluster(linkStrengthIntraCluster)\n // linkDistance between meta-nodes on the template (Force template only)\n .forceLinkDistance(forceLinkDistance)\n // linkStrength between meta-nodes of the template (Force template only)\n .forceLinkStrength(forceLinkStrength)\n // Charge between the meta-nodes (Force template only)\n .forceCharge(forceChargeAdjustment)\n // Used to compute the template force nodes size (Force template only)\n .forceNodeSize(d => d.radius);\n }\n\n // Initialize the simulation\n let layout = sim.numDimensions(dimensions).nodes(nodes);\n\n if (groupingForce) {\n layout = layout.force('group', groupingForce);\n }\n\n // Run the force on the links\n if (linkDistance) {\n let linkForce = layout.force('link');\n if (linkForce) {\n linkForce\n .id(d => d.id)\n .links(edges)\n // When no mode passed, its a tree layout\n // so let's use a larger distance\n .distance(linkDistance);\n\n if (groupingForce) {\n linkForce = linkForce.strength(groupingForce?.getLinkStrength ?? 0.1);\n }\n }\n }\n\n const nodeMap = new Map(nodes.map(n => [n.id, n]));\n\n return {\n step() {\n // Run the simulation til we get a stable result\n while (sim.alpha() > 0.01) {\n sim.tick();\n }\n return true;\n },\n getNodePosition(id: string) {\n if (getNodePosition) {\n const pos = getNodePosition(id, { graph, drags, nodes, edges });\n if (pos) {\n return pos;\n }\n }\n\n if (drags?.[id]?.position) {\n // If we dragged, we need to use that position\n return drags?.[id]?.position as any;\n }\n\n return nodeMap.get(id);\n }\n };\n}\n","import circular from 'graphology-layout/circular.js';\nimport { LayoutFactoryProps } from './types';\nimport { buildNodeEdges } from './layoutUtils';\n\nexport interface CircularLayoutInputs extends LayoutFactoryProps {\n /**\n * Radius of the circle.\n */\n radius: number;\n}\n\nexport function circular2d({\n graph,\n radius,\n drags,\n getNodePosition\n}: CircularLayoutInputs) {\n const layout = circular(graph, {\n scale: radius\n });\n\n const { nodes, edges } = buildNodeEdges(graph);\n\n return {\n step() {\n return true;\n },\n getNodePosition(id: string) {\n if (getNodePosition) {\n const pos = getNodePosition(id, { graph, drags, nodes, edges });\n if (pos) {\n return pos;\n }\n }\n\n if (drags?.[id]?.position) {\n // If we dragged, we need to use that position\n return drags?.[id]?.position as any;\n }\n\n return layout?.[id];\n }\n };\n}\n","import { InternalGraphNode } from '../types';\nimport { DepthNode, getNodeDepth } from './depthUtils';\nimport { LayoutFactoryProps, LayoutStrategy } from './types';\nimport { hierarchy, stratify, tree } from 'd3-hierarchy';\nimport { buildNodeEdges } from './layoutUtils';\n\nexport interface HierarchicalLayoutInputs extends LayoutFactoryProps {\n /**\n * Direction of the layout. Default 'td'.\n */\n mode?: 'td' | 'lr';\n /**\n * Factor of distance between nodes. Default 1.\n */\n nodeSeparation?: number;\n /**\n * Size of each node. Default [50,50]\n */\n nodeSize?: [number, number];\n}\n\nconst DIRECTION_MAP = {\n td: {\n x: 'x',\n y: 'y',\n factor: -1\n },\n lr: {\n x: 'y',\n y: 'x',\n factor: 1\n }\n};\n\nexport function hierarchical({\n graph,\n drags,\n mode = 'td',\n nodeSeparation = 1,\n nodeSize = [50, 50],\n getNodePosition\n}: HierarchicalLayoutInputs): LayoutStrategy {\n const { nodes, edges } = buildNodeEdges(graph);\n\n const { depths } = getNodeDepth(nodes, edges);\n const rootNodes = Object.keys(depths).map(d => depths[d]);\n\n const root = stratify<DepthNode>()\n .id(d => d.data.id)\n .parentId(d => d.ins?.[0]?.data?.id)(rootNodes);\n\n const treeRoot = tree()\n .separation(() => nodeSeparation)\n .nodeSize(nodeSize)(hierarchy(root));\n\n const treeNodes = treeRoot.descendants();\n const path = DIRECTION_MAP[mode];\n\n const mappedNodes = new Map<string, InternalGraphNode>(\n nodes.map(n => {\n const { x, y } = treeNodes.find((t: any) => t.data.id === n.id);\n return [\n n.id,\n {\n ...n,\n [path.x]: x * path.factor,\n [path.y]: y * path.factor,\n z: 0\n }\n ];\n })\n );\n\n return {\n step() {\n return true;\n },\n getNodePosition(id: string) {\n if (getNodePosition) {\n const pos = getNodePosition(id, { graph, drags, nodes, edges });\n if (pos) {\n return pos;\n }\n }\n\n if (drags?.[id]?.position) {\n // If we dragged, we need to use that position\n return drags?.[id]?.position as any;\n }\n\n return mappedNodes.get(id);\n }\n };\n}\n","import noverlapLayout from 'graphology-layout-noverlap';\nimport { LayoutFactoryProps } from './types';\nimport { buildNodeEdges } from './layoutUtils';\n\nexport interface NoOverlapLayoutInputs extends LayoutFactoryProps {\n /**\n * Grid size. Default 20.\n */\n gridSize?: number;\n\n /**\n * Ratio of the layout. Default 10.\n */\n ratio?: number;\n\n /**\n * Maximum number of iterations. Default 50.\n */\n maxIterations?: number;\n\n /**\n * Margin between nodes. Default 10.\n */\n margin?: number;\n}\n\nexport function nooverlap({\n graph,\n margin,\n drags,\n getNodePosition,\n ratio,\n gridSize,\n maxIterations\n}: NoOverlapLayoutInputs) {\n const { nodes, edges } = buildNodeEdges(graph);\n\n const layout = noverlapLayout(graph, {\n maxIterations,\n inputReducer: (_key, attr) => ({\n ...attr,\n // Have to specify defaults for the engine\n x: attr.x || 0,\n y: attr.y || 0\n }),\n settings: {\n ratio,\n margin,\n gridSize\n }\n });\n\n return {\n step() {\n return true;\n },\n getNodePosition(id: string) {\n if (getNodePosition) {\n const pos = getNodePosition(id, { graph, drags, nodes, edges });\n if (pos) {\n return pos;\n }\n }\n\n if (drags?.[id]?.position) {\n // If we dragged, we need to use that position\n return drags?.[id]?.position as any;\n }\n\n return layout?.[id];\n }\n };\n}\n","import forceAtlas2Layout from 'graphology-layout-forceatlas2';\nimport { LayoutFactoryProps } from './types';\nimport random from 'graphology-layout/random.js';\n\nexport interface ForceAtlas2LayoutInputs extends LayoutFactoryProps {\n /**\n * Should the node’s sizes be taken into account. Default: false.\n */\n adjustSizes?: boolean;\n\n /**\n * whether to use the Barnes-Hut approximation to compute\n * repulsion in O(n*log(n)) rather than default O(n^2),\n * n being the number of nodes. Default: false.\n */\n barnesHutOptimize?: boolean;\n\n /**\n * Barnes-Hut approximation theta parameter. Default: 0.5.\n */\n barnesHutTheta?: number;\n\n /**\n * Influence of the edge’s weights on the layout. To consider edge weight, don’t\n * forget to pass weighted as true. Default: 1.\n */\n edgeWeightInfluence?: number;\n\n /**\n * Strength of the layout’s gravity. Default: 10.\n */\n gravity?: number;\n\n /**\n * Whether to use Noack’s LinLog model. Default: false.\n */\n linLogMode?: boolean;\n\n /**\n * Whether to consider edge weights when calculating repulsion. Default: false.\n */\n outboundAttractionDistribution?: boolean;\n\n /**\n * Scaling ratio for repulsion. Default: 100.\n */\n scalingRatio?: number;\n\n /**\n * Speed of the slowdown. Default: 1.\n */\n slowDown?: number;\n\n /**\n * Whether to use the strong gravity mode. Default: false.\n */\n strongGravityMode?: boolean;\n\n /**\n * Number of iterations to perform. Default: 50.\n */\n iterations?: number;\n}\n\nexport function forceAtlas2({\n graph,\n drags,\n iterations,\n ...rest\n}: ForceAtlas2LayoutInputs) {\n // Note: We need to assign a random position to each node\n // in order for the force atlas to work.\n // Reference: https://graphology.github.io/standard-library/layout-forceatlas2.html#pre-requisites\n random.assign(graph);\n\n const layout = forceAtlas2Layout(graph, {\n iterations,\n settings: rest\n });\n\n return {\n step() {\n return true;\n },\n getNodePosition(id: string) {\n // If we dragged, we need to use that position\n return (drags?.[id]?.position as any) || layout?.[id];\n }\n };\n}\n","import { LayoutFactoryProps } from './types';\nimport { buildNodeEdges } from './layoutUtils';\n\nexport function custom({ graph, drags, getNodePosition }: LayoutFactoryProps) {\n const { nodes, edges } = buildNodeEdges(graph);\n\n return {\n step() {\n return true;\n },\n getNodePosition(id: string) {\n return getNodePosition(id, { graph, drags, nodes, edges });\n }\n };\n}\n","import { concentricLayout } from 'layout/concentric2d';\nimport { LayoutFactoryProps, LayoutStrategy } from './types';\nimport { forceDirected, ForceDirectedLayoutInputs } from './forceDirected';\nimport { circular2d, CircularLayoutInputs } from './circular2d';\nimport { ConcentricLayoutInputs } from './concentric2d';\nimport { hierarchical, HierarchicalLayoutInputs } from './hierarchical';\nimport { NoOverlapLayoutInputs, nooverlap } from './nooverlap';\nimport { ForceAtlas2LayoutInputs, forceAtlas2 } from './forceatlas2';\nimport { custom } from './custom';\n\nexport type LayoutOverrides = Partial<\n | Omit<ForceDirectedLayoutInputs, 'dimensions' | 'mode'>\n | CircularLayoutInputs\n | ConcentricLayoutInputs\n | HierarchicalLayoutInputs\n>;\n\nexport const FORCE_LAYOUTS = [\n 'forceDirected2d',\n 'treeTd2d',\n 'treeLr2d',\n 'radialOut2d',\n 'treeTd3d',\n 'treeLr3d',\n 'radialOut3d',\n 'forceDirected3d'\n];\n\nexport function layoutProvider({\n type,\n ...rest\n}: LayoutFactoryProps | LayoutOverrides): LayoutStrategy {\n if (FORCE_LAYOUTS.includes(type)) {\n const { nodeStrength, linkDistance, nodeLevelRatio } =\n rest as ForceDirectedLayoutInputs;\n\n if (type === 'forceDirected2d') {\n return forceDirected({\n ...rest,\n dimensions: 2,\n nodeLevelRatio: nodeLevelRatio || 2,\n nodeStrength: nodeStrength || -250,\n linkDistance,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n } else if (type === 'treeTd2d') {\n return forceDirected({\n ...rest,\n mode: 'td',\n dimensions: 2,\n nodeLevelRatio: nodeLevelRatio || 5,\n nodeStrength: nodeStrength || -250,\n linkDistance: linkDistance || 50,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n } else if (type === 'treeLr2d') {\n return forceDirected({\n ...rest,\n mode: 'lr',\n dimensions: 2,\n nodeLevelRatio: nodeLevelRatio || 5,\n nodeStrength: nodeStrength || -250,\n linkDistance: linkDistance || 50,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n } else if (type === 'radialOut2d') {\n return forceDirected({\n ...rest,\n mode: 'radialout',\n dimensions: 2,\n nodeLevelRatio: nodeLevelRatio || 5,\n nodeStrength: nodeStrength || -500,\n linkDistance: linkDistance || 100,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n } else if (type === 'treeTd3d') {\n return forceDirected({\n ...rest,\n mode: 'td',\n dimensions: 3,\n nodeLevelRatio: nodeLevelRatio || 2,\n nodeStrength: nodeStrength || -500,\n linkDistance: linkDistance || 50\n } as ForceDirectedLayoutInputs);\n } else if (type === 'treeLr3d') {\n return forceDirected({\n ...rest,\n mode: 'lr',\n dimensions: 3,\n nodeLevelRatio: nodeLevelRatio || 2,\n nodeStrength: nodeStrength || -500,\n linkDistance: linkDistance || 50,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n } else if (type === 'radialOut3d') {\n return forceDirected({\n ...rest,\n mode: 'radialout',\n dimensions: 3,\n nodeLevelRatio: nodeLevelRatio || 2,\n nodeStrength: nodeStrength || -500,\n linkDistance: linkDistance || 100,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n } else if (type === 'forceDirected3d') {\n return forceDirected({\n ...rest,\n dimensions: 3,\n nodeLevelRatio: nodeLevelRatio || 2,\n nodeStrength: nodeStrength || -250,\n linkDistance,\n forceLayout: type\n } as ForceDirectedLayoutInputs);\n }\n } else if (type === 'circular2d') {\n const { radius } = rest as CircularLayoutInputs;\n return circular2d({\n ...rest,\n radius: radius || 300\n } as CircularLayoutInputs);\n } else if (type === 'concentric2d') {\n return concentricLayout(rest as CircularLayoutInputs);\n } else if (type === 'hierarchicalTd') {\n return hierarchical({ ...rest, mode: 'td' } as HierarchicalLayoutInputs);\n } else if (type === 'hierarchicalLr') {\n return hierarchical({ ...rest, mode: 'lr' } as HierarchicalLayoutInputs);\n } else if (type === 'nooverlap') {\n const { graph, maxIterations, ratio, margin, gridSize, ...settings } =\n rest as NoOverlapLayoutInputs;\n\n return nooverlap({\n type: 'nooverlap',\n graph,\n margin: margin || 10,\n maxIterations: maxIterations || 50,\n ratio: ratio || 10,\n gridSize: gridSize || 20,\n ...settings\n });\n } else if (type === 'forceatlas2') {\n const { graph, iterations, gravity, scalingRatio, ...settings } =\n rest as ForceAtlas2LayoutInputs;\n\n return forceAtlas2({\n type: 'forceatlas2',\n graph,\n ...settings,\n scalingRatio: scalingRatio || 100,\n gravity: gravity || 10,\n iterations: iterations || 50\n });\n } else if (type === 'custom') {\n return custom({\n type: 'custom',\n ...rest\n } as LayoutFactoryProps);\n }\n\n throw new Error(`Layout ${type} not found.`);\n}\n","import { GraphEdge, GraphNode } from '../types';\nimport { getNodeDepth } from './depthUtils';\nimport { LayoutTypes } from './types';\n\n/**\n * Given a set of nodes and edges, determine the type of layout that\n * is most ideal. This is very beta.\n */\nexport function recommendLayout(\n nodes: GraphNode[],\n edges: GraphEdge[]\n): LayoutTypes {\n const { invalid } = getNodeDepth(nodes as any[], edges as any[]);\n const nodeCount = nodes.length;\n\n if (!invalid) {\n // Large tree layouts\n if (nodeCount > 100) {\n return 'radialOut2d';\n } else {\n // Smaller tree layouts\n return 'treeTd2d';\n }\n }\n\n // Circular layouts\n return 'forceDirected2d';\n}\n","import { PerspectiveCamera } from 'three';\nimport { EdgeLabelPosition } from '../symbols';\n\nexport type LabelVisibilityType = 'all' | 'auto' | 'none' | 'nodes' | 'edges';\n\ninterface CalcLabelVisibilityArgs {\n nodeCount: number;\n nodePosition?: { x: number; y: number; z: number };\n labelType: LabelVisibilityType;\n camera?: PerspectiveCamera;\n}\n\nexport function calcLabelVisibility({\n nodePosition,\n labelType,\n camera\n}: CalcLabelVisibilityArgs) {\n return (shape: 'node' | 'edge', size: number) => {\n const isAlwaysVisible =\n labelType === 'all' ||\n (labelType === 'nodes' && shape === 'node') ||\n (labelType === 'edges' && shape === 'edge');\n\n if (\n !isAlwaysVisible &&\n camera &&\n nodePosition &&\n camera?.position?.z / camera?.zoom - nodePosition?.z > 6000\n ) {\n return false;\n }\n\n if (isAlwaysVisible) {\n return true;\n } else if (labelType === 'auto' && shape === 'node') {\n if (size > 7) {\n return true;\n } else if (\n camera &&\n nodePosition &&\n camera.position.z / camera.zoom - nodePosition.z < 3000\n ) {\n return true;\n }\n }\n\n return false;\n };\n}\n\nexport function getLabelOffsetByType(\n offset: number,\n position: EdgeLabelPosition\n): number {\n switch (position) {\n case 'above':\n return offset;\n case 'below':\n return -offset;\n case 'inline':\n case 'natural':\n default:\n return 0;\n }\n}\n\nexport const isServerRender = typeof window === 'undefined';\n","import pagerank from 'graphology-metrics/centrality/pagerank.js';\nimport { SizingStrategy, SizingStrategyInputs } from './types';\n\nexport function pageRankSizing({\n graph\n}: SizingStrategyInputs): SizingStrategy {\n const ranks = pagerank(graph);\n\n return {\n ranks,\n getSizeForNode: (nodeID: string) => ranks[nodeID] * 80\n };\n}\n","import { SizingStrategy, SizingStrategyInputs } from './types';\nimport { degreeCentrality } from 'graphology-metrics/centrality/degree.js';\n\nexport function centralitySizing({\n graph\n}: SizingStrategyInputs): SizingStrategy {\n const ranks = degreeCentrality(graph);\n\n return {\n ranks,\n getSizeForNode: (nodeID: string) => ranks[nodeID] * 20\n };\n}\n","import { SizingStrategy, SizingStrategyInputs } from './types';\n\nexport function attributeSizing({\n graph,\n attribute,\n defaultSize\n}: SizingStrategyInputs): SizingStrategy {\n const map = new Map();\n\n if (attribute) {\n graph.forEachNode((id, node) => {\n const size = node.data?.[attribute];\n if (isNaN(size)) {\n console.warn(`Attribute ${size} is not a number for node ${node.id}`);\n }\n\n map.set(id, size || 0);\n });\n } else {\n console.warn('Attribute sizing configured but no attribute provided');\n }\n\n return {\n getSizeForNode: (nodeId: string) => {\n if (!attribute || !map) {\n return defaultSize;\n }\n\n return map.get(nodeId);\n }\n };\n}\n","import { pageRankSizing } from './pageRank';\nimport { centralitySizing } from './centrality';\nimport { attributeSizing } from './attribute';\nimport { SizingStrategyInputs } from './types';\nimport { scaleLinear } from 'd3-scale';\n\nexport type SizingType =\n | 'none'\n | 'pagerank'\n | 'centrality'\n | 'attribute'\n | 'default';\n\nexport interface NodeSizeProviderInputs extends SizingStrategyInputs {\n /**\n * The sizing strategy to use.\n */\n type: SizingType;\n}\n\nconst providers = {\n pagerank: pageRankSizing,\n centrality: centralitySizing,\n attribute: attributeSizing,\n none: ({ defaultSize }: SizingStrategyInputs) => ({\n getSizeForNode: (_id: string) => defaultSize\n })\n};\n\nexport function nodeSizeProvider({ type, ...rest }: NodeSizeProviderInputs) {\n const provider = providers[type]?.(rest);\n if (!provider && type !== 'default') {\n throw new Error(`Unknown sizing strategy: ${type}`);\n }\n\n const { graph, minSize, maxSize } = rest;\n const sizes = new Map();\n let min;\n let max;\n\n graph.forEachNode((id, node) => {\n let size;\n if (type === 'default') {\n size = node.size || rest.defaultSize;\n } else {\n size = provider.getSizeForNode(id);\n }\n\n if (min === undefined || size < min) {\n min = size;\n }\n\n if (max === undefined || size > max) {\n max = size;\n }\n\n sizes.set(id, size);\n });\n\n // Relatively scale the sizes\n if (type !== 'none') {\n const scale = scaleLinear()\n .domain([min, max])\n .rangeRound([minSize, maxSize]);\n\n for (const [nodeId, size] of sizes) {\n sizes.set(nodeId, scale(size));\n }\n }\n\n return sizes;\n}\n","import Graph from 'graphology';\nimport { nodeSizeProvider, SizingType } from '../sizing';\nimport {\n GraphEdge,\n GraphNode,\n InternalGraphEdge,\n InternalGraphNode\n} from '../types';\nimport { calcLabelVisibility, LabelVisibilityType } from './visibility';\nimport { LayoutStrategy } from '../layout';\n\n/**\n * Initialize the graph with the nodes/edges.\n */\nexport function buildGraph(\n graph: Graph,\n nodes: GraphNode[],\n edges: GraphEdge[]\n) {\n // TODO: We probably want to make this\n // smarter and only add/remove nodes\n graph.clear();\n\n const addedNodes = new Set<string>();\n\n for (const node of nodes) {\n try {\n if (!addedNodes.has(node.id)) {\n graph.addNode(node.id, node);\n addedNodes.add(node.id);\n }\n } catch (e) {\n console.error(`[Graph] Error adding node '${node.id}`, e);\n }\n }\n\n for (const edge of edges) {\n if (!addedNodes.has(edge.source) || !addedNodes.has(edge.target)) {\n continue;\n }\n\n try {\n graph.addEdge(edge.source, edge.target, edge);\n } catch (e) {\n console.error(\n `[Graph] Error adding edge '${edge.source} -> ${edge.target}`,\n e\n );\n }\n }\n\n return graph;\n}\n\ninterface TransformGraphInput {\n graph: Graph;\n layout: LayoutStrategy;\n sizingType?: SizingType;\n labelType?: LabelVisibilityType;\n sizingAttribute?: string;\n minNodeSize?: number;\n maxNodeSize?: number;\n defaultNodeSize?: number;\n clusterAttribute?: string;\n}\n\n/**\n * Transform the graph into a format that is easier to work with.\n */\nexport function transformGraph({\n graph,\n layout,\n sizingType,\n labelType,\n sizingAttribute,\n defaultNodeSize,\n minNodeSize,\n maxNodeSize,\n clusterAttribute\n}: TransformGraphInput) {\n const nodes: InternalGraphNode[] = [];\n const edges: InternalGraphEdge[] = [];\n const map = new Map<string, InternalGraphNode>();\n\n const sizes = nodeSizeProvider({\n graph,\n type: sizingType,\n attribute: sizingAttribute,\n minSize: minNodeSize,\n maxSize: maxNodeSize,\n defaultSize: defaultNodeSize\n });\n\n const nodeCount = graph.nodes().length;\n const checkVisibility = calcLabelVisibility({ nodeCount, labelType });\n\n graph.forEachNode((id, node) => {\n const position = layout.getNodePosition(id);\n const { data, fill, icon, label, size, ...rest } = node;\n const nodeSize = sizes.get(node.id);\n const labelVisible = checkVisibility('node', nodeSize);\n\n const nodeLinks = graph.inboundNeighbors(node.id) || [];\n const parents = nodeLinks.map(n => graph.getNodeAttributes(n));\n\n const n: InternalGraphNode = {\n ...(node as any),\n size: nodeSize,\n labelVisible,\n label,\n icon,\n fill,\n cluster: clusterAttribute ? data[clusterAttribute] : undefined,\n parents,\n data: {\n ...rest,\n ...(data ?? {})\n },\n position: {\n ...position,\n x: position.x || 0,\n y: position.y || 0,\n z: position.z || 1\n }\n };\n\n map.set(node.id, n);\n nodes.push(n);\n });\n\n graph.forEachEdge((_id, link) => {\n const from = map.get(link.source);\n const to = map.get(link.target);\n\n if (from && to) {\n const { data, id, label, size, ...rest } = link;\n const labelVisible = checkVisibility('edge', size);\n\n // TODO: Fix type\n edges.push({\n ...link,\n id,\n label,\n labelVisible,\n size,\n data: {\n