Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph reloading when hovering the menu #518

Open
uhessbai opened this issue May 21, 2024 · 5 comments
Open

Graph reloading when hovering the menu #518

uhessbai opened this issue May 21, 2024 · 5 comments

Comments

@uhessbai
Copy link

uhessbai commented May 21, 2024

Hello !

I have an issue with a context menu which is coloring a node when hovering the linked part of the menu, therefore when i'm hovering a part of the menu, the graph is completely refreshing, which creates bumps

Here is an example :

Animation

The app is containing the drawer and the graph :

export function App() {

  const [nodes, setNodes] = useState([]);
  const [links, setLinks] = useState([]);
  const [dataNodes, setDataNodes] = useState([]);
  const [menuItemHovered, setMenuItemHovered] = useState(null);

  useEffect(() => {
    fetch('../d3-dependencies.csv')
    .then(r => r.text())
    .then(d3.csvParse)
    .then(data => {
      setDataNodes(data);
      const n = [], links = [];
      data.forEach(({ size, path }) => {
        const levels = path.split('/'),
          level = levels.length - 1,
          module = level > 0 ? levels[1] : null,
          leaf = levels.pop(),
          parent = levels.join('/');
        const node = {
          path,
          leaf,
          module,
          size: +size || 20,
          level
        };
        n.push(node);
        if (parent) {
          links.push({source: parent, target: path, targetNode: node});
        }
    });
    setNodes(n);
    setLinks(links);
}) 
}, []);

const onMenuItemHover = (pathHovered) => {
  console.log({pathHovered});
  setMenuItemHovered(pathHovered);
}
  if(nodes.length === 0  || links.length === 0){
    return <p>loading</p>
  }
  return (
    <div className="App">
        <Drawer onHover={onMenuItemHover} data={dataNodes}></Drawer>
        <div id="3d-graph"></div>
        <ForceTree 
          nodeHoveredFromMenu={menuItemHovered}
          data={{nodes, links}}
        />
    </div>
  )
}

My drawer looks like this :

export default function PermanentDrawerLeft({data, onHover}) {
   
  return (
      <Drawer
        sx={{
          width: drawerWidth,
          flexShrink: 0,
          '& .MuiDrawer-paper': {
            width: drawerWidth,
            boxSizing: 'border-box',
          },
        }}
        variant="permanent"
        anchor="left"
      >
        <Toolbar />
        <Divider />
        <List>
          {data.filter(item => item.path.split('/').length === 2).map(({size, path}) => (
            <ListItem key={path} disablePadding onMouseEnter={() => onHover(path)}>
              <ListItemButton>
                <ListItemIcon>
                    <InboxIcon />
                </ListItemIcon>
                <ListItemText primary={path} />
              </ListItemButton>
            </ListItem>
          ))}
        </List>
        <Divider />
    
      </Drawer>
  );
}

And here is the graph with its interactions :

// highlight 
const highlightNodes = new Set();
let hoverNode = null;

// colors
let lastFolder ='';
let colorIndex = 0;
const colors = [
  'rgba(171, 34, 34, 1)',
  'rgba(255, 208, 0, 1)',
  'rgba(30, 184, 97, 1)',
  'rgba(45, 151, 189, 1)',
  'rgba(116, 124, 165, 1)',
  // 'rgba(186, 97, 140, 1)',
  // 'rgba(160, 42, 184, 1)',
  'rgba(40, 19, 176, 1)',
  'rgba(234, 249, 217, 1)',
  'rgba(226, 212, 186, 1)',

]
const groupNodes = {}

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

