ECharts - 数据可视化图表库

学习目标

完成本章学习后,你将能够:

  • 理解ECharts的核心概念和使用场景
  • 在Vue 3项目中集成和使用ECharts
  • 创建常用的图表类型(折线图、柱状图、饼图等)
  • 实现响应式图表和数据动态更新
  • 掌握图表交互和主题定制

前置知识

学习本章内容前,你需要掌握:

问题引入

实际场景

小李在开发一个数据分析后台时,产品经理提出了以下需求:

  1. 销售数据展示:需要用折线图展示最近30天的销售趋势
  2. 产品占比分析:需要用饼图展示各产品的销售占比
  3. 地区对比:需要用柱状图对比不同地区的销售额
  4. 实时更新:图表数据需要每5秒自动刷新
  5. 响应式布局:图表需要适配不同屏幕尺寸
  6. 交互功能:鼠标悬停显示详细数据,点击图例筛选数据

小李想知道:有没有成熟的图表库可以快速实现这些需求?

为什么选择ECharts

ECharts是百度开源的一个强大的数据可视化库,具有以下优势:

  1. 功能强大:支持20+种图表类型,满足各种数据展示需求
  2. 性能优秀:基于Canvas渲染,支持大数据量展示
  3. 交互丰富:内置缩放、拖拽、高亮等交互功能
  4. 主题丰富:提供多种内置主题,支持自定义
  5. 文档完善:中英文文档齐全,示例丰富
  6. 社区活跃:GitHub 60k+ stars,持续维护更新
  7. Vue集成简单:有官方的Vue组件封装

核心概念

概念1:ECharts基本结构

ECharts图表由以下核心部分组成:

1. 图表容器(Container)

需要一个DOM元素作为图表的容器:

<div id="chart" style="width: 600px; height: 400px;"></div>
2. 图表实例(Instance)

通过echarts.init()创建图表实例:

const chart = echarts.init(document.getElementById('chart'));
3. 配置项(Option)

配置项定义了图表的所有内容:

const option = {
  title: { text: '销售趋势' },      // 标题
  xAxis: { data: ['周一', '周二'] }, // X轴
  yAxis: {},                         // Y轴
  series: [{                         // 数据系列
    type: 'line',
    data: [120, 200]
  }]
};
4. 渲染图表

使用setOption()方法渲染图表:

chart.setOption(option);

概念2:在Vue 3中使用ECharts

安装ECharts

# 使用pnpm(推荐)
pnpm add echarts

# 使用npm
npm install echarts

# 使用yarn
yarn add echarts

基础使用模式

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;

onMounted(() => {
  // 初始化图表
  chartInstance = echarts.init(chartRef.value);
  
  // 配置图表
  const option = {
    // 图表配置
  };
  
  chartInstance.setOption(option);
});

onUnmounted(() => {
  // 销毁图表实例,释放资源
  if (chartInstance) {
    chartInstance.dispose();
  }
});
</script>

<template>
  <div ref="chartRef" style="width: 600px; height: 400px;"></div>
</template>

常用图表类型

1. 折线图(Line Chart)

作用:展示数据随时间或类别的变化趋势

使用场景

  • 销售趋势分析
  • 温度变化曲线
  • 股票价格走势

完整示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;

// 模拟销售数据
const salesData = {
  dates: ['1月', '2月', '3月', '4月', '5月', '6月', '7月'],
  sales: [120, 200, 150, 280, 210, 300, 350],
  target: [150, 180, 200, 250, 280, 320, 360]
};

