第8章 8.4 Canvas 与 ECharts:让数据「活」起来
上 一章我们学会了用蓝牙和 NFC 把手机变成万能遥控器,但遥控器上总得有个显示屏吧?这一章我们就来搞定画布和图表——让数据不再是冰冷的数字,而是能一目了然的可视化大屏。
想象一下:你做一个校园食堂的小工具,能实时显示「今天哪个窗口排队最短」「本周人气菜TOP5」——这种需求,靠文字列表根本做不到,得靠 Canvas 绑图画 + ECharts 绑图表。
学完这章,你就能做出这样的东西:
🎯 开场 3 分钟:为什么要学这个?
你肯定遇到过这些痛点
- 老板让你做个「数据看板」,你只会扔一个表格,用户看得直打哈欠
- 想做一张折线图展示成绩趋势,网上搜了一堆 JS 库,看文档看到怀疑人生
- uniapp 里画个圆角矩形,试了半天
view组件的border-radius,效果惨不忍睹
本章能解决什么
- 用 Canvas 绑图绑任意形状(圆、曲线、自定义图形)
- 用 ECh\n\n
\n\n
\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.data 和 series.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 个核心点
- Canvas 是「画板」:用
beginPath/moveTo/lineTo/arc/fill等指令绑图,最后draw()提交 - ECharts 是「图表工厂」:把数据塞进
option配置对象,它自动给你画成图表 - 两者组合能做数据大屏:Canvas 负责自定义图形,ECharts 负责标准图表,各取所长
延伸学习资源
- ECharts 官方示例库 — 超多可copy的示例
- uniapp Canvas 文档 — 官方 API 说明
- 《Python 数据分析》(pandas / matplotlib 部分)— 桌面端数据可视化的思路可以借鉴
互动钩子
「你做过数据可视化相关的项目吗?踩过什么坑?评论区聊聊,老粉优先回复!」
下章预告:学会了 Canvas 和 ECharts,是时候做个「真家伙」了——下一章我们综合实战,做一个可视化数据大屏,把食堂排队、成绩分析、步数统计全部串起来,做成一张可以展示的「驾驶舱」!🎯

评论(0)