vasturiano / react-force-graph

React component for 2D, 3D, VR and AR force directed graphs
https://vasturiano.github.io/react-force-graph/example/large-graph/
MIT License
2.28k stars 288 forks source link

Load node data on hover #529

Open happylolonly opened 4 months ago

happylolonly commented 4 months ago

What is correct way to load node data on hover?

hyeonbeomsong commented 1 month ago

onNodeHover?: (node: NodeObject | null, previousNode: NodeObject | null) => void;

@happylolonly You can get the value of the node from the onNodeHover function.

In the case of react, <ForceGraph onNodeHover={(node) => console.log("hovering node", node) } /> Is this the right question for you?

Not-yourCoder commented 1 week ago

Suppose i want to render a tooltip displaying the node details on hover, how would i do it. Also for some reason when i hover over the node, the entire graph re renders. It keeps re rendering until unless i remove my mouse from the node. the code ; ` import React, { useMemo, useState, useCallback, useRef } from 'react'; import ForceGraph2D from 'react-force-graph-2d'; import { graphData } from './data'; import { generateNodesForForceGraph } from './utils/nodeGenerator'; import { generateEdgesForForceGraph } from './utils/edgeGenerator';

const HighlightGraph: React.FC = () => { // Generate nodes and links const nodes = generateNodesForForceGraph(graphData); const links = generateEdgesForForceGraph(graphData);

// Memoize node and link cross-referencing
const { nodesWithNeighbors, enhancedLinks } = useMemo(() => {
    // Cross-link node objects
    const nodesMap = new Map(nodes.map(node => [node.id, { ...node, neighbors: [], links: [] }]));

    const enhancedLinks = links.map(link => {
        const sourceNode = nodesMap.get(link.source);
        const targetNode = nodesMap.get(link.target);

        if (sourceNode && targetNode) {
            sourceNode.neighbors.push(targetNode);
            targetNode.neighbors.push(sourceNode);
            sourceNode.links.push(link);
            targetNode.links.push(link);
        }

        return {
            ...link,
            source: sourceNode,
            target: targetNode
        };
    });

    return {
        nodesWithNeighbors: Array.from(nodesMap.values()),
        enhancedLinks
    };
}, [nodes, links]);

// State for highlighting with useRef to prevent unnecessary re-renders
const highlightNodesRef = useRef(new Set());
const highlightLinksRef = useRef(new Set());
const [hoverNode, setHoverNode] = useState<any>(null);

// Optimized node hover handler
const handleNodeHover = useCallback((node) => {
    // Prevent unnecessary state updates if the hovered node is the same
    if (node === hoverNode) return;

    console.log("node hovered", node)
    // Clear previous highlights
    highlightNodesRef.current.clear();
    highlightLinksRef.current.clear();

    if (node) {
        // Add hovered node
        highlightNodesRef.current.add(node);

        // Add neighbors and their links
        node.neighbors.forEach(neighbor => {
            highlightNodesRef.current.add(neighbor);
        });
        node.links.forEach(link => {
            highlightLinksRef.current.add(link);
        });
    }

    // Update hover node state
    setHoverNode(node);
}, [hoverNode]);

// Link hover handler
const handleLinkHover = useCallback((link) => {
    // Clear previous highlights
    highlightNodesRef.current.clear();
    highlightLinksRef.current.clear();

    if (link) {
        highlightLinksRef.current.add(link);
        highlightNodesRef.current.add(link.source);
        highlightNodesRef.current.add(link.target);
    }

    // Reset hover node
    setHoverNode(null);
}, []);

// Optimized ring painting with useCallback
const paintRing = useCallback((node, ctx) => {
    // Only paint for highlighted nodes
    if (highlightNodesRef.current.has(node)) {
        ctx.beginPath();
        ctx.arc(node.x, node.y, 4 * 1.4, 0, 2 * Math.PI, false);
        ctx.fillStyle = node === hoverNode ? 'red' : 'orange';
        ctx.fill();
    }
}, [hoverNode]);

// Full node pointer area
const nodePointerAreaPaint = useCallback((node, color, ctx) => {
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.arc(node.x, node.y, 4 * 1.4, 0, 2 * Math.PI, false);
    ctx.fill();
}, []);

console.log("Node paint", nodePointerAreaPaint)
return (
    <ForceGraph2D
        graphData={{
            nodes: nodesWithNeighbors,
            links: enhancedLinks
        }}
        nodeRelSize={4}
        autoPauseRedraw={false}

        nodePointerAreaPaint={nodePointerAreaPaint}
        // Link styling
        linkWidth={link => highlightLinksRef.current.has(link) ? 5 : 1}
        linkDirectionalParticles={4}
        linkDirectionalParticleWidth={link => highlightLinksRef.current.has(link) ? 4 : 0}

        // Node styling
        nodeCanvasObjectMode={node => highlightNodesRef.current.has(node) ? 'before' : undefined}
        nodeCanvasObject={paintRing}

        // Interaction handlers
        onNodeHover={handleNodeHover}
        onLinkHover={handleLinkHover}
    />
);

};

export default HighlightGraph;`