前言
终于又到周末了,上一周的一篇3d文章 带你入门three.js——从0到1实现一个3d可视化地图很开心😺收到了这么多小伙伴的喜欢,这是对我知识输出的肯定。再次感谢大家!这周我又来了,这次给大家分享一下可视化图表比较简单的图表📈但同时我们又不得不学会的 那就是————「折线图」。读完本篇文章你可以学到什么
- js实现直线方程
- 折线图的表达
- canvas的一些api灵活的运用
直线折线图
我们先去非常有名的Echarts 官网看一看,他的折线图是什么样子的?如图:
echats折线图
从图中可以得到以下2d图形元素:
- 直线(两个端点是圆的)
- 直线(两个端点是直线的)
- 文字
好像仔细分析一下也没什么嘛,其实就是画直线和加文字。OK, 问下自己canvas如何画直线?是不是有一个ctx.LineTo的方法,但是他画出来的是直线没有端点的所以呢?我们以此基础进行封装,并且直线的端点的图形可控, 同时还有文字位于直线的位置是不是可以画出这样的图形呢?我们接下来进行实操环节。
画布的创建
第一步我们肯定是进行画布的创建,这里没什么好讲的。这里我在html 新建一个canvas, 我新建了一个类叫「lineChart」 直接上代码:
class lineChart { constructor(data, type) { this.get2d() } get2d() { const canvas = document.getElementById('canvas') this.ctx = canvas.getContext('2d') } }
上面代码没什么好讲的,然后我在为canvas 画布设置背景色。代码如下:
<style> * { padding: 0; margin: 0; } canvas { background: aquamarine; } </style>
canvas绘图操作复习
其实折线图,本质上就是一个画直线,只不过在原有画直线的能力上,给他做一些增强。我用一个画三角形的例子:带你熟悉一下画线操作。
先看下api:
lineTo(x, y)
绘制一条从当前位置到指定x以及y位置的直线。
直线一般是由两个点组成的,该方法有两个参数:x以及y ,代表坐标系中直线结束的点。开始点和之前的绘制路径有关,之前路径的结束点就是接下来的开始点,等等。。。开始点也可以通过moveTo()
函数改变。
moveTo 是什么就在画布中移动笔触, 也就是你开始画的第一个点,或者你可以想象一下在纸上作业,一支钢笔或者铅笔的笔尖从一个点到另一个点的移动过程。
moveTo(*x*, *y*)
将笔触移动到指定的坐标x以及y上。
介绍完毕, 开始实战环节:
drawtriangle() { this.ctx.moveTo(25, 25) this.ctx.lineTo(105, 25) this.ctx.lineTo(25, 105) }
我们先移动一个点, 然后再画条直线, 然后再画条直线。如果写到你认为结束了,你就错了
「你还差一个很重要的一步就是画布描边或者是填充, 我刚开始学也会忘记这个」。
这里給大家整理下canvas 的整个画图流程
- 首先,你需要创建路径起始点。
- 然后你使用画图命令去画出路径。
- 之后你把路径封闭。
- 「一旦路径生成,你就能通过描边或填充路径区域来渲染图形。」
也就是我们刚才所做的一切只是在准备路径,所以我们需要「描边」或者「填充」来渲染图形, 我们来看下这两个api。
// 通过线条来绘制图形轮廓。 ctx.stroke() // 通过填充路径的内容区域生成实心的图形。 ctx.fill()
我们把填充加上去:看下效果:
填充三角形
我们看下描边效果:
未闭合
你会发现为什么没有闭合?,代码是这样的:
this.moveTo(25, 25) this.lineTo(105, 25) this.lineTo(25, 105) this.stroke()
这就说明了一个重要问题就是什么呢?
描边是默认不闭合的,需要我们手动闭合 填充默认会帮我们闭合图形, 并且填充
既然发现了问题,我们就需要解决问题,那么canvas 如何闭合路径呢??
closePath:
闭合路径之后图形绘制命令又重新指向到上下文中。
代码如下:
this.moveTo(25, 25) this.lineTo(105, 25) this.lineTo(25, 105) this.closePath() this.stroke()
这时候效果图已经出来了:
闭合三角形
有closePath?难道没有开始路径?答案是当然有的:
// 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。 this.beginPath()
这里会问这个有什么作用呢?
首先 生成路径的第一步叫做beginPath()。本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形、等等)构成图形。而每次这个方法调用之后,列表清空重置,然后我们就可以重新绘制新的图形。
「注意:当前路径为空,即调用beginPath()之后,或者canvas刚建的时候,第一条路径构造命令通常被视为是moveTo(),无论实际上是什么。出于这个原因,你几乎总是要在设置路径之后专门指定你的起始位置。」
closePath 其实也不是必须的,如果图形已经是闭合的,就不需要调用, 到这里canvas的基本绘图操作复习就到这里,后面还有一些实战api : 我就例子中给大家讲解, 不然会显得很生硬。
封装画直线方法
再次之前,我把canvas中每一个点的位置都用一个point2d 点去表示并且写了一些方法,我在之前的文章都有仔细讲过这里我就不展开说了: 3千字长文canvas实现任意正多边形的移动(点、线、面) 这一篇文章。这里我就直接放上代码:
export class Point2d { constructor(x, y) { this.x = x || 0 this.y = y || 0 this.id = ++current } clone() { return new Point2d(this.x, this.y) } equal(v) { return this.x === v.x && this.y === v.y } add2Map() { pointMap.push(this) return this } add(v) { this.x += v.x this.y += v.y return this } abs() { return [Math.abs(this.x), Math.abs(this.y)] } sub(v) { this.x -= v.x this.y -= v.y return this } equal(v) { return this.x === v.x && this.y === v.y } rotate(center, angle) { const c = Math.cos(angle), s = Math.sin(angle) const x = this.x - center.x const y = this.y - center.y this.x = x * c - y * s + center.x this.y = x * s + y * c + center.y return this } distance(p) { const [x, y] = this.clone().sub(p).abs() return x * x + y * y } distanceSq(p) { const [x, y] = this.clone().sub(p).abs() return Math.sqrt(x * x + y * y) } static random(width, height) { return new Point2d(Math.random() * width, Math.random() * height) } cross(v) { return this.x * v.y - this.y * v.x } }
分别对应的是一些静态方法、叉乘、 两个点之间求距离哇等等。
我们先在画布上画一条基础的直线, 我们先用random, 在画布上重新生成两个点,然后画出一条随机的直线, 代码如下:
new lineChart().drawLine( Point2d.random(500, 500), Point2d.random(500, 500) ) // 画直线 drawLine(start, end) { const { x: startX, y: startY } = start const { x: endX, y: endY } = end this.beginPath() this.moveTo(startX, startY) this.lineTo(endX, endY) this.stroke() }
js实现直线方程
这里没有好展示的,我们还是分析下echarts 官方的折线图直线,直线两旁是两个圆的,想一想?其实这边涉及到一个数学知识,各位小伙伴,Fly再一次化身数学老师给大家讲解,主要是帮有些小伙伴复习复习。这里我们已经知道直线的开始点和结束点,在数学中我们可以确定一条直线方程,那么我们就可以求出直线上任意一点的(x,y)坐标。那么直线的两个端点的圆心我们就可以确定?半径也可以确定了就是圆心分别到开始点和结束点的距离。
第一步:实现直线方程
我们先看下直线方程的几种表达方式:
- 一般式:「Ax+By+C=0」(A、B不同时为0)【适用于所有直线】
- 点斜式:「y-y0=k(x-x0) 【「适用于不垂直于x轴的直线」】」 表示斜率为k,且过(x0,y0)的直线
- 截距式:「x/a+y/b=1」【适用于不过原点或不垂直于x轴、y轴的直线】
- 两点式:表示过(x1,y1)和(x2,y2)的直线 【适用于不垂直于x轴、y轴的直线】 「(x1≠x2,y1≠y2)」
两点式
这里很明显我们适合第四种:已经知道直线的起始点和结束点可以求出直线方程。我给出以下代码:
export function computeLine(p0, p1, t) { let x1 = p0.x let y1 = p0.y let x2 = p1.x let y2 = p1.y // 说明直线平行 y轴 if (x1 === x2) { return new Point2d(x1, t) } // 平行X轴的情况 if (y1 === y2) { return new Point2d(t, y1) } const y = ((t - x1) / (x2 - x1)) * (y2 - y1) + y1 return new Point2d(t, y) }
p0、p1、 对应的两个直线点 t 就是参数,对应直线的x,我们求出y,返回新的点就好了 。我们默认以开始点和结束点的 x 位置分别 减去或者加一个固定的值 , 求得圆心。直接看下图吧:
草稿图
这个图已经很明显了, 1和2 之间的距离就是半径, 所以我们只要求出点1 和点4 好像 就OK了, canvas 中是怎么画圆呢有一个arc 这个api :
arc(x, y, radius, startAngle, endAngle, anticlockwise)
画一个以(x,y)为圆心的以radius为半径的圆弧(圆),从startAngle开始到endAngle结束,按照anticlockwise给定的方向(默认为顺时针)来生成。
「注意:
arc()
函数中表示角的单位是弧度,不是角度。角度与弧度的js表达式:」
「弧度=(Math.PI/180)*角度。」
圆肯定就是从0-360度, 代码如下:
drawCircle(center, radius = 4) { const { x, y } = center this.ctx.beginPath() this.ctx.arc(x, y, radius, 0, Math.PI * 2, true) // 绘制 this.ctx.fill() }
准备工作都做好了, 我们就开始实现话带圆的直线吧。画图的步骤就是
- 先画开始圆
- 画直线
- 画结束圆
画开始圆和画结束圆其实可以封装成一个方法:他们最主要的区别其实就是起始点的不同,代码如下:
drawLineCircle(start, end, type) { const flag = type === 'left' const { x: startX, y: startY } = start const { x: endX, y: endY } = end const center = this.getOnePointOnLine( start.clone(), end.clone(), flag ? startX - this.distance : endX + this.distance ) // 两点之间的距离 不熟悉的小伙伴可以看下上面的文章 const radius = (flag ? start : end).clone().distanceSq(center) this.drawCircle(center, radius) }
这样我们就可以画圆了。先看下效果图:
直线两端圆点
到这里我们就已经结束了折线图的第一个部分, 紧接着进入第二部分: