mirror of
https://github.com/reconurge/flowsint.git
synced 2026-07-19 18:00:42 -05:00
feat(app): remove md files and update styles
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { useGraphStore } from '@/stores/graph-store'
|
||||
import React, { useRef, useCallback } from 'react'
|
||||
// import GraphViewer from './graph-viewer'
|
||||
import GraphViewer from './graph-viewer'
|
||||
// import WebGLGraphViewer from './webgl'
|
||||
import ContextMenu from './context-menu'
|
||||
import GraphViewer from './graph-viewer'
|
||||
import { useParams } from '@tanstack/react-router'
|
||||
|
||||
const GraphMain = () => {
|
||||
@@ -58,12 +57,12 @@ const GraphMain = () => {
|
||||
return (
|
||||
<div ref={containerRef} className="relative h-full w-full bg-background">
|
||||
{/* <WebGLGraphViewer
|
||||
sketchId={sketchId as string}
|
||||
nodes={filteredNodes}
|
||||
edges={filteredEdges}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeRightClick={onNodeContextMenu}
|
||||
onBackgroundClick={handleBackgroundClick}
|
||||
layoutMode={layoutMode}
|
||||
/> */}
|
||||
<GraphViewer
|
||||
nodes={filteredNodes}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* WebGL Graph Viewer - Legacy Entry Point
|
||||
* This file is kept for backward compatibility.
|
||||
* The actual implementation has been refactored into a modular architecture.
|
||||
* @see ./webgl/ for the new modular implementation
|
||||
*/
|
||||
import React from 'react'
|
||||
import type { GraphNode, GraphEdge } from '@/types'
|
||||
import WebGLGraphViewer from './webgl'
|
||||
|
||||
/**
|
||||
* @deprecated Use the new modular implementation from './webgl' directly
|
||||
*/
|
||||
interface WebGLGraphViewerProps {
|
||||
nodes: GraphNode[]
|
||||
edges: GraphEdge[]
|
||||
className?: string
|
||||
style?: React.CSSProperties
|
||||
onNodeClick?: (node: GraphNode, event: MouseEvent) => void
|
||||
onNodeRightClick?: (node: GraphNode, event: MouseEvent) => void
|
||||
onBackgroundClick?: () => void
|
||||
showIcons?: boolean
|
||||
showLabels?: boolean
|
||||
layoutMode?: 'none' | 'force' | 'dagre'
|
||||
}
|
||||
|
||||
/**
|
||||
* WebGL Graph Viewer Component
|
||||
* High-performance graph visualization using WebGL (Pixi.js) and D3-force
|
||||
*
|
||||
* @deprecated This is a legacy wrapper. Import from './webgl' for the new modular version.
|
||||
*/
|
||||
const WebGLGraphViewerLegacy: React.FC<WebGLGraphViewerProps> = (props) => {
|
||||
return <WebGLGraphViewer {...props} />
|
||||
}
|
||||
|
||||
export default WebGLGraphViewerLegacy
|
||||
@@ -1,395 +0,0 @@
|
||||
# Changelog - WebGL Graph Viewer
|
||||
|
||||
## [2.3.0] - Layout Modes Architecture
|
||||
|
||||
### 🏗️ Architecture Refactoring: Unified View System
|
||||
|
||||
**Major architectural change**: Graph views consolidated into a single "graph" view with switchable layout modes.
|
||||
|
||||
#### ✨ New Features
|
||||
|
||||
##### Unified View Architecture
|
||||
- **4 Views instead of 5**: `graph`, `table`, `relationships`, `map`
|
||||
- **Graph view with layout modes**: `none` (default), `force`, `dagre`
|
||||
- **Dynamic layout switching**: Change layouts without changing views
|
||||
- **Layout buttons**: Only visible when in graph view
|
||||
|
||||
##### Layout Mode Support
|
||||
```typescript
|
||||
type LayoutMode = 'none' | 'force' | 'dagre'
|
||||
|
||||
// none: Static grid layout (default)
|
||||
// force: D3 force-directed physics simulation
|
||||
// dagre: Hierarchical top-down layout
|
||||
```
|
||||
|
||||
##### Store Updates
|
||||
```typescript
|
||||
// graph-controls-store.ts
|
||||
view: 'graph' | 'table' | 'map' | 'relationships'
|
||||
layoutMode: 'none' | 'force' | 'dagre'
|
||||
setLayoutMode: (mode: LayoutMode) => void
|
||||
```
|
||||
|
||||
### 🔧 Technical Changes
|
||||
|
||||
#### Updated Files
|
||||
|
||||
**`use-force-simulation.ts`**
|
||||
- Now supports 3 layout modes
|
||||
- Static grid layout for `layoutMode === 'none'`
|
||||
- Force simulation only runs when `layoutMode === 'force'`
|
||||
- Dagre hierarchical layout for `layoutMode === 'dagre'`
|
||||
|
||||
**`toolbar.tsx`**
|
||||
- Layout buttons (Force/Dagre) only visible in graph view
|
||||
- View buttons always visible
|
||||
- Updated disabled states to use `view !== 'graph'`
|
||||
|
||||
**`graph-main.tsx`**
|
||||
- Passes `layoutMode` prop to WebGLGraphViewer
|
||||
- Removed separate hierarchy/force view logic
|
||||
|
||||
**`WebGLGraphViewer`**
|
||||
- Added `layoutMode?: LayoutMode` prop
|
||||
- Defaults to `'none'` (static grid)
|
||||
|
||||
### 📝 Migration Guide
|
||||
|
||||
#### For Users
|
||||
|
||||
**Before:**
|
||||
- Separate views: "Force" and "Hierarchy"
|
||||
- Switching between force/hierarchy changed the entire view
|
||||
|
||||
**After:**
|
||||
- Single "Graph" view
|
||||
- Toggle between Force/Dagre layouts within graph view
|
||||
- Default: Static grid (no automatic layout)
|
||||
|
||||
#### For Developers
|
||||
|
||||
**Old imports (still work):**
|
||||
```typescript
|
||||
import WebGLGraphViewer from './webgl-graph-viewer'
|
||||
// Legacy wrapper, automatically redirects to new module
|
||||
```
|
||||
|
||||
**New imports (recommended):**
|
||||
```typescript
|
||||
import WebGLGraphViewer from './webgl'
|
||||
```
|
||||
|
||||
**Props:**
|
||||
```typescript
|
||||
<WebGLGraphViewer
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
layoutMode="force" // NEW: 'none' | 'force' | 'dagre'
|
||||
// ... other props
|
||||
/>
|
||||
```
|
||||
|
||||
### 🎯 User Experience
|
||||
|
||||
**Graph View Workflow:**
|
||||
1. Open graph view (default: static grid layout)
|
||||
2. Click "Force Layout" button → activates physics simulation
|
||||
3. Click "Dagre Layout" button → applies hierarchical layout
|
||||
4. Layout buttons hidden when switching to table/map/relationships views
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
|
||||
None! Fully backward compatible via legacy wrapper.
|
||||
|
||||
### 📊 Performance
|
||||
|
||||
- Static grid layout (none): Instant rendering, no CPU overhead
|
||||
- Force layout: Same performance as before
|
||||
- Dagre layout: One-time calculation, then static
|
||||
|
||||
---
|
||||
|
||||
## [2.2.0] - Smart Label Decluttering
|
||||
|
||||
### 🚀 Major Feature: Intelligent Label Selection
|
||||
|
||||
**No more overlapping labels!** Complete redesign of the labeling system with collision detection and priority-based selection.
|
||||
|
||||
#### ✨ New Features
|
||||
|
||||
##### Smart Decluttering Algorithm
|
||||
- **Collision detection**: Labels never overlap (4px minimum margin)
|
||||
- **Priority-based selection**: Important nodes get labels first
|
||||
- **Bounding box calculation**: Accurate screen-space dimensions
|
||||
- **Greedy algorithm**: Optimal label placement without overlap
|
||||
- **Spatial hashing**: Fast collision detection for large graphs (10,000+ nodes)
|
||||
|
||||
##### Priority Scoring System
|
||||
```typescript
|
||||
priority = connectionScore * 0.7 + centerScore * 0.3
|
||||
```
|
||||
- **Connection score (70%)**: Highly connected nodes prioritized
|
||||
- **Center score (30%)**: Viewport-centered nodes prioritized
|
||||
- **Selection-aware**: Selected/highlighted nodes always show labels
|
||||
|
||||
##### Performance Optimizations
|
||||
- Small graphs (< 500 nodes): Accurate O(n²) collision detection
|
||||
- Large graphs (≥ 500 nodes): Fast O(1) spatial hashing
|
||||
- Max 200 labels by default (configurable)
|
||||
- Throttled recalculation (200ms)
|
||||
- Zoom hiding (labels hidden during active zoom)
|
||||
|
||||
### 📊 Performance Impact
|
||||
|
||||
| Graph Size | Before | After | Result |
|
||||
|------------|--------|-------|--------|
|
||||
| 100 nodes | 60 FPS (overlaps) | 60 FPS | ✅ No overlap |
|
||||
| 1,000 nodes | 50 FPS (overlaps) | 58 FPS | ✅ +16% + Clean |
|
||||
| 5,000 nodes | 45 FPS (overlaps) | 55 FPS | ✅ +22% + Clean |
|
||||
| 10,000 nodes | 35 FPS (overlaps) | 50 FPS | ✅ +43% + Clean |
|
||||
|
||||
**Benefits:**
|
||||
- 🎨 **Cleaner visuals**: No overlapping labels
|
||||
- 🧠 **Smarter selection**: Important nodes prioritized
|
||||
- 🚀 **Better performance**: Fewer labels = faster rendering
|
||||
- 📍 **Viewport-aware**: Relevant labels shown
|
||||
|
||||
### 🔧 Technical Changes
|
||||
|
||||
#### New Module: `label-decluttering.ts`
|
||||
```typescript
|
||||
// New functions
|
||||
calculateLabelBoundingBox() // Exact screen-space dimensions
|
||||
boundingBoxesOverlap() // Collision detection
|
||||
selectLabelsWithDecluttering() // Accurate selection
|
||||
selectLabelsWithDeclutteringFast() // Fast selection for large graphs
|
||||
```
|
||||
|
||||
#### New Type: `LabelBoundingBox`
|
||||
```typescript
|
||||
interface LabelBoundingBox {
|
||||
nodeId: string
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
priority: number // 0-1, higher = more important
|
||||
}
|
||||
```
|
||||
|
||||
#### Updated: `use-graph-renderer.ts`
|
||||
- Replaced `calculateVisibleLabels()` with `selectLabelsWithDeclutteringFast()`
|
||||
- Integrated viewport and camera position
|
||||
- Added decluttering to zoom throttling
|
||||
|
||||
### 📁 New Files
|
||||
|
||||
- `utils/label-decluttering.ts` - Core decluttering algorithms
|
||||
- `DECLUTTERING.md` - Comprehensive documentation
|
||||
|
||||
### 📝 Documentation
|
||||
|
||||
- New [DECLUTTERING.md](./DECLUTTERING.md) with detailed explanation
|
||||
- Updated [README.md](./README.md) with decluttering section
|
||||
- Updated exports with new functions
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
|
||||
None! Fully backward compatible.
|
||||
|
||||
### 🔄 Migration
|
||||
|
||||
No migration needed - decluttering is automatic!
|
||||
|
||||
Old function still available (deprecated):
|
||||
```typescript
|
||||
// Old way (deprecated, no collision detection)
|
||||
calculateVisibleLabels(nodes, zoomLevel)
|
||||
|
||||
// New way (automatic in renderer)
|
||||
selectLabelsWithDeclutteringFast(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [2.1.0] - Styled Label Backgrounds
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
#### Cosmograph-Inspired Label Backgrounds
|
||||
- **Beautiful label backgrounds** with rounded corners
|
||||
- **Subtle borders** for better visibility
|
||||
- **Configurable styling** (padding, radius, colors, opacity)
|
||||
- **Responsive alpha** based on highlight state
|
||||
- **Performance optimized** - backgrounds update with labels
|
||||
|
||||
#### Visual Improvements
|
||||
- Labels now stand out clearly against any background
|
||||
- Better readability in dense graphs
|
||||
- Professional, polished appearance
|
||||
- Consistent with modern graph visualization tools
|
||||
|
||||
### 🎨 Style Configuration
|
||||
|
||||
```ts
|
||||
NODE_LABEL_PADDING_X: 6 // Horizontal padding
|
||||
NODE_LABEL_PADDING_Y: 3 // Vertical padding
|
||||
NODE_LABEL_BORDER_RADIUS: 4 // Rounded corners
|
||||
NODE_LABEL_BORDER_WIDTH: 1 // Border thickness
|
||||
LABEL_BG_COLOR: 0x1a1a1a // Dark background
|
||||
LABEL_BG_BORDER_COLOR: 0x404040 // Subtle gray border
|
||||
NODE_LABEL_BG_ALPHA: 0.92 // 92% opacity
|
||||
NODE_LABEL_BG_BORDER_ALPHA: 0.3 // 30% opacity
|
||||
```
|
||||
|
||||
### 🔧 Technical Changes
|
||||
|
||||
- Added `NodeLabelObjects` type with `text` and `background`
|
||||
- Updated `createNodeLabel()` to return label objects
|
||||
- Modified `updateNodeLabel()` to manage backgrounds
|
||||
- Optimized rendering order (backgrounds before text)
|
||||
|
||||
---
|
||||
|
||||
## [2.0.0] - LOD System + Context-Aware Labels
|
||||
|
||||
### 🚀 Major Features
|
||||
|
||||
#### Level of Details (LOD) System
|
||||
- **Adaptive rendering** based on zoom level
|
||||
- **4 LOD levels**: minimal, low, medium, high
|
||||
- **Automatic optimization** - no configuration needed
|
||||
- **Performance gain**: +80% when zoomed out
|
||||
|
||||
#### Context-Aware Edge Labels
|
||||
- Edge labels **only appear when nodes are active**
|
||||
- Active = hovered, selected, or highlighted
|
||||
- **90% reduction** in visual clutter
|
||||
- **50% fewer labels** rendered on average
|
||||
|
||||
### ✨ Improvements
|
||||
|
||||
#### Architecture
|
||||
- **Modular structure**: 16 specialized files
|
||||
- **Hooks-based**: Clean separation of concerns
|
||||
- **Type-safe**: Full TypeScript coverage
|
||||
- **Testable**: Each module can be tested independently
|
||||
|
||||
#### Performance
|
||||
- **Texture caching**: Icons loaded once, reused globally
|
||||
- **Batch rendering**: Edges grouped by style
|
||||
- **Early returns**: Skip calculations for invisible elements
|
||||
- **Manual rendering**: Better control over frame timing
|
||||
|
||||
#### Developer Experience
|
||||
- **LOD Indicator**: Debug component for development
|
||||
- **Comprehensive docs**: README with examples
|
||||
- **Public exports**: Hooks and utilities available
|
||||
- **Backward compatible**: Old imports still work
|
||||
|
||||
### 📊 Performance Metrics
|
||||
|
||||
| Nodes | Zoom Out (0.25x) | Medium (1.0x) | Zoomed In (2.0x) |
|
||||
|-------|------------------|---------------|------------------|
|
||||
| 1K | 60 FPS | 60 FPS | 60 FPS |
|
||||
| 5K | 60 FPS | 55 FPS | 50 FPS |
|
||||
| 10K | 55-60 FPS | 50 FPS | 45 FPS |
|
||||
| 50K | 45-60 FPS | 35 FPS | 25-35 FPS |
|
||||
|
||||
### 🔧 Technical Details
|
||||
|
||||
#### LOD Thresholds
|
||||
```ts
|
||||
LOD_ICON_MIN_ZOOM: 0.6 // Icons visible at zoom >= 0.6
|
||||
LOD_NODE_LABEL_MIN_ZOOM: 0.5 // Node labels visible at zoom >= 0.5
|
||||
LOD_EDGE_LABEL_MIN_ZOOM: 1.2 // Edge labels enabled at zoom >= 1.2
|
||||
LOD_HIGH_DETAIL_ZOOM: 1.5 // High quality mode at zoom >= 1.5
|
||||
```
|
||||
|
||||
#### Edge Label Logic
|
||||
```ts
|
||||
// Show edge label if:
|
||||
// 1. Zoom >= LOD_EDGE_LABEL_MIN_ZOOM
|
||||
// 2. Source node is active OR target node is active
|
||||
// Active = hovered || selected || highlighted
|
||||
```
|
||||
|
||||
### 📁 File Structure
|
||||
|
||||
```
|
||||
webgl/
|
||||
├── index.tsx # Main component (150 lines)
|
||||
├── constants.ts # Configuration
|
||||
├── exports.ts # Public API
|
||||
├── README.md # Documentation
|
||||
├── CHANGELOG.md # This file
|
||||
├── components/
|
||||
│ └── lod-indicator.tsx # Debug component
|
||||
├── hooks/ # Business logic
|
||||
│ ├── use-pixi-app.ts
|
||||
│ ├── use-force-simulation.ts
|
||||
│ ├── use-graph-interactions.ts
|
||||
│ ├── use-zoom-pan.ts
|
||||
│ └── use-graph-renderer.ts
|
||||
├── renderers/ # Rendering logic
|
||||
│ ├── node-renderer.ts
|
||||
│ ├── edge-renderer.ts
|
||||
│ └── label-renderer.ts
|
||||
├── utils/ # Utilities
|
||||
│ ├── texture-cache.ts
|
||||
│ ├── visibility-calculator.ts
|
||||
│ └── color-utils.ts
|
||||
└── types/
|
||||
└── graph.types.ts # TypeScript types
|
||||
```
|
||||
|
||||
### 🎯 Breaking Changes
|
||||
|
||||
None - fully backward compatible!
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Fixed edge label overlap issues
|
||||
- Fixed icon flickering on zoom
|
||||
- Fixed memory leak on component unmount
|
||||
|
||||
### 📝 Migration Guide
|
||||
|
||||
#### From v1.0 (monolithic)
|
||||
```tsx
|
||||
// Old way - still works!
|
||||
import WebGLGraphViewer from './webgl-graph-viewer'
|
||||
|
||||
// New way - recommended
|
||||
import WebGLGraphViewer from './webgl'
|
||||
```
|
||||
|
||||
#### Using new features
|
||||
```tsx
|
||||
// Enable LOD indicator for debugging
|
||||
import { LODIndicator } from './webgl/exports'
|
||||
|
||||
// Use LOD utilities
|
||||
import { getLODLevel, shouldShowNodeIcons } from './webgl/exports'
|
||||
|
||||
const lodLevel = getLODLevel(zoomLevel)
|
||||
console.log(`Current LOD: ${lodLevel}`) // 'minimal', 'low', 'medium', 'high'
|
||||
```
|
||||
|
||||
### 👥 Contributors
|
||||
|
||||
- Refactored from monolithic 1000-line component
|
||||
- Implemented modular architecture
|
||||
- Added LOD system
|
||||
- Context-aware edge labels
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] - Initial Release
|
||||
|
||||
- Basic WebGL graph visualization
|
||||
- Pixi.js + D3-force
|
||||
- Node interactions (hover, click, drag)
|
||||
- Zoom/pan controls
|
||||
- Basic icon and label support
|
||||
@@ -1,418 +0,0 @@
|
||||
# Label Decluttering System
|
||||
|
||||
This document explains the intelligent label decluttering system that prevents labels from overlapping.
|
||||
|
||||
## Overview
|
||||
|
||||
The decluttering system automatically selects which labels to display based on:
|
||||
1. **Collision detection** - Labels never overlap
|
||||
2. **Priority scoring** - Important nodes get labels first
|
||||
3. **Spatial awareness** - Viewport-centered labels preferred
|
||||
4. **Selection state** - Selected/highlighted nodes always show labels
|
||||
|
||||
## Architecture
|
||||
|
||||
### Before: Simple Selection
|
||||
```typescript
|
||||
// Old system (basic, no collision detection)
|
||||
calculateVisibleLabels(nodes, zoomLevel)
|
||||
// - Selected top N nodes by connection count
|
||||
// - No collision detection
|
||||
// - Labels could overlap completely
|
||||
```
|
||||
|
||||
### After: Intelligent Decluttering
|
||||
```typescript
|
||||
// New system (collision-aware, priority-based)
|
||||
selectLabelsWithDeclutteringFast(
|
||||
nodes,
|
||||
nodeSize,
|
||||
zoomLevel,
|
||||
fontSizeSetting,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
cameraX,
|
||||
cameraY,
|
||||
selectedNodeIds,
|
||||
highlightedNodeIds
|
||||
)
|
||||
// - Calculates real bounding boxes
|
||||
// - Detects collisions
|
||||
// - Greedy selection by priority
|
||||
// - Optimized with spatial hashing for large graphs
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. Bounding Box Calculation
|
||||
|
||||
Each label's exact screen space is calculated:
|
||||
|
||||
```typescript
|
||||
interface LabelBoundingBox {
|
||||
nodeId: string
|
||||
x: number // Center X position
|
||||
y: number // Top Y position
|
||||
width: number // Background width (text + padding)
|
||||
height: number // Background height (text + padding)
|
||||
priority: number // 0-1, higher = more important
|
||||
}
|
||||
```
|
||||
|
||||
**Calculation includes:**
|
||||
- Text length estimation (`fontSize * 0.6` per character)
|
||||
- Background padding (`NODE_LABEL_PADDING_X`, `NODE_LABEL_PADDING_Y`)
|
||||
- Label position (below node: `node.y + nodeSize + fontSize * 0.6`)
|
||||
- Zoom-adjusted font size
|
||||
|
||||
### 2. Priority Scoring
|
||||
|
||||
Labels are prioritized using multiple factors:
|
||||
|
||||
```typescript
|
||||
priority = connectionScore * 0.7 + centerScore * 0.3
|
||||
```
|
||||
|
||||
**Connection Score (70% weight):**
|
||||
- Normalized by neighbor count
|
||||
- `Math.min(node.neighbors.length / 20, 1)`
|
||||
- Highly connected nodes = more important
|
||||
|
||||
**Center Score (30% weight):**
|
||||
- Distance from viewport center
|
||||
- Closer to center = more important
|
||||
- Calculated as: `1 - distanceFromCenter / maxDistance`
|
||||
|
||||
**Result:**
|
||||
- Nodes with many connections in viewport center get highest priority
|
||||
- Peripheral or less-connected nodes deprioritized
|
||||
|
||||
### 3. Collision Detection
|
||||
|
||||
Two methods for different graph sizes:
|
||||
|
||||
#### Accurate Detection (< 500 nodes)
|
||||
```typescript
|
||||
function boundingBoxesOverlap(a: LabelBoundingBox, b: LabelBoundingBox): boolean {
|
||||
// Add 4px margin for breathing room
|
||||
const margin = 4
|
||||
|
||||
// Calculate bounds with margin
|
||||
const aLeft = a.x - a.width / 2 - margin
|
||||
const aRight = a.x + a.width / 2 + margin
|
||||
const aTop = a.y - margin
|
||||
const aBottom = a.y + a.height + margin
|
||||
|
||||
// Check for overlap
|
||||
return !(aRight < bLeft || aLeft > bRight || aBottom < bTop || aTop > bBottom)
|
||||
}
|
||||
```
|
||||
|
||||
#### Spatial Hashing (≥ 500 nodes)
|
||||
For large graphs, uses a grid-based approach:
|
||||
|
||||
```typescript
|
||||
const gridSize = 100 // 100px grid cells
|
||||
const occupiedCells = new Set<string>()
|
||||
|
||||
// Mark cell as occupied
|
||||
const cellKey = `${Math.floor(x / gridSize)},${Math.floor(y / gridSize)}`
|
||||
occupiedCells.add(cellKey)
|
||||
|
||||
// Also mark adjacent cells (9 cells total)
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
occupiedCells.add(`${cellX + dx},${cellY + dy}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- O(1) collision check instead of O(n²)
|
||||
- 10-100x faster for large graphs
|
||||
- Slight accuracy tradeoff acceptable for performance
|
||||
|
||||
### 4. Greedy Selection Algorithm
|
||||
|
||||
Labels are selected in order of priority:
|
||||
|
||||
```typescript
|
||||
// Step 1: Sort all labels by priority (highest first)
|
||||
boundingBoxes.sort((a, b) => b.priority - a.priority)
|
||||
|
||||
// Step 2: Always add selected/highlighted nodes first
|
||||
const placedBboxes = [...selectedBboxes, ...highlightedBboxes]
|
||||
visibleSet.add(selectedNodeIds)
|
||||
visibleSet.add(highlightedNodeIds)
|
||||
|
||||
// Step 3: Greedily add non-colliding labels
|
||||
for (const candidate of regularBboxes) {
|
||||
let hasCollision = false
|
||||
|
||||
for (const placed of placedBboxes) {
|
||||
if (boundingBoxesOverlap(candidate, placed)) {
|
||||
hasCollision = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCollision) {
|
||||
placedBboxes.push(candidate)
|
||||
visibleSet.add(candidate.nodeId)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Guarantees:**
|
||||
- ✅ Selected/highlighted nodes always have visible labels
|
||||
- ✅ No two labels ever overlap (4px minimum margin)
|
||||
- ✅ Higher priority nodes get labels before lower priority
|
||||
- ✅ Maximum label density without collisions
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Algorithm Selection
|
||||
```typescript
|
||||
if (nodes.length < 500) {
|
||||
// Use accurate collision detection
|
||||
selectLabelsWithDecluttering(...)
|
||||
} else {
|
||||
// Use spatial hashing
|
||||
selectLabelsWithDeclutteringFast(..., maxLabels: 200)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Throttling
|
||||
```typescript
|
||||
// Recalculate max once every 200ms during zoom
|
||||
if (now - lastLabelUpdateRef.current > 200) {
|
||||
lastLabelUpdateRef.current = now
|
||||
setVisibleLabels(selectLabelsWithDeclutteringFast(...))
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Zoom Hiding
|
||||
```typescript
|
||||
// Hide ALL labels during active zoom (< 300ms since last zoom change)
|
||||
if (isZoomingRef.current) {
|
||||
labelObjects.text.visible = false
|
||||
labelObjects.background.visible = false
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Frame Limiting
|
||||
```typescript
|
||||
// Still limit to 100 labels rendered per frame
|
||||
const MAX_LABELS_PER_FRAME = 100
|
||||
let renderedLabelCount = 0
|
||||
|
||||
// In render loop:
|
||||
if (renderedLabelCount >= MAX_LABELS_PER_FRAME) {
|
||||
label.visible = false
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
| Graph Size | Old System | New System | Improvement |
|
||||
|------------|-----------|------------|-------------|
|
||||
| 100 nodes | 60 FPS | 60 FPS | Same |
|
||||
| 500 nodes | 55 FPS (overlaps) | 60 FPS (clean) | +9% + No overlap |
|
||||
| 1,000 nodes | 50 FPS (overlaps) | 58 FPS (clean) | +16% + No overlap |
|
||||
| 5,000 nodes | 45 FPS (overlaps) | 55 FPS (clean) | +22% + No overlap |
|
||||
| 10,000 nodes | 35 FPS (overlaps) | 50 FPS (clean) | +43% + No overlap |
|
||||
|
||||
**Benefits:**
|
||||
- 🚀 **Better performance** (fewer labels = faster rendering)
|
||||
- 🎨 **Cleaner visuals** (no overlapping labels)
|
||||
- 🧠 **Smarter selection** (important nodes prioritized)
|
||||
- 📍 **Viewport-aware** (relevant labels shown)
|
||||
|
||||
## Customization
|
||||
|
||||
### Adjust Priority Weighting
|
||||
```typescript
|
||||
// In label-decluttering.ts > calculateNodePriority()
|
||||
priority = connectionScore * 0.7 + centerScore * 0.3
|
||||
// ^^^^ ^^^^
|
||||
// Importance Position
|
||||
|
||||
// Make position more important:
|
||||
priority = connectionScore * 0.5 + centerScore * 0.5
|
||||
|
||||
// Make connections more important:
|
||||
priority = connectionScore * 0.9 + centerScore * 0.1
|
||||
```
|
||||
|
||||
### Adjust Collision Margin
|
||||
```typescript
|
||||
// In boundingBoxesOverlap()
|
||||
const margin = 4 // Increase for more spacing
|
||||
```
|
||||
|
||||
### Adjust Grid Size (Large Graphs)
|
||||
```typescript
|
||||
// In selectLabelsWithDeclutteringFast()
|
||||
const gridSize = 100 // Smaller = more accurate, slower
|
||||
// Larger = faster, less accurate
|
||||
```
|
||||
|
||||
### Adjust Max Labels
|
||||
```typescript
|
||||
selectLabelsWithDeclutteringFast(
|
||||
...,
|
||||
maxLabels: 200 // Increase for more labels, decrease for performance
|
||||
)
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
The decluttering system is automatically used in `use-graph-renderer.ts`:
|
||||
|
||||
```typescript
|
||||
// On zoom change (throttled)
|
||||
const visibleLabels = selectLabelsWithDeclutteringFast(
|
||||
simulationNodes,
|
||||
forceSettings.nodeSize.value,
|
||||
zoomLevel,
|
||||
forceSettings.nodeLabelFontSize?.value ?? 50,
|
||||
viewport.width,
|
||||
viewport.height,
|
||||
-stage.position.x / stage.scale.x, // Camera X
|
||||
-stage.position.y / stage.scale.y, // Camera Y
|
||||
selectedNodeIds,
|
||||
highlightedNodeIds
|
||||
)
|
||||
setVisibleLabels(visibleLabels)
|
||||
|
||||
// In render loop
|
||||
const shouldShow = showLabelsLOD && visibleLabels.has(node.id) && !isZoomingRef.current
|
||||
updateNodeLabel(labelObjects, node, nodeSize, shouldShow, ...)
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
|
||||
1. **Dynamic repulsion**: Slightly adjust label positions to fit more
|
||||
2. **Multi-line labels**: Wrap long text instead of truncating
|
||||
3. **Leader lines**: Connect labels to nodes when repositioned
|
||||
4. **Semantic zoom**: Different label content at different zoom levels
|
||||
5. **Animation**: Smooth fade in/out when labels change
|
||||
6. **GPU acceleration**: Move collision detection to compute shader
|
||||
|
||||
## Comparison: Before vs After
|
||||
|
||||
### Before (Simple Selection)
|
||||
```
|
||||
● ●
|
||||
Long Label Name Another Label
|
||||
●
|
||||
Third Label
|
||||
|
||||
❌ Labels overlap
|
||||
❌ Hard to read
|
||||
❌ No spatial awareness
|
||||
❌ Random selection
|
||||
```
|
||||
|
||||
### After (Decluttering)
|
||||
```
|
||||
● ●
|
||||
Long Label Name
|
||||
|
||||
●
|
||||
Another Label
|
||||
|
||||
●
|
||||
|
||||
✅ No overlap (4px margin)
|
||||
✅ Clear and readable
|
||||
✅ Viewport-centered priority
|
||||
✅ Smart selection
|
||||
```
|
||||
|
||||
## Debug & Testing
|
||||
|
||||
### Visualize Bounding Boxes (Development)
|
||||
```typescript
|
||||
// In use-graph-renderer.ts render loop
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const bbox = calculateLabelBoundingBox(node, ...)
|
||||
if (bbox) {
|
||||
const debugRect = new Graphics()
|
||||
debugRect.rect(
|
||||
bbox.x - bbox.width / 2,
|
||||
bbox.y,
|
||||
bbox.width,
|
||||
bbox.height
|
||||
)
|
||||
debugRect.stroke({ width: 1, color: 0xff0000, alpha: 0.5 })
|
||||
debugContainer.addChild(debugRect)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Log Priority Scores
|
||||
```typescript
|
||||
const visibleLabels = selectLabelsWithDeclutteringFast(...)
|
||||
console.log('Visible labels:', visibleLabels.size)
|
||||
console.log('Top priority nodes:',
|
||||
boundingBoxes.slice(0, 10).map(b => ({
|
||||
id: b.nodeId,
|
||||
priority: b.priority
|
||||
}))
|
||||
)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `calculateLabelBoundingBox()`
|
||||
Calculates the exact screen-space bounding box for a node label.
|
||||
|
||||
**Parameters:**
|
||||
- `node: SimulationNode` - The node
|
||||
- `nodeSize: number` - Visual node size
|
||||
- `zoomLevel: number` - Current zoom
|
||||
- `fontSizeSetting: number` - Font size %
|
||||
- `viewportWidth: number` - Viewport width
|
||||
- `viewportHeight: number` - Viewport height
|
||||
- `cameraX: number` - Camera X position
|
||||
- `cameraY: number` - Camera Y position
|
||||
|
||||
**Returns:** `LabelBoundingBox | null`
|
||||
|
||||
### `boundingBoxesOverlap()`
|
||||
Checks if two bounding boxes collide (with 4px margin).
|
||||
|
||||
**Parameters:**
|
||||
- `a: LabelBoundingBox` - First box
|
||||
- `b: LabelBoundingBox` - Second box
|
||||
|
||||
**Returns:** `boolean` - True if overlap
|
||||
|
||||
### `selectLabelsWithDecluttering()`
|
||||
Accurate greedy selection with full collision detection.
|
||||
|
||||
**Use for:** < 500 nodes
|
||||
|
||||
**Returns:** `Set<string>` - Node IDs with visible labels
|
||||
|
||||
### `selectLabelsWithDeclutteringFast()`
|
||||
Fast selection with spatial hashing for large graphs.
|
||||
|
||||
**Use for:** ≥ 500 nodes
|
||||
|
||||
**Parameters:**
|
||||
- All same as `calculateLabelBoundingBox()`
|
||||
- `selectedNodeIds: Set<string>` - Always show
|
||||
- `highlightedNodeIds: Set<string>` - Always show
|
||||
- `maxLabels: number` - Max labels (default: 200)
|
||||
|
||||
**Returns:** `Set<string>` - Node IDs with visible labels
|
||||
|
||||
## License
|
||||
|
||||
Internal use only.
|
||||
@@ -1,194 +0,0 @@
|
||||
# Label Styling Guide
|
||||
|
||||
This document explains the label background styling system inspired by Cosmograph.
|
||||
|
||||
## Visual Appearance
|
||||
|
||||
### Node Labels with Backgrounds (Active Only)
|
||||
|
||||
**Important**: Backgrounds are shown ONLY for active nodes (hovered or selected) to maintain optimal performance.
|
||||
|
||||
```
|
||||
Normal Node: Active Node (hover/selected):
|
||||
● ●
|
||||
Node Label ┌──────────────┐
|
||||
│ Node Label │ ← Background appears
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
### Anatomy of a Label Background
|
||||
|
||||
```
|
||||
┌─────────────────────────────┐ ← Border (1px, #404040, 30% opacity)
|
||||
│ ┌─ Padding (6px horizontal) │
|
||||
│ │ │
|
||||
│ │ Text Content │ ← Text (#e0e0e0)
|
||||
│ │ │
|
||||
│ └─ Padding (3px vertical) │
|
||||
└─────────────────────────────┘
|
||||
Background (#1a1a1a, 92% opacity)
|
||||
Border Radius: 4px
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Default Values
|
||||
|
||||
```typescript
|
||||
NODE_LABEL_PADDING_X: 6 // 6px left and right
|
||||
NODE_LABEL_PADDING_Y: 3 // 3px top and bottom
|
||||
NODE_LABEL_BORDER_RADIUS: 4 // 4px rounded corners
|
||||
NODE_LABEL_BORDER_WIDTH: 1 // 1px border
|
||||
LABEL_BG_COLOR: 0x1a1a1a // Dark gray (#1a1a1a)
|
||||
LABEL_BG_BORDER_COLOR: 0x404040 // Medium gray (#404040)
|
||||
NODE_LABEL_BG_ALPHA: 0.92 // 92% opacity
|
||||
NODE_LABEL_BG_BORDER_ALPHA: 0.3 // 30% opacity
|
||||
```
|
||||
|
||||
### State-Based Styling
|
||||
|
||||
Labels and backgrounds adapt based on interaction state:
|
||||
|
||||
| State | Label Visible | Background Visible | Alpha |
|
||||
|-------|---------------|-------------------|-------|
|
||||
| **Normal** | ✅ (if in visible set) | ❌ No background | 1.0 |
|
||||
| **Hovered** | ✅ Always | ✅ **Background shown** | 1.0 |
|
||||
| **Selected** | ✅ Always | ✅ **Background shown** | 1.0 |
|
||||
| **Dimmed** | ✅ (if visible) | ❌ No background | 0.2 |
|
||||
|
||||
**Performance Note**: Backgrounds only render for active nodes (hovered/selected), reducing GPU load by ~99%.
|
||||
|
||||
## Customization Examples
|
||||
|
||||
### Lighter Background
|
||||
```typescript
|
||||
LABEL_BG_COLOR: 0x2a2a2a // Lighter gray
|
||||
NODE_LABEL_BG_ALPHA: 0.95 // More opaque
|
||||
```
|
||||
|
||||
### More Prominent Border
|
||||
```typescript
|
||||
LABEL_BG_BORDER_COLOR: 0x606060 // Lighter border
|
||||
NODE_LABEL_BG_BORDER_ALPHA: 0.5 // More visible
|
||||
NODE_LABEL_BORDER_WIDTH: 2 // Thicker
|
||||
```
|
||||
|
||||
### More Padding
|
||||
```typescript
|
||||
NODE_LABEL_PADDING_X: 8 // More horizontal space
|
||||
NODE_LABEL_PADDING_Y: 4 // More vertical space
|
||||
```
|
||||
|
||||
### Sharper Corners
|
||||
```typescript
|
||||
NODE_LABEL_BORDER_RADIUS: 2 // Less rounded
|
||||
```
|
||||
|
||||
### Glass Morphism Effect
|
||||
```typescript
|
||||
LABEL_BG_COLOR: 0x1a1a1a
|
||||
NODE_LABEL_BG_ALPHA: 0.7 // More transparent
|
||||
LABEL_BG_BORDER_COLOR: 0xffffff // White border
|
||||
NODE_LABEL_BG_BORDER_ALPHA: 0.2 // Subtle glow
|
||||
```
|
||||
|
||||
## Rendering Details
|
||||
|
||||
### Rendering Order
|
||||
|
||||
1. **Background graphics** (rendered first)
|
||||
2. **Text content** (rendered on top)
|
||||
|
||||
This ensures text is always readable and clickable.
|
||||
|
||||
### Dynamic Sizing
|
||||
|
||||
Backgrounds automatically adapt to text size:
|
||||
```typescript
|
||||
const textMetrics = label.getBounds()
|
||||
const bgWidth = textMetrics.width + PADDING_X * 2
|
||||
const bgHeight = textMetrics.height + PADDING_Y * 2
|
||||
```
|
||||
|
||||
### Performance Optimizations
|
||||
|
||||
✅ **Context-Aware Rendering**: Backgrounds shown ONLY for active nodes (hovered/selected)
|
||||
✅ **Dimension Estimation**: Fast approximation instead of expensive `getBounds()`
|
||||
✅ **Dirty Checking**: Only redraw when dimensions change
|
||||
✅ **Cached Calculations**: Store width to avoid recalculation
|
||||
✅ **Combined Draw Calls**: Fill and stroke in one operation
|
||||
✅ **GPU Compositing**: Alpha blending handled by hardware
|
||||
|
||||
**Result**: ~99% reduction in background rendering cost! 🚀
|
||||
|
||||
## Comparison with Cosmograph
|
||||
|
||||
| Feature | Cosmograph | This Implementation |
|
||||
|---------|-----------|---------------------|
|
||||
| Background | ✅ | ✅ |
|
||||
| Border | ✅ | ✅ |
|
||||
| Rounded corners | ✅ | ✅ |
|
||||
| Adaptive opacity | ✅ | ✅ |
|
||||
| Configurable | Limited | ✅ Fully |
|
||||
| Performance | High | High |
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Readability**: Keep background alpha > 0.8 for best readability
|
||||
2. **Contrast**: Ensure border color contrasts with background
|
||||
3. **Padding**: More padding = clearer labels but larger footprint
|
||||
4. **Border**: Keep border subtle (alpha 0.2-0.4) to avoid visual noise
|
||||
|
||||
## Examples in Code
|
||||
|
||||
### Basic Label
|
||||
```typescript
|
||||
const label = createNodeLabel("John Doe")
|
||||
// Returns: { text: Text, background: Graphics }
|
||||
```
|
||||
|
||||
### Update Label
|
||||
```typescript
|
||||
updateNodeLabel(
|
||||
labelObjects,
|
||||
node,
|
||||
nodeSize,
|
||||
shouldShow,
|
||||
zoomLevel,
|
||||
fontSizeSetting,
|
||||
isHighlighted,
|
||||
hasAnyHighlight
|
||||
)
|
||||
// Automatically updates both text and background
|
||||
```
|
||||
|
||||
### Custom Styling (in constants.ts)
|
||||
```typescript
|
||||
export const GRAPH_CONSTANTS = {
|
||||
// ... other constants
|
||||
|
||||
// Custom label style
|
||||
NODE_LABEL_PADDING_X: 8,
|
||||
NODE_LABEL_PADDING_Y: 5,
|
||||
NODE_LABEL_BORDER_RADIUS: 6,
|
||||
LABEL_BG_COLOR: 0x2a2a2a,
|
||||
NODE_LABEL_BG_ALPHA: 0.95,
|
||||
|
||||
// ... rest of constants
|
||||
}
|
||||
```
|
||||
|
||||
## Browser Rendering
|
||||
|
||||
Labels use Pixi.js Graphics API which:
|
||||
- Renders via WebGL for maximum performance
|
||||
- Supports anti-aliasing for smooth borders
|
||||
- Hardware accelerated alpha blending
|
||||
- Batches similar draw calls automatically
|
||||
|
||||
## Accessibility
|
||||
|
||||
While this is a canvas-based visualization (not DOM), consider:
|
||||
- High contrast mode support via color constants
|
||||
- Font size adjustments via zoom level
|
||||
- Clear backgrounds improve readability for all users
|
||||
@@ -1,358 +0,0 @@
|
||||
# WebGL Graph Viewer
|
||||
|
||||
A high-performance, modular graph visualization component built with Pixi.js (WebGL) and D3-force.
|
||||
|
||||
## Features
|
||||
|
||||
- **High Performance**: WebGL-based rendering with Pixi.js
|
||||
- **Physics Simulation**: D3-force for realistic node interactions
|
||||
- **Modular Architecture**: Clean separation of concerns
|
||||
- **Type Safe**: Full TypeScript support
|
||||
- **Customizable**: Extensive configuration options
|
||||
- **Interactive**: Zoom, pan, drag, hover, and click interactions
|
||||
- **Smart Decluttering**: Intelligent label selection with collision detection
|
||||
- **Progressive Disclosure**: Priority-based label visibility
|
||||
- **Icon Support**: SVG icon rendering with texture caching
|
||||
- **Styled Labels**: Cosmograph-inspired label backgrounds with borders
|
||||
- **Context-Aware**: Edge labels only appear when nodes are active
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
webgl/
|
||||
├── index.tsx # Main component
|
||||
├── constants.ts # Configuration constants
|
||||
├── types/
|
||||
│ └── graph.types.ts # TypeScript types
|
||||
├── hooks/
|
||||
│ ├── use-pixi-app.ts # Pixi.js app management
|
||||
│ ├── use-force-simulation.ts # D3-force simulation
|
||||
│ ├── use-graph-interactions.ts # User interactions
|
||||
│ ├── use-zoom-pan.ts # Zoom/pan controls
|
||||
│ └── use-graph-renderer.ts # Main rendering logic
|
||||
├── renderers/
|
||||
│ ├── node-renderer.ts # Node rendering
|
||||
│ ├── edge-renderer.ts # Edge rendering
|
||||
│ └── label-renderer.ts # Label rendering
|
||||
└── utils/
|
||||
├── texture-cache.ts # Icon texture caching
|
||||
├── visibility-calculator.ts # LOD thresholds
|
||||
├── label-decluttering.ts # Smart label selection
|
||||
└── color-utils.ts # Color utilities
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example
|
||||
|
||||
```tsx
|
||||
import WebGLGraphViewer from '@/components/graphs/webgl'
|
||||
|
||||
function MyGraph() {
|
||||
const nodes = [
|
||||
{ id: '1', data: { label: 'Node 1', type: 'person' } },
|
||||
{ id: '2', data: { label: 'Node 2', type: 'company' } },
|
||||
]
|
||||
|
||||
const edges = [
|
||||
{ source: '1', target: '2', label: 'works at' },
|
||||
]
|
||||
|
||||
return (
|
||||
<WebGLGraphViewer
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodeClick={(node) => console.log('Clicked:', node)}
|
||||
showIcons={true}
|
||||
showLabels={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### With Custom Handlers
|
||||
|
||||
```tsx
|
||||
<WebGLGraphViewer
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodeClick={(node, event) => {
|
||||
if (event.ctrlKey) {
|
||||
// Multi-select
|
||||
} else {
|
||||
// Single select
|
||||
}
|
||||
}}
|
||||
onNodeRightClick={(node, event) => {
|
||||
// Show context menu
|
||||
}}
|
||||
onBackgroundClick={() => {
|
||||
// Deselect all
|
||||
}}
|
||||
className="my-graph"
|
||||
style={{ width: '100%', height: '600px' }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `nodes` | `GraphNode[]` | Required | Array of graph nodes |
|
||||
| `edges` | `GraphEdge[]` | Required | Array of graph edges |
|
||||
| `className` | `string` | `''` | CSS class name |
|
||||
| `style` | `React.CSSProperties` | `undefined` | Inline styles |
|
||||
| `onNodeClick` | `(node, event) => void` | `undefined` | Node click handler |
|
||||
| `onNodeRightClick` | `(node, event) => void` | `undefined` | Node right-click handler |
|
||||
| `onBackgroundClick` | `() => void` | `undefined` | Background click handler |
|
||||
| `showIcons` | `boolean` | `true` | Show node icons |
|
||||
| `showLabels` | `boolean` | `true` | Show node/edge labels |
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### 1. Smart Label Decluttering
|
||||
|
||||
**Intelligent label selection with collision detection**
|
||||
|
||||
The graph uses an advanced decluttering algorithm that:
|
||||
- ✅ **Prevents overlap**: Labels never collide (4px minimum margin)
|
||||
- ✅ **Priority-based**: Important nodes (highly connected, viewport-centered) get labels first
|
||||
- ✅ **Collision detection**: Real bounding box calculations
|
||||
- ✅ **Spatial hashing**: O(1) collision checks for large graphs (10,000+ nodes)
|
||||
- ✅ **Selection-aware**: Selected/highlighted nodes always show labels
|
||||
|
||||
**How it works:**
|
||||
|
||||
```typescript
|
||||
// Combines multiple factors for smart selection
|
||||
priority = connectionScore * 0.7 + centerScore * 0.3
|
||||
|
||||
// Greedy algorithm:
|
||||
// 1. Sort nodes by priority (highest first)
|
||||
// 2. Always add selected/highlighted nodes
|
||||
// 3. Add remaining nodes if they don't collide
|
||||
```
|
||||
|
||||
**Performance:**
|
||||
- Small graphs (< 500 nodes): Accurate collision detection
|
||||
- Large graphs (≥ 500 nodes): Fast spatial hashing
|
||||
- 200 max labels by default (configurable)
|
||||
|
||||
See [DECLUTTERING.md](./DECLUTTERING.md) for detailed documentation.
|
||||
|
||||
### 2. Level of Details (LOD) System
|
||||
Automatically adjusts visual complexity based on zoom level:
|
||||
|
||||
| Zoom Level | LOD | Features Visible |
|
||||
|------------|-----|------------------|
|
||||
| < 0.5 | **Minimal** | Nodes + Edges only |
|
||||
| 0.5 - 0.6 | **Low** | + Basic node labels |
|
||||
| 0.6 - 1.2 | **Medium** | + Icons |
|
||||
| 1.2 - 1.5 | **High** | + Edge labels* |
|
||||
| > 1.5 | **Ultra** | All features at max quality |
|
||||
|
||||
**Edge labels are context-aware**: They only appear when at least one of their connected nodes is active (hovered or selected), reducing visual clutter and improving performance.
|
||||
|
||||
```ts
|
||||
import { getLODLevel, shouldShowNodeIcons } from './exports'
|
||||
|
||||
const lodLevel = getLODLevel(zoomLevel) // 'minimal', 'low', 'medium', 'high'
|
||||
const showIcons = shouldShowNodeIcons(zoomLevel) // boolean
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- 🚀 **60 FPS maintained** even with 10,000+ nodes when zoomed out
|
||||
- 💾 **Reduced GPU load** by hiding unnecessary details
|
||||
- ⚡ **Instant zoom** response without lag
|
||||
|
||||
### 3. Texture Caching
|
||||
Icons are loaded once and cached globally:
|
||||
```ts
|
||||
import { textureCache } from './utils/texture-cache'
|
||||
|
||||
// Preload textures
|
||||
await textureCache.preloadBatch(['person', 'company'])
|
||||
|
||||
// Get cached texture
|
||||
const texture = textureCache.get('person')
|
||||
```
|
||||
|
||||
### 4. Batch Rendering
|
||||
Edges are grouped by style and rendered in batches:
|
||||
- Highlighted edges (orange)
|
||||
- Dimmed edges (low opacity)
|
||||
- Default edges (normal)
|
||||
|
||||
### 5. Progressive Label Disclosure (Deprecated)
|
||||
|
||||
**Note:** Progressive disclosure is now handled by the Smart Decluttering system (see above).
|
||||
|
||||
|
||||
Labels appear based on:
|
||||
- Node importance (connection count)
|
||||
- Current zoom level
|
||||
- Selection/highlight state
|
||||
- **LOD thresholds**
|
||||
|
||||
**Smart Edge Labels:**
|
||||
- Edge labels are **context-aware** and only appear when their nodes are active
|
||||
- Active = hovered, selected, or highlighted
|
||||
- This reduces visual clutter by 90% in typical use cases
|
||||
- Performance improvement: ~50% fewer labels rendered at any time
|
||||
|
||||
### 6. Manual Rendering
|
||||
Pixi.js app uses manual rendering (`autoStart: false`) for better control.
|
||||
|
||||
### 7. Optimized Updates
|
||||
- Refs used for values that don't need re-renders
|
||||
- Memoized calculations
|
||||
- Debounced resize handler
|
||||
- **LOD-based early returns** (skip calculations for invisible elements)
|
||||
|
||||
## Customization
|
||||
|
||||
### Constants
|
||||
Edit `constants.ts` to customize:
|
||||
- Node sizes and colors
|
||||
- Edge widths and colors
|
||||
- Zoom limits
|
||||
- Label visibility thresholds
|
||||
- **LOD thresholds** (when to show/hide features)
|
||||
- **Label styling** (background, border, padding)
|
||||
- Animation durations
|
||||
|
||||
**LOD Configuration:**
|
||||
```ts
|
||||
// In constants.ts
|
||||
LOD_ICON_MIN_ZOOM: 0.6, // Show icons when zoom >= 0.6
|
||||
LOD_NODE_LABEL_MIN_ZOOM: 0.5, // Show node labels when zoom >= 0.5
|
||||
LOD_EDGE_LABEL_MIN_ZOOM: 1.2, // Enable edge labels when zoom >= 1.2
|
||||
// (only shown when nodes are active)
|
||||
LOD_HIGH_DETAIL_ZOOM: 1.5, // High detail mode when zoom >= 1.5
|
||||
```
|
||||
|
||||
**Label Styling:**
|
||||
```ts
|
||||
// Node label backgrounds (Cosmograph-inspired)
|
||||
NODE_LABEL_PADDING_X: 6, // Horizontal padding
|
||||
NODE_LABEL_PADDING_Y: 3, // Vertical padding
|
||||
NODE_LABEL_BORDER_RADIUS: 4, // Rounded corners
|
||||
NODE_LABEL_BORDER_WIDTH: 1, // Border thickness
|
||||
LABEL_BG_COLOR: 0x1a1a1a, // Background color
|
||||
LABEL_BG_BORDER_COLOR: 0x404040, // Border color
|
||||
NODE_LABEL_BG_ALPHA: 0.92, // Background opacity
|
||||
NODE_LABEL_BG_BORDER_ALPHA: 0.3, // Border opacity
|
||||
```
|
||||
|
||||
**Edge Label Behavior:**
|
||||
Edge labels are now **context-aware** - they only appear when:
|
||||
1. Zoom level >= `LOD_EDGE_LABEL_MIN_ZOOM` (LOD check)
|
||||
2. At least one connected node is **active** (hovered or selected)
|
||||
|
||||
This dramatically reduces visual clutter while keeping relevant information visible.
|
||||
|
||||
### Renderers
|
||||
Modify renderer functions to change visual appearance:
|
||||
- `renderNode()` - Node shapes and colors
|
||||
- `renderEdges()` - Edge styles
|
||||
- `updateNodeLabel()` - Label positioning and styling
|
||||
|
||||
### Hooks
|
||||
Extend or replace hooks for custom behavior:
|
||||
- `useForceSimulation()` - Custom physics
|
||||
- `useGraphInteractions()` - Custom interactions
|
||||
- `useZoomPan()` - Custom zoom/pan behavior
|
||||
|
||||
## Debugging
|
||||
|
||||
### LOD Indicator
|
||||
Enable the LOD indicator to see current zoom level and active LOD mode:
|
||||
|
||||
```tsx
|
||||
// In index.tsx, uncomment the import and component:
|
||||
import { LODIndicator } from './components/lod-indicator'
|
||||
|
||||
// In the return statement:
|
||||
<LODIndicator
|
||||
zoomLevel={currentZoom}
|
||||
visible={process.env.NODE_ENV === 'development'}
|
||||
/>
|
||||
```
|
||||
|
||||
The indicator shows:
|
||||
- Current LOD level (color-coded)
|
||||
- Description of visible features
|
||||
- Exact zoom value
|
||||
|
||||
**Color Legend:**
|
||||
- 🔴 Red: Minimal (nodes + edges only)
|
||||
- 🟠 Orange: Low (+ basic labels)
|
||||
- 🟡 Yellow: Medium (+ icons)
|
||||
- 🟢 Green: High (all features)
|
||||
|
||||
### Performance Profiling
|
||||
```tsx
|
||||
// Use Chrome DevTools Performance tab
|
||||
// Look for "render" calls in the flame graph
|
||||
// LOD should reduce GPU time when zoomed out
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
npm test -- webgl
|
||||
```
|
||||
|
||||
### Visual Testing
|
||||
```tsx
|
||||
// In Storybook
|
||||
import WebGLGraphViewer from '@/components/graphs/webgl'
|
||||
|
||||
export default {
|
||||
title: 'Graph/WebGLGraphViewer',
|
||||
component: WebGLGraphViewer,
|
||||
}
|
||||
```
|
||||
|
||||
### LOD Testing
|
||||
1. Zoom out completely - should see minimal LOD (no icons, no labels)
|
||||
2. Zoom to 0.6x - icons should appear
|
||||
3. Zoom to 1.2x - edge labels **enabled** (but only visible on hover/select)
|
||||
4. Hover over a node - edge labels for connected edges should appear
|
||||
5. Select a node - edge labels remain visible
|
||||
6. Deselect/unhover - edge labels disappear
|
||||
7. Use LODIndicator component to verify thresholds
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
Tested with LOD system:
|
||||
- **1,000 nodes**: 60 FPS (all zoom levels)
|
||||
- **5,000 nodes**: 60 FPS (zoomed out), 50-60 FPS (zoomed in)
|
||||
- **10,000 nodes**: 55-60 FPS (zoomed out), 40-50 FPS (zoomed in)
|
||||
- **50,000 nodes**: 45-60 FPS (zoomed out), 25-35 FPS (zoomed in)
|
||||
|
||||
Performance depends on:
|
||||
- Device capabilities
|
||||
- **Zoom level (LOD)** ⭐ Major factor
|
||||
- Number of visible labels
|
||||
- Active interactions
|
||||
|
||||
**LOD Impact:**
|
||||
- Zoomed out (LOD: minimal): **+80% performance**
|
||||
- Medium zoom (LOD: medium): **+40% performance**
|
||||
- Fully zoomed in (LOD: high): Baseline performance
|
||||
- **Context-aware edge labels**: **+50% fewer labels** rendered on average
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- Chrome 90+
|
||||
- Firefox 88+
|
||||
- Safari 14+
|
||||
- Edge 90+
|
||||
|
||||
Requires WebGL support.
|
||||
|
||||
## License
|
||||
|
||||
Internal use only.
|
||||
Reference in New Issue
Block a user