onMounted(() => {
  // 初始化图表
  chartInstance = echarts.init(chartRef.value);
  
  // 配置折线图
  const option = {
    // 标题
    title: {
      text: '2024年销售趋势',
      left: 'center',
      textStyle: {
        fontSize: 18,
        fontWeight: 'bold'
      }
    },
    
    // 提示框
    tooltip: {
      trigger: 'axis',  // 坐标轴触发
      axisPointer: {
        type: 'cross'   // 十字准星指示器
      }
    },
    
    // 图例
    legend: {
      data: ['实际销售', '目标销售'],
      top: 30
    },
    
    // 网格
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true  // 包含坐标轴标签
    },
    
    // X轴
    xAxis: {
      type: 'category',
      data: salesData.dates,
      boundaryGap: false  // 坐标轴两边不留白
    },
    
    // Y轴
    yAxis: {
      type: 'value',
      name: '销售额(万元)'
    },
    
    // 数据系列
    series: [
      {
        name: '实际销售',
        type: 'line',
        data: salesData.sales,
        smooth: true,  // 平滑曲线
        itemStyle: {
          color: '#42b883'  // 线条颜色
        },
        areaStyle: {  // 区域填充
          color: {
            type: 'linear',
            x: 0,
            y: 0,
            x2: 0,
            y2: 1,
            colorStops: [
              { offset: 0, color: 'rgba(66, 184, 131, 0.3)' },
              { offset: 1, color: 'rgba(66, 184, 131, 0.05)' }
            ]
          }
        }
      },
      {
        name: '目标销售',
        type: 'line',
        data: salesData.target,
        smooth: true,
        itemStyle: {
          color: '#ff6b6b'
        },
        lineStyle: {
          type: 'dashed'  // 虚线
        }
      }
    ]
  };
  
  chartInstance.setOption(option);
  
  // 响应式调整
  window.addEventListener('resize', () => {
    chartInstance.resize();
  });
});

onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
  }
  window.removeEventListener('resize', () => {});
});
</script>

<template>
  <div class="chart-container">
    <div ref="chartRef" class="chart"></div>
  </div>
</template>

<style scoped>
.chart-container {
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

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

运行结果

  • 显示两条平滑的折线(实际销售和目标销售)
  • 实际销售线有渐变填充区域
  • 目标销售线为虚线
  • 鼠标悬停显示详细数据
  • 窗口大小改变时图表自动调整

2. 柱状图(Bar Chart)

作用:对比不同类别的数据大小

使用场景

  • 地区销售对比
  • 产品销量排名
  • 年度数据对比

完整示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;

// 模拟地区销售数据
const regionData = {
  regions: ['北京', '上海', '广州', '深圳', '杭州', '成都'],
  sales: [320, 380, 290, 410, 260, 310],
  growth: [12, 18, -5, 22, 8, 15]  // 增长率
};

onMounted(() => {
  chartInstance = echarts.init(chartRef.value);
  
  const option = {
    title: {
      text: '各地区销售额对比',
      subtext: '2024年第一季度',
      left: 'center'
    },
    
    tooltip: {
      trigger: 'axis',
      axisPointer: {
        type: 'shadow'  // 阴影指示器
      },
      formatter: (params) => {
        // 自定义提示框内容
        const region = params[0].name;
        const sales = params[0].value;
        const growth = regionData.growth[params[0].dataIndex];
        return `
          <div style="padding: 5px;">
            <strong>${region}</strong><br/>
            销售额:${sales}万元<br/>
            增长率:<span style="color: ${growth > 0 ? '#42b883' : '#ff6b6b'}">
              ${growth > 0 ? '+' : ''}${growth}%
            </span>
          </div>
        `;
      }
    },
    
    grid: {
      left: '3%',
      right: '4%',
      bottom: '3%',
      containLabel: true
    },
    
    xAxis: {
      type: 'category',
      data: regionData.regions,
      axisLabel: {
        interval: 0,  // 显示所有标签
        rotate: 0     // 标签旋转角度
      }
    },
    
    yAxis: {
      type: 'value',
      name: '销售额(万元)'
    },
    
    series: [
      {
        name: '销售额',
        type: 'bar',
        data: regionData.sales,
        // 根据增长率设置柱子颜色
        itemStyle: {
          color: (params) => {
            const growth = regionData.growth[params.dataIndex];
            return growth > 0 ? '#42b883' : '#ff6b6b';
          }
        },
        // 显示数值标签
        label: {
          show: true,
          position: 'top',
          formatter: '{c}万'
        },
        // 柱子宽度
        barWidth: '50%'
      }
    ]
  };
  
  chartInstance.setOption(option);
  
  window.addEventListener('resize', () => {
    chartInstance.resize();
  });
});

onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
  }
  window.removeEventListener('resize', () => {});
});
</script>

