ECharts 数据可视化
大约 9 分钟约 2843 字
ECharts 数据可视化
简介
ECharts 是百度开源的 JavaScript 图表库,支持折线图、柱状图、饼图、散点图、地图等几十种图表类型。Vue3 项目通过 vue-echarts 或直接封装 ECharts 实例,可以快速构建数据看板、实时监控和统计报表。
ECharts 最初由百度 EFE(前端技术部)开发,现已捐赠给 Apache 基金会(Apache ECharts)。它基于 Canvas 和 SVG 渲染,支持千万级数据量渲染,提供丰富的交互能力(缩放、拖拽、联动、下钻)和主题定制。在工业监控、金融报表、数据大屏等场景中被广泛使用。
特点
基础配置
安装和使用
npm install echarts vue-echarts// 全局注册
import { createApp } from 'vue'
import ECharts from 'vue-echarts'
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { LineChart, BarChart, PieChart } from 'echarts/charts'
import {
TitleComponent, TooltipComponent, LegendComponent,
GridComponent, DataZoomComponent
} from 'echarts/components'
use([CanvasRenderer, LineChart, BarChart, PieChart,
TitleComponent, TooltipComponent, LegendComponent,
GridComponent, DataZoomComponent])
app.component('VChart', ECharts)按需引入(减小包体积)
// echarts/setup.ts —— 只引入需要的模块
import { use, type ComposeOption } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import {
LineChart, BarChart, PieChart, ScatterChart,
type LineSeriesOption, type BarSeriesOption,
type PieSeriesOption, type ScatterSeriesOption,
} from 'echarts/charts'
import {
TitleComponent, type TitleComponentOption,
TooltipComponent, type TooltipComponentOption,
LegendComponent, type LegendComponentOption,
GridComponent, type GridComponentOption,
DataZoomComponent, type DataZoomComponentOption,
ToolboxComponent, type ToolboxComponentOption,
DatasetComponent, type DatasetComponentOption,
TransformComponent,
} from 'echarts/components'
use([
CanvasRenderer,
LineChart, BarChart, PieChart, ScatterChart,
TitleComponent, TooltipComponent, LegendComponent,
GridComponent, DataZoomComponent, ToolboxComponent,
DatasetComponent, TransformComponent,
])
// 组合类型(获得完整的类型提示)
type EChartsOption = ComposeOption<
| LineSeriesOption
| BarSeriesOption
| PieSeriesOption
| ScatterSeriesOption
| TitleComponentOption
| TooltipComponentOption
| LegendComponentOption
| GridComponentOption
| DataZoomComponentOption
| ToolboxComponentOption
| DatasetComponentOption
>
export type { EChartsOption }封装通用图表组件
<!-- components/BaseChart.vue -->
<template>
<VChart
ref="chartRef"
:option="mergedOption"
:loading="loading"
:autoresize="true"
:style="{ height: height }"
:theme="theme"
@click="handleClick"
@legendselectchanged="handleLegendSelect"
/>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import VChart from 'vue-echarts'
import type { EChartsOption } from '@/echarts/setup'
interface Props {
option: EChartsOption
loading?: boolean
height?: string
theme?: string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
height: '400px',
theme: 'default',
})
const emit = defineEmits<{
click: [params: any]
legendSelect: [params: any]
}>()
const chartRef = ref<InstanceType<typeof VChart>>()
const mergedOption = computed(() => ({
backgroundColor: 'transparent',
animationDuration: 800,
animationEasing: 'cubicInOut',
...props.option,
}))
function handleClick(params: any) {
emit('click', params)
}
function handleLegendSelect(params: any) {
emit('legendSelect', params)
}
// 暴露图表实例方法
function getChartInstance() {
return chartRef.value?.chart
}
function resize() {
chartRef.value?.resize()
}
// 导出为图片
function toDataURL(type = 'png') {
return chartRef.value?.chart.getDataURL({ type, pixelRatio: 2 })
}
defineExpose({ getChartInstance, resize, toDataURL })
</script>基础图表
<!-- 折线图 -->
<script setup lang="ts">
import { ref } from 'vue'
const lineOption = ref({
title: { text: '月度销售额' },
tooltip: { trigger: 'axis' },
legend: { data: ['2024年', '2023年'] },
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: { type: 'value', name: '万元' },
series: [
{
name: '2024年',
type: 'line',
data: [120, 132, 101, 134, 190, 230],
smooth: true,
areaStyle: { opacity: 0.3 }
},
{
name: '2023年',
type: 'line',
data: [80, 92, 71, 104, 150, 180],
smooth: true
}
]
})
</script>
<template>
<VChart :option="lineOption" style="height: 400px" autoresize />
</template>组合图表
柱状图 + 折线图
// 混合图表 — 销量 + 增长率
const mixOption = ref({
title: { text: '产品销量与增长率' },
tooltip: { trigger: 'axis' },
legend: { data: ['销量', '增长率'] },
xAxis: {
type: 'category',
data: ['产品A', '产品B', '产品C', '产品D', '产品E']
},
yAxis: [
{ type: 'value', name: '销量', position: 'left' },
{ type: 'value', name: '增长率(%)', position: 'right', axisLabel: { formatter: '{value}%' } }
],
series: [
{
name: '销量',
type: 'bar',
data: [320, 280, 250, 200, 150],
itemStyle: { borderRadius: [4, 4, 0, 0] }
},
{
name: '增长率',
type: 'line',
yAxisIndex: 1,
data: [15, 12, 8, -5, -10],
itemStyle: { color: '#ee6666' }
}
]
})饼图
// 饼图 — 分类占比
const pieOption = ref({
title: { text: '设备类型分布', left: 'center' },
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', left: 'left' },
series: [{
type: 'pie',
radius: ['40%', '70%'], // 环形图
avoidLabelOverlap: true,
itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 },
label: { show: true, formatter: '{b}\n{d}%' },
data: [
{ value: 35, name: 'PLC', itemStyle: { color: '#5470c6' } },
{ value: 25, name: '传感器', itemStyle: { color: '#91cc75' } },
{ value: 20, name: '网关', itemStyle: { color: '#fac858' } },
{ value: 15, name: 'IPC', itemStyle: { color: '#ee6666' } },
{ value: 5, name: '其他', itemStyle: { color: '#73c0de' } }
]
}]
})水印柱状图与渐变色
// 渐变柱状图
const gradientBarOption = ref({
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: { type: 'value' },
series: [{
type: 'bar',
data: [120, 200, 150, 80, 70, 110, 130],
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#79bbff' },
],
},
},
// 柱子背景
showBackground: true,
backgroundStyle: {
color: 'rgba(180, 180, 180, 0.1)',
borderRadius: [4, 4, 0, 0],
},
}],
})
// 堆叠柱状图
const stackBarOption = ref({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: { data: ['直接访问', '搜索引擎', '邮件营销', '联盟广告'] },
xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五'] },
yAxis: { type: 'value' },
series: [
{ name: '直接访问', type: 'bar', stack: 'total', data: [320, 302, 301, 334, 390] },
{ name: '搜索引擎', type: 'bar', stack: 'total', data: [120, 132, 101, 134, 90] },
{ name: '邮件营销', type: 'bar', stack: 'total', data: [220, 182, 191, 234, 290] },
{ name: '联盟广告', type: 'bar', stack: 'total', data: [150, 212, 201, 154, 190] },
],
})散点图与热力图
// 散点图
const scatterOption = ref({
tooltip: { trigger: 'item', formatter: '{a}: ({c})' },
xAxis: { type: 'value', name: '面积(m²)' },
yAxis: { type: 'value', name: '价格(万)' },
series: [{
name: '房源',
type: 'scatter',
symbolSize: 10,
data: [
[60, 150], [80, 200], [100, 280], [120, 350],
[50, 120], [70, 180], [90, 250], [110, 300],
[40, 100], [130, 400],
],
itemStyle: { color: '#5470c6' },
}],
})
// 带回归线的散点图
const scatterWithLine = ref({
xAxis: { type: 'value' },
yAxis: { type: 'value' },
series: [
{
name: '数据点',
type: 'scatter',
data: generateRandomData(50),
},
{
name: '趋势线',
type: 'line',
smooth: true,
showSymbol: false,
data: [[0, 20], [100, 180]],
lineStyle: { type: 'dashed', color: '#ee6666' },
},
],
})实时数据
动态更新图表
// 实时数据更新
const chartData = ref<number[]>([])
const chartLabels = ref<string[]>([])
// 定时追加数据
let timer: number
onMounted(() => {
timer = window.setInterval(() => {
const now = new Date()
chartLabels.value.push(now.toLocaleTimeString())
chartData.value.push(Math.random() * 100)
// 最多保留 30 个点
if (chartLabels.value.length > 30) {
chartLabels.value.shift()
chartData.value.shift()
}
}, 1000)
})
onUnmounted(() => clearInterval(timer))
const realtimeOption = computed(() => ({
title: { text: '实时温度监控' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: chartLabels.value },
yAxis: { type: 'value', name: '温度(℃)', min: 0, max: 100 },
series: [{
type: 'line',
data: chartData.value,
smooth: true,
markLine: {
data: [
{ yAxis: 80, name: '报警线', lineStyle: { color: 'red' } },
{ yAxis: 60, name: '预警线', lineStyle: { color: 'orange' } }
]
}
}]
}))WebSocket 实时数据
// 基于 WebSocket 的实时图表
import { ref, computed, onMounted, onUnmounted } from 'vue'
function useRealtimeChart(url: string, maxPoints = 60) {
const data = ref<{ time: string; value: number }[]>([])
let ws: WebSocket | null = null
let reconnectTimer: number
function connect() {
ws = new WebSocket(url)
ws.onmessage = (event) => {
const point = JSON.parse(event.data)
data.value.push(point)
if (data.value.length > maxPoints) {
data.value.shift()
}
}
ws.onclose = () => {
reconnectTimer = window.setTimeout(connect, 3000)
}
ws.onerror = () => {
ws?.close()
}
}
onMounted(() => connect())
onUnmounted(() => {
ws?.close()
clearTimeout(reconnectTimer)
})
const option = computed(() => ({
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: data.value.map(d => d.time),
boundaryGap: false,
},
yAxis: { type: 'value', scale: true },
series: [{
type: 'line',
data: data.value.map(d => d.value),
smooth: true,
showSymbol: false,
areaStyle: { opacity: 0.15 },
}],
}))
return { option, data }
}
// 使用
const { option: realtimeOption } = useRealtimeChart('wss://api.example.com/ws/temperature')主题定制
// 自定义主题
// echarts/theme.ts
const darkTheme = {
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'],
backgroundColor: '#1a1a2e',
textStyle: { color: '#ccc' },
title: { textStyle: { color: '#eee' } },
legend: { textStyle: { color: '#ccc' } },
tooltip: {
backgroundColor: 'rgba(50, 50, 50, 0.9)',
textStyle: { color: '#fff' },
},
xAxis: {
axisLine: { lineStyle: { color: '#555' } },
axisLabel: { color: '#ccc' },
splitLine: { lineStyle: { color: '#333' } },
},
yAxis: {
axisLine: { lineStyle: { color: '#555' } },
axisLabel: { color: '#ccc' },
splitLine: { lineStyle: { color: '#333' } },
},
}
// 注册主题
import { registerTheme } from 'echarts/core'
registerTheme('dark-custom', darkTheme)
// 使用自定义主题
// <VChart theme="dark-custom" :option="option" />性能优化
// 大数据量渲染优化
// 1. 开启渐进式渲染
const largeLineOption = {
series: [{
type: 'line',
data: largeDataArray, // 10万+ 数据点
progressive: 5000, // 渐进式渲染阈值
progressiveThreshold: 3000, // 超过 3000 个点启用渐进式渲染
}],
}
// 2. 使用 dataset(数据集)减少配置量
const datasetOption = {
dataset: {
source: [
['产品', '2023', '2024'],
['A', 120, 200],
['B', 200, 150],
['C', 150, 280],
],
},
xAxis: { type: 'category' },
yAxis: { type: 'value' },
series: [
{ type: 'bar', encode: { x: 0, y: 1 }, name: '2023' },
{ type: 'bar', encode: { x: 0, y: 2 }, name: '2024' },
],
}
// 3. 使用 transform 进行数据转换
const transformOption = {
dataset: [
{
source: rawData,
},
{
transform: {
type: 'sort',
config: { dimension: 1, order: 'desc' },
},
},
],
series: [{ type: 'bar' }],
}
// 4. 懒加载图表(减少首屏负担)
// 路由配置
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue'),
},
]Dashboard 布局
多图表看板
<template>
<div class="dashboard">
<el-row :gutter="16">
<el-col :span="6" v-for="stat in stats" :key="stat.label">
<div class="stat-card">
<span class="stat-value">{{ stat.value }}</span>
<span class="stat-label">{{ stat.label }}</span>
</div>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-top: 16px">
<el-col :span="16">
<div class="chart-card">
<VChart :option="trendOption" style="height: 350px" autoresize />
</div>
</el-col>
<el-col :span="8">
<div class="chart-card">
<VChart :option="pieOption" style="height: 350px" autoresize />
</div>
</el-col>
</el-row>
</div>
</template>优点
缺点
总结
ECharts 是前端数据可视化的首选。Vue3 项目推荐 vue-echarts 组件化使用。核心模式:定义 option 对象 -> 绑定到 VChart 组件 -> 数据变化自动更新。按需引入 charts/components 减小体积。实时数据用 computed 动态更新 option,Dashboard 用 Grid 布局组合多图表。
最佳实践清单:
- 始终按需引入 ECharts 模块,避免全量引入增大包体积
- 封装 BaseChart 组件统一处理 loading、resize、主题
- 大数据量使用 progressive 渐进式渲染
- 使用 dataset 管理数据,减少 series 配置重复
- 实时图表使用 WebSocket + computed 响应式更新
- 图表组件使用 v-if 或路由懒加载,避免首屏加载过多图表
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
- 前端主题最好同时看浏览器原理、框架机制和工程化约束。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
- 对关键页面先建立状态流和数据流,再考虑组件拆分。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
- 只追框架新特性,不分析实际渲染成本。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
- 继续补齐设计系统、SSR/边缘渲染、监控告警和组件库治理。
适用场景
- 当你准备把《ECharts 数据可视化》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《ECharts 数据可视化》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《ECharts 数据可视化》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《ECharts 数据可视化》最大的收益和代价分别是什么?
