Custom extension development
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:
- Event listening
- Action triggering
- 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:
- 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()
};
}
- 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);
};
- 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:
- Using ECharts debug mode
echarts.setDebugMode({
// Show rendering time
showFPS: true,
// Preserve drawing command logs
logRecord: true
});
- 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();
}
}
- 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:
- Modular packaging
// webpack.config.js
module.exports = {
entry: './src/extension.js',
output: {
library: 'EChartsExtension',
libraryTarget: 'umd',
filename: 'echarts-extension.min.js'
},
externals: {
echarts: 'echarts'
}
};
- npm publishing preparation
{
"name": "echarts-custom-extension",
"version": "1.0.0",
"main": "dist/extension.js",
"peerDependencies": {
"echarts": "^5.0.0"
}
}
- 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:
- 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);
- 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
}
};
}
};
}
- 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