Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
DaniilRyb opened this issue Oct 2, 2023 · 0 comments
Open

Comments

@DaniilRyb
Copy link

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>
      )}
    </>
  );
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant