如上图,堆叠的时候,柱子上方显示堆叠的总数。那不堆叠的时候,如下图,就显示自己的数据
那么怎么实现这个功能呢(完整组件代码在文末)
首先要解决的问题1:
你怎么知道用户通过图例(legend)点击事件来切换系列的可见性?
initChart() {
this.chartInstance = echarts.init(this.$refs.chart);
this.updateChart();
// 添加图例点击事件监听
this.chartInstance.on("legendselectchanged", params => {
console.log("图例点击变化:", params);
console.log("当前选中图例:", params.selected);
// 更新堆叠图的标签统计值
this.updateStackedLabels(params.selected);
});
},
ok,那么就是要计算堆叠的时候,总额是多少了
// 计算堆叠组总和(只计算可见系列)
calculateStackTotal(series, stackName, dataIndex, selectedState) {
return series
.filter(s =>
s.stack === stackName &&
s.type === "bar" &&
selectedState[s.name] !== false
)
.reduce((sum, s) => sum + (s.data[dataIndex] || 0), 0);
},
计算好了显示上去呗
// 更新堆叠图标签统计值(只在最顶部柱子显示)
updateStackedLabels(selectedState) {
if (!this.chartInstance) return;
const option = this.chartInstance.getOption();
const series = option.series;
// 记录每个堆叠组的最顶部可见系列
const topVisibleSeries = {};
// 第一次遍历:找出每个堆叠组的最顶部可见系列
series.forEach(serie => {
if (serie.stack && selectedState[serie.name] !== false) {
if (!topVisibleSeries[serie.stack] ||
series.indexOf(serie) > series.indexOf(topVisibleSeries[serie.stack])) {
topVisibleSeries[serie.stack] = serie;
}
}
});
// 第二次遍历:设置标签显示
series.forEach(serie => {
if (serie.stack) {
const isVisible = selectedState[serie.name] !== false;
const isTopVisible = topVisibleSeries[serie.stack] === serie;
serie.label = {
show: this.showLabel && isVisible && isTopVisible,
position: "top",
color: "#000", // 黑色字体
formatter: params => {
if (!isVisible || !isTopVisible) return "";
const stackTotal = this.calculateStackTotal(
series,
serie.stack,
params.dataIndex,
selectedState
);
return this.$options.filters.ThousandSeparate(stackTotal, 2);
}
};
}
});
this.chartInstance.setOption({ series });
},
ok,我们的目标实现了:堆叠的时候显示统计值,不堆叠的时候显示自己的值
那么问题是,刷新页面,堆叠,显示的是各自的值,要点一下图例触发变动,才生效
那么就不太好,所以需要想办法触发一下。
已知
computed: {
// 生成图例数据
computedLegendData() {
return [
...this.seriesData.map(item => item.name),
...this.lineData.map(item => item.name)
].filter(Boolean);
}
}
legend: {
show: this.showLegend,
data: this.computedLegendData,
type: "scroll",
bottom: 0
},
于是,等他渲染完,要触发一下label的更新
updateChart() {
if (!this.chartInstance) return;
const option = {
...this.getBaseOption(),
series: this.getSeriesConfig()
};
this.chartInstance.setOption(option, true);
// 触发更新
const initialSelectedState = this.computedLegendData.reduce((acc, name) => {
acc[name] = true;
return acc;
}, {});
this.updateStackedLabels(initialSelectedState);
},
如此就大功告成了,最后附上这个组件的完整代码
这个组件我肯定还会继续优化,但是后面优化后的代码不会更新到本文了,有兴趣可以关注之后我发的博文。
点击查看代码
<!--
/**
* @file EchartsBarLine.vue
* @description 通用的 ECharts 柱状图与折线图组合组件。
*
* ### 功能说明:
* 该组件封装了基于 ECharts 的柱状图和折线图,支持多柱子、堆叠柱子、动态效果、背景网格线显示控制等功能。
*
* ### 封装原因:
* 为了提高代码复用性,简化复杂图表的开发流程,统一管理图表配置,减少重复代码。
*
* ### 优点:
* - 高度可定制:支持自定义横纵坐标、堆叠柱子、折线图等。
* - 灵活性强:通过 props 控制图表的各种属性(如动画、背景线、图注等)。
* - 易于维护:集中管理图表逻辑,便于后续扩展和修改。
*
* ### 使用场景:
* - 数据可视化展示:适用于需要同时展示柱状图和折线图的场景。
* - 多维度数据分析:适合对比多个数据系列的趋势或分布。
* - 动态数据更新:实时更新图表数据,展示动态变化趋势。
*/
-->
<template>
<div class="echarts-wrapper">
<a-spin v-if="isLoding" />
<div
ref="chart"
class="echarts-chart"
:style="{ width: width, height: height, display: isLoding ? 'none' : 'block' }"
/>
</div>
</template>
<script>
import * as echarts from "echarts";
import "../css/echarts-theme.less";
export default {
name: "EchartsBarLine",
props: {
// 图表宽度
width: {
type: String,
default: "100%"
},
// 图表高度
height: {
type: String,
default: "400px"
},
// 横坐标数据
xAxisData: {
type: Array,
required: true
},
// 纵坐标数据(多柱子、堆叠柱子)
seriesData: {
type: Array,
required: true
},
// 折线图数据
lineData: {
type: Array,
default: () => []
},
// 是否显示背景横线
showGridLines: {
type: Boolean,
default: true
},
// 图注
legendData: {
type: Array,
default: () => []
},
// 动画效果开关
animation: {
type: Boolean,
default: true
},
// 自定义颜色数组
colors: {
type: Array,
default: null // 如果未传入,则使用默认主题色
},
// 是否启用平滑曲线
smooth: {
type: Boolean,
default: true
},
// 柱状图宽度
barWidth: {
type: [String, Number],
default: "30%"
},
// 是否显示图例
showLegend: {
type: Boolean,
default: true
},
// 是否显示数据标签
showLabel: {
type: Boolean,
default: false
},
// 图表标题
title: {
type: String,
default: ""
},
// 图表副标题
subTitle: {
type: String,
default: ""
},
isLoding: {
type: Boolean,
default: false
}
},
data() {
return {
chartInstance: null,
resizeObserver: null
};
},
computed: {
finalColors() {
// 如果用户传入了自定义颜色数组,则使用传入的颜色
if (this.colors && this.colors.length > 0) {
return this.colors;
}
// 否则,从 CSS 变量中加载默认主题色
const rootStyles = window.getComputedStyle(document.documentElement);
const seriesColors = [];
for (let i = 1; i <= 16; i++) {
const color = rootStyles.getPropertyValue(`--series-color-${i}`).trim();
if (color) {
seriesColors.push(color);
}
}
return seriesColors;
},
// 生成图例数据
computedLegendData() {
return [
...this.seriesData.map(item => item.name),
...this.lineData.map(item => item.name)
].filter(Boolean);
}
},
watch: {
xAxisData: {
handler() {
this.updateChart();
},
deep: true
},
seriesData: {
handler() {
this.updateChart();
},
deep: true
},
lineData: {
handler() {
this.updateChart();
},
deep: true
},
showGridLines() {
this.updateChart();
},
legendData() {
this.updateChart();
},
colors() {
this.updateChart();
},
smooth() {
this.updateChart();
},
barWidth() {
this.updateChart();
},
showLegend() {
this.updateChart();
},
showLabel() {
this.updateChart();
},
title() {
this.updateChart();
},
subTitle() {
this.updateChart();
}
},
mounted() {
this.initChart();
this.initResizeObserver();
},
beforeDestroy() {
this.disposeChart();
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
},
methods: {
initChart() {
this.chartInstance = echarts.init(this.$refs.chart);
this.updateChart();
// 添加图例点击事件监听
this.chartInstance.on("legendselectchanged", params => {
// 更新堆叠图的标签统计值
this.updateStackedLabels(params.selected);
});
},
// 计算堆叠组总和(只计算可见系列)
calculateStackTotal(series, stackName, dataIndex, selectedState) {
return series
.filter(s =>
s.stack === stackName &&
s.type === "bar" &&
selectedState[s.name] !== false
)
.reduce((sum, s) => sum + (s.data[dataIndex] || 0), 0);
},
// 更新堆叠图标签统计值(只在最顶部柱子显示)
updateStackedLabels(selectedState) {
if (!this.chartInstance) return;
const option = this.chartInstance.getOption();
const series = option.series;
// 记录每个堆叠组的最顶部可见系列
const topVisibleSeries = {};
// 第一次遍历:找出每个堆叠组的最顶部可见系列
series.forEach(serie => {
if (serie.stack && selectedState[serie.name] !== false) {
if (!topVisibleSeries[serie.stack] ||
series.indexOf(serie) > series.indexOf(topVisibleSeries[serie.stack])) {
topVisibleSeries[serie.stack] = serie;
}
}
});
// 第二次遍历:设置标签显示
series.forEach(serie => {
if (serie.stack) {
const isVisible = selectedState[serie.name] !== false;
const isTopVisible = topVisibleSeries[serie.stack] === serie;
serie.label = {
show: this.showLabel && isVisible && isTopVisible,
position: "top",
color: "#000", // 黑色字体
formatter: params => {
if (!isVisible || !isTopVisible) return "";
const stackTotal = this.calculateStackTotal(
series,
serie.stack,
params.dataIndex,
selectedState
);
return this.$options.filters.ThousandSeparate(stackTotal, 2);
}
};
}
});
this.chartInstance.setOption({ series });
},
initResizeObserver() {
if (typeof ResizeObserver !== "undefined") {
this.resizeObserver = new ResizeObserver(() => {
this.handleResize();
});
this.resizeObserver.observe(this.$el);
} else {
window.addEventListener("resize", this.handleResize);
}
},
handleResize() {
if (this.chartInstance) {
this.chartInstance.resize();
}
},
getBaseOption() {
return {
backgroundColor: "transparent",
title: {
text: this.title,
subtext: this.subTitle,
left: "left",
textStyle: {
fontSize: 18,
fontWeight: "bold"
},
subtextStyle: {
fontSize: 14,
color: "#999"
}
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow"
},
confine: true,
appendToBody: true,
// 自定义 tooltip 格式化函数
formatter: params => {
let tooltipContent = "";
// 遍历每个数据项
params.forEach((item, index) => {
const value = item.value; // 获取当前数据值
const formattedValue = this.$options.filters.ThousandSeparate(value, 2); // 格式化数字
tooltipContent += `
<div>
<span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${item.color};"></span>
${item.seriesName}: ${formattedValue}
</div>
`;
});
// 添加 x 轴的名称
tooltipContent = `<strong>${params[0].name}</strong><br>` + tooltipContent;
return tooltipContent;
}
},
legend: {
show: this.showLegend,
data: this.computedLegendData,
type: "scroll",
bottom: 0
},
grid: {
left: "3%",
right: "3%",
bottom: this.showLegend ? "15%" : "3%",
top: "15%",
containLabel: true
},
xAxis: {
type: "category",
data: this.xAxisData,
axisTick: {
alignWithLabel: true
},
axisLabel: {
interval: 0,
rotate: 30
}
},
yAxis: {
type: "value",
splitLine: {
show: this.showGridLines,
lineStyle: {
type: "dashed"
}
}
}
};
},
getSeriesConfig() {
const series = [];
// 柱状图配置
this.seriesData.forEach((item, index) => {
series.push({
name: item.name,
type: "bar",
stack: item.stack || null,
data: item.data,
barWidth: this.barWidth,
animation: this.animation,
itemStyle: {
color: this.finalColors[index % this.finalColors.length],
borderRadius: [2, 2, 0, 0]
},
label: {
show: this.showLabel,
position: "top"
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: "rgba(0, 0, 0, 0.5)"
}
}
});
});
// 折线图配置
this.lineData.forEach((item, index) => {
series.push({
name: item.name,
type: "line",
data: item.data,
smooth: this.smooth,
animation: this.animation,
symbol: "circle",
symbolSize: 8,
itemStyle: {
color:
this.finalColors[
(index + this.seriesData.length) % this.finalColors.length
]
},
lineStyle: {
width: 3
},
label: {
show: this.showLabel,
position: "top"
}
});
});
return series;
},
updateChart() {
if (!this.chartInstance) return;
const option = {
...this.getBaseOption(),
series: this.getSeriesConfig()
};
this.chartInstance.setOption(option, true);
// 触发更新
const initialSelectedState = this.computedLegendData.reduce((acc, name) => {
acc[name] = true;
return acc;
}, {});
this.updateStackedLabels(initialSelectedState);
},
disposeChart() {
if (this.chartInstance) {
this.chartInstance.dispose();
this.chartInstance = null;
}
}
}
};
</script>
<style scoped>
.echarts-wrapper {
position: relative;
width: 100%;
height: 100%;
min-height: 300px;
}
.echarts-chart {
width: 100%;
height: 100%;
}
/* 响应式调整 */
@media (max-width: 768px) {
.echarts-wrapper {
padding: 8px;
}
/* 调整X轴标签 */
:deep(.echarts-axis-x .echarts-axis-label) {
font-size: 10px;
transform: rotate(-45deg);
margin-top: 5px;
}
/* 调整图例位置 */
:deep(.echarts-legend) {
bottom: 5px !important;
}
}
</style>