手动实现Antv F2的折线图

1,319 阅读1分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

前言

在接触各种各样的需求后,总能碰到一些比较特殊、平常比较少接触的技术。例如统计展示、证劵类的图表等等。引入库虽然能够解决我们的问题,但是保不准那天要在库上面魔改,所以手动实现让自己多一个选择。学习了很多前辈的文章,今天我也来总结下我的心得~

效果展示

image.png

获取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"即可。

如下图

image.png

绘制折线

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的逻辑和绘制标签的一样。 image.png

绘制颜色

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();
}

image.png

上面虽然画出了折线图,但canvas本身并没有过渡效果,每一次绘制出来就不会发生变动。 如果想要实现折线动画,需要进行足够频繁的绘制就能达到动画效果。 QQ20220828-180254-HD.gif 具体实现参考路径动画🎨万物皆可动里面有详细的解说。
关键是把上一个点存起来,每次moveTo(prevX,prevY)的时候都接到上一个点,配合requestAnimationFrame达到连线动画。

三次贝塞尔曲线

image.png

最后

本文的源码在这,只是一个简单的示例DEMO。