CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-zrender

A lightweight 2D graphics library providing canvas and SVG rendering for Apache ECharts

Overview
Eval results
Files

events.mddocs/

Event Handling

ZRender provides a comprehensive event system for handling mouse, touch, and custom events. The event system supports hit testing, event delegation, interaction handling, and custom event types with full event lifecycle management.

Event System Architecture

ZRender's event system is built on several key components:

  • Event capture and bubbling through the element hierarchy
  • Hit testing for determining which elements receive events
  • Event delegation for efficient event management
  • Custom event support for application-specific interactions

Core Event Interfaces

Element Event Methods

All graphics elements inherit event handling capabilities:

interface Element {
  // Event binding
  on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx>, context?: Ctx): this;
  on<Ctx>(eventName: string, eventHandler: Function, context?: Ctx): this;
  
  // Event unbinding
  off(eventName?: string, eventHandler?: Function): void;
  
  // Event triggering
  trigger(eventName: string, event?: any): void;
  
  // Event state
  silent: boolean;     // Disable all events on this element
  ignore: boolean;     // Ignore in hit testing
}

ZRender Instance Event Methods

The main ZRender instance provides global event handling:

interface ZRender {
  // Global event binding
  on<Ctx>(eventName: ElementEventName, eventHandler: ElementEventCallback<Ctx>, context?: Ctx): this;
  on<Ctx>(eventName: string, eventHandler: Function, context?: Ctx): this;
  
  // Global event unbinding
  off(eventName?: string, eventHandler?: Function): void;
  
  // Manual event triggering
  trigger(eventName: string, event?: unknown): void;
  
  // Hit testing
  findHover(x: number, y: number): { target: Displayable; topTarget: Displayable } | undefined;
  
  // Cursor management
  setCursorStyle(cursorStyle: string): void;
}

Event Types

Mouse Events

type ElementEventName = 
  | 'click' 
  | 'dblclick'
  | 'mousedown' 
  | 'mouseup' 
  | 'mousemove' 
  | 'mouseover' 
  | 'mouseout'
  | 'mouseenter'
  | 'mouseleave'
  | 'contextmenu';

Touch Events

type TouchEventName = 
  | 'touchstart'
  | 'touchmove' 
  | 'touchend'
  | 'touchcancel';

Drag Events

type DragEventName = 
  | 'drag'
  | 'dragstart'
  | 'dragend'
  | 'dragenter'
  | 'dragleave'
  | 'dragover'
  | 'drop';

Global Events

type GlobalEventName = 
  | 'rendered'      // Fired after rendering frame
  | 'finished'      // Fired when animation finishes
  | 'frame';        // Fired on each animation frame

Event Objects

Base Event Interface

interface ElementEvent {
  type: string;
  event: MouseEvent | TouchEvent | PointerEvent;
  target: Element;
  topTarget: Element;
  cancelBubble: boolean;
  offsetX: number;
  offsetY: number;
  gestureEvent?: GestureEvent;
  pinchX?: number;
  pinchY?: number;
  pinchScale?: number;
  wheelDelta?: number;
  zrByTouch?: boolean;
  which?: number;
  stop(): void;
}

interface ElementEventCallback<Ctx = any, Impl = any> {
  (this: CbThis$1<Ctx, Impl>, e: ElementEvent): boolean | void;
}

Rendered Event

interface RenderedEvent {
  elapsedTime: number;  // Time taken to render frame in milliseconds
}

Usage Examples

Basic Event Handling

import { Circle, Rect } from "zrender";

const circle = new Circle({
  shape: { cx: 150, cy: 150, r: 50 },
  style: { fill: '#74b9ff', cursor: 'pointer' }
});

// Basic click handler
circle.on('click', (e) => {
  console.log('Circle clicked!', e);
  console.log('Click position:', e.offsetX, e.offsetY);
});