<template>
  <div class="chart-container">
    <div ref="chartRef" class="chart"></div>
  </div>
</template>

<style scoped>
.chart-container {
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

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

运行结果

  • 显示6个地区的销售额柱状图
  • 增长为正的柱子显示绿色,负增长显示红色
  • 柱子顶部显示具体数值
  • 鼠标悬停显示详细信息(包含增长率)

3. 饼图(Pie Chart)

作用:展示各部分占总体的比例

使用场景

  • 产品销售占比
  • 用户来源分析
  • 预算分配展示

完整示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;

// 模拟产品销售数据
const productData = [
  { name: '手机', value: 3500, color: '#5470c6' },
  { name: '电脑', value: 2800, color: '#91cc75' },
  { name: '平板', value: 1800, color: '#fac858' },
  { name: '耳机', value: 1200, color: '#ee6666' },
  { name: '其他', value: 700, color: '#73c0de' }
];

onMounted(() => {
  chartInstance = echarts.init(chartRef.value);
  
  const option = {
    title: {
      text: '产品销售占比',
      subtext: '2024年第一季度',
      left: 'center'
    },
    
    tooltip: {
      trigger: 'item',
      formatter: '{a} <br/>{b}: {c}万元 ({d}%)'
    },
    
    legend: {
      orient: 'vertical',  // 垂直布局
      left: 'left',
      top: 'middle'
    },
    
    series: [
      {
        name: '销售额',
        type: 'pie',
        radius: ['40%', '70%'],  // 环形图(内外半径)
        center: ['60%', '50%'],  // 图表位置
        avoidLabelOverlap: true,
        itemStyle: {
          borderRadius: 10,  // 圆角
          borderColor: '#fff',
          borderWidth: 2
        },
        label: {
          show: true,
          formatter: '{b}\n{d}%'  // 显示名称和百分比
        },
        emphasis: {  // 高亮样式
          label: {
            show: true,
            fontSize: 16,
            fontWeight: 'bold'
          },
          itemStyle: {
            shadowBlur: 10,
            shadowOffsetX: 0,
            shadowColor: 'rgba(0, 0, 0, 0.5)'
          }
        },
        data: productData.map(item => ({
          value: item.value,
          name: item.name,
          itemStyle: { color: item.color }
        }))
      }
    ]
  };
  
  chartInstance.setOption(option);
  
  window.addEventListener('resize', () => {
    chartInstance.resize();
  });
});

onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
  }
  window.removeEventListener('resize', () => {});
});
</script>

<template>
  <div class="chart-container">
    <div ref="chartRef" class="chart"></div>
  </div>
</template>

