[英]Implement force-directed graph in next js
我正在嘗試創建一個force-directed graph
來映射機構中課程之間的交互。 在我的前端使用 Next JS + TypeScript。
已經嘗試使用react-flow
、 dagre
、 vis.network
進行多次嘗試,但得到的不是window: undefined
錯誤,就是該死的 alignment 節點沒有被強制定向到我定義的框內。
在我繼續開箱即用地實施d3-force
之前,有人可以為此推薦任何替代解決方案嗎?
這是我的節點和邊緣的樣子:
這是我對reactflow & dagre
的嘗試:
import React, { useCallback, useEffect, useState } from 'react';
import ReactFlow, {
addEdge,
useNodesState,
useEdgesState,
Edge,
Node,
Position,
ConnectionLineType,
ReactFlowProvider,
MiniMap,
Controls,
Background,
} from 'react-flow-renderer';
import dagre from 'dagre';
import { NodeData, useCourseNodes } from 'src/hooks/useCourseNodes';
import { useDepartment } from '@contexts/ActiveDepartmentContext';
import {
useUpdateActiveCourse,
} from '@contexts/ActiveCourseContext';
import { useDrawerOpen, useUpdateDrawerOpen } from '@contexts/DrawerContext';
const dagreGraph = new dagre.graphlib.Graph({directed:true});
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 10.2;
const nodeHeight = 6.6;
const getLayoutedElements = (
nodes: Node[],
edges:Edge[],
) => {
// const isHorizontal = direction === 'LR';
dagreGraph.setGraph( {width:900, height:900, nodesep:20, ranker:'longest-path' });
nodes.forEach((node: Node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge: Edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
nodes.forEach((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
// node.targetPosition = isHorizontal ? Position.Left : Position.Top;
// node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
node.targetPosition = Position.Top;
node.sourcePosition = Position.Bottom;
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
node.position = {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
};
console.log(nodeWithPosition)
return node;
})
return { layoutedNodes:nodes, layoutedEdges:edges };
};
const LayoutFlow = () => {
const activeDept = useDepartment();
const setActiveCourse = useUpdateActiveCourse();
const setDrawerOpen = useUpdateDrawerOpen()
const drawerOpen = useDrawerOpen();
const {courseList, edgeList} = useCourseNodes()
const { layoutedNodes, layoutedEdges } = getLayoutedElements(courseList, edgeList)
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
const [edges, setEdges,onEdgesChange] = useEdgesState(layoutedEdges);
console.log(layoutedNodes)
const onConnect = useCallback(
(params) =>
setEdges((eds) =>
addEdge({ ...params, type: ConnectionLineType.SimpleBezier, animated: true }, eds),
),
[],
);
// ? For switching between layouts (horizontal & vertical) for phone & desktop
// const onLayout = useCallback(
// (direction) => {
// const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
// nodes,
// edges,
// direction
// );
// setNodes([...layoutedNodes]);
// setEdges([...layoutedEdges]);
// },
// [nodes, edges]
// );
// ? M1 - for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
// ? M2 - (Applied currently in useEffect block below)for force re-rendering react flow graph on state change - https://github.com/wbkd/react-flow/issues/1168
useEffect(() => {
const {layoutedNodes, layoutedEdges} = getLayoutedElements(courseList, edgeList)
setNodes([...layoutedNodes]);
setEdges([...layoutedEdges]);
}, [activeDept, drawerOpen]);
return (
<div style={{ width: '100%', height: '100%' }} className="layoutflow">
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={(e: React.MouseEvent, node: Node<NodeData>) => {
e.preventDefault();
// created a copy of the node since we're only deleting the "label" property from the node object to conveniently map the rest of the data to the "data" property of the active course
const nodeCopy = JSON.parse(JSON.stringify(node))
const { data } = nodeCopy;
const { label } = data
delete data.label
setActiveCourse({
courseId: label,
data
});
setDrawerOpen(true);
}}
connectionLineType={ConnectionLineType.SimpleBezier}
fitView
>
<MiniMap />
<Controls />
{/* <Background /> */}
</ReactFlow>
</ReactFlowProvider>
<div className="controls">
{/* <button onClick={() => onLayout('TB')}>vertical layout</button>
<button onClick={() => onLayout('LR')}>horizontal layout</button> */}
</div>
</div>
);
};
export default LayoutFlow;
這是我對vis.network
的嘗試:
import { useCourseNodes } from "@hooks/useCourseNodes";
import React, { useEffect, useRef } from "react";
import { Network } from "vis-network";
const GraphLayoutFour: React.FC = () => {
const {courseList:nodes, edgeList:edges} = useCourseNodes()
// Create a ref to provide DOM access
const visJsRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const network =
visJsRef.current &&
new Network(visJsRef.current, { nodes, edges } );
// Use `network` here to configure events, etc
}, [visJsRef, nodes, edges]);
return typeof window !== "undefined" ? <div ref={visJsRef} /> : <p>NOT AVAILABLE</p>;
};
export default GraphLayoutFour;
這是我對react-sigma
的嘗試
import React, { ReactNode, useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { UndirectedGraph } from "graphology";
import erdosRenyi from "graphology-generators/random/erdos-renyi";
import randomLayout from "graphology-layout/random";
import chroma from "chroma-js";
import { Attributes } from "graphology-types";
import { ControlsContainer, ForceAtlasControl, SearchControl, SigmaContainer, useLoadGraph, useRegisterEvents, useSetSettings, useSigma, ZoomControl } from "react-sigma-v2/lib/esm";
interface MyCustomGraphProps {
children?: ReactNode;
}
export const MyCustomGraph: React.FC<MyCustomGraphProps> = ({ children }) => {
const sigma = useSigma();
const registerEvents = useRegisterEvents();
const loadGraph = useLoadGraph();
const setSettings = useSetSettings();
const [hoveredNode, setHoveredNode] = useState<any>(null);
useEffect(() => {
// Create the graph
const graph = erdosRenyi(UndirectedGraph, { order: 100, probability: 0.2 });
randomLayout.assign(graph);
graph.nodes().forEach(node => {
graph.mergeNodeAttributes(node, {
label: "label",
size: Math.max(4, Math.random() * 10),
color: chroma.random().hex(),
});
});
loadGraph(graph);
// Register the events
registerEvents({
enterNode: event => setHoveredNode(event.node),
leaveNode: () => setHoveredNode(null),
});
}, []);
useEffect(() => {
setSettings({
nodeReducer: (node, data) => {
const graph = sigma.getGraph();
const newData: Attributes = { ...data, highlighted: data.highlighted || false };
if (hoveredNode) {
//TODO : add type safety
if (node === hoveredNode || (graph as any).neighbors(hoveredNode).includes(node)) {
newData.highlighted = true;
} else {
newData.color = "#E2E2E2";
newData.highlighted = false;
}
}
return newData;
},
edgeReducer: (edge, data) => {
const graph = sigma.getGraph();
const newData = { ...data, hidden: false };
//TODO : add type safety
if (hoveredNode && !(graph as any).extremities(edge).includes(hoveredNode)) {
newData.hidden = true;
}
return newData;
},
});
}, [hoveredNode]);
return <>{children}</>;
};
ReactDOM.render(
<React.StrictMode>
<SigmaContainer>
<MyCustomGraph />
<ControlsContainer position={"bottom-right"}>
<ZoomControl />
<ForceAtlasControl autoRunFor={2000} />
</ControlsContainer>
<ControlsContainer position={"top-right"}>
<SearchControl />
</ControlsContainer>
</SigmaContainer>
</React.StrictMode>,
document.getElementById("root"),
);
import { useCourseNodes } from '@hooks/useCourseNodes'
import dynamic from 'next/dynamic';
import React from 'react'
import { useSigma } from 'react-sigma-v2/lib/esm';
const GraphLayoutThree = () => {
const isBrowser = () => typeof window !== "undefined"
const { courseList, edgeList } = useCourseNodes()
const sigma = useSigma();
if(isBrowser) {
const SigmaContainer = dynamic(import("react-sigma-v2").then(mod => mod.SigmaContainer), {ssr: false});
const MyGraph = dynamic(import("./CustomGraph").then(mod => mod.MyCustomGraph), {ssr: false});
return (
<SigmaContainer style={{ height: "500px", width: "500px" }} >
<MyGraph/>
</SigmaContainer>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutThree
這是我對react-force-graph
的嘗試(注意:我在使用它時稍微修改了邊緣以具有 from-to 而不是 source-target)
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
import dynamic from "next/dynamic";
const GraphLayoutTwo = () => {
const isBrowser = () => typeof window !== "undefined"
if(isBrowser) {
const MyGraph = dynamic(import("./CustomGraphTwo").then(mod => mod.default), {ssr: false});
return (
<MyGraph/>
)
}
else return (<p>NOT AVAILABLE</p>)
}
export default GraphLayoutTwo
為了實現類似的功能,我們在 nextjs 應用程序中使用了react-graph-vis
。
如果出現 window is not defined 錯誤,只需包裝組件並使用dynamic
導入
// components/graph.tsx
export const Graph = ({data, options, events, ...props}) => {
return (
<GraphVis
graph={transformData(data)}
options={options}
events={events}
/>
)
}
然后在你的頁面
// pages/index.ts
const Graph = dynamic(() => (import("../components/graph").then(cmp => cmp.Graph)), { ssr: false })
const Index = () => {
return (
<>
<Graph data={...} .... />
</>
)
}
export default Index;
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.