Echart工厂支持柱状图(bar)折线图(line)散点图(scatter)饼图(pie)雷达图(radar)极坐标柱状图(polarBar)和极坐标折线图(polarLine)等多种图表,及其对应扩展图表:
git链接:sq/UI/src/components/Echarts at main · afigzb/sq (github.com)https://github.com/afigzb/sq/tree/main/UI/src/components/Echarts
展示页面,后续附带详细的说明:
引言
ECharts 是一个功能强大的图表库,广泛应用于数据可视化场景。然而,其复杂的配置项和高学习曲线常常让开发者望而却步。本文将介绍一个精心设计的图表工厂系统,通过封装 ECharts 的复杂性,提供简洁的 API 和统一的开发体验,帮助开发者快速构建图表,提高效率和代码可维护性。本文将全面介绍其设计背景、架构、功能特性及使用方法,带你了解如何利用它简化图表开发。
设计背景:为什么我要重新封装一个图表工厂?
见此代码:
const option = {
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666'],
backgroundColor: '#ffffff',
xAxis: {
type: 'category',
data: ['A', 'B', 'C'],
axisLine: { lineStyle: { color: '#cccccc' } },
axisLabel: { color: '#666666' },
splitLine: { lineStyle: { color: '#cccccc', opacity: 0.4 } }
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#cccccc' } },
axisLabel: { color: '#666666' },
splitLine: { lineStyle: { color: '#cccccc', opacity: 0.4 } }
},
series: [{
type: 'bar',
data: [120, 200, 150]
}],
tooltip: {
backgroundColor: '#333333',
textStyle: { color: '#ffffff' }
},
grid: { left: '5%', right: '5%', bottom: '15%', top: '5%' }
};
这是一个标准的Echart 配置项,在实际开发过程中项目中往往不止一个Echart图表,同时图表的配置项也远比这负责,这就导致了:
- 配置重复:每个图表都需要重复设置颜色、背景、提示框等通用配置。
- 维护困难:修改主题或样式时,需要逐一调整每个图表的配置。
- 类型散乱:不同图表类型的配置差异大,缺乏统一抽象。
- 维护成本高:ECharts 的 API 庞大,写好的配置项难以更改。
基于这些问题,EChartFactory2 的设计目标是:
- 简化配置:从繁琐的手动配置转为简单的数据输入。
- 统一接口:让所有图表类型共享一致的调用方式。
- 集中管理:通过主题系统统一管理样式,支持动态切换。
- 易于扩展:方便添加新图表类型和功能。
架构设计:分层抽象的思考过程
核心设计思想:分离通用与特定
为了探寻Echart图表的设计规律我收集了项目中常见的图表配置,进行对比分析,如下:
// 柱状图配置示例
const barOption = {
color: ['#5470c6', '#91cc75', '#fac858'], // 🔄 重复出现
backgroundColor: '#ffffff', // 🔄 重复出现
grid: { left: '5%', right: '5%', top: '5%', bottom: '15%' }, // 🔄 重复出现
tooltip: { // 🔄 重复出现
backgroundColor: '#333333',
textStyle: { color: '#ffffff' }
},
xAxis: { // ✨ 图表特定
type: 'category',
data: ['销售', '市场', '研发'],
axisLine: { lineStyle: { color: '#cccccc' } },
axisLabel: { color: '#666666' }
},
yAxis: { // ✨ 图表特定
type: 'value',
axisLine: { lineStyle: { color: '#cccccc' } },
axisLabel: { color: '#666666' }
},
series: [{ // ✨ 图表特定
type: 'bar',
data: [320, 280, 450]
}]
};
// 折线图配置示例
const lineOption = {
color: ['#5470c6', '#91cc75', '#fac858'], // 🔄 重复出现
backgroundColor: '#ffffff', // 🔄 重复出现
grid: { left: '5%', right: '5%', top: '5%', bottom: '15%' }, // 🔄 重复出现
tooltip: { // 🔄 重复出现
backgroundColor: '#333333',
textStyle: { color: '#ffffff' }
},
xAxis: { // ✨ 图表特定(和柱状图相似)
type: 'category',
data: ['1月', '2月', '3月'],
axisLine: { lineStyle: { color: '#cccccc' } },
axisLabel: { color: '#666666' }
},
yAxis: { // ✨ 图表特定(和柱状图相似)
type: 'value',
axisLine: { lineStyle: { color: '#cccccc' } },
axisLabel: { color: '#666666' }
},
series: [{ // ✨ 图表特定(配置差异大)
type: 'line',
data: [820, 932, 901],
smooth: true,
symbol: 'circle'
}]
};
// 饼图配置示例
const pieOption = {
color: ['#5470c6', '#91cc75', '#fac858'], // 🔄 重复出现
backgroundColor: '#ffffff', // 🔄 重复出现
tooltip: { // 🔄 重复出现
backgroundColor: '#333333',
textStyle: { color: '#ffffff' }
},
// ❌ 注意:饼图没有 xAxis、yAxis、grid
series: [{ // ✨ 图表特定(完全不同)
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '搜索引擎' },
{ value: 735, name: '直接访问' },
{ value: 580, name: '邮件营销' }
]
}]
};
// 雷达图配置示例:
const radarOption = {
color: ['#5470c6', '#91cc75', '#fac858'], // 🔄 重复出现
backgroundColor: '#ffffff', // 🔄 重复出现
tooltip: { // 🔄 重复出现
backgroundColor: '#333333',
textStyle: { color: '#ffffff' }
},
// ❌ 注意:雷达图没有 xAxis、yAxis、grid
radar: { // ✨ 图表特定(独有的坐标系)
indicator: [
{ name: '销售', max: 100 },
{ name: '管理', max: 100 },
{ name: '技术', max: 100 }
]
},
series: [{ // ✨ 图表特定(又是不同的结构)
type: 'radar',
data: [{
value: [60, 73, 85],
name: '预算分配'
}]
}]
};
通过对比分析,我们可以发现这些图标基本可以划分成以下几部分:
// 通用配置(所有图表都需要,配置内容基本相同)
const universalConfig = {
color: [], // 调色板 - 所有图表都需要
backgroundColor: '', // 背景色 - 所有图表都需要
tooltip: {}, // 提示框 - 所有图表都需要,但触发方式可能不同
legend: {}, // 图例 - 大部分图表需要
toolbox: {} // 工具箱 - 看项目需求,但配置方式变化很小
};
// 特定配置(每种图表独有,配置内容差异很大)
const specificConfig = {
// 直角坐标系图表(柱状图、折线图、散点图)
xAxis: {}, // X轴配置
yAxis: {}, // Y轴配置
grid: {}, // 网格配置
// 极坐标图表
polar: {}, // 极坐标配置
angleAxis: {}, // 角度轴
radiusAxis: {}, // 径向轴
// 雷达图
radar: {}, // 雷达图配置(带指示器)
// 所有图表都有,但配置差异巨大
series: [] // 系列配置(每种图表类型完全不同)
};
如果能自动生成通用配置,只让用户关心数据和特定需求,Echart代码将得到极大程度的简化。
配置映射系统的设计
有了这个思路后,我开始思考:如果每种图表类型都有一个"配置生成器",那么我只需要告诉它图表类型和数据,它就能自动生成完整的配置。
最初的想法很简单,只要吧需要配置的东西单独抽象出来统一配置不就可以了:
const CHART_TYPE_CONFIGS = {
bar: {
series: (data) => ({ type: 'bar', data: data.data })
},
line: {
series: (data) => ({ type: 'line', data: data.data })
}
// ...
};
但很快我就发现问题了——不同图表需要的坐标系完全不同!
第一个难题:坐标系的差异
当我试图处理饼图时,发现它根本不需要 xAxis 和 yAxis,而雷达图需要的是 radar 配置。如果还是用传统思路,我又要写很多 if-else:
// 这样写太丑了...
if (chartType === 'pie') {
// 不要坐标轴
} else if (chartType === 'radar') {
// 要雷达配置
} else {
// 要直角坐标系
}
这时我意识到,坐标系才是图表的核心差异。于是我重新整理思路:
| 坐标系类型 | 适用图表 | 需要的配置 |
|-----------|---------|-----------|
| 直角坐标系 (cartesian) | 柱状图、折线图、散点图 | xAxis + yAxis + grid |
| 极坐标系 (polar) | 极坐标柱状图、极坐标折线图 | polar + angleAxis + radiusAxis |
| 雷达坐标系 (radar) | 雷达图 | radar (带indicator) |
| 无坐标系 (none) | 饼图 | 隐藏所有坐标轴 |
这样一来,我的配置映射就变成了两层结构:
const CHART_TYPE_CONFIGS = {
bar: {
coordinateSystem: 'cartesian', // 👈 指定用哪种坐标系
series: (data, theme, config) => ({ /* 系列配置 */ })
},
pie: {
coordinateSystem: 'none', // 👈 饼图不需要坐标系
series: (data, theme, config) => ({ /* 系列配置 */ })
}
};
第二个难题:主题样式的统一
有了坐标系分类,我又遇到新问题:每次创建图表都要设置颜色、背景色、字体等样式,这些重复工作能否自动化?
我回顾了之前写的图表,发现比较常见的是几种风格:
- 默认风格:白底黑字,全给Echart自动化
- 科技风格:黑底彩色,大屏常用
- 简约风格:浅色背景,较为正式
与其每次都手写这些样式,不如做成主题系统:
const themes = {
default: {
colors: {
series: ['#5470c6', '#91cc75', '#fac858'],
background: { chart: '#ffffff', tooltip: '#333333' },
text: { primary: '#333333', secondary: '#666666' }
}
},
futuristic: {
colors: {
series: ['#00d4ff', '#ff6b9d', '#7fff00'],
background: { chart: '#0a0a0a', tooltip: 'rgba(0,0,0,0.8)' },
text: { primary: '#ffffff', secondary: '#cccccc' }
}
}
};
这样我们就能一键切换整个图表的视觉风格了。
第三个难题:如何合并配置?
现在有了图表类型配置、坐标系配置、主题配置,但我们的逻辑是把Echart拆解成一个个独立部分,最终要合并在一起才是我们需要的ECharts 配置
显然简单的 Object.assign 不可行,因为Echarts配置是多层嵌套的:
const config1 = { series: [{ itemStyle: { color: 'red' } }] };
const config2 = { series: [{ itemStyle: { borderWidth: 2 } }] };
// Object.assign 会直接覆盖,丢失 color 配置
Object.assign(config1, config2);
// 结果:{ series: [{ itemStyle: { borderWidth: 2 } }] } ❌
// 我需要的是深度合并
// 结果:{ series: [{ itemStyle: { color: 'red', borderWidth: 2 } }] } ✅
所以我写了一个深度合并函数,确保所有配置都能正确合并。
整合:EChartFactory2 的诞生
有了这些基础设施,我开始设计核心的工厂类。我的设计原则是:
- 使用简单:简单调用函数就能创建
- 配置灵活:支持自定义配置覆盖默认值
- 功能完整:支持主题切换、类型切换、动态更新
于是有了这样的 API:
// 创建图表
const factory = new EChartFactory2(container, 'bar', 'default');
// 更新数据
factory.update({
xAxis: ['产品A', '产品B', '产品C'],
series: { data: [120, 200, 150] }
});
// 切换主题
factory.switchTheme('futuristic');
// 切换类型
factory.switchType('line');
实际效果:还算让人满意
我们做一个简单的对比,假设用户的需求是:
创建一个销售数据的柱状图,要求科技风格,支持堆叠显示。
传统写法:
const option = {
color: ['#00d4ff', '#ff6b9d', '#7fff00', '#ffaa00'],
backgroundColor: '#0a0a0a',
grid: {
left: '3%', right: '4%', bottom: '3%', top: '4%',
containLabel: true,
borderColor: '#333333'
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderColor: '#00d4ff',
borderWidth: 1,
textStyle: { color: '#ffffff', fontSize: 12 },
axisPointer: {
type: 'shadow',
shadowStyle: { color: 'rgba(0, 212, 255, 0.2)' }
}
},
legend: {
textStyle: { color: '#ffffff' },
icon: 'rect',
itemHeight: 8,
itemGap: 20
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月'],
axisLine: { lineStyle: { color: '#333333', width: 1 } },
axisLabel: { color: '#cccccc', fontSize: 11 },
splitLine: { show: false }
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#333333', width: 1 } },
axisLabel: { color: '#cccccc', fontSize: 11 },
splitLine: {
lineStyle: { color: '#333333', width: 0.5, opacity: 0.6 }
}
},
series: [
{
name: '销售额',
type: 'bar',
stack: 'total',
data: [120, 132, 101, 134],
itemStyle: {
color: '#00d4ff',
borderRadius: [2, 2, 0, 0]
}
},
{
name: '利润',
type: 'bar',
stack: 'total',
data: [220, 182, 191, 234],
itemStyle: {
color: '#ff6b9d',
borderRadius: [2, 2, 0, 0]
}
}
]
};
const chart = echarts.init(document.getElementById('chart'));
chart.setOption(option);
用工厂后:
const chart = createChart(document.getElementById('chart'), 'bar', 'futuristic');
chart.update({
xAxis: ['1月', '2月', '3月', '4月'],
series: [
{ name: '销售额', data: [120, 132, 101, 134] },
{ name: '利润', data: [220, 182, 191, 234] }
]
}, { stack: 'total' });
代码量从 60 行减少到 5 行,减少了 92%!
更重要的是,假设现在客户说"把这个图改成折线图",我只需要把代码中的bar改成line即可:
const chart = createChart(document.getElementById('chart'), 'line', 'futuristic');
chart.update({
xAxis: ['1月', '2月', '3月', '4月'],
series: [
{ name: '销售额', data: [120, 132, 101, 134] },
{ name: '利润', data: [220, 182, 191, 234] }
]
}, { stack: 'total' });
而且越复杂的配置,我这边修改起来就越简单,越统一。
详细代码可以从git中获取:
sq/UI/src/components/Echarts at main · afigzb/sq (github.com)https://github.com/afigzb/sq/tree/main/UI/src/components/Echarts