Skip to content

Highlight System for Graph ComponentsΒ #118

@draedful

Description

@draedful

WIP

🎯 Problem Statement

Currently, @gravity-ui/graph lacks a unified system for programmatically managing component states (highlighting, focusing, prioritizing). Developers need to manually implement that.

πŸ’‘ Proposed Solution

Implement a Universal Highlight System that provides clean programming interfaces for managing component states

Core Principles

πŸ“ Detailed Requirements

1. Two Fundamentally Different Highlight Modes

🎯 Mode 1: highlight() - Selective Highlighting

// ONLY highlights specified entities, others remain UNCHANGED
graph.highlight({
  block: ['user-1', 'user-2'],
  connection: ['critical-link']
});

// Result:
// user-1: HighlightVisualMode.Highlight (20)
// user-2: HighlightVisualMode.Highlight (20) 
// critical-link: HighlightVisualMode.Highlight (20)
// ALL OTHER entities: undefined (normal state - untouched)

πŸ” Mode 2: focus() - Spotlight with Dimming

// Highlights targets AND lowlights EVERYTHING ELSE
graph.focus({
  block: ['important-node'],
  connection: ['main-flow']
});

// Result:
// important-node: HighlightVisualMode.Highlight (20)
// main-flow: HighlightVisualMode.Highlight (20)
// ALL OTHER entities: HighlightVisualMode.Lowlight (10) ← KEY DIFFERENCE!

πŸ”„ Mode Management

// Clear all states - everything returns to normal
graph.clearHighlight();

// Result: ALL entities = undefined (normal state)

⚑ Critical Differences Explained

Aspect highlight() focus()
Target entities Highlight (20) Highlight (20)
Non-target entities undefined (unchanged) Lowlight (10) (dimmed)
Use case "Show important items" "Focus attention, hide noise"
Performance βœ… Fast (only targets change) ⚠️ Slower (all entities change)
Visual impact 🎯 Selective emphasis πŸ” Dramatic contrast

2. Universal ID System

// Core prefixes (built-in library support)
block:user-123              // Blocks
connection:link-456         // Connections  
anchor:user-123:output      // Anchors

// User extensions (unlimited extensibility)
group:team-alpha           // Custom groups
layer:background           // Custom layers
plugin:my-element          // Plugin components
myApp:special-feature      // Application-specific

3. Real-World Usage Scenarios

🎯 When to use highlight()

// βœ… Show search results (don't hide other data)
const searchResults = ['user-123', 'user-456'];
graph.highlight({ block: searchResults });

// βœ… Mark validation errors (keep context visible)
const invalidNodes = validateGraph();
graph.highlight({ block: invalidNodes.map(n => n.id) });

// βœ… Show related elements (preserve workflow context)
graph.highlight({
  block: ['selected-node'],
  connection: getConnectedEdges('selected-node'),
  anchor: getRelatedAnchors('selected-node')
});

πŸ” When to use focus()

// βœ… Critical path analysis (hide distractions)
const criticalPath = findCriticalPath();
graph.focus({ 
  block: criticalPath.nodes,
  connection: criticalPath.edges 
});

// βœ… Debug specific workflow (isolate problem area) 
graph.focus({
  block: ['error-source'],
  connection: ['failed-connection']
});

// βœ… Presentation mode (spotlight key elements)
graph.focus({ block: ['demo-node-1', 'demo-node-2'] });

⚑ Performance Impact

// highlight() - FAST: Only changes target entities
graph.highlight({ block: ['node-1'] });
// Changes: 1 entity state
// Performance: O(targets)

// focus() - SLOWER: Changes ALL entities in graph
graph.focus({ block: ['node-1'] });  
// Changes: ALL entity states (targets + non-targets)
// Performance: O(all entities)

// For large graphs (1000+ entities), consider highlight() for frequent operations

4. Component Integration

// Extends 
class GraphComponent {

  // 1. Define unique ID
  public abstract getHighlightId(): string {
    return `myPlugin:${this.id}`;
  }

  afterInit() {
    this.onSignal(
        computed(() => this.store.graph.highlightService.getEntityHighlightMode(this.getHighlightId())),
        (mode) => this.setState({ highlightMode: mode }),
    );
  }
}

class BaseConnection extends GraphComponent {
  public getHighlightId(): string {
    return `connection:${this.state.id}`;
  }
}

class Block extends GraphComponent {
  public getHighlightId(): string {
    return `block:${this.state.id}`;
  }
}

class Anchor extends GraphComponent {
  public getHighlightId(): string {
    return `anchor:${this.id}`;
  }
}

5. Internal State Management Logic

