react-cytoscapejs icon indicating copy to clipboard operation
react-cytoscapejs copied to clipboard

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

Open DaniilRyb opened this issue 2 years ago • 0 comments

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>
      )}
    </>
  );
};

DaniilRyb avatar Oct 02 '23 09:10 DaniilRyb