最近一直在做鸿蒙开发,正好记录下。
interface Point {
x: number,
y: number
}
export class PointItem {
date: string
value: number
constructor(date: string, value: number) {
this.date = date
this.value = value
}
public PointItem(point: PointItem) {
this.date = point.date
this.value = point.value
}
}
@Component
export struct LineChart {
@State xTicks: String[] = [] // x轴显示的刻度
@State yTicks: number[] = [] // y轴显示的刻度值
private settings: RenderingContextSettings = new RenderingContextSettings(true)
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
@Prop canvasWidth: number // 画布的宽度
private canvasHeight = 300 // 画布的高度
private yWidth = 20 // y轴的文字宽度
private xHeight = 20 // X轴的文字高度
private points: Point[] = [] // 原始数量大小
private sps: any[] = [] // 平滑曲线的数量
private grayColor = '#ccc'
@State minY: number = this.xHeight // 对应的是y轴最小值
@Prop xGridCount: number // x轴网格线的数量
@Prop yGridCount: number // y轴网格线的数量
private drawInterval: number = -1; // 定时器
@State startIndex: number = 0 // 动画出现点
@State useAnimate: boolean = false // 是否使用动画
@State animateCount: number = 2 // 使用动画时 一个间隔时间内绘制的点或者线的数量
@State animateTimeGap: number = 100 // 使用动画时的时间间隔
@Prop smooth: boolean // 是否使用平滑曲线
@State logs: string = ""
private gap = (this.canvasWidth - this.yWidth) / data.length // 两个点之间的宽度 用来判断点击的范围是否在某个点内
@State scaleRatio: number = 2 // 缩放比例 最小1 最大
@State lastPoint: number = -1 // 点击了图表对应的x轴的位置 用于画垂直虚线
@Link clickPoint: PointItem
@State showAera: boolean = true // 是否显示面积图
@State aeraYBase: number = 0 // 面积图的基准,默认是最小值
aboutToAppear() {
let max = Math.ceil(Math.max(...data.map((d => d.value))) / 10) * 10
let min = Math.floor(Math.min(...data.map((d => d.value))) / 10) * 10
this.minY = min
this.aeraYBase = min
// 从上到下画 上大下小
for (let i = this.yGridCount - 1; i >= 0; i--) {
this.yTicks.push(min + i * ((max - min) / (this.yGridCount - 1)))
}
for (let i = 0; i < this.xGridCount - 1; i++) {
this.xTicks.push(data[Math.round(i * (data.length / (this.xGridCount - 1)))].date)
}
this.xTicks.push(data[data.length-1].date);
let xgap = (this.canvasWidth - this.yWidth) / data.length
let ratio = 0.1 // 最小代表多少值
let e = (max - min) / ratio // Y轴 按照值 分多少份
let ev = (this.canvasHeight - this.xHeight) / e // 每份代表多少多少坐标值
let nullIndexs = []
for (let i = 0; i < data.length; i++) {
if (data[i].value === null) {
nullIndexs.push(i)
}
this.points.push({
x: i * xgap + this.yWidth,
y: data[i].value === null ? null : (max - data[i].value) / ratio * ev
})
}
if (this.smooth) {
// 使用平滑曲线 计算平滑曲线的点
this.sps = bezier.catmullRom2bezier(this.points, false, null)
}
}
updateTime = () => {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasWidth)
this.drawXLine()
this.drawYLine()
this.drawGrid()
}
// 画网格
drawGrid() {
this.ctx.save()
this.ctx.fillStyle = this.grayColor
let h = (this.canvasHeight - this.xHeight) / (this.yGridCount - 1)
let w = (this.canvasWidth - this.yWidth) / (this.xGridCount - 1)
// 横向
for (let i = 0; i < this.yGridCount - 1; i++) {
this.ctx.beginPath()
this.ctx.fillStyle = colors[i]
this.ctx.moveTo(this.yWidth, h * i)
this.ctx.setLineDash([1, 5])
this.ctx.lineTo(this.canvasWidth, h * i)
this.ctx.stroke()
}
// 纵向
for (let i = 1; i < this.xGridCount; i++) {
this.ctx.beginPath()
this.ctx.fillStyle = colors[i]
this.ctx.moveTo(w * i + this.yWidth, 0)
this.ctx.setLineDash([1, 5])
this.ctx.lineTo(w * i + this.yWidth, this.canvasHeight - this.xHeight)
this.ctx.stroke()
}
this.ctx.restore()
}
private path2Db: Path2D = new Path2D()
// 画背景面积
drawAera() {
this.ctx.save()
this.ctx.beginPath()
let height = this.canvasHeight - this.xHeight
let grad = this.ctx.createLinearGradient(0, 0, 0, height)
grad.addColorStop(0.0, '#1890FF')
grad.addColorStop(1.0, '#f7f7f7')
this.ctx.fillStyle = grad
let start = 0
if (this.useAnimate) {
if (this.points[this.startIndex].y == null) {
start = this.startIndex + 1
this.path2Db.moveTo(this.points[start].x, this.points[start].y)
} else {
start = this.startIndex
this.path2Db.moveTo(this.points[this.startIndex].x, this.points[this.startIndex].y)
}
let tn = this.startIndex + this.animateCount
let total = Math.min(this.smooth ? this.sps.length : data.length, tn)
for (let i = this.startIndex + 1; i < total; i++) {
let p = this.points[i]
if (p.y === null) {
start = i + 1
this.path2Db.moveTo(this.points[start].x, this.points[start].y)
continue
}
this.path2Db.lineTo(p.x, p.y)
if (i + 1 < total - 1) {
if (this.points[i+1].y === null) {
this.path2Db.lineTo(p.x, height)
this.path2Db.lineTo(this.points[start].x, height)
continue
}
}
if (i === total - 1) {
this.path2Db.lineTo(p.x, height)
this.path2Db.lineTo(this.points[start].x, height)
}
}
} else {
this.path2Db.moveTo(this.points[0].x, this.points[0].y)
for (let i = 1; i < this.points.length; i++) {
let p = this.points[i]
if (p.y === null) {
start = i + 1
this.path2Db.moveTo(this.points[i+1].x, this.points[i+1].y)
continue
}
this.path2Db.lineTo(p.x, p.y)
if (i + 1 < this.points.length - 1) {
if (this.points[i+1].y === null) {
this.path2Db.lineTo(p.x, height)
this.path2Db.lineTo(this.points[start].x, height)
continue
}
}
if (i === this.points.length - 1) {
this.path2Db.lineTo(p.x, height)
this.path2Db.lineTo(this.points[start].x, height)
}
}
}
this.path2Db.closePath()
this.ctx.fill(this.path2Db)
this.ctx.restore()
}
// 画点
drawPoints() {
let radius = 2
this.ctx.save()
this.ctx.fillStyle = colors[0]
let total = 0
if (this.useAnimate) {
let tn = this.startIndex + this.animateCount
total = Math.min(this.smooth ? this.sps.length : data.length, tn)
} else {
total = this.smooth ? this.sps.length : data.length
}
for (let i = this.startIndex; i < total; i++) {
if (this.points[i].y === null) {
continue
}
this.ctx.beginPath()
this.ctx.arc(this.points[i].x, this.points[i].y, radius, 0, 2 * Math.PI, false)
this.ctx.fill()
}
this.ctx.restore()
}
// 画折线
drawLine() {
this.ctx.save()
this.ctx.beginPath()
this.ctx.lineWidth = 1
this.ctx.strokeStyle = colors[0]
this.ctx.moveTo(this.points[this.startIndex].x, this.points[this.startIndex].y)
let total = 0
if (this.useAnimate) {
let tn = this.startIndex + this.animateCount
total = Math.min(this.smooth ? this.sps.length : data.length, tn)
} else {
total = this.smooth ? this.sps.length : data.length
}
if (this.smooth) {
for (let i = this.startIndex; i < total; i++) {
if (this.points[i].y === null) {
++i
this.ctx.moveTo(this.points[i].x, this.points[i].y)
} else {
let sp = this.sps[i];
this.ctx.bezierCurveTo(sp[1], sp[2], sp[3], sp[4], sp[5], sp[6])
}
}
} else {
for (let i = this.startIndex; i < total; i++) {
if (this.points[i].y === null) {
i++
this.ctx.moveTo(this.points[i].x, this.points[i].y)
} else {
this.ctx.lineTo(this.points[i].x, this.points[i].y)
}
}
}
this.ctx.stroke()
this.ctx.restore()
}
// x轴
drawXLine() {
this.ctx.save()
this.ctx.beginPath()
this.ctx.lineWidth = 1
this.ctx.strokeStyle = this.grayColor
this.ctx.moveTo(this.yWidth, this.canvasHeight - this.xHeight)
this.ctx.lineTo(this.canvasWidth, this.canvasHeight - this.xHeight)
this.ctx.stroke()
this.ctx.font = '20px'
this.ctx.textAlign = "center"
this.ctx.textBaseline = "middle"
for (let i = 0; i < this.xTicks.length; i++) {
const xValue = this.xTicks[i];
let x = 0
if (i === 0) {
this.ctx.textAlign = "start"
x = this.yWidth
} else if (i === this.xTicks.length - 1) {
this.ctx.textAlign = "end"
x = this.canvasWidth
} else {
x = i * (this.canvasWidth - this.yWidth) / (this.xTicks.length - 1)
}
this.ctx.fillStyle = '#000'
this.ctx.fillText(`${xValue}`, x, this.canvasHeight - this.xHeight + 10)
}
this.ctx.closePath()
this.ctx.restore()
}
// y轴
drawYLine() {
this.ctx.save()
this.ctx.beginPath()
this.ctx.lineWidth = 1
this.ctx.strokeStyle = this.grayColor
this.ctx.moveTo(this.yWidth, 0)
this.ctx.lineTo(this.yWidth, this.canvasHeight - this.xHeight)
this.ctx.stroke()
this.ctx.font = '20px'
this.ctx.textAlign = "end"
this.ctx.textBaseline = "middle"
let maxWidth = this.ctx.measureText(`${this.yTicks[1]}`).width
for (let i = 0; i < this.yTicks.length; i++) {
const yValue = this.yTicks[i];
let y = 0
if (i === 0) {
y = 5
} else if (i === this.yGridCount) {
y = this.canvasWidth - this.xHeight
} else {
y = i * (this.canvasHeight - this.xHeight) / (this.yGridCount - 1)
}
this.ctx.fillStyle = '#000'
this.ctx.fillText(`${yValue}`, maxWidth, y)
}
this.ctx.restore()
}
// 绘制点击时数据的提示
drawToolTip(x: number) {
if (this.lastPoint !== -1) {
this.ctx.clearRect(this.lastPoint - 0.5, 0, 1, this.canvasHeight - this.xHeight)
}
this.ctx.save()
this.ctx.beginPath()
this.ctx.fillStyle = this.grayColor
// this.ctx.fillRect(0, 0, 100, 20)
this.ctx.lineWidth = 1
this.ctx.moveTo(x, 0)
this.ctx.setLineDash([1, 5])
this.ctx.lineTo(x, this.canvasHeight - this.xHeight)
this.ctx.stroke()
this.ctx.restore()
this.lastPoint = x
}
build() {
Column() {
Text(this.logs)
// Scroll() {
Stack({ alignContent: Alignment.Center }) {
Canvas(this.ctx)
.id("canvas")
.width(this.canvasWidth)
.height(this.canvasHeight)
.onReady(() => {
this.updateTime()
if (this.useAnimate) {
this.drawInterval = setInterval(() => {
this.drawLine()
if (this.showAera) {
this.drawAera()
}
this.drawPoints()
if (this.useAnimate) {
this.startIndex = this.startIndex + this.animateCount - 1
}
if (this.drawInterval !== -1 && this.startIndex >= data.length) {
clearInterval(this.drawInterval)
return
}
}, this.animateTimeGap)
} else {
this.drawLine()
if (this.showAera) {
this.drawAera()
}
this.drawPoints()
}
})
.onTouch((e: TouchEvent) => {
if (e.type == TouchType.Down || e.type == TouchType.Move) {
this.getCurrentPointData([e.touches[0].x, e.touches[0].screenX])
}
})
.backgroundColor(Color.Gray)
if (this.lastPoint !== -1) {
Line()
.startPoint([this.lastPoint, 0])
.endPoint([this.lastPoint, this.canvasHeight - this.xHeight])
.stroke(this.grayColor)
.strokeWidth(1)
.strokeDashArray([10, 3])
.strokeLineCap(LineCapStyle.Round)
}
}
// .backgroundColor(Color.Gray)
// }
// .layoutWeight(1)
}
}
aboutToDisappear() {
if (this.drawInterval !== -1) {
clearInterval(this.drawInterval)
}
}
getCurrentPointData(e: [number, number]) {
let index = -1
let distance = -1
for (let i = 0; i < this.points.length; i++) {
const element = this.points[i];
let dis = bezier.distance([e[0], 0], [element.x, 0])
if (dis <= this.gap) {
if (index === -1 || dis < distance) {
distance = dis
index = i
}
}
}
if (index !== -1) {
this.lastPoint = e[1] // 按照屏幕中的位置画线
}
}
}