简体   繁体   English

在 next.js 中实现力导向图

[英]Implement force-directed graph in next js

I'm trying to create a force-directed graph for mapping the interactions between courses in an institution.我正在尝试创建一个force-directed graph来映射机构中课程之间的交互。 Using Next JS + TypeScript for my frontend.在我的前端使用 Next JS + TypeScript。

Have tried several attempts at charting this out using react-flow , dagre , vis.network but am getting either a window: undefined error or just the damn alignment of nodes not being force-directed inside the box I have defined.已经尝试使用react-flowdagrevis.network进行多次尝试,但得到的不是window: undefined错误,就是该死的 alignment 节点没有被强制定向到我定义的框内。

Before I move on with implementing d3-force right out of the box, can someone please recommend any alternative solution to this?在我继续开箱即用地实施d3-force之前,有人可以为此推荐任何替代解决方案吗?

Here's what my nodes & edges look like:这是我的节点和边缘的样子:

Here's my attempt with reactflow & dagre :这是我对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;

Here's my attempt with vis.network : (note: I did slightly modify edges to have from-to instead of source-target when working with this)这是我对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;

Here's my attempt with react-sigma这是我对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

Here's my attempt with react-force-graph (note: I did slightly modify edges to have from-to instead of source-target when working with this)这是我对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

To implement something similar we use react-graph-vis inside a nextjs application.为了实现类似的功能,我们在 nextjs 应用程序中使用了react-graph-vis

If you have the window is not defined error, just wrap the component and import it with dynamic如果出现 window is not defined 错误,只需包装组件并使用dynamic导入

// components/graph.tsx

export const Graph = ({data, options, events, ...props}) => {

      return (
              <GraphVis
                graph={transformData(data)}
                options={options}
                events={events}
              />

      )
}

then in your page然后在你的页面

// 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