news 2026/8/15 10:16:10

08-时序数据可视化与异常分析:让数据开口说话

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
08-时序数据可视化与异常分析:让数据开口说话

时序数据可视化与异常分析:让数据开口说话

大家好,我是黒漂技术佬。前面几篇我们把数据采了、存了、查了,但你有没有发现一个问题——这些数据全是数字。运营人员对着满屏的26.3, 26.5, 27.1, 29.8...看半天也看不出啥名堂。但如果你把同样的数据画成一条曲线,温度从26度突然飙到30度的那个拐点,一眼就能看到。这就是可视化的力量。

一、时序数据可视化的价值

人脑对数字的感知能力很弱,对图形的感知能力极强。同样一组温度数据:

26.3, 26.5, 26.7, 27.1, 28.3, 30.1, 32.5, 33.8, 33.2, 31.5, 29.8, 28.2

你能一眼看出哪个时刻温度开始异常吗?很难。但画成折线图,那个陡峭的上升曲线直接跳到你眼前。

可视化在工控物联网场景中至少解决三个问题:

  • 实时监控:运维人员一眼掌握所有设备运行状态,不用挨个查数字
  • 趋势分析:长期数据画成曲线,设备老化趋势、季节性规律一目了然
  • 异常发现:数据可视化后,异常值往往表现为曲线上的尖峰、断崖或平台,比数值阈值告警更直观

二、可视化方案选型

方案一:Grafana 对接 InfluxDB(主流方案)

Grafana 是开源的数据可视化平台,和 InfluxDB 是天作之合。优点是配置即图表,几乎不用写代码;缺点是图表样式定制能力有限,且独立于你的业务系统。

适合场景:运维监控大盘、设备实时状态看板。

方案二:SpringBoot + ECharts 自研报表

ECharts 是百度开源的图表库(现在归Apache了),通过 JavaScript 渲染各种图表。后端用 SpringBoot 查 InfluxDB 返回 JSON,前端用 ECharts 绘图。

优点是完全可控,能和业务系统深度集成;缺点是要自己写前后端代码,开发量大。

适合场景:面向业务方的运营报表、需要和订单/库存数据联动的综合看板。

两种方案的对比

维度GrafanaSpringBoot + ECharts
开发成本低(配置为主)高(需开发前后端)
图表丰富度高(内置几十种面板)中(ECharts也够用)
定制能力中(模板变量、告警规则)高(完全自主控制)
业务集成弱(独立系统)强(嵌入业务页面)
告警能力内置(Alert规则)需自研
典型场景运维监控业务报表

实际项目中两种方案经常并用:Grafana 给运维团队用,ECharts 看板给业务团队用。

三、Grafana 对接 InfluxDB

安装 Grafana

# Docker方式(最简单)dockerrun-d--name=grafana\-p3000:3000\grafana/grafana:latest# 默认账号密码: admin / admin

启动后访问http://localhost:3000,用默认账号登录。

配置 InfluxDB 数据源

  1. 进入 Grafana → Configuration → Data Sources → Add data source
  2. 选择InfluxDB
  3. 填写关键配置:
Query Language: Flux(必须选Flux,不是InfluxQL) URL: http://localhost:8086 Authentication: 勾选 With Credentials(或在下面填Token) Organization: your-org Token: 你的API Token Default Bucket: cabinet_raw
  1. 点击Save & Test,看到 “Data source is working” 就说明通了

创建 Dashboard 和 Panel

Dashboard 是看板,一个 Dashboard 可以包含多个 Panel(图表)。创建一个 Panel 时,核心是写 Flux 查询。

Panel 1:实时温度折线图

在 Panel 的 Query 区域直接写 Flux:

from(bucket: "cabinet_raw") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["device_id"] == "cabinet-001") |> filter(fn: (r) => r["_field"] == "temp") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)

关键点:

  • v.timeRangeStartv.timeRangeStop是 Grafana 内置变量,自动绑定面板右上角的时间范围选择器。用户选"过去1小时",查询就自动查1小时
  • v.windowPeriod也是内置变量,Grafana 根据时间范围自动选择合适的聚合粒度。选"过去1小时"可能用1分钟聚合,选"过去7天"可能用1小时聚合

Panel 配置:

Visualization: Time Series(折线图) Title: cabinet-001 温度变化 X轴: time Y轴: temp (°C)

Panel 2:多设备温度对比

from(bucket: "cabinet_raw") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["_field"] == "temp") |> filter(fn: (r) => r["device_id"] == "cabinet-001" or r["device_id"] == "cabinet-002" or r["device_id"] == "cabinet-003") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)

Grafana 会自动按device_id分组,画出三条不同颜色的曲线。

Panel 3:设备状态表格

