From 9b3c57471fc57eae4a8ba95c19b8bf4e8efa8c75 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Fri, 3 Apr 2026 15:02:34 +0200 Subject: [PATCH] feat: componente GraphContainer con sigma.js y graphology Visualizacion interactiva de grafos con WebGL via sigma.js, estructura de datos graphology, y layout ForceAtlas2 adaptativo. Soporta grafos dirigidos multi-edge, leyenda de tipos de nodo, y eventos click/double-click. Nuevas deps: graphology, sigma, recharts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../functions/ui/graph/graph_container.md | 103 +++++ frontend/functions/ui/graph/index.tsx | 237 ++++++++++++ frontend/package.json | 5 + frontend/pnpm-lock.yaml | 355 ++++++++++++++++++ 4 files changed, 700 insertions(+) create mode 100644 frontend/functions/ui/graph/graph_container.md create mode 100644 frontend/functions/ui/graph/index.tsx diff --git a/frontend/functions/ui/graph/graph_container.md b/frontend/functions/ui/graph/graph_container.md new file mode 100644 index 00000000..edbfc2ef --- /dev/null +++ b/frontend/functions/ui/graph/graph_container.md @@ -0,0 +1,103 @@ +--- +name: graph_container +kind: component +lang: ts +domain: ui +version: "1.0.0" +purity: impure +signature: "GraphContainer(props: GraphContainerProps): JSX.Element" +description: "Interactive graph visualization with sigma.js, graphology, and ForceAtlas2 layout" +tags: [component, ui, graph, visualization, sigma, graphology, network] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["graphology", "graphology-layout-forceatlas2", "sigma"] +tested: false +tests: [] +test_file_path: "" +file_path: "frontend/functions/ui/graph/index.tsx" +props: + - name: data + type: "GraphData" + required: true + description: "Graph data with nodes and edges arrays" + - name: layout + type: "'organic' | 'random'" + required: false + description: "Layout algorithm (default: organic/ForceAtlas2)" + - name: showLegend + type: "boolean" + required: false + description: "Show node type legend overlay" + - name: nodeTypes + type: "NodeType[]" + required: false + description: "Node type definitions for legend" + - name: onNodeClick + type: "(node: GraphNode) => void" + required: false + description: "Node click handler" + - name: onNodeDoubleClick + type: "(node: GraphNode) => void" + required: false + description: "Node double-click handler" + - name: theme + type: "GraphTheme" + required: false + description: "Visual theme overrides" + - name: height + type: "string | number" + required: false + description: "Container height (default: 100%)" + - name: className + type: "string" + required: false + description: "Additional CSS classes" +emits: [] +has_state: true +framework: react +variant: [] +--- + +## Ejemplo + +```tsx +import { GraphContainer } from '@fn_library/graph' +import type { GraphData } from '@fn_library/graph' + +const data: GraphData = { + nodes: [ + { id: '1', label: 'Node A', color: '#e74c3c', size: 10 }, + { id: '2', label: 'Node B', color: '#3498db', size: 8 }, + ], + edges: [ + { id: 'e1', source: '1', target: '2', label: 'connects', type: 'arrow' }, + ], +} + +function MyGraph() { + return ( + console.log('clicked:', node.id)} + height="500px" + /> + ) +} +``` + +## Notas + +- Usa graphology como estructura de datos de grafo +- ForceAtlas2 para layout organico (iterations adaptativas segun numero de nodos) +- Sigma.js para renderizado WebGL de alto rendimiento +- Soporta grafos dirigidos multi-edge +- El componente limpia la instancia Sigma al desmontar diff --git a/frontend/functions/ui/graph/index.tsx b/frontend/functions/ui/graph/index.tsx new file mode 100644 index 00000000..e1fb584a --- /dev/null +++ b/frontend/functions/ui/graph/index.tsx @@ -0,0 +1,237 @@ +import * as React from "react" +import Graph from "graphology" +import forceAtlas2 from "graphology-layout-forceatlas2" +import { Sigma } from "sigma" + +// ── Types ───────────────────────────────────────────────────────────────── + +export interface GraphNode { + id: string + label: string + type?: string + color?: string + size?: number + x?: number + y?: number + [key: string]: unknown +} + +export interface GraphEdge { + id: string + source: string + target: string + label?: string + color?: string + size?: number + type?: "arrow" | "line" + weight?: number + [key: string]: unknown +} + +export interface GraphData { + nodes: GraphNode[] + edges: GraphEdge[] +} + +export interface NodeType { + type: string + color: string + label: string +} + +export interface GraphTheme { + backgroundColor?: string + nodeColor?: string + nodeSize?: number + edgeColor?: string + edgeSize?: number + labelColor?: string + selectionColor?: string +} + +export interface GraphContainerProps { + data: GraphData + layout?: "organic" | "random" + showToolbar?: boolean + showLegend?: boolean + showMinimap?: boolean + nodeTypes?: NodeType[] + onNodeClick?: (node: GraphNode) => void + onNodeDoubleClick?: (node: GraphNode) => void + enableSelection?: boolean + selectionMode?: "single" | "multiple" + theme?: GraphTheme + height?: string | number + className?: string +} + +const DEFAULT_THEME: Required = { + backgroundColor: "var(--background, #0a0a0f)", + nodeColor: "#95a5a6", + nodeSize: 8, + edgeColor: "rgba(255,255,255,0.19)", + edgeSize: 1, + labelColor: "#e0e0e0", + selectionColor: "#3b82f6", +} + +// ── Component ───────────────────────────────────────────────────────────── + +function GraphContainer({ + data, + layout = "organic", + showLegend = false, + nodeTypes = [], + onNodeClick, + onNodeDoubleClick, + theme: themeProp, + height = "100%", + className, +}: GraphContainerProps) { + const containerRef = React.useRef(null) + const sigmaRef = React.useRef(null) + const graphRef = React.useRef(null) + const theme = React.useMemo( + () => ({ ...DEFAULT_THEME, ...themeProp }), + [themeProp], + ) + + // Build + render + React.useEffect(() => { + const el = containerRef.current + if (!el) return + + // Cleanup previous instance + if (sigmaRef.current) { + sigmaRef.current.kill() + sigmaRef.current = null + } + + const g = new Graph({ multi: true, type: "directed" }) + graphRef.current = g + + // Add nodes + for (const n of data.nodes) { + g.addNode(n.id, { + label: n.label, + x: n.x ?? (Math.random() - 0.5) * 10, + y: n.y ?? (Math.random() - 0.5) * 10, + size: n.size ?? theme.nodeSize, + color: n.color ?? theme.nodeColor, + type: n.type, + }) + } + + // Add edges + for (const e of data.edges) { + try { + g.addEdgeWithKey(e.id, e.source, e.target, { + label: e.label, + size: e.size ?? theme.edgeSize, + color: e.color ?? theme.edgeColor, + type: e.type === "arrow" ? "arrow" : "line", + weight: e.weight ?? 1, + }) + } catch { + // skip duplicate keys + } + } + + // Layout + if (layout === "organic" && g.order > 0) { + forceAtlas2.assign(g, { + iterations: Math.min(500, Math.max(100, g.order * 5)), + settings: { + gravity: 1, + scalingRatio: 2, + slowDown: 5, + barnesHutOptimize: g.order > 300, + }, + }) + } + + // Render + const renderer = new Sigma(g, el, { + renderEdgeLabels: false, + defaultEdgeColor: theme.edgeColor, + defaultNodeColor: theme.nodeColor, + labelColor: { color: theme.labelColor }, + labelSize: 11, + }) + + sigmaRef.current = renderer + + // Events + if (onNodeClick) { + renderer.on("clickNode", ({ node }) => { + const attrs = g.getNodeAttributes(node) + onNodeClick({ id: node, ...attrs } as unknown as GraphNode) + }) + } + if (onNodeDoubleClick) { + renderer.on("doubleClickNode", ({ node }) => { + const attrs = g.getNodeAttributes(node) + onNodeDoubleClick({ id: node, ...attrs } as unknown as GraphNode) + }) + } + + return () => { + renderer.kill() + sigmaRef.current = null + graphRef.current = null + } + }, [data, layout, theme, onNodeClick, onNodeDoubleClick]) + + // Container background + const containerStyle: React.CSSProperties = { + height, + width: "100%", + position: "relative", + background: theme.backgroundColor, + borderRadius: "var(--radius, 0.5rem)", + overflow: "hidden", + } + + return ( +
+
+ {showLegend && nodeTypes.length > 0 && ( +
+ {nodeTypes.map((nt) => ( +
+ + {nt.label} +
+ ))} +
+ )} +
+ ) +} + +export { GraphContainer } diff --git a/frontend/package.json b/frontend/package.json index 8c89cd00..2ccba2f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,10 +18,15 @@ "@fontsource-variable/geist": "^5.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "graphology": "^0.26.0", + "graphology-layout-forceatlas2": "^0.10.1", + "graphology-types": "^0.24.8", "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "recharts": "^3.8.1", "shadcn": "^4.1.1", + "sigma": "^3.0.2", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.4.0" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 133ea1d6..a6c9a9b4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -20,6 +20,15 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + graphology: + specifier: ^0.26.0 + version: 0.26.0(graphology-types@0.24.8) + graphology-layout-forceatlas2: + specifier: ^0.10.1 + version: 0.10.1(graphology-types@0.24.8) + graphology-types: + specifier: ^0.24.8 + version: 0.24.8 lucide-react: specifier: ^1.7.0 version: 1.7.0(react@19.2.4) @@ -29,9 +38,15 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) + recharts: + specifier: ^3.8.1 + version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1) shadcn: specifier: ^4.1.1 version: 4.1.1(typescript@6.0.2) + sigma: + specifier: ^3.0.2 + version: 3.0.2(graphology-types@0.24.8) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -526,6 +541,17 @@ packages: '@oxc-project/types@0.122.0': resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/binding-android-arm64@1.0.0-rc.12': resolution: {integrity: sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -628,6 +654,12 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tailwindcss/node@4.2.2': resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} @@ -724,6 +756,33 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -735,6 +794,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/validate-npm-package-name@4.0.2': resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} @@ -931,6 +993,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -944,6 +1050,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -1031,6 +1140,9 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + esbuild@0.27.4: resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} @@ -1052,6 +1164,13 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -1189,6 +1308,24 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphology-layout-forceatlas2@0.10.1: + resolution: {integrity: sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==} + peerDependencies: + graphology-types: '>=0.19.0' + + graphology-types@0.24.8: + resolution: {integrity: sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==} + + graphology-utils@2.5.2: + resolution: {integrity: sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==} + peerDependencies: + graphology-types: '>=0.23.0' + + graphology@0.26.0: + resolution: {integrity: sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==} + peerDependencies: + graphology-types: '>=0.24.0' + graphql@16.13.2: resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -1232,6 +1369,12 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1239,6 +1382,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + ip-address@10.1.0: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} @@ -1683,6 +1830,21 @@ packages: peerDependencies: react: ^19.2.4 + react-is@19.2.4: + resolution: {integrity: sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -1691,6 +1853,22 @@ packages: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1785,6 +1963,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + sigma@3.0.2: + resolution: {integrity: sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -1957,6 +2138,9 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@8.0.3: resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2517,6 +2701,18 @@ snapshots: '@oxc-project/types@0.122.0': {} + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + '@rolldown/binding-android-arm64@1.0.0-rc.12': optional: true @@ -2575,6 +2771,10 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + '@tailwindcss/node@4.2.2': dependencies: '@jridgewell/remapping': 2.3.5 @@ -2654,6 +2854,30 @@ snapshots: tslib: 2.8.1 optional: true + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 @@ -2664,6 +2888,8 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/validate-npm-package-name@4.0.2': {} '@vitejs/plugin-react@6.0.1(vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))': @@ -2827,12 +3053,52 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + data-uri-to-buffer@4.0.1: {} debug@4.4.3: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + dedent@1.7.2: {} deepmerge@4.3.1: {} @@ -2896,6 +3162,8 @@ snapshots: dependencies: es-errors: 1.3.0 + es-toolkit@1.45.1: {} + esbuild@0.27.4: optionalDependencies: '@esbuild/aix-ppc64': 0.27.4 @@ -2933,6 +3201,10 @@ snapshots: etag@1.8.1: {} + eventemitter3@5.0.4: {} + + events@3.3.0: {} + eventsource-parser@3.0.6: {} eventsource@3.0.7: @@ -3114,6 +3386,22 @@ snapshots: graceful-fs@4.2.11: {} + graphology-layout-forceatlas2@0.10.1(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + graphology-utils: 2.5.2(graphology-types@0.24.8) + + graphology-types@0.24.8: {} + + graphology-utils@2.5.2(graphology-types@0.24.8): + dependencies: + graphology-types: 0.24.8 + + graphology@0.26.0(graphology-types@0.24.8): + dependencies: + events: 3.3.0 + graphology-types: 0.24.8 + graphql@16.13.2: {} has-symbols@1.1.0: {} @@ -3151,6 +3439,10 @@ snapshots: ignore@5.3.2: {} + immer@10.2.0: {} + + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3158,6 +3450,8 @@ snapshots: inherits@2.0.4: {} + internmap@2.0.3: {} + ip-address@10.1.0: {} ipaddr.js@1.9.1: {} @@ -3516,6 +3810,17 @@ snapshots: react: 19.2.4 scheduler: 0.27.0 + react-is@19.2.4: {} + + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + react@19.2.4: {} recast@0.23.11: @@ -3526,6 +3831,32 @@ snapshots: tiny-invariant: 1.3.3 tslib: 2.8.1 + recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@19.2.4)(react@19.2.4)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.4 + immer: 10.2.0 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-is: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -3695,6 +4026,13 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + sigma@3.0.2(graphology-types@0.24.8): + dependencies: + events: 3.3.0 + graphology-utils: 2.5.2(graphology-types@0.24.8) + transitivePeerDependencies: + - graphology-types + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -3834,6 +4172,23 @@ snapshots: vary@1.1.2: {} + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@8.0.3(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0