Canvas 将 Min-Max 心率采样绘成连续包络波形

实时心率与同步采样 Min-Max 包络波形

心率记录需要同时返回 BPM 和一秒采样波形。设备端每秒处理 500 个 ADC 样本,将它们分成 10 个桶,每个桶输出一个 8-bit 最小值和最大值。这样一条记录会携带 20 个波形字节,主机端再把它显示在实时心率曲线下方。

这类数据不能直接按 20 个普通采样点连线。20 个字节实际代表 10 组范围,每组范围覆盖一段原始样本。最终图表采用“一个桶一个 x 坐标”的包络绘制,再叠加一条中心线,既保留极值,也避免把 Min-Max 数据误读成带有明确先后顺序的波形点。

Min-Max 桶语义

协议中的波形部分由 10 组连续的 [min,max] 组成。每组覆盖 50 个新样本,桶边界保留一个重叠样本,用于保持边沿信息。minmax 只描述这段样本的范围,协议没有携带极值出现的原始索引。

主机端为每个桶建立一条记录:

字段 含义
timestamp 桶中心对应的时间
min 该桶的 8-bit 最小值
max 该桶的 8-bit 最大值
value (min + max) / 2,用于中心线

如果把每组 [min,max] 拆成两个有序时间点,页面必须猜测极值先后。上升段、下降段和重复量化值都会让这条猜测产生凹刺或不自然的平台。包络绘制直接使用协议提供的范围信息,避免引入这层猜测。

桶中心坐标与 15 秒窗口

记录时间戳表示这一秒波形的结束位置。第 pairIndex 个桶的中心时间计算为:

const bucketTimestamp = recordTimestamp - 1 +
    (pairIndex + 0.5) / 10;

10 个桶均匀落在 [recordTimestamp - 1, recordTimestamp] 内。每个桶只有一个 x 坐标,minmax 共同描述这个位置的上下边界。

调试器只保留最近 15 秒,也就是 150 个桶。单条读取、批量读取和连续读取都进入同一个 appendHeartRateSignalRecord() 数据入口。记录用 id:timestamp 去重,Canvas 重绘通过一个待处理的 requestAnimationFrame 合并,避免一次批量响应触发多次完整绘制。

包络路径与中心线

Canvas 先把所有桶的 minmax 映射到动态 8-bit 纵轴。包络路径分两段建立:第一段从左到右连接每个桶的 max,第二段从右到左连接每个桶的 min,最后闭合路径并填充淡色区域。

ctx.beginPath();
for (let index = start; index < end; index++) {
  const bucket = buckets[index];
  const x = xForTimestamp(bucket.timestamp);
  const maxY = yForValue(bucket.max, min, max, area);
  if (index === start) {
    ctx.moveTo(x, maxY);
  } else {
    ctx.lineTo(x, maxY);
  }
}

for (let index = end - 1; index >= start; index--) {
  const bucket = buckets[index];
  const x = xForTimestamp(bucket.timestamp);
  const minY = yForValue(bucket.min, min, max, area);
  ctx.lineTo(x, minY);
}

ctx.closePath();
ctx.fill();

包络上下边界使用较浅的青绿色描边,中心线使用桶的中点:

const value = (bucket.min + bucket.max) / 2;
const y = yForValue(value, min, max, area);

包络路径覆盖相邻桶之间的整个区间,所以画面不会出现独立柱子之间的空白。中心线提供清晰的趋势轮廓,淡色区域继续保留每个桶的极值范围。截图中的上下边界、中心线和右侧 0 到 255 刻度共同表达了同一组采样数据。

8-bit 量化的视觉表现

设备端把 12-bit ADC 极值量化为 8-bit:有效 ADC 值执行 sample >> 4,超出 12-bit 范围的值饱和到 255。量化会让相邻桶出现相同的最小值或最大值,包络边界可能形成短平台,中心线也会保留阶梯状变化。

这种平台属于量化后的数据特征。它与多个采样点被错误放到同一 x 坐标的情况不同。包络图把一个桶的范围画在一个时间位置,中心线只使用一个中点值,因此图形结构和数据结构保持一致。

上位机数据流与显示分工

实时心率区域和同步采样区域使用两套历史数据:

这样的分工让 BPM 趋势和采样形状各自使用合适的纵轴。BPM 图固定业务范围,采样图使用 8-bit 动态范围,两个图不会因为量纲不同互相压缩。

数据边界与验证结果

Min-Max 包络能保证每个桶的极值进入画面,却不能恢复极值在 50 个原始样本中的准确先后。如果产品需要一条严格对应原始时间顺序的折线,协议还需要传输极值索引或保留有序采样点。当前协议只提供范围信息,包络加中心线是与协议语义一致的显示方式。

这次实现完成了三层校验:协议记录长度和 20 字节波形解析通过静态测试;调试器内联 JavaScript 通过语法检查;实机页面确认实时心率和同步波形上下排列,波形窗口稳定显示最近 15 秒。最终图形保持了极值信息,也让时间轴、包络范围和趋势线各自承担清晰的显示职责。