|
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<Graph>[]) => 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<Graph & GraphActions>((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; |
|
|