436 lines
12 KiB
Vue
436 lines
12 KiB
Vue
|
|
<template>
|
|||
|
|
<div ref="chartContainer" style="width: 100%; height: 300px;"></div>
|
|||
|
|
</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
|
|||
|
|
},
|
|||
|
|
// 新增:堆叠组名称(用于stacked模式)
|
|||
|
|
stackName: {
|
|||
|
|
type: String,
|
|||
|
|
default: '总量'
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
myChart: null
|
|||
|
|
};
|
|||
|
|
},
|
|||
|
|
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 || (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: '左轴',
|
|||
|
|
position: 'left',
|
|||
|
|
axisLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: this.colors[0]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
color: '#666',
|
|||
|
|
formatter: '{value}'
|
|||
|
|
},
|
|||
|
|
splitLine: {
|
|||
|
|
show: false
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
type: 'value',
|
|||
|
|
name: '右轴',
|
|||
|
|
position: 'right',
|
|||
|
|
axisLine: {
|
|||
|
|
show: true,
|
|||
|
|
lineStyle: {
|
|||
|
|
color: this.colors[1]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
axisLabel: {
|
|||
|
|
color: '#666',
|
|||
|
|
formatter: '{value}'
|
|||
|
|
},
|
|||
|
|
splitLine: {
|
|||
|
|
show: false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 获取系列配置
|
|||
|
|
getSeriesConfig(processedSeries) {
|
|||
|
|
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]; // 顶部圆角
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
stackName() {
|
|||
|
|
if (this.myChart && this.chartType === 'stacked') {
|
|||
|
|
this.renderChart();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|