<style scoped>
.chart-container {
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

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

运行结果

  • 显示环形饼图,展示各产品销售占比
  • 左侧显示图例
  • 每个扇区显示名称和百分比
  • 鼠标悬停时扇区放大并显示详细信息

4. 动态更新图表

作用:实时更新图表数据

使用场景

  • 实时监控数据
  • 定时刷新报表
  • 用户交互更新

完整示例

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;
let updateTimer = null;

// 初始数据
const data = ref({
  time: [],
  value: []
});

// 生成随机数据
const generateData = () => {
  const now = new Date();
  const timeStr = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
  const value = Math.floor(Math.random() * 100) + 50;
  
  // 保留最近20个数据点
  data.value.time.push(timeStr);
  data.value.value.push(value);
  
  if (data.value.time.length > 20) {
    data.value.time.shift();
    data.value.value.shift();
  }
};

// 更新图表
const updateChart = () => {
  if (!chartInstance) return;
  
  const option = {
    title: {
      text: '实时数据监控',
      left: 'center'
    },
    
    tooltip: {
      trigger: 'axis'
    },
    
    xAxis: {
      type: 'category',
      data: data.value.time,
      boundaryGap: false
    },
    
    yAxis: {
      type: 'value',
      name: '数值',
      min: 0,
      max: 200
    },
    
    series: [
      {
        name: '实时数据',
        type: 'line',
        data: data.value.value,
        smooth: true,
        symbol: 'circle',
        symbolSize: 6,
        itemStyle: {
          color: '#42b883'
        },
        areaStyle: {
          color: {
            type: 'linear',
            x: 0,
            y: 0,
            x2: 0,
            y2: 1,
            colorStops: [
              { offset: 0, color: 'rgba(66, 184, 131, 0.5)' },
              { offset: 1, color: 'rgba(66, 184, 131, 0.1)' }
            ]
          }
        }
      }
    ]
  };
  
  chartInstance.setOption(option);
};

// 开始自动更新
const startAutoUpdate = () => {
  updateTimer = setInterval(() => {
    generateData();
    updateChart();
  }, 2000);  // 每2秒更新一次
};

// 停止自动更新
const stopAutoUpdate = () => {
  if (updateTimer) {
    clearInterval(updateTimer);
    updateTimer = null;
  }
};

onMounted(() => {
  chartInstance = echarts.init(chartRef.value);
  
  // 初始化一些数据
  for (let i = 0; i < 10; i++) {
    generateData();
  }
  
  updateChart();
  startAutoUpdate();
  
  window.addEventListener('resize', () => {
    chartInstance.resize();
  });
});

onUnmounted(() => {
  stopAutoUpdate();
  if (chartInstance) {
    chartInstance.dispose();
  }
  window.removeEventListener('resize', () => {});
});
</script>

<template>
  <div class="chart-container">
    <div class="controls">
      <button @click="startAutoUpdate" class="btn">▶️ 开始</button>
      <button @click="stopAutoUpdate" class="btn">⏸️ 暂停</button>
      <span class="status">
        最新数值:{{ data.value[data.value.length - 1] || 0 }}
      </span>
    </div>
    <div ref="chartRef" class="chart"></div>
  </div>
</template>

<style scoped>
.chart-container {
  padding: 20px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.controls {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 15px;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 5px;
  background: #42b883;
  color: white;
  cursor: pointer;
  font-size: 14px;
}

.btn:hover {
  background: #35a372;
}

.status {
  margin-left: auto;
  font-weight: bold;
  color: #42b883;
}

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

运行结果

  • 图表每2秒自动更新一次
  • 显示最近20个数据点
  • 可以暂停和继续更新
  • 实时显示最新数值

最佳实践

企业级应用场景

场景1:创建可复用的图表组件

在企业应用中,通常需要创建可复用的图表组件:

<!-- components/BaseChart.vue -->
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';

/**
 * 通用图表组件
 * @param {Object} option - ECharts配置项
 * @param {String} theme - 主题名称
 * @param {Number} height - 图表高度
 */
const props = defineProps({
  option: {
    type: Object,
    required: true
  },
  theme: {
    type: String,
    default: 'default'
  },
  height: {
    type: Number,
    default: 400
  }
});

const chartRef = ref(null);
let chartInstance = null;

// 初始化图表
const initChart = () => {
  if (!chartRef.value) return;
  
  chartInstance = echarts.init(chartRef.value, props.theme);
  chartInstance.setOption(props.option);
  
  // 响应式调整
  window.addEventListener('resize', handleResize);
};

// 处理窗口大小变化
const handleResize = () => {
  if (chartInstance) {
    chartInstance.resize();
  }
};

// 监听配置变化
watch(() => props.option, (newOption) => {
  if (chartInstance) {
    chartInstance.setOption(newOption, true);  // true表示不合并
  }
}, { deep: true });

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

onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
  }
  window.removeEventListener('resize', handleResize);
});

// 暴露图表实例,供父组件使用
defineExpose({
  getInstance: () => chartInstance
});
</script>

<template>
  <div 
    ref="chartRef" 
    class="base-chart"
    :style="{ height: height + 'px' }"
  ></div>
</template>

<style scoped>
.base-chart {
  width: 100%;
}
</style>

使用可复用组件

<script setup>
import { ref, computed } from 'vue';
import BaseChart from '@/components/BaseChart.vue';

const salesData = ref([120, 200, 150, 280, 210, 300, 350]);

// 计算图表配置
const chartOption = computed(() => ({
  title: { text: '销售趋势' },
  xAxis: {
    type: 'category',
    data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月']
  },
  yAxis: { type: 'value' },
  series: [{
    type: 'line',
    data: salesData.value,
    smooth: true
  }]
}));

// 更新数据
const updateData = () => {
  salesData.value = salesData.value.map(() => 
    Math.floor(Math.random() * 300) + 100
  );
};
</script>

<template>
  <div>
    <button @click="updateData">更新数据</button>
    <BaseChart :option="chartOption" :height="400" />
  </div>
</template>

优势

  • 代码复用,减少重复
  • 统一管理图表生命周期
  • 自动处理响应式和销毁
  • 易于维护和扩展
场景2:多图表联动

实现多个图表之间的数据联动:

<script setup>
import { ref, computed } from 'vue';
import BaseChart from '@/components/BaseChart.vue';

// 选中的地区
const selectedRegion = ref(null);

// 地区数据
const regionData = ref([
  { name: '北京', sales: 320, products: [80, 120, 60, 60] },
  { name: '上海', sales: 380, products: [100, 140, 80, 60] },
  { name: '广州', sales: 290, products: [70, 100, 70, 50] },
  { name: '深圳', sales: 410, products: [110, 150, 90, 60] }
]);

// 地区销售柱状图配置
const regionChartOption = computed(() => ({
  title: { text: '各地区销售额' },
  tooltip: { trigger: 'axis' },
  xAxis: {
    type: 'category',
    data: regionData.value.map(r => r.name)
  },
  yAxis: { type: 'value' },
  series: [{
    type: 'bar',
    data: regionData.value.map(r => r.sales),
    itemStyle: {
      color: (params) => {
        return params.name === selectedRegion.value ? '#42b883' : '#5470c6';
      }
    }
  }]
}));

// 产品销售饼图配置
const productChartOption = computed(() => {
  const region = regionData.value.find(r => r.name === selectedRegion.value);
  const products = region ? region.products : [0, 0, 0, 0];
  
  return {
    title: { 
      text: selectedRegion.value ? `${selectedRegion.value}产品销售` : '请选择地区'
    },
    tooltip: { trigger: 'item' },
    series: [{
      type: 'pie',
      radius: '60%',
      data: [
        { name: '手机', value: products[0] },
        { name: '电脑', value: products[1] },
        { name: '平板', value: products[2] },
        { name: '其他', value: products[3] }
      ]
    }]
  };
});

// 处理地区点击
const handleRegionClick = (region) => {
  selectedRegion.value = region;
};
</script>

<template>
  <div class="dashboard">
    <div class="chart-row">
      <div class="chart-card">
        <BaseChart 
          :option="regionChartOption" 
          :height="300"
          @click="handleRegionClick"
        />
      </div>
      
      <div class="chart-card">
        <BaseChart 
          :option="productChartOption" 
          :height="300"
        />
      </div>
    </div>
    
    <div class="region-buttons">
      <button 
        v-for="region in regionData" 
        :key="region.name"
        @click="handleRegionClick(region.name)"
        :class="{ active: selectedRegion === region.name }"
      >
        {{ region.name }}
      </button>
    </div>
  </div>
</template>

<style scoped>
.dashboard {
  padding: 20px;
}

.chart-row {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
  margin-bottom: 20px;
}

