echarts 自定义图例,并且一个图列控制多个系列
解决自定义图例无法控制系列显示/隐藏的问题。使用ECharts的dispatchAction
来手动控制系列可见性:
<template><div><!-- 自定义图例容器 --><div class="custom-legend"><div v-for="(type, index) in visibleTypes" :key="index"class="legend-item":class="{ 'disabled': !legendStatus[type] }"@click="toggleLegend(type)"><span class="legend-icon" :style="{ backgroundColor: typeColors[type] }"></span><span>{{ type }}</span></div></div><div id="chartSc" class="chart"></div></div>
</template><script>
import * as echarts from 'echarts';export default {props: {data: {type: Array,default: () => []},},data() {return {// 图例状态存储对象legendStatus: {},// 要显示的用电类型visibleTypes: ['办公楼', '供电所', '配电房', '变电站'],// 类型颜色映射typeColors: {'办公楼': '#0362D4','供电所': '#00B4FF','配电房': '#12E7BC','变电站': '#F4A637'},// 图表实例myChart: null,// 存储系列索引映射seriesIndexMap: {}};},watch: {data: {handler: function(newVal) {this.processChartData(newVal);},immediate: true,deep: true}},methods: {// 初始化图例状态initLegendStatus() {const status = {};this.visibleTypes.forEach(type => {status[type] = true;});this.legendStatus = status;},// 切换图例状态toggleLegend(type) {// 更新图例状态this.$set(this.legendStatus, type, !this.legendStatus[type]);// 控制对应系列的显示/隐藏this.toggleSeriesVisibility(type);},// 控制对应系列显示/隐藏toggleSeriesVisibility(type) {if (!this.myChart) return;// 获取该类型对应的所有系列索引const seriesIndices = this.seriesIndexMap[type];if (!seriesIndices || seriesIndices.length === 0) return;// 执行显示/隐藏操作seriesIndices.forEach(seriesIndex => {this.myChart.dispatchAction({type: 'legendToggleSelect',name: this.myChart.getOption().series[seriesIndex].name});});},processChartData(chartData) {if (!chartData || chartData.length === 0) {this.drawStackedBar([], [], false);return;}// 初始化图例状态this.initLegendStatus();// 从后端数据动态解析城市、类型和年份const cities = [...new Set(chartData.map(item => item.mgt_org_name))].filter(Boolean);const years = [...new Set(chartData.map(item => item.elec_year))].filter(Boolean).sort();// 计算数据最大值let maxValue = 0;chartData.forEach(item => {const value = parseFloat(item.papr) || 0;if (value > maxValue) maxValue = value;});const useTenThousand = maxValue > 100000; // 判断是否使用万单位// 初始化数据结构const dataMap = {};cities.forEach(city => {dataMap[city] = {};this.visibleTypes.forEach(type => {dataMap[city][type] = {};years.forEach(year => {dataMap[city][type][year] = 0;});});});// 填充数据chartData.forEach(item => {const city = item.mgt_org_name;const type = item.elec_type;const year = item.elec_year;let value = parseFloat(item.papr) || 0;// 如果类型不在可见类型中,跳过if (!this.visibleTypes.includes(type)) return;// 如果使用万单位,转换数据if (useTenThousand) {value = value / 10000;}if (cities.includes(city)) {dataMap[city][type][year] = value;}});// 准备系列数据const series = [];// 重置系列索引映射this.seriesIndexMap = {};// 为每个年份创建系列years.forEach(year => {this.visibleTypes.forEach(type => {const seriesName = `${type} (${year})`;// 添加到系列数据series.push({name: seriesName,type: 'bar',stack: year, // 按年份堆叠data: cities.map(city => dataMap[city][type][year]),itemStyle: { color: this.typeColors[type] },barWidth: 15,barGap: '25%',emphasis: {focus: 'series'}});// 记录系列索引if (!this.seriesIndexMap[type]) {this.seriesIndexMap[type] = [];}this.seriesIndexMap[type].push(series.length - 1);});});// 绘制图表this.drawStackedBar(cities, series, useTenThousand);},drawStackedBar(cities, series, useTenThousand) {const chartDom = document.getElementById('chartSc');if (!chartDom) return;// 销毁旧图表实例if (this.myChart) {echarts.dispose(this.myChart);}// 创建新图表实例this.myChart = echarts.init(chartDom);const unit = useTenThousand ? '万kWh' : 'kWh'; // 动态单位// 创建图表配置const option = {tooltip: {trigger: 'axis',axisPointer: { type: 'shadow' },formatter: function(params) {let result = `${params[0].name}<br>`;params.forEach(param => {if (param.value === null || param.value === undefined) return;// 格式化数值显示const formattedValue = typeof param.value === 'number' ? param.value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : param.value;result += `${param.marker} ${param.seriesName}: ${formattedValue} ${unit}<br>`;});return result;}},// 启用内置图例但隐藏legend: {show: true,selectedMode: false, // 禁用图例点击textStyle: { color: 'transparent' },itemStyle: { opacity: 0 },data: series.map(s => s.name)},grid: { top: '10%',bottom: '20%',left: '3%',right: '4%',containLabel: true },xAxis: {type: 'category',data: cities,axisLine: { lineStyle: { color: '#6f6e68' } },axisLabel: { color: '#fff', fontSize: 12,interval: 0},axisTick: { show: false }},yAxis: {type: 'value',name: unit,nameTextStyle: { color: '#fff', fontSize: 12 },axisLine: { show: false, lineStyle: { color: '#409eff' } },axisLabel: { color: '#fff', fontSize: 12,formatter: (value) => {// 格式化y轴标签return typeof value === 'number' ? value.toLocaleString(undefined, { maximumFractionDigits: 1 }) : value;}},splitLine: { lineStyle: { color: '#015B9F',type: 'dashed'} },axisTick: {show: false}},series: series,dataZoom: [{type: 'inside',start: 0,end: 100}]};// 设置图表配置this.myChart.setOption(option);// 添加点击事件处理this.myChart.on('click', (params) => {if (params.componentType === 'series') {// 获取点击的用电类型(系列名称)const elecType = params.seriesName.split('(')[0].trim();// 触发自定义事件并传递类型值this.$emit('type-click', elecType);}});// 初始化系列可见性this.visibleTypes.forEach(type => {if (!this.legendStatus[type]) {this.toggleSeriesVisibility(type);}});window.addEventListener('resize', () => this.myChart.resize());}},mounted() {this.$nextTick(() => {this.processChartData(this.data);});},beforeDestroy() {if (this.myChart) {echarts.dispose(this.myChart);}}
};
</script><style scoped>
.chart {width: 100%;height: 100%;min-height: 300px;
}/* 自定义图例样式 */
.custom-legend {display: flex;justify-content: center;padding: 10px 0;margin-bottom: 10px;
}.legend-item {display: flex;align-items: center;margin: 0 12px;cursor: pointer;transition: all 0.3s;padding: 4px 8px;border-radius: 4px;background-color: rgba(0, 0, 0, 0.2);
}.legend-item:hover {background-color: rgba(255, 255, 255, 0.1);
}.legend-icon {display: inline-block;width: 12px;height: 12px;border-radius: 50%;margin-right: 6px;
}.legend-item.disabled {opacity: 0.3;
}.legend-item.disabled .legend-icon {background-color: #ccc !important;
}
</style>
关键修复点:
-
使用ECharts的
dispatchAction
直接控制系列可见性:toggleSeriesVisibility(type) {if (!this.myChart) return;const seriesIndices = this.seriesIndexMap[type];if (!seriesIndices || seriesIndices.length === 0) return;seriesIndices.forEach(seriesIndex => {this.myChart.dispatchAction({type: 'legendToggleSelect',name: this.myChart.getOption().series[seriesIndex].name});}); }
-
构建系列索引映射:
// 在processChartData中 this.seriesIndexMap = {};years.forEach(year => {this.visibleTypes.forEach(type => {const seriesName = `${type} (${year})`;series.push({...});// 记录系列索引if (!this.seriesIndexMap[type]) {this.seriesIndexMap[type] = [];}this.seriesIndexMap[type].push(series.length - 1);}); });
-
启用内置图例但隐藏:
legend: {show: true,selectedMode: false, // 禁用图例点击textStyle: { color: 'transparent' },itemStyle: { opacity: 0 },data: series.map(s => s.name) }
- 这样可以利用ECharts的内置图例功能,但通过CSS隐藏它
- 保留图例功能以便
dispatchAction
可以正常工作
-
初始化系列可见性:
// 在drawStackedBar中 this.visibleTypes.forEach(type => {if (!this.legendStatus[type]) {this.toggleSeriesVisibility(type);} });
-
简化状态管理:
- 移除了不必要的状态变量
- 专注于使用
dispatchAction
直接操作图表
为什么这个版本能解决问题:
-
直接使用ECharts API:
- 通过
dispatchAction
直接触发图例选择操作,绕过了手动管理系列状态的复杂性 - 这是ECharts官方推荐的系列可见性控制方式
- 通过
-
精确的系列映射:
- 通过
seriesIndexMap
准确记录每种类型对应的所有系列索引 - 点击图例时能精确找到并控制所有相关年份的系列
- 通过
-
保留内置图例功能:
- 虽然隐藏了内置图例,但保留了它的功能
- 这样自定义图例的操作可以通过API映射到内置图例功能上
-
可靠的初始化:
- 确保在图表渲染后立即应用初始可见性状态
这个解决方案应该能100%解决点击图例无法控制系列显示/隐藏的问题。点击"办公楼"图例会同时控制"办公楼(2024)"和"办公楼(2025)"系列的显示与隐藏,并且会更新图例的禁用状态样式。