vue3 echarts ts 环形图中间文字 样式设置

vue3 echarts ts 环形图中间文字 样式设置

一、背景与问题

在数据可视化场景中,环形图常用于展示占比关系。ECharts 提供了完整的环形图支持,但其默认的中间文字样式(如百分比、标题等)往往无法满足复杂业务需求。例如:

  • 业务场景需要多行文本
  • 需要动态切换文本内容
  • 需要特殊字体/渐变/阴影效果
  • 需要响应式调整文本位置

传统做法需要通过 label 配置项控制,但容易出现文本重叠、样式丢失等问题。本文将深入解析 ECharts 环形图中间文字样式设置的底层原理,并提供可复用的解决方案。

二、基本原理

ECharts 环形图的中间文字主要通过 series.label 配置项控制。其工作原理如下:

  1. 渲染机制:当 radius 设置为 ['20%', '40%'] 时,图表会在环形区域中心生成一个文本元素
  2. 文本定位:通过 label 的 formatter 控制内容,position 控制位置(默认为 center)
  3. 样式控制:通过 rich 配置实现复杂样式,textStyle 控制基本样式
  4. 特殊需求:需要通过 labelLine 控制连接线,emphasis 控制高亮状态

关键配置项结构:

series: {
  type: 'pie',
  radius: ['20%', '40%'],
  label: {
    formatter: '{b}: {d}%',
    position: 'center',
    textStyle: {
      fontSize: 20,
      color: '#fff'
    }
  },
  labelLine: {
    show: false
  }
}

三、环境准备

确保项目已安装必要依赖:

npm install echarts @types/echarts --save

创建 Vue3 + TS 组件结构:

// src/components/RingChart.vue
<template>
  <div ref="chartRef" class="chart-container"></div>
</template>

<script lang="ts">
import { defineComponent, onMounted, onBeforeUnmount, ref } from 'vue'
import * as echarts from 'echarts'

export default defineComponent({
  name: 'RingChart',
  setup() {
    const chartRef = ref<HTMLElement | null>(null)
    const chartInstance = ref<echarts.ECharts | null>(null)

    const initChart = () => {
      if (!chartRef.value) return
      chartInstance.value = echarts.init(chartRef.value)
      // 初始化配置
    }

    onMounted(() => {
      initChart()
    })

    onBeforeUnmount(() => {
      chartInstance.value?.dispose()
    })

    return { chartRef }
  }
})
</script>

<style scoped>
.chart-container {
  width: 100%;
  height: 400px;
}
</style>

四、核心实现

1. 基础样式设置

const option = {
  series: [{
    type: 'pie',
    radius: ['20%', '40%'],
    data: [
      { value: 335, name: 'A' },
      { value: 310, name: 'B' },
      { value: 270, name: 'C' },
      { value: 150, name: 'D' }
    ],
    label: {
      formatter: '{b}: {d}%',
      position: 'center',
      textStyle: {
        fontSize: 24,
        color: '#ffffff',
        fontWeight: 'bold'
      }
    },
    labelLine: {
      show: false
    }
  }]
}

关键代码解释:

  • formatter 支持模板字符串,可自定义显示内容
  • textStyle 控制字体样式,支持所有 CSS 样式属性
  • position: 'center' 确保文本位于环形中心

2. 复杂样式设置

const option = {
  series: [{
    type: 'pie',
    radius: ['20%', '40%'],
    data: [
      { value: 335, name: 'A' },
      { value: 310, name: 'B' },
      { value: 270, name: 'C' },
      { value: 150, name: 'D' }
    ],
    label: {
      formatter: '{b}: {d}%',
      position: 'center',
      rich: {
        name: {
          fontSize: 24,
          color: '#FFD700'
        },
        value: {
          fontSize: 16,
          color: '#FFFFFF'
        }
      },
      textStyle: {
        color: 'transparent'
      }
    },
    labelLine: {
      show: false
    }
  }]
}

关键代码解释:

  • rich 配置允许定义多个样式块
  • textStyle.color: 'transparent' 使默认文字透明,只显示 rich 中定义的样式
  • 这种方式可实现多行文本样式差异化

3. 动态文本设置

const option = {
  series: [{
    type: 'pie',
    radius: ['20%', '40%'],
    data: [
      { value: 335, name: 'A' },
      { value: 310, name: 'B' },
      { value: 270, name: 'C' },
      { value: 150, name: 'D' }
    ],
    label: {
      formatter: (params: any) => {
        const total = params.seriesData.reduce((sum, d) => sum + d.value, 0)
        return `Total: ${total} / ${params.seriesData.length} Items`
      },
      position: 'center',
      textStyle: {
        fontSize: 20,
        color: '#00FF00'
      }
    },
    labelLine: {
      show: false
    }
  }]
}