.chart-card {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.region-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.region-buttons button {
  padding: 10px 20px;
  border: 2px solid #ddd;
  border-radius: 5px;
  background: white;
  cursor: pointer;
  transition: all 0.3s;
}

.region-buttons button.active {
  background: #42b883;
  color: white;
  border-color: #42b883;
}
</style>

优势

  • 多图表数据联动
  • 交互体验流畅
  • 数据关联清晰

常见陷阱

陷阱1:忘记销毁图表实例

错误示例

<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);

onMounted(() => {
  // ❌ 错误:没有保存实例引用,无法销毁
  echarts.init(chartRef.value).setOption({
    // 配置...
  });
});

// 组件卸载时,图表实例没有被销毁,造成内存泄漏
</script>

正确做法

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;  // ✅ 保存实例引用

onMounted(() => {
  chartInstance = echarts.init(chartRef.value);
  chartInstance.setOption({
    // 配置...
  });
});

onUnmounted(() => {
  // ✅ 正确:销毁图表实例
  if (chartInstance) {
    chartInstance.dispose();
    chartInstance = null;
  }
});
</script>

原因分析

  • 不销毁图表会导致内存泄漏
  • 特别是在单页应用中频繁切换页面时
  • 必须在组件卸载时调用dispose()
陷阱2:图表容器没有设置高度

错误示例

<template>
  <!-- ❌ 错误:容器没有高度,图表无法显示 -->
  <div ref="chartRef"></div>
</template>

正确做法

<template>
  <!-- ✅ 正确:明确设置容器高度 -->
  <div ref="chartRef" style="height: 400px;"></div>
  
  <!-- 或使用CSS类 -->
  <div ref="chartRef" class="chart-container"></div>
</template>

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

原因分析

  • ECharts需要明确的容器尺寸
  • 容器高度为0时,图表无法渲染
  • 可以使用固定高度或百分比高度(父元素需要有高度)
陷阱3:数据更新后图表不刷新

错误示例

<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
const data = ref([120, 200, 150]);

onMounted(() => {
  const chart = echarts.init(chartRef.value);
  chart.setOption({
    series: [{ type: 'line', data: data.value }]
  });
  
  // ❌ 错误:数据变化后没有更新图表
  setTimeout(() => {
    data.value = [200, 300, 250];
    // 图表不会自动更新
  }, 2000);
});
</script>

正确做法

<script setup>
import { ref, onMounted, watch } from 'vue';
import * as echarts from 'echarts';

const chartRef = ref(null);
const data = ref([120, 200, 150]);
let chartInstance = null;

onMounted(() => {
  chartInstance = echarts.init(chartRef.value);
  updateChart();
});

// ✅ 正确:监听数据变化,更新图表
watch(data, () => {
  updateChart();
}, { deep: true });

const updateChart = () => {
  if (!chartInstance) return;
  
  chartInstance.setOption({
    series: [{ type: 'line', data: data.value }]
  });
};

// 更新数据
setTimeout(() => {
  data.value = [200, 300, 250];  // 图表会自动更新
}, 2000);
</script>

原因分析

  • ECharts不会自动监听数据变化
  • 需要手动调用setOption()更新图表
  • 使用watch监听数据变化是最佳实践

性能优化建议

建议1:按需引入
// ❌ 不推荐:全量引入(体积大)
import * as echarts from 'echarts';

