第8章 8.4 Canvas 与 ECharts:让数据「活」起来

上 一章我们学会了用蓝牙和 NFC 把手机变成万能遥控器,但遥控器上总得有个显示屏吧?这一章我们就来搞定画布图表——让数据不再是冰冷的数字,而是能一目了然的可视化大屏。

想象一下:你做一个校园食堂的小工具,能实时显示「今天哪个窗口排队最短」「本周人气菜TOP5」——这种需求,靠文字列表根本做不到,得靠 Canvas 绑图画 + ECharts 绑图表。

学完这章,你就能做出这样的东西:


🎯 开场 3 分钟:为什么要学这个?

你肯定遇到过这些痛点

  • 老板让你做个「数据看板」,你只会扔一个表格,用户看得直打哈欠
  • 想做一张折线图展示成绩趋势,网上搜了一堆 JS 库,看文档看到怀疑人生
  • uniapp 里画个圆角矩形,试了半天 view 组件的 border-radius,效果惨不忍睹

本章能解决什么

  • Canvas 绑图绑任意形状(圆、曲线、自定义图形)
  • ECh\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\narts 绑绑绑5分钟做出折线图、饼图、柱状图
  • 用两者的组合,做出可交互的数据可视化大屏

🧱 基础 25 分钟:核心概念

8.4.1 Canvas 绑图:画布是个「画板」

什么是 Canvas?

生活类比:Canvas 就像一块空白画板,你告诉它「在 (100, 100) 位置画一个半径 50 的圆」,它就乖乖画出来。它是指令式的——你说什么,它画什么。

为什么要用 Canvas?

  • view 组件能绑的形状太单一(矩形、圆形还行,但曲线、渐变就抓瞎)
  • 需要高性能绑图(比如游戏、地图标注、数据可视化底层)
  • Canvas 是像素级控制,想绑什么形状都可以

怎么用(uniapp 版)

// 1. 在 pages.json 里配置支持 canvas
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "Canvas 绑图",
"disableScroll": true,
"canvasConfigs": [
  {
    "canvasId": "myCanvas",
    "type": "2d"
  }
]
}
}
<!-- 2. 在 .vue 文件里放一个 canvas 标签 -->
<template>
<view class="container">
<canvas canvas-id="myCanvas" id="myCanvas" style="width: 300px; height: 300px;"></canvas>
</view>
</template>
// 3. 在 <script> 里开始画
export default {
onLoad() {
// 一定要等 canvas ready 了再画
setTimeout(this.drawSomething, 100);
},
methods: {
drawSomething() {
  // 获取 canvas 上下文(就是那块画板的操作柄)
  const ctx = uni.createCanvasContext('myCanvas', this);

  // 绑个矩形(填充色蓝色)
  ctx.setFillStyle('#3498db');
  ctx.fillRect(50, 50, 200, 100);  // x, y, 宽, 高

  // 画一条线
  ctx.beginPath();
  ctx.moveTo(50, 180);      // 起点
  ctx.lineTo(250, 180);    // 终点
  ctx.setStrokeStyle('#e74c3c');
  ctx.stroke();

  // 写字
  ctx.setFillStyle('#2c3e50');
  ctx.font = '20px Arial';
  ctx.fillText('小明的成绩单', 70, 110);

  // 最重要的一步:把刚才的指令「提交」给画布
  ctx.draw();
}
}
}

运行效果:页面上会出现一个蓝色矩形 + 一条红线 + 一行字。

这 4 行代码在干嘛:
- setFillStyle 设置「画笔颜色」
- fillRect 在指定位置绑「填色矩形」
- moveTo/lineTo 控制画笔「移动路径」
- draw()真正执行,没有这行,画布上什么都没有

8.4.2 Canvas 进阶:圆形、弧线、渐变

上一节是「方块 + 直线」,现在升级画点圆的:

