2019 年做监控产品时,我交付过一张“看起来非常正常”的折线图。接口返回、代码执行和图表渲染都没有报错,产品同学却发现某天设备没有上报,曲线反而平滑地从前一天连到了后一天,像是业务稳定增长。代码没有坏,图表达错了意思。

那次之后我不再把接口数组直接塞给图表库。原始记录要经过字段校验、时间与单位归一、业务聚合、缺失值策略和视觉编码。任何一步默认处理,都可能改变用户读到的结论。

原始数据经过校验、语义聚合和视觉编码后进入图表渲染

图 1:图表配置只是最后一层,前面的数据语义决定它到底在说什么。

先保留“缺失”和“零”的区别

设备没有上报是 missing,设备上报数量为零是 0。如果前端用 value || 0 统一处理,两种事实会被抹平:

type Sample = {
  timestamp: number;
  value: number | null;
  quality: "reported" | "missing" | "estimated";
};
 
function normalize(raw: unknown): Sample {
  const record = raw as Record<string, unknown>;
  const timestamp = Date.parse(String(record.time));
  if (!Number.isFinite(timestamp)) throw new Error(`invalid time: ${record.time}`);
 
  if (record.value === null || record.value === undefined) {
    return { timestamp, value: null, quality: "missing" };
  }
 
  const value = Number(record.value);
  if (!Number.isFinite(value)) throw new Error(`invalid value: ${record.value}`);
  return { timestamp, value, quality: "reported" };
}

缺失点在折线中应该断开、标灰或明确标注,而不是自动补零或连线。是否插值属于业务决定,不能让图表库默认值替团队做。

聚合口径要跟数据一起走

同一个“日活”可能按自然日、过去 24 小时或用户时区计算。只传 {time, value},前端无法解释它。我们后来让查询结果携带口径:

type MetricSeries = {
  metric: "active_devices";
  unit: "count";
  timezone: "Asia/Shanghai";
  interval: "1h" | "1d";
  aggregation: "distinct_device";
  samples: Sample[];
};

Tooltip、导出文件和图表标题都从这份元数据生成,避免页面上写“日活”,接口实际返回小时峰值。

一张图需要明确的质量门禁

渲染前我会检查:时间是否单调;重复时间点如何合并;单位是否一致;异常值是否超出可接受范围;数据点数量是否超过前端预算。

检查 失败处理
时间戳无法解析 拒绝该记录并上报字段样例
同一时间点重复 按明确聚合规则合并,不能只取最后一条
单位不一致 在数据层统一换算,图表层不猜
缺失率超过阈值 图上展示数据质量提示
点数超过预算 服务端降采样或限制时间范围

“尽量画出来”在监控产品里不一定友好。一张静默错误的图,比明确告诉用户数据不完整更危险。

视觉编码也要接受评审

我曾把多条趋势线自动分配成十几种相近颜色,在自己的显示器上还能区分,投影后几乎一样。后来视觉映射也进入配置和测试:关键系列颜色固定;状态颜色有稳定语义;颜色之外再用线型或标记区分;Tooltip 保留原始值和单位。

const chartSpec = {
  x: { field: "timestamp", type: "time" },
  y: { field: "value", unit: series.unit, zeroBaseline: false },
  line: { connectNulls: false },
  quality: { field: "quality", missingStyle: "gap" },
};

zeroBaseline: false 也不能无条件使用。折线图为了观察细小波动可以截断纵轴,柱状图截断基线却会夸大差异。图表类型本身携带表达规则。

给数据管线留可检查的中间结果

排查图表问题时,如果只有最终 option,很难判断错误来自接口、转换还是渲染。我会在开发模式保留每一阶段摘要:原始记录数、过滤数、缺失率、聚合后点数和最终区间。生产日志只记录统计,不上传敏感明细。

这次“没有报错的错误”让我改变了对可视化的理解。图表不是把数值映射成像素的组件,而是一条解释数据的管线。工程师不仅要保证它能画,还要能回答每个点从哪里来、经过什么规则、为什么以这种方式出现。