vue3+TS+echarts 柱状图自定义图形

发布于:2025-05-01 ⋅ 阅读:(23) ⋅ 点赞:(0)

需要实现的效果如下

<script setup lang="ts" name="RepsSingleBarChart">
  import * as echarts from 'echarts'
  import { getInitecharts } from '@/utils/echart'

  // 定义 props 类型
  interface Props {
    id: string
    dataObj: Record<string, unknown> | null
  }
  // 定义 SeriesOption 类型
  interface SeriesOption {
    type: string
    data: number[]
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    custom?: any // 允许自定义属性
    renderItem?: (
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      params: any,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      api: any,
    ) => {
      type: string
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      children?: any[]
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      shape?: Record<string, any>
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      style?: Record<string, any>
      silent?: boolean
    }
    encode?: {
      x: number
      y: number
    }
  }
  // 定义 ECharts 配置项类型
  interface EChartsOption {
    tooltip?: echarts.TooltipComponentOption
    grid?: echarts.GridComponentOption
    xAxis?: echarts.XAXisComponentOption | echarts.XAXisComponentOption[]
    yAxis?: echarts.YAXisComponentOption | echarts.YAXisComponentOption[]
    series?: SeriesOption | SeriesOption[]
    title?: echarts.TitleComponentOption
    [key: string]: unknown
  }

  // 定义 props
  const props = defineProps<Props>()

  // 定义 ECharts 实例类型
  let myChart: echarts.ECharts | null = null
  // 定义 ECharts 配置项
  const option = ref<EChartsOption>({
    tooltip: {
      show: false,
    },
    grid: {
      top: '31px',
      left: '17px',
      right: '21px',
      bottom: '9px',
      containLabel: true,
    },
    xAxis: [
      {
        type: 'category',
        data: ['2025.01', '2025.02', '2025.03', '2025.04', '2025.05', '2025.06'],
        axisTick: {
          alignWithLabel: true,
          show: false,
        },
        axisLine: {
          show: true, // 确保显示轴线
          lineStyle: {
            color: 'rgba(153,204,255,0.50)',
            type: 'solid', // 设置为实线
            width: 1,
          },
        },
        axisLabel: {
          color: '#DAECFF',
          fontSize: 14,
          fontFamily: 'Arial, Arial-400',
          fontWeight: 400,
          lineHeight: 36,
        },
        // 添加X轴网格线配置
        splitLine: {
          show: true, // 不显示X轴方向的网格线
          lineStyle: {
            color: 'rgba(153,204,255,0.20)',
            type: 'dashed',
            width: 1,
          },
        },
      },
    ],
    yAxis: [
      {
        type: 'value',
        axisLabel: {
          color: 'rgba(218,236,255,0.60)',
          fontSize: 14,
          fontFamily: 'Arial, Arial-400',
          fontWeight: 400,
          lineHeight: 20,
        },
        // Y轴轴线样式
        axisLine: {
          show: false,
        },
        // Y轴刻度线样式
        axisTick: {
          show: false,
        },
        // Y轴网格线样式(设置为虚线)
        splitLine: {
          show: true,
          lineStyle: {
            color: 'rgba(153,204,255,0.20)', // 浅色虚线
            type: 'dashed', // 虚线样式
            dashOffset: 2, // 虚线偏移量
            width: 1,
          },
        },
      },
    ],
    series: [
      {
        type: 'custom',
        data: [10, 0, 200, 334, 390, 330],
        encode: {
          // 将类别和值映射到维度
          x: 0,
          y: 1,
        },
        renderItem: (_, api) => {
          // api.value(0)/params.dataIndex // 当前下标
          // api.value(1) // 当前下标对应的值
          // api.coord([api.value(0), api.value(1)]) // 当前下标对应的值的XY坐标,对应的是中心点
          // api.coord([api.value(0), 0]) // 底部的XY坐标

          // 小长方形之间的间隔
          const gap = 4
          // 小长方形宽度
          const segmentWidth = 22
          // 小长方形高度
          const segmentHeight = 6
          // 底部的XY坐标
          const base = api.coord([api.value(0), 0])
          // 数值的XY坐标
          const value = api.coord([api.value(0), api.value(1)])
          // 计算需要多少个小长方形
          let segmentCount = Math.round((base[1] - value[1]) / (segmentHeight + gap))
          // 数值
          const number = api.value(1)
          // 最大的Y坐标
          let maxY = 0

          if (segmentCount === 0 && number) {
            segmentCount = 1
          }

          let children = []
          for (let i = 0; i < segmentCount; i++) {
            maxY = base[1] - segmentHeight - i * (segmentHeight + gap)
            children.push({
              type: 'rect',
              shape: {
                x: value[0] - segmentWidth / 2, // 居中
                y: maxY, // 从下往上排列
                width: segmentWidth,
                height: segmentHeight,
              },
              style: {
                fill: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
                  { offset: 0, color: '#00d4ff' },
                  { offset: 1, color: '#00aaff' },
                ]),
              },
            })
          }
          if (segmentCount === 0) {
            children = [
              {
                type: 'rect',
                shape: {
                  x: value[0] - segmentWidth / 2, // 居中
                  y: base[1] - 1, // 从下往上排列
                  width: segmentWidth,
                  height: 1,
                },
                style: {
                  fill: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
                    { offset: 0, color: '#00d4ff' },
                    { offset: 1, color: '#00aaff' },
                  ]),
                },
              },
              {
                type: 'text',
                style: {
                  text: number.toString(), // 显示数值
                  fill: '#FFFFFF', // 文字颜色
                  fontSize: 14,
                  fontFamily: 'Arial, Arial-700',
                  fontWeight: '700',
                  lineHeight: 24,
                  textAlign: 'center',
                },
                position: [
                  value[0], // x位置(居中)
                  base[1] - 24, // y位置
                ],
                z: 10, // 确保文本在最上层
              },
            ]
          } else {
            // 添加数值文本(在柱子顶部)
            children.push({
              type: 'text',
              style: {
                text: number.toString(), // 显示数值
                fill: '#FFFFFF', // 文字颜色
                fontSize: 14,
                fontFamily: 'Arial, Arial-700',
                fontWeight: '700',
                lineHeight: 24,
                textAlign: 'center',
              },
              position: [
                value[0], // x位置(居中)
                maxY - segmentHeight * 3 - 5, // y位置(顶部上方5px)
              ],
              z: 10, // 确保文本在最上层
            })
          }

          return {
            type: 'group',
            children,
            silent: true, // 禁用交互
          }
        },
      },
    ],
  })
  // 定义空数据时的提示信息
  const emptyObj = reactive<Record<string, string>>({
    dashboardPtglRzxxs: '暂无入驻学校',
  })

  /**
   * @description: 渲染
   * @return {void}
   */
  const getRender = () => {
    if (myChart) {
      myChart.dispose()
    }

    myChart = getInitecharts(echarts, props.id)

    nextTick(() => {
      if (!myChart) return

      if (props.dataObj && Object.keys(props.dataObj).length > 0) {
        myChart.setOption(option.value)
      } else {
        myChart.setOption({
          // 禁用所有交互
          tooltip: {
            show: false,
          },
          graphic: {
            elements: [
              {
                type: 'group',
                left: 'center',
                top: 'center',
                silent: true, // 关键设置:禁用交互
                children: [
                  {
                    type: 'image',
                    style: {
                      image: '/src/assets/img/table-empty.png',
                      width: 88,
                      height: 88,
                    },
                    y: 0,
                  },
                  {
                    type: 'text',
                    style: {
                      text: emptyObj[props.id],
                      fontSize: 14,
                      fontWeight: '400',
                      fill: 'rgba(255, 255, 255, .8)',
                      opacity: '.8',
                      fontFamily: 'Microsoft YaHei UI, Microsoft YaHei UI-400',
                      lineHeight: 24,
                      letterSpacing: 0.07,
                    },
                    y: 88 + 4, // 图片高度 + 间隔
                    x: 2,
                  },
                ],
              },
            ],
          },
        })
      }
    })
  }

  watch(
    () => props.dataObj,
    () => {
      getRender()
    },
    { deep: true },
  )

  onMounted(() => {
    getRender()
  })
</script>

<template>
  <div :id="props.id" class="bar"></div>
</template>

<style lang="less" scoped>
  .bar {
    width: 100%;
    height: 100%;
  }
</style>

封装echarts统一调用

import * as echarts from 'echarts'

/**
 * @description: 初始化echarts
 * @param {typeof echarts} echartsInstance echarts实例
 * @param {string} id dom的id
 * @param {('canvas' | 'svg')} renderer 渲染方式
 * @returns {echarts.ECharts} echarts实例
 */
export function getInitecharts(
  echartsInstance: typeof echarts,
  id: string,
  renderer: 'canvas' | 'svg' = 'canvas',
): echarts.ECharts {
  const dom = document.getElementById(id)
  if (!dom) {
    throw new Error(`Element with id "${id}" not found`)
  }
  return echartsInstance.init(dom, null, {
    renderer,
    useDirtyRect: false,
  })
}


网站公告

今日签到

点亮在社区的每一天
去签到