vasturiano / 3d-force-graph

3D force-directed graph component using ThreeJS/WebGL
https://vasturiano.github.io/3d-force-graph/example/large-graph/
MIT License
4.48k stars 803 forks source link

Showing labels front of node #669

Open longlp opened 4 months ago

longlp commented 4 months ago

I have try two options for showing labels with node

619DE843-3942-44C2-856B-C0C5C71766BE

8EF7BCAD-8888-44C4-A537-8F9AA7D441C0

My requirement is showing the text front of node ( for sprite text the position is center of node)

I’m working around with calculating distance between node and camera and set font size of text but still meet the UX requirements (it’s delayed due to event stop of control trigger with debound function).

I try to set potions of Sprite but the methods to calculate of Sprite text after rotating is complexity and still delay due to debound when rotating)

Can your guy have any ideas to solve this problem or have any others ideas for this requirement plz give me some instructions. Thank you all.!

EncompassingResidential commented 3 months ago

Hi @longlp , I got sprites to work in my React app. Here is my code. down below that I'll show the HTML code that I can not get to work if you can help.

Sprites / canvas code : TreeForceGraphComponent.js

import React, { useEffect } from 'react'; import ForceGraph3D from 'react-force-graph-3d'; import { Group, Mesh, MeshBasicMaterial, SphereGeometry, Sprite, SpriteMaterial, Texture } from 'three';

-function TreeForceGraphComponent({ tree_18_GraphData, targetBAGName, areNodeLabelsOn }) {

function createTextSprite(text) {
  const canvas = document.createElement('canvas');
  const context = canvas.getContext('2d');

  canvas.width = 512; 
  canvas.height = 256;
  context.font = '24px Arial';
  context.fillStyle = 'rgba(255, 255, 255, 1.0)';

  context.textAlign = 'left';
  context.textBaseline = 'middle';

  context.fillText(text, canvas.width / 2, canvas.height / 2);

  const texture = new Texture(canvas);
  texture.needsUpdate = true;

  const spriteMaterial = new SpriteMaterial({ map: texture });
  const sprite = new Sprite(spriteMaterial);

  sprite.scale.set(75, 75, 1);

  return sprite;
}

const nodeThreeObject = areNodeLabelsOn ? node => {

const group = new Group();

const nodeGeometry = new SphereGeometry(7);
const nodeMaterial = new MeshBasicMaterial({ color: node.color });
const mesh = new Mesh(nodeGeometry, nodeMaterial);
group.add(mesh);

const sprite = createTextSprite(node.id);
sprite.color = node.color;
sprite.textHeight = 1;
sprite.position.x = 0;
sprite.position.y = 0;
sprite.position.z = 8;
return sprite;

} : null;

 return (
    <div>
    <ForceGraph3D
        graphData={tree_18_GraphData}
        width={1000}
        height={500}
        backgroundColor="#B4B5C5"
        nodeThreeObject={ areNodeLabelsOn ? nodeThreeObject : undefined }
        nodeThreeObjectExtend={true}
        nodeLabel={nodes => `${nodes.id || 'SomE ThinG'} :: ${nodes.description || 'Empty Description'}`}
        nodeAutoColorBy="group"
        linkDirectionalParticles={6}
        linkWidth={3}
        linkOpacity={0.5}
        />
    </div>
);

}

export default TreeForceGraphComponent;

Here is React code trying to add HTML on each node, but not working: TreeForceGraphComponent.js

import React, { useCallback, useEffect, useRef } from 'react';

import ForceGraph3D from 'react-force-graph-3d'; import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; import { logEntry } from './Utilities';

function TreeForceGraphComponent({ tree_18_GraphData, targetBAGName, areNodeLabelsOn, focusNodeId }) {

const fgRef = useRef();

const nodeThreeObject = areNodeLabelsOn ? node => {

const nodeEl = document.createElement('div');
nodeEl.textContent = node.id;
nodeEl.style.color = node.color;
nodeEl.className = 'node-label';
const label = new CSS2DObject(nodeEl);

label.position.setY(5);

return label;

};

useEffect(() => {
const css2DRenderer = new CSS2DRenderer();

document.body.appendChild(css2DRenderer.domElement);

// Set the size of the CSS2DRenderer to match your graph container
// This might be dynamic depending on your setup
css2DRenderer.setSize(window.innerWidth, window.innerHeight);

// Update ForceGraph3D to use the CSS2DRenderer
if (fgRef.current) {
  fgRef.current.renderer([css2DRenderer]);
}

}, []);

const nodeAutoColorBy = (node) => { if (node.id === targetBAGName) { return 12; } return node.group; };

const linkColorDetermine = link => { if (link.type === 'Past Member') { return 'green'; } else if (link.type === 'Current Member') { return 'darkgreen'; } else if (link.type === 'Genre') { return 'lightgreen'; } else if (link.type === 'Label') { return 'blue'; } else { return 'gray'; } }

return (

logEntry('TreeForceGraphComponent', ` onNodeDragStart: ${node.id}`) } onNodeDragEnd={node => logEntry('TreeForceGraphComponent', ` onNodeDragEnd: ${node.id}`) } onNodeClick={handleNodeClick} width={1000} height={500} backgroundColor="#B4B5C5" nodeAutoColorBy={nodeAutoColorBy} nodeThreeObject={areNodeLabelsOn ? nodeThreeObject : undefined } nodeThreeObjectExtend={true} // false turns off nodes with nodeThreeObject linkWidth={areNodeLabelsOn ? 2 : 1} linkOpacity={areNodeLabelsOn ? 0.5 : 0.4} linkColor={linkColorDetermine} linkDirectionalParticles={6} nodeLabel={nodes => `${nodes.id || 'SomE ThinG'} :: ${nodes.description || 'Empty Description'}`} />

); }

