微信小程序中如何繪制天氣折線圖?下面本篇文章就來給大家介紹一下在微信小程序中使用canvas繪制天氣折線圖的方法,以及使用三階貝塞爾曲線擬合溫度點,使之變得圓滑,曲線底部有背景色,希望對大家有所幫助!

折線
效果圖:

自定義組件 line-chart
<canvas type="2d" id="line" class="line-class" style="width:{{width}}px;height:{{height}}px" />Component({
externalClasses: ['line-class'],
properties: {
width: String,
height: String,
data: Array,
},
observers: {
width() {
// 這里監聽 width 變化重繪 canvas
// 動態傳入 width 好像只能這樣了..
const query = this.createSelectorQuery();
query
.select('#line')
.fields({ node: true, size: true })
.exec(res => {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
const width = res[0].width; // 畫布寬度
const height = res[0].height; // 畫布高度
console.log(`寬度: ${width}, 高度: ${height}`);
const dpr = wx.getSystemInfoSync().pixelRatio;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
// 開始繪圖
this.drawLine(ctx, width, height, this.data.data);
});
},
},
methods: {
drawLine(ctx, width, height, data) {
const Max = Math.max(...data);
const Min = Math.min(...data);
// 把 canvas 的寬度, 高度按一定規則平分
const startX = width / (data.length * 2), // 起始點的橫坐標 X
baseY = height * 0.9, // 基線縱坐標 Y
diffX = width / data.length,
diffY = (height * 0.7) / (Max - Min); // 高度預留 0.2 寫溫度
ctx.beginPath();
ctx.textAlign = 'center';
ctx.font = '13px Microsoft YaHei';
ctx.lineWidth = 2;
ctx.strokeStyle = '#ABDCFF';
// 畫折線圖的線
data.forEach((item, index) => {
const x = startX + diffX * index,
y = baseY - (item - Min) * diffY;
ctx.fillText(`${item}°`, x, y - 10);
ctx.lineTo(x, y);
});
ctx.stroke();
// 畫折線圖背景
ctx.lineTo(startX + (data.length - 1) * diffX, baseY); // 基線終點
ctx.lineTo(startX, baseY); // 基線起點
const lingrad = ctx.createLinearGradient(0, 0, 0, height * 0.7);
lingrad.addColorStop(0, 'rgba(255,255,255,0.9)');
lingrad.addColorStop(1, 'rgba(171,220,255,0)');
ctx.fillStyle = lingrad;
ctx.fill();
// 畫折線圖上的小圓點
ctx.beginPath();
data.forEach((item, index) => {
const x = startX + diffX * index,
y = baseY - (item - Min) * diffY;
ctx.moveTo(x, y);
ctx.arc(x, y, 3, 0, 2 * Math.PI);
});
ctx.fillStyle = '#0396FF';
ctx.fill();
},
},
});data 就是溫度數組,如 [1, 2, ...]
因為不知道溫度數值有多少個,因此這里的 width 動態傳入
有個小問題,就是寬度過大的話真機不會顯示...
// 獲取 scroll-view 的總寬度
wx.createSelectorQuery()
.select('.hourly')
.boundingClientRect(rect => {
this.setData({
scrollWidth: rect.right - rect.left,
});
})
.exec();<view class="title">小時概述</view>
<scroll-view scroll-x scroll-y class="scroll" show-scrollbar="{{false}}" enhanced="{{true}}">
<view class="hourly">
<view wx:for="{{time}}" wx:key="index">{{item}}</view>
</view>
<line-chart line-class="line" width="{{scrollWidth}}" height="100" data="{{temp}}" />
</scroll-view>這里寫 scroll-x 和 scroll-y,要不會出現絕對定位偏移的問題,也不知道為什么

.scroll {
position: relative;
height: 150px;
width: 100%;
}
.hourly {
display: flex;
height: 150px;
position: absolute;
top: 0;
}
.hourly > view {
min-width: 3.5em;
text-align: center;
}
.line { // 折線圖絕對定位到底部
position: absolute;
bottom: 0;
}這里使用絕對定位其實是想模擬墨跡天氣這種折線圖和每一天在一個塊內的效果,所以 hourly 要和 scroll-view 等高,canvas 需要定位一下
主要是不知道墨跡天氣怎么實現的,只能暫時這樣

三階貝塞爾曲線
效果圖

emmm,好像并不怎么圓滑
計算控制點
首先寫一個點類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}Canvas貝塞爾曲線繪制工具 (karlew.com)
http://wx.karlew.com/canvas/bezier/
通過上面這個網站可以知道三階貝塞爾曲線各個參數的意義

也就是使用 bezierCurveTo 的時候最后一個點是下一個點,前兩個是控制點
控制點的計算參考: 貝塞爾曲線控制點確定的方法 - 百度文庫
https://wenku.baidu.com/view/c790f8d46bec0975f565e211.html
濃縮一下就是

這里的 a 和 b 可以是任意正數
因此定義一個計算某點的控制點 A 和 B 的方法
/**
* 計算當前點的貝塞爾曲線控制點
* @param {Point} previousPoint: 前一個點
* @param {Point} currentPoint: 當前點
* @param {Point} nextPoint1: 下一個點
* @param {Point} nextPoint2: 下下個點
* @param {Number} scale: 系數
*/
calcBezierControlPoints(
previousPoint,
currentPoint,
nextPoint1,
nextPoint2,
scale = 0.25
) {
let x = currentPoint.x + scale * (nextPoint1.x - previousPoint.x);
let y = currentPoint.y + scale * (nextPoint1.y - previousPoint.y);
const controlPointA = new Point(x, y); // 控制點 A
x = nextPoint1.x - scale * (nextPoint2.x - currentPoint.x);
y = nextPoint1.y - scale * (nextPoint2.y - currentPoint.y);
const controlPointB = new Point(x, y); // 控制點 B
return { controlPointA, controlPointB };
}這里 scale 就是 a 和 b,不過將它們的取值相等
但是第一個點沒有 previousPoint,倒數第二個點沒有 nextPoint2
因此當點是第一個的時候,使用 currentPoint 代替 previousPoint
當倒數第二個點的時候,使用 nextPoint1 代替 nextPoint2

至于最后一個點,不需要做任何事,因為 bezierCurveTo 第三個參數就是下一個點,只需要提供坐標就能連起來,不需要計算控制點
因此繪制三階貝塞爾曲線的方法:
/**
* 繪制貝塞爾曲線
* ctx.bezierCurveTo(控制點1, 控制點2, 當前點);
*/
drawBezierLine(ctx, data, options) {
const { startX, diffX, baseY, diffY, Min } = options;
ctx.beginPath();
// 先移動到第一個點
ctx.moveTo(startX, baseY - (data[0] - Min) * diffY);
data.forEach((e, i) => {
let curPoint, prePoint, nextPoint1, nextPoint2, x, y;
// 當前點
x = startX + diffX * i;
y = baseY - (e - Min) * diffY;
curPoint = new Point(x, y);
// 前一個點
x = startX + diffX * (i - 1);
y = baseY - (data[i - 1] - Min) * diffY;
prePoint = new Point(x, y);
// 下一個點
x = startX + diffX * (i + 1);
y = baseY - (data[i + 1] - Min) * diffY;
nextPoint1 = new Point(x, y);
// 下下個點
x = startX + diffX * (i + 2);
y = baseY - (data[i + 2] - Min) * diffY;
nextPoint2 = new Point(x, y);
if (i === 0) {
// 如果是第一個點, 則前一個點用當前點代替
prePoint = curPoint;
} else if (i === data.length - 2) {
// 如果是倒數第二個點, 則下下個點用下一個點代替
nextPoint2 = nextPoint1;
} else if (i === data.length - 1) {
// 最后一個點直接退出
return;
}
const { controlPointA, controlPointB } = this.calcBezierControlPoints(
prePoint,
curPoint,
nextPoint1,
nextPoint2
);
ctx.bezierCurveTo(
controlPointA.x,
controlPointA.y,
controlPointB.x,
controlPointB.y,
nextPoint1.x,
nextPoint1.y
);
});
ctx.stroke();
},





