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.21k stars 285 forks source link

Where found example for control menu #350

Open Hann1bal opened 2 years ago

Hann1bal commented 2 years ago

Can add in previews how add controls menu. Because i'll try to add menu and nowhere can't find examples how to use. And One more questions: how to reuse data in collapsed graph?

const ExpandableGraph: FC<IGraph> = (graphsData: IGraph, ids: number) => {
    const [rootId, setRootId] = useState(3)
    const graphRef =useRef()
    const forceUpdate = useForceUpdate();

    const graphData = useMemo(
        () => ({
            Id: _.cloneDeep(graphsData.Id),
            Name: _.cloneDeep(graphsData.Name),
            nodes: _.cloneDeep(graphsData.nodes),
            rootId: _.cloneDeep(graphsData.rootId),
            links: _.cloneDeep(graphsData.links)
        }),
        [graphsData.Id, graphsData.Name, graphsData.nodes, graphsData.rootId, graphsData.links]
    );
    const graphData2 =graphData
    const [graphDataCopy, setGraphObject] = useState<IGraph>(graphData)
    useEffect(() => {
        if(graphData!==graphData2)
        {
            console.log(332323232)
        }
        if (rootId!==graphData.rootId) {
            setGraphObject(graphData)
            d3f.forceCenter(0, 0)
        }
    }, [graphData.rootId])
    const nodesById = useMemo(() => {
        const nodesById = Object.fromEntries(graphDataCopy.nodes.map(node => [node.id, node]));
        if (graphDataCopy.rootId && graphDataCopy.rootId !== rootId) {
            setRootId(ids)
        }
        // link parent/children
        graphDataCopy.nodes.forEach(node => {
            node.collapsed = node.id !== rootId;
            node.childLinks = [];
        });
        graphDataCopy.links.forEach(link => {
            nodesById[link.source].childLinks.push(link)
        });

        return nodesById;
    }, [graphDataCopy.links, graphDataCopy.nodes, graphDataCopy.rootId, ids, rootId]);

    const getPrunedTree = useCallback(() => {
        const visibleNodes = [];
        const visibleLinks = [];
        (function traverseTree(node = nodesById[rootId]) {
            visibleNodes.push(node);
            if (node.collapsed) return;
            visibleLinks.push(...node.childLinks);
            node.childLinks
                .map(link => ((typeof link.target) === 'object') ? link.target : nodesById[link.target]) // get child node
                .forEach(traverseTree);
        })();

        return {nodes: visibleNodes, links: visibleLinks};
    }, [nodesById]);

    const [prunedTree, setPrunedTree] = useState(getPrunedTree());

    const handleNodeClick = useCallback(node => {
        node.collapsed = !node.collapsed; // toggle collapse state
        setPrunedTree(getPrunedTree())
    }, []);

    return <ForceGraph3D
        enablePointerInteraction={true}
            backgroundColor={'#000000'}
        nodeThreeObjectExtend={true}
        linkDirectionalParticles={2}
        nodeThreeObject={(node) => {
            const myText = new SpriteText(node.name);
            const myScene = new THREE.Scene();
            myScene.add(myText);
            return myScene;
        }}
        graphData={prunedTree}
        onNodeClick={handleNodeClick}
    />;
};

export default observer(ExpandableGraph);

but give that error Uncaught TypeError: Cannot read properties of undefined (reading 'collapsed') at traverseTree (Graph3D.tsx:64:1) at Graph3D.tsx:69:1 at ExpandableGraph (Graph3D.tsx:74:1) at observer.ts:57:1 at useObserver.ts:115:1 at trackDerivedFunction (derivation.ts:179:1) at Reaction.track (reaction.ts:137:1) at useObserver (useObserver.ts:113:1) at wrappedComponent (observer.ts:57:1) at renderWithHooks (react-dom.development.js:15020:1) Because data memorized and after changing incoming data state and if income new values copy data stay same?

Hann1bal commented 2 years ago

Ok i fix it for half. i found problem in data structure because my graph is heterogenus and one child node have link as child to another root child node. This bug because nodes not compare between and try to render two times same node. How fix it?

import React, {FC, useCallback, useEffect, useMemo, useRef, useState} from "react";
import {ForceGraph3D} from "react-force-graph";

import {observer} from "mobx-react-lite";
import {IGraph} from "../models/users/IGraph";
import SpriteText from "three-spritetext";
import * as THREE from "three";
import _ from "lodash";
import {INodes} from "../models/users/INodes";
import dat from "dat-gui";

