Skip to content

Highlight System for Graph Components #118

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
});

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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