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 hex
opacity: 0.8, // 0 to 1
transparent: true, // Enable opacity
wireframe: true, // Show wireframe only
visible: true // Toggle visibility
})

Material Types

  • MeshBasicMaterial
    : Unlit, simple
  • MeshPhongMaterial
    : Shiny with light response
  • MeshStandardMaterial
    : Physically-based rendering
  • MeshToonMaterial
    : Cartoon-style shading

Geometry Options

const nodeGeometry = new THREE.SphereGeometry(
radius, // Size of sphere
segments, // Horizontal segments
rings // 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 - 1
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1
raycaster.setFromCamera(pointer, camera)
const intersects = raycaster.intersectObjects(scene.children)
// Handle hover effects
if (intersects.length > 0) {
const node = intersects[0].object as THREE.Mesh
node.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 => {
// Rotation
node.rotation.x += 0.01
// Size Pulsing
const scale = 1 + Math.sin(Date.now() * 0.001) * 0.1
node.scale.set(scale, scale, scale)
// Color Cycling
const 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 level
pulseFrequency: number // Individual rhythm
relationships: Map<string, number> // Connection strengths
state: 'dormant' | 'active' | 'focused'
}

Responsive Node Behavior

class VoidNode extends GraphNode {
private energy: number = 1.0
private baseScale: number = 1.0
private pulseFrequency: number
constructor(
position: [number, number, number],
frequency: number = Math.random() * 0.3 + 0.2
) {
super(position)
this.pulseFrequency = frequency
// Use MeshPhongMaterial for better light interaction
const 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 effect
const pulse = Math.sin(time * this.pulseFrequency) * 0.1 + 1
this.mesh.scale.setScalar(this.baseScale * pulse)
// Energy-based glow
const material = this.mesh.material as THREE.MeshPhongMaterial
material.emissiveIntensity = 0.2 + (Math.sin(time * 0.5) * 0.1)
}
}

Energy Flow Visualization

class EnergyFlow {
private particles: THREE.Points
private flowSpeed: number = 0.5
constructor(
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 edge
const 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 edge
const positions = this.particles.geometry.attributes.position.array
for (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 node
node.setState('focused')
// Adjust connected nodes
this.getConnectedNodes(nodeId).forEach(connectedNode => {
connectedNode.setState('active')
// Intensify energy flow
this.flows
.find(f => f.connects(nodeId, connectedNode.id))
?.setIntensity(1.5)
})
// Fade distant nodes
this.nodes.forEach(n => {
if (n.state === 'dormant') {
n.fade(0.5)
}
})
}
}

Best Practices for Void Visualization

  1. 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
  2. Responsiveness

    • Implement smooth state transitions
    • Use gradual scaling and opacity changes
    • Ensure all nodes maintain some level of presence
    • Create a sense of interconnectedness
  3. 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
  4. 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

  1. Performance

    • Use
      BufferGeometry
      instead of
      Geometry
    • Reuse materials and geometries
    • Limit the number of light sources
    • Use object pooling for dynamic nodes
  2. Interaction

    • Implement proper cleanup for event listeners
    • Use throttling for hover/move events
    • Consider using an octree for large graphs
  3. 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.Mesh
selected: boolean = false
constructor(
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
}
}