or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

animations.mdchart-runtime.mdcomponents.mdcompositions.mdcoordinates.mddata-transforms.mdencoding-scales.mdextensions.mdindex.mdinteractions.mdmarks.mdthemes.md
tile.json

extensions.mddocs/

Extension and Customization

Extension system for registering custom marks, transforms, interactions, and components to extend G2's capabilities.

Capabilities

Extension System Overview

Core extension mechanisms for adding custom functionality to G2.

/**
 * Registers custom components to a library
 * @param name - Component name with prefix (e.g., "mark.custom", "transform.custom")
 * @param component - Component implementation
 * @param library - Target library (optional, defaults to global library)
 */
function register(name: string, component: any, library?: Library): void;

/**
 * Creates extended Chart class with custom library
 * @param Runtime - Base Runtime class
 * @param library - Custom library with registered components
 * @returns Extended Chart constructor
 */
function extend<Spec, Library>(
  Runtime: new (options: RuntimeOptions) => Runtime<Spec>,
  library: Library
): new (options?: RuntimeOptions) => API<Spec, Library>;

/**
 * Creates custom library with component registry
 * @returns Library object with registered components
 */
function createLibrary(): Library;

Custom Mark Creation

Creating custom mark types for specialized visualizations.

/**
 * Base mark interface for custom implementations
 */
interface CustomMark {
  /** Mark rendering function */
  render(renderer: any, data: any[], options: any): void;
  /** Data preprocessing */
  preprocess?(data: any[]): any[];
  /** Update method for data changes */
  update?(data: any[], options: any): void;
  /** Cleanup method */
  destroy?(): void;
}

/**
 * Custom mark registration
 */
register("mark.customName", CustomMarkImplementation);

Custom Mark Examples:

// Custom violin plot mark
class ViolinMark implements CustomMark {
  render(renderer: any, data: any[], options: any) {
    // Render violin plot using density estimation
    const densityData = this.calculateDensity(data, options);
    const violinPath = this.createViolinPath(densityData);
    
    return renderer.createPath({
      path: violinPath,
      fill: options.fill || "#1f77b4",
      stroke: options.stroke || "#fff",
      strokeWidth: options.strokeWidth || 1
    });
  }
  
  private calculateDensity(data: any[], options: any) {
    // Kernel density estimation implementation
    // ...
  }
  
  private createViolinPath(densityData: any[]) {
    // Create SVG path for violin shape
    // ...
  }
}

// Register the custom mark
register("mark.violin", ViolinMark);

// Use the custom mark
const chart = new Chart({ container: "chart" });
chart.violin()
  .data(distributionData)
  .encode("x", "category")
  .encode("y", "values");

Custom Transform Functions

Creating custom data transformation functions.

/**
 * Custom transform interface
 */
interface CustomTransform {
  /** Transform function */
  transform(data: any[], options: any): any[];
  /** Transform metadata */
  meta?: {
    name: string;
    description: string;
    options?: any;
  };
}

/**
 * Custom transform registration
 */
register("transform.customName", CustomTransformImplementation);

Custom Transform Examples:

// Custom outlier detection transform
class OutlierDetection implements CustomTransform {
  meta = {
    name: "outlierDetection",
    description: "Detects and flags outliers using IQR method",
    options: {
      field: "Field to analyze for outliers",
      method: "Detection method: 'iqr' | 'zscore' | 'modified-zscore'",
      threshold: "Threshold multiplier (default: 1.5 for IQR)"
    }
  };

  transform(data: any[], options: any = {}) {
    const { field, method = "iqr", threshold = 1.5 } = options;
    
    if (method === "iqr") {
      return this.detectOutliersIQR(data, field, threshold);
    } else if (method === "zscore") {
      return this.detectOutliersZScore(data, field, threshold);
    }
    
    return data;
  }
  
  private detectOutliersIQR(data: any[], field: string, threshold: number) {
    const values = data.map(d => d[field]).sort((a, b) => a - b);
    const q1 = this.quantile(values, 0.25);
    const q3 = this.quantile(values, 0.75);
    const iqr = q3 - q1;
    const lowerBound = q1 - threshold * iqr;
    const upperBound = q3 + threshold * iqr;
    
    return data.map(d => ({
      ...d,
      isOutlier: d[field] < lowerBound || d[field] > upperBound
    }));
  }
  