// Mouse enter/leave for hover effects
circle.on('mouseenter', (e) => {
  console.log('Mouse entered circle');
  circle.animate('style')
    .when(200, { fill: '#a29bfe' })
    .start();
});

circle.on('mouseleave', (e) => {
  console.log('Mouse left circle');
  circle.animate('style')
    .when(200, { fill: '#74b9ff' })
    .start();
});

// Double click handler
circle.on('dblclick', (e) => {
  console.log('Double clicked!');
  circle.animate('scale')
    .when(300, [1.5, 1.5])
    .when(600, [1, 1])
    .start('easeOutBounce');
});

zr.add(circle);

Advanced Event Handling

import { Rect, Group } from "zrender";

// Event delegation using groups
const buttonGroup = new Group();

const createButton = (text: string, x: number, y: number, color: string) => {
  const button = new Group({
    position: [x, y]
  });
  
  const bg = new Rect({
    shape: { x: 0, y: 0, width: 100, height: 40, r: 5 },
    style: { fill: color }
  });
  
  const label = new Text({
    style: {
      text: text,
      fill: '#ffffff',
      fontSize: 14,
      textAlign: 'center',
      textVerticalAlign: 'middle'
    },
    position: [50, 20]
  });
  
  button.add(bg);
  button.add(label);
  
  // Store button data
  button.buttonData = { text, color };
  
  return button;
};

// Create buttons
const button1 = createButton('Button 1', 50, 50, '#e17055');
const button2 = createButton('Button 2', 170, 50, '#00b894');
const button3 = createButton('Button 3', 290, 50, '#fdcb6e');

buttonGroup.add(button1);
buttonGroup.add(button2);
buttonGroup.add(button3);

// Handle events on the group level
buttonGroup.on('click', (e) => {
  // Find which button was clicked
  let clickedButton = e.target;
  while (clickedButton && !clickedButton.buttonData) {
    clickedButton = clickedButton.parent;
  }
  
  if (clickedButton && clickedButton.buttonData) {
    console.log('Clicked button:', clickedButton.buttonData.text);
    
    // Visual feedback
    const bg = clickedButton.children()[0];
    bg.animate('style')
      .when(100, { fill: '#2d3436' })
      .when(200, { fill: clickedButton.buttonData.color })
      .start();
  }
});

zr.add(buttonGroup);

Drag and Drop

import { Circle, Rect } from "zrender";

// Draggable circle
const draggableCircle = new Circle({
  shape: { cx: 200, cy: 200, r: 30 },
  style: { fill: '#ff7675', cursor: 'move' },
  draggable: true
});

let isDragging = false;
let dragOffset = { x: 0, y: 0 };

draggableCircle.on('mousedown', (e) => {
  isDragging = true;
  const pos = draggableCircle.position || [0, 0];
  dragOffset.x = e.offsetX - pos[0];
  dragOffset.y = e.offsetY - pos[1];
  
  // Visual feedback
  draggableCircle.animate('style')
    .when(100, { shadowBlur: 10, shadowColor: '#ff7675' })
    .start();
});

// Use global mouse events for smooth dragging
zr.on('mousemove', (e) => {
  if (isDragging) {
    const newX = e.offsetX - dragOffset.x;
    const newY = e.offsetY - dragOffset.y;
    draggableCircle.position = [newX, newY];
    zr.refresh();
  }
});

zr.on('mouseup', (e) => {
  if (isDragging) {
    isDragging = false;
    
    // Remove visual feedback
    draggableCircle.animate('style')
      .when(100, { shadowBlur: 0 })
      .start();
  }
});

// Drop zone
const dropZone = new Rect({
  shape: { x: 400, y: 150, width: 100, height: 100 },
  style: { 
    fill: 'none', 
    stroke: '#ddd', 
    lineWidth: 2, 
    lineDash: [5, 5] 
  }
});

// Drop zone events
dropZone.on('dragover', (e) => {
  dropZone.style.stroke = '#00b894';
  zr.refresh();
});

