react-force-graph icon indicating copy to clipboard operation
react-force-graph copied to clipboard

Question about Responsive Width

Open s2t2 opened this issue 4 years ago • 14 comments

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 avatar Oct 30 '20 17:10 s2t2

@s2t2 you can just use a 3rd party lib to make any sizeable chart responsive, for example: react-sizeme.

vasturiano avatar Nov 03 '20 05:11 vasturiano

@s2t2 this solved it for me - can we close the issue?

JonThom avatar Dec 02 '20 07:12 JonThom

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 .

s2t2 avatar Dec 02 '20 14:12 s2t2

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)

JonThom avatar Dec 02 '20 19:12 JonThom

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

korki43 avatar Dec 04 '20 13:12 korki43

Edited my comment above since I had omitted passing width={props.size.width} to the ForceGraph2D component

JonThom avatar Dec 04 '20 15:12 JonThom

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:

Screen Shot 2020-12-10 at 1 57 38 PM

Less Wide:

Screen Shot 2020-12-10 at 1 57 45 PM

Least Wide (Stacked due to TWBS):

Screen Shot 2020-12-10 at 1 57 55 PM

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

s2t2 avatar Dec 10 '20 18:12 s2t2

Looks good, thanks for sharing!

JonThom avatar Dec 11 '20 18:12 JonThom

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}
               .... /> 
}

amney avatar Jul 13 '21 18:07 amney

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 :

tefkah avatar Jul 21 '21 09:07 tefkah

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 avatar Nov 01 '23 22:11 THEaustinlopez

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

}

chip-davis avatar Jan 19 '24 23:01 chip-davis

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?

fullstackwebdev avatar Apr 23 '24 22:04 fullstackwebdev

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?

chip-davis avatar Apr 24 '24 02:04 chip-davis