Ecahrts图表开发最佳实践探索:主题和数据集

Echarts 使用门槛很低,对照着配置项手册能轻松搭建出一个图表。

但是一个复杂的可视化页面会包含很多图表,一个项目中也可能会有很多可视化页面,仅仅使用 Echarts 基础能力会导致配置项很庞大,并且不利于统一修改。

这种同一项目下的不同页面其实往往会有一个大致的主题风格,基于这种背景就可以思考一下 Echarts 图表开发的最佳实践。

下面我分样式和数据两块来介绍。

  1. 主题管理样式
  2. 数据集管理数据

使用主题管理样式

Echarts 原生支持主题功能。

主题是一份基础样式配置,具体图表的 option 中没有配置的样式规则,会使用主题中的规则,这最大的好处是便于统一管理和减少 option 配置代码,在项目有统一风格的背景下非常好用。

Echarts5 除了官网展示的默认主题外,还内置了 dark 主题,可以像这样切换成深色模式:

javascript 复制代码
echarts.init(dom, 'dark');

另外,也可以自己增加主题:

javascript 复制代码
const myTheme = {/***/}
echarts.registerTheme('themeName', myTheme);
echarts.init(dom, 'themeName');

你可以在 官方主题编辑器 中直观感受。

比如要开发一个这样的图表:

先注册一个主题

tsx 复制代码
echarts.registerTheme('themeName', {
  color: ["#50b1f9", "#ead267", "#de8344"],
  textStyle: { fontSize: 12, color: "#fff" },
  tooltip: {/*省略具体配置*/},
  bar: {/*省略具体配置*/},
  legend: {/*省略具体配置*/},
  grid: {/*省略具体配置*/},
  categoryAxis: {/*省略具体配置*/},
  valueAxis: {/*省略具体配置*/},
})

然后生成图表

tsx 复制代码
const chart = echarts.init(dom, 'themeName');
chart.setOption({
  series: [
    {type: 'bar', data: [/**/]},
    {type: 'bar', data: [/**/]},
  ],
  yAxis: {type: 'value'},
  xAxis: {type: 'category'},
})

只需要这么点配置项就可以生成一个拥有深度个性化样式的图表,如果需要做很多个统一主题风格的图表,就会很方便。

主题的配置项

主题和 option 配置项并不完全相同,官网也没有给出主题具体的配置项手册,并且 主题编辑器 导出的 JSON 文件只能用于在编辑器中导入使用,而不能直接作为主题在 Echarts 中注册。😓

所以初次使用会比较疑惑,不知道怎么配,其实大部分配置和 option 是相同的。这里我列出和 option 不同的常用主题配置项:

配置名称 规则
categoryAxis 可以是 yAxis 也可以是 xAxis,要看哪个的 type 是 category
valueAxis 可以是 yAxis 也可以是 xAxis,要看哪个的 type 是 value
bar、line、pie、map等等 具体 series 类型,比如 bar 就是 type 是 bar 的 series,所有类型的 series 都是这样。

可能还有一些和 option 不同的没有列出。

扩展原生主题

有一些属性是不支持在主题中直接配置的。

对于某些特定的图表组件或功能,比如 dataZoom,主题不会支持它全部的配置细节,因为这些组件具有较强的功能特性和复杂的用户交互。

虽然不支持直接配置,但是主题中其他配置可能会作用到 dataZoom 上,比如颜色、字体等。

不过,我们可以自己封装一层让主题直接支持这些不被支持的属性。

思路: 做一个 Echarts 通用组件,作用是传入一个 option 就渲染出一个图表。然后将主题配置保存起来以便组件内部获取到,然后在组件内将我们自己扩展的配置合并到调用者传入的 option 上,比如用 lodash.merge 方法。

来看代码,第一步导出自己的主题配置方法,使用者不可以直接调用 ecahrts.registerTheme,使用我们导出的方法,目的是将主题保存起来:

typescript 复制代码
import { registerTheme } from "Echarts/core";
import { cloneDeep, merge } from "lodash-es";

const themeStore = new Map<string, Theme>();

export const registerEchartsTheme = (name: string, content: Theme) => {
  // 先保存
  themeStore.set(name, content);
  // 再注册
  registerTheme(name, content);
};

export const getEchartsTheme = (name: string) => {
  return cloneDeep(themeStore.get(name));
};

第二步做一个通用组件,这里我选择在 echarts-for-react 基础上封装。

tsx 复制代码
import type { EchartsReactProps } from "echarts-for-react";
import ReactEcharts from "Echarts-for-react";
import { cloneDeep, merge } from "lodash-es";
import React from "react";

import { getEchartsTheme } from "./themes";

export interface EchartsProps extends EchartsReactProps {
  style?: React.CSSProperties;
  theme?: string | null;
}

const Echarts = React.memo(
  React.forwardRef<ReactEcharts, EchartsProps>((props, ref) => {
    
    // 拿到主题配置
    const themeContent = React.useMemo(
      () => getEchartsTheme(props.theme) ?? {},
      [props.theme]
    );

    const mergeDataZoom = (dz: object | object[]) => {
      const copyDz = cloneDeep(dz);

      const mergeImpl = (singleDz) => {
        switch (singleDz.type) {
          case "slider":
            // 扩展的sliderDataZoom配置
            merge(singleDz, themeContent.sliderDataZoom ?? {});
            break;
          case "inside":
            // 扩展的insideDataZoom配置
            merge(singleDz, themeContent.insideDataZoom ?? {});
            break;
          default:
            break;
        }
      };

      if (Array.isArray(copyDz)) {
        copyDz.forEach((o) => mergeImpl(o));
      } else {
        mergeImpl(copyDz);
      }

      return copyDz;
    };

    const internalOpt = { ...props.option };
    if (internalOpt.dataZoom) {
      // 如果传入的option存在dataZoom配置,则和主题中的相关配置做合并
      internalOpt.dataZoom = mergeDataZoom(internalOpt.dataZoom);
    }

    return (
      <ReactEcharts
        {...props}
        ref={ref}
        theme={props.theme}
        option={internalOpt}
        style={props.style}
      />
    );
  })
);

通过以上代码我们成功为主题扩展了 sliderDataZoominsideDataZoom 配置,分别对应以下两个类型的 dataZoom 组件:

所有不被原生主题直接支持的配置项都可以按照这样的方式自由扩展,具体的扩展规则就要由开发者自己决定了。

formatter 函数的扩展

这是 Echarts 原生的 tooltip 样式

我们往往需要更复杂的 tooltip,比如

  1. 复杂的样式
  2. 需要在展示数据前做一些转换
  3. 增加数值单位
  4. ......

在 Echarts 的 rich 功能不能满足需求时,往往要自己写 tooltip.formatter函数。

假设我们已经在主题里写了一个 formatter 函数,提供了统一的样式支持。

比较尴尬的一点是,这种函数通常需要一些外部的状态,比如要给它传入数值的单位。主题中的 formatter 函数显然没办法接受参数。

那这种场景就要放弃使用主题了吗?当然不是,我们还是要自己扩展。

在上面的 Echarts 组件的基础之上,加一个 prop 名为 formatterArg,意思是 formatter 函数的参数。

tsx 复制代码
import type { EchartsReactProps } from "Echarts-for-react";

export interface EchartsProps extends EchartsReactProps {
  style?: React.CSSProperties;
  theme?: string | null;
  formatterArg?: any;
}

在组件内部拦截 option 或者主题的 formatter 函数

tsx 复制代码
const internalOpt = {...props.option}
if (props.formatterArg) {
  const tooltipFmtFun =
    internalOpt.tooltip?.formatter ?? themeContent.tooltip?.formatter;
  // 如果有formatter函数,再包一层函数
  if (typeof tooltipFmtFun === "function") {
    internalOpt.tooltip.formatter = (...args) => {
      return tooltipFmtFun(...args, formatterArg);
    };
  }
}

这样就成功把 formatterArg 传到了 formatter 函数中了,这里以 tooltip 来举例,其他所有组件的 formatter 都可以按这样的方式注入 formatterArg

也就是说,主题中的 formatter 指定一个接受的参数规则,所有图表只需要按照这个规则传入 formatterArg 即可,依然可以享受主题的统一性,不需要写很多个格式化函数。

tsx 复制代码
<Echarts formatterArg={{......}} option={{......}} />

主题比公共 option 好在哪

到这里主题就介绍完了,我们做了一个基础的 Echarts 组件,props 支持接收一个主题名字。

如果再加一个 Context 层,可以做到修改一次 Context,所有图表主题统一切换。

对比一下主题比公共 option 好在哪,之前没有使用主题时,为了减少配置项代码,通常会写一些公共 option,在具体组件的 option 中引入,然后再修改,但这和主题对比有两个缺点:

  1. 解构层级可能很深
tsx 复制代码
// 公共option
const commonOption = {
  yAxis: {
    type: 'value',
    label: {
      textStyle: {
        color: 'red'
      }
    }
  }
}

//具体组件option
const option = {
  ...commonOption,
  yAxis: {
    ...commonOption.yAxis,
    itemStyle: {
      ...commonOption.yAxis.itemStyle,
      color: 'green'
    }
  }
}
  1. option 中 series 使用不方便,主题可以直接配置具体 series 的样式。
tsx 复制代码
// 公共option
const commonOption = {
  series: [
    {
      type: 'bar',
      /***/
    },
    {
      type: 'line',
      /***/
    },
  ]
}

//具体组件option,比如需要两个折线
const option = {
  series: [
    {
      // 需要知道commonOption中line类型在第几位
      ...commonOption.series[1]
      type: 'line',
      /***/
    },
    {
      ...commonOption.series[1]
      type: 'line',
      /***/
    },
  ]
}


const theme = {
  // 主题直接定义折线图
  line: {/***/}
}

不仅 series ,所有同时支持数组和对象格式的情况,或者 yAxis 既可能是分类轴也可能是数值轴的情况,都会有这种问题。主题可以直接配置 line (具体系列类型)、categoryAxis等。

另外,使用主题并不影响使用公共 option,可以共同使用。往往是主题 + 公共 option 一起会更好用


使用数据集管理数据

在实际开发中,推荐将数据和样式配置分离开,这也是官网的建议:

数据集(dataset)是专门用来管理数据的组件。虽然每个系列都可以在 series.data 中设置数据,但是从 ECharts4 支持数据集开始,更推荐使用数据集来管理数据。因为这样,数据可以被多个组件复用,也方便进行 "数据和其他配置" 分离的配置风格。毕竟,在运行时,数据是最常改变的,而其他配置大多并不会改变。

可以阅读官方文档来学习数据集的具体用法,这里就不赘述了,官方也有一个表格数据转换工具

如果根据自己项目的需求开发一个将后端数据转为数据集格式的公共函数,开发图表的冗余代码会进一步减少。

数据集的局限性

数据集可以覆盖大多数场景,但也有其局限性,当样式需要通过数据来算得出时,数据集不容易应付。

比如下面这样的图表

柱体和柱体的数据标签会因数据的正负而改变方向,这种场景下需要拿到每个系列的具体数据,既然已经得出系列的具体数据,也就没有必要使用数据集了。

相关推荐
莹雨潇潇11 分钟前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr20 分钟前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho1 小时前
【TypeScript】知识点梳理(三)
前端·typescript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记3 小时前
【复习】HTML常用标签<table>
前端·html
丁总学Java3 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele3 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀4 小时前
CSS——属性值计算
前端·css
DOKE4 小时前
VSCode终端:提升命令行使用体验
前端