dropZone.on('dragleave', (e) => {
  dropZone.style.stroke = '#ddd';
  zr.refresh();
});

dropZone.on('drop', (e) => {
  console.log('Dropped on zone!');
  dropZone.style.fill = 'rgba(0, 184, 148, 0.1)';
  dropZone.style.stroke = '#00b894';
  zr.refresh();
});

zr.add(draggableCircle);
zr.add(dropZone);

Touch Events

import { Circle } from "zrender";

const touchCircle = new Circle({
  shape: { cx: 150, cy: 350, r: 40 },
  style: { fill: '#fd79a8' }
});

// Touch event handling
touchCircle.on('touchstart', (e) => {
  console.log('Touch started');
  touchCircle.animate('scale')
    .when(100, [0.9, 0.9])
    .start();
});

touchCircle.on('touchend', (e) => {
  console.log('Touch ended');
  touchCircle.animate('scale')
    .when(100, [1, 1])
    .start();
});

touchCircle.on('touchmove', (e) => {
  // Handle touch move
  const touch = e.event.touches[0];
  if (touch) {
    const rect = zr.dom.getBoundingClientRect();
    const x = touch.clientX - rect.left;
    const y = touch.clientY - rect.top;
    touchCircle.position = [x, y];
    zr.refresh();
  }
});

zr.add(touchCircle);

Custom Events

import { Group, Rect, Text } from "zrender";

// Custom component with custom events
class CustomButton extends Group {
  constructor(options: { text: string, x: number, y: number }) {
    super({ position: [options.x, options.y] });
    
    this.background = new Rect({
      shape: { x: 0, y: 0, width: 120, height: 40, r: 5 },
      style: { fill: '#74b9ff' }
    });
    
    this.label = new Text({
      style: {
        text: options.text,
        fill: '#ffffff',
        fontSize: 14,
        textAlign: 'center'
      },
      position: [60, 20]
    });
    
    this.add(this.background);
    this.add(this.label);
    
    this.setupEvents();
  }
  
  private setupEvents() {
    this.on('click', () => {
      // Trigger custom event
      this.trigger('buttonClick', { 
        button: this, 
        text: this.label.style.text 
      });
    });
    
    this.on('mouseenter', () => {
      this.trigger('buttonHover', { button: this, hovered: true });
    });
    
    this.on('mouseleave', () => {
      this.trigger('buttonHover', { button: this, hovered: false });
    });
  }
}

// Create custom button
const customButton = new CustomButton({ text: 'Custom', x: 50, y: 400 });

// Listen to custom events
customButton.on('buttonClick', (e) => {
  console.log('Custom button clicked:', e.text);
  alert(`Button "${e.text}" was clicked!`);
});

customButton.on('buttonHover', (e) => {
  const color = e.hovered ? '#a29bfe' : '#74b9ff';
  e.button.background.animate('style')
    .when(150, { fill: color })
    .start();
});

zr.add(customButton);

Event Bubbling and Propagation

import { Group, Circle, Rect } from "zrender";

// Nested elements for testing event bubbling
const container = new Group({ position: [300, 300] });

const outerRect = new Rect({
  shape: { x: 0, y: 0, width: 200, height: 150 },
  style: { fill: 'rgba(116, 185, 255, 0.3)', stroke: '#74b9ff' }
});

const innerCircle = new Circle({
  shape: { cx: 100, cy: 75, r: 30 },
  style: { fill: '#e17055' }
});

container.add(outerRect);
container.add(innerCircle);

// Event handlers with bubbling
outerRect.on('click', (e) => {
  console.log('Outer rect clicked');
  // Event will bubble up to container
});

innerCircle.on('click', (e) => {
  console.log('Inner circle clicked');
  
  // Stop event bubbling
  if (e.event.ctrlKey) {
    e.cancelBubble = true;
    console.log('Event bubbling stopped');
  }
});

container.on('click', (e) => {
  console.log('Container clicked (bubbled up)');
});

