'create second thread (web-worker) to run complex function of force layout graph outside of main thread in react

Hi everyone I created a network graph using Pixi and d3 and my graph is to slow for that I have to work complex functions in the second thread

here is App.js

import './App.css';


 import { ForceGraph } from './components/forceGraph';
    import data from './data/data.json';

import React from 'react';


function App() {
  const nodeHoverTooltip = React.useCallback((node) => {
    return `<div>     
      <b>${node.name}</b>
    </div>`;
  }, []);

  return (
    <div className="App">
      <ForceGraph
        linksData={data.links}
        nodesData={data.nodes}
        nodeHoverTooltip={nodeHoverTooltip}
      />
    </div>
  );
}

export default App;

here is forceGraph.js

import React from 'react';

import { runForceGraphPixi } from './forceGraphGeneratorPixi';
import styles from './forceGraph.module.css';

export function ForceGraph({ linksData, nodesData, nodeHoverTooltip }) {
  const containerRef = React.useRef(null);

  React.useEffect(() => {
    if (containerRef.current) {
      runForceGraphPixi(
        containerRef.current,
        linksData,
        nodesData,
        nodeHoverTooltip
      );
    }
  }, [linksData, nodesData, nodeHoverTooltip]);

  return <div ref={containerRef} className={styles.container} />;
}

here is forceGraphGeneratorPixi.js

import * as d3 from 'd3';
import * as PIXI from 'pixi.js';
import { Viewport } from 'pixi-viewport';
import styles from './forceGraph.module.css';

export function runForceGraphPixi(
  container,
  linksData,
  nodesData,
  nodeHoverTooltip
) {
  const links = linksData.map((d) => Object.assign({}, d));
  const nodes = nodesData.map((d) => Object.assign({}, d));

  const containerRect = container.getBoundingClientRect();
  const height = containerRect.height;
  const width = containerRect.width;
  let dragged = false;

  container.innerHTML = '';

  const color = () => {
    return '#f0f8ff';
  };


 

  // Add the tooltip element to the graph
  const tooltip = document.querySelector('#graph-tooltip');
  if (!tooltip) {
    const tooltipDiv = document.createElement('div');
    tooltipDiv.classList.add(styles.tooltip);
    tooltipDiv.style.opacity = '0';
    tooltipDiv.id = 'graph-tooltip';
    document.body.appendChild(tooltipDiv);
  }
  const div = d3.select('#graph-tooltip');

  const addTooltip = (hoverTooltip, d, x, y) => {
    div.transition().duration(200).style('opacity', 0.9);
    div
      .html(hoverTooltip(d))
      .style('left', `${x}px`)
      .style('top', `${y - 28}px`);
  };

  const removeTooltip = () => {
    div.transition().duration(200).style('opacity', 0);
  };

  const colorScale = (num) => parseInt(color().slice(1), 16);

  const app = new PIXI.Application({
    width,
    height,
    antialias: !0,
    transparent: !0,
    resolution: 1,
  });
  container.appendChild(app.view);

  // create viewport
  const viewport = new Viewport({
    screenWidth: width,
    screenHeight: height,
    worldWidth: width * 4,
    worldHeight: height * 4,
    passiveWheel: false,

    interaction: app.renderer.plugins.interaction, // the interaction module is important for wheel to work properly when renderer.view is placed or scaled
  });

  app.stage.addChild(viewport);

  // activate plugins
  viewport
    .drag()
    .pinch()
    .wheel()
    .decelerate()
    .clampZoom({ minWidth: width / 4, minHeight: height / 4 });

  const simulation = d3
    .forceSimulation(nodes)
    .force(
      'link',
      d3
        .forceLink(links) // This force provides links between nodes
        .id((d) => d.id) // This sets the node id accessor to the specified function. If not specified, will default to the index of a node.
        .distance(50)
    )
    .force('charge', d3.forceManyBody().strength(-500)) // This adds repulsion (if it's negative) between nodes.
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force(
      'collision',
      d3
        .forceCollide()
        .radius((d) => d.radius)
        .iterations(2)
    )
    .velocityDecay(0.8);

  /*
   Implementation
   */

  let visualLinks = new PIXI.Graphics();
  viewport.addChild(visualLinks);

  nodes.forEach((node) => {
    const { name, gender } = node;
    node.gfx = new PIXI.Graphics();
    node.gfx.lineStyle(1, 0xd3d3d3);
    node.gfx.beginFill(colorScale(node.id));
    node.gfx.drawCircle(0, 0, 24);
    node.gfx.endFill();
    node.gfx
      // events for click
      .on('click', (e) => {
        if (!dragged) {
          e.stopPropagation();
        }
        dragged = false;
      });

    viewport.addChild(node.gfx);

    node.gfx.interactive = true;
    node.gfx.buttonMode = true;

    // create hit area, needed for interactivity
    node.gfx.hitArea = new PIXI.Circle(0, 0, 24);

    // show tooltip when mouse is over node
    node.gfx.on('mouseover', (mouseData) => {
      addTooltip(
        nodeHoverTooltip,
        { name },
        mouseData.data.originalEvent.pageX,
        mouseData.data.originalEvent.pageY
      );
    });

    // make circle half-transparent when mouse leaves
    node.gfx.on('mouseout', () => {
      removeTooltip();
    });

    const text = new PIXI.Text(name, {
      fontSize: 12,
      fill: '#000',
    });
    text.anchor.set(0.5);
    text.resolution = 2;
    node.gfx.addChild(text);
  });

  const ticked = () => {
    nodes.forEach((node) => {
      let { x, y, gfx } = node;
      gfx.position = new PIXI.Point(x, y);
    });

    for (let i = visualLinks.children.length - 1; i >= 0; i--) {
      visualLinks.children[i].destroy();
    }

    visualLinks.clear();
    visualLinks.removeChildren();
    visualLinks.alpha = 1;

    links.forEach((link) => {
      let { source, target, number } = link;
      visualLinks.lineStyle(2, 0xd3d3d3);
      visualLinks.moveTo(source.x, source.y);
      visualLinks.lineTo(target.x, target.y);
    });

    visualLinks.endFill();
  };

  // Listen for tick events to render the nodes as they update in your Canvas or SVG.
  simulation.on('tick', ticked);

  return {
    destroy: () => {
      nodes.forEach((node) => {
        node.gfx.clear();
      });
      visualLinks.clear();
    },
  };
}

