阿里云主机折上折
  • 微信号
Current Site:Index > Custom extension development

Custom extension development

Author:Chuan Chen 阅读数:17407人阅读 分类: ECharts

Extension Development Basics

ECharts' extension development primarily revolves around two core concepts: custom series and custom components. Custom series allow developers to create entirely new chart types, while custom components are used to extend auxiliary elements like coordinate systems and legends. Both extension methods rely on ECharts' plugin mechanism, implemented through the echarts.register* series of APIs.

// Basic extension registration example
echarts.registerSeriesType('customSeries', {
    // Series type definition
});

echarts.registerComponent('customComponent', {
    // Component definition
});

Extension development requires an understanding of ECharts' rendering pipeline: configuration initialization -> data processing -> layout calculation -> graphic rendering -> interaction handling. Custom extensions mainly intervene in the data processing and graphic rendering stages by overriding specific methods to implement functionality.

Custom Series Development

Custom series are the most powerful way to extend ECharts, suitable for creating entirely new chart types. Development requires implementing a series definition object with a custom series.type value, including the following key methods:

echarts.registerSeriesType('waterfall', {
    // Required series type declaration
    type: 'custom',
    
    // Initialization method
    init: function (option) {
        // Initialization logic
    },
    
    // Data-to-visual mapping
    getInitialData: function (option, ecModel) {
        // Return dataset
    },
    
    // Core rendering logic method
    renderItem: function (params, api) {
        // Return graphic element definition
        return {
            type: 'rect',
            shape: {
                x: api.value(0),
                y: api.value(1),
                width: api.value(2),
                height: api.value(3)
            },
            style: {
                fill: api.visual('color')
            }
        };
    }
});

Practical example: Developing a 3D bar chart series. Requires handling 3D coordinate transformation and adding lighting effects:

renderItem: function(params, api) {
    const value = api.value(2); // Get third-dimensional data
    const depth = Math.min(value * 10, 300);
    
    return {
        type: 'cube',
        shape: {
            x: api.coord([api.value(0), api.value(1)])[0],
            y: api.coord([api.value(0), api.value(1)])[1],
            width: 20,
            height: -api.size([1, api.value(1)])[1],
            depth: depth
        },
        style: {
            fill: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                { offset: 0, color: '#37A2DA' },
                { offset: 1, color: '#0d1d3a' }
            ]),
            shadowBlur: 10,
            shadowColor: 'rgba(0,0,0,0.3)'
        }
    };
}

Custom Component Development

Component extensions are suitable for auxiliary elements like toolbars, legends, and axes. A typical component structure includes lifecycle methods and rendering logic:

echarts.registerComponent('timelineControl', {
    // Component initialization
    init: function(ecModel, api) {
        this._dom = document.createElement('div');
        this._dom.className = 'ec-timeline';
        api.getZr().dom.appendChild(this._dom);
    },
    
    // Rendering logic
    render: function(ecModel, api, payload) {
        const data = ecModel.getSeriesByType('line')[0].getData();
        this._dom.innerHTML = `
            <input type="range" min="0" max="${data.count() - 1}" 
                   value="0" class="ec-timeline-slider">
        `;
    },
    
    // Event handling
    dispose: function() {
        this._dom.remove();
    }
});

Practical example: Developing a dynamic data filter component that can filter series data in real-time:

render: function(ecModel, api) {
    const seriesData = ecModel.getSeriesByType('bar')[0].getRawData();
    const filterControl = document.createElement('div');
    
    seriesData.each(['category'], function(cat) {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = true;
        checkbox.addEventListener('change', function() {
            // Trigger data filtering
            api.dispatchAction({
                type: 'dataFilter',
                seriesIndex: 0,
                filter: function(dataIndex) {
                    return checkbox.checked;
                }
            });
        });
        filterControl.appendChild(checkbox);
    });
    
    this._container.innerHTML = '';
    this._container.appendChild(filterControl);
}

Advanced Rendering Techniques

For complex visualization needs, WebGL renderers can be combined for high-performance rendering. ECharts provides a GL extension interface:

// WebGL extension example
const glRenderer = new echarts.gl.ChartGL(dom, {
    devicePixelRatio: 2
});

glRenderer.setOption({
    series: [{
        type: 'customGL',
        coordinateSystem: 'geo',
        renderItem: function(params, api) {
            // Return WebGL drawing instructions
            return {
                command: 'drawElements',
                attributes: {
                    position: new Float32Array([...]),
                    color: new Float32Array([...])
                },
                elements: new Uint16Array([...]),
                count: 6
            };
        }
    }]
});

Particle system implementation example:

renderItem: function(params, api) {
    const particleCount = 1000;
    const positions = new Float32Array(particleCount * 3);
    const colors = new Float32Array(particleCount * 4);
    
    // Initialize particle positions and colors
    for (let i = 0; i < particleCount; i++) {
        positions[i * 3] = Math.random() * 1000;
        positions[i * 3 + 1] = Math.random() * 500;
        positions[i * 3 + 2] = Math.random() * 200 - 100;
        
        colors[i * 4] = Math.random();
        colors[i * 4 + 1] = Math.random();
        colors[i * 4 + 2] = Math.random();
        colors[i * 4 + 3] = 0.8;
    }
    
    return {
        command: 'drawArrays',
        primitive: 'POINTS',
        attributes: {
            position: positions,
            color: colors
        },
        count: particleCount
    };
}

Interaction Extensions

Custom interactions require combining ECharts' event system and action mechanism. Typical interaction extensions include the following elements:

  1. Event listening
  2. Action triggering
  3. State management
// Custom drag interaction
zr.on('mousedown', function(e) {
    const pointInPixel = [e.offsetX, e.offsetY];
    const pointInGrid = api.convertFromPixel('grid', pointInPixel);
    
    // Check if graphic element is hit
    if (api.containPixel('series', pointInPixel)) {
        this._dragging = true;
        this._startPos = pointInGrid;
    }
});

