react-force-graph
react-force-graph copied to clipboard
Question about Responsive Width
Hi, I see there is the ability to customize the height and width properties of the graph container, but I was looking for some guidance on how to make the graph have a responsive width. I tried setting width="100%"
but that didn't seem to work.
@s2t2 you can just use a 3rd party lib to make any sizeable chart responsive, for example: react-sizeme.
@s2t2 this solved it for me - can we close the issue?
Care to post an example code?
When I tried putting it inside a responsive container, after resizing the window, the nodes and links started malfunctioning.
On Wed, Dec 2, 2020, 2:18 AM JonThom [email protected] wrote:
@s2t2 https://github.com/s2t2 this solved it for me - can we close the issue?
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/vasturiano/react-force-graph/issues/233#issuecomment-737044248, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAKENJ5D2JX65XQ3VEK7DULSSXS4XANCNFSM4TFG3FXQ .
Hi, Here is an example using the higher-order component setup. Note that only the width resizing worked for me. Edit: in the below example, I have wrapped ForceGraph2D in a higher-order component Graph. The resizeme module continuously passes a 'size' prop to the Graph component, which includes width and height, which can be passed to teh ForceGraph2D component, as below. As I said only width worked for me. If you can't get it to work, give a shout and I'll try to set up a proper working example.
import { withSize } from 'react-sizeme';
const withSizeHOC = withSize({monitorWidth:true, monitorHeight:false, noPlaceholder:true})
// example data
const graphData = {nodes:[{"id":"1","__label":"Julia"},{"id":"2","__label":"Bob"}],links:[{"id":"link1","source":"1", "target":"2"}]}
function Graph(props) {
return <ForceGraph2D
graphData={graphData}
width={props.size.width}
/>;
};
export default withSizeHOC(Graph)
I stored the width and height of the window in state and changed it on resize.
const [displayWidth, setDisplayWidth] = useState(window.innerWidth);
const [displayHeight, setDisplayHeight] = useState(window.innerHeight);
window.addEventListener('resize', () => {
setDisplayWidth(window.innerWidth);
setDisplayHeight(window.innerHeight);
});
return (
<ForceGraph3D
width={displayWidth}
height={displayHeight}
...
/>
)
Edited my comment above since I had omitted passing width={props.size.width}
to the ForceGraph2D component
Thanks @JonThom your react-sizeme example works - you're amazing! Thanks also @korki43 - your code does get the window sizes.
FYI: here is the full example I'm using, in case you want to add to docs, or for anyone googling later. I happen to be placing two different graphs inside a twitter bootstrap responsive-resizing container row (see images at bottom).
NOTE: in real life we would probably also pass custom data via props to the child graph, but for example I just used the same data for each graph.
// ExampleSizemeGraph.js
import React, { PureComponent, createRef} from 'react'
import { ForceGraph2D } from 'react-force-graph'
import { withSize } from 'react-sizeme'
var exampleData = {
nodes:[
{"id":"node1", "__label":"Julia"},
{"id":"node2", "__label":"Bob"},
],
links:[
{"id":"link1", "source":"node1", "target":"node2"}
]
}
const withSizeHOC = withSize({monitorWidth:true, monitorHeight:false, noPlaceholder:true})
class ExampleGraph extends PureComponent {
constructor(props) {
super(props)
this.state = {}
this.containerRef = createRef()
}
render() {
var width = this.props.size.width
return (
<ForceGraph2D
ref={this.containerRef}
width={width}
height={400}
backgroundColor="#101020"
graphData={exampleData}
nodeId="id"
nodeVal={5}
nodeColor={(node) => this.props.nodeColor }
nodeLabel={(node) => node["__label"] }
nodeRelSize={1}
linkColor={() => 'rgba(255,255,255,0.2)'}
linkDirectionalParticles={2}
linkDirectionalParticleWidth={2}
d3VelocityDecay={0.6}
/>
)
}
}
export default withSizeHOC(ExampleGraph)
// A Parent Component
import React, {useState} from 'react'
import Container from 'react-bootstrap/Container'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
import Card from 'react-bootstrap/Card'
import ExampleGraph from "./ExampleSizemeGraph"
export default function MyParentComponent() {
return (
<Container fluid>
<Card>
<Card.Body>
<Card.Title><h3>Example Networks</h3></Card.Title>
<Row>
<Col sm={12} md={12} lg={6}>
<Card>
<Card.Body>
<Card.Text className="app-center">Network 0</Card.Text>
<ExampleGraph key="network-0" nodeColor="blue"}/>
</Card.Body>
</Card>
</Col>
<Col sm={12} md={12} lg={6}>
<Card>
<Card.Body>
<Card.Text className="app-center">Network 1</Card.Text>
<ExampleGraph key="network-1" nodeColor="red"/>
</Card.Body>
</Card>
</Col>
</Row>
</Card.Body>
</Card>
</Container>
)
}
Widest:

Less Wide:

Least Wide (Stacked due to TWBS):