here is forceGraph.module.css

.container {
  width: 100%;
  height: 80vh;
  position: relative;
  background-color: white;
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.6);
}

.container svg {
  display: block;
  width: 100%;
  height: 100%;
}

.male {
  fill: rgb(22, 130, 218);
}

.female {
  fill: rgb(246, 0, 0);
}

.node text {
  font: 12px sans-serif;
}

div.tooltip {
  position: absolute;
  text-align: center;
  width: 110px;
  padding: 10px;
  font: 12px sans-serif;
  background: lightsteelblue;
  border: 0;
  border-radius: 8px;
  pointer-events: none;
}

.contextMenu {
  stroke: #00557d;
  fill: #ffffff;
}

.menuEntry {
  cursor: pointer;
}

.menuEntry text {
  font-size: 12px;
  stroke: #00557d;
}

here is my data.json

{
  "nodes": [
    {
      "id": 1,
      "name": "Andy",
      "gender": "male"
    },
    {
      "id": 2,
      "name": "Betty",
      "gender": "female"
    },
    {
      "id": 3,
      "name": "Cate",
      "gender": "female"
    },
    {
      "id": 4,
      "name": "Dave",
      "gender": "male"
    },
    {
      "id": 5,
      "name": "Ellen",
      "gender": "female"
    },
    {
      "id": 6,
      "name": "Fiona",
      "gender": "female"
    },
    {
      "id": 7,
      "name": "Garry",
      "gender": "male"
    },
    {
      "id": 8,
      "name": "Holly",
      "gender": "female"
    },
    {
      "id": 9,
      "name": "Iris",
      "gender": "female"
    },
    {
      "id": 10,
      "name": "Jane",
      "gender": "female"
    }
  ],
  "links": [
    {
      "source": 1,
      "target": 2
    },
    {
      "source": 1,
      "target": 5
    },
    {
      "source": 1,
      "target": 6
    },

    {
      "source": 2,
      "target": 3
    },
    {
      "source": 2,
      "target": 7
    }
  ,

    {
      "source": 3,
      "target": 4
    },
    {
      "source": 8,
      "target": 3
    }
  ,
    {
      "source": 4,
      "target": 5
    }
  ,

    {
      "source": 4,
      "target": 9
    },
    {
      "source": 5,
      "target": 10
    }
  ]
}

After this I'm getting result click in link to check result

After this i add worker.js,and while I'm importing any other library inside worker.js I'm getting some error but I fixed that one also and the code is below

deep-thought.js(worker)

import addition from './addition';
import * as d3 from 'd3';

/* eslint-disable no-restricted-globals */
self.onmessage = (question) => {
  const reciveData = question.data;
  const nodes = question.data.nodes;
  const links = question.data.links;
  console.log(nodes);
  console.log(links);
  console.log(reciveData);

  const result = addition(42);

  self.postMessage({
    result: result,
  });
};

updated forceGraphGeneratorPixi.js

import * as d3 from 'd3';
import * as PIXI from 'pixi.js';
import { Viewport } from 'pixi-viewport';
import styles from './forceGraph.module.css';