from(bucket: "cabinet_raw") |> range(start: -5m) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["_field"] == "temp" or r["_field"] == "voltage" or r["_field"] == "current") |> group(columns: ["device_id", "_field"]) |> last()

Visualization 选Table,展示每个设备的最新温度、电压、电流值。

使用模板变量

Grafana 的模板变量(Variables)让你在看板上加一个下拉框,动态切换查询参数。比如选择不同设备:

  1. Dashboard Settings → Variables → Add variable
  2. 配置:
Name: device Type: Query Query: import "influxdata/influxdb/schema" schema.measurements(bucket: "cabinet_raw") // 或者用tag values查询
  1. 在 Panel 的 Flux 查询中使用变量:
from(bucket: "cabinet_raw") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) |> filter(fn: (r) => r["device_id"] == "${device}") |> filter(fn: (r) => r["_field"] == "temp")

看板顶部会出现一个下拉框,选择不同设备,所有 Panel 自动刷新。

Grafana 告警规则

Grafana 内置告警引擎,可以基于查询结果自动触发告警。

配置示例:温度超过10°C时告警

  1. 进入 Alerting → Alert Rules → New Alert Rule
  2. 配置查询和条件:
Query A: from(bucket: "cabinet_raw") |> range(start: -5m) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["_field"] == "temp") |> group(columns: ["device_id"]) |> last() Condition: WHEN last() OF query(A, 5m, now) IS ABOVE 10
  1. 配置告警通知渠道(Contact Point):
类型: 钉钉 Webhook / 飞书 Webhook / 邮件 消息模板: ${device_id} 温度异常: 当前 ${temp}°C,阈值 10°C
  1. 保存后,Grafana 每5秒检查一次查询结果,超过阈值就发通知

四、SpringBoot + ECharts 自研报表

当 Grafana 满足不了业务定制需求时,就得自己来。

后端接口

@RestController@RequestMapping("/api/chart")publicclassChartController{privatefinalInfluxDBUtilinfluxDBUtil;publicChartController(InfluxDBUtilinfluxDBUtil){this.influxDBUtil=influxDBUtil;}/** * 查询设备温度变化数据(给ECharts折线图用) */@GetMapping("/temp-trend")publicMap<String,Object>tempTrend(@RequestParamStringdeviceId,@RequestParam(defaultValue="1")inthours){Stringflux=String.format("from(bucket: \"cabinet_raw\")\n"+" |> range(start: -%dh)\n"+" |> filter(fn: (r) => r[\"_measurement\"] == \"cabinet_metrics\")\n"+" |> filter(fn: (r) => r[\"device_id\"] == \"%s\")\n"+" |> filter(fn: (r) => r[\"_field\"] == \"temp\")\n"+" |> aggregateWindow(every: 5m, fn: mean, createEmpty: false)\n"+" |> sort(columns: [\"_time\"])",hours,deviceId);List<FluxTable>tables=influxDBUtil.queryRaw(flux);List<String>timeAxis=newArrayList<>();List<Double>tempValues=newArrayList<>();for(FluxTabletable:tables){for(FluxRecordrecord:table.getRecords()){timeAxis.add(formatTime(record.getTime()));tempValues.add((Double)record.getValue());}}Map<String,Object>result=newHashMap<>();result.put("timeAxis",timeAxis);result.put("tempValues",tempValues);result.put("deviceId",deviceId);returnresult;}privateStringformatTime(Instantinstant){returnDateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault()).format(instant);}}

前端 ECharts 渲染

<divid="tempChart"style="width:100%;height:400px;"></div><scriptsrc="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script><script>constchart=echarts.init(document.getElementById('tempChart'));fetch('/api/chart/temp-trend?deviceId=cabinet-001&hours=1').then(res=>res.json()).then(data=>{chart.setOption({title:{text:data.deviceId+' 温度变化'},tooltip:{trigger:'axis'},xAxis:{type:'category',data:data.timeAxis},yAxis:{type:'value',name:'温度(°C)'},series:[{name:'温度',type:'line',data:data.tempValues,smooth:true,markLine:{data:[{yAxis:10,name:'高温阈值',lineStyle:{color:'#ff0000'}}]}}]});});</script>

markLine画了一条红色的高温阈值线,数据曲线超过这条线就能直观看到异常。

五、报表统计场景

日报/周报/月报自动生成

用 InfluxDB Task 定时聚合数据,生成报表写入 MySQL,前端直接查 MySQL 展示:

