<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>