【入门到精通】鸿蒙next开发:基于Canvas实现数据可视化:折线图/柱状图/饼状图/雷达图

往期鸿蒙5.0全套实战文章必看:(文中附带全栈鸿蒙5.0学习资料)


基于Canvas实现数据可视化:折线图/柱状图/饼状图/雷达图

背景介绍

在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场景一:折线图

效果图如下所示:

9.png

具体实现:

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场景二:实心柱状图

效果图如下所示:

10.png

具体实现:

柱状表格的实现与折线不同的是,柱形绘制使用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 场景三:绘制饼图

效果图如下所示:

11.png

具体实现:

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场景四:仿雷达图

效果图如下所示:

12.png

具体实现:

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")

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值