背景介绍
在app开发中,经常需要使用到一些图表的开发,大多数我们会使用第三方的库直接实现,如果第三方没有提供我们想要的效果,这个时候修改起来就比较麻烦了;
本篇文章主要介绍基于canvas使用CanvasRenderingContext2D和Path2D相关API来实现折线图/饼状图/柱状图/雷达图。
CanvasRenderingContext相关API:
- 通过moveTo路径从当前点移动到指定点。
- 通过lineTo从当前点到指定点进行路径连接。
- 通过rect创建矩形路径。
- 通过stroke绘制线条。
- 通过fill绘制填充区域。
- 通过globalAlpha设置透明度。
- 通过font设置文字大小。
- 通过strokeColor设置线条(画笔)的颜色。
- 通过fillStyle设置填充的颜色。
- 通过textAlign设置文字对齐方式。
- 通过fillText绘制文字。
- 通过measureText获取文字尺寸。
- 通过arc圆弧绘制。
path2D相关API:
- 通过moveTo移动点(笔)。
- 通过lineTo画线。
- 通过closePath将路径的当前点移回到路径的起点。
- 通过stroke根据指定的路径,进行边框绘制操作。
2.1场景一:折线图
效果图如下所示:
具体实现:
1:绘制表格用到的属性如下,
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
private path2Db: Path2D = new Path2D()
// 画布的宽度
private canvasWidth = 350
//表格与画布的间距
private gridGap = 35
//X抽上每个表格的宽度
private gridWidth = 0
//Y抽上每个表格的宽度
private gridHeight = 0
//Y抽上每个表格的宽度
private y_List:string[] = ['0','10','20','30','40'];
//X抽上数据
private x_List:string[] = ['星期一','星期二','星期三','星期四','星期五','星期六','星期日'];
//折线图数据
private dataList:number[] = [13,23,21,33,3,17,6];
//折线图数据对应的坐标点
private positionList:PositionModel[] =[] ;
2:根据画布宽度canvasGap与展示表格的间距gridGap,以及X轴和Y轴对应的数据,可以计算出表格每一个网格的尺寸, 根据给定的折线图数据dataList,可以得到对应的PositionList存储每个绘制点对应的坐标集合。
aboutToAppear() {
//计算Y抽上每个表格的宽度
this.gridHeight = (this.canvasWidth - 2 * this.gridGap) / this.y_List.length
//计算X抽上每个表格的宽度
this.gridWidth = (this.canvasWidth - 2 * this.gridGap) / this.x_List.length
//计算折线图数据对应的坐标点
for (let index = 0; index < this.dataList.length; index++) {
let x = this.gridGap + this.gridWidth * index + this.gridWidth / 2
let y = this.canvasWidth - this.gridGap - this.dataList[index] / 10 * this.gridHeight;
let model = new PositionModel(x,y);
this.positionList.push(model)
}
}
3:根据Y抽方向给定的数组,使用CanvasRenderingContext的moveTo和lineTo绘制X抽对应的6条表格直线,
通过font设置文字大小,通过fillStyle设置文字颜色,通过textAlign设置文字对齐方式,再根据fillText传入展示的文本和起始坐标点来绘制文字。
//画X方向线条和Y抽对应的刻度和文字
public drawYLine(){
this.context.fillStyle = '#666666' //画笔填充颜色
//this.context.strokeStyle = '#666666' //画笔线条颜色
this.context.lineWidth = 2
//画X方向线条和Y抽对应的刻度和文字
for (let index = 0; index < this.y_List.length + 1; index++) {
this.context.beginPath()
this.context.moveTo(this.gridGap - 5, this.canvasWidth - this.gridGap - this.gridHeight * index)
this.context.lineTo(this.gridGap + this.x_List.length * this.gridWidth, this.canvasWidth - this.gridGap - this.gridHeight * index)
this.context.stroke()
this.context.font = '30px sans-serif'
this.context.fillStyle = '#333333'
this.context.textAlign = "right"
this.context.fillText(this.y_List[index],this.gridGap - 10, this.canvasWidth - this.gridGap - this.gridHeight * index + 3)
}
this.context.fillText('温度',this.gridGap + 15, this.gridGap - 5)
}
4:根据X轴方向给定的数组,使用CanvasRenderingContext的moveTo和lineTo绘制Y抽直线,以及绘制X抽分割线
通过font设置文字大小,通过fillStyle设置文字颜色,通过textAlign设置文字对齐方式,再根据fillText,传入展示的文本和起始坐标点来绘制文字。
//画Y方向线条//画X轴方向刻度和文字
public drawXLine(){
this.context.fillStyle = '#666666' //画笔填充颜色
//this.context.strokeStyle = '#666666' //画笔线条颜色
this.context.lineWidth = 2
//画Y方向线条
this.context.beginPath()
this.context.moveTo(this.gridGap, this.canvasWidth - this.gridGap)
this.context.lineTo(this.gridGap, this.canvasWidth - this.gridGap - this.gridHeight * this.y_List.length)
this.context.stroke()
//画X轴方向刻度和文字
for (let index = 0; index < this.x_List.length; index++) {
this.context.beginPath()
//2 是线条宽度
this.context.moveTo(this.gridGap + this.gridWidth - 2 + this.gridWidth * index, this.canvasWidth - this.gridGap )
this.context.lineTo(this.gridGap + this.gridWidth - 2 + this.gridWidth * index, this.canvasWidth - this.gridGap + 5)
this.context.stroke()
this.context.font = '30px sans-serif'
this.context.fillStyle = '#333333'
this.context.textAlign = "center"
this.context.fillText(this.x_List[index],this.gridGap - 2 + (index + 1) * this.gridWidth - 20, this.canvasWidth - this.gridGap + 15)
}
}
5:根据PositionList存储的坐标点,使用CanvasRenderingContext的moveTo和lineTo绘制折线图
通过font设置文字大小,通过fillStyle设置文字颜色,通过textAlign设置文字对齐方式,再根据fillText传入展示的文本和起始坐标点来绘制文字。
public drawChart() {
this.context.strokeStyle = 'rgba(20, 227, 60, 1.00)' //画笔线条颜色
for (let index = 0; index < this.dataList.length; index++) {
let model = this.positionList[index]
let x = model.position_x
let y = model.position_y
if (index == 0) {
this.context.moveTo(x, y)
}else {
this.context.lineTo(x, y)
}
}
this.context.stroke()
}
public drawValueInfo() {
this.context.font = '30px sans-serif'
this.context.fillStyle = '#ffdb2626'
this.context.textAlign = "center"
for (let index = 0; index < this.dataList.length; index++) {
let model = this.positionList[index]
this.context.fillText(this.dataList[index].toString(),model.position_x, model.position_y - 5)
}
}
2.2场景二:实心柱状图
效果图如下所示:
1.柱状表格的实现与折线不同的是,柱形绘制使用CanvasRenderingContext的rect(传入绘制的起始坐标X,Y,和对应size)以及fill来实现。
public drawChart() {
this.context.fillStyle = 'rgba(20, 227, 60, 1.00)' //画笔填充颜色
this.context.strokeStyle = 'rgba(20, 227, 60, 1.00)' //画笔线条颜色
for (let index = 0; index < this.dataList.length; index++) {
let model = this.positionList[index]
let x = model.position_x
let y = model.position_y
this.context.rect(x, y, 20, this.dataList[index] / 10 * this.gridHeight) // Create a 100*100 rectangle at (20, 20)
this.context.fill()
}
}
2.3 场景三:绘制饼图
效果图如下所示:
1:根据给定的数组,和对应的占比,再计算对应比例的角度大小,使用CanvasRenderingContext的arc绘制对应的圆弧。
const expense_categories = ['购物', '出行', '餐饮', '医疗', '美容', '娱乐', '教育', '房租']
// 画扇形
this.context.beginPath()
this.context.arc(centerX, centerY, arcRadius, startAngle, endAngle)
this.context.lineWidth = arcWidth
this.context.strokeStyle = color
this.context.stroke()
this.context.restore()
2:根据各扇形对应的中心点,和半径,以及三角函数math.sin,math.cos,得到折线的起始点
第三个点根据角度大小的判断,调整坐标点,
使用CanvasRenderingContext的moveTo和lineTo绘制各扇形对应的折线。
// 画折线
let centerAngle = startAngle + angle / 2
let r = radius + brokenLineLength / 2
let x1 = centerX + (r - brokenLineLength) * Math.cos(centerAngle)
let y1 = centerY + (r - brokenLineLength) * Math.sin(centerAngle)
let x2 = centerX + r * Math.cos(centerAngle)
let y2 = centerY + r * Math.sin(centerAngle)
let x3 = x2
let y3 = y2
if (centerAngle < Math.PI / 2) {
this.context.textAlign = 'right'
x3 = x2 + 15
} else {
this.context.textAlign = 'left'
x3 = x2 - 15
}
// 折线
let leaderLineColor = this.options.leaderLineColorFn(item, i)
this.context.beginPath()
this.context.lineWidth = brokenLineWidth
this.context.strokeStyle = leaderLineColor
this.context.moveTo(x1, y1)
this.context.lineTo(x2, y2)
this.context.lineTo(x3, y3)
this.context.stroke()
3.通过measureText获取文字的宽度,根据对应的角度,调整文本的起始点和对齐方式,
通过font设置文字大小,通过fillStyle设置文字颜色,通过textAlign设置文字对齐方式,再根据fillText,传入展示的文本和起始坐标点来绘制文字。
// 画文字
// 设置字体样式
const labelStyle = this.options.labelStyleFn(item, i)
this.context.textBaseline = 'middle'
this.context.fillStyle = labelStyle.fontColor
this.context.font = fp2px(labelStyle.fontSize) + 'px sans-serif'
// 获取文本
let label = this.options.labelFn(data[i], i)
let textWidth = this.context.measureText(label).width
let x4 = x3
let y4 = y3
if (centerAngle < Math.PI / 2) {
this.context.textAlign = 'right'
x3 = x2 + 15
x4 = x3 + textWidth + 3
} else {
this.context.textAlign = 'left'
x3 = x2 - 15
x4 = x3 - textWidth - 3
}
this.context.fillText(label, x4, y4)
2.4场景四:仿雷达图
效果图如下所示:
1:实现雷达图对应属性
//背景
private path2Db: Path2D = new Path2D()
//能力值展示
private ratePath2Db: Path2D = new Path2D()
// 计算正五边形的顶点坐标
private baseRadius:number = 150; // 设置半径
//画布半径
private canvasRadius:number = 200; // 设置半径
private angleOffset:number = (Math.PI * 2) / 6; // 计算每个顶点之间的角度间隔
// 圈数
private count:number = 5;
// 各能力值
private rateArray:number[] = [0.5,1.0,0.15,0.7,0.4,0.65];
//各能力名称
private nameList:string[] = ['推进','战绩','生存','团战','发育','输出'];
//各能力对应的坐标点
private positionList:PositionModel[] =[] ;
2:根据画布的中心点,以及雷达图的半径,用Math.sin(angle)和Math.cos(angle)计算出雷达图各个点所对应的坐标点,
用positionList存储
使用path2D的moveTo和lineTo绘制折线图,使用closePath闭合路径,stroke绘制边框,绘制5条不同半径的6边形。
//绘制背景
for (let index = 0; index < 6; index++) {
this.baseRadius = 150 - (index * 30)
const firstX = this.baseRadius * Math.sin(0) + this.canvasRadius;
const firstY = this.baseRadius * Math.cos(0) + this.canvasRadius;
if (index == 0) {
let firstModel = new PositionModel(firstX,firstY);
this.positionList.push(firstModel)
}
this.path2Db.moveTo(firstX, firstY)
for (let i = 1; i < 6; i++) {
const angle = i * this.angleOffset;
const x = this.baseRadius * Math.sin(angle) + this.canvasRadius;
const y = this.baseRadius * Math.cos(angle) + this.canvasRadius;
this.path2Db.lineTo(x,y);
if (index == 0) {
let model = new PositionModel(x,y);
this.positionList.push(model)
}
}
this.path2Db.closePath()
this.context.stroke(this.path2Db)
}
3.绘制能力对应的名字,positonList存储的坐标点,通过measureText获取文字尺寸,来调整各点文本对应的绘制位置,
使用CanvasRenderingContext的font设置文字大小,通过fillStyle设置文字颜色,通过textAlign设置文字对齐方式,再根据fillText,传入展示的文本和起始坐标点来绘制文字。
//绘制各坐标对应的名称
this.context.font = '50px sans-serif'
this.context.fillStyle = '#333333'
//可以根据文字得宽高来调整位置 demo里面没有用到
const textWidth = this.context.measureText('推进').width; // 获取文字的长度
const textHeight = this.context.measureText('推进').height; // 获取文字的长度
for(let i = 0; i < this.positionList.length;i++){
let model = this.positionList[i]
let name = this.nameList[i]
if (i == 0) {
model.position_x -= 15;
model.position_y += 20;
}else if (i == 1 || i == 2) {
model.position_x += 10;
model.position_y += 5;
}else if (i == 3) {
model.position_x -= 15;
model.position_y -= 8;
}else if (i == 4 || i == 5) {
model.position_x -= 40;
model.position_y += 5;
}
this.context.fillText(name, model.position_x, model.position_y)
}
})
4.根据给定的rateArray给出的能力值,根据画布的中心点,以及对应能力值雷达图的半径,用Math.sin(angle)和Math.cos(angle)计算出需要绘制雷达图各个点所对应的坐标点,
使用path2D的moveTo和lineTo绘制折线图,使用closePath闭合路径,
使用CanvasRenderingContext的stroke绘制边框,用fillStyle和globalAlhpa分别设置填充区域颜色和透明度,最后调用fill完成绘制。
//绘制能力值对应的路径
for (let index = 0; index < this.rateArray.length; index++) {
if (index == 0) {
let tempRadius:number = this.rateArray[index] * 125 + 25;
this.ratePath2Db.moveTo(tempRadius * Math.sin(0) + this.canvasRadius , tempRadius * Math.cos(0) + this.canvasRadius)
}else {
let tempRadius:number = this.rateArray[index] * 125 + 25;
const angle = index * this.angleOffset;
const x = tempRadius * Math.sin(angle) + this.canvasRadius;
const y = tempRadius * Math.cos(angle) + this.canvasRadius;
this.ratePath2Db.lineTo( x , y );
}
}
this.ratePath2Db.closePath()
this.context.stroke(this.ratePath2Db)
this.context.fillStyle = '#00ff00'
this.context.globalAlpha = 0.4
this.context.fill(this.ratePath2Db, "evenodd")
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。