plotly / react-cytoscapejs

React component for Cytoscape.js network visualisations
MIT License
470 stars 69 forks source link

Infinite render graph force-directed on parameter "randomize": false #124

Open DaniilRyb opened 11 months ago

DaniilRyb commented 11 months ago

Hello! I found problem of infinite render page (or graph i don't know what exactly) Bug: infinite render (Chrome Version 116.0.5845.187 (Official Build) (arm64))

Basic dependencies for convenience: "cytoscape": "^3.0.0", "react-cytoscapejs": "^2.0.0", "cytoscape-euler": "^1.2.2",

The main problem is in the CytoscapeСomponent (all code of the Graph component on React) and in the layouts object, namely the property "randomize": false.

One of the main reasons, it seems to me, is the following:

  1. Problem in React. There may be something with the life cycles of rendering components that conflict with the library itself. Although of course they did, but still.
  2. Perhaps some fields in the layout object are ignored, because the "numIter" object parameter simply does not work in this situation.

Perhaps, bug in current component, if you find, please write about it. With randomize: true, all works good just in case, I attach the entire component code.

{
  "name": "project",
  "version": "0.1.0",
  "private": true,
  "homepage": "/",
  "dependencies": {
    "@reduxjs/toolkit": "^1.9.3",
    "@types/chart.js": "^2.9.38",
    "@types/chartjs": "^0.0.31",
    "@types/cytoscape": "^3.19.11",
    "@types/react": "^18.0.29",
    "@types/react-cytoscapejs": "^1.2.2",
    "@types/react-dom": "^18.0.11",
    "@types/react-redux": "^7.1.25",
    "@types/styled-components": "^5.1.27",
    "@types/webpack-bundle-analyzer": "^4.6.0",
    "ajv": "^8.11.0",
    "chart.js": "^3.9.1",
    "chartjs-adapter-date-fns": "^3.0.0",
    "chartjs-adapter-moment": "^1.0.1",
    "chartjs-plugin-datalabels": "^2.2.0",
    "chartjs-plugin-piechart-outlabels": "^0.1.4",
    "cytoscape": "^3.0.0",
    "cytoscape-cola": "^2.5.1",
    "cytoscape-euler": "^1.2.2",
    "date-fns": "^2.30.0",
    "dotenv-webpack": "^8.0.1",
    "export-from-json": "^1.7.3",
    "graphology-types": "^0.24.7",
    "i18next": "23.2.3",
    "i18next-browser-languagedetector": "7.1.0",
    "i18next-http-backend": "^2.2.1",
    "moment": "^2.29.4",
    "popper.js": "^1.16.1",
    "react": "^18.2.0",
    "react-bootstrap": "^2.8.0",
    "react-chartjs-2": "^4.3.1",
    "react-cytoscapejs": "^2.0.0",
    "react-dom": "^18.2.0",
    "react-i18next": "13.0.1",
    "react-json-pretty": "^2.2.0",
    "react-json-view": "^1.21.3",
    "react-redux": "^8.1.1",
    "react-router-dom": "6.14.0",
    "react-scripts": "^5.0.1",
    "redux": "^4.2.1",
    "styled-components": "^6.0.7",
    "tippy.js": "^6.2.5",
    "uuid": "^9.0.0",
    "web-vitals": "3.3.2"
  },
  "scripts": {
    "test_debug": "webpack serve --open --config webpack.test_debug.ts",
    "test_release": "webpack --config webpack.test_release.ts",
    "preprod_debug": "webpack serve --open --config webpack.preprod_debug.ts",
    "preprod_release": "webpack --config webpack.preprod_release.ts",
    "prod_debug": "webpack --config webpack.prod_debug.ts",
    "prod_release": "webpack --config webpack.prod_release.ts",
    "development": "webpack serve --open --config webpack.development.ts",
    "production": "webpack --config webpack.production.ts",
    "lint": "eslint src/**/*.{ts,tsx}",
    "lint:fix": "eslint --fix 'src/**/*.{ts,tsx}'",
    "format": "prettier --write 'src/**/*.{ts,tsx,css}'",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/cytoscape-euler": "^1.2.1",
    "@types/cytoscape-popper": "^2.0.1",
    "@types/lodash": "^4.14.196",
    "@types/node": "^18.15.11",
    "@types/react-d3-graph": "^2.6.4",
    "@types/uuid": "^9.0.1",
    "@types/webpack": "^5.28.1",
    "@typescript-eslint/eslint-plugin": "^5.59.7",
    "@typescript-eslint/parser": "^5.59.7",
    "autoprefixer": "^10.4.14",
    "bootstrap": "^5.2.2",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^11.0.0",
    "css-loader": "^6.8.1",
    "css-minimizer-webpack-plugin": "^5.0.0",
    "eslint": "^8.41.0",
    "eslint-config-prettier": "^8.8.0",
    "eslint-config-standard-with-typescript": "^34.0.1",
    "eslint-import-resolver-typescript": "^3.5.5",
    "eslint-plugin-import": "^2.27.5",
    "eslint-plugin-n": "^15.7.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-promise": "^6.1.1",
    "eslint-plugin-react": "^7.32.2",
    "html-webpack-plugin": "^5.5.1",
    "node-sass": "^9.0.0",
    "postcss": "^8.4.23",
    "postcss-loader": "^7.3.3",
    "prettier": "^2.8.8",
    "sass-loader": "^13.3.2",
    "style-loader": "^3.3.2",
    "ts-loader": "^9.4.2",
    "ts-node": "^10.9.1",
    "typescript": "^5.0.4",
    "webpack": "^5.81.0",
    "webpack-bundle-analyzer": "^4.8.0",
    "webpack-cli": "^5.0.2",
    "webpack-dev-server": "^4.13.3",
    "webpack-merge": "^5.8.0"
  },
  "overrides": {
    "@svgr/webpack": "^8.1.0"
  },
  "volta": {
    "node": "18.17.1"
  }
}

Source code:


import { useParams } from "react-router-dom";
import React, { FC, useCallback } from "react";
import {
  CollectionReturnValue,
  Core,
  EventObject,
  LayoutOptions,
  NodeSingular,
  Stylesheet,
  use,
} from "cytoscape";
import styled from "styled-components";
import CytoscapeComponent from "react-cytoscapejs";
import euler from "cytoscape-euler";

import { useGenerateGraph } from "../../hooks/use-generate-graph/useGenerateGraph";
import user from "../../assets/userId.svg";
import device from "../../assets/deviceId.svg";
import ip from "../../assets/ip.svg";
import deviceHash from "../../assets/deviceHash.svg";
import app from "../../assets/applicationId.svg";
import { Error } from "../error/Error";
import { Spinner } from "../ui/spinner/Spinner";
use(euler);

const CytoscapeComponentStyled = styled.div`
  width: 800px;
  height: 600px;
  border: 2px solid #ccc;
  border-radius: 5px;
`;
export const layouts = {
  random: {
    name: "random",
    animate: true,
    randomize: false,
  },
  grid: {
    name: "grid",
    animate: true,
    randomize: false,
  },
  circle: {
    name: "circle",
    animate: true,
    randomize: false,
  },
  breadthfirst: {
    name: "breadthfirst",
    animate: true,
    randomize: false,
  },
  klay: {
    name: "klay",
    animate: true,
    randomize: false,
    padding: 4,
    nodeDimensionsIncludeLabels: true,
    klay: {
      spacing: 40,
      mergeEdges: false,
    },
  },
  fcose: {
    name: "fcose",
    animate: true,
    randomize: false,
  },
  cose: {
    name: "cose",
    animate: true,
    randomize: false,
  },

  cola: {
    name: "cola",
    animate: true,
    randomize: false,
  },
  dagre: {
    name: "dagre",
    animate: true,
    randomize: false,
  },
  euler: {
    name: "euler",
  },
};

export const Graph: FC = () => {
  const { user_id: userId } = useParams();
  const nodeType: "user" | "device" | "application" | "ip" | "subnet" = "user";
  const {
    elements,
    status,
    error: { statusCode, statusMessage, statusText },
  } = useGenerateGraph(nodeType, userId as string, 3, ["subnet"]);

  const stylesheet = [
    {
      selector: "node",
      style: {
        // label: "data(title)", // Отключаем текстовую надпись
        "text-max-width": "50px", // Максимальная ширина текста
        "text-margin-x": "-60px", // Внешний отступ по горизонтали
        "text-margin-y": "-25px", // Внешний отступ по горизонтали
        "background-color": "#e5e1e1",
        color: "#000000",
        "text-valign": "center",
        "overlay-padding": "6px",
        "text-outline-color": "#ffffff",
        "text-outline-width": "2px",
        fontSize: 13,
        "border-width": "1px",
      },
    },
    {
      selector: "edge",
      style: {
        width: 1,
        "line-color": "rgba(199,198,198,0.56)",
        "curve-style": "bezier",
      },
    },
    {
      selector: `node[id="${sessionStorage.getItem(
        "team",
      )}:${nodeType}:${userId}"]`,
      style: {
        width: 50,
        height: 50,
        "background-image": `url(${user})`,
        "background-repeat": "no-repeat",
      },
    },
    {
      selector: `node[label="user"]`,
      style: {
        "background-image": `url(${user})`,
        "background-repeat": "no-repeat",
      },
    },
    {
      selector: 'node[label="device"]',
      style: {
        "background-image": `url(${device})`,
        "background-color": "rgb(227,172,172)",
        "background-repeat": "no-repeat",
      },
    },
    {
      selector: 'node[label="application"]',
      style: {
        "background-image": `url(${app})`,
        "background-color": "rgb(161,188,225)",
        "background-repeat": "no-repeat",
      },
    },
    {
      selector: 'node[label="device_hash"]',
      style: {
        "background-image": `url(${deviceHash})`,
        "background-color": "rgb(148,211,158)",
        "background-repeat": "no-repeat",
      },
    },
    {
      selector: 'node[label="ip"]',
      style: {
        "background-image": `url(${ip})`,
        "background-color": "rgb(203,177,215)",
        "background-repeat": "no-repeat",
      },
    },
    {
      selector: ".highlighted",
      style: {
        "background-color": "green",
        "line-color": "green",
        "target-arrow-color": "green",
      },
    },
  ] as Stylesheet[];

  const handleNodeMouseOver = useCallback((event: EventObject) => {
    const node: NodeSingular = event.target as NodeSingular;
    const neighbors: CollectionReturnValue = node.neighborhood();
    const divGraphTooltip = document.getElementById("graphTooltip");
    if (divGraphTooltip) {
      divGraphTooltip.style.opacity = "1";
      divGraphTooltip.style.padding = "0.5rem";
      divGraphTooltip.style.cursor = "pointer";
      //divGraphTooltip.style.transform = "translate(-50%, 75%)"
      divGraphTooltip.style.position = "absolute";
      divGraphTooltip.style.zIndex = "9999";
      divGraphTooltip.innerText = node.data("title");
      divGraphTooltip.style.background = "rgba(0,0,0,0.7)";
      divGraphTooltip.style.borderRadius = "3px";
      divGraphTooltip.style.color = "#fff";
      divGraphTooltip.style.transition = "all .1s ease";

      const graphCanvasPosition = document.getElementById(
        "graphCanvasPosition",
      );
      graphCanvasPosition?.addEventListener("mousemove", (e) => {
        divGraphTooltip.style.left = e.x + "px";
        divGraphTooltip.style.top = e.y + 100 + "px";
      });
    }
    neighbors.edges().style("line-color", "#3dbe20");
    neighbors.edges().style("width", 4);
  }, []);

  const handleNodeMouseOut = useCallback((event: EventObject) => {
    const node: NodeSingular = event.target as NodeSingular;
    const divGraphTooltip: HTMLElement | null =
      document.getElementById("graphTooltip");
    if (divGraphTooltip) divGraphTooltip.style.opacity = "0";
    const neighbors = node.neighborhood();

    neighbors.edges().style("line-color", "rgba(199,198,198,0.56)");
    neighbors.edges().style("width", 1);
  }, []);

  const layouts = {
    name: "euler",
    animate: false,
    // refresh: 10,
    gravity: -3,
    dragCoeff: 0.02,
    padding: 25,
    numIter: 100,
    // minTemp: 1.0,
    randomize: true,
   /* fit: true,*/
    /*componentSpacing: 100,
    coolingFactor: 0.99,
    fit: true,
    timeStep: 2,*/
    /*   initialTemp: 1000,
    minTemp: 1.0,
    nestingFactor: 1.2,
    ungrabifyWhileSimulating: true,
    nodeDimensionsIncludeLabels: false,
    nodeOverlap: 4,
    numIter: 200,
    ,*/
    // springCoeff(edge: any): number {
    //   return 0.0008;
    // },
    // springLength(edge: any): number {
    //   return 80;
    // },
    // mass(node: any): number {
    //   return 8;
    // },
  } as LayoutOptions

  return (
    <>
      {status === "error" && (
        <Error
          statusCode={statusCode}
          statusText={statusText}
          statusMessage={statusMessage}
        />
      )}
      {status === "loading" && (
        <CytoscapeComponentStyled>
          <Spinner />
        </CytoscapeComponentStyled>
      )}
      {status === "success" && (
        <CytoscapeComponentStyled>
          <CytoscapeComponent
            id="graphCanvasPosition"
            elements={elements}
            stylesheet={stylesheet}
            minZoom={0.25}
            maxZoom={5}
            layout={layouts}
            style={{
              width: "100%",
              height: "100%",
              backgroundColor: "#f1f4fa",
              margin: "0",
              padding: "0",
            }}
            cy={(cy: Core) => {
              cy.on("mouseover", "node", handleNodeMouseOver);
              cy.on("mouseout", "node", handleNodeMouseOut);
              return () => {
                cy.off("mouseover", "node", handleNodeMouseOver);
                cy.off("mouseout", "node", handleNodeMouseOut);
              };
            }}
          />
          <div id="graphTooltip" />
        </CytoscapeComponentStyled>
      )}
    </>
  );
};