enum HighlightVisualMode {
  Highlight = 20,  // High priority state
  Lowlight = 10,   // Low priority state
  // Normal = undefined (neutral state)
}

type HighlightServiceMode = 'highlight' | 'focus';

// Core logic - how getEntityHighlightMode() works differently
class HighlightService {
  public getEntityHighlightMode(entityId: string): HighlightVisualMode | undefined {
    const state = this.$state.value;
    if (!state.active) return undefined;

    const isTargeted = state.entities.has(entityId);
    
    if (state.mode === 'highlight') {
      // ✨ HIGHLIGHT MODE: Only targets get state, others stay normal
      return isTargeted ? HighlightVisualMode.Highlight : undefined;
      
    } else { // focus mode
      // πŸ”₯ FOCUS MODE: Targets get highlighted, EVERYONE ELSE gets lowlighted  
      return isTargeted ? HighlightVisualMode.Highlight : HighlightVisualMode.Lowlight;
    }
  }
}

πŸ”„ Mode Switching Behavior

// Starting state: everything normal (undefined)
// Initial: all entities = undefined

graph.highlight({ block: ['A', 'B'] });
// Result: A=Highlight(20), B=Highlight(20), others=undefined

graph.focus({ block: ['C'] }); // ← REPLACES previous state!
// Result: C=Highlight(20), A=Lowlight(10), B=Lowlight(10), others=Lowlight(10)

graph.highlight({ connection: ['X'] }); // ← REPLACES focus!
// Result: X=Highlight(20), A=undefined, B=undefined, C=undefined, others=undefined

6. Event System

// React to highlight changes
graph.on('highlight-changed', (event) => {
  console.log('Mode:', event.mode);           // 'highlight' | 'focus' | undefined
  console.log('Entities:', event.entities);   // ['block:id1', 'connection:id2']
  console.log('Previous:', event.previous);   // Previous state
});

πŸš€ Usage Examples

Side-by-Side Mode Comparison

Scenario: Graph with 5 nodes (A, B, C, D, E)

// BEFORE: All nodes normal
// A=undefined, B=undefined, C=undefined, D=undefined, E=undefined

// =====================================================
// 🎯 HIGHLIGHT MODE - Selective emphasis
// =====================================================
graph.highlight({ block: ['A', 'B'] });

// RESULT: Only targets change, others UNTOUCHED
// A=Highlight(20) ← highlighted  
// B=Highlight(20) ← highlighted
// C=undefined     ← normal (unchanged)
// D=undefined     ← normal (unchanged)  
// E=undefined     ← normal (unchanged)
// =====================================================
// πŸ” FOCUS MODE - Dramatic spotlight effect
// =====================================================
graph.focus({ block: ['A', 'B'] });

// RESULT: Targets highlighted, EVERYTHING ELSE dimmed
// A=Highlight(20) ← highlighted
// B=Highlight(20) ← highlighted  
// C=Lowlight(10)  ← dimmed ⚠️
// D=Lowlight(10)  ← dimmed ⚠️
// E=Lowlight(10)  ← dimmed ⚠️

Real-World Impact

Graph Size highlight() Changes focus() Changes
10 entities 2 entities 10 entities (all!)
100 entities 5 entities 100 entities (all!)
1000 entities 10 entities 1000 entities (all!)

πŸ“Š Performance: highlight() scales with targets, focus() scales with total graph size!

Visual State Diagram

INITIAL STATE: All entities normal
β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β”
β”‚ A β”‚ β”‚ B β”‚ β”‚ C β”‚ β”‚ D β”‚ β”‚ E β”‚  
β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜
  βšͺ    βšͺ    βšͺ    βšͺ    βšͺ     (all undefined)

graph.highlight({ block: ['A', 'B'] });
β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β”
β”‚ A β”‚ β”‚ B β”‚ β”‚ C β”‚ β”‚ D β”‚ β”‚ E β”‚  
β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜
  🟒    🟒    βšͺ    βšͺ    βšͺ     ← Only targets change!

graph.focus({ block: ['A', 'B'] });
β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β” β”Œβ”€β”€β”€β”
β”‚ A β”‚ β”‚ B β”‚ β”‚ C β”‚ β”‚ D β”‚ β”‚ E β”‚  
β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜ β””β”€β”€β”€β”˜
  🟒    🟒    πŸ”΄    πŸ”΄    πŸ”΄     ← ALL entities change!

Legend:
🟒 Highlight(20)  πŸ”΄ Lowlight(10)  βšͺ Normal(undefined)

External Integration

// Analytics integration
graph.on('highlight-changed', (event) => {
  analytics.track('graph_highlight', {
    mode: event.mode,
    entityCount: event.entities.length
  });
  event.preventDefault(); // if you want to prevent default behavior
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions