概要

在前端开发中,经常能和图表之类的插件打交道,ECharts算是非常熟悉的老朋友了。封装ECharts的想法是来自于一次开发中碰到根据不同数据源渲染出有差异的图表。比方说选择数据源A,图表里要显示数据源A里的温度,压力和流量的趋势折线图。选择数据源B,图表里要显示数据源B里的温度,热负荷的趋势折线图。

先前的方案是把图表的初始化和渲染分开,这样虽然能复用一部分。但是在渲染图表部分,充斥着大量重复判断逻辑,相似的功能随着数据源扩大不停的堆叠,严重违背了DRY原则。

“Don’t Repeat Yourself” (DRY) 是一种软件开发原则,它鼓励开发人员避免在系统中重复代码。DRY 背后的主要思想是通过确保特定知识或逻辑仅存在于代码库中的一个位置来减少冗余并提高效率。当开发人员遵循 DRY 原则时,他们的目标是创建可在代码库的各个部分使用的可重用组件、函数或模块。这不仅使代码更易于维护,而且还最大限度地减少了出错的机会,因为只需要在一个位置进行更改或更新。

于是乎便有了本文基于Class封装的ECharts动态生成器组件,该ECharts动态生成器着重于封装echart的option属性的生成逻辑,通过模板化的参数灵活生成不同的options。 希望能给到大家一些帮助。

整体构造

主要构成部分

  1. 核心类:EChartDynamicGenerator 类作为核心,负责整个图表的生命周期管理,包括初始化、渲染、调整大小和销毁等操作。
  2. 配置管理:通过注册机制管理两种配置类型:
  • yAxisTypes:管理 Y 轴配置,每种配置包含名称、位置、格式等属性。
  • dataTypes:管理数据类型配置,每种配置关联特定的 Y 轴类型,并定义多个数据系列。
  1. 配置生成:通过多个私有方法生成 ECharts 所需的完整配置:
  • getUsedYAxisTypes:收集需要使用的 Y 轴类型。
  • generateYAxisTypeIndexMap:生成 Y 轴类型到索引的映射。
  • generateYAxis:生成 Y 轴配置数组。
  • generateSeries:根据数据类型配置生成系列数据。
  • generateEChartConfig:整合所有配置,生成最终的 ECharts 配置。
  1. 生命周期管理:提供了完整的生命周期方法,包括初始化、渲染、调整大小和销毁,确保资源的正确管理。

构造器

EChartDynamicGenerator构造器保存四个属性,分别是echartId,echart实例,y轴类型和实际数据类型。

constructor(echartId) {
    this.echartId = echartId;
    this.chartInstance = null;
    //y轴类型。该对象维护一个映射,key为y轴类型标识,value为y轴配置
    this.yAxisTypes = {};
    //数据类型。该对象维护一个映射,key为数据类型标识,value为数据类型配置
    this.dataTypes = {};

    // 添加窗口卸载时的资源清理
    window.addEventListener('beforeunload', this.destroy.bind(this));
}

配置注册与配置参数构造示例

通过注册机制管理两种配置类型,yAxisTypes 与 dataTypes

/**
 * 注册新的y轴类型
 * @param {String} type 轴类型标识
 * @param {Object} config 轴配置
 * 示例:this.registerYAxisType('temperature', {
        name: '温度℃',
        position: 'left',
        formatter: '{value}',
        color: '#555555',
        offset: 20

    });
 */
registerYAxisType(type, config) {
    this.yAxisTypes[type] = config;
}

/**
 * 注册新的数据类型
 * @param {String} type 数据类型标识
 * @param {Object} config 数据类型配置
 * 示例: this.registerDataType('OneTemp', {
        yAxisType: 'temperature',
        series: [
            { name: '一次供水温度℃', dataKey: 'oneGoTemp', color: '#ff6600' },
            { name: '一次回水温度℃', dataKey: 'oneBackTemp', color: '#ff9900' }
        ]
    });
 */
registerDataType(type, config) {
    this.dataTypes[type] = config;
}

动态生成options

generateEChartConfig方法是EChartDynamicGenerator 类的核心实现,负责生成最终图表要的options,实现动态生成。

  1. 首先我们根据展示的数据类型确认需要用到的y轴类型。使用Set对象维护使用的y轴类型,因为存在数据共用y轴的情况。
    displayDataTypes是dataTypes配置的子集或全集

ES2022 开始,你能够借助#符号来定义私有方法

/**
 * 收集使用的y轴类型
 * @param {Array} displayDataTypes 展示的数据类型
 * @returns {Set} 使用的y轴类型集合
 */
 #getUsedYAxisTypes(displayDataTypes) {
    const usedYAxisTypes = new Set();
    for (const type of displayDataTypes) {
        const dataTypeConfig = this.dataTypes[type];
        if (dataTypeConfig && this.yAxisTypes[dataTypeConfig.yAxisType]) {
            usedYAxisTypes.add(dataTypeConfig.yAxisType);
        }
    }
    return usedYAxisTypes;
}
  1. 生成y轴类型索引映射,方便后续series项的yAxisIndex。
/**
 * 生成y轴类型索引映射
 * @param {Set} usedYAxisTypes 使用的轴类型集合
 */
#generateYAxisTypeIndexMap(usedYAxisTypes) {
    const yAxisTypeIndexMap = new Map();
    let index = 0;
    usedYAxisTypes.forEach(type => {
        yAxisTypeIndexMap.set(type, index++);
    });
    return yAxisTypeIndexMap;
}
  1. 生成y轴配置
    根据遍历的需要显示的y轴类型集合,安装EChart的格式生成生成y轴配置。
/**
 * 生成y轴配置
 * @param {Set} usedYAxisTypes 使用的y轴类型集合
 */
#generateYAxis(usedYAxisTypes) {
    return Array.from(usedYAxisTypes).map(type => {
        const axisConfig = this.yAxisTypes[type];
        return {
            name: axisConfig.name,
            position: axisConfig.position,
            type: 'value',
            axisLabel: {
                formatter: axisConfig.formatter,
                textStyle: {
                    color: axisConfig.color,
                    fontWeight: 500
                }
            },
            nameTextStyle: {
                color: axisConfig.color
            },
            splitLine: {
                show: true,
                lineStyle: {
                    color: '#55555520'
                }
            },
            offset: axisConfig.offset || 0
        };
    });
}
  1. 生成series配置,根据展示的数据类型生成series。在遍历dataTypeConfig.series时,使用解构配置项,获取关键的dataKey,该dataKey是用来获取数据的标识。

dataKey作为渲染方法传入的chartData对象的键名,从而引出后面的数据。chartData还有一个默认的xAxisData属性,包含的是x轴数据

/**
 * 生成series配置,根据展示的数据类型生成series
 * @param {Array} displayDataTypes 展示的数据类型
 * @param {Map} yAxisTypeIndexMap y轴类型索引映射
 * @param {Object} chartData 图表数据
 */
#generateSeries(displayDataTypes, yAxisTypeIndexMap, chartData) {
    const series = [];
    for (const type of displayDataTypes) {
        const dataTypeConfig = this.dataTypes[type];
        if (!dataTypeConfig) continue;
        const yAxisType = dataTypeConfig.yAxisType;
        const axisConfig = this.yAxisTypes[yAxisType];
        if (!axisConfig) continue;
        const yAxisIndex = yAxisTypeIndexMap.get(yAxisType);
        // 为该数据类型添加所属的series
        dataTypeConfig.series.forEach(seriesConfig => {
            // 解构配置项,分离dataKey和其他配置
            // dataKey是用来获取数据的标识
            const { dataKey, ...restConfig } = seriesConfig;
            // 创建series项
            const seriesItem = {
                ...restConfig,  // 透传其他配置项
                type: restConfig.type || 'line',
                yAxisIndex: yAxisIndex,
                data: chartData[dataKey] || []
            };
            // 动态添加柱状图圆角
            if (restConfig.type === 'bar') {
                seriesItem.itemStyle = {
                    ...seriesItem.itemStyle,
                    barBorderRadius: [10, 10, 10, 10]
                };
            }
            // 合并颜色配置
            if (restConfig.color) {
                seriesItem.itemStyle = {
                    ...seriesItem.itemStyle,
                    color: restConfig.color
                };
            }
            series.push(seriesItem);
        });
    }
    return series;
}
  1. 生成图表配置。将前面的各类生成的配置项和默认配置组合。实现代码复用
/**
 * 生成图表配置
 * @param {Array} displayDataTypes 展示的数据类型
 * @param {Object} chartData 图表数据
 * @param {Object} options 额外配置选项
 */
#generateEChartConfig(displayDataTypes, chartData, options) {
    const usedYAxisTypes = this.#getUsedYAxisTypes(displayDataTypes);
    const yAxisTypeIndexMap = this.#generateYAxisTypeIndexMap(usedYAxisTypes);
    const yAxis = this.#generateYAxis(usedYAxisTypes);
    const series = this.#generateSeries(displayDataTypes, yAxisTypeIndexMap, chartData);

    // 默认配置
    const defaultOptions = {
        tooltip: {
            trigger: 'axis',
            textStyle: {
                align: 'left'
            }
        },
        legend: {
            top: '4%'
        },
        grid: {
            left: '5%',
            right: '5%',
            top: '10%',
            bottom: '3%',
            containLabel: true
        },
        toolbox: {
            show: true,
            feature: {
                dataZoom: {
                    yAxisIndex: 'none'
                },
                dataView: { readOnly: false },
                magicType: { type: ['line', 'bar'] },
                restore: {},
                saveAsImage: {}
            }
        },
        xAxis: {
            type: 'category',
            boundaryGap: false,
            axisLine: {
                lineStyle: {
                    color: '#777777FF'
                }
            },
            data: chartData.xAxisData || []  // 防御性处理
        }
    };

    return {
        ...defaultOptions,
        ...options,
        yAxis,
        series
    };
}
  1. 组件的初始化与渲染。displayDataTypes为字符串数组,包含要展示的数据类型。chartData上文也简单提到了下,是一个包含数据的对象。默认xAxisData属性,包含的是x轴数据,其余键名和dataType.series属性里的dataKey保存一致,用于获取特定series的数据

return this 实现链式调用

/**
 * 初始化图表
 */
init() {
    this.chartInstance = echarts.init(document.getElementById(this.echartId));
    return this;
}

/**
 * 渲染图表
 * @param {Array} displayDataTypes 展示的数据类型
 * @param {Object} chartData 图表数据
 * @param {Object} [options] 额外配置选项
 */
render(displayDataTypes, chartData, options = {}) {
    if (!this.chartInstance) {
        console.error('echart图表尚未被初始化');
        return;
    }
    const config = this.#generateEChartConfig(displayDataTypes, chartData, options);
    this.chartInstance.clear();
    this.chartInstance.setOption(config);
    return this;
}

完整代码

/**********************************************************
 * @author		Araby
 * @version		1.0
 * @date		2025/6/12
 * @des	        ECharts图表动态生成器组件
 **********************************************************/

/**
 * ECharts图表生成器
 */
class EChartDynamicGenerator {
    constructor(echartId) {
        this.echartId = echartId;
        this.chartInstance = null;
        //y轴类型。该对象维护一个映射,key为y轴类型标识,value为y轴配置
        this.yAxisTypes = {};
        //数据类型。该对象维护一个映射,key为数据类型标识,value为数据类型配置
        this.dataTypes = {};

        // 添加窗口卸载时的资源清理
        window.addEventListener('beforeunload', this.destroy.bind(this));
    }

    /**
     * 注册新的y轴类型
     * @param {String} type 轴类型标识
     * @param {Object} config 轴配置
     * 示例:this.registerYAxisType('temperature', {
            name: '温度℃',
            position: 'left',
            formatter: '{value}',
            color: '#555555',
            offset: 20

        });
     */
    registerYAxisType(type, config) {
        this.yAxisTypes[type] = config;
    }

    /**
     * 注册新的数据类型
     * @param {String} type 数据类型标识
     * @param {Object} config 数据类型配置
     * 示例: this.registerDataType('OneTemp', {
            yAxisType: 'temperature',
            series: [
                { name: '一次供水温度℃', dataKey: 'oneGoTemp', color: '#ff6600' },
                { name: '一次回水温度℃', dataKey: 'oneBackTemp', color: '#ff9900' }
            ]
        });
     */
    registerDataType(type, config) {
        this.dataTypes[type] = config;
    }

    /**
     * 初始化图表
     */
    init() {
        this.chartInstance = echarts.init(document.getElementById(this.echartId));
        return this;
    }

    /**
     * 渲染图表
     * @param {Array} displayDataTypes 展示的数据类型
     * @param {Object} chartData 图表数据
     * @param {Object} [options] 额外配置选项
     */
    render(displayDataTypes, chartData, options = {}) {
        if (!this.chartInstance) {
            console.error('echart图表尚未被初始化');
            return;
        }
        const config = this.#generateEChartConfig(displayDataTypes, chartData, options);
        this.chartInstance.clear();
        this.chartInstance.setOption(config);
        return this;
    }

    /**
     * 收集使用的y轴类型
     * @param {Array} displayDataTypes 展示的数据类型
     * @returns {Set} 使用的y轴类型集合
     */
     #getUsedYAxisTypes(displayDataTypes) {
        const usedYAxisTypes = new Set();
        for (const type of displayDataTypes) {
            const dataTypeConfig = this.dataTypes[type];
            if (dataTypeConfig && this.yAxisTypes[dataTypeConfig.yAxisType]) {
                usedYAxisTypes.add(dataTypeConfig.yAxisType);
            }
        }
        return usedYAxisTypes;
    }

    /**
     * 生成图表配置
     * @param {Array} displayDataTypes 展示的数据类型
     * @param {Object} chartData 图表数据
     * @param {Object} options 额外配置选项
     */
    #generateEChartConfig(displayDataTypes, chartData, options) {
        const usedYAxisTypes = this.#getUsedYAxisTypes(displayDataTypes);
        const yAxisTypeIndexMap = this.#generateYAxisTypeIndexMap(usedYAxisTypes);
        const yAxis = this.#generateYAxis(usedYAxisTypes);
        const series = this.#generateSeries(displayDataTypes, yAxisTypeIndexMap, chartData);

        // 默认配置
        const defaultOptions = {
            tooltip: {
                trigger: 'axis',
                textStyle: {
                    align: 'left'
                }
            },
            legend: {
                top: '4%'
            },
            grid: {
                left: '5%',
                right: '5%',
                top: '10%',
                bottom: '3%',
                containLabel: true
            },
            toolbox: {
                show: true,
                feature: {
                    dataZoom: {
                        yAxisIndex: 'none'
                    },
                    dataView: { readOnly: false },
                    magicType: { type: ['line', 'bar'] },
                    restore: {},
                    saveAsImage: {}
                }
            },
            xAxis: {
                type: 'category',
                boundaryGap: false,
                axisLine: {
                    lineStyle: {
                        color: '#777777FF'
                    }
                },
                data: chartData.xAxisData || []  // 防御性处理
            }
        };

        return {
            ...defaultOptions,
            ...options,
            yAxis,
            series
        };
    }


    /**
     * 生成series配置,根据展示的数据类型生成series
     * @param {Array} displayDataTypes 展示的数据类型
     * @param {Map} yAxisTypeIndexMap y轴类型索引映射
     * @param {Object} chartData 图表数据
     */
    #generateSeries(displayDataTypes, yAxisTypeIndexMap, chartData) {
        const series = [];
        for (const type of displayDataTypes) {
            const dataTypeConfig = this.dataTypes[type];
            if (!dataTypeConfig) continue;
            const yAxisType = dataTypeConfig.yAxisType;
            const axisConfig = this.yAxisTypes[yAxisType];
            if (!axisConfig) continue;
            const yAxisIndex = yAxisTypeIndexMap.get(yAxisType);
            // 为该数据类型添加所属的series
            dataTypeConfig.series.forEach(seriesConfig => {
                // 解构配置项,分离dataKey和其他配置
                // dataKey是用来获取数据的标识
                const { dataKey, ...restConfig } = seriesConfig;
                // 创建series项
                const seriesItem = {
                    ...restConfig,  // 透传其他配置项
                    type: restConfig.type || 'line',
                    yAxisIndex: yAxisIndex,
                    data: chartData[dataKey] || []
                };
                // 动态添加柱状图圆角
                if (restConfig.type === 'bar') {
                    seriesItem.itemStyle = {
                        ...seriesItem.itemStyle,
                        barBorderRadius: [10, 10, 10, 10]
                    };
                }
                // 合并颜色配置
                if (restConfig.color) {
                    seriesItem.itemStyle = {
                        ...seriesItem.itemStyle,
                        color: restConfig.color
                    };
                }
                series.push(seriesItem);
            });
        }
        return series;
    }

    /**
     * 生成y轴配置
     * @param {Set} usedYAxisTypes 使用的y轴类型集合
     */
    #generateYAxis(usedYAxisTypes) {
        return Array.from(usedYAxisTypes).map(type => {
            const axisConfig = this.yAxisTypes[type];
            return {
                name: axisConfig.name,
                position: axisConfig.position,
                type: 'value',
                axisLabel: {
                    formatter: axisConfig.formatter,
                    textStyle: {
                        color: axisConfig.color,
                        fontWeight: 500
                    }
                },
                nameTextStyle: {
                    color: axisConfig.color
                },
                splitLine: {
                    show: true,
                    lineStyle: {
                        color: '#55555520'
                    }
                },
                offset: axisConfig.offset || 0
            };
        });
    }


    /**
     * 生成y轴类型索引映射
     * @param {Set} usedYAxisTypes 使用的轴类型集合
     */
    #generateYAxisTypeIndexMap(usedYAxisTypes) {
        const yAxisTypeIndexMap = new Map();
        let index = 0;
        usedYAxisTypes.forEach(type => {
            yAxisTypeIndexMap.set(type, index++);
        });
        return yAxisTypeIndexMap;
    }

    /**
     * 调整图表大小
     */
    resize() {
        if (this.chartInstance) {
            this.chartInstance.resize();
        }
        return this;
    }

    /**
     * 销毁图表实例
     */
    destroy() {
        if (this.chartInstance) {
            this.chartInstance.dispose();
            this.chartInstance = null;
        }
        return this;
    }
}

使用方式

/**
 * 换热站历史趋势图表控制器
 */
class TrendLineEChart {
    constructor(echartId) {
        this.echartId = echartId;
        this.echartGenerator = new EChartDynamicGenerator(echartId);
        this.init();
    }

    /**
     * 初始化控制器
     */
    init() {
        this.echartGenerator.init();
        // 监听窗口大小变化,自动调整图表
        window.addEventListener('resize', () => {
            this.echartGenerator.resize();
        });
        // 注册数据类型
        this.registerDataTypes();
    }

    /**
     * 注册本实例需要的数据类型,包括数据类型和y轴类型
     */
    registerDataTypes() {
        this.echartGenerator.registerYAxisType('temperature', {
            name: '温度℃',
            position: 'left',
            formatter: '{value}',
            color: '#555555',
            offset: 20

        });

        this.echartGenerator.registerYAxisType('pressure', {
            name: '压力MPa',
            position: 'right',
            formatter: '{value}',
            color: '#555555',
            offset: 20
        });
        // 在注册数据类型时直接指定颜色
        this.echartGenerator.registerDataType('OneTemp', {
            yAxisType: 'temperature',
            series: [
                { name: '一次供水温度℃', dataKey: 'oneGoTemp', color: '#ff6600' },
                { name: '一次回水温度℃', dataKey: 'oneBackTemp', color: '#ff9900' }
            ]
        });

        this.echartGenerator.registerDataType('TwoTemp', {
            yAxisType: 'temperature',
            series: [
                { name: '二次供水温度℃', dataKey: 'twoGoTemp', color: '#ff6633' },
                { name: '二次回水温度℃', dataKey: 'twoBackTemp', color: '#ffcc33' }
            ]
        });

        this.echartGenerator.registerDataType('OnePress', {
            yAxisType: 'pressure',
            series: [
                { name: '一次供水压力Mpa', dataKey: 'oneGoPress', color: '#009900' },
                { name: '一次回水压力Mpa', dataKey: 'oneBackPress', color: '#00ff66' }
            ]
        });

        this.echartGenerator.registerDataType('TwoPress', {
            yAxisType: 'pressure',
            series: [
                { name: '二次供水压力Mpa', dataKey: 'twoGoPress', color: '#00cc00' },
                { name: '二次回水压力Mpa', dataKey: 'twoBackPress', color: '#00cc99' }
            ]
        });
    }

    /**
     * 加载并渲染图表数据
     * @param {String} setId 换热站ID
     * @param {String} timeType 时间类型
     * @param {String} startTime 开始时间
     * @param {String} endTime 结束时间
     */
    loadAndRenderData(setId, timeType, startTime, endTime) {
        const loadingIndex = layer.load(0, { shade: [0.01, '#fff'] });
        $.ajax({
            type: 'GET',
            url: `你的api=${setId}&timeType=${timeType}&startTime=${startTime}&endTime=${endTime}`,
            dataType: 'json',
            success: (res) => {
                if (res.status != 0) {
                    layer.msg(res.msg, { time: 6000 });
                    return;
                }
                // 转换数据格式
                const chartData = this.transformData(res.data);
                // 获取展示的数据类型
                const displayDataTypes = returnEnergyType();
                // 渲染图表
                this.echartGenerator.render(displayDataTypes, chartData);
            },
            error: (res) => {
                console.error('生成图表错误', res);
            },
            complete: () => {
                layer.close(loadingIndex);
            }
        });
    }

    /**
     * 转换数据格式
     * @param {Array} apiData API返回的数据
     */
    transformData(apiData) {
        return {
            xAxisData: apiData.map(item => item.HisTime),
            oneGoTemp: apiData.map(item => item.OneGoTemp),
            oneBackTemp: apiData.map(item => item.OneBackTemp),
            oneGoPress: apiData.map(item => item.OneGoPress),
            oneBackPress: apiData.map(item => item.OneBackPress),
            twoGoTemp: apiData.map(item => item.TwoGoTemp),
            twoBackTemp: apiData.map(item => item.TwoBackTemp),
            twoGoPress: apiData.map(item => item.TwoGoPress),
            twoBackPress: apiData.map(item => item.TwoBackPress),
        };
    }

    /**
     * 刷新图表
     */
    refresh() {
        const setId = $('#rdoSetsContent input[type=radio][name=rdoSets]:checked').val();
        const timeType = $('input[name=radio_TimeType]:checked').val() || 1;
        const startTime = _startTime;
        const endTime = _endTime;

        this.loadAndRenderData(setId, timeType, startTime, endTime);
    }
}

let myTrendLineEChart;
$(document).ready(function () {
    myTrendLineEChart = new TrendLineEChart('chart_line_SetHisTrend');
    myTrendLineEChart.refresh();

    // 监听复选框变化,刷新图表
    $('input[type=checkbox][name=chkEnergyType]').on('change', function () {
        stationChartController.refresh();
    });

    // 监听单选按钮变化,刷新图表
    $('input[type=radio][name=rdoSets]').on('change', function () {
        stationChartController.refresh();
    });
});

小结

这样下来,一个通过 Class 封装 ECharts 动态生成器就完成了。以配置注册机制解耦 Y 轴与数据类型,实现多数据源图表的零重复逻辑渲染,遵循 DRY 原则提升代码可维护性。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