EDIT: interestingly, if you click and drag a node to rotate one of the graphs above, the other graph will also rotate. but if each graph has different data (I think it is the links that need to be different) then that behavior won't happen anymore
Looks good, thanks for sharing!
Thank you to all the above posters, I came to a similar solution via hooks, which I'll share in case it's useful to others:
Using the package https://www.npmjs.com/package/@react-hook/window-size
import { useWindowSize } from '@react-hook/window-size';
const Container = () => {
const [width, height] = useWindowSize();
return <ForceGraph2D
width={width}
height={height}
.... />
}
I came to a similar solution via hooks, which I'll share in case it's useful to others:
Thank you! I couldn't get the other solution to work. but yours worked wonderfully :
Is there a potential solution for dynamic width AND height? Also, instead of tracking the whole window what if it's placed inside a MUI Card for example?
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react'
import { ForceGraph2D } from 'react-force-graph'
const NODE_R = 8
type Node = {
id: string;
name: string;
val: number;
neighbors?: Node[];
links?: Link[];
x?: number;
y?: number;
}
type Link = {
source: string | Node
target: string | Node
}
function genRandomTree(N = 300, reverse = false) {
return {
nodes: [...Array(N).keys()].map((i) => ({
id: `id${i}`,
name: `name${i}`,
val: Math.floor(Math.random() * 20) + 1,
})),
links: [...Array(N).keys()]
.filter((id) => id)
.map((id) => ({
[reverse ? 'target' : 'source']: `id${id}`,
[reverse ? 'source' : 'target']: `id${Math.round(Math.random() * (id - 1))}`,
})),
}
}
const Test2DGraph: React.FC = () => {
const data = useMemo(() => {
const gData: { nodes: Node[]; links: Link[] } = genRandomTree(80) as { nodes: Node[]; links: Link[] }
const nodeById: { [id: string]: Node } = {}
gData.nodes.forEach((node) => {
nodeById[node.id] = node
})
gData.links.forEach((link) => {
const a = nodeById[link.source as string]
const b = nodeById[link.target as string]
!a.neighbors && (a.neighbors = [])
!b.neighbors && (b.neighbors = [])
a.neighbors.push(b)
b.neighbors.push(a)
!a.links && (a.links = [])
!b.links && (b.links = [])
a.links.push(link)
b.links.push(link)
})
return gData
}, [])
const [highlightNodes, setHighlightNodes] = React.useState(new Set())
const [highlightLinks, setHighlightLinks] = React.useState(new Set())
const [hoverNode, setHoverNode] = React.useState(null)
const updateHighlight = () => {
setHighlightNodes(highlightNodes)
setHighlightLinks(highlightLinks)
}
const handleNodeHover = (node) => {
highlightNodes.clear()
highlightLinks.clear()
if (node) {
highlightNodes.add(node)
node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor))
node.links.forEach((link) => highlightLinks.add(link))
}
setHoverNode(node || null)
updateHighlight()
}
const handleLinkHover = (link) => {
highlightNodes.clear()
highlightLinks.clear()
if (link) {
highlightLinks.add(link)
highlightNodes.add(link.source)
highlightNodes.add(link.target)
}
updateHighlight()
}
const paintRing = React.useCallback(
(node, ctx) => {
ctx.beginPath()
ctx.arc(node.x, node.y, NODE_R * 1.4, 0, 2 * Math.PI, false)
ctx.fillStyle = node === hoverNode ? 'red' : 'orange'
ctx.fill()
},
[hoverNode]
)
const forceRef = useRef(null)
useEffect(() => {
if (forceRef.current) {
forceRef.current.d3Force('charge').strength(-400)
}
}, [])
return (
<ForceGraph2D
ref={forceRef}
graphData={data}
nodeRelSize={NODE_R}
autoPauseRedraw={false}
linkWidth={(link) => (highlightLinks.has(link) ? 5 : 1)}
linkDirectionalParticles={4}
linkDirectionalParticleWidth={(link) => (highlightLinks.has(link) ? 4 : 0)}
nodeCanvasObjectMode={(node) => (highlightNodes.has(node) ? 'before' : undefined)}
nodeCanvasObject={paintRing}
onNodeHover={handleNodeHover}
onLinkHover={handleLinkHover}
/>
)
}
export default Test2DGraph
this is just one of the examples from the docs with a few tweaks to make it work in a functional component but this is then placed inside a card like this:
<CustomCard
cardSx={{ margin: '0 0 25px 5px', height: 'auto' }}
title="Node Graph"
subheading="Find connections"
actionComponent={
<Tabs value={tab} onChange={handleChange} centered aria-label="node tabs">
<Tab label="Nexus" />
<Tab label="Data" />
</Tabs>
}
>
{tab === 0 && <Test2DGraph />}
{tab === 1 && <DataTableTab />}
</CustomCard>
How would I constrain the graph to stay within the card body?
Is there a potential solution for dynamic width AND height? Also, instead of tracking the whole window what if it's placed inside a MUI Card for example?
import React, { useState, useMemo, useCallback, useRef, useEffect } from 'react' import { ForceGraph2D } from 'react-force-graph' const NODE_R = 8 type Node = { id: string; name: string; val: number; neighbors?: Node[]; links?: Link[]; x?: number; y?: number; } type Link = { source: string | Node target: string | Node } function genRandomTree(N = 300, reverse = false) { return { nodes: [...Array(N).keys()].map((i) => ({ id: `id${i}`, name: `name${i}`, val: Math.floor(Math.random() * 20) + 1, })), links: [...Array(N).keys()] .filter((id) => id) .map((id) => ({ [reverse ? 'target' : 'source']: `id${id}`, [reverse ? 'source' : 'target']: `id${Math.round(Math.random() * (id - 1))}`, })), } } const Test2DGraph: React.FC = () => { const data = useMemo(() => { const gData: { nodes: Node[]; links: Link[] } = genRandomTree(80) as { nodes: Node[]; links: Link[] } const nodeById: { [id: string]: Node } = {} gData.nodes.forEach((node) => { nodeById[node.id] = node }) gData.links.forEach((link) => { const a = nodeById[link.source as string] const b = nodeById[link.target as string] !a.neighbors && (a.neighbors = []) !b.neighbors && (b.neighbors = []) a.neighbors.push(b) b.neighbors.push(a) !a.links && (a.links = []) !b.links && (b.links = []) a.links.push(link) b.links.push(link) }) return gData }, []) const [highlightNodes, setHighlightNodes] = React.useState(new Set()) const [highlightLinks, setHighlightLinks] = React.useState(new Set()) const [hoverNode, setHoverNode] = React.useState(null) const updateHighlight = () => { setHighlightNodes(highlightNodes) setHighlightLinks(highlightLinks) } const handleNodeHover = (node) => { highlightNodes.clear() highlightLinks.clear() if (node) { highlightNodes.add(node) node.neighbors.forEach((neighbor) => highlightNodes.add(neighbor)) node.links.forEach((link) => highlightLinks.add(link)) } setHoverNode(node || null) updateHighlight() } const handleLinkHover = (link) => { highlightNodes.clear() highlightLinks.clear() if (link) { highlightLinks.add(link) highlightNodes.add(link.source) highlightNodes.add(link.target) } updateHighlight() } const paintRing = React.useCallback( (node, ctx) => { ctx.beginPath() ctx.arc(node.x, node.y, NODE_R * 1.4, 0, 2 * Math.PI, false) ctx.fillStyle = node === hoverNode ? 'red' : 'orange' ctx.fill() }, [hoverNode] ) const forceRef = useRef(null) useEffect(() => { if (forceRef.current) { forceRef.current.d3Force('charge').strength(-400) } }, []) return ( <ForceGraph2D ref={forceRef} graphData={data} nodeRelSize={NODE_R} autoPauseRedraw={false} linkWidth={(link) => (highlightLinks.has(link) ? 5 : 1)} linkDirectionalParticles={4} linkDirectionalParticleWidth={(link) => (highlightLinks.has(link) ? 4 : 0)} nodeCanvasObjectMode={(node) => (highlightNodes.has(node) ? 'before' : undefined)} nodeCanvasObject={paintRing} onNodeHover={handleNodeHover} onLinkHover={handleLinkHover} /> ) } export default Test2DGraph
this is just one of the examples from the docs with a few tweaks to make it work in a functional component but this is then placed inside a card like this:
<CustomCard cardSx={{ margin: '0 0 25px 5px', height: 'auto' }} title="Node Graph" subheading="Find connections" actionComponent={ <Tabs value={tab} onChange={handleChange} centered aria-label="node tabs"> <Tab label="Nexus" /> <Tab label="Data" /> </Tabs> } > {tab === 0 && <Test2DGraph />} {tab === 1 && <DataTableTab />} </CustomCard>
How would I constrain the graph to stay within the card body?
@THEaustinlopez Did you find a good solution for this?
EDIT for future people. I have my graph in a MUI Paper component and was able to use React Sizeme to get it all to work. This allows for canvas resizing when the window changes and allows the zoomToFit function to work well. Here is some relevant code:
import { SizeMe } from "react-sizeme";
export function MyGraph() {
return (
<>
<SizeMe monitorWidth monitorHeight>
{({ size }) => (
<Paper className="neoPaper" elevation={12} sx={{ minHeight: "80em" }}>
<GraphBuilder
graphData={myGraphData}
minZoom={1}
width={size.width}
height={size.height}
/>
</Paper>
)}
</SizeMe>
</>
}
export function GraphBuilder(graphData, width, height) {
const fgRef = useRef<any>(); (could never get an actual type to work with this)
return (
<ForceGraph2D
graphData={graphData}
ref={fgRef}
onNodeDragEnd={(node) => {
node.fx = node.x;
node.fy = node.y;
node.fz = node.z;
}}
width={width}
height={height}
minZoom={minZoom}
onNodeHover={() => {}}
cooldownTicks={60}
onEngineStop={() => {
fgRef.current.zoomToFit(1000, 110);
}}
/>
)
}
Well, I tried all of these, and I can't get it to work.
Is there a way to just assign an ID to the canvas so I can manipulate it with just vanilla javascript?
Well, I tried all of these, and I can't get it to work.
Is there a way to just assign an ID to the canvas so I can manipulate it with just vanilla javascript?
Can you show your code?