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、gridseries: [{ // ✨ 图表特定(完全不同)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、gridradar: { // ✨ 图表特定(独有的坐标系)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