import { ViewPort } from "react-zoomable-ui/dist/ViewPort"; import { CanvasDirection } from "reaflow/dist/layout/elkLayout"; import { create } from "zustand"; import { getChildrenEdges } from "src/lib/utils/graph/getChildrenEdges"; import { getOutgoers } from "src/lib/utils/graph/getOutgoers"; import { parser } from "src/lib/utils/json/jsonParser"; import { NodeData, EdgeData } from "src/types/graph"; import useJson from "./useJson"; export interface Graph { viewPort: ViewPort | null; direction: CanvasDirection; loading: boolean; graphCollapsed: boolean; foldNodes: boolean; fullscreen: boolean; collapseAll: boolean; nodes: NodeData[]; edges: EdgeData[]; collapsedNodes: string[]; collapsedEdges: string[]; collapsedParents: string[]; selectedNode: NodeData | null; path: string; } const initialStates: Graph = { viewPort: null, direction: "RIGHT", loading: true, graphCollapsed: false, foldNodes: false, fullscreen: false, collapseAll: false, nodes: [], edges: [], collapsedNodes: [], collapsedEdges: [], collapsedParents: [], selectedNode: null, path: "", }; interface GraphActions { setGraph: (json?: string, options?: Partial[]) => void; setLoading: (loading: boolean) => void; setDirection: (direction: CanvasDirection) => void; setViewPort: (ref: ViewPort) => void; setSelectedNode: (nodeData: NodeData) => void; focusFirstNode: () => void; expandNodes: (nodeId: string) => void; expandGraph: () => void; collapseNodes: (nodeId: string) => void; collapseGraph: () => void; getCollapsedNodeIds: () => string[]; getCollapsedEdgeIds: () => string[]; toggleFold: (value: boolean) => void; toggleFullscreen: (value: boolean) => void; toggleCollapseAll: (value: boolean) => void; zoomIn: () => void; zoomOut: () => void; centerView: () => void; clearGraph: () => void; setZoomFactor: (zoomFactor: number) => void; } const useGraph = create((set, get) => ({ ...initialStates, toggleCollapseAll: collapseAll => { set({ collapseAll }); get().collapseGraph(); }, clearGraph: () => set({ nodes: [], edges: [], loading: false }), getCollapsedNodeIds: () => get().collapsedNodes, getCollapsedEdgeIds: () => get().collapsedEdges, setSelectedNode: nodeData => set({ selectedNode: nodeData }), setGraph: (data, options) => { const { nodes, edges } = parser(data ?? useJson.getState().json); if (get().collapseAll) { set({ nodes, edges, ...options }); get().collapseGraph(); } else { set({ nodes, edges, collapsedParents: [], collapsedNodes: [], collapsedEdges: [], graphCollapsed: false, ...options, }); } }, setDirection: (direction = "RIGHT") => { set({ direction }); setTimeout(() => get().centerView(), 200); }, setLoading: loading => set({ loading }), expandNodes: nodeId => { const [childrenNodes, matchingNodes] = getOutgoers( nodeId, get().nodes, get().edges, get().collapsedParents ); const childrenEdges = getChildrenEdges(childrenNodes, get().edges); const nodesConnectedToParent = childrenEdges.reduce((nodes: string[], edge) => { edge.from && !nodes.includes(edge.from) && nodes.push(edge.from); edge.to && !nodes.includes(edge.to) && nodes.push(edge.to); return nodes; }, []); const matchingNodesConnectedToParent = matchingNodes.filter(node => nodesConnectedToParent.includes(node) ); const nodeIds = childrenNodes.map(node => node.id).concat(matchingNodesConnectedToParent); const edgeIds = childrenEdges.map(edge => edge.id); const collapsedParents = get().collapsedParents.filter(cp => cp !== nodeId); const collapsedNodes = get().collapsedNodes.filter(nodeId => !nodeIds.includes(nodeId)); const collapsedEdges = get().collapsedEdges.filter(edgeId => !edgeIds.includes(edgeId)); set({ collapsedParents, collapsedNodes, collapsedEdges, graphCollapsed: !!collapsedNodes.length, }); }, collapseNodes: nodeId => { const [childrenNodes] = getOutgoers(nodeId, get().nodes, get().edges); const childrenEdges = getChildrenEdges(childrenNodes, get().edges); const nodeIds = childrenNodes.map(node => node.id); const edgeIds = childrenEdges.map(edge => edge.id); set({ collapsedParents: get().collapsedParents.concat(nodeId), collapsedNodes: get().collapsedNodes.concat(nodeIds), collapsedEdges: get().collapsedEdges.concat(edgeIds), graphCollapsed: !!get().collapsedNodes.concat(nodeIds).length, }); }, collapseGraph: () => { const edges = get().edges; const tos = edges.map(edge => edge.to); const froms = edges.map(edge => edge.from); const parentNodesIds = froms.filter(id => !tos.includes(id)); const secondDegreeNodesIds = edges .filter(edge => parentNodesIds.includes(edge.from)) .map(edge => edge.to); const collapsedParents = get() .nodes.filter(node => !parentNodesIds.includes(node.id) && node.data?.isParent) .map(node => node.id); const collapsedNodes = get() .nodes.filter( node => !parentNodesIds.includes(node.id) && !secondDegreeNodesIds.includes(node.id) ) .map(node => node.id); const closestParentToRoot = Math.min(...collapsedParents.map(n => +n)); const focusNodeId = `g[id*='node-${closestParentToRoot}']`; const rootNode = document.querySelector(focusNodeId); set({ collapsedParents, collapsedNodes, collapsedEdges: get() .edges.filter(edge => !parentNodesIds.includes(edge.from)) .map(edge => edge.id), graphCollapsed: true, }); if (rootNode) { get().viewPort?.camera?.centerFitElementIntoView(rootNode as HTMLElement, { elementExtraMarginForZoom: 300, }); } }, expandGraph: () => { set({ collapsedNodes: [], collapsedEdges: [], collapsedParents: [], graphCollapsed: false, }); }, focusFirstNode: () => { const rootNode = document.querySelector("g[id*='node-1']"); get().viewPort?.camera?.centerFitElementIntoView(rootNode as HTMLElement, { elementExtraMarginForZoom: 100, }); }, setZoomFactor: zoomFactor => { const viewPort = get().viewPort; viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, zoomFactor); }, zoomIn: () => { const viewPort = get().viewPort; viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, viewPort.zoomFactor + 0.1); }, zoomOut: () => { const viewPort = get().viewPort; viewPort?.camera?.recenter(viewPort.centerX, viewPort.centerY, viewPort.zoomFactor - 0.1); }, centerView: () => { const viewPort = get().viewPort; viewPort?.updateContainerSize(); const canvas = document.querySelector(".jsoncrack-canvas") as HTMLElement | null; if (canvas) { viewPort?.camera?.centerFitElementIntoView(canvas); } }, toggleFold: foldNodes => { set({ foldNodes }); get().setGraph(); }, toggleFullscreen: fullscreen => set({ fullscreen }), setViewPort: viewPort => set({ viewPort }), })); export default useGraph;