  private quantile(values: number[], q: number): number {
    const index = (values.length - 1) * q;
    const lower = Math.floor(index);
    const upper = Math.ceil(index);
    const weight = index % 1;
    
    return values[lower] * (1 - weight) + values[upper] * weight;
  }
}

// Register the custom transform
register("transform.outlierDetection", new OutlierDetection());

// Use the custom transform
chart
  .point()
  .data(dataset)
  .transform("outlierDetection", { field: "value", method: "iqr" })
  .encode("x", "category")
  .encode("y", "value")
  .encode("color", "isOutlier")
  .scale("color", {
    type: "ordinal",
    domain: [true, false],
    range: ["red", "blue"]
  });

Custom Interaction Types

Creating custom interaction behaviors.

/**
 * Custom interaction interface
 */
interface CustomInteraction {
  /** Initialize interaction */
  init(chart: any, options: any): void;
  /** Handle events */
  on(event: string, handler: Function): void;
  /** Cleanup interaction */
  destroy(): void;
}

/**
 * Custom interaction registration
 */
register("interaction.customName", CustomInteractionImplementation);

Custom Interaction Examples:

// Custom pan and zoom interaction
class PanZoomInteraction implements CustomInteraction {
  private chart: any;
  private options: any;
  private isDragging = false;
  private lastPosition = { x: 0, y: 0 };
  private scale = 1;

  init(chart: any, options: any = {}) {
    this.chart = chart;
    this.options = {
      enablePan: true,
      enableZoom: true,
      zoomSensitivity: 0.001,
      ...options
    };
    
    this.bindEvents();
  }

  private bindEvents() {
    const container = this.chart.getContainer();
    
    if (this.options.enableZoom) {
      container.addEventListener("wheel", this.handleWheel.bind(this));
    }
    
    if (this.options.enablePan) {
      container.addEventListener("mousedown", this.handleMouseDown.bind(this));
      container.addEventListener("mousemove", this.handleMouseMove.bind(this));
      container.addEventListener("mouseup", this.handleMouseUp.bind(this));
    }
  }

  private handleWheel(event: WheelEvent) {
    event.preventDefault();
    
    const delta = -event.deltaY * this.options.zoomSensitivity;
    this.scale *= (1 + delta);
    this.scale = Math.max(0.1, Math.min(10, this.scale));
    
    this.updateTransform();
  }

  private handleMouseDown(event: MouseEvent) {
    this.isDragging = true;
    this.lastPosition = { x: event.clientX, y: event.clientY };
  }

  private handleMouseMove(event: MouseEvent) {
    if (!this.isDragging) return;
    
    const deltaX = event.clientX - this.lastPosition.x;
    const deltaY = event.clientY - this.lastPosition.y;
    
    // Update pan offset
    this.updatePan(deltaX, deltaY);
    this.lastPosition = { x: event.clientX, y: event.clientY };
  }

  private updateTransform() {
    // Apply zoom and pan transforms to chart
    const plotArea = this.chart.getPlotArea();
    plotArea.attr("transform", `scale(${this.scale})`);
  }

  destroy() {
    const container = this.chart.getContainer();
    container.removeEventListener("wheel", this.handleWheel);
    container.removeEventListener("mousedown", this.handleMouseDown);
    container.removeEventListener("mousemove", this.handleMouseMove);
    container.removeEventListener("mouseup", this.handleMouseUp);
  }
}

// Register the custom interaction
register("interaction.panZoom", PanZoomInteraction);

// Use the custom interaction
chart
  .point()
  .data(largeDataset)
  .encode("x", "x")
  .encode("y", "y")
  .interaction("panZoom", {
    enablePan: true,
    enableZoom: true,
    zoomSensitivity: 0.002
  });

Custom Scale Types

Creating custom scale functions for specialized data mappings.

/**
 * Custom scale interface
 */
interface CustomScale {
  /** Domain setter/getter */
  domain(): any[];
  domain(domain: any[]): this;
  
