鸿蒙开发折线图

最近一直在做鸿蒙开发,正好记录下。

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] // 按照屏幕中的位置画线
    }
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345