<template>
<div class="chart-wrapper">
<div ref="chartRef" style="width: 100%; height: 550px;"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';
const chartRef = ref(null);
let myChart = null;
/**
* 注意:箱线图通常需要较多数据点来计算 [最小值, 下四分位数, 中位数, 上四分位数, 最大值]
* 如果您的 value 只有 3 个值,箱体看起来会比较窄。
*/
const rawData = [
{
name: '批次1',
listArr: [
{ name: 'a1', value: [12, 25, 26, 27, 40, 45, 50] },
{ name: 'a2', value: [5, 15, 16, 17, 22, 30, 35] }
]
},
{
name: '批次2',
listArr: [
{ name: 'b1', value: [10, 25, 26, 27, 33, 40, 55] },
{ name: 'b2', value: [8, 15, 16, 17, 25, 28, 32] },
{ name: 'b3', value: [2, 10, 12, 14, 20, 25, 28] }
]
},
{
name: '批次3',
listArr: [
{ name: 'c1', value: [15, 20, 22, 24, 30, 35, 42] },
{ name: 'c2', value: [10, 18, 19, 20, 26, 30, 33] }
]
}
];
const initChart = () => {
if (!chartRef.value) return;
myChart = echarts.init(chartRef.value);
// 1. 数据转换
const childCategories = [];
const boxData = []; // 存储箱线图原始数组
const groupInfos = [];
let currentIndex = 0;
rawData.forEach((parent) => {
const start = currentIndex;
const len = parent.listArr.length;
parent.listArr.forEach((child) => {
childCategories.push(child.name);
boxData.push(child.value); // 箱线图的数据是一个数组
});
groupInfos.push({
name: parent.name,
start: start,
end: start + len - 1
});
currentIndex += len;
});
const option = {
title: {
text: '多层级 X 轴箱体图方案',
left: 'center',
top: 10
},
tooltip: {
trigger: 'item',
axisPointer: { type: 'shadow' }
},
grid: {
top: 80,
bottom: 120, // 留够空间给底部的括号和文字
left: '10%',
right: '10%'
},
xAxis: {
type: 'category',
data: childCategories,
boundaryGap: true,
nameGap: 30,
axisLabel: { interval: 0, margin: 15 },
axisTick: { alignWithLabel: true },
axisLine: { lineStyle: { color: '#ccc' } }
},
yAxis: {
type: 'value',
name: '数值',
splitLine: { lineStyle: { type: 'dashed' } }
},
series: [
{
name: '箱线图',
type: 'boxplot',
data: boxData,
itemStyle: {
color: '#b8c5f2',
borderColor: '#5470c6'
}
},
// 自定义系列:绘制半包围括号和居中文字
{
type: 'custom',
renderItem: (params, api) => {
const group = groupInfos[params.dataIndex];
const startPos = api.coord([group.start, 0]);
const endPos = api.coord([group.end, 0]);
const width = api.size([1, 0])[0];
// 几何中心计算
const xLeft = startPos[0] - width / 2 + 5; // 稍微内缩一点点好看
const xRight = endPos[0] + width / 2 - 5;
const xCenter = (xLeft + xRight) / 2;
const yBase = params.coordSys.y + params.coordSys.height;
const yTop = yBase + 15; // 括号横线离 X 轴的距离
const yBottom = yBase + 40; // 括号竖线下伸的长度
const yText = yBase + 65; // 文字高度
return {
type: 'group',
children: [
// 绘制括号 [左下, 左上, 右上, 右下]
{
type: 'polyline',
shape: {
points: [
[xLeft, yBottom],
[xLeft, yTop],
[xRight, yTop],
[xRight, yBottom]
]
},
style: {
stroke: '#888',
lineWidth: 2,
fill: 'none'
}
},
// 绘制居中文字
{
type: 'text',
style: {
text: group.name,
x: xCenter,
y: yText,
textAlign: 'center',
textFont: 'bold 14px sans-serif',
fill: '#333'
}
}
]
};
},
data: groupInfos.map((_, i) => i),
clip: false,
silent: true
}
]
};
myChart.setOption(option);
};
onMounted(() => {
initChart();
window.addEventListener('resize', () => myChart && myChart.resize());
});
onUnmounted(() => {
if (myChart) myChart.dispose();
});
</script>
<style scoped>
.chart-wrapper {
background: #ffffff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
}
</style>