or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

behavior-system.mdcontrollers.mdgraph-management.mdindex.mditem-management.mdshape-system.mdutilities.md
tile.json

controllers.mddocs/

Controllers

Abstract controller classes for implementing custom layout and event handling systems. Controllers provide the foundation for extending G6-core with custom rendering, layout algorithms, and event processing capabilities.

Capabilities

AbstractLayout Controller

Base class for implementing custom layout algorithms.

/**
 * Abstract layout controller providing layout algorithm framework
 * Must be extended to implement specific layout strategies
 */
abstract class AbstractLayout {
  graph: IAbstractGraph;
  destroyed: boolean;
  
  constructor(graph: IAbstractGraph);
  
  // Abstract methods that must be implemented
  abstract getLayoutType(): string;
  abstract layout(success?: () => void): boolean;
  
  // Implemented methods
  init(): void;
  getType(): string;
  refreshLayout(): void;
  destroy(): void;
  getData(): GraphData;
  getNodeSize(node: INode): [number, number] | number;
}

Usage Example:

import { AbstractLayout } from "@antv/g6-core";

class CustomLayout extends AbstractLayout {
  getLayoutType(): string {
    return 'custom-layout';
  }
  
  layout(success?: () => void): boolean {
    const data = this.getData();
    const nodes = data.nodes || [];
    
    // Implement custom layout algorithm
    nodes.forEach((node, index) => {
      node.x = index * 100;
      node.y = Math.sin(index) * 50;
    });
    
    // Apply positions to graph
    nodes.forEach(nodeData => {
      const node = this.graph.findById(nodeData.id);
      if (node) {
        node.updatePosition({ x: nodeData.x, y: nodeData.y });
      }
    });
    
    if (success) success();
    return true;
  }
}

// Usage in graph implementation
class MyGraph extends AbstractGraph {
  protected initLayoutController() {
    this.layoutController = new CustomLayout(this);
  }
}

AbstractEvent Controller

Base class for implementing custom event handling systems.

/**
 * Abstract event controller providing event management framework
 * Must be extended to implement specific event handling strategies
 */
abstract class AbstractEvent {
  graph: IAbstractGraph;
  destroyed: boolean;
  
  constructor(graph: IAbstractGraph);
  
  // Abstract methods that must be implemented
  abstract initEvents(): void;
  abstract destroy(): void;
}

Usage Example:

import { AbstractEvent } from "@antv/g6-core";

class CustomEventController extends AbstractEvent {
  private eventListeners: Map<string, Function[]> = new Map();
  
  initEvents(): void {
    const canvas = this.graph.getContainer();
    
    // Initialize mouse events
    this.bindEvent(canvas, 'mousedown', this.onMouseDown.bind(this));
    this.bindEvent(canvas, 'mousemove', this.onMouseMove.bind(this));
    this.bindEvent(canvas, 'mouseup', this.onMouseUp.bind(this));
    
    // Initialize keyboard events
    this.bindEvent(document, 'keydown', this.onKeyDown.bind(this));
    this.bindEvent(document, 'keyup', this.onKeyUp.bind(this));
  }
  
  private bindEvent(target: EventTarget, event: string, handler: Function) {
    const listeners = this.eventListeners.get(event) || [];
    listeners.push(handler);
    this.eventListeners.set(event, listeners);
    
    target.addEventListener(event, handler as EventListener);
  }
  
  private onMouseDown(e: MouseEvent) {
    const point = this.graph.getPointByClient(e.clientX, e.clientY);
    // Handle mouse down with custom logic
    console.log('Mouse down at:', point);
  }
  
  private onMouseMove(e: MouseEvent) {
    const point = this.graph.getPointByClient(e.clientX, e.clientY);
    // Handle mouse move with custom logic
  }
  
  private onMouseUp(e: MouseEvent) {
    const point = this.graph.getPointByClient(e.clientX, e.clientY);
    // Handle mouse up with custom logic
    console.log('Mouse up at:', point);
  }
  
  private onKeyDown(e: KeyboardEvent) {
    // Handle keyboard input
    if (e.key === 'Delete') {
      // Delete selected items
      const selectedItems = this.graph.findAllByState('node', 'selected');
      selectedItems.forEach(item => this.graph.removeItem(item));
    }
  }
  
  private onKeyUp(e: KeyboardEvent) {
    // Handle key release
  }
  