// --- GRAPH ---  
export const ForceTree = ({ data, nodeHoveredFromMenu = null }) => {
  
  const fgRef = useRef();

  // --- resize
  const [displayWidth, setDisplayWidth] = useState(window.innerWidth);
  const [displayHeight, setDisplayHeight] = useState(window.innerHeight);
  window.addEventListener('resize', () => {
    setDisplayWidth(window.innerWidth - 240);
    setDisplayHeight(window.innerHeight);
  });

  // --- controls
  const [controls] = useState({ 'DAG Orientation': null});

  // --- update render
  const forceUpdate = useForceUpdate();
 
  // --- modal
  const [open, setOpen] = useState(false);
  
  const handleOpen = (node) => { 
    fgRef.node = node.leaf;
    setOpen(true);
  };
  const handleClose = () => setOpen(false);

  // --- hover highlight
  const [highlightNodes, setHighlightNodes] = useState(new Set());
  const [hoverNode, setHoverNode] = useState(null);


useEffect(() => {
  console.log({nodeHoveredFromMenu})
  const hoveredNode = data.nodes.find(n => n.path === nodeHoveredFromMenu);
  console.log({hoveredNode});
  handleNodeHover(hoveredNode);
}, [nodeHoveredFromMenu])

const updateHighlight = () => {
  setHighlightNodes(highlightNodes);
};

const handleNodeHover = node => {
  console.log(node);
  highlightNodes.clear();
  highlightNodes.add(node);
  setHoverNode(node || null);
  updateHighlight();
};

const handleResize = (node) => {
  //const size = highlightNodes.has(node) ? node === hoverNode ? 4 : 2 : null;
  return 4;
}

// --- Colors
  const handleColors = (node) => {
    const color = highlightNodes.has(node) ? node === hoverNode ? 'rgb(255,0,0,1)' : 'rgba(255,160,0,0.8)' : null
    if (color === null) {
     // handling drawing color
     const path = node.path.split('/');
     const folderOfFile = path[path.length -2];
     if (groupNodes.hasOwnProperty(folderOfFile)) {
        return groupNodes[folderOfFile]
     } else {
        groupNodes[folderOfFile] = colors[colorIndex];
        colorIndex++;
        if (colorIndex >= colors.length -1) {
          colorIndex = 0;
          }
      }
      lastFolder = node.path.split('/').splice(-2);
     return colors[colorIndex];
    }
    return color;
  };

  // DEV MODE : render doublé à cause du strictMode

  useEffect(() => {
    // add collision force
    fgRef.current.d3Force('collision', d3.forceCollide(node => Math.sqrt(100 / (node.level + 1))));
  }, []);
  
console.log(ForceGraph3D);

  const forceGraph =   <ForceGraph3D
    width={displayWidth}
    height={displayHeight}
    ref={fgRef}
    graphData={data}
   // dagMode={controls['DAG Orientation']}
    dagLevelDistance={300}
    backgroundColor="#101020"
    linkColor={() => 'rgba(255,255,255,0.2)'}
    nodeRelSize={2} // resize on hover : change parameter of nodeRelsize in package to wait for a function instead of number (and then make a callback checking if node is hovered)
    nodeId="path"
    nodeVal={node => 100 / (node.level * node.level + 1)}
    nodeLabel="path"
   // nodeAutoColorBy="module"
    linkDirectionalParticles={0} 
    linkDirectionalParticleWidth={0}
    nodeColor={handleColors}
    d3VelocityDecay={0.3}
    enableNodeDrag={false}
    onNodeClick={node => handleOpen(node)}
    onNodeHover={handleNodeHover}
    />

  return (
  <div className="graph-container">
      <Modal
    open={open}
    onClose={handleClose}
    aria-labelledby="modal-modal-title"
    aria-describedby="modal-modal-description"
  >
    <Box sx={style}>
      <Typography id="modal-modal-title" variant="h6" component="h2">
      {fgRef.node}
      </Typography>
      <Typography id="modal-modal-description" sx={{ mt: 2 }}>
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Aut, dolor minus! Culpa omnis suscipit facere corporis veritatis modi. Ad blanditiis deserunt tempora labore cum dolores accusamus. Dolores minima autem natus!
      </Typography>
    </Box>
  </Modal>
  {forceGraph}
    </div>);
};

Could you help me ?

I hope this code is not too long, let me know if you prefer getting the repository or if the question is not appropriate

Thanks for reading, have a good day !

@vasturiano
Copy link
Owner

@uhessbai thanks for reaching out. It's more practical if you submit an online example on https://codesandbox.io/. However in your case most likely this is happening because you're not memoizing the functions that you're passing to the component, for instance:

nodeVal={node => 100 / (node.level * node.level + 1)}

Therefore at every rerender the component receives a new function and needs to recompute every node position and re-heat the layout.

Same thing might be true with your data prop if it's a new reference each time (that part of the code is not present above).

@uhessbai
Copy link
Author

@vasturiano thanks for your reply ! I will come back in a few days at home, I will try that soon :)

@uhessbai
Copy link
Author

Ok so i tried some things, for nodeVal i can use a static value which works ! As adviced i also put the code on codesanbox (config is not on point tho) at this link

But for "data" its another kind of issue : my problem is that my app component hosts the drawer and the forcetree, and the info goes from one component to the other to help the highlight of nodes and i dont know if i there is a way to solve this issue, i tried by myself and with the help of ai but my knowledge of react seems not clear enough because even though i tried multiple things i was going around in circles

Could you bring more details about your clue on this issue ?

Also our answer greatly helped me to better understand how the layout works, thanks for that too !

@vasturiano
Copy link
Owner

@uhessbai essentially you should try to keep your data object memoized, i.e. its reference doesn't change from one render to the next. This is regardless of whether it belongs to the force graph component or its parent component. At the time that it is created it should not be regenerated at every render, unless needed.

@uhessbai
Copy link
Author

@vasturiano thank you very much ! I will try that soon, i'll post the solution there if it might help someone!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants