簡體   English   中英

在 next.js 中實現力導向圖

[英]Implement force-directed graph in next js

我正在嘗試創建一個force-directed graph來映射機構中課程之間的交互。 在我的前端使用 Next JS + TypeScript。

已經嘗試使用react-flowdagrevis.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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM