跳到主要内容

数据可视化指南

📊 概述

本指南帮助您将 AIECG API 返回的 ECG/PPG 数据可视化,提供常用图表库的集成示例和详细参数说明。

内容导航

  • 📈 ECG 信号绘制: 单导联和12导联波形图
  • 💓 PPG 信号绘制: 多通道PPG波形显示
  • 📊 图表库推荐: 不同场景的最佳选择
  • 性能优化: 大数据量实时绘制技巧

📈 ECG 信号可视化

数据格式说明

从 AIECG API 获取的 ECG 数据格式:

// 单导联数据示例
const singleLeadData = {
ecgData: [512, 515, 520, 518, 525, 530, ...], // 信号数据点数组
ecgSampleRate: 500, // 采样率 500Hz(每秒500个点)
adcGain: 1000.0, // ADC增益系数
adcZero: 0.0 // ADC零点电压
};

// 12导联数据示例
const twelveLeadData = {
ecgSampleRate: 500, // 采样率
lead_I: [512, 515, 520, ...], // I 导联数据
lead_II: [510, 518, 522, ...], // II 导联数据
lead_III: [508, 520, 525, ...], // III 导联数据
lead_aVR: [505, 512, 518, ...], // aVR 导联数据
lead_aVL: [515, 520, 528, ...], // aVL 导联数据
lead_aVF: [518, 525, 532, ...], // aVF 导联数据
lead_V1: [520, 530, 538, ...], // V1 导联数据
lead_V2: [522, 532, 540, ...], // V2 导联数据
lead_V3: [525, 535, 542, ...], // V3 导联数据
lead_V4: [528, 538, 545, ...], // V4 导联数据
lead_V5: [530, 540, 548, ...], // V5 导联数据
lead_V6: [532, 542, 550, ...] // V6 导联数据
};

方案一:使用 Chart.js 绘制

安装

npm install chart.js

单导联 ECG 绘制

import Chart from 'chart.js/auto';

/**
* 绘制单导联 ECG 波形图
* @param {Array} ecgData - ECG 信号数据数组
* @param {number} samplingRate - 采样率(Hz)
* @returns {Chart} Chart.js 实例
*/
function drawECGChart(ecgData, samplingRate) {
const ctx = document.getElementById('ecgChart').getContext('2d');

// 生成时间轴数据(单位:秒)
// 例如:采样率500Hz,15000个点 = 30秒数据
const timeData = ecgData.map((_, index) => (index / samplingRate).toFixed(3));

const chart = new Chart(ctx, {
type: 'line',
data: {
labels: timeData,
datasets: [{
label: 'ECG 信号',
data: ecgData,
borderColor: '#00A86B', // 线条颜色:医疗绿
borderWidth: 1.5, // 线条宽度
pointRadius: 0, // 不显示数据点(提升性能)
tension: 0, // 线条平滑度(0=直线,0.4=平滑曲线)
fill: false // 不填充区域
}]
},
options: {
responsive: true, // 响应式布局
maintainAspectRatio: false, // 不保持宽高比,允许自定义高度
animation: false, // 禁用动画(大数据量时提升性能)

interaction: {
mode: 'nearest', // 鼠标交互模式
axis: 'x', // 只在X轴触发
intersect: false // 不需要精确悬停在点上
},

plugins: {
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true, // 使用圆点样式
padding: 15
}
},
title: {
display: true,
text: 'ECG 波形图',
font: {
size: 16,
weight: 'bold'
}
},
tooltip: {
enabled: true,
backgroundColor: 'rgba(0,0,0,0.8)',
callbacks: {
// 自定义提示框内容
label: function(context) {
return `时间: ${context.label}s, 幅值: ${context.parsed.y}`;
}
}
}
},

scales: {
x: {
type: 'linear', // 线性坐标轴
title: {
display: true,
text: '时间 (秒)',
font: { size: 14 }
},
ticks: {
maxTicksLimit: 20, // 最多显示20个刻度标签
callback: function(value) {
return value.toFixed(1); // 保留1位小数
}
},
grid: {
color: '#e8e8e8', // 网格线颜色
lineWidth: 1
}
},
y: {
title: {
display: true,
text: '幅值',
font: { size: 14 }
},
grid: {
color: '#e8e8e8',
lineWidth: 1
}
}
}
}
});

return chart;
}

// 使用示例
const ecgData = [512, 515, 520, 518, 525, ...]; // 您的 ECG 数据
const samplingRate = 500; // 采样率

const chart = drawECGChart(ecgData, samplingRate);

实现效果说明

Canvas 尺寸设置

<!-- 建议设置固定高度,宽度自适应 -->
<canvas id="ecgChart" style="height: 400px;"></canvas>