// 画一个饼形(扇形)
drawPie() {
const ctx = uni.createCanvasContext('myCanvas', this);

// 饼形本质是多个「弧线 + 两条半径」
const centerX = 150, centerY = 150, radius = 100;
const data = [30, 25, 20, 25];  // 4 个扇区的比例
const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'];
let startAngle = 0;

data.forEach((value, index) => {
const endAngle = startAngle + (value / 100) * Math.PI * 2;

ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.arc(centerX, centerY, radius, startAngle, endAngle);
ctx.closePath();
ctx.setFillStyle(colors[index]);
ctx.fill();

startAngle = endAngle;
});

ctx.draw();
}

代码解释
- arc(x, y, 半径, 起始弧度, 结束弧度) 绑圆弧
- closePath() 把弧的两端用直线连起来,形成扇形
- 循环累加角度,实现「多个扇区拼成一个饼」

生活类比:切披萨——每次下刀是一个半径,转着圈切,每块都是「两条半径 + 一段圆弧」。

8.4.3 ECharts 绑图表:现成的「图表工厂」

什么是 ECharts?

生活类比:Canvas 是「手动绑图」,ECharts 是「去4S店买车」——人家把发动机、轮子都给你装好了,你只要说「我要一辆红色 SUV」,它就给你产出一辆完整的车。

ECharts 是百度开源的数据可视化库,uniapp 有适配器让它跑在小程序端。

为什么要用 ECharts?

  • 5 分钟上手,绑个折线图不用写 100 行
  • 图表类型丰富:折线图、饼图、柱状图、热力图、地图……
  • 交互强:支持绑绑缩放、数据筛选、hover 提示

怎么用(uniapp + ECharts)

第一步:安装

npm install echarts uni-echarts