  /** Range setter/getter */
  range(): any[];
  range(range: any[]): this;
  
  /** Scale function */
  map(value: any): any;
  
  /** Inverse scale function */
  invert?(value: any): any;
  
  /** Generate ticks */
  ticks?(count?: number): any[];
}

/**
 * Custom scale registration
 */
register("scale.customName", CustomScaleImplementation);

Custom Scale Examples:

// Custom logarithmic scale with configurable base
class CustomLogScale implements CustomScale {
  private _domain: [number, number] = [1, 10];
  private _range: [number, number] = [0, 1];
  private base: number;

  constructor(base = 10) {
    this.base = base;
  }

  domain(): [number, number];
  domain(domain: [number, number]): this;
  domain(domain?: [number, number]) {
    if (domain === undefined) return this._domain;
    this._domain = domain;
    return this;
  }

  range(): [number, number];
  range(range: [number, number]): this;
  range(range?: [number, number]) {
    if (range === undefined) return this._range;
    this._range = range;
    return this;
  }

  map(value: number): number {
    const [d0, d1] = this._domain;
    const [r0, r1] = this._range;
    
    const logValue = Math.log(value) / Math.log(this.base);
    const logD0 = Math.log(d0) / Math.log(this.base);
    const logD1 = Math.log(d1) / Math.log(this.base);
    
    const t = (logValue - logD0) / (logD1 - logD0);
    return r0 + t * (r1 - r0);
  }

  invert(value: number): number {
    const [d0, d1] = this._domain;
    const [r0, r1] = this._range;
    
    const t = (value - r0) / (r1 - r0);
    const logD0 = Math.log(d0) / Math.log(this.base);
    const logD1 = Math.log(d1) / Math.log(this.base);
    
    const logValue = logD0 + t * (logD1 - logD0);
    return Math.pow(this.base, logValue);
  }

  ticks(count = 10): number[] {
    const [d0, d1] = this._domain;
    const logD0 = Math.log(d0) / Math.log(this.base);
    const logD1 = Math.log(d1) / Math.log(this.base);
    
    const ticks: number[] = [];
    const step = (logD1 - logD0) / (count - 1);
    
    for (let i = 0; i < count; i++) {
      const logValue = logD0 + i * step;
      ticks.push(Math.pow(this.base, logValue));
    }
    
    return ticks;
  }
}

// Register custom scale
register("scale.customLog", (base?: number) => new CustomLogScale(base));

// Use custom scale
chart
  .point()
  .data(exponentialData)
  .encode("x", "x")
  .encode("y", "y")
  .scale("y", {
    type: "customLog",
    base: 2, // Base-2 logarithm
    domain: [1, 1024]
  });

Custom Component Creation

Creating custom chart components like axes, legends, and annotations.

/**
 * Custom component interface
 */
interface CustomComponent {
  /** Component initialization */
  init(container: any, options: any): void;
  /** Render component */
  render(data: any, options: any): void;
  /** Update component */
  update(data: any, options: any): void;
  /** Component cleanup */
  destroy(): void;
}

/**
 * Custom component registration
 */
register("component.customName", CustomComponentImplementation);

Custom Component Examples:

// Custom annotation component
class AnnotationComponent implements CustomComponent {
  private container: any;
  private annotations: any[] = [];

  init(container: any, options: any) {
    this.container = container;
  }

  render(data: any, options: any) {
    const { annotations = [] } = options;
    
    annotations.forEach((annotation: any) => {
      this.createAnnotation(annotation);
    });
  }

  private createAnnotation(annotation: any) {
    const { type, x, y, text, style = {} } = annotation;
    
    if (type === "text") {
      const textElement = this.container
        .append("text")
        .attr("x", x)
        .attr("y", y)
        .text(text)
        .style("font-size", style.fontSize || "12px")
        .style("fill", style.color || "#000");
        
      this.annotations.push(textElement);
    } else if (type === "line") {
      const lineElement = this.container
        .append("line")
        .attr("x1", annotation.x1)
        .attr("y1", annotation.y1)
        .attr("x2", annotation.x2)
        .attr("y2", annotation.y2)
        .style("stroke", style.stroke || "#000")
        .style("stroke-width", style.strokeWidth || 1);
        
      this.annotations.push(lineElement);
    }
  }