显示效果

  • 📊 X轴:显示时间轴,范围从 0 到数据总时长(如 30 秒)
  • 📈 Y轴:显示幅值,自动根据数据范围调整
  • 🎨 波形:绿色连续线条,清晰展示心电波形的 P、QRS、T 波特征
  • 🖱️ 交互:鼠标悬停显示精确的时间和幅值

参数说明

参数说明推荐值效果
borderWidth线条粗细1.5-2过粗影响细节,过细不清晰
pointRadius数据点大小0设为0隐藏点,提升性能
tension曲线平滑度0ECG需要尖锐波形,不平滑
animation动画效果false关闭动画加速渲染
maxTicksLimitX轴标签数量15-20避免标签过密重叠

方案二:使用 ECharts 绘制

安装

npm install echarts

单导联 ECG 绘制(带缩放功能)

import * as echarts from 'echarts';

/**
* 使用 ECharts 绘制 ECG 波形图
* @param {Array} ecgData - ECG 信号数据数组
* @param {number} samplingRate - 采样率(Hz)
* @returns {echarts.ECharts} ECharts 实例
*/
function drawECGWithECharts(ecgData, samplingRate) {
const chartDom = document.getElementById('ecgChart');
const myChart = echarts.init(chartDom);

// 生成时间轴(毫秒)
// 例如:采样率500Hz,每个点间隔 1000/500 = 2ms
const timeData = ecgData.map((_, index) =>
((index / samplingRate) * 1000).toFixed(1)
);

const option = {
title: {
text: 'ECG 波形图',
left: 'center',
textStyle: {
fontSize: 18,
fontWeight: 'bold'
}
},

tooltip: {
trigger: 'axis', // 坐标轴触发
axisPointer: {
type: 'cross', // 十字准星指示器
label: {
backgroundColor: '#6a7985'
}
},
formatter: function(params) {
const time = parseFloat(params[0].axisValue);
const value = params[0].data;
return `时间: ${time.toFixed(1)}ms<br/>幅值: ${value}`;
}
},

grid: {
left: '5%',
right: '5%',
bottom: '15%', // 为 dataZoom 留空间
top: '10%',
containLabel: true,
backgroundColor: '#fafafa'
},

xAxis: {
type: 'category',
data: timeData,
name: '时间 (ms)',
nameLocation: 'middle',
nameGap: 35,
nameTextStyle: {
fontSize: 14,
fontWeight: 'bold'
},
axisLine: {
lineStyle: {
color: '#333'
}
},
axisTick: {
alignWithLabel: true
}
},

yAxis: {
type: 'value',
name: '幅值',
nameLocation: 'middle',
nameGap: 50,
nameTextStyle: {
fontSize: 14,
fontWeight: 'bold'
},
splitLine: {
lineStyle: {
color: '#e8e8e8',
type: 'solid'
}
}
},

series: [{
name: 'ECG',
type: 'line',
data: ecgData,
smooth: false, // 不平滑,保持原始波形
symbol: 'none', // 不显示数据点标记
sampling: 'lttb', // 采样策略:Largest-Triangle-Three-Buckets
lineStyle: {
color: '#00A86B',
width: 1.5
},
animation: false // 关闭动画
}],

// 缩放和滚动功能
dataZoom: [
{
type: 'inside', // 内置缩放(鼠标滚轮)
start: 0, // 初始显示数据的起始位置(百分比)
end: 100, // 初始显示数据的结束位置(百分比)
zoomOnMouseWheel: true, // 滚轮缩放
moveOnMouseMove: false, // 鼠标移动不触发平移
moveOnMouseWheel: true // 按住 Shift + 滚轮平移
},
{
type: 'slider', // 滑动条缩放
start: 0,
end: 100,
height: 30,
bottom: 10,
borderColor: '#ccc',
fillerColor: 'rgba(0, 168, 107, 0.2)',
handleStyle: {
color: '#00A86B'
},
textStyle: {
color: '#333'
}
}
],

// 工具箱
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none' // Y轴不缩放
},
restore: {}, // 重置
saveAsImage: { // 保存为图片
pixelRatio: 2
}
}
}
};

myChart.setOption(option);

// 响应窗口大小变化
window.addEventListener('resize', () => {
myChart.resize();
});

return myChart;
}

// 使用示例
const ecgData = [512, 515, 520, 518, ...]; // 您的 ECG 数据
const samplingRate = 500;

const chart = drawECGWithECharts(ecgData, samplingRate);

实现效果说明

容器尺寸设置

<div id="ecgChart" style="width: 100%; height: 500px;"></div>