const useForceUpdate = () => {
    const setToggle = useState(false)[1];
    return () => setToggle(b => !b);
};

const ExpandableGraph: FC<IGraph> = (graphsData: IGraph, ids: number) => {
    const forceUpdate = useForceUpdate();
    const fgRef = useRef();

    const graphData = useMemo(
        () => ({
            Id: _.cloneDeep(graphsData.Id),
            Name: _.cloneDeep(graphsData.Name),
            nodes: _.cloneDeep(graphsData.nodes),
            rootId: _.cloneDeep(graphsData.rootId),
            links: _.cloneDeep(graphsData.links)
        }),
        [graphsData.Id, graphsData.Name, graphsData.nodes, graphsData.rootId, graphsData.links]
    );
    const [graphDataCopy, setGraphObject] = useState<IGraph>(graphData)
    const [controls] = useState({'NodeId': 1});
    const [controlsname] = useState({'Show Node As Text': false});
    useEffect(()=>{
        setGraphObject({
            Id: _.cloneDeep(graphsData.Id),
            Name: _.cloneDeep(graphsData.Name),
            nodes: _.cloneDeep(graphsData.nodes),
            rootId: _.cloneDeep(graphsData.rootId),
            links: _.cloneDeep(graphsData.links)
        })
    }, [])
    useEffect(() => {

        const gui = new dat.GUI();

        gui.add(controls, 'NodeId', graphData.nodes.map(node => node.id))
            .onChange(forceUpdate);
        gui.add(controlsname, 'Show Node As Text', [true, false])
            .onChange(forceUpdate);
    }, [])

    const nodesById = useMemo(() => {

        const nodesById = Object.fromEntries(graphDataCopy.nodes.map(node => [node.id, node]));
        // link parent/children

        graphDataCopy.nodes.forEach(node => {
            node.collapsed = node.id !== controls["NodeId"];
            node.childLinks = [];
        });
        graphDataCopy.links.forEach(link =>
            nodesById[link.source].childLinks.push(link)
        );

        return nodesById;
    }, [graphDataCopy.links, graphDataCopy.nodes, graphDataCopy.rootId]);

    const getPrunedTree = useCallback(() => {
        const visibleNodes = [];
        const visibleLinks = [];
        console.log(graphDataCopy.rootId);
        (function traverseTree(node: INodes = nodesById[controls["NodeId"]]) {

            visibleNodes.push(node);

            if (node.collapsed) return;

            visibleLinks.push(...node.childLinks);
            node.childLinks
                .map(link => ((typeof link.target) === 'object') ? link.target : nodesById[link.target]) // get child node
                .forEach(traverseTree);
        })();

        return {nodes: visibleNodes, links: visibleLinks};
    }, [nodesById]);

    const [prunedTree, setPrunedTree] = useState(getPrunedTree());

    const handleNodeClick = useCallback(node => {
        node.collapsed = !node.collapsed; // toggle collapse state
        setPrunedTree(getPrunedTree());
        forceUpdate();
    }, []);

    return <ForceGraph3D
        enablePointerInteraction={true}
        ref={fgRef}
        backgroundColor={'#000000'}
        nodeThreeObjectExtend={true}
        linkDirectionalArrowLength={5.5}
        linkDirectionalArrowRelPos={1.3}
        linkWidth={1.2}
        linkDirectionalParticles={1}
        linkDirectionalParticleWidth={2}
        linkDirectionalParticleColor={"#ffe202"}
        nodeThreeObject={(node) => {
            const myText = new SpriteText(node.name);
            const myText2 = new THREE.BoxGeometry(1, 1, 1);
            const material = new THREE.MeshBasicMaterial({color: 0x00ff00});
            const cube = new THREE.Mesh(myText2, material);

            const myScene = new THREE.Scene();
            const myScene2 = new THREE.Scene();
            myScene.add(myText);
            myScene2.add(cube)
            return controlsname['Show Node As Text'] ? myScene : myScene2
        }}
        graphData={prunedTree}
        onNodeClick={handleNodeClick}
    />;
};

export default observer(ExpandableGraph);

But i did not understand how rerender graph without functions onClickHandler, because when i change rootId, to me need a click on node for refresh prunedTree? And when i change views default node or spritetext from node to sprite text returned good, but when i try make a rollback state doesn't changing and spritetext not change back to ball or cube.

image

Windows 10 Chrome React 17.0.2 Typescript

Hann1bal commented 2 years ago

And how to move gui bar to render window? image