export default TreeForceGraphComponent;

longlp-sosene commented 3 months ago

Just work around and solve my problem with inject the callback function to handle increase fontsize (base on distance between Node and Camera) with control change event

Graph.controls()
    .addEventListener('change', ev => {
        handleChangeTextFormat()
    });

function getNodeDistanceWithCamera(node) {
    // get the position of the node
    // @ts-ignore
    const obj = node;
    if (!obj.x || !obj.y || !obj.z) return 0;
    const nodePosition = new THREE.Vector3(obj.x, obj.y, obj.z);
    // then get the distance between the node and camera , console.log it
    return nodePosition.distanceTo(Graph.cameraPosition())
}

function handleChangeTextFormat() {
    Graph.nodeThreeObject(node => {
        // console.log('handling text format');
        let label = node.label
        let distance = getNodeDistanceWithCamera(node) ?? 1;
        let fontSize = controls['Font size'] + highlight;
        let labelFontSize= fontSize + (2000 / distance)
        const nodeEl = document.createElement('div');
        nodeEl.textContent = label;
        nodeEl.className = 'node-label';
        nodeEl.style.fontSize = labelFontSize + 'px;
        return new CSS2DObject(nodeEl);
    })
}
EncompassingResidential commented 3 months ago

Thank you Le Phi for replying. I got some un-referenced variables compiler errors:

import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

// Is your Graph variable this: const Graph = useRef(); // If not this then I need your Graph declaration / definition.

// I have this three library import, but you are referencing a THREE.Vector3() // what is your THREE definition? import { Group, Mesh, MeshBasicMaterial, SphereGeometry, Sprite, SpriteMaterial, Texture } from 'three';

// What is your highlight variable defined as ? let fontSize = controls['Font size'] + highlight;

EncompassingResidential commented 3 months ago

Le, I figured it out how to show labels all the time. I don't need this HTML specific solution. Since I got this to work I'm not sure what advantage the HTML path is?

The clue was in https://github.com/vasturiano/3d-force-graph/blob/master/example/text-nodes/index.html

I needed to 1 - npm install three-spritetext 2 - import import SpriteText from 'three-spritetext';

so all I needed was:

import React, { useCallback, useEffect, useRef } from 'react'; import ForceGraph3D from 'react-force-graph-3d'; import SpriteText from 'three-spritetext';

const fgRef = useRef();

const nodeThreeObject = areNodeLabelsOn ? node => {

const sprite = new SpriteText(node.id);
sprite.color = node.color;
sprite.textHeight = 8;
sprite.position.x = 0;
sprite.position.y = 0;
sprite.position.z = 8;
return sprite;

} : null;

return (

logEntry('TreeForceGraphComponent', ` onNodeDragStart: ${node.id}`) } onNodeDragEnd={node => logEntry('TreeForceGraphComponent', ` onNodeDragEnd: ${node.id}`) } onNodeClick={handleNodeClick} width={1000} height={500} backgroundColor="#B4B5C5" nodeAutoColorBy={nodeAutoColorBy} nodeThreeObject={areNodeLabelsOn ? nodeThreeObject : undefined } nodeThreeObjectExtend={true} // false turns off nodes with nodeThreeObject linkWidth={areNodeLabelsOn ? 2 : 1} linkOpacity={areNodeLabelsOn ? 0.5 : 0.4} linkColor={linkColorDetermine} linkDirectionalParticles={6} nodeLabel={areNodeLabelsOn ? undefined : nodes => `${nodes.id || 'SomE ThinG'} :: ${nodes.description || 'Empty Description'}`} />

);

longlp-sosene commented 3 months ago

sprite.position.x = 0; sprite.position.y = 0; sprite.position.z = 8; Yeah, but the problem is when you rotate, the label will be behind node :D

longlp-sosene commented 3 months ago

Thank you Le Phi for replying. I got some un-referenced variables compiler errors:

import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';

// Is your Graph variable this: const Graph = useRef(); // If not this then I need your Graph declaration / definition.

// I have this three library import, but you are referencing a THREE.Vector3() // what is your THREE definition? import { Group, Mesh, MeshBasicMaterial, SphereGeometry, Sprite, SpriteMaterial, Texture } from 'three';

// What is your highlight variable defined as ? let fontSize = controls['Font size'] + highlight;

const Graph = ForceGraph3D({ extraRenderers: [new CSS2DRenderer()] }) (container3DGraph) .backgroundColor('#1C1C1E') .enableNodeDrag(true) .enableNavigationControls(true) ....

import * as THREE from 'three'; just remove highlight

dhilst commented 2 months ago

By default, when I hover the mouse over a node I get the label displayed, is there any way to use the same mechanism to toggle the label visibility? I would like to toggle the node label visibility on click

Do I need SpriteText for that?

Great project BTW :tada: !!

longlp-sosene commented 2 months ago

By default, when I hover the mouse over a node I get the label displayed, is there any way to use the same mechanism to toggle the label visibility? I would like to toggle the node label visibility on click

Do I need SpriteText for that?

Great project BTW 🎉 !!

You can handle it by using SpriteText or CSS2DObject with default opacity of text color = 0, and using highlight when click on node onNodeClick or onNodeHove (see example for more detail) to set opacity = 1