  update(data: any, options: any) {
    this.destroy();
    this.render(data, options);
  }

  destroy() {
    this.annotations.forEach(annotation => annotation.remove());
    this.annotations = [];
  }
}

// Register custom component
register("component.annotation", AnnotationComponent);

// Use custom component
chart
  .line()
  .data(timeSeriesData)
  .encode("x", "date")
  .encode("y", "value")
  .component("annotation", {
    annotations: [
      {
        type: "text",
        x: 300,
        y: 50,
        text: "Peak Performance",
        style: { fontSize: "14px", color: "red" }
      },
      {
        type: "line",
        x1: 290,
        y1: 100,
        x2: 310,
        y2: 80,
        style: { stroke: "red", strokeWidth: 2 }
      }
    ]
  });

Plugin System

Creating reusable plugins that bundle multiple extensions.

/**
 * Plugin interface
 */
interface Plugin {
  /** Plugin name */
  name: string;
  /** Plugin version */
  version?: string;
  /** Install function */
  install(G2: any, options?: any): void;
  /** Uninstall function */
  uninstall?(G2: any): void;
}

/**
 * Install plugin
 */
function use(plugin: Plugin, options?: any): void;

Plugin Examples:

// Statistical analysis plugin
const StatisticsPlugin: Plugin = {
  name: "statistics",
  version: "1.0.0",
  
  install(G2: any, options: any = {}) {
    // Register multiple statistical transforms
    register("transform.regression", LinearRegression);
    register("transform.correlation", CorrelationMatrix);
    register("transform.clustering", KMeansClustering);
    
    // Register statistical marks
    register("mark.regression", RegressionLine);
    register("mark.confidence", ConfidenceBand);
    
    // Register statistical interactions
    register("interaction.statisticalTooltip", StatisticalTooltip);
    
    console.log("Statistics plugin installed");
  },
  
  uninstall(G2: any) {
    // Cleanup registered components
    console.log("Statistics plugin uninstalled");
  }
};

// Use plugin
use(StatisticsPlugin);

// Now use statistical features
chart
  .point()
  .data(correlationData)
  .transform("regression", { method: "linear" })
  .encode("x", "x")
  .encode("y", "y")
  .mark("regression")
  .interaction("statisticalTooltip");

Extension Best Practices

Guidelines for creating robust and maintainable extensions.

/**
 * Extension best practices and patterns
 */

// 1. Proper error handling
class RobustCustomMark implements CustomMark {
  render(renderer: any, data: any[], options: any) {
    try {
      // Validate inputs
      if (!data || !Array.isArray(data)) {
        throw new Error("Invalid data provided to custom mark");
      }
      
      // Implementation
      return this.renderImplementation(renderer, data, options);
    } catch (error) {
      console.error("Custom mark render error:", error);
      // Graceful fallback
      return this.renderFallback(renderer, data, options);
    }
  }
}

// 2. TypeScript integration
interface CustomMarkOptions {
  color?: string;
  size?: number;
  opacity?: number;
}

class TypedCustomMark implements CustomMark {
  render(
    renderer: any, 
    data: any[], 
    options: CustomMarkOptions
  ) {
    // Type-safe implementation
  }
}

// 3. Performance optimization
class OptimizedCustomMark implements CustomMark {
  private cache = new Map();
  
  render(renderer: any, data: any[], options: any) {
    const cacheKey = this.getCacheKey(data, options);
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    
    const result = this.renderImplementation(renderer, data, options);
    this.cache.set(cacheKey, result);
    return result;
  }
}

Extension Distribution

Packaging and distributing custom extensions.

// Package structure for G2 extension
export default {
  name: "my-g2-extension",
  install(G2: any) {
    // Register all components
    register("mark.myMark", MyCustomMark);
    register("transform.myTransform", MyCustomTransform);
    register("interaction.myInteraction", MyCustomInteraction);
  }
};

// Usage in consuming application
import MyExtension from "my-g2-extension";
import { Chart, use } from "@antv/g2";

use(MyExtension);

const chart = new Chart({ container: "chart" });
// Now use extended functionality