ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

HarmonyOS智慧农业应用数据分析与可视化模块开发实践

HarmonyOS智慧农业应用数据分析与可视化模块开发实践 HarmonyOS 智慧农业管理应用开发教程这个系列写到第14篇这期内容聚焦数据分析与可视化模块。种地这件事做到一半你会发现环境传感器每天产生成百上千条温度、湿度、光照数据如果不做分析它们就是一串躺在数据库里没人看的数字。本篇把整个数据分析模块从需求拆解、方案选型、图表组件实现到数据存储与页面落地的完整过程梳理清楚适合做到应用中期、准备上数据展示能力的HarmonyOS开发者参考也适合想了解智能农业数据链路怎么设计的同学读一读。1. 先拆需求这块功能到底要解决什么问题1.1 智慧农业数据从哪来、流到哪去智慧农业应用里数据来源从物理链路看就三类土壤传感器温湿度、pH、EC、环境传感器空气温湿度、光照、CO2浓度和设备运行状态浇灌阀门、风机、补光灯的开关记录。在HarmonyOS应用这一侧我们通常不直接跟传感器打交道而是通过云端接口或者本地的IoT网关拿到数据。但做数据分析模块的第一件事不是打开IDE写代码而是把数据的流向画清楚传感器定时上报网关做协议转换应用端通过HTTPS/WebSocket接收然后落到本地数据库最后经过聚合计算渲染到图表上。我在“高高种地”这个项目里前期图方便把数据源设成了本地模拟器每隔30秒生成一条环境记录真实接口只做了封装预留。这样做的最大好处是图表开发阶段不用依赖网络环境数据格式对口之后切换真实数据源只需要改一个数据仓库的实现类。这个阶段容易犯的错误是把数据库设计成“能存就行”直接存原始上报记录不做分表、不做清洗。等图表要做日粒度聚合时全表扫一遍几万条数据卡顿立刻显现。所以数据流设计的第一步是明确后端给什么、应用存什么、图表用什么。1.2 明确分析维度环境趋势、生长记录、投入产出可视化不是把数据随便画成图先要回答“用户到底想知道什么”。农业场景里高频的需求我总结为三类环境趋势过去一周的温度、湿度变化曲线看是否有异常波动是否触发过阈值。种植户真正关心的是“昨天半夜温度是不是掉到警戒线以下”而不是某个瞬时值。生长记录每批作物从播种、发芽、开花到结果的时间节点对比不同批次的生长周期差异。这块数据偏事件型适合用时间轴或者带节点的折线图展示。投入产出种子、肥料、水电的投入金额对比以及对应批次的产量和预估收益。这是报表型需求环形图和柱状图最直观。前期跟用户聊需求时他们其实提不出“我要折线图、柱状图”这种明确要求只会说“我想看看这段时间大棚里环境稳不稳定”。做技术的我们就要把模糊需求翻译成具体的图表和维度。这也是数据分析模块和其它功能模块的区别它的价值不在于界面多酷炫而在于能不能让用户在5秒内看懂过去30天的变化。第14篇这个节点整个应用已经有账号体系、传感器管理、设备控制这些基础能力数据分析模块天然承担着“把前面攒下来的数据变成决策依据”的职责。2. 方案选型图表库、存储方式和统计逻辑怎么定2.1 图表实现方案对比第三方库、Canvas自绘还是原生组件HarmonyOS NEXTAPI 12对应5.0.0(12)生态里图表方案大体有三条路第一条是直接用第三方图表库比如社区移植的MPChart、或者基于Web的图表组件。优点是省事各种图表类型齐全自带动画和触摸事件。缺点也很明显包体积会增加好几兆遇到API版本升级时可能出现底层方法变更导致编译不过的坑而且定制样式时你得去翻别人的源码。第二条是Web组件加载ECharts页面。如果你熟悉前端这条路上手最快生态也最丰富。但跨语言桥接的通信开销、页面加载白屏、数据更新时的postMessage调用在低端设备上体验并不好。做单页数据面板可以接受做高频刷新的实时曲线就会有明显卡顿。第三条就是我最终选择的方案Canvas自绘。HarmonyOS的Canvas组件和CanvasRenderingContext2D接口已经非常完整画折线图、柱状图、环形图根本不需要多少代码而且全部逻辑可控、性能可控、包体积零增加。很多人一听自绘就发怵其实图表绘制的本质就是坐标换算加图形绘制一旦你画过第一个折线图后面都是同样的套路重复用。我当时的取舍标准很简单项目只需要三类图表数据量在千条级别且后续要跟应用整体视觉风格统一。这种情况下自绘的性价比是最高的。方案开发量包体积定制能力性能兼容风险第三方图表库小偏大中中有Web ECharts中中强中低有Canvas自绘大零最强高低2.2 存储与数据流的设计首选项、关系型数据库还是只存内存图表要呈现历史趋势数据就必须落盘。HarmonyOS NEXT提供了几种本地存储方案这里要分清它们的适用边界Preferences首选项适合存轻量配置比如用户上次选择的统计周期、图表主题、阈值设置以key-value形式读写性能极好但不适合存结构化列表。关系型数据库RelationalStore适合存传感器明细记录。我给env_record表建了 device_id、temp、humidity、timestamp 这些字段建了timestamp索引按天聚合查询时快很多。分布式数据服务KVStore适合多设备同步场景比如手机和平板都要看同一份数据。但本项目前期用不到接口要留但实现可以先不做。数据流上我分了三层采集层生成原始记录、存储层做持久化、计算层做聚合。采集层是定时任务每次收到一条数据就insert计算层只做一件事从库里按时间段取数然后内存中分组聚合。这里有个经验聚合计算尽量不要每次实时去数据库做GROUP BY尤其是跨月查询时SQL聚合和内存聚合的性能差异能达到一个数量级。我的做法是提前把原始记录查出来在ArkTS里用Map按天分组再算均值这样逻辑可调试、可单测、可复用。2.3 统计口径和计算逻辑要先定下来图表画得对不对取决于统计口径定不定。这块最容易跟业务产生分歧比如“日均温度”到底是一天24小时所有采集点的算术平均还是只统计白天6点到18点的均温种植户可能更关心白天温度因为夜间低温是另一套预警逻辑。我建议在开发前就把口径文档化列清楚日均温度 当天所有有效采集点的算术平均有效指非空且在物理范围内比如-40℃到60℃之间。积温生长度日 日平均温度减去生长下限温度低于下限记0逐日累加。它对判断作物是否达到开花期很有参考价值。投入产出比 某批次总投入 / 预估产量单位是元/斤。这个指标能直接指导定价和下一季的种植计划。口径一旦定了所有图表都按同一套函数计算避免折线图用了一套逻辑、详情页又用了另一套。我踩过这个坑同一个“均温”列表页和图表页差了2度查了半天发现一个过滤了异常值另一个没过滤。3. 核心实现三类图表组件从0到13.1 折线图画环境趋势坐标换算、网格与曲线绘制先看折线图这是数据分析模块最核心的图表。绘制步骤拆开就四步定义绘图区域、计算数据范围、换算坐标点、连线描边。我封装了一个LineChart组件核心思路是把Canvas的宽度高度通过onReady回调拿到后按内边距计算出实际绘图区。然后遍历数据找最大最小值再把每个数据点映射到画布坐标private drawLineChart(records: EnvRecord[]) { const w this.canvasWidth; const h this.canvasHeight; const padding { l: 40, r: 16, t: 20, b: 30 }; const innerW w - padding.l - padding.r; const innerH h - padding.t - padding.b; let minTemp 999; let maxTemp -999; records.forEach(r { minTemp Math.min(minTemp, r.temp); maxTemp Math.max(maxTemp, r.temp); }); const range Math.max(10, maxTemp - minTemp); const stepX records.length 1 ? innerW / (records.length - 1) : 0; this.ctx.clearRect(0, 0, w, h); // 画横向网格线 for (let i 0; i 4; i) { const y padding.t innerH * i / 4; this.ctx.strokeStyle #e8e8e8; this.ctx.lineWidth 1; this.ctx.beginPath(); this.ctx.moveTo(padding.l, y); this.ctx.lineTo(w - padding.r, y); this.ctx.stroke(); } // 画温度折线 this.ctx.strokeStyle #3c78d8; this.ctx.lineWidth 2; this.ctx.beginPath(); records.forEach((r, idx) { const x padding.l stepX * idx; const y padding.t innerH * (1 - (r.temp - minTemp) / range); if (idx 0) { this.ctx.moveTo(x, y); } else { this.ctx.lineTo(x, y); } }); this.ctx.stroke(); }写坐标换算时有个很关键的公式y padding.t innerH * (1 - (value - min) / range)。因为Canvas坐标系是y轴朝下的所以数据越大y值越小需要用1 - 比例翻转。这个细节很多新手会忘画出来的图完全是倒的。另外一个实操经验是stepX在数据只有一条时要特殊处理否则会出现除零得到无穷大坐标Canvas画出来是一条斜穿整个画布的线。我们的环境采集通常至少几十条数据但列表页筛选条件可能只筛出一天、一条数据异常处理必须前置。3.2 柱状图画产量对比分组柱、圆角矩形与数值标签柱状图用来展示各批次的产量对比、或者各区域的环境指标均值。绘制逻辑主要是计算每一根柱子的x位置和高度然后逐根绘制。柱状图关键参数有三个柱子宽度、柱子间距、绘图区底部基准线。宽度和间距要按柱子数量动态计算否则数量多时柱子会重叠private drawBarChart(items: BarItem[], canvasW: number, canvasH: number) { const padding { l: 40, r: 16, t: 24, b: 40 }; const innerW canvasW - padding.l - padding.r; const innerH canvasH - padding.t - padding.b; const maxValue Math.max(...items.map(i i.value)); // 动态计算柱子宽度 const barWidth Math.min(32, innerW / items.length * 0.6); const gap (innerW - barWidth * items.length) / (items.length 1); items.forEach((item, idx) { const x padding.l gap * (idx 1) barWidth * idx; const barH innerH * (item.value / maxValue); const y padding.t innerH - barH; this.ctx.fillStyle item.color; // 画圆角矩形视觉更柔和 this.roundRect(x, y, barWidth, barH, 4); this.ctx.fill(); }); }这里我加了圆角矩形看起来精致很多利润虽然不大但整体观感提升明显。圆角矩形的实现用ctx.roundRect在API 12及以上是原生支持的如果版本低就要手动arcTo拼。画完柱子之后数值标签是重点。我建议把数值显示在柱顶上方字号用12fpHarmonyOS里推荐fp作为字体单位跟sp类似对齐方式用textAligncenter这样柱状图不用凑近看也能读出大概量级。3.3 环形图画投入产出占比扇区计算与中心文案环形图是饼图的变体中间挖空更适合展示比例数据。画法上比折线图还简单但有几个细节要注意起始角度从-90度开始12点钟方向每一段扇区占的弧度按比例分配挖空部分用内圆半径加一次反向绘制实现。private drawDonutChart(segments: SegmentItem[], canvasW: number, canvasH: number) { const centerX canvasW / 2; const centerY canvasH / 2; const outerR Math.min(canvasW, canvasH) / 2 - 24; const innerR outerR * 0.62; let startAngle -Math.PI / 2; const total segments.reduce((sum, seg) sum seg.value, 0); segments.forEach(seg { const angle seg.value / total * Math.PI * 2; this.ctx.beginPath(); this.ctx.moveTo(centerX, centerY); this.ctx.arc(centerX, centerY, outerR, startAngle, startAngle angle); this.ctx.closePath(); this.ctx.fillStyle seg.color; this.ctx.fill(); // 绘制内圆形成圆环 this.ctx.beginPath(); this.ctx.arc(centerX, centerY, innerR, 0, Math.PI * 2); this.ctx.fillStyle #ffffff; this.ctx.fill(); startAngle angle; }); }实现时注意drawDonut里的内圆填充要在每段扇区之后立即执行如果所有扇区画完后再统一挖孔会因为Canvas默认的叠加模式覆盖掉所有扇区。这段逻辑我当时写了三版才弄对第一次挖完孔只剩最下面一段扇区第二次忘记恢复globalCompositeOperation导致后面的折线图全被挖掉了。环形图中心通常会放一个总金额或总面积这个可以单独用ctx.fillText绘制在centerX, centerY位置再调整textBaseline到middle。中心文案的字体需要单独指定font: 14fp Arial确保在平板上不糊。3.4 点击图表区块联动详情是加分项图表如果只能看不能点交互感会差很多。HarmonyOS的Canvas组件支持绑定触摸事件思路是拿到触摸点的坐标反推命中的区块然后跳转详情页或弹出数据卡片。以柱状图为例命中判断很简单预先把每根柱子的矩形区域存到数组里触摸时遍历数组判断坐标是否落在矩形范围内。环形图复杂一点需要换算触摸点到圆心的距离和角度判断是否落在扇区范围内。但核心都是纯数学计算不需要复杂的图形学知识。命中后我做了两件事一是高亮当前柱子重绘时用半透明遮罩覆盖其它柱子二是把该批次的投入产出明细通过router.pushUrl带到详情页。数据传递用router的参数是比较轻量的方式只传一个批次ID详情页再按ID查库。注意Canvas的touch事件回调里拿到的坐标是相对组件的不需要再做坐标转换。但如果你在Scroll或Grid里套Canvas手势会被父容器拦截这时父子同时消费触摸事件要处理好onTouch的返回值。4. 数据层本地存储、定时采集与多端扩展4.1 首选项存配置、关系型数据库存明细数据存储不能眉毛胡子一把抓。我在项目里把key-value型的用户偏好统计周期、图表配色主题、阈值告警设置放Preferences把传感器明细记录放关系型数据库。前者读写毫秒级返回适合频繁读写但不依赖事务的场景后者支持SQL查询、排序、分页适合数据量大且需要聚合分析的场景。关系型数据库建表语句逻辑类似SQLite但API调用风格不同。需要先获取RdbStore实例然后执行executeSql建表CREATE TABLE IF NOT EXISTS env_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, temp REAL NOT NULL, humidity REAL NOT NULL, soil_temp REAL, soil_moisture REAL, light INTEGER, timestamp INTEGER NOT NULL ) CREATE INDEX IF NOT EXISTS idx_env_ts ON env_record(timestamp)我把timestamp建了索引因为所有图表查询都是按时间段过滤的没有索引的SQLite会在几万条数据后明显变慢。排序和分页用Query对象链式调用let predicates new relationalStore.RdbPredicates(env_record); predicates.greaterThan(timestamp, startTime) .lessThan(timestamp, endTime) .orderByDesc(timestamp); let resultSet await this.rdbStore.query(predicates);查询结果用while (resultSet.goToNextRecord()) 遍历取字段时要根据列类型调用resultSet.getDouble/getString这一点跟其它语言ORM不同容易类型报错。4.2 定时采集任务的生命周期管理采集任务用的是setInterval但页面生命周期管理必须做好否则应用切后台还继续采集既耗电又费流量。我的做法是在页面aboutToAppear时启动定时器aboutToDisappear时清理aboutToAppear() { // 页面显示时启动采集任务 this.timerId setInterval(() { this.collectEnvData(); }, 30000); } aboutToDisappear() { // 页面不可见时清理避免后台持续采集 if (this.timerId) { clearInterval(this.timerId); this.timerId undefined; } }这个设计还有个进阶版本把采集任务抽到独立的公共服务比如CollectService由应用级EntryAbility来管理生命周期而不是绑定在某个页面。这样即使用户从数据分析页跳到了控制页采集仍在后台继续。消防页面如果切到后台很久还可以用onAppBackground事件暂停采集、onAppForeground恢复。种地设备的数据实时性要求没到秒级30秒采集一次足够太频繁反而给数据库和CPU增加无谓负载。4.3 预留服务端接口的扩展点项目早期用本地模拟数据但架构上必须预留接入真实数据源的口子。我定义了一个抽象的数据仓库接口interface EnvDataSource { fetchHistory(deviceId: string, startTime: number, endTime: number): PromiseEnvRecord[]; fetchRealtime(deviceId: string): PromiseEnvRecord; }本地实现和云端实现各自维护一套页面通过依赖注入选择数据源。这样以后对接物联网平台时只新增一个CloudDataSource类图表页面一行代码都不用改。接口隔离这块值得投入因为智慧农业项目后期大概率会接正规的IoT平台数据上报频率、消息推送格式都变了有这层抽象能省下很多返工时间。另一个扩展点是订阅推送若平台支持WebSocket或消息推送实时数据可以不依赖定时拉取改成服务端主动推。HarmonyOS NEXT提供WebSocket客户端逻辑不难但要注意断线重连策略指数退避固定间隔结合避免断网恢复后所有设备同时重连造成服务端压力。5. 页面搭建一个数据分析面板的完整落地过程5.1 页面路由和参数传递数据分析面板不是单页我拆成了两层概览面板Dashboard和详情页。概览面板放图表卡片详情页放对应维度的明细数据和扩展图表。路由用系统提供的router.pushUrlrouter.pushUrl({ url: pages/AnalysisDetailPage, params: { deviceId: this.deviceId, category: temp, startTime: this.rangeStart, endTime: this.rangeEnd } });这里的关键是参数别传太多对象最好只传ID和基础条件详情页再根据ID查库。传整个对象容易遇到循环引用、序列化异常而且详情页刷新后参数丢失。详情页接收参数用router.getParams()返回的是Object类型需要做一次类型断言const params router.getParams() as Recordstring, Object; const deviceId params.deviceId as string;5.2 图表卡片组件化与复用每个图表卡片在项目里都是一个独立组件比如LineChartCard、BarChartCard、DonutChartCard。组件内部自己根据输入数据渲染对应图形通过Prop接收父页面传进来的数据Component export struct LineChartCard { Prop records: EnvRecord[]; Prop title: string 环境趋势; private chartCtx: CanvasRenderingContext2D new CanvasRenderingContext2D(new RenderingContextSettings(true)); build() { Column() { Text(this.title) .fontSize(16) .fontWeight(FontWeight.Medium) .margin({ bottom: 8 }) Canvas(this.chartCtx) .width(100%) .height(180) .onReady(() { this.draw(); }) } } }组件化的好处是概览面板可以加多个卡片实例只改数据不改结构。某些卡片要带右上角的“查看详情”跳转按钮这需要父页面注入回调用Link或者函数类型的Prop都能实现。函数类型的Prop在ArkTS里要先声明类型接口直接写箭头函数会有一点类型声明的麻烦要注意。5.3 动态刷新与状态同步实时数据场景必须处理“数据更新后图表重新绘制”的问题。HarmonyOS的状态管理机制是声明式响应当State数据变化时build方法重跑但Canvas不是普通组件它的内容是命令式绘制的状态变了系统不会自动重绘需要手动调用draw()。我的做法是在aboutToAppear里设置定时器拉取最新数据数据更新后调用this.chartCtx.invalidate()或者直接再次调用绘制函数。CanvasRenderingContext2D在API 12里已经支持invalidate()方法它会把画布标记为需要重绘。这里要注意一个性能问题每次重绘都不该全量重传数据。比如折线图实时更新我维护一个环形buffer只保留最近500个点每次重绘只画这500个点避免数组无限增长导致绘制卡顿。Array.shift()在元素多时性能一般建议直接用固定长度的数组手动覆盖索引。Watch装饰器也能配合Prop使用父组件数据变化后子组件通过Watch监听到自动触发重绘。这个思路比外部手动调invalidate()更符合声明式范式代码耦合度更低推荐用这个方式。5.4 性能优化懒加载与画布复用数据分析页的图表卡片数量可能很多如果全部在页面加载时一次性绘制首次进入会有明显白屏等待。我的优化手段有两个懒加载和画布复用。懒加载思路是给卡片设定“进入可视区才绘制”的逻辑。HarmonyOS的ListItem有onAppear回调可以在这个时机触发绘制。或者在Scroll的onScroll回调里判断当前滚动位置计算哪些卡片在可视范围内。画布复用的核心是避免重复new CanvasRenderingContext2D。一个页面多个Canvas组件每个都要创建独立的Context这个对象开销不小。复用的方式是图标卡片组件只维护一个Context内部多个绘制方法切换绘制场景。但这要求所有图表画在同一个Canvas组件上布局需要自由定位设计上会复杂一些。实测下来三张卡片以内不优化完全没问题超过五张建议上懒加载。生产环境我用LazyForEach做列表渲染再加上懒加载页面滑动很顺滑内存占用也稳定。6. 问题排查与避坑实录6.1 图表渲染空白或刷新失败图表空白是开发期出现频率最高的问题。我排查的套路固定三步第一确认Canvas的onReady回调是否触发。如果没有触发说明Canvas组件可能没有被加进视图树或者尺寸为0。HarmonyOS里给Canvas设置width(100%)在父容器没有明确高度时可能解析成0导致onReady不触发图表根本画不出来。解决办法是给父容器也设置明确高度或者使用百分比搭配最小高度约束。第二确认ctx是否为空。CanvasRenderingContext2D创建后在onReady之前调用绘制是无效的。如果代码在aboutToAppear里执行了绘制此时画布还没准备好就会出现空白。所有绘制逻辑必须放在onReady回调之后或者等onReady触发后再调用。第三检查坐标计算是否产生NaN或无穷大。数据为空、最大最小值相等、记录数只有一条这三个边界情况都会导致坐标换算结果异常画出来的东西不在可视范围内。给数据做过滤和默认值兜底是最稳妥的防御式编程。6.2 不同终端屏幕适配问题同样的代码在手机上一个尺寸在平板上另一个尺寸这是因为图表宽高绝对值。我的做法是Canvas宽高通过组件的onAreaChange事件动态获取所有绘制的坐标都基于实际获取的宽高计算不写死任何像素值。字体缩放在平板上特别容易踩坑10fp在平板上渲染出来偏小在手机上又偏大。我的经验是图表内标签字体直接用12fp起步如果容器宽度不够就截断数据而非缩小字体。数据较多时增加滚动区域比压缩字号更合理。手机上展示24小时每小时均值横向滚动比缩到一屏内更清晰。如果你的应用需要支持折叠屏、平板等多种形态建议列表使用GridRow做栅格布局手机上一列平板两列卡片宽度跟随栅格自适应。6.3 ArkTS语法的几个坑ArkTS和TypeScript看起来像但约束更严有几个坑我反复撞一是不能用any类型。图表数据聚合时我一开始写const map: any {}编译直接报错。ArkTS要求使用Recordstring, number[]这类明确的联合类型或接口。这其实是好事强制开发把数据结构想清楚但初期的学习代价不小。二是Object类型不能直接调用方法。从router.getParams()返回的对象需要先断言成具体的类型否则.访问属性会抛TS错误。这跟JS的自由风格差别很大容易兜不住。三是JSON.parse返回的也是Object不能直接.length或forEach。需要自定接口做解析映射比如interface ApiResponse { code: number; data: EnvRecord[]; } const resp JSON.parse(raw) as ApiResponse; const list resp.data;四是箭头函数里的this。ArkTS的严格模式下嵌套箭头函数访问外层this一般没问题但如果你在回调里使用function关键字声明的普通函数this指向会变化。统一用箭头函数可以避免这类玄学问题。6.4 后台采集失效与电量平衡应用切后台后定时器可能被系统挂起回到前台时数据出现缺口。如果做实时监测类功能需要用setTimeout递归而非setInterval并在onAppForeground时立即补采一次数据private scheduleNextCollect() { this.timerId setTimeout(() { this.collectEnvData(); // 递归调度下一次 this.scheduleNextCollect(); }, 30000); }定时精度在HarmonyOS上不是实时操作系统级别系统耗电优化会合并唤醒时间偏离个几秒到几十秒都正常数据采集端需要容忍这个抖动。我的采集任务容忍5分钟以内的间歇影响不大但对采集时间戳的连续性判断有帮助。另外如果应用退到后台超过半小时还在持续采集会明显耗电用户反馈“后台为什么这么费电”就成了差评热点。我的建议是退后台超过10分钟自动暂停采集回到前台重新建立会话同时从服务端拉取后台期间的数据补齐缺口。智慧农业场景传感器数据本来就存在云端本地补采是备选方案。7. 一点心得怎么让可视化真正帮到种地的人这个模块开发完我最大的体会是数据分析可视化的核心价值不是把数据画得漂亮而是帮用户缩短“看到问题到做出决定”的时间。温度曲线如果只是画出来种植户还是要自己去盯有没有异常。更好的是把阈值告警结合起来曲线在某个时刻变红、同时弹出一条“凌晨3点温度低于设定阈值可能触发冻害风险”的提醒这才是真正有用的可视化。折线图、柱状图、环形图只是工具工具背后的业务理解才是项目能不能落地的关键。如果你也在做类似的HarmonyOS智慧农业应用建议下一步可以加一个“异常检测”模块用简单的阈值规则扫描历史数据把异常片段标出来再配上对应的农事建议。数据可视化从“展示”走向“辅助决策”这一步迈过去整套系统的价值会有质的提升。
返回列表