export function runForceGraphPixi(
  container,
  linksData,
  nodesData,
  nodeHoverTooltip
) {
  const links = linksData.map((d) => Object.assign({}, d));
  const nodes = nodesData.map((d) => Object.assign({}, d));

  const containerRect = container.getBoundingClientRect();
  const height = containerRect.height;
  const width = containerRect.width;
  let dragged = false;

  container.innerHTML = '';

  const color = () => {
    return '#f0f8ff';
  };

  // Pass data to web-worker
  const worker = new Worker(new URL('../deep-thought.js', import.meta.url));
  worker.postMessage({
    nodes: nodes,
    links: links,
  });
  worker.onmessage = ({ data: { answer } }) => {
    console.log(answer);
  };

  // Add the tooltip element to the graph
  const tooltip = document.querySelector('#graph-tooltip');
  if (!tooltip) {
    const tooltipDiv = document.createElement('div');
    tooltipDiv.classList.add(styles.tooltip);
    tooltipDiv.style.opacity = '0';
    tooltipDiv.id = 'graph-tooltip';
    document.body.appendChild(tooltipDiv);
  }
  const div = d3.select('#graph-tooltip');

  const addTooltip = (hoverTooltip, d, x, y) => {
    div.transition().duration(200).style('opacity', 0.9);
    div
      .html(hoverTooltip(d))
      .style('left', `${x}px`)
      .style('top', `${y - 28}px`);
  };

  const removeTooltip = () => {
    div.transition().duration(200).style('opacity', 0);
  };

  const colorScale = (num) => parseInt(color().slice(1), 16);

  const app = new PIXI.Application({
    width,
    height,
    antialias: !0,
    transparent: !0,
    resolution: 1,
  });
  container.appendChild(app.view);

  // create viewport
  const viewport = new Viewport({
    screenWidth: width,
    screenHeight: height,
    worldWidth: width * 4,
    worldHeight: height * 4,
    passiveWheel: false,

    interaction: app.renderer.plugins.interaction, // the interaction module is important for wheel to work properly when renderer.view is placed or scaled
  });

  app.stage.addChild(viewport);

  // activate plugins
  viewport
    .drag()
    .pinch()
    .wheel()
    .decelerate()
    .clampZoom({ minWidth: width / 4, minHeight: height / 4 });

  const simulation = d3
    .forceSimulation(nodes)
    .force(
      'link',
      d3
        .forceLink(links) // This force provides links between nodes
        .id((d) => d.id) // This sets the node id accessor to the specified function. If not specified, will default to the index of a node.
        .distance(50)
    )
    .force('charge', d3.forceManyBody().strength(-500)) // This adds repulsion (if it's negative) between nodes.
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force(
      'collision',
      d3
        .forceCollide()
        .radius((d) => d.radius)
        .iterations(2)
    )
    .velocityDecay(0.8);

  /*
   Implementation
   */

  let visualLinks = new PIXI.Graphics();
  viewport.addChild(visualLinks);

  nodes.forEach((node) => {
    const { name, gender } = node;
    node.gfx = new PIXI.Graphics();
    node.gfx.lineStyle(1, 0xd3d3d3);
    node.gfx.beginFill(colorScale(node.id));
    node.gfx.drawCircle(0, 0, 24);
    node.gfx.endFill();
    node.gfx
      // events for click
      .on('click', (e) => {
        if (!dragged) {
          e.stopPropagation();
        }
        dragged = false;
      });

    viewport.addChild(node.gfx);

    node.gfx.interactive = true;
    node.gfx.buttonMode = true;

    // create hit area, needed for interactivity
    node.gfx.hitArea = new PIXI.Circle(0, 0, 24);

    // show tooltip when mouse is over node
    node.gfx.on('mouseover', (mouseData) => {
      addTooltip(
        nodeHoverTooltip,
        { name },
        mouseData.data.originalEvent.pageX,
        mouseData.data.originalEvent.pageY
      );
    });

    // make circle half-transparent when mouse leaves
    node.gfx.on('mouseout', () => {
      removeTooltip();
    });

    const text = new PIXI.Text(name, {
      fontSize: 12,
      fill: '#000',
    });
    text.anchor.set(0.5);
    text.resolution = 2;
    node.gfx.addChild(text);
  });

  const ticked = () => {
    nodes.forEach((node) => {
      let { x, y, gfx } = node;
      gfx.position = new PIXI.Point(x, y);
    });

    for (let i = visualLinks.children.length - 1; i >= 0; i--) {
      visualLinks.children[i].destroy();
    }

    visualLinks.clear();
    visualLinks.removeChildren();
    visualLinks.alpha = 1;

    links.forEach((link) => {
      let { source, target, number } = link;
      visualLinks.lineStyle(2, 0xd3d3d3);
      visualLinks.moveTo(source.x, source.y);
      visualLinks.lineTo(target.x, target.y);
    });

    visualLinks.endFill();
  };

  // Listen for tick events to render the nodes as they update in your Canvas or SVG.
  simulation.on('tick', ticked);

  return {
    destroy: () => {
      nodes.forEach((node) => {
        node.gfx.clear();
      });
      visualLinks.clear();
    },
  };
}

addition.js(perform simple addition in web-worker)

const addition = (data) => {
      const result = data + 1;
      return result;
    };

export default addition;

so i perform simple addition using web-worker but now i want to perform simulation and tick function in webWorker and after performing i want to use it in my code to replace complex function because i'm new to web-worker i'm not figure-out how to do that



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source