第二步:创建图表组件(新建 components/echarts/echarts.vue

这个组件比较特殊,需要做双向绑定和适配:

<template>
<canvas
v-if="canvasId"
:canvas-id="canvasId"
:id="canvasId"
class="echarts"
:style="{width: width + 'px', height: height + 'px'}"
@touchstart="touchStart"
@touchmove="touchMove"
@touchend="touchEnd"
></canvas>
</template>

<script>
import * as echarts from 'echarts';
import '../../static/echarts/echarts.min.js';  // 下载的 echarts 文件

export default {
name: 'ECharts',
props: {
canvasId: { type: String, required: true },
option: { type: Object, required: true },
width: { type: Number, default: 300 },
height: { type: Number, default: 300 }
},
data() {
return { chart: null };
},
watch: {
option: {
  deep: true,
  handler(val) {
    this.setOption(val);
  }
}
},
mounted() {
this.init();
},
beforeDestroy() {
if (this.chart) {
  this.chart.dispose();
}
},
methods: {
init() {
  const query = uni.createSelectorQuery().in(this);
  query.select(`#${this.canvasId}`).boundingClientRect().exec(res => {
    if (res[0]) {
      this.chart = echarts.init(document.getElementById(this.canvasId));
      this.setOption(this.option);
    }
  });
},
setOption(option) {
  if (this.chart) {
    this.chart.setOption(option);
  }
},
touchStart(e) { this.chart && e.preventDefault(); },
touchMove(e) { this.chart && e.preventDefault(); },
touchEnd(e) { this.chart && e.preventDefault(); }
}
}
</script>

第三步:在页面里用

<template>
<view class="container">
<text class="title">本周学习时长统计</text>
<!-- 直接把 option 传进去,ECharts 帮你画 -->
<echarts canvas-id="chart1" :option="chartOption" :width="350" :height="300"></echarts>
</view>
</template>

<script>
import echarts from '@/components/echarts/echarts.vue';

export default {
components: { echarts },
data() {
return {
  chartOption: {
    title: { text: '每日学习时长(小时)', left: 'center' },
    tooltip: { trigger: 'axis' },
    xAxis: {
      type: 'category',
      data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
    },
    yAxis: { type: 'value', name: '小时' },
    series: [{
      type: 'line',
      data: [2.5, 3.0, 1.5, 4.0, 2.0, 3.5, 4.5],
      areaStyle: { color: 'rgba(52, 152, 219, 0.3)' },  // 填充渐变
      lineStyle: { color: '#3498db', width: 3 },
      itemStyle: { color: '#3498db' }
    }]
  }
};
}
}
</script>

效果:页面上出现一条带面积填充的蓝色折线图,hover 上去能看到具体数值。

这坨配置在干嘛:
- xAxis.data 是「横坐标刻度」
- series.data 是「每个点的数值」
- type: 'line' 是「折线图」(改 bar 就是柱状图,改 pie 就是饼图)
- areaStyle 是「折线下面填色」

8.4.4 图表组合:多图联动

单个图太孤单?来一个「Dashboard」布局:

<template>
<view class="dashboard">
<view class="row">
  <echarts canvas-id="lineChart" :option="lineOption" :width="160" :height="160"></echarts>
  <echarts canvas-id="pieChart" :option="pieOption" :width="160" :height="160"></echarts>
</view>
<view class="row">
  <echarts canvas-id="barChart" :option="barOption" :width="340" :height="160"></echarts>
</view>
</view>
</template>

<script>
export default {
data() {
return {
  lineOption: {
    title: { text: '月度趋势', textStyle: { fontSize: 12 } },
    series: [{ type: 'line', data: [120, 200, 150, 80, 70, 110, 130] }]
  },
  pieOption: {
    title: { text: '占比', textStyle: { fontSize: 12 } },
    series: [{
      type: 'pie',
      radius: '60%',
      data: [
        { value: 335, name: '直接访问' },
        { value: 310, name: '邮件营销' },
        { value: 234, name: '联盟广告' }
      ]
    }]
  },
  barOption: {
    title: { text: '门店业绩(万元)', textStyle: { fontSize: 12 } },
    xAxis: { type: 'category', data: ['北京', '上海', '广州', '深圳'] },
    yAxis: { type: 'value' },
    series: [{ type: 'bar', data: [1200, 1800, 950, 1100] }]
  }
};
}
}
</script>

<style>
.dashboard { padding: 10px; }
.row { display: flex; justify-content: space-around; margin-bottom: 10px; }
</style>

🔥 实战 35 分钟:3 个递进小项目

项目 1(5 分钟):Canvas 绘制的「圆角卡片」

需求:画一个带圆角和阴影效果的卡片(Canvas 版本)。

<template>
<view class="page">
<canvas canvas-id="cardCanvas" id="cardCanvas" style="width: 300px; height: 200px;"></canvas>
</view>
</template>

<script>
export default {
onLoad() {
setTimeout(this.drawCard, 100);
},
methods: {
drawCard() {
  const ctx = uni.createCanvasContext('cardCanvas', this);
  const x = 20, y = 20, w = 260, h = 160, r = 15;

  // 圆角矩形路径
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.lineTo(x + w - r, y);
  ctx.arcTo(x + w, y, x + w, y + r, r);
  ctx.lineTo(x + w, y + h - r);
  ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
  ctx.lineTo(x + r, y + h);
  ctx.arcTo(x, y + h, x, y + h - r, r);
  ctx.lineTo(x, y + r);
  ctx.arcTo(x, y, x + r, y, r);
  ctx.closePath();

  // 填充 + 阴影
  ctx.setShadow(0, 5, 10, 'rgba(0,0,0,0.2)');
  ctx.setFillStyle('#ffffff');
  ctx.fill();

  // 写标题
  ctx.setShadow(0, 0, 0, 'rgba(0,0,0,0)');
  ctx.setFillStyle('#2c3e50');
  ctx.font = 'bold 18px Arial';
  ctx.fillText('小明的好友列表', 40, 60);

  ctx.setFillStyle('#7f8c8d');
  ctx.font = '14px Arial';
  ctx.fillText('共 128 位好友', 40, 90);

  ctx.draw();
}
}
}
</script>

预期输出:一个白色圆角卡片,带投影,标题「小明的好友列表」。

一句话解释arcTo 是绑圆角的关键,它根据两条切线的交点自动计算圆弧。


项目 2(15 分钟):ECharts 读取 JSON 数据绘制「班级成绩分布」

需求:从本地 JSON 文件读取班级成绩,绑一张柱状图 + 饼图组合。

准备数据文件static/data/scores.json):

{
"className": "初三(2)班",
"scores": [
{ "name": "语文", "avg": 85 },
{ "name": "数学", "avg": 78 },
{ "name": "英语", "avg": 92 },
{ "name": "物理", "avg": 81 },
{ "name": "化学", "avg": 88 }
],
"distribution": [
{ "range": "0-60", "count": 3 },
{ "range": "60-70", "count": 5 },
{ "range": "70-80", "count": 12 },
{ "range": "80-90", "count": 18 },
{ "range": "90-100", "count": 7 }
]
}

页面代码

<template>
<view class="container">
<text class="header">📊 {{ className }} 成绩分析</text>
<view class="chart-row">
  <echarts canvas-id="barChart" :option="barOption" :width="170" :height="200"></echarts>
  <echarts canvas-id="pieChart" :option="pieOption" :width="170" :height="200"></echarts>
</view>
</view>
</template>

<script>
import echarts from '@/components/echarts/echarts.vue';

export default {
components: { echarts },
data() {
return {
  className: '加载中...',
  barOption: {},
  pieOption: {}
};
},
onLoad() {
this.loadData();
},
methods: {
loadData() {
  // 读取本地 JSON
  uni.request({
    url: '/static/data/scores.json',
    success: (res) => {
      const data = res.data;
      this.className = data.className;
      this.renderBarChart(data.scores);
      this.renderPieChart(data.distribution);
    }
  });
},
renderBarChart(scores) {
  this.barOption = {
    title: { text: '各科平均分', left: 'center', textStyle: { fontSize: 14 } },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: scores.map(s => s.name) },
    yAxis: { type: 'value', min: 0, max: 100 },
    series: [{
      type: 'bar',
      data: scores.map(s => s.avg),
      itemStyle: {
        color: (params) => {
          const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6'];
          return colors[params.dataIndex % colors.length];
        }
      }
    }]
  };
},
renderPieChart(distribution) {
  this.pieOption = {
    title: { text: '分数段分布', left: 'center', textStyle: { fontSize: 14 } },
    tooltip: { trigger: 'item', formatter: '{b}: {c}人 ({d}%)' },
    series: [{

      type: 'pie',
      radius: ['40%', '70%'],
      center: ['50%', '55%'],
      data: distribution.map(d => ({ name: d.range, value: d.count }))
    }]
  };
}
}
}
</script>

<style>
.container { padding: 20px; background: #f5f6fa; min-height: 100vh; }
.header { font-size: 20px; font-weight: bold; display: block; margin-bottom: 20px; text-align: center; }
.chart-row { display: flex; justify-content: space-around; background: white; border-radius: 10px; padding: 10px; }
</style>

预期输出:左侧是彩色柱状图(各科平均分),右侧是环形饼图(分数段人数占比)。

一句话解释:把 JSON 里的数组 map 成 ECharts 需要的格式,就是数据可视化的「翻译」工作。


项目 3(15 分钟):Canvas + ECharts 组合「个人数据中心」

需求:做一个「个人数据中心」小工具,Canvas 画头像背景,ECharts 显示今日步数、周消费、心情曲线。

<template>
<view class="dashboard">
<!-- Canvas 头像区 -->
<view class="avatar-section">
  <canvas canvas-id="avatarCanvas" id="avatarCanvas" style="width: 80px; height: 80px;"></canvas>
  <view class="user-info">
    <text class="username">{{ userName }}</text>
    <text class="date">{{ today }}</text>
  </view>
</view>

<!-- 3 个小图表 -->
<view class="charts-grid">
  <view class="chart-card">
    <echarts canvas-id="stepsChart" :option="stepsOption" :width="105" :height="105"></echarts>
  </view>
  <view class="chart-card">
    <echarts canvas-id="expenseChart" :option="expenseOption" :width="105" :height="105"></echarts>
  </view>
  <view class="chart-card">
    <echarts canvas-id="moodChart" :option="moodOption" :width="105" :height="105"></echarts>
  </view>
</view>
</view>
</template>

<script>
import echarts from '@/components/echarts/echarts.vue';

export default {
components: { echarts },
data() {
return {
  userName: '小明',
  today: new Date().toLocaleDateString('zh-CN'),
  stepsOption: {},
  expenseOption: {},
  moodOption: {}
};
},
onLoad() {
this.drawAvatar();
this.loadStats();
},
methods: {
drawAvatar() {
  const ctx = uni.createCanvasContext('avatarCanvas', this);
  // 画圆形背景
  ctx.beginPath();
  ctx.arc(40, 40, 35, 0, Math.PI * 2);
  const gradient = ctx.createLinearGradient(0, 0, 80, 80);
  gradient.addColorStop(0, '#667eea');
  gradient.addColorStop(1, '#764ba2');
  ctx.setFillStyle(gradient);
  ctx.fill();
  // 画姓名首字
  ctx.setFillStyle('#ffffff');
  ctx.font = 'bold 28px Arial';
  ctx.textAlign = 'center';
  ctx.fillText('小', 40, 50);
  ctx.draw();
},
loadStats() {
  // 模拟从接口获取数据
  const mockData = {
    steps: { value: 8532, goal: 10000 },
    expense: { categories: ['餐饮', '交通', '购物', '娱乐'], values: [45, 12, 230, 80] },
    mood: [3, 4, 3, 5, 4, 5, 4]
  };
  this.renderSteps(mockData.steps);
  this.renderExpense(mockData.expense);
  this.renderMood(mockData.mood);
},
renderSteps(data) {
  this.stepsOption = {
    title: { text: '今日步数', left: 'center', textStyle: { fontSize: 11 } },
    series: [{
      type: 'gauge',
      startAngle: 180,
      endAngle: 0,
      center: ['50%', '70%'],
      radius: '90%',
      min: 0,
      max: data.goal,
      splitNumber: 4,
      axisLine: { lineStyle: { width: 6, color: [[data.value/data.goal, '#2ecc71'], [1, '#ecf0f1']] } },
      pointer: { itemStyle: { color: '#2ecc71' } },
      axisTick: { show: false },
      splitLine: { show: false },
      axisLabel: { show: false },
      detail: { formatter: '{value}', fontSize: 16, fontWeight: 'bold', offsetCenter: [0, '10%'] },
      data: [{ value: data.value }]
    }]
  };
},
renderExpense(data) {
  this.expenseOption = {
    title: { text: '本周消费', left: 'center', textStyle: { fontSize: 11 } },
    tooltip: { trigger: 'item', formatter: '{b}: ¥{c}' },
    series: [{
      type: 'pie',
      radius: '70%',
      center: ['50%', '55%'],
      data: data.categories.map((name, i) => ({ name, value: data.values[i] })),
      label: { fontSize: 8 }
    }]
  };
},
renderMood(data) {
  this.moodOption = {
    title: { text: '心情曲线', left: 'center', textStyle: { fontSize: 11 } },
    radar: {
      indicator: [{ name: '周一' }, { name: '周二' }, { name: '周三' }, { name: '周四' }, { name: '周五' }, { name: '周六' }, { name: '周日' }],
      shape: 'polygon',
      splitNumber: 3
    },
    series: [{
      type: 'radar',
      data: [{ value: data, name: '心情指数', areaStyle: { color: 'rgba(52, 152, 219, 0.3)' } }]
    }]
  };
}
}
}
</script>

<style>
.dashboard { padding: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
.avatar-section { display: flex; align-items: center; margin-bottom: 20px; }
.avatar-section canvas { border-radius: 50%; }
.user-info { margin-left: 15px; }
.username { font-size: 20px; color: white; font-weight: bold; display: block; }
.date { font-size: 12px; color: rgba(255,255,255,0.8); }
.charts-grid { display: flex; justify-content: space-around; flex-wrap: wrap; }
.chart-card { background: white; border-radius: 10px; margin-bottom: 10px; }
</style>

预期输出:顶部是渐变圆形头像 + 用户信息,下面 3 个小卡片分别是步数仪表盘、消费饼图、心情雷达图。

一句话解释gauge 是仪表盘图表,radar 是雷达图,两者组合能做「健康数据」类的一切可视化。


💪 进阶 20 分钟:常见坑 + 性能小贴士

坑 1:Canvas 画布「吃」了事件

// ❌ 错误:canvas 标签没有绑定事件,导致点击无响应
<canvas canvas-id="myCanvas" style="width: 300px; height: 300px;"></canvas>

// ✅ 正确:需要加 @click 或者在 canvas 外层包 view
<canvas canvas-id="myCanvas" style="width: 300px; height: 300px;" @click="onCanvasTap"></canvas>
// 或者
<view @click="onCanvasTap">
<canvas canvas-id="myCanvas" style="width: 300px; height: 300px;"></canvas>
</view>

坑 2:ECharts 图「裂」了,重新进入页面不刷新

// ❌ 错误:onShow 里直接 setOption,但 chart 实例可能还没创建
onShow() {
this.chartOption.series[0].data = [5, 6, 7];  // 修改数据
this.$refs.myChart.setOption(this.chartOption);  // 报错:chart is null
}

// ✅ 正确:用 $nextTick 或者 watch option 变化
onShow() {
this.chartOption.series[0].data = [5, 6, 7];
},
watch: {
chartOption: {
deep: true,
handler(val) {
  if (this.$refs.myChart) {
    this.$refs.myChart.setOption(val);
  }
}
}
}

坑 3:Canvas 在不同手机上「模糊」

// ❌ 错误:直接用 style 设置宽高,实际像素不够
<canvas canvas-id="myCanvas" style="width: 300px; height: 300px;"></canvas>

// ✅ 正确:通过 canvas 的 width 和 height 属性设置物理像素
// 同时在 style 里设置 CSS 宽高来控制显示大小
<canvas
canvas-id="myCanvas"
style="width: 300px; height: 300px;"
:width="300 * 2"  // 乘以 dpr 解决模糊
:height="300 * 2"
></canvas>

// 或者动态获取 dpr
const dpr = uni.getSystemInfoSync().pixelRatio;
ctx.setCanvasWithd(dpr);  // uniapp 特有 API

坑 4:ECharts 图表「太大」撑出屏幕

// ❌ 错误:写死宽高,在小屏手机上溢出
<echarts canvas-id="chart" :option="option" :width="500" :height="400"></echars>

// ✅ 正确:用百分比 + 动态计算
computed: {
chartWidth() {
return uni.getSystemInfoSync().windowWidth - 40;  // 留点边距
}
}
<echarts canvas-id="chart" :option="option" :width="chartWidth" :height="250"></echarts>

坑 5:Canvas draw() 之后「再也改不了」

// ❌ 错误:以为可以分次画,结果只有最后一次 draw() 的内容显示
ctx.fillRect(10, 10, 50, 50);
ctx.draw();  // 提交了
ctx.fillRect(100, 100, 50, 50);
ctx.draw();  // 覆盖了第一次

// ✅ 正确:所有绘制操作完成后,统一 draw() 一次
ctx.fillRect(10, 10, 50, 50);
ctx.fillRect(100, 100, 50, 50);
ctx.fillText('最后才提交', 50, 50);
ctx.draw();  // 一次性提交所有内容

性能小优化:大量数据用「采样」

// 假如有 10000 个数据点,直接绑会卡死
// ❌ 错误
series: [{ data: hugeArray }]

// ✅ 正确:采样显示,每隔 N 个取一个
function sampleData(arr, targetCount) {
const step = Math.ceil(arr.length / targetCount);
return arr.filter((_, i) => i % step === 0);
}
series: [{ data: sampleData(hugeArray, 500) }]

调试技巧:Canvas 绘制结果「截图」排查

// 临时加一个按钮,点击时导出 canvas 内容
export default {
methods: {
exportCanvas() {
  uni.canvasToTempFilePath({
    canvasId: 'myCanvas',
    success: (res) => {
      console.log('截图路径:', res.tempFilePath);
      uni.previewImage({ urls: [res.tempFilePath] });
    }
  });
}
}
}

✏️ 练习题 + 作业题

练习题(10 分钟)

练习 1(2 分钟):改颜色
- 输入:项目 1 的圆角卡片代码
- 预期输出:把填充色从 #ffffff 改成 #f0f8ff(淡蓝背景)
- 提示:找到 setFillStyle 那行,改掉颜色值就行

练习 2(2 分钟):加判断
- 输入:项目 1 的代码
- 预期输出:如果卡片宽度 w > 200,显示「宽卡片」,否则显示「窄卡片」
- 提示:在 fillText 前面加一个 if 判断,改 fillText 的内容

练习 3(3 分钟):新数据画柱状图
- 输入:[85, 92, 78, 96, 88] 这组数据代表 5 个学生的身高
- 预期输出:用 ECharts 绑一张柱状图,x 轴标「学生1」到「学生5」
- 提示:参考项目 2 的 renderBarChart,只需要改 xAxis.dataseries.data

练习 4(3 分钟):串项目 1 + 项目 2
- 输入:把项目 1 的圆角卡片作为项目 2 页面的「标题栏」
- 预期输出:页面顶部是圆角卡片(写着班级名),下面是两个图表
- 提示:把项目 1 的 drawCard 方法加到项目 2 的 onLoad 里,动态传入班级名

练习 5(挑战题,5 分钟):修报错
- 输入:以下代码运行时报错「chart is not defined」
- 代码:

onShow() {
this.chartOption.series[0].data = [1, 2, 3];
this.$refs.myEcharts.setOption(this.chartOption);
}
  • 预期输出:不报错,图表正常更新
  • 提示:报错是因为 chart 还没创建好,考虑用 this.$nextTick 或者在 onLoad 而非 onShow 里更新

作业题(30 分钟-2 小时)

作业:做一个「个人健康数据看板」

  • 需求描述:做一个可以记录和展示每日健康数据的小工具,数据自己 Mock(也可以让用户输入)
  • 功能点
    1. Canvas 画一个「身体轮廓图」作为背景装饰
    2. ECharts 展示步数(仪表盘)、睡眠时长(柱状图)、饮水量(饼图)
    3. 点击卡片可以修改当天数据(用 uni.showModal 输入新值)
  • 加分项
    1. 数据持久化到本地(uni.setStorage
    2. 切换日期查看历史数据
  • 验收标准:能跑起来 + 3 个图表都正常显示 + 点击能修改数据
  • 提交方式:评论区贴关键代码片段或 GitHub 链接

📚 总结 + 资源

本章 3 个核心点

  1. Canvas 是「画板」:用 beginPath / moveTo / lineTo / arc / fill 等指令绑图,最后 draw() 提交
  2. ECharts 是「图表工厂」:把数据塞进 option 配置对象,它自动给你画成图表
  3. 两者组合能做数据大屏:Canvas 负责自定义图形,ECharts 负责标准图表,各取所长

延伸学习资源

互动钩子

「你做过数据可视化相关的项目吗?踩过什么坑?评论区聊聊,老粉优先回复!」


下章预告:学会了 Canvas 和 ECharts,是时候做个「真家伙」了——下一章我们综合实战,做一个可视化数据大屏,把食堂排队、成绩分析、步数统计全部串起来,做成一张可以展示的「驾驶舱」!🎯

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。