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