  destroy(): void {
    // Clean up all event listeners
    this.eventListeners.forEach((listeners, event) => {
      listeners.forEach(listener => {
        document.removeEventListener(event, listener as EventListener);
        this.graph.getContainer().removeEventListener(event, listener as EventListener);
      });
    });
    this.eventListeners.clear();
    this.destroyed = true;
  }
}

// Usage in graph implementation
class MyGraph extends AbstractGraph {
  protected initEventController() {
    this.eventController = new CustomEventController(this);
  }
}

Layout Controller Methods

Detailed methods available in layout controllers.

/**
 * Initialize layout controller
 * Called automatically during graph construction
 */
init(): void;

/**
 * Get layout type identifier
 * @returns Layout type string
 */
getType(): string;

/**
 * Get layout algorithm identifier (abstract)
 * Must return unique string identifying the layout algorithm
 * @returns Layout algorithm identifier
 */
abstract getLayoutType(): string;

/**
 * Execute layout algorithm (abstract)
 * Must implement the actual layout calculation and positioning
 * @param success - Callback function called on successful completion
 * @returns True if layout executed successfully
 */
abstract layout(success?: () => void): boolean;

/**
 * Refresh layout with current data
 * Recalculates layout using existing configuration
 */
refreshLayout(): void;

/**
 * Get current graph data for layout calculations
 * @returns Graph data with nodes, edges, and combos
 */
getData(): GraphData;

/**
 * Get node size for layout calculations
 * @param node - Node instance
 * @returns Node size as [width, height] or single number
 */
getNodeSize(node: INode): [number, number] | number;

/**
 * Clean up layout controller resources
 * Removes references and prepares for garbage collection
 */
destroy(): void;

Event Controller Methods

Detailed methods for event controller implementation.

/**
 * Initialize event system (abstract)
 * Must set up all necessary event listeners and handlers
 * Called automatically during graph construction
 */
abstract initEvents(): void;

/**
 * Clean up event controller (abstract)
 * Must remove all event listeners and clean up resources
 * Called during graph destruction
 */
abstract destroy(): void;

/**
 * Check if controller has been destroyed
 * @returns True if controller is destroyed
 */
isDestroyed(): boolean;

Implementation Guidelines

Layout Controller Best Practices

  1. Data Processing: Always use getData() to get current graph data
  2. Node Sizing: Use getNodeSize() for consistent node size calculations
  3. Position Updates: Update node positions through the graph instance
  4. Async Support: Support async layout calculations with success callbacks
  5. Performance: Implement efficient algorithms for large graphs
  6. Configuration: Support layout-specific configuration options

Event Controller Best Practices

  1. Event Cleanup: Always remove event listeners in destroy()
  2. Memory Management: Avoid memory leaks by cleaning up references
  3. Event Delegation: Use event delegation for dynamic content
  4. Performance: Debounce high-frequency events like mousemove
  5. Cross-browser: Handle browser differences in event handling
  6. Accessibility: Support keyboard navigation and screen readers

Advanced Examples

Force-Directed Layout

class ForceDirectedLayout extends AbstractLayout {
  private config = {
    center: [0, 0],
    nodeStrength: -30,
    edgeStrength: 0.1,
    iterations: 300,
    alpha: 0.3,
    alphaDecay: 0.01
  };
  
  getLayoutType(): string {
    return 'force-directed';
  }
  
  layout(success?: () => void): boolean {
    const data = this.getData();
    const nodes = data.nodes || [];
    const edges = data.edges || [];
    
    // Initialize positions randomly if not set
    nodes.forEach(node => {
      if (node.x === undefined) node.x = Math.random() * 500;
      if (node.y === undefined) node.y = Math.random() * 500;
    });
    
    // Run force simulation
    for (let i = 0; i < this.config.iterations; i++) {
      this.simulateForces(nodes, edges);
      this.config.alpha *= this.config.alphaDecay;
    }
    
    // Apply final positions
    this.applyPositions(nodes);
    
    if (success) success();
    return true;
  }
  
  private simulateForces(nodes: NodeConfig[], edges: EdgeConfig[]) {
    // Implement force calculations
    // - Repulsion between nodes
    // - Attraction along edges
    // - Center force
  }
  
  private applyPositions(nodes: NodeConfig[]) {
    nodes.forEach(nodeData => {
      const node = this.graph.findById(nodeData.id);
      if (node) {
        node.updatePosition({ x: nodeData.x!, y: nodeData.y! });
      }
    });
  }
}

