eachart多层级X轴箱体图

image

 

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

  

 

posted @ 2026-04-25 13:58  惠鹏曦  阅读(10)  评论(0)    收藏  举报