zr.add(container);

Global Event Handling

// Global event handlers
zr.on('click', (e) => {
  console.log('Global click at:', e.offsetX, e.offsetY);
});

zr.on('rendered', (e: RenderedEvent) => {
  console.log('Frame rendered in:', e.elapsedTime, 'ms');
});

// Performance monitoring
zr.on('rendered', (e: RenderedEvent) => {
  if (e.elapsedTime > 16.67) { // > 60fps threshold
    console.warn('Slow frame detected:', e.elapsedTime, 'ms');
  }
});

// Global keyboard events (requires DOM focus)
document.addEventListener('keydown', (e) => {
  switch(e.key) {
    case 'Escape':
      // Clear selections, cancel operations, etc.
      console.log('Escape pressed - clearing state');
      break;
    case 'Delete':
      // Delete selected elements
      console.log('Delete pressed');
      break;
  }
});

Event Cleanup and Memory Management

import { Circle } from "zrender";

// Proper event cleanup
class ManagedElement {
  private element: Circle;
  private handlers: { [key: string]: Function } = {};
  
  constructor() {
    this.element = new Circle({
      shape: { cx: 100, cy: 100, r: 30 },
      style: { fill: '#00cec9' }
    });
    
    this.setupEvents();
  }
  
  private setupEvents() {
    // Store handler references for cleanup
    this.handlers.click = (e: any) => {
      console.log('Managed element clicked');
    };
    
    this.handlers.hover = (e: any) => {
      this.element.animate('style')
        .when(200, { fill: '#55efc4' })
        .start();
    };
    
    this.handlers.leave = (e: any) => {
      this.element.animate('style')
        .when(200, { fill: '#00cec9' })
        .start();
    };
    
    // Bind events
    this.element.on('click', this.handlers.click);
    this.element.on('mouseenter', this.handlers.hover);
    this.element.on('mouseleave', this.handlers.leave);
  }
  
  public dispose() {
    // Clean up event handlers
    this.element.off('click', this.handlers.click);
    this.element.off('mouseenter', this.handlers.hover);
    this.element.off('mouseleave', this.handlers.leave);
    
    // Remove from scene
    if (this.element.parent) {
      this.element.parent.remove(this.element);
    }
    
    // Clear references
    this.handlers = {};
  }
  
  public getElement() {
    return this.element;
  }
}

// Usage
const managedEl = new ManagedElement();
zr.add(managedEl.getElement());

// Later, clean up properly
// managedEl.dispose();

Event Performance Optimization

// Efficient event handling for many elements
const createOptimizedEventHandling = () => {
  const container = new Group();
  const elements: Circle[] = [];
  
  // Create many elements
  for (let i = 0; i < 100; i++) {
    const circle = new Circle({
      shape: { 
        cx: (i % 10) * 50 + 25, 
        cy: Math.floor(i / 10) * 50 + 25, 
        r: 20 
      },
      style: { fill: `hsl(${i * 3.6}, 70%, 60%)` }
    });
    
    // Store identifier
    circle.elementId = i;
    elements.push(circle);
    container.add(circle);
  }
  
  // Use single event handler on container instead of individual handlers
  container.on('click', (e) => {
    // Find clicked element
    let target = e.target;
    while (target && target.elementId === undefined) {
      target = target.parent;
    }
    
    if (target && target.elementId !== undefined) {
      console.log('Clicked element:', target.elementId);
      
      // Apply effect
      target.animate('scale')
        .when(200, [1.2, 1.2])
        .when(400, [1, 1])
        .start();
    }
  });
  
  return container;
};

const optimizedGroup = createOptimizedEventHandling();
zr.add(optimizedGroup);

Install with Tessl CLI

npx tessl i tessl/npm-zrender

docs

animation.md

core-zrender.md

events.md

graphics-primitives.md

index.md

shapes.md

styling.md

text-images.md

utilities.md

tile.json