Graph Node Visualization Options
A guide to customizing node appearances and behaviors in Three.js graph visualizations, including materials, properties, and implementation examples.
Published February 11, 2025
This document outlines various options for customizing node appearances and behaviors in Three.js graph visualizations.
1. Basic Visual Properties
Materials
const nodeMaterial = new THREE.MeshBasicMaterial({color: 0x0ea5e9, // Color in hexopacity: 0.8, // 0 to 1transparent: true, // Enable opacitywireframe: true, // Show wireframe onlyvisible: true // Toggle visibility})
Material Types
: Unlit, simpleMeshBasicMaterial
: Shiny with light responseMeshPhongMaterial
: Physically-based renderingMeshStandardMaterial
: Cartoon-style shadingMeshToonMaterial
Geometry Options
const nodeGeometry = new THREE.SphereGeometry(radius, // Size of spheresegments, // Horizontal segmentsrings // Vertical rings)
2. Interactive Features
Node Data Storage
const mesh = new THREE.Mesh(nodeGeometry, nodeMaterial)mesh.userData = {id: node.id,type: node.type,selected: false,hover: false}
Hover Detection
const raycaster = new THREE.Raycaster()const pointer = new THREE.Vector2()function onPointerMove(event: MouseEvent) {pointer.x = (event.clientX / window.innerWidth) * 2 - 1pointer.y = -(event.clientY / window.innerHeight) * 2 + 1raycaster.setFromCamera(pointer, camera)const intersects = raycaster.intersectObjects(scene.children)// Handle hover effectsif (intersects.length > 0) {const node = intersects[0].object as THREE.Meshnode.scale.set(1.2, 1.2, 1.2)}}
3. Visual Effects
Glow Effect
const glowMaterial = new THREE.ShaderMaterial({uniforms: {color: { value: new THREE.Color(0x00ff00) }},vertexShader: /* shader code */,fragmentShader: /* shader code */,transparent: true})
Node Labels
function createLabel(text: string, position: THREE.Vector3) {const canvas = document.createElement('canvas')const context = canvas.getContext('2d')const texture = new THREE.CanvasTexture(canvas)const spriteMaterial = new THREE.SpriteMaterial({ map: texture })const sprite = new THREE.Sprite(spriteMaterial)sprite.position.copy(position)return sprite}
4. Animations
Node Animations
function animate() {requestAnimationFrame(animate)nodes.forEach(node => {// Rotationnode.rotation.x += 0.01// Size Pulsingconst scale = 1 + Math.sin(Date.now() * 0.001) * 0.1node.scale.set(scale, scale, scale)// Color Cyclingconst hue = (Date.now() * 0.001) % 1;(node.material as THREE.MeshBasicMaterial).color.setHSL(hue, 1, 0.5)})renderer.render(scene, camera)}
5. Custom Shapes
Custom Geometry
function createCustomNode() {const geometry = new THREE.BufferGeometry()const vertices = new Float32Array([-1.0, -1.0, 1.0,1.0, -1.0, 1.0,1.0, 1.0, 1.0// Additional vertices...])geometry.setAttribute('position',new THREE.BufferAttribute(vertices, 3))return geometry}
6. Void-Specific Node Visualization
Enhanced Node Properties
interface EnhancedNode extends Node {energy: number // Current energy levelpulseFrequency: number // Individual rhythmrelationships: Map<string, number> // Connection strengthsstate: 'dormant' | 'active' | 'focused'}
Responsive Node Behavior
class VoidNode extends GraphNode {private energy: number = 1.0private baseScale: number = 1.0private pulseFrequency: numberconstructor(position: [number, number, number],frequency: number = Math.random() * 0.3 + 0.2) {super(position)this.pulseFrequency = frequency// Use MeshPhongMaterial for better light interactionconst material = new THREE.MeshPhongMaterial({color: 0x0ea5e9,emissive: 0x0ea5e9,emissiveIntensity: 0.2,transparent: true,opacity: 0.8})this.mesh.material = material}update(time: number) {// Gentle pulsing effectconst pulse = Math.sin(time * this.pulseFrequency) * 0.1 + 1this.mesh.scale.setScalar(this.baseScale * pulse)// Energy-based glowconst material = this.mesh.material as THREE.MeshPhongMaterialmaterial.emissiveIntensity = 0.2 + (Math.sin(time * 0.5) * 0.1)}}
Energy Flow Visualization
class EnergyFlow {private particles: THREE.Pointsprivate flowSpeed: number = 0.5constructor(startNode: VoidNode,endNode: VoidNode,particleCount: number = 20) {const geometry = new THREE.BufferGeometry()const material = new THREE.PointsMaterial({color: 0x0ea5e9,size: 0.05,transparent: true,opacity: 0.6})// Initialize particle positions along the edgeconst positions = new Float32Array(particleCount * 3)// ... particle position initialization ...geometry.setAttribute('position',new THREE.BufferAttribute(positions, 3))this.particles = new THREE.Points(geometry, material)}update(time: number) {// Animate particles along the edgeconst positions = this.particles.geometry.attributes.position.arrayfor (let i = 0; i < positions.length; i += 3) {const t = (time * this.flowSpeed + i) % positions.length// ... update particle positions ...}this.particles.geometry.attributes.position.needsUpdate = true}}
Contextual Interaction
class VoidGraph {private nodes: Map<string, VoidNode> = new Map()private flows: EnergyFlow[] = []highlightNode(nodeId: string) {const node = this.nodes.get(nodeId)if (!node) return// Highlight focused nodenode.setState('focused')// Adjust connected nodesthis.getConnectedNodes(nodeId).forEach(connectedNode => {connectedNode.setState('active')// Intensify energy flowthis.flows.find(f => f.connects(nodeId, connectedNode.id))?.setIntensity(1.5)})// Fade distant nodesthis.nodes.forEach(n => {if (n.state === 'dormant') {n.fade(0.5)}})}}
Best Practices for Void Visualization
-
Energy and Flow
- Use subtle, continuous animations
- Maintain consistent rhythm in pulsing effects
- Ensure energy flows feel organic and natural
- Keep particle effects minimal and meaningful
-
Responsiveness
- Implement smooth state transitions
- Use gradual scaling and opacity changes
- Ensure all nodes maintain some level of presence
- Create a sense of interconnectedness
-
Visual Harmony
- Keep color palette aligned with void theme
- Use emissive materials for inner glow
- Balance node visibility with background
- Maintain depth through subtle lighting
-
Performance Considerations
- Batch similar animations
- Use shared materials and geometries
- Implement level-of-detail for distant nodes
- Optimize particle system updates
This approach creates a visualization that reflects the void's nature as described:
"The void responds to your presence with subtle shifts of light and shadow, acknowledging you without imposing. It's reminiscent of watching stars slowly reveal themselves as your eyes adjusted to the night sky."
Best Practices
-
Performance
- Use
instead ofBufferGeometryGeometry - Reuse materials and geometries
- Limit the number of light sources
- Use object pooling for dynamic nodes
- Use
-
Interaction
- Implement proper cleanup for event listeners
- Use throttling for hover/move events
- Consider using an octree for large graphs
-
Visual Clarity
- Maintain consistent node sizes
- Use color schemes that work well together
- Consider color-blind friendly palettes
- Add proper lighting for depth perception
Implementation Example
class GraphNode {mesh: THREE.Meshselected: boolean = falseconstructor(position: [number, number, number],color: number = 0x0ea5e9) {const geometry = new THREE.SphereGeometry(0.2, 32, 32)const material = new THREE.MeshPhongMaterial({color,transparent: true,opacity: 0.8})this.mesh = new THREE.Mesh(geometry, material)this.mesh.position.set(...position)}highlight() {this.mesh.scale.set(1.2, 1.2, 1.2);(this.mesh.material as THREE.MeshPhongMaterial).opacity = 1}unhighlight() {this.mesh.scale.set(1, 1, 1);(this.mesh.material as THREE.MeshPhongMaterial).opacity = 0.8}}