ECharts图表工厂,完整代码+思路逻辑

发布于:2025-05-23 ⋅ 阅读:(18) ⋅ 点赞:(0)

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图表,同时图表的配置项也远比这负责,这就导致了:

  1. 配置重复:每个图表都需要重复设置颜色、背景、提示框等通用配置。
  2. 维护困难:修改主题或样式时,需要逐一调整每个图表的配置。
  3. 类型散乱:不同图表类型的配置差异大,缺乏统一抽象。
  4. 维护成本高: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 的诞生

有了这些基础设施,我开始设计核心的工厂类。我的设计原则是:

  1. 使用简单:简单调用函数就能创建
  1. 配置灵活:支持自定义配置覆盖默认值
  1. 功能完整:支持主题切换、类型切换、动态更新

于是有了这样的 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