Echarts图表动态更新实战 定时刷新数据实时展示监控大屏自动轮播案例教程
说实话,做数据可视化的同学90%都踩过同一个坑——图表一旦渲染出来就死了,数据变了它还在那儿摆着原来的样子。今天咱们就把这个痛点彻底解决掉,做一个真正活起来的监控大屏。
先搞懂动态更新的核心逻辑
Echarts图表动态更新的本质其实就三步:获取新数据、更新图表实例、重新渲染。听起来简单,但真做起来有很多细节要注意。
先来看一个最基础的定时刷新demo,帮你理解整个机制:
// 创建图表实例
const chartDom = document.getElementById('main');
const myChart = echarts.init(chartDom);
// 初始数据
let currentData = [150, 230, 224, 218, 135, 147, 260];
// 渲染函数
function renderChart(data) {
myChart.setOption({
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [{
data: data,
type: 'line',
smooth: true,
areaStyle: {}
}]
}, true); // 第二个参数true表示不合并,直接替换
}
// 初始渲染
renderChart(currentData);
// 定时刷新 - 每3秒更新一次
setInterval(() => {
// 模拟新数据
currentData = currentData.map(item => item + Math.floor(Math.random() * 100 - 50));
renderChart(currentData);
}, 3000);
这个例子虽然简单,但已经把核心逻辑讲清楚了。每次调用setOption时,Echarts会对比新旧配置,只更新变化的部分,性能上比直接dispose重建要高效得多。
监控大屏的完整架构
真正的监控大屏不是单一图表,而是多个图表组成的网格布局。我直接给你看一个实战项目的结构:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>实时数据监控大屏</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a1a;
color: #fff;
font-family: 'PingFang SC', sans-serif;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
height: 80px;
background: linear-gradient(90deg, transparent, rgba(0, 150, 255, 0.3), transparent);
display: flex;
align-items: center;
justify-content: center;
border-bottom: 2px solid rgba(0, 150, 255, 0.5);
}
.header h1 {
font-size: 32px;
letter-spacing: 8px;
text-shadow: 0 0 20px rgba(0, 150, 255, 0.8);
}
/* 时间显示 */
.time-display {
position: absolute;
right: 40px;
top: 25px;
font-size: 18px;
color: #00d4ff;
}
/* 主内容区 - 网格布局 */
.main-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 15px;
padding: 15px;
height: calc(100vh - 80px);
}
/* 每个卡片容器 */
.chart-card {
background: rgba(10, 20, 40, 0.8);
border: 1px solid rgba(0, 150, 255, 0.3);
border-radius: 8px;
padding: 15px;
position: relative;
transition: all 0.3s;
}
.chart-card:hover {
border-color: rgba(0, 150, 255, 0.8);
box-shadow: 0 0 30px rgba(0, 150, 255, 0.2);
}
/* 卡片标题 */
.chart-card .card-title {
font-size: 16px;
color: #00d4ff;
margin-bottom: 10px;
padding-left: 10px;
border-left: 3px solid #00d4ff;
}
/* 图表容器 */
.chart-container {
width: 100%;
height: calc(100% - 35px);
}
/* 特殊布局 */
.chart-card.large { grid-column: span 2; }
.chart-card.tall { grid-row: span 2; }
/* 状态指示器 */
.status-indicator {
position: absolute;
top: 15px;
right: 15px;
width: 10px;
height: 10px;
border-radius: 50%;
background: #00ff88;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="header">
<h1>实时监控数据大屏</h1>
<div class="time-display" id="currentTime"></div>
</div>
<div class="main-container">
<div class="chart-card large">
<div class="card-title">实时流量趋势</div>
<div class="status-indicator"></div>
<div id="chart1" class="chart-container"></div>
</div>
<div class="chart-card">
<div class="card-title">系统负载</div>
<div class="status-indicator"></div>
<div id="chart2" class="chart-container"></div>
</div>
<div class="chart-card">
<div class="card-title">用户分布</div>
<div class="status-indicator"></div>
<div id="chart3" class="chart-container"></div>
</div>
<div class="chart-card">
<div class="card-title">响应时间</div>
<div class="status-indicator"></div>
<div id="chart4" class="chart-container"></div>
</div>
<div class="chart-card large">
<div class="card-title">错误日志统计</div>
<div class="status-indicator"></div>
<div id="chart5" class="chart-container"></div>
</div>
<div class="chart-card">
<div class="card-title">数据库连接数</div>
<div class="status-indicator"></div>
<div id="chart6" class="chart-container"></div>
</div>
</div>
<script>
// 时间显示
function updateTime() {
const now = new Date();
document.getElementById('currentTime').textContent =
now.toLocaleString('zh-CN', { hour12: false });
}
setInterval(updateTime, 1000);
updateTime();
// 图表配置
const charts = {};
// 初始化所有图表
function initCharts() {
charts.line = echarts.init(document.getElementById('chart1'));
charts.gauge = echarts.init(document.getElementById('chart2'));
charts.pie = echarts.init(document.getElementById('chart3'));
charts.bar = echarts.init(document.getElementById('chart4'));
charts.heatmap = echarts.init(document.getElementById('chart5'));
charts.radar = echarts.init(document.getElementById('chart6'));
}
initCharts();
// 模拟数据源
const mockData = {
line: {
xAxis: [],
series: [[], []]
},
gauge: { value: 45 },
pie: { data: [] },
bar: { xAxis: [], series: [] },
heatmap: { xAxis: [], yAxis: [], data: [] },
radar: { indicator: [], series: [] }
};
// 初始化数据
function initMockData() {
const hours = Array.from({length: 24}, (_, i) => `${i}:00`);
mockData.line.xAxis = hours;
mockData.line.series[0] = hours.map(() => Math.floor(Math.random() * 1000 + 500));
mockData.line.series[1] = hours.map(() => Math.floor(Math.random() * 800 + 300));
mockData.pie.data = [
{ value: Math.floor(Math.random() * 100), name: '北京' },
{ value: Math.floor(Math.random() * 100), name: '上海' },
{ value: Math.floor(Math.random() * 100), name: '广州' },
{ value: Math.floor(Math.random() * 100), name: '深圳' },
{ value: Math.floor(Math.random() * 100), name: '其他' }
];
mockData.bar.xAxis = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
mockData.bar.series = [
Array.from({length: 5}, () => Math.floor(Math.random() * 500))
];
mockData.heatmap.xAxis = hours;
mockData.heatmap.yAxis = ['周一', '周二', '周三', '周四', '周五'];
mockData.heatmap.data = [];
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 24; j++) {
mockData.heatmap.data.push([j, i, Math.floor(Math.random() * 100)]);
}
}
mockData.radar.indicator = [
{ name: 'CPU', max: 100 },
{ name: '内存', max: 100 },
{ name: '磁盘', max: 100 },
{ name: '网络', max: 100 },
{ name: 'IO', max: 100 }
];
mockData.radar.series = [
Array.from({length: 5}, () => Math.floor(Math.random() * 100))
];
}
initMockData();
// 配置项
const options = {
line: {
tooltip: { trigger: 'axis' },
legend: { data: ['流入', '流出'], textStyle: { color: '#fff' }, top: 0 },
grid: { top: 40, bottom: 20, left: 50, right: 20 },
xAxis: { type: 'category', data: mockData.line.xAxis, axisLine: { lineStyle: { color: '#333' } }, axisLabel: { color: '#aaa' } },
yAxis: { type: 'value', axisLine: { lineStyle: { color: '#333' } }, axisLabel: { color: '#aaa' }, splitLine: { lineStyle: { color: '#222' } } },
series: [
{ name: '流入', type: 'line', smooth: true, data: mockData.line.series[0], itemStyle: { color: '#00d4ff' }, areaStyle: { opacity: 0.2 } },
{ name: '流出', type: 'line', smooth: true, data: mockData.line.series[1], itemStyle: { color: '#ff6b6b' }, areaStyle: { opacity: 0.2 } }
]
},
gauge: {
series: [{
type: 'gauge',
center: ['50%', '60%'],
radius: '80%',
min: 0, max: 100,
progress: { show: true, width: 15, itemStyle: { color: '#00ff88' } },
axisLine: { lineStyle: { width: 15, color: [[0.3, '#67b7ff'], [0.7, '#ffbd13'], [1, '#ff6b6b']] } },
axisTick: { show: false },
splitLine: { length: 15, lineStyle: { width: 2, color: '#999' } },
pointer: { length: '50%', width: 5, itemStyle: { color: 'auto' } },
anchor: { show: true, size: 15, itemStyle: { borderColor: '#fff', borderWidth: 3 } },
axisLabel: { distance: 20, color: '#999', fontSize: 10 },
detail: { valueAnimation: true, formatter: '{value}%', fontSize: 20, color: '#fff', offsetCenter: [0, '20%'] },
data: [{ value: mockData.gauge.value, name: 'CPU负载' }]
}]
},
pie: {
tooltip: { trigger: 'item' },
legend: { orient: 'vertical', right: 10, top: 'center', textStyle: { color: '#aaa' } },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 10, borderColor: '#0a0a1a', borderWidth: 2 },
label: { show: false },
emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold', color: '#fff' } },
data: mockData.pie.data
}]
},
bar: {
tooltip: { trigger: 'axis', backgroundColor: 'rgba(10,20,40,0.9)', textStyle: { color: '#fff' } },
grid: { top: 20, bottom: 20, left: 50, right: 20 },
xAxis: { type: 'category', data: mockData.bar.xAxis, axisLine: { lineStyle: { color: '#333' } }, axisLabel: { color: '#aaa' } },
yAxis: { type: 'value', axisLine: { lineStyle: { color: '#333' } }, axisLabel: { color: '#aaa' }, splitLine: { lineStyle: { color: '#222' } } },
series: [{
data: mockData.bar.series[0],
type: 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00d4ff' },
{ offset: 1, color: '#0066cc' }
]),
borderRadius: [4, 4, 0, 0]
}
}]
},
heatmap: {
tooltip: { position: 'top', formatter: p => `${mockData.heatmap.xAxis[p.data[0]]}<br>${p.data[1]}: ${p.data[2]}` },
grid: { top: 10, bottom: 30, left: 60, right: 10 },
xAxis: { type: 'category', data: mockData.heatmap.xAxis, splitArea: { show: true, areaStyle: { color: ['rgba(255,255,255,0.02)', 'rgba(0,0,0,0.1)'] } }, axisLabel: { color: '#aaa', rotate: 45 } },
yAxis: { type: 'category', data: mockData.heatmap.yAxis, splitArea: { show: true }, axisLabel: { color: '#aaa' } },
visualMap: { min: 0, max: 100, show: false, calculable: false, inRange: { color: ['#0a0a1a', '#0066cc', '#00d4ff'] } },
series: [{ type: 'heatmap', data: mockData.heatmap.data, label: { show: false } }]
},
radar: {
tooltip: { trigger: 'item' },
legend: { data: ['资源使用率'], textStyle: { color: '#aaa' }, top: 0 },
radar: {
indicator: mockData.radar.indicator,
shape: 'circle',
splitNumber: 5,
axisName: { color: '#00d4ff' },
splitLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.2)' } },
splitArea: { show: false },
axisLine: { lineStyle: { color: 'rgba(0, 212, 255, 0.3)' } }
},
series: [{
type: 'radar',
data: [{ value: mockData.radar.series[0], name: '资源使用率', areaStyle: { opacity: 0.3 }, lineStyle: { color: '#00d4ff' }, itemStyle: { color: '#00d4ff' } }]
}]
}
};
// 渲染所有图表
function renderAllCharts() {
Object.keys(charts).forEach(key => {
charts[key].setOption(options[key], true);
});
}
renderAllCharts();
// 动态更新数据
function updateCharts() {
// 更新折线图 - 移除最旧数据,添加新数据
mockData.line.series[0].shift();
mockData.line.series[0].push(Math.floor(Math.random() * 1000 + 500));
mockData.line.series[1].shift();
mockData.line.series[1].push(Math.floor(Math.random() * 800 + 300));
charts.line.setOption({
series: [
{ data: mockData.line.series[0] },
{ data: mockData.line.series[1] }
]
});
// 更新仪表盘
mockData.gauge.value = Math.floor(Math.random() * 100);
charts.gauge.setOption({
series: [{ data: [{ value: mockData.gauge.value, name: 'CPU负载' }] }]
});
// 更新饼图
mockData.pie.data.forEach(item => {
item.value = Math.floor(Math.random() * 100);
});
charts.pie.setOption({
series: [{ data: mockData.pie.data }]
});
// 更新柱状图
mockData.bar.series[0] = Array.from({length: 5}, () => Math.floor(Math.random() * 500));
charts.bar.setOption({
series: [{ data: mockData.bar.series[0] }]
});
// 更新热力图
mockData.heatmap.data = mockData.heatmap.data.map(point => [
point[0], point[1], Math.floor(Math.random() * 100)
]);
charts.heatmap.setOption({
series: [{ data: mockData.heatmap.data }]
});
// 更新雷达图
mockData.radar.series[0] = Array.from({length: 5}, () => Math.floor(Math.random() * 100));
charts.radar.setOption({
series: [{ value: mockData.radar.series[0] }]
});
}
// 每2秒更新一次数据
setInterval(updateCharts, 2000);
// 窗口大小变化时自适应
window.addEventListener('resize', () => {
Object.values(charts).forEach(chart => chart.resize());
});
</script>
</body>
</html>
这段代码可以直接保存为HTML文件运行,你会看到一个完整的监控大屏,所有图表每2秒自动更新数据。
关于性能优化的一些实话
刚才的代码能跑,但如果你在大屏上挂十几个这样的图表,每秒都重新请求数据,浏览器会卡得你怀疑人生。实际项目中我一般会做这几件事:
1. 按需更新,不要全量刷新
// 不好的做法 - 每次都替换整个配置
chart.setOption(fullOption);
// 好的做法 - 只更新变化的数据
chart.setOption({
series: [{
data: newData // 只更新数据,其他配置不动
}]
});
2. 数据节流,控制刷新频率
// 使用防抖,确保高频变化时只取最后一次
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// WebSocket数据进来时,每500ms最多更新一次
const debouncedUpdate = debounce(() => {
chart.setOption({ series: [{ data: latestData }] });
}, 500);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateMockData(data);
debouncedUpdate();
};
3. 图表销毁时机
如果你做的是多页面应用,记得在路由切换时销毁不用的图表实例:
// Vue组件卸载时
onUnmounted(() => {
chart.dispose();
});
// React组件卸载时
useEffect(() => {
return () => {
chart.dispose();
};
}, []);
自动轮播功能实现
监控大屏有时候需要多个图表轮流展示,比如你有10个图表但屏幕只显示4个,这时候轮播就派上用场了。
class ChartCarousel {
constructor(charts, interval = 5000) {
this.charts = charts;
this.currentIndex = 0;
this.interval = interval;
this.timer = null;
this.isPaused = false;
}
// 启动轮播
start() {
this.showChart(this.currentIndex);
this.timer = setInterval(() => this.next(), this.interval);
}
// 下一张
next() {
if (this.isPaused) return;
this.hideChart(this.currentIndex);
this.currentIndex = (this.currentIndex + 1) % this.charts.length;
this.showChart(this.currentIndex);
}
// 显示图表
showChart(index) {
const container = this.charts[index].container;
container.style.opacity = '0';
container.style.transform = 'translateY(20px)';
setTimeout(() => {
container.style.transition = 'all 0.5s ease';
container.style.opacity = '1';
container.style.transform = 'translateY(0)';
this.charts[index].instance.resize();
}, 50);
}
// 隐藏图表
hideChart(index) {
const container = this.charts[index].container;
container.style.opacity = '0';
container.style.transform = 'translateY(-20px)';
}
// 暂停
pause() {
this.isPaused = true;
}
// 继续
resume() {
this.isPaused = false;
}
// 销毁
destroy() {
clearInterval(this.timer);
}
}
// 使用示例
const carouselCharts = [
{ instance: charts.line, container: document.getElementById('chart1') },
{ instance: charts.gauge, container: document.getElementById('chart2') },
{ instance: charts.pie, container: document.getElementById('chart3') },
{ instance: charts.bar, container: document.getElementById('chart4') }
];
const carousel = new ChartCarousel(carouselCharts, 8000);
carousel.start();
// 鼠标悬停时暂停
document.querySelector('.main-container').addEventListener('mouseenter', () => carousel.pause());
document.querySelector('.main-container').addEventListener('mouseleave', () => carousel.resume());
对接真实后端数据的正确姿势
刚才的数据都是模拟的,实际项目中你肯定要对接后端。这里分享几个踩坑经验:
WebSocket vs 轮询,怎么选?
如果数据变化频率很高(每秒多次),用WebSocket;如果变化不频繁(几秒一次),轮询完全够用,而且实现简单得多。
// 轮询方案 - 简单可靠
function pollingUpdate(url, callback, interval = 3000) {
async function fetchData() {
try {
const response = await fetch(url);
const data = await response.json();
callback(data);
} catch (error) {
console.error('数据获取失败:', error);
} finally {
setTimeout(fetchData, interval);
}
}
fetchData();
return () => clearTimeout(fetchData); // 返回清理函数
}
// 使用
const stopPolling = pollingUpdate('/api/metrics', (data) => {
updateCharts(data);
});
// 组件销毁时
stopPolling();
// WebSocket方案 - 实时性更好
class RealtimeChart {
constructor(socketUrl, chartInstance) {
this.chart = chartInstance;
this.socket = null;
this.reconnectTimer = null;
this.url = socketUrl;
this.connect();
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('WebSocket连接成功');
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.chart.setOption({
series: [{ data: data.values }]
});
};
this.socket.onclose = () => {
console.log('连接断开,3秒后重连...');
this.reconnectTimer = setTimeout(() => this.connect(), 3000);
};
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error);
this.socket.close();
};
}
destroy() {
clearTimeout(this.reconnectTimer);
this.socket.close();
}
}
几种常见场景的解决方案
场景一:数据量很大,折线图卡顿
当数据点超过几百个时,折线图渲染会明显变慢。解决办法是前端做降采样:
function downsample(data, maxPoints) {
if (data.length <= maxPoints) return data;
const step = Math.ceil(data.length / maxPoints);
const result = [];
for (let i = 0; i < data.length; i += step) {
const chunk = data.slice(i, i + step);
result.push({
value: chunk.reduce((a, b) => a + b, 0) / chunk.length,
time: chunk[0].time
});
}
return result;
}
场景二:多图表联动
点击一个图表的某个数据点,其他图表同步高亮:
// 主图表
lineChart.on('highlight', (params) => {
// 通知其他图表同步高亮
gaugeChart.dispatchAction({
type: 'highlight',
seriesIndex: 0,
dataIndex: params.dataIndex
});
});
// 其他图表监听
gaugeChart.on('downplay', () => {
lineChart.dispatchAction({ type: 'downplay' });
});
场景三:大数据量热力图
热力图数据点超过几千个也会卡顿,建议做分块加载或者使用Canvas渲染:
// 使用canvas渲染的散点图代替热力图,性能提升明显
series: [{
type: 'scatter',
symbolSize: 10,
itemStyle: {
color: function(params) {
// 根据值返回颜色
return params[2] > 80 ? '#ff6b6b' :
params[2] > 50 ? '#ffbd13' : '#00d4ff';
}
},
data: downscaledHeatmapData
}]
最后几点实战建议
做监控大屏这么久,有几个体会挺深的:
配色不要太花哨。 很多新手喜欢用高饱和度的颜色,结果大屏亮得刺眼,看久了眼睛疼。推荐用深色背景配低饱和度的蓝绿色系,看起来舒服且专业。
别忽视错误处理。 数据接口挂了怎么办?图表应该显示”数据加载中”或者”获取失败”的状态,而不是白一块。
// 添加错误状态
chart.setOption({
series: [{ data: [] }],
graphic: [{
type: 'text',
left: 'center',
top: 'center',
style: {
text: '数据加载失败,请稍后重试',
fill: '#ff6b6b',
fontSize: 16
}
}]
});
适配不同屏幕。 大屏通常不是标准分辨率,建议用rem或者vw/vh做响应式,配合resize事件动态调整。
// 根据屏幕宽度调整字体大小
function setRootFontSize() {
const baseWidth = 1920;
const currentWidth = window.innerWidth;
const ratio = currentWidth / baseWidth;
document.documentElement.style.fontSize = `${16 * ratio}px`;
}
setRootFontSize();
window.addEventListener('resize', setRootFontSize);
把这些要点串起来,你就能做出一个性能良好、视觉效果专业的实时监控大屏了。动态更新的核心就一句话:数据变了,图表跟着变,但不要每次都推倒重来。掌握这个度,你的大屏就不会卡顿。