// Task: 每天凌晨1点生成昨天的日报 option task = { name: "daily_report", every: 1d, offset: 1h } // 日均温度 avgTemp = from(bucket: "cabinet_raw") |> range(start: -1d) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["_field"] == "temp") |> group(columns: ["device_id"]) |> mean() |> set(key: "_field", value: "daily_avg_temp") // 日最高温度 maxTemp = from(bucket: "cabinet_raw") |> range(start: -1d) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["_field"] == "temp") |> group(columns: ["device_id"]) |> max() |> set(key: "_field", value: "daily_max_temp") // 合并写入日报Bucket union(tables: [avgTemp, maxTemp]) |> to(bucket: "cabinet_daily", org: "your-org")

SpringBoot 再起一个定时任务,每天凌晨2点从cabinet_daily读取数据写入 MySQL 报表表,供前端查询展示。

设备运行率统计

设备运行率 = 运行时长 / 总时长 × 100%。通过统计设备状态 measurement 中compressor_on = true的时长占比来计算:

// 统计某设备过去24小时的压缩机运行率 running = from(bucket: "cabinet_raw") |> range(start: -24h) |> filter(fn: (r) => r["_measurement"] == "cabinet_status") |> filter(fn: (r) => r["device_id"] == "cabinet-001") |> filter(fn: (r) => r["_field"] == "compressor_on") |> filter(fn: (r) => r["_value"] == 1) |> count() total = from(bucket: "cabinet_raw") |> range(start: -24h) |> filter(fn: (r) => r["_measurement"] == "cabinet_status") |> filter(fn: (r) => r["device_id"] == "cabinet-001") |> filter(fn: (r) => r["_field"] == "compressor_on") |> count() // 运行率 = running / total × 100% // 在Java端计算百分比

异常告警次数排行

统计各设备过去7天的告警次数,排出Top 10:

from(bucket: "cabinet_raw") |> range(start: -7d) |> filter(fn: (r) => r["_measurement"] == "cabinet_events") |> filter(fn: (r) => r["event_type"] == "ALARM") |> group(columns: ["device_id"]) |> count() |> group() |> sort(columns: ["_value"], desc: true) |> limit(n: 10)

结果直接喂给 ECharts 画柱状图,哪个设备老出问题一目了然。

六、异常数据分析

阈值告警

最简单直接的异常检测:设一个上下限,超出就告警。

// 阈值规则配置publicclassAlarmRule{privateStringfield;// 监控字段: temp, voltage, currentprivatedoubleminValue;// 下限privatedoublemaxValue;// 上限privateintdurationSec;// 持续时间(秒),避免瞬时波动误报}// 检查逻辑publicbooleancheckThreshold(Stringfield,doublevalue,AlarmRulerule){returnvalue<rule.getMinValue()||value>rule.getMaxValue();}

关键细节是持续时间判断:温度瞬间跳到11°C然后马上回落,不算异常(可能是传感器噪声)。只有持续超过阈值5分钟才告警。用 Flux 实现:

// 温度持续5分钟超过10°C才告警 from(bucket: "cabinet_raw") |> range(start: -10m) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics") |> filter(fn: (r) => r["_field"] == "temp") |> filter(fn: (r) => r["_value"] > 10.0) |> group(columns: ["device_id"]) |> count() // 如果5分钟内有超过60条(5分钟÷5秒=60条),说明持续超温

突变检测

有些异常不是超阈值,而是"突然跳变"。比如温度一直稳定在5°C,突然跳到8°C——虽然没超10°C阈值,但这种跳变本身可能意味着制冷系统出了问题。