显示效果

  • 🔍 缩放功能

    • 鼠标滚轮:上下滚动缩放时间轴
    • Shift + 滚轮:左右平移查看不同时间段
    • 底部滑动条:拖动查看任意时间段
  • 📊 滚动显示效果

    初始状态:显示完整 30 秒数据
    ┌────────────────────────────────────┐
    │ ECG 波形(0-30s) │
    │ P QRS T P QRS T ... │
    └────────────────────────────────────┘

    放大后:底部滑动条显示当前查看位置
    ┌────────────────────────────────────┐
    │ ECG 波形(5-10s 放大) │
    │ P波 QRS波群 T波 细节清晰 │
    └────────────────────────────────────┘
    滑动条:[━━■━━━━━━━━━━━━━━━━━━━━━]
    ↑ 当前查看位置(5-10秒)
  • 🎯 交互特性

    • 十字准星:鼠标移动时显示精确坐标
    • 工具栏:右上角提供重置、截图等功能
    • 数据采样:自动优化显示(LTTB算法)

参数详解

参数说明推荐值效果
dataZoom[0].type缩放类型'inside'内置缩放,无UI,鼠标操作
dataZoom[1].type缩放类型'slider'滑动条,可视化当前位置
sampling数据采样策略'lttb'大数据量时自动采样
grid.bottom底部边距'15%'为滑动条预留空间
symbol数据点标记'none'隐藏标记点

12导联 ECG 同时显示

/**
* 绘制 12 导联 ECG(垂直排列)
* @param {Object} leadData - 包含12个导联数据的对象
* @param {number} samplingRate - 采样率
* @returns {echarts.ECharts} ECharts 实例
*/
function draw12LeadECG(leadData, samplingRate) {
const chartDom = document.getElementById('ecg12LeadChart');
const myChart = echarts.init(chartDom);

const leads = ['I', 'II', 'III', 'aVR', 'aVL', 'aVF', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6'];
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A',
'#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E2',
'#F8B88B', '#FAD7A0', '#AED6F1', '#A9DFBF'
];

// 计算每个导联占用的高度(百分比)
const gridHeight = 100 / 12 - 0.8; // 每个网格高度,留0.8%间隙
const grids = [];
const xAxes = [];
const yAxes = [];
const series = [];

leads.forEach((lead, index) => {
const leadKey = `lead_${lead}`;
const data = leadData[leadKey];
const timeData = data.map((_, i) => (i / samplingRate).toFixed(3));

// 配置网格
grids.push({
left: '8%',
right: '3%',
top: `${index * (gridHeight + 0.8) + 1}%`,
height: `${gridHeight}%`,
backgroundColor: index % 2 === 0 ? '#fafafa' : '#ffffff' // 斑马纹
});

// X轴配置(只显示最后一个)
xAxes.push({
type: 'category',
data: timeData,
gridIndex: index,
show: index === leads.length - 1, // 只在最底部显示X轴
axisLabel: {
show: index === leads.length - 1,
formatter: '{value}s'
},
axisTick: { show: false },
axisLine: { show: index === leads.length - 1 }
});

// Y轴配置
yAxes.push({
type: 'value',
gridIndex: index,
name: lead, // 导联名称
nameLocation: 'middle',
nameGap: 35,
nameTextStyle: {
fontSize: 12,
fontWeight: 'bold',
color: colors[index]
},
splitLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false } // 不显示Y轴刻度值
});

// 数据系列
series.push({
name: lead,
type: 'line',
xAxisIndex: index,
yAxisIndex: index,
data: data,
symbol: 'none',
lineStyle: {
color: colors[index],
width: 1.5
},
animation: false
});
});

const option = {
title: {
text: '12 导联 ECG',
left: 'center',
textStyle: {
fontSize: 18,
fontWeight: 'bold'
}
},
grid: grids,
xAxis: xAxes,
yAxis: yAxes,
series: series,

// 全局缩放控制
dataZoom: [{
type: 'inside',
xAxisIndex: Array.from({length: 12}, (_, i) => i), // 控制所有12个X轴
start: 0,
end: 100,
zoomOnMouseWheel: true
}],

tooltip: {
trigger: 'axis',
axisPointer: {
type: 'line',
lineStyle: {
color: '#999',
width: 1,
type: 'dashed'
}
}
}
};

myChart.setOption(option);
return myChart;
}

12导联实现效果

容器尺寸

<div id="ecg12LeadChart" style="width: 100%; height: 1200px;"></div>

显示效果布局

