493 lines
13 KiB
Vue
493 lines
13 KiB
Vue
<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>
|