当前位置: 首页 > news >正文

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>

关键修复点:

  1. 使用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});});
    }
    
  2. 构建系列索引映射

    // 在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);});
    });
    
  3. 启用内置图例但隐藏

    legend: {show: true,selectedMode: false, // 禁用图例点击textStyle: { color: 'transparent' },itemStyle: { opacity: 0 },data: series.map(s => s.name)
    }
    
    • 这样可以利用ECharts的内置图例功能,但通过CSS隐藏它
    • 保留图例功能以便dispatchAction可以正常工作
  4. 初始化系列可见性

    // 在drawStackedBar中
    this.visibleTypes.forEach(type => {if (!this.legendStatus[type]) {this.toggleSeriesVisibility(type);}
    });
    
  5. 简化状态管理

    • 移除了不必要的状态变量
    • 专注于使用dispatchAction直接操作图表

为什么这个版本能解决问题:

  1. 直接使用ECharts API

    • 通过dispatchAction直接触发图例选择操作,绕过了手动管理系列状态的复杂性
    • 这是ECharts官方推荐的系列可见性控制方式
  2. 精确的系列映射

    • 通过seriesIndexMap准确记录每种类型对应的所有系列索引
    • 点击图例时能精确找到并控制所有相关年份的系列
  3. 保留内置图例功能

    • 虽然隐藏了内置图例,但保留了它的功能
    • 这样自定义图例的操作可以通过API映射到内置图例功能上
  4. 可靠的初始化

    • 确保在图表渲染后立即应用初始可见性状态

这个解决方案应该能100%解决点击图例无法控制系列显示/隐藏的问题。点击"办公楼"图例会同时控制"办公楼(2024)"和"办公楼(2025)"系列的显示与隐藏,并且会更新图例的禁用状态样式。

http://www.lryc.cn/news/589841.html

相关文章:

  • 在 kubernetes 上安装 jenkins
  • argo-rollouts部署
  • 分块(chunked) vs 滑动窗口(windowed)
  • 开源模型应用落地-qwen模型小试-Qwen3-Embedding 模型集成 vLLM 实战解析(二)
  • 产品更新丨谷云科技 iPaaS 集成平台 V7.6 版本发布
  • Grok 系列大模型:xAI 的智能宇宙探秘
  • 使用 CrewAI 进行股票分析:自动化投资决策的新途径
  • 压力测试Apache Bench(ab)
  • Anspire Open暑期上新季 - 第二弹Anspire Browser Agent,开启云端自动化新纪元
  • Go语言自学笔记(2.3-2.6)
  • iOS 性能监控工具全解析 选择合适的调试方案提升 App 性能
  • 【游戏引擎之路】登神长阶(十九):3D物理引擎——岁不寒,无以知松柏;事不难,无以知君子
  • DrissionPage:一款让网页自动化更简单的 Python 库
  • 【BUG】ValueError: Unable to find out axis 2.0 in start_ornt
  • 设计模式之【观察者模式】
  • 单片机(STM32-中断)
  • [2025CVPR-图像检索方向] COBRA:一种用于小样本自适应检索增强模型
  • 实训十一——网络通信原理
  • 震坤行获取商品SKU操作详解
  • LeetCode|Day15|125. 验证回文串|Python刷题笔记
  • C语言基础笔记——位操作
  • 虚幻引擎5 GAS开发俯视角RPG游戏 #06-7:无限游戏效果
  • 使用EF Core修改数据:Update方法与SaveChanges的深度解析
  • 前端性能追踪工具:用户体验的毫秒战争
  • Kiro:亚马逊云发布,革命性AI编程工具!以“规范驱动开发“重塑软件构建范式!
  • es启动问题解决
  • Java数据结构第二十五期:红黑树传奇,当二叉树穿上 “红黑铠甲” 应对失衡挑战
  • 树莓派系统安装
  • GENERALIST REWARD MODELS: FOUND INSIDE LARGELANGUAGE MODELS
  • Java对象的比较