┌─────────────────────────────────────────┐
│ 12 导联 ECG │
├─────────────────────────────────────────┤
│ I │ ~~~~~~~~~~~~~ │ ← 第1导联(红色)
├─────────────────────────────────────────┤
│ II │ ~~~~~~~~~~~~~ │ ← 第2导联(青色)
├─────────────────────────────────────────┤
│ III │ ~~~~~~~~~~~~~ │ ← 第3导联(蓝色)
├─────────────────────────────────────────┤
│ aVR │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ aVL │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ aVF │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ V1 │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ V2 │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ V3 │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ V4 │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ V5 │ ~~~~~~~~~~~~~ │
├─────────────────────────────────────────┤
│ V6 │ ~~~~~~~~~~~~~ │
└──────────────────────────────────0-10s──┘
↑ 时间轴

交互特性

  • 🔗 同步缩放:鼠标滚轮缩放,所有12个导联同步
  • 🎨 颜色区分:每个导联使用不同颜色,便于识别
  • 📏 对齐显示:所有导联时间轴对齐,便于对比分析

💓 PPG 信号可视化

PPG 数据格式

const ppgData = {
ppgData: {
red: [120.5, 118.3, 122.1, 125.6, ...], // 红光通道(660nm)
green: [125.6, 123.2, 127.8, 130.1, ...], // 绿光通道(525nm,最常用)
infrared: [115.2, 113.8, 117.5, 119.3, ...] // 红外通道(940nm)
},
ppgSampleRate: 100, // PPG采样率通常为 25-125Hz
duration: 30 // 时长(秒)
};

使用 Chart.js 绘制多通道 PPG