// ✅ 推荐:按需引入(减小体积)
import * as echarts from 'echarts/core';
import { LineChart, BarChart, PieChart } from 'echarts/charts';
import {
  TitleComponent,
  TooltipComponent,
  GridComponent,
  LegendComponent
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

// 注册必需的组件
echarts.use([
  LineChart,
  BarChart,
  PieChart,
  TitleComponent,
  TooltipComponent,
  GridComponent,
  LegendComponent,
  CanvasRenderer
]);
建议2:大数据量优化
// 处理大数据量时的优化配置
const option = {
  series: [{
    type: 'line',
    data: largeDataArray,
    // 开启大数据量优化
    large: true,
    largeThreshold: 2000,  // 数据量超过2000时启用优化
    // 采样策略
    sampling: 'lttb'  // 使用LTTB采样算法
  }]
};
建议3:使用防抖优化resize
<script setup>
import { useDebounceFn } from '@vueuse/core';

// 使用防抖优化resize事件
const debouncedResize = useDebounceFn(() => {
  if (chartInstance) {
    chartInstance.resize();
  }
}, 200);

onMounted(() => {
  window.addEventListener('resize', debouncedResize);
});

onUnmounted(() => {
  window.removeEventListener('resize', debouncedResize);
});
</script>

实践练习

练习1:创建销售仪表盘(难度:中等)

需求描述

创建一个销售数据仪表盘,包含:

  1. 月度销售趋势折线图
  2. 产品销售占比饼图
  3. 地区销售对比柱状图
  4. 数据可以动态更新
  5. 图表响应式布局

实现提示

  1. 使用Grid布局组织多个图表
  2. 创建可复用的图表组件
  3. 使用computed计算图表配置
  4. 实现数据更新功能
📖 点击查看参考答案
<script setup>
import { ref, computed } from 'vue';
import BaseChart from '@/components/BaseChart.vue';

// 销售数据
const salesData = ref({
  months: ['1月', '2月', '3月', '4月', '5月', '6月'],
  sales: [120, 200, 150, 280, 210, 300],
  products: [
    { name: '手机', value: 3500 },
    { name: '电脑', value: 2800 },
    { name: '平板', value: 1800 },
    { name: '其他', value: 900 }
  ],
  regions: [
    { name: '北京', value: 320 },
    { name: '上海', value: 380 },
    { name: '广州', value: 290 },
    { name: '深圳', value: 410 }
  ]
});

// 折线图配置
const trendOption = computed(() => ({
  title: { text: '月度销售趋势', left: 'center' },
  tooltip: { trigger: 'axis' },
  xAxis: {
    type: 'category',
    data: salesData.value.months
  },
  yAxis: { type: 'value', name: '销售额(万元)' },
  series: [{
    type: 'line',
    data: salesData.value.sales,
    smooth: true,
    itemStyle: { color: '#42b883' },
    areaStyle: {
      color: {
        type: 'linear',
        x: 0, y: 0, x2: 0, y2: 1,
        colorStops: [
          { offset: 0, color: 'rgba(66, 184, 131, 0.3)' },
          { offset: 1, color: 'rgba(66, 184, 131, 0.05)' }
        ]
      }
    }
  }]
}));

// 饼图配置
const productOption = computed(() => ({
  title: { text: '产品销售占比', left: 'center' },
  tooltip: { trigger: 'item' },
  legend: { orient: 'vertical', left: 'left' },
  series: [{
    type: 'pie',
    radius: '60%',
    data: salesData.value.products,
    emphasis: {
      itemStyle: {
        shadowBlur: 10,
        shadowOffsetX: 0,
        shadowColor: 'rgba(0, 0, 0, 0.5)'
      }
    }
  }]
}));

// 柱状图配置
const regionOption = computed(() => ({
  title: { text: '地区销售对比', left: 'center' },
  tooltip: { trigger: 'axis' },
  xAxis: {
    type: 'category',
    data: salesData.value.regions.map(r => r.name)
  },
  yAxis: { type: 'value', name: '销售额(万元)' },
  series: [{
    type: 'bar',
    data: salesData.value.regions.map(r => r.value),
    itemStyle: { color: '#5470c6' },
    label: { show: true, position: 'top' }
  }]
}));

// 模拟数据更新
const refreshData = () => {
  salesData.value.sales = salesData.value.sales.map(() => 
    Math.floor(Math.random() * 300) + 100
  );
  salesData.value.products = salesData.value.products.map(p => ({
    ...p,
    value: Math.floor(Math.random() * 3000) + 1000
  }));
  salesData.value.regions = salesData.value.regions.map(r => ({
    ...r,
    value: Math.floor(Math.random() * 400) + 200
  }));
};
</script>

<template>
  <div class="dashboard">
    <div class="header">
      <h2>销售数据仪表盘</h2>
      <button @click="refreshData" class="refresh-btn">
        🔄 刷新数据
      </button>
    </div>
    
    <div class="charts-grid">
      <div class="chart-card full-width">
        <BaseChart :option="trendOption" :height="300" />
      </div>
      
      <div class="chart-card">
        <BaseChart :option="productOption" :height="300" />
      </div>
      
      <div class="chart-card">
        <BaseChart :option="regionOption" :height="300" />
      </div>
    </div>
  </div>
</template>

<style scoped>
.dashboard {
  padding: 20px;
  background: #f5f5f5;
  min-height: 100vh;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.refresh-btn {
  padding: 10px 20px;
  background: #42b883;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  font-size: 14px;
}

.refresh-btn:hover {
  background: #35a372;
}

.charts-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px;
}

.chart-card {
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.full-width {
  grid-column: 1 / -1;
}

@media (max-width: 768px) {
  .charts-grid {
    grid-template-columns: 1fr;
  }
}
</style>

答案解析

  1. 组件复用:使用BaseChart组件减少重复代码
  2. 响应式数据:使用computed自动更新图表配置
  3. 布局设计:使用Grid布局实现响应式
  4. 数据更新:点击按钮刷新所有图表数据
  5. 移动适配:小屏幕自动切换为单列布局

练习2:实现图表主题切换(难度:简单)

需求描述

为图表添加主题切换功能:

  1. 支持明亮和暗黑两种主题
  2. 主题切换时图表平滑过渡
  3. 主题设置保存到localStorage

实现提示

  1. 使用ECharts内置主题或自定义主题
  2. 使用useLocalStorage保存主题设置
  3. 监听主题变化,重新初始化图表
📖 点击查看参考答案
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue';
import { useLocalStorage } from '@vueuse/core';
import * as echarts from 'echarts';

const chartRef = ref(null);
let chartInstance = null;

// 主题设置(保存到localStorage)
const theme = useLocalStorage('chart-theme', 'light');

// 图表配置
const option = {
  title: { text: '主题切换示例' },
  xAxis: {
    type: 'category',
    data: ['周一', '周二', '周三', '周四', '周五']
  },
  yAxis: { type: 'value' },
  series: [{
    type: 'bar',
    data: [120, 200, 150, 280, 210]
  }]
};

// 初始化图表
const initChart = () => {
  if (chartInstance) {
    chartInstance.dispose();
  }
  
  // 使用指定主题初始化
  chartInstance = echarts.init(chartRef.value, theme.value);
  chartInstance.setOption(option);
};

// 切换主题
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
};

// 监听主题变化
watch(theme, () => {
  initChart();
});

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

onUnmounted(() => {
  if (chartInstance) {
    chartInstance.dispose();
  }
});
</script>

<template>
  <div :class="['container', theme]">
    <div class="controls">
      <button @click="toggleTheme">
        {{ theme === 'light' ? '🌙 切换暗黑' : '☀️ 切换明亮' }}
      </button>
    </div>
    <div ref="chartRef" class="chart"></div>
  </div>
</template>

<style scoped>
.container {
  padding: 20px;
  min-height: 100vh;
  transition: background 0.3s;
}

.container.light {
  background: #ffffff;
}

.container.dark {
  background: #1a1a1a;
}

.controls {
  margin-bottom: 20px;
}

.controls button {
  padding: 10px 20px;
  border: none;
  border-radius: 5px;
  background: #42b883;
  color: white;
  cursor: pointer;
}

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

答案解析

  1. 主题保存:使用useLocalStorage持久化主题设置
  2. 主题切换:监听主题变化,重新初始化图表
  3. 平滑过渡:使用CSS transition实现背景色过渡
  4. 用户体验:刷新页面后主题保持不变

进阶阅读

下一步

恭喜你完成了ECharts的学习!

接下来你可以:

  1. 学习表单验证Vuelidate验证库
  2. 学习国际化Vue I18n
  3. 学习日期处理Dayjs日期库
  4. 学习HTTP请求Axios拦截器
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