DSDSWeb/src/views/statistics/components/BarChart.vue

493 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
ref="chartContainer"
class="bar-chart-host"
:style="chartHostStyle"
/>
</template>
<script>
import * as echarts from 'echarts';
export default {
name: 'AdvancedMixedChart',
props: {
title: {
type: String,
default: '图表标题'
},
chartData: {
type: [Array, Object],
default: () => []
},
categories: {
type: Array,
default: () => []
},
chartType: {
type: String,
default: 'single', // 'single' | 'multiple' | 'mixed' | 'stacked'
validator: (value) => ['single', 'multiple', 'mixed', 'stacked'].includes(value)
},
colors: {
type: Array,
default: () => ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272']
},
// 新增是否显示双Y轴
dualYAxis: {
type: Boolean,
default: false
},
/** 双 Y 轴时左侧轴名称 */
leftAxisName: {
type: String,
default: ''
},
/** 双 Y 轴时右侧轴名称 */
rightAxisName: {
type: String,
default: ''
},
/** 图表区域高度(像素) */
height: {
type: Number,
default: 300
},
// 新增堆叠组名称用于stacked模式
stackName: {
type: String,
default: '总量'
}
},
data() {
return {
myChart: null
};
},
computed: {
chartHostStyle() {
const h = typeof this.height === 'number' ? `${this.height}px` : '300px';
return { width: '100%', height: h };
}
},
mounted() {
this.initChart();
},
beforeDestroy() {
if (this.myChart) {
this.myChart.dispose();
}
window.removeEventListener('resize', this.handleResize);
},
methods: {
initChart() {
this.$nextTick(() => {
this.myChart = echarts.init(this.$refs.chartContainer);
this.renderChart();
window.addEventListener('resize', this.handleResize);
});
},
// 处理数据格式
processChartData() {
if (this.chartType === 'single') {
// 单系列柱状图
const data = Array.isArray(this.chartData) && this.chartData.length > 0
? this.chartData
: new Array(this.categories.length).fill(0);
return [{
name: '数量',
type: 'bar',
data: data
}];
} else if (this.chartType === 'multiple') {
// 多系列柱状图
if (Array.isArray(this.chartData) && this.chartData.every(item => typeof item === 'number')) {
return [{
name: '系列1',
type: 'bar',
data: this.chartData
}];
} else if (Array.isArray(this.chartData)) {
return this.chartData.map((series, index) => ({
name: series.name || `系列${index + 1}`,
type: series.type || 'bar',
data: series.data || [],
yAxisIndex: series.yAxisIndex || 0
}));
}
} else if (this.chartType === 'mixed') {
// 柱状图和折线图组合
if (Array.isArray(this.chartData)) {
return this.chartData.map((series, index) => ({
name: series.name || (series.type === 'line' ? '折线' : '柱状') + (index + 1),
type: series.type || (index === 0 ? 'bar' : 'line'),
data: series.data || [],
yAxisIndex:
series.yAxisIndex !== undefined && series.yAxisIndex !== null
? series.yAxisIndex
: (this.dualYAxis && index > 0 ? 1 : 0)
}));
}
} else if (this.chartType === 'stacked') {
// 堆叠柱状图 [2,3](@ref)
if (Array.isArray(this.chartData) && this.chartData.every(item => typeof item === 'number')) {
// 如果是数字数组,转换为单个堆叠系列
return [{
name: '数量',
type: 'bar',
data: this.chartData,
stack: this.stackName // 设置堆叠属性 [2](@ref)
}];
} else if (Array.isArray(this.chartData)) {
// 处理多系列堆叠柱状图
return this.chartData.map((series, index) => ({
name: series.name || `系列${index + 1}`,
type: 'bar',
data: series.data || [],
yAxisIndex: series.yAxisIndex || 0,
stack: series.stack || this.stackName // 设置堆叠属性相同stack值的系列会堆叠 [2,3](@ref)
}));
}
}
return [{
name: '数据',
type: 'bar',
data: new Array(this.categories.length).fill(0)
}];
},
// 获取Y轴配置
getYAxisConfig() {
if (!this.dualYAxis) {
// 单Y轴配置
return [{
type: 'value',
axisLine: {
show: true,
lineStyle: {
color: '#999'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#666'
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'solid'
}
}
}];
}
// 双Y轴配置
return [
{
type: 'value',
name: this.leftAxisName || '左轴',
position: 'left',
axisLine: {
show: true,
lineStyle: {
color: this.colors[0]
}
},
axisLabel: {
color: '#666',
formatter: '{value}'
},
splitLine: {
show: false
}
},
{
type: 'value',
name: this.rightAxisName || '右轴',
position: 'right',
axisLine: {
show: true,
lineStyle: {
color: this.colors[1]
}
},
axisLabel: {
color: '#666',
formatter: '{value}'
},
splitLine: {
show: false
}
}
];
},
// 获取系列配置
getSeriesConfig(processedSeries) {
const firstBarIndex = processedSeries.findIndex(s => s.type === 'bar');
const barSeriesCount = processedSeries.filter(s => s.type === 'bar').length;
const tightBarGroup =
this.chartType !== 'stacked' &&
barSeriesCount >= 2 &&
(this.chartType === 'mixed' || this.chartType === 'multiple');
return processedSeries.map((series, index) => {
const isLine = series.type === 'line';
const isStacked = this.chartType === 'stacked';
// 基础配置,包含堆叠属性 [2](@ref)
const baseConfig = {
name: series.name,
type: series.type,
data: series.data,
yAxisIndex: series.yAxisIndex || 0,
stack: series.stack // 保留堆叠属性,如果存在
};
if (isLine) {
// 折线图配置
return {
...baseConfig,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 3,
color: this.colors[index % this.colors.length]
},
itemStyle: {
color: this.colors[index % this.colors.length]
}
};
} else {
// 柱状图配置
const barConfig = {
...baseConfig,
barWidth: this.chartType === 'single' ? '60%' : '40%',
itemStyle: {
color: this.colors[index % this.colors.length]
},
emphasis: {
itemStyle: {
color: this.colors[index % this.colors.length],
shadowBlur: 10,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
};
// 为堆叠柱状图调整柱宽和样式 [3](@ref)
if (isStacked) {
barConfig.barWidth = '50%';
barConfig.itemStyle.borderRadius = [2, 2, 0, 0]; // 顶部圆角
}
// 同一类目下多根柱(如混合图双柱):柱间距为 0使两柱紧贴barGap 写在首个 bar 系列上)
if (tightBarGroup && index === firstBarIndex) {
barConfig.barGap = '0%';
}
return barConfig;
}
});
},
// 渲染图表
renderChart() {
const processedSeries = this.processChartData();
const seriesConfig = this.getSeriesConfig(processedSeries);
const yAxisConfig = this.getYAxisConfig();
// 计算图例显示条件:单系列时不显示图例
const showLegend = this.chartType !== 'single';
// 堆叠柱状图的tooltip配置 [2](@ref)
const tooltipFormatter = this.chartType === 'stacked'
? this.getStackedTooltipFormatter
: null;
const option = {
title: {
text: this.title,
left: 'left',
top: 10,
textStyle: {
color: '#333',
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: this.chartType === 'stacked' ? 'shadow' : 'line' // 堆叠模式使用阴影指针 [2](@ref)
},
formatter: tooltipFormatter || this.getDefaultTooltipFormatter(),
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#ddd',
borderWidth: 1,
textStyle: {
color: '#333'
}
},
legend: {
show: showLegend,
data: seriesConfig.map(s => s.name),
right: 10,
top: 10,
orient: 'horizontal',
type: 'scroll',
textStyle: {
color: '#666'
}
},
grid: {
left: '3%',
right: this.dualYAxis ? '8%' : '3%',
bottom: '8%',
top: showLegend ? 60 : 40,
containLabel: true
},
xAxis: {
type: 'category',
data: this.categories,
axisLine: {
lineStyle: {
color: '#999'
}
},
axisTick: {
show: false
},
axisLabel: {
color: '#666',
interval: 0,
rotate: this.categories.length > 6 ? 45 : 0,
margin: 10
}
},
yAxis: yAxisConfig,
series: seriesConfig
};
this.myChart.setOption(option);
},
// 默认tooltip格式化器
getDefaultTooltipFormatter() {
return (params) => {
let result = `<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">${params[0].name}</div>`;
params.forEach(param => {
const color = param.color;
const seriesName = param.seriesName;
const value = param.value;
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${color};"></span>`;
result += `
<div style="display: flex; justify-content: space-between; align-items: center; margin: 4px 0; min-width: 120px;">
<div style="display: flex; align-items: center;">
${marker}
<span style="margin-right: 15px;">${seriesName}</span>
</div>
<span style="font-weight: bold; font-size: 13px;">${value}</span>
</div>
`;
});
return result;
};
},
// 堆叠柱状图tooltip格式化器 [2](@ref)
getStackedTooltipFormatter(params) {
let result = `<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px;">${params[0].name}</div>`;
let total = 0;
// 计算总和
params.forEach(param => {
total += param.value || 0;
});
params.forEach(param => {
const color = param.color;
const seriesName = param.seriesName;
const value = param.value;
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0;
const marker = `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${color};"></span>`;
result += `
<div style="display: flex; justify-content: space-between; align-items: center; margin: 4px 0; min-width: 150px;">
<div style="display: flex; align-items: center;">
${marker}
<span style="margin-right: 15px;">${seriesName}</span>
</div>
<div style="text-align: right;">
<span style="font-weight: bold; font-size: 13px;">${value}</span>
</div>
</div>
`;
});
return result;
},
handleResize() {
if (this.myChart) {
this.myChart.resize();
}
}
},
watch: {
chartData: {
handler() {
if (this.myChart) {
this.renderChart();
}
},
deep: true
},
categories() {
if (this.myChart) {
this.renderChart();
}
},
chartType() {
if (this.myChart) {
this.renderChart();
}
},
dualYAxis() {
if (this.myChart) {
this.renderChart();
}
},
leftAxisName() {
if (this.myChart) {
this.renderChart();
}
},
rightAxisName() {
if (this.myChart) {
this.renderChart();
}
},
height() {
this.$nextTick(() => {
if (this.myChart) {
this.myChart.resize();
}
});
},
stackName() {
if (this.myChart && this.chartType === 'stacked') {
this.renderChart();
}
}
}
};
</script>