/**
* 绘制多通道 PPG 信号
* @param {Object} ppgData - 包含 red/green/infrared 的对象
* @param {number} samplingRate - 采样率
* @returns {Chart} Chart.js 实例
*/
function drawPPGChart(ppgData, samplingRate) {
const ctx = document.getElementById('ppgChart').getContext('2d');

// 生成时间轴
const timeData = ppgData.green.map((_, index) =>
(index / samplingRate).toFixed(2)
);

const chart = new Chart(ctx, {
type: 'line',
data: {
labels: timeData,
datasets: [
{
label: '绿光通道 (Green)',
data: ppgData.green,
borderColor: '#2ECC71', // 绿色
backgroundColor: 'rgba(46, 204, 113, 0.1)',
borderWidth: 2,
pointRadius: 0,
tension: 0.2, // 稍微平滑(PPG波形较圆润)
fill: false,
yAxisID: 'y'
},
{
label: '红光通道 (Red)',
data: ppgData.red,
borderColor: '#E74C3C', // 红色
backgroundColor: 'rgba(231, 76, 60, 0.1)',
borderWidth: 2,
pointRadius: 0,
tension: 0.2,
fill: false,
yAxisID: 'y'
},
{
label: '红外通道 (IR)',
data: ppgData.infrared,
borderColor: '#9B59B6', // 紫色
backgroundColor: 'rgba(155, 89, 182, 0.1)',
borderWidth: 2,
pointRadius: 0,
tension: 0.2,
fill: false,
yAxisID: 'y'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,

interaction: {
mode: 'index', // 显示同一时间点的所有通道数据
intersect: false
},

plugins: {
title: {
display: true,
text: 'PPG 多通道信号',
font: { size: 16, weight: 'bold' }
},
legend: {
display: true,
position: 'top',
labels: {
usePointStyle: true,
padding: 15
}
},
tooltip: {
callbacks: {
title: function(context) {
return `时间: ${context[0].label}s`;
},
label: function(context) {
return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`;
}
}
}
},

scales: {
x: {
title: {
display: true,
text: '时间 (秒)',
font: { size: 14 }
},
grid: {
color: '#e8e8e8'
}
},
y: {
title: {
display: true,
text: '光强度值',
font: { size: 14 }
},
grid: {
color: '#e8e8e8'
}
}
}
}
});

return chart;
}

PPG 显示效果

Canvas 设置

<canvas id="ppgChart" style="height: 450px;"></canvas>

显示效果

  光强度

140 │ 绿光 ~~~~~~
130 │ / \ / \ / \ ← 绿光通道(最明显的脉搏波)
120 │ / \/ \/ \
110 │
100 │ 红光 ~~~~~~ ← 红光通道(用于血氧计算)
90 │ / \ / \ / \
80 │ / \/ \/ \

70 │ 红外 ~~~~~~ ← 红外通道(穿透深,基础信号)
60 │ / \ / \ / \
└────────────────────→ 时间(秒)
0 5 10 15 20 25 30

参数说明

参数说明PPG 特殊设置
tension曲线平滑度0.2(PPG波形较平滑)
interaction.mode交互模式'index'(同时显示三通道)
borderWidth线条粗细2(稍粗,便于区分通道)

⚡ 实时数据流可视化

应用场景

实时数据流可视化适用于以下场景:

  • 🏥 穿戴设备监控:实时显示心率、ECG 波形
  • 🔴 实时采集:硬件设备持续发送数据到客户端
  • 📡 WebSocket 推送:服务器实时推送分析结果
  • 🎯 延迟显示:缓冲 2-3 秒后再展示(平滑过渡)

三种实时显示模式

模式1:滚动窗口(推荐)

效果说明:固定显示最近 N 秒数据,老数据从左侧移出,新数据从右侧进入。

时刻 T=0s:
┌────────────────────────┐
│ ~~~~~~~ │ ← 显示 0-10 秒
└────────────────────────┘
0 5 10

时刻 T=5s(新增5秒数据):
┌────────────────────────┐
│ ~~~~~~~~ │ ← 显示 5-15 秒(左侧数据移出)
└────────────────────────┘
5 10 15

时刻 T=10s:
┌────────────────────────┐
│ ~~~~~~~ │ ← 显示 10-20 秒
└────────────────────────┘
10 15 20

Chart.js 实现

/**
* 实时滚动窗口图表类
*/
class RealtimeScrollChart {
constructor(canvasId, windowSize = 10, samplingRate = 500) {
this.windowSize = windowSize; // 显示窗口(秒)
this.samplingRate = samplingRate; // 采样率(Hz)
this.maxPoints = windowSize * samplingRate; // 最大点数
this.dataBuffer = []; // 数据缓冲区
this.timeBuffer = []; // 时间缓冲区
this.startTime = Date.now();

this.initChart(canvasId);
}

initChart(canvasId) {
const ctx = document.getElementById(canvasId).getContext('2d');

this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'ECG 实时信号',
data: [],
borderColor: '#00A86B',
borderWidth: 1.5,
pointRadius: 0,
tension: 0,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false, // 关键:必须关闭动画

plugins: {
title: {
display: true,
text: '实时 ECG 监控(滚动窗口)',
font: { size: 16, weight: 'bold' }
},
legend: { display: true }
},

scales: {
x: {
title: {
display: true,
text: '时间 (秒)'
},
ticks: {
maxTicksLimit: 10,
callback: function(value, index, values) {
// 显示相对时间
return value.toFixed(1);
}
}
},
y: {
title: {
display: true,
text: '幅值'
}
}
}
}
});
}

/**
* 添加新数据点(单个或批量)
* @param {Array|Number} newData - 新数据(数组或单个值)
*/
addData(newData) {
// 统一处理为数组
const dataArray = Array.isArray(newData) ? newData : [newData];

dataArray.forEach(value => {
// 添加到缓冲区
this.dataBuffer.push(value);

// 计算相对时间(秒)
const elapsedTime = (Date.now() - this.startTime) / 1000;
this.timeBuffer.push(elapsedTime.toFixed(2));

// 超过窗口大小时,移除最老的数据
if (this.dataBuffer.length > this.maxPoints) {
this.dataBuffer.shift();
this.timeBuffer.shift();
}
});

// 更新图表
this.updateChart();
}

/**
* 更新图表显示
*/
updateChart() {
this.chart.data.labels = this.timeBuffer;
this.chart.data.datasets[0].data = this.dataBuffer;
this.chart.update('none'); // 'none' 模式:无动画,立即更新
}

/**
* 清空数据
*/
clear() {
this.dataBuffer = [];
this.timeBuffer = [];
this.startTime = Date.now();
this.updateChart();
}
}

// 使用示例:模拟实时数据流
const realtimeChart = new RealtimeScrollChart('realtimeChart', 10, 500);

// 模拟 WebSocket 接收数据
function simulateWebSocket() {
setInterval(() => {
// 模拟接收 10 个新数据点(20ms 间隔)
const newData = Array.from({ length: 10 }, () =>
500 + Math.sin(Date.now() / 200) * 50 + Math.random() * 20
);

realtimeChart.addData(newData);
}, 20); // 每 20ms 接收一批数据
}

// 启动模拟
simulateWebSocket();

效果特点

  • 流畅滚动:老数据自动移出,新数据持续进入
  • 固定窗口:始终显示最近 10 秒数据
  • 性能优化:只保留窗口内数据,内存占用恒定
  • 适合长期监控:可以持续运行数小时不卡顿

模式2:擦除模式(心电监护仪风格)

效果说明:模拟传统心电监护仪效果,显示一条擦除线,擦除后的区域显示新数据。

时刻 T=0-5s:
┌────────────────────────┐
│ ~~~~| │ ← 绿色波形 | 黑色擦除线
└────────────────────────┘

时刻 T=5-10s(擦除线移动):
┌────────────────────────┐
│ ~~~~~~~~| │ ← 擦除线向右移动
└────────────────────────┘

时刻 T=10s(循环到起点):
┌────────────────────────┐
│|~~~~~~~~~~ │ ← 擦除线回到起点,循环显示
└────────────────────────┘

Chart.js + 插件实现

/**
* 擦除模式实时图表类
*/
class RealtimeEraseChart {
constructor(canvasId, totalDuration = 10, samplingRate = 500) {
this.totalDuration = totalDuration; // 总时长(秒)
this.samplingRate = samplingRate; // 采样率
this.totalPoints = totalDuration * samplingRate;
this.dataBuffer = new Array(this.totalPoints).fill(null); // 初始化为 null
this.currentIndex = 0; // 当前写入位置
this.eraseWidth = samplingRate * 0.5; // 擦除线宽度(0.5秒)

this.initChart(canvasId);
}

initChart(canvasId) {
const ctx = document.getElementById(canvasId).getContext('2d');

// 生成时间轴
const labels = Array.from({ length: this.totalPoints }, (_, i) =>
(i / this.samplingRate).toFixed(2)
);

this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'ECG 信号',
data: this.dataBuffer,
borderColor: '#00A86B',
borderWidth: 2,
pointRadius: 0,
tension: 0,
fill: false,
spanGaps: true // 跨越 null 值
},
{
label: '擦除线',
data: [], // 动态更新
borderColor: 'rgba(255, 0, 0, 0.5)',
borderWidth: 3,
pointRadius: 0,
fill: false,
borderDash: [5, 5] // 虚线
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,

plugins: {
title: {
display: true,
text: '实时 ECG 监控(擦除模式)',
font: { size: 16, weight: 'bold' }
},
legend: { display: true }
},

scales: {
x: {
title: { display: true, text: '时间 (秒)' },
ticks: { maxTicksLimit: 10 }
},
y: {
title: { display: true, text: '幅值' },
min: 400,
max: 600
}
}
}
});
}

/**
* 添加新数据点
*/
addData(newData) {
const dataArray = Array.isArray(newData) ? newData : [newData];

dataArray.forEach(value => {
// 写入当前位置
this.dataBuffer[this.currentIndex] = value;

// 擦除前方数据(制造擦除线效果)
for (let i = 1; i <= this.eraseWidth; i++) {
const eraseIndex = (this.currentIndex + i) % this.totalPoints;
this.dataBuffer[eraseIndex] = null;
}

// 移动到下一个位置
this.currentIndex = (this.currentIndex + 1) % this.totalPoints;
});

// 更新擦除线位置
this.updateEraseLine();

// 更新图表
this.chart.data.datasets[0].data = this.dataBuffer;
this.chart.update('none');
}

/**
* 更新擦除线
*/
updateEraseLine() {
const eraseLineData = new Array(this.totalPoints).fill(null);
const eraseX = this.currentIndex;

// 在擦除线位置画一条竖线
if (eraseX < this.totalPoints) {
eraseLineData[eraseX] = 600; // Y轴最大值
}

this.chart.data.datasets[1].data = eraseLineData;
}

/**
* 清空数据
*/
clear() {
this.dataBuffer.fill(null);
this.currentIndex = 0;
this.chart.data.datasets[0].data = this.dataBuffer;
this.chart.update('none');
}
}

// 使用示例
const eraseChart = new RealtimeEraseChart('eraseChart', 10, 500);

// 模拟数据流
setInterval(() => {
const newData = Array.from({ length: 10 }, () =>
500 + Math.sin(Date.now() / 200) * 50 + Math.random() * 20
);
eraseChart.addData(newData);
}, 20);

效果特点

  • 经典风格:模拟医院心电监护仪
  • 循环显示:到达末尾后自动从头开始
  • 擦除线:红色虚线标识当前位置
  • 固定内存:数据循环覆盖,无需清理

模式3:延迟缓冲模式

效果说明:数据先缓冲 2-3 秒,然后批量更新,避免频繁刷新导致的卡顿。

/**
* 延迟缓冲实时图表类
*/
class RealtimeBufferedChart {
constructor(canvasId, bufferDelay = 2, windowSize = 10, samplingRate = 500) {
this.bufferDelay = bufferDelay * 1000; // 缓冲延迟(毫秒)
this.windowSize = windowSize;
this.samplingRate = samplingRate;
this.maxPoints = windowSize * samplingRate;

this.dataBuffer = []; // 显示缓冲区
this.timeBuffer = [];
this.pendingData = []; // 待处理数据
this.pendingTime = [];

this.startTime = Date.now();
this.lastUpdateTime = Date.now();

this.initChart(canvasId);
this.startUpdateLoop();
}

initChart(canvasId) {
const ctx = document.getElementById(canvasId).getContext('2d');

this.chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'ECG 信号(延迟 2秒)',
data: [],
borderColor: '#00A86B',
borderWidth: 1.5,
pointRadius: 0,
tension: 0,
fill: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,

plugins: {
title: {
display: true,
text: '实时 ECG 监控(延迟缓冲模式)',
font: { size: 16, weight: 'bold' }
},
legend: { display: true }
},

scales: {
x: {
title: { display: true, text: '时间 (秒)' },
ticks: { maxTicksLimit: 10 }
},
y: {
title: { display: true, text: '幅值' }
}
}
}
});
}

/**
* 接收新数据(存入待处理队列)
*/
receiveData(newData) {
const dataArray = Array.isArray(newData) ? newData : [newData];
const currentTime = Date.now();

dataArray.forEach(value => {
this.pendingData.push(value);
this.pendingTime.push(currentTime);
});
}

/**
* 启动定时更新循环
*/
startUpdateLoop() {
setInterval(() => {
this.processBuffer();
}, 100); // 每 100ms 检查一次
}

/**
* 处理缓冲区(将延迟足够的数据移到显示区)
*/
processBuffer() {
const currentTime = Date.now();
const moveIndices = [];

// 找出延迟超过阈值的数据
this.pendingTime.forEach((time, index) => {
if (currentTime - time >= this.bufferDelay) {
moveIndices.push(index);
}
});

if (moveIndices.length === 0) return;

// 移动数据到显示缓冲区
moveIndices.forEach(index => {
const value = this.pendingData[index];
const time = ((this.pendingTime[index] - this.startTime) / 1000).toFixed(2);

this.dataBuffer.push(value);
this.timeBuffer.push(time);

// 超过窗口大小时移除旧数据
if (this.dataBuffer.length > this.maxPoints) {
this.dataBuffer.shift();
this.timeBuffer.shift();
}
});

// 清除已处理的数据
this.pendingData = this.pendingData.filter((_, i) => !moveIndices.includes(i));
this.pendingTime = this.pendingTime.filter((_, i) => !moveIndices.includes(i));

// 更新图表
this.updateChart();
}

/**
* 更新图表
*/
updateChart() {
this.chart.data.labels = this.timeBuffer;
this.chart.data.datasets[0].data = this.dataBuffer;
this.chart.update('none');
}

/**
* 清空所有数据
*/
clear() {
this.dataBuffer = [];
this.timeBuffer = [];
this.pendingData = [];
this.pendingTime = [];
this.startTime = Date.now();
this.updateChart();
}
}

// 使用示例
const bufferedChart = new RealtimeBufferedChart('bufferedChart', 2, 10, 500);

// 模拟实时接收数据
setInterval(() => {
const newData = Array.from({ length: 10 }, () =>
500 + Math.sin(Date.now() / 200) * 50 + Math.random() * 20
);
bufferedChart.receiveData(newData); // 数据先进入缓冲队列
}, 20);

效果特点

  • 平滑显示:避免频繁更新导致的闪烁
  • 延迟可控:可设置 1-5 秒缓冲延迟
  • 批量更新:减少渲染次数,性能更好
  • 适合网络波动:缓冲可以平滑网络延迟

WebSocket 集成示例

/**
* WebSocket + 实时图表完整示例
*/
class WebSocketRealtimeChart {
constructor(canvasId, wsUrl) {
this.chart = new RealtimeScrollChart(canvasId, 10, 500);
this.connectWebSocket(wsUrl);
}

connectWebSocket(wsUrl) {
this.ws = new WebSocket(wsUrl);

this.ws.onopen = () => {
console.log('WebSocket 连接成功');
};

this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);

// 假设服务器发送格式:{ type: 'ecg_data', data: [512, 515, ...] }
if (message.type === 'ecg_data' && Array.isArray(message.data)) {
this.chart.addData(message.data);
}
} catch (error) {
console.error('解析 WebSocket 消息失败:', error);
}
};

this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};

this.ws.onclose = () => {
console.log('WebSocket 连接关闭,3秒后重连...');
setTimeout(() => {
this.connectWebSocket(wsUrl);
}, 3000);
};
}

disconnect() {
if (this.ws) {
this.ws.close();
}
}
}

// 使用示例
const wsChart = new WebSocketRealtimeChart(
'wsChart',
'wss://api.aiecg.com/realtime/ecg'
);

// 页面卸载时断开连接
window.addEventListener('beforeunload', () => {
wsChart.disconnect();
});

性能优化建议

1. 控制更新频率

// ❌ 不好:每接收一个点就更新(太频繁)
ws.onmessage = (event) => {
chart.addData(event.data);
chart.update(); // 可能每秒更新 500 次!
};

// ✅ 好:批量更新
let buffer = [];
ws.onmessage = (event) => {
buffer.push(...event.data);
};

setInterval(() => {
if (buffer.length > 0) {
chart.addData(buffer);
buffer = [];
}
}, 50); // 每 50ms 更新一次(20 FPS)

2. 使用 requestAnimationFrame

class OptimizedRealtimeChart {
constructor(canvasId) {
this.chart = new RealtimeScrollChart(canvasId);
this.pendingData = [];
this.isUpdateScheduled = false;

this.startAnimationLoop();
}

addData(newData) {
this.pendingData.push(...(Array.isArray(newData) ? newData : [newData]));

// 如果还没有安排更新,则安排一次
if (!this.isUpdateScheduled) {
this.isUpdateScheduled = true;
}
}

startAnimationLoop() {
const loop = () => {
if (this.isUpdateScheduled && this.pendingData.length > 0) {
this.chart.addData(this.pendingData);
this.pendingData = [];
this.isUpdateScheduled = false;
}

requestAnimationFrame(loop);
};

requestAnimationFrame(loop);
}
}

3. 降采样显示

// 如果采样率很高(如 1000Hz),可以降采样显示
function downsample(data, factor = 2) {
return data.filter((_, index) => index % factor === 0);
}

// 实际采样率 1000Hz,显示时降为 500Hz
const displayData = downsample(receivedData, 2);
chart.addData(displayData);

三种模式对比

特性滚动窗口擦除模式延迟缓冲
视觉效果流畅滚动经典监护仪平滑显示
内存占用固定固定稍高(双缓冲)
CPU占用
实时性实时实时延迟2-3秒
适用场景通用推荐展示/演示网络不稳定
实现难度⭐⭐ 中⭐⭐⭐ 较难⭐⭐⭐ 较难

推荐选择

  • 🎯 一般应用:选择"滚动窗口模式"(实现简单,效果好)
  • 🏥 医疗展示:选择"擦除模式"(专业感强)
  • 📡 网络传输:选择"延迟缓冲模式"(容错性好)

📊 图表库推荐

根据不同场景选择合适的图表库:

图表库特点适用场景学习难度
Chart.js• 轻量(60KB)
• API简单
• 响应式
• 快速原型
• 中小型数据
• 简单交互
⭐ 易
ECharts• 功能强大
• 中文文档
• 交互丰富
• 复杂图表
• 多图表联动
• 数据分析
⭐⭐ 中
Plotly.js• WebGL 加速
• 科学绘图
• 3D 图表
• 大数据量(>100K点)
• 实时数据流
• 高性能需求
⭐⭐⭐ 难
D3.js• 底层控制
• 高度定制
• SVG 操作
• 特殊需求
• 完全自定义
• 复杂动画
⭐⭐⭐⭐ 很难
uPlot• 极致性能
• 超轻量(45KB)
• 专注时序
• 实时监控
• 嵌入式设备
• 移动端
⭐⭐ 中

推荐方案

  • 🚀 快速开始:Chart.js
  • 🎯 生产环境:ECharts
  • 大数据量:Plotly.js 或 uPlot
  • 🎨 定制需求:D3.js

💡 常见问题

Q1: 图表显示不全或被截断?

确保容器有明确的高度:

<!-- ❌ 错误:没有设置高度 -->
<canvas id="ecgChart"></canvas>

<!-- ✅ 正确:明确高度 -->
<canvas id="ecgChart" style="height: 400px;"></canvas>

Q2: 大数据量时图表卡顿?

使用以下优化方案:

  1. 关闭动画:设置 animation: false
  2. 隐藏数据点:设置 pointRadius: 0
  3. 减少数据点:对原始数据进行合理的降采样
  4. 使用 dataZoom:ECharts 的 dataZoom 功能只渲染可见区域
  5. 切换到高性能库:对于超大数据量(>10万点),考虑使用 Plotly.js(支持WebGL)或 uPlot

Q3: 如何导出图表为图片?

// Chart.js / ECharts 通用方法
function exportChart(chart, filename = 'chart.png') {
const canvas = chart.canvas; // Chart.js
// const canvas = chart.getDom().querySelector('canvas'); // ECharts

const url = canvas.toDataURL('image/png');
const link = document.createElement('a');
link.download = filename;
link.href = url;
link.click();
}

📞 技术支持

如需帮助,请联系: