App Inventor 2 动态图表教程 - ECharts与Chart组件实现实时曲线图

« 返回首页

概述

在App Inventor 2中实现动态折线图/曲线图有两种主要方案:

方案 灵活度 性能 难度
Chart组件(内置) ⭐⭐ 高 ⭐
ECharts + WebView ⭐⭐⭐⭐⭐ 中 ⭐⭐⭐

方案一:内置Chart组件

组件位置

组件面板 → Charts → Chart + LineChart

基本使用

Chart组件需要配合数据源组件使用:

  • ChartData — 静态数据
  • FileData — 从文件读取数据
  • WebData — 从网络API获取数据
  • BluetoothData — 从蓝牙获取数据

添加Chart组件

  1. 在Designer中拖入 Chart 组件
  2. 设置Chart类型为 Line(折线图)
  3. 添加 ChartData 组件作为数据源

动态添加数据点

when 按钮1.Click() {
  global 值 = TextBox1.Text
  ChartData2D1.AddEntry("1", 值)
}

// 带标签的数据点
when 按钮2.Click() {
  global 标签 = join("第", 数据计数, "个")
  global 值 = TextBox1.Text
  ChartData2D1.AddEntry(标签, 值)
}

清除图表数据

when 按钮3.Click() {
  ChartData2D1.RemoveDataSource()
}

实时更新图表(配合Clock)

global 数据计数 = 0

when Screen1.Initialize() {
  Clock1.TimerInterval = 1000  // 每秒更新
  Clock1.TimerEnabled = true
}

when Clock1.Timer() {
  global 数据计数 = 数据计数 + 1
  global 随机值 = Math.random() * 100
  ChartData2D1.AddEntry(join("T", 数据计数), 随机值)
  
  // 限制显示最近20个点
  if (数据计数 > 20) {
    ChartData2D1.RemoveEntry("0", "0") // dummy
  }
}

Chart组件属性

属性 说明
Type 图表类型(Line/Bar/Scatter/Area)
Description 图表描述
LabelsFromX 是否从X轴数据生成标签
GridEnabled 是否显示网格线
LegendEnabled 是否显示图例
XFromZero X轴是否从0开始

多条折线

添加多个ChartData组件,每个对应一条线:

when Clock1.Timer() {
  global 值1 = 传感器值1
  global 值2 = 传感器值2
  ChartData2D1.AddEntry("1", 值1)  // 线1
  ChartData2D2.AddEntry("2", 值2)  // 线2
}

方案二:ECharts + WebView

ECharts是百度开源的强大图表库,通过WebView可以在App Inventor中使用。

HTML文件(echarts_dynamic.html)

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>
body { margin: 0; background: #fff; }
#chart { width: 100vw; height: 100vh; }
</style>
</head>
<body>
<div id="chart"></div>
<script>
var myChart = echarts.init(document.getElementById('chart'));
var data = [];
var labels = [];
var maxPoints = 30;

var option = {
  title: { text: '实时数据监控', left: 'center', textStyle: { fontSize: 16 } },
  tooltip: { trigger: 'axis' },
  grid: { left: '10%', right: '5%', bottom: '15%', top: '20%' },
  xAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } },
  yAxis: { type: 'value', axisLabel: { fontSize: 10 } },
  series: [{
    type: 'line',
    data: data,
    smooth: true,
    lineStyle: { color: '#4CAF50', width: 2 },
    areaStyle: { color: 'rgba(76,175,80,0.2)' },
    itemStyle: { color: '#4CAF50' }
  }],
  dataZoom: [{ type: 'inside' }]
};

myChart.setOption(option);

setInterval(function() {
  var msg = window.AppInventor.getWebViewString();
  if (msg && msg !== "") {
    try {
      var parsed = JSON.parse(msg);
      if (parsed.type === "add") {
        data.push(parsed.value);
        labels.push(parsed.label || "");
        if (data.length > maxPoints) {
          data.shift();
          labels.shift();
        }
        myChart.setOption({ xAxis: { data: labels }, series: [{ data: data }] });
      } else if (parsed.type === "multi") {
        // 多条线
        option.series = parsed.series;
        option.xAxis.data = parsed.labels;
        myChart.setOption(option, true);
      } else if (parsed.type === "clear") {
        data = [];
        labels = [];
        myChart.setOption({ xAxis: { data: [] }, series: [{ data: [] }] });
      }
    } catch(e) {}
    window.AppInventor.setWebViewString("");
  }
}, 100);
</script>
</body>
</html>

App Inventor积木块

global 数据计数 = 0

when Screen1.Initialize() {
  WebViewer1.HomeUrl = "file:///android_asset/echarts_dynamic.html"
  Clock1.TimerInterval = 500
  Clock1.TimerEnabled = true
}

when Clock1.Timer() {
  global 数据计数 = 数据计数 + 1
  global 值 = Math.random() * 100
  global 消息 = join("{\"type\":\"add\",\"label\":\"", 数据计数, "\",\"value\":", 值, "}")
  WebViewer1.WebViewString = 消息
}

when 按钮_清除图表.Click() {
  WebViewer1.WebViewString = "{\"type\":\"clear\"}"
  global 数据计数 = 0
}

多条折线(ECharts)

when 按钮_显示多条线.Click() {
  global JSON数据 = "{\"type\":\"multi\",\"labels\":[\"1月\",\"2月\",\"3月\",\"4月\",\"5月\"],\"series\":[{\"type\":\"line\",\"name\":\"温度\",\"data\":[5,8,15,22,28],\"smooth\":true},{\"type\":\"line\",\"name\":\"湿度\",\"data\":[80,70,60,55,50],\"smooth\":true}]}"
  WebViewer1.WebViewString = JSON数据
}

实战案例:温度监测折线图

功能需求

  • 每2秒采集一次温度(模拟)
  • 实时更新折线图
  • 显示最高/最低/平均温度
  • 可以暂停/继续

积木块代码

global 温度列表 = []
global 时间列表 = []
global 是否运行 = false
global 采集计数 = 0

when Screen1.Initialize() {
  WebViewer1.HomeUrl = "file:///android_asset/echarts_dynamic.html"
}

when 按钮_开始.Click() {
  global 是否运行 = true
  Clock1.TimerInterval = 2000
  Clock1.TimerEnabled = true
  按钮_开始.Text = "暂停"
}

when Clock1.Timer() {
  if (是否运行) {
    global 采集计数 = 采集计数 + 1
    // 模拟温度数据(20-30度随机波动)
    global 温度 = 25 + (Math.random() - 0.5) * 10
    global 温度 = formatAsDecimal(温度, 1)
    global 时间标签 = Clock1.FormatDateTime(Clock1.Now(), "HH:mm:ss")
    
    addItemsTo(温度列表, 温度)
    addItemsTo(时间列表, 时间标签)
    
    // 更新图表
    global 消息 = join("{\"type\":\"add\",\"label\":\"", 时间标签, "\",\"value\":", 温度, "}")
    WebViewer1.WebViewString = 消息
    
    // 更新统计
    global 最高 = math.max(温度列表)
    global 最低 = math.min(温度列表)
    // AI2没有直接的列表求和积木,这里假设存在相关方法,或者用其他方式展示
    标签_统计.Text = join("最高:", 最高, "°C | 最低:", 最低, "°C")
  }
}

常见问题

Q1: Chart组件数据多了会卡顿?

内置Chart组件在100+数据点时可能变慢。解决方案:

  • 定期清除旧数据
  • 使用ECharts方案(性能更好)
  • 限制显示的数据点数量

Q2: ECharts图表在WebView中显示不全?

确保HTML中容器尺寸正确:

#chart { width: 100vw; height: 100vh; }

WebView的宽高也需要正确设置。

Q3: 如何实现饼图/柱状图?

ECharts方案中修改series的type:

  • 折线图:type: 'line'
  • 柱状图:type: 'bar'
  • 饼图:type: 'pie'
  • 散点图:type: 'scatter'

Q4: 图表可以导出为图片吗?

ECharts支持导出图片:

var imgData = myChart.getDataURL({ type: 'png' });
window.AppInventor.setWebViewString("IMG:" + imgData);

总结

需求 推荐方案
简单折线图 内置Chart组件
实时动态图 ECharts + WebView
多种图表类型 ECharts
自定义样式 ECharts
离线使用 内置Chart组件
文档反馈