/** * 突变检测:当前值与过去N分钟平均值的偏差超过阈值 */publicbooleancheckSuddenChange(StringdeviceId,doublecurrentValue,Stringfield){// 查过去10分钟的平均值Stringflux=String.format("from(bucket: \"cabinet_raw\")\n"+" |> range(start: -10m)\n"+" |> filter(fn: (r) => r[\"_measurement\"] == \"cabinet_metrics\")\n"+" |> filter(fn: (r) => r[\"device_id\"] == \"%s\")\n"+" |> filter(fn: (r) => r[\"_field\"] == \"%s\")\n"+" |> mean()",deviceId,field);doubleavgValue=queryAvgValue(flux);doubledeviation=Math.abs(currentValue-avgValue);// 温度偏差超过3°C视为突变returndeviation>3.0;}

同比环比

把当前数据和历史同期对比,发现"不正常但没超阈值"的异常。比如某台售货柜本周平均温度比上周高了2°C,虽然都还在正常范围内,但这个上升趋势可能意味着制冷系统在缓慢老化。

// 本周平均温度 thisWeek = from(bucket: "cabinet_10m") |> range(start: -7d) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics_10m") |> filter(fn: (r) => r["device_id"] == "cabinet-001") |> filter(fn: (r) => r["_field"] == "temp") |> mean() |> findRecord(fn: (key) => true, idx: 0) // 上周平均温度 lastWeek = from(bucket: "cabinet_10m") |> range(start: -14d, stop: -7d) |> filter(fn: (r) => r["_measurement"] == "cabinet_metrics_10m") |> filter(fn: (r) => r["device_id"] == "cabinet-001") |> filter(fn: (r) => r["_field"] == "temp") |> mean() |> findRecord(fn: (key) => true, idx: 0) // 输出环比变化 difference = thisWeek._value - lastWeek._value

七、无人售货柜运营数据可视化看板实战

把以上内容整合成一个完整的运营看板:

看板布局

┌─────────────────────────────────────────────────────────┐ │ 无人售货柜运营看板 │ ├──────────────┬──────────────┬──────────────┬───────────┤ │ 在线设备数 │ 今日告警次数 │ 平均温度 │ 异常设备数 │ │ 985/1000 │ 23 │ 5.2°C │ 15 │ ├──────────────┴──────────────┴──────────────┴───────────┤ │ │ │ 全设备实时温度分布图(热力图) │ │ │ ├─────────────────────────┬───────────────────────────────┤ │ │ │ │ Top10 告警设备排行 │ 设备运行率统计(柱状图) │ │ (柱状图) │ │ │ │ │ ├─────────────────────────┴───────────────────────────────┤ │ │ │ 选中设备温度/电压/电流趋势图(多Y轴折线图) │ │ │ ├─────────────────────────────────────────────────────────┤ │ 设备明细表格(设备ID | 温度 | 电压 | 状态 | 最后上报时间) │ └─────────────────────────────────────────────────────────┘

数据来源拆分

看板模块数据源查询方式
在线设备数InfluxDB cabinet_raw统计5分钟内有数据的设备数
今日告警次数InfluxDB cabinet_eventscount + filter(event_type=ALARM)
平均温度InfluxDB cabinet_raw所有设备temp的mean()
异常设备数应用层计算温度超阈值的设备去重计数
温度热力图InfluxDB cabinet_rawgroup(device_id) + last()
告警排行InfluxDB cabinet_eventsgroup(device_id) + count + sort + limit
运行率InfluxDB cabinet_status压缩机ON时长占比
趋势图InfluxDB cabinet_rawaggregateWindow(mean)
设备明细表格InfluxDB cabinet_rawgroup(device_id) + last()

这套看板用 Grafana 搭,一个下午就能搞定。如果用 ECharts 自研,大概需要2~3天,但可以和你的业务后台深度整合。


数据可视化不是"锦上添花",而是数据落地的最后一公里。采得到、存得好、查得快,最后还得"看得见"——运营人员看到的不是数字,是设备健康度、是业务趋势、是需要立即处理的异常。这条链路通了,你的时序数据平台才真正有价值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/15 10:12:01

零成本构建可信网站:免费域名与SSL证书实战指南

1. 项目概述&#xff1a;零成本构建可信网站的技术基石 在互联网上拥有一个属于自己的网站&#xff0c;无论是用于个人博客、项目展示还是小型业务&#xff0c;第一步往往就是获取一个域名和一张SSL证书。域名是你的门牌号&#xff0c;而SSL证书则是门上的那把安全锁&#xff0…

作者头像 李华
网站建设 2026/8/15 10:09:07

状态模式与策略模式深度辨析:从误用到重构的实战指南

1. 项目概述&#xff1a;当策略模式“误入歧途”在软件设计的江湖里&#xff0c;状态模式和策略模式这对“孪生兄弟”常常让开发者感到困惑。它们都基于组合和接口&#xff0c;都旨在将行为封装成独立的类&#xff0c;乍一看&#xff0c;UML图长得都差不多。这就导致了一个常见…

作者头像 李华
网站建设 2026/8/15 10:08:55

数学建模竞赛全解析:从技能提升到实战策略,助你高效备赛

1. 从“值不值”到“怎么值”&#xff1a;一个建模老兵的视角 “数学建模值不值得参加&#xff1f;”这个问题&#xff0c;几乎每年都会在各大高校的论坛、新生群里被反复提起。作为一个从本科到研究生&#xff0c;从参赛者到指导者&#xff0c;完整经历过这个周期的人&#xf…

作者头像 李华
网站建设 2026/8/15 10:08:29

【AgentScope 2.0】07-沙箱(Sandbox)详解

版本基准:本文档基于 AgentScope 2.0 GA(v2.0.0)编写。具体版本号以 Release Notes 为准。 一句话概括 沙箱就是把 AI Agent 关在一个隔离的安全笼子里干活——它可以在笼子里随便折腾&#xff08;装软件、跑命令、写文件&#xff09;&#xff0c;但不会影响到你的主机系统&am…

作者头像 李华