Extension system for registering custom marks, transforms, interactions, and components to extend G2's capabilities.
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;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");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"]
});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
});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]
});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 }
}
]
});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");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;
}
}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