关键代码解释:

  • formatter 可接受参数,支持动态计算
  • 通过 params 可获取系列数据、当前数据点等信息
  • 适用于需要动态计算总和、平均值等场景

五、完整案例

1. 响应式环形图组件

// src/components/ResponsiveRingChart.vue
<template>
  <div ref="chartRef" class="chart-container"></div>
</template>

<script lang="ts">
import { defineComponent, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import * as echarts from 'echarts'

export default defineComponent({
  name: 'ResponsiveRingChart',
  props: {
    data: {
      type: Array as () => Array<{ value: number; name: string }>,
      default: () => [
        { value: 335, name: 'A' },
        { value: 310, name: 'B' },
        { value: 270, name: 'C' },
        { value: 150, name: 'D' }
      ]
    },
    radius: {
      type: Array,
      default: () => ['20%', '40%']
    },
    title: {
      type: String,
      default: '环形图'
    }
  },
  setup(props) {
    const chartRef = ref<HTMLElement | null>(null)
    const chartInstance = ref<echarts.ECharts | null>(null)
    const chartWidth = ref(0)
    const chartHeight = ref(0)

    const initChart = () => {
      if (!chartRef.value) return
      chartInstance.value = echarts.init(chartRef.value)
      
      const option: echarts.EChartOption = {
        title: {
          text: props.title,
          left: 'center',
          textStyle: {
            fontSize: 20,
            color: '#333'
          }
        },
        series: [{
          type: 'pie',
          radius: props.radius,
          data: props.data,
          label: {
            formatter: (params: any) => {
              const total = params.seriesData.reduce((sum, d) => sum + d.value, 0)
              return `${params.name}: ${((params.value / total) * 100).toFixed(1)}%`
            },
            position: 'center',
            rich: {
              percent: {
                fontSize: 24,
                color: '#FFD700'
              },
              name: {
                fontSize: 16,
                color: '#FFFFFF'
              }
            },
            textStyle: {
              color: 'transparent'
            }
          },
          labelLine: {
            show: false
          }
        }]
      }

      chartInstance.value.setOption(option)
      
      // 响应式调整
      const resizeHandler = () => {
        if (chartRef.value) {
          const width = chartRef.value.clientWidth
          const height = chartRef.value.clientHeight
          chartInstance.value?.resize()
          // 可选:动态调整文本大小
          if (width < 300) {
            option.series[0].label.textStyle.fontSize = 12
          } else {
            option.series[0].label.textStyle.fontSize = 20
          }
          chartInstance.value.setOption(option)
        }
      }

      window.addEventListener('resize', resizeHandler)
      watch(() => props.data, () => {
        if (chartInstance.value) {
          chartInstance.value.setOption({
            series: [{
              data: props.data
            }]
          })
        }
      })
    }

    onMounted(() => {
      initChart()
    })

    onBeforeUnmount(() => {
      chartInstance.value?.dispose()
      window.removeEventListener('resize', resizeHandler)
    })

    return { chartRef }
  }
})
</script>

<style scoped>
.chart-container {
  width: 100%;
  height: 400px;
  background: #f0f0f0;
  border-radius: 12px;
  overflow: hidden;
}
</style>

关键实现说明:

  • 响应式设计:通过 resize 事件动态调整图表尺寸
  • 动态样式:根据容器大小调整文本字号
  • 数据绑定:通过 watch 监听 props 变化
  • 安全考虑:避免直接操作 DOM 元素

六、源码解析

以 formatter 函数为例,其内部调用链如下:

  1. formatter 被注册到 series.label 配置
  2. 在 render 阶段,ECharts 会调用 formatter 函数
  3. 函数接收 params 参数,包含:

    • params.name: 数据项名称
    • params.value: 数据值
    • params.percent: 占比
    • params.seriesData: 当前系列的所有数据
  4. 返回的字符串被渲染为文本内容
// ECharts 源码片段(简化版)
function renderLabel(params: any, label: any) {
  const text = label.formatter(params)
  const textElement = createTextElement(text)
  textElement.style = label.textStyle
  textElement.rich = label.rich
  // ...其他渲染逻辑
}

七、进阶使用

1. 动态样式切换

const option = {
  series: [{
    type: 'pie',
    radius: ['20%', '40%'],
    data: [
      { value: 335, name: 'A' },
      { value: 310, name: 'B' },
      { value: 270, name: 'C' },
      { value: 150, name: 'D' }
    ],
    label: {
      formatter: (params: any) => {
        const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches
        const color = isDarkMode ? '#FFD700' : '#00FF00'
        return `${params.name}: ${params.value} ${color}`
      },
      position: 'center',
      textStyle: {
        color: 'transparent'
      }
    }
  }]
}

2. 动画效果控制

const option = {
  series: [{
    type: 'pie',
    radius: ['20%', '40%'],
    data: [
      { value: 335, name: 'A' },
      { value: 310, name: 'B' },
      { value: 270, name: 'C' },
      { value: 150, name: 'D' }
    ],
    label: {
      formatter: '{b}: {d}%',
      position: 'center',
      textStyle: {
        fontSize: 20,
        color: '#ffffff'
      }
    },
    labelLine: {
      show: true
    },
    animation: false // 关闭动画
  }]
}

八、性能与工程实践

1. 性能优化

  • 避免频繁重绘:使用 setOption 的 notMerge 参数控制是否合并配置
  • 文本缓存:对于固定文本内容,可使用 textStyle 配置而非 rich
  • 动态样式:避免在 formatter 中执行复杂计算
const option = {
  series: [{
    type: 'pie',
    radius: ['20%', '40%'],
    data: [
      { value: 335, name: 'A' },
      { value: 310, name: 'B' },
      { value: 270, name: 'C' },
      { value: 150, name: 'D' }
    ],
    label: {
      formatter: '{b}: {d}%',
      position: 'center',
      textStyle: {
        fontSize: 20,
        color: '#ffffff'
      }
    }
  }]
}

2. 安全风险

  • XSS 防护:确保 formatter 中的动态内容经过转义
  • 数据验证:对传入的 data 进行格式校验
  • 权限控制:避免将敏感数据直接渲染为文本

3. 工程实践建议

  • 使用 TypeScript 定义配置项类型:

    type RingChartOption = {
      title: string
      series: Array<{
        type: 'pie'
        radius: [string, string]
        data: Array<{ value: number; name: string }>
        label: {
          formatter: (params: any) => string
          position: string
          textStyle: {
            fontSize: number
            color: string
          }
        }
      }>
    }
  • 建立配置项工厂函数:

    const createOption = (data: Array<{ value: number; name: string }>): echarts.EChartOption => {
      return {
        series: [{
          type: 'pie',
          radius: ['20%', '40%'],
          data,
          label: {
            formatter: '{b}: {d}%',
            position: 'center',
            textStyle: {
              fontSize: 20,
              color: '#ffffff'
            }
          }
        }]
      }
    }

九、常见问题与踩坑

1. 文字重叠问题

问题表现:文本显示在环形图外或被遮挡
解决办法:

  • 调整 radius 值
  • 使用 labelLine 控制连接线
  • 通过 padding 调整容器边距

2. 样式未生效

常见原因:

  • 未正确设置 textStyle 或 rich 配置
  • 使用了错误的配置项(如 label.textStyle 而非 label.textStyle)
  • 未正确引入 ECharts 库

3. 响应式失效

解决办法:

  • 监听 resize 事件
  • 在 resize 时重新设置 option
  • 使用 echarts.init 时传入容器尺寸

4. 动态内容渲染错误

错误示例:

formatter: (params: any) => {
  return eval(`params.${someDynamicKey}`)
}

改进方案:

formatter: (params: any) => {
  const key = someDynamicKey
  return params[key] || 'N/A'
}

十、最佳实践

  1. 优先使用 rich 配置:实现复杂样式时更灵活
  2. 避免频繁更新配置:使用 notMerge 参数优化性能
  3. 使用 TypeScript 类型:确保配置项类型安全
  4. 考虑响应式设计:动态调整文本样式和位置
  5. 注意安全性:对动态内容进行转义处理
  6. 使用工厂函数:提高代码可维护性
  7. 合理使用动画:避免过度动画影响性能

十一、总结

通过深入分析 ECharts 环形图中间文字样式设置的原理,我们了解到其底层实现机制和配置项关系。本文提供了多个代码示例,涵盖基础样式、复杂样式、动态内容等场景,同时给出了完整的响应式组件实现。

在实际开发中,建议根据具体需求选择合适的实现方式:

  • 简单样式:直接使用 textStyle 配置
  • 复杂样式:使用 rich 实现多样式块
  • 动态内容:通过 formatter 实现动态计算
  • 响应式需求:结合 resize 事件和容器尺寸调整

需要注意的是,这种方案适合需要突出显示中间文字的场景(如仪表盘、统计面板),但不适合需要频繁交互或复杂动画的场景。在实现过程中应特别注意性能优化和安全性问题,确保最终效果符合业务需求。

最后修改于:2026年09月16日 18:59

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日