Advanced Event Controller

class AdvancedEventController extends AbstractEvent {
  private dragState: {
    dragging: boolean;
    target: Item | null;
    startPoint: Point | null;
  } = { dragging: false, target: null, startPoint: null };
  
  initEvents(): void {
    const container = this.graph.getContainer();
    
    // Mouse events
    container.addEventListener('mousedown', this.onMouseDown.bind(this));
    container.addEventListener('mousemove', this.onMouseMove.bind(this));
    container.addEventListener('mouseup', this.onMouseUp.bind(this));
    
    // Touch events for mobile
    container.addEventListener('touchstart', this.onTouchStart.bind(this));
    container.addEventListener('touchmove', this.onTouchMove.bind(this));
    container.addEventListener('touchend', this.onTouchEnd.bind(this));
    
    // Keyboard events
    document.addEventListener('keydown', this.onKeyDown.bind(this));
    
    // Wheel events for zooming
    container.addEventListener('wheel', this.onWheel.bind(this));
  }
  
  private onMouseDown(e: MouseEvent) {
    const point = this.graph.getPointByClient(e.clientX, e.clientY);
    const target = this.getTargetItem(point);
    
    if (target) {
      this.dragState = {
        dragging: true,
        target,
        startPoint: point
      };
      
      // Emit custom events
      this.graph.emit('item:dragstart', {
        item: target,
        canvasX: point.x,
        canvasY: point.y,
        clientX: e.clientX,
        clientY: e.clientY,
        x: point.x,
        y: point.y,
        wheelDelta: 0,
        target: target.getKeyShape(),
        type: 'dragstart'
      } as IG6GraphEvent);
    }
  }
  
  private onMouseMove(e: MouseEvent) {
    const point = this.graph.getPointByClient(e.clientX, e.clientY);
    
    if (this.dragState.dragging && this.dragState.target) {
      // Update item position
      this.dragState.target.updatePosition(point);
      
      this.graph.emit('item:drag', {
        item: this.dragState.target,
        canvasX: point.x,
        canvasY: point.y,
        clientX: e.clientX,
        clientY: e.clientY,
        x: point.x,
        y: point.y,
        wheelDelta: 0,
        target: this.dragState.target.getKeyShape(),
        type: 'drag'
      } as IG6GraphEvent);
    }
  }
  
  private onMouseUp(e: MouseEvent) {
    if (this.dragState.dragging && this.dragState.target) {
      const point = this.graph.getPointByClient(e.clientX, e.clientY);
      
      this.graph.emit('item:dragend', {
        item: this.dragState.target,
        canvasX: point.x,
        canvasY: point.y,
        clientX: e.clientX,
        clientY: e.clientY,
        x: point.x,
        y: point.y,
        wheelDelta: 0,
        target: this.dragState.target.getKeyShape(),
        type: 'dragend'
      } as IG6GraphEvent);
    }
    
    this.dragState = { dragging: false, target: null, startPoint: null };
  }
  
  private getTargetItem(point: Point): Item | null {
    // Find item at the given point
    const nodes = this.graph.getNodes();
    for (const node of nodes) {
      const bbox = node.getBBox();
      if (point.x >= bbox.x && point.x <= bbox.x + bbox.width &&
          point.y >= bbox.y && point.y <= bbox.y + bbox.height) {
        return node;
      }
    }
    return null;
  }
  
  destroy(): void {
    const container = this.graph.getContainer();
    // Remove all event listeners
    container.removeEventListener('mousedown', this.onMouseDown);
    container.removeEventListener('mousemove', this.onMouseMove);
    container.removeEventListener('mouseup', this.onMouseUp);
    // ... remove other listeners
    
    this.destroyed = true;
  }
}

Types

interface IAbstractGraph {
  // Graph interface methods as documented in graph-management.md
}

interface GraphData {
  nodes?: NodeConfig[];
  edges?: EdgeConfig[];
  combos?: ComboConfig[];
}

interface NodeConfig {
  id: string;
  x?: number;
  y?: number;
  [key: string]: any;
}

interface Point {
  x: number;
  y: number;
}

interface IG6GraphEvent {
  item: Item | null;
  canvasX: number;
  canvasY: number;
  clientX: number;
  clientY: number;
  x: number;
  y: number;
  wheelDelta: number;
  target: IShapeBase;
  type: string;
}