zr.on('mousemove', function(e) {
    if (this._dragging) {
        const currPos = api.convertFromPixel('grid', [e.offsetX, e.offsetY]);
        const delta = [currPos[0] - this._startPos[0], currPos[1] - this._startPos[1]];
        
        // Update series data
        api.dispatchAction({
            type: 'updateData',
            seriesIndex: 0,
            dataIndex: this._selectedIndex,
            value: delta
        });
    }
});

Complex example: Implementing a chart element connection feature:

let linePoints = [];
zr.on('click', function(e) {
    const point = api.convertFromPixel('grid', [e.offsetX, e.offsetY]);
    linePoints.push(point);
    
    if (linePoints.length >= 2) {
        // Add custom graphic
        api.setOption({
            graphic: {
                type: 'polyline',
                shape: {
                    points: linePoints
                },
                style: {
                    stroke: '#ff4500',
                    lineWidth: 2
                }
            }
        });
        linePoints = [];
    }
});

Performance Optimization Strategies

Large-scale data rendering requires special handling:

  1. Incremental rendering technique
renderItem: function(params, api) {
    // Only render visible area
    const viewport = api.getViewport();
    if (!isInViewport(api.value(0), api.value(1), viewport)) {
        return null;
    }
    
    // Simplify graphic complexity
    return {
        type: 'circle',
        shape: {
            cx: api.value(0),
            cy: api.value(1),
            r: 3
        },
        style: api.style()
    };
}
  1. WebWorker data processing
// Main thread
const worker = new Worker('dataProcessor.js');
worker.postMessage(rawData);
worker.onmessage = function(e) {
    chart.setOption({
        series: [{
            data: e.processedData
        }]
    });
};

// Worker thread
onmessage = function(e) {
    const result = heavyDataProcessing(e.data);
    postMessage(result);
};
  1. Layered rendering technique
// Render different precision data across multiple series
series: [{
    type: 'custom',
    renderItem: renderHighDetail,
    data: highDetailData,
    progressive: 1000
}, {
    type: 'custom',
    renderItem: renderLowDetail,
    data: lowDetailData,
    progressive: 4000
}]

Debugging and Testing

Debugging techniques for developing complex extensions:

  1. Using ECharts debug mode
echarts.setDebugMode({
    // Show rendering time
    showFPS: true,
    // Preserve drawing command logs
    logRecord: true
});
  1. Custom logging system
function debugRenderItem(params, api) {
    console.group('Render Item');
    console.log('Data Index:', params.dataIndex);
    console.log('Data Value:', api.value(0));
    
    try {
        // Actual rendering logic
        return realRenderItem(params, api);
    } catch (e) {
        console.error('Render Error:', e);
        throw e;
    } finally {
        console.groupEnd();
    }
}
  1. Unit testing solution
describe('Custom Series Test', function() {
    let chart;
    beforeEach(function() {
        chart = echarts.init(document.createElement('div'));
    });
    
    it('should render basic shapes', function() {
        chart.setOption({
            series: [{
                type: 'custom',
                renderItem: testRenderItem,
                data: testData
            }]
        });
        
        const elements = chart.getZr().storage.getDisplayList();
        assert(elements.length > 0);
    });
});

Publishing and Integration

Post-development publishing process:

  1. Modular packaging
// webpack.config.js
module.exports = {
    entry: './src/extension.js',
    output: {
        library: 'EChartsExtension',
        libraryTarget: 'umd',
        filename: 'echarts-extension.min.js'
    },
    externals: {
        echarts: 'echarts'
    }
};
  1. npm publishing preparation
{
    "name": "echarts-custom-extension",
    "version": "1.0.0",
    "main": "dist/extension.js",
    "peerDependencies": {
        "echarts": "^5.0.0"
    }
}
  1. Automated integration example
// Dynamically load extension in project
import('echarts-custom-extension').then(ext => {
    ext.registerTo(echarts);
    const chart = echarts.init(dom);
    chart.setOption({
        series: [{
            type: 'customSeriesFromExtension'
        }]
    });
});

Extension Ecosystem Development

Practical solutions for building a complete extension ecosystem:

  1. Extension registry pattern
class ExtensionRegistry {
    constructor() {
        this._extensions = new Map();
    }
    
    register(name, extension) {
        if (this._extensions.has(name)) {
            throw new Error(`Extension ${name} already registered`);
        }
        this._extensions.set(name, extension);
    }
    
    applyTo(chartInstance) {
        this._extensions.forEach((ext, name) => {
            ext.registerTo(chartInstance);
        });
    }
}

// Global registry
const registry = new ExtensionRegistry();
registry.register('timeline', TimelineExtension);
registry.register('3dBar', ThreeDBarExtension);
  1. Theme adaptation solution
function createThemeAwareExtension(baseTheme) {
    return {
        renderItem: function(params, api) {
            const theme = api.getTheme();
            const color = theme.color[params.dataIndex % theme.color.length];
            
            return {
                type: 'rect',
                style: {
                    fill: color,
                    shadowColor: theme.shadowColor
                }
            };
        }
    };
}
  1. Multi-instance coordination solution
// Main chart
const mainChart = echarts.init(document.getElementById('main'));
mainChart.on('highlight', function(e) {
    // Sync to detail chart
    detailChart.dispatchAction({
        type: 'highlight',
        dataIndex: e.dataIndex
    });
});

// Detail chart
const detailChart = echarts.init(document.getElementById('detail'));
detailChart.on('select', function(e) {
    // Sync to main chart
    mainChart.dispatchAction({
        type: 'select',
        dataIndex: e.dataIndex
    });
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.