我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
前言
在接触各种各样的需求后,总能碰到一些比较特殊、平常比较少接触的技术。例如统计展示、证劵类的图表等等。引入库虽然能够解决我们的问题,但是保不准那天要在库上面魔改,所以手动实现让自己多一个选择。学习了很多前辈的文章,今天我也来总结下我的心得~
效果展示
获取canvas实例
const canvas = document.querySelector('#canvas');
const ctx = canvas.getContext('2d');
读取数据并获取最大值和最小值
折线图的数据利用mock.js
随机生成。请求地址为https://mock.mengxuegu.com/mock/62f719a8f2652f239bd0a7d1/ds/antvRatio
function readRatio() {
fetch('https://mock.mengxuegu.com/mock/62f719a8f2652f239bd0a7d1/ds/antvRatio').then( res => {
return res.json();
}).then( res => {
const { data: { list } } = res;
let arr = list.reduce( (acc,item) => {
acc.push(item.value);
return acc;
},[]);
max = Math.max(...arr);
min = Math.min(...arr);
if (min > 0) min = 0;
if (max < 0 && min < 0) max = 0;
})
}
整理数据后利用Math
中的方法得到最大最小值,下面加了一个判断主要处理数据全正或全负的情况
- 如果最小值大于0则将最小值设为从0开始。
- 如果最大值以及最小值都是负的则将最大值设为从0开始。
计算刻度
画线咱只要调用lineTo
绘不就完了吗,算啥刻度啊?
ctx.beginPath();
ctx.moveTo(20,20);
ctx.lineTo(60, 120);
ctx.lineTo(80, 30);
ctx.stroke();
已知画布的宽高是80*120
咱可以这样写死绘制出线段,如果数据是灵活的超出了宽高那不是超出可视区域绘制了吗。假如给的数据是500,那么就要将500浓缩在80*120
的画布中。刻度的作用就是帮我们定位到数据在画布中的位置。
x轴的刻度
// 绘制宽度 / 数据总长度
let xScale = (width - marginLeft) / (data.length - 1);
y轴的刻度
// 绘制高度 / (最大值 - 最小值)
let yScale = (height - (marginTop + marginBot)) / (max - min);
绘制标签以及横线
function drawLabel(lineNum) {
const diff = (max - min) / (lineNum - 1);
ctx.textBaseline = "middle";
ctx.strokeStyle = '#F4F5F6';
for(let i = 0; i < lineNum; i++) {
let text = max - i * diff,
x = marginLeft - ctx.measureText(text).width,
y = marginTop + yScale * (max - text);
ctx.fillText(text, x, y);
drawLine(marginLeft + space, y, canvas.width, y);
}
}
function drawLine(startX, startY, endX, endY) {
ctx.save();
ctx.lineWidth = 1;
ctx.translate(0.5, 0.5);
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(endX, endY);
ctx.stroke();
ctx.restore();
}
- lineNum可以动态配置绘制几个标签以及几条线。
- 需要注意的是线和文字对齐只要设置
ctx.textBaseline = "middle"
即可。
如下图
绘制折线
function drawPoint() {
ctx.save();
ctx.textBaseline = "middle";
ctx.strokeStyle = 'orange';
ctx.lineWidth = 1;
ctx.translate(0.5, 0.5);
ctx.beginPath();
for(let i = 0; i < data.length; i++) {
let x = (marginLeft + space) + i * xScale,
y = marginTop + yScale * (max - data[i].value);
if (i == 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
ctx.restore();
}
beginPath
要在循环前设,这样才能连起来。- x、y的逻辑和绘制标签的一样。
绘制颜色
function drawBgColor() {
ctx.lineTo((marginLeft + space) + (data.length - 1) * xScale, canvas.height);
ctx.lineTo((marginLeft + space), canvas.height);
var gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, 'rgba(255,131,0,0.10)');
gradient.addColorStop(1, 'rgba(255,131,0,0.00)');
ctx.fillStyle = gradient;
ctx.fill();
}
上面虽然画出了折线图,但canvas本身并没有过渡效果,每一次绘制出来就不会发生变动。
如果想要实现折线动画,需要进行足够频繁的绘制就能达到动画效果。
具体实现参考路径动画🎨万物皆可动里面有详细的解说。
关键是把上一个点存起来,每次moveTo(prevX,prevY)
的时候都接到上一个点,配合requestAnimationFrame
达到连线动画。
三次贝塞尔曲线
最后
本文的源码在这,只是一个简单的示例DEMO。