• HarmonyOS 实战开发-使用canvas实现图表系列之折线图


    一、功能结构

    实现一个公共组件的时候,首先分析一下大概的实现结构以及开发思路,方便我们少走弯路,也可以使组件更加容易拓展,维护性更强。然后我会把功能逐个拆开来讲,这样大家才能学习到更详细的内容。下面简单阐述下折线图组件的功能结构:

    以上是基础的功能结构框架,包含一些比较简单的基础功能,后续还有点击触发、动画等功能也会规划进去。这一期我们先实现上面这些基础的功能,后续再慢慢拓展。

    二、公共属性

    1. 一个组件肯定会有一些公共的属性作为动态参数,便于组件之间的信息传递,我们分别讲解一下五个公共属性的作用:画布的宽度(cWidth)和高度(cHeight),这个是最基本的。但是我这里控制是非必传,默认值都是 100%就可以了。
    2. 画布的内部留白间距(cSpace)。主要是用来控制内容区与画布外框的距离,避免绘画的内容被截掉。
    3. 字体大小(fontSize)。主要是来控制整个绘画内容的字体大小,全局性,避免每个小功能都需要传字体大小。
    4. 字体颜色(color)。与字体大小的功能一致。
    5. 图表数据(data)。用来存储图表内容的数组,其中 name 与 value 是必传的。

    以下是具体的代码:

    // 图表数据的特征接口
    interface interface_data {
      name: string | number;
      value: string | number;
      [key: string]: any;
    }
    
    // 图表的特征接口
    interface interface_option {
      cWidth?: string | number,
      cHeight?: string | number,
      fontSize?: string | number,
      color?: string,
      cSpace?: number,
      data?: interface_data[]
    }
    
    // option 默认值
    const def_option: interface_option = {
      cWidth: '100%',
      cHeight: '100%',
      fontSize: 10,
      color: '#333',
      cSpace: 20,
      data: []
    }
    
    @Component
    export struct McLineChart {
      private settings: RenderingContextSettings = new RenderingContextSettings(true)
      private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
      @State options: interface_option = {}
      aboutToAppear() {
        this.options = Object.assign({}, def_option, this.options)
      }
      build() {
        Canvas(this.context)
          .width(this.options.cWidth)
          .height(this.options.cHeight)
          .onReady(() => {
    
          })
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    三、绘画坐标轴

    绘画图表内容区部分,首先是绘画坐标轴,坐标轴分为 X 轴跟 Y 轴,我们要先开始画 Y 轴,原因是:y 轴上要显示文本标签,如果一开始没有得到文本标签对应的宽度最大值,那么 Y 轴跟 X 轴的起点坐标就会有偏差,会导致绘画全部错位,下图是完整的坐标轴的效果。

    1.绘画 Y 轴

    Y 轴整体是由轴线、分割线、刻度线、文本标签四个部分组成的,四个部分都有先后关系,而且包含一定的算法逻辑,下面简单用一个概念图进行讲解。

    首先用 500*500 的矩形作为我们这次的画布,我们可以在图上看到 Y 轴整体包含了文本标签、Y 轴线、分割线、刻度线四个部分。而 canvas 绘画基本都是通过坐标来定位的,Y 轴整体的四个部分的起点与结束坐标都互相有关系,甚至需要把内部间距、分割间距、y 轴线高度、文本最大的宽度四个属性计算在内。以上是概念与思路,接下来我们逐一讲解代码:

    1、计算得到文本最长宽度(maxNameW ),我们可以从图中看到,不论是 y 轴线、刻度线还是分割线的起点坐标都是需要内容间距、文本标签、文本标签与分割线间隔相加计算得到,而为了保持对齐,所以我们需要计算出文本最长宽度。而 y 轴的文本一般都是数据(data)对应的数值,所以我们需要得到传入数据(data)中的最大值。然后讲最大值分割成五等分。以下就是计算获取最大文本宽度的代码,部分逻辑我也会写在代码上:

    build() {
        Canvas(this.context)
          .width(this.options.cWidth)
          .height(this.options.cHeight)
          .backgroundColor(this.options.backgroundColor)
          .onReady(() => {
            const values: number[] = this.options.data.map((item) => Number(item.value || 0))
            const maxValue = Math.max(...values)
            let maxNameW = 0
            let cSpiltNum = 5 // 分割等分
            let cSpiltVal = maxValue / cSpiltNum // 计算分割间距
            for(var i = 0; i <= this.options.data.length; i++){
              // 用最大值除于分割等分得到每一个文本的间隔值,而每一次遍历用间隔值乘于i就能得到每个刻度对应的数值了,计算得到得知需要保留整数且转成字符串
              const text = (cSpiltVal * i).toFixed(0)
              const textWidth = this.context.measureText(text).width; // 获取文字的长度
              maxNameW = textWidth > maxNameW ? textWidth : maxNameW // 每次进行最大值的匹配
            }
          })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2、绘画文本标签,我们可以从图中看到文本标签的 x 坐标只跟内部间距有关,而且我们从上面代码就已经得到每个刻度的分割间距了,从而可以得到每个文本的 y 轴。

    .onReady(() => {
       ....
       for(var i = 0; i <= this.options.data.length; i++){
         ...
         // 绘画文本标签
         this.context.fillText(text, this.options.cSpace, cSpiltVal * (this.options.data.length - i) + this.options.cSpace , 0);
       }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3、绘画刻度线。我们可以从概念图得到,刻度线的起点 x 坐标算法是:内部间距(cSpace)加最长文本宽度(maxNameW )加上文本与刻度线的间距,起点 y 坐标则跟文本一样,通过分割间距与下角标的关系得到每个刻度的 y 坐标;而终点 x 坐标则是刻度线的长度,终点 y 坐标则跟起点的 y 坐标一样,我设置默认长度是 5,这样就能得到我们的刻度线了。代码如下:

    .onReady(() => {
      ....
      const length = this.options.data.length
      for(var i = 0; i <= length; i++){
        ...
      }
      // 上面是获取最长文本宽度的代码
      // 画线的方法
      function drawLine(x, y, X, Y){
        this.context.beginPath();
        this.context.moveTo(x, y);
        this.context.lineTo(X, Y);
        this.context.stroke();
        this.context.closePath();
      }
      for(var i = 0; i <= length; i++){
        const item = this.options.data[i]
        // 绘画文本标签
        ctx.fillText(text, this.options.cSpace,  cSpiltVal * (this.data.length - i) + this.options.cSpace, 0);
        // 内部间距+文本长度
        const scaleX = this.options.cSpace + maxNameW
        // 通过数据最大值算出等分间隔,从而计算出每一个的终点坐标
        const scaleY = cSpiltVal * (length - i) + this.options.cSpace
        // 这里的5就是我设置文本跟刻度线的间隔与刻度线的长度
        drawLine(scaleX, scaleY, scaleX + 5 + 5, scaleY);
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    4、绘画 y 轴线。继续分析概览图,从图中我们可以得到:y 轴线的起点 x 坐标的算法是:内部间距(cSpace)加最长文本宽度(maxNameW )加上文本与刻度线的间距以及刻度线长度,起点 y 坐标则是内部上间距;而终点 x 坐标与起点 x 坐标相同,终点 y 坐标算法是:画布高度减去上下两边的内部间距。通过以上计算关系就能绘画出 y 轴线了。代码如下:

    .onReady(() => {
      ...
      // 上面是绘画其他组成部分代码
       const startX = this.options.cSpace + maxNameW + 5 + 5
       const startY = this.options.cSpace
       const endX = startX
       const endY = this.context.height - (this.options.cSpace * 2)
       drawLine(startX, startY, endX, endY); // 绘画y轴
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    5、绘画分割线。其实从图中可以看出分割线与刻度线差不多,起点 x 坐标算法是:在刻度线起点 x 坐标基础上加刻度线长度;起点 y 轴与刻度线相同。而终点的 x 坐标算法:画布宽度减去起点 x 坐标;终点的 y 坐标与起点的 y 坐标相同。具体代码如下:

    .onReady(() => {
      ....
      // 上面是获取最长文本宽度的代码
      for(var i = 0; i <= length; i++){
        const item = this.options.data[i]
        // 绘画文本标签跟刻度
        ...
        // 绘画分割线
        const splitX = scaleX + 5 + 5
        const splitY = scaleY
        drawLine(splitX, splitY, this.context.width - splitX - this.options.cSpace, splitY);
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2.绘画 X 轴

    绘画完 Y 轴之后,我们接着绘画 X 轴, X 轴与 Y 轴绘画逻辑一致,只是方向不同而已。具体的算法就不一一详解,可以参考一下概念图。

    而与绘画 Y 轴不一致的在于:

    1. 最长对象不一样。Y 轴最长是文本宽度;而 X 轴需要获取的最长是文本高度。
    2. 间隔分割数不一样。Y 轴是自定义的分割数;而 X 轴分割线是实际数据的长度。
    3. 分割间距长度算法不一样。Y 轴算法是用数据最大值处于自定义的分割数;而 X 轴算法是用画布宽度减去(左右两边的内部间隙以及 Y 轴宽度(文本最长宽度加上刻度线宽度)),再除去数据的长度,得到每个间隔的长度。

    除了上面三点需要注意的,其他的就是调换一下计算的位置。X 轴整体的代码如下:

    .onReady(() => {
    
      const cSpace = this.options.cSpace
    
      // 上面是绘制y轴的代码
      ....
    
      // 绘制x轴
    
      // 获取每个分割线的间距:this.context.width - 20为x轴的长度
    
      let xSplitSpacing = parseInt(String((this.context.width - cSpace * 2 - maxNameW) / this.options.data.length))
    
      let x = 0;
    
      for(var i = 0; i <= this.options.data.length; i++){
    
        // 绘画分割线
    
        x = xSplitSpacing * (i + 1) // 计算每个数值的x坐标值
    
        this.drawLine(x + cSpace + maxNameW, this.context.height - cSpace, x + cSpace + maxNameW, cSpace);
    
        // 绘制刻度
    
        this.drawLine(x + cSpace + maxNameW, this.context.height - cSpace, x + cSpace + maxNameW, this.context.height - cSpace);
    
        // 绘制文字刻度标签
    
        const text = this.options.data[i].name
    
        const textWidth = this.context.measureText(text).width; // 获取文字的长度
    
        // 这里文本的x坐标需要减去本身文本宽度的一半,这样才能居中显示, y坐标这是画布高度减去内部间距即可
    
        this.context.fillText(text, x + cSpace + maxNameW - textWidth / 2, this.context.height - cSpace, 0);
    
      }
    
    this.context.save();
    
      this.context.rotate(-Math.PI/2);
    
      this.context.restore();
    
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    四、绘画折线区

    绘画完坐标轴之后,就可以来绘画折线区的内容了。也是整个画布重点的部分。折线区分为三个部分:绘画折线、绘画标点、绘画文本。

    1.绘画折线

    从上面的图可以看出折线直接就是把实际数据的数值转成 x 跟 y 坐标,再通过连线连接起来。而每一个转折点的 x 坐标算法跟 x 轴的刻度或者文本是一样的,而 y 坐标是实际数值通过一定算法转成我们需要的高度。x 坐标我们已经获取了,只要是攻克我们的 y 坐标即可。可以通过图来观察一下在画布中与实际数据的关系:

    首先 Y 轴的高度代表的是实际数据的最大值,这个我们绘画 Y 轴的时候就得到的结果,那我们则可以算出 Y 轴高度与实际数据的缩放倍数(scale),而折线的的每个 y 坐标对应的也是实际数值,需要把实际数值转换成画布中高度,那么就用实际数值乘与刚刚得到的缩放倍数(scale)就能得到转化后的高度了。

    虽然我们已经得到每个转折点缩放后的高度,但是如果要跟 Y 轴坐标一一对应的 y 坐标的画,还需要用画布的高度减去下边内部高度加 x 轴高度,再减去缩放后的实际高度。这样算出来的才是我们想要的 y 坐标值,大概算法关系已经知道了,以下是最终代码:

    .onReady(() => {
      ...
      // 上面是绘制x轴跟y轴的代码
      // 绘画折线
      const ySacle = (this.context.height - cSpace *2) / maxValue // 计算出y轴与实际最大值的缩放倍数
      //连线
      this.context.beginPath();
      for(var i=0; i< this.options.data.length; i++){
        const dotVal = String(this.options.data[i].value);
        const x = xSplitSpacing * (i + 1) + cSpace + maxNameW // 计算每个数值的x坐标值
        const y = this.context.height - cSpace - parseInt(dotVal * ySacle); // 画布的高度减去下边内部高度加x轴高度,再减去缩放后的实际高度
        if(i==0){
          // 第一个作为起点
          this.context.moveTo( x, y );
        }else{
          this.context.lineTo( x, y );
        }
      }
      ctx.stroke();
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.绘画标点、文本标签

    画完折线我们基本能得到很多东西,比如折线上每个转折点的 x 跟 y 坐标值。这样对我们绘画标点与文本标签就很方便了:

    .onReady(() => {
      ...
      // 上面是绘制x轴跟y轴的代码
      // 绘画折线
      const ySacle = (this.context.height - cSpace *2) / maxValue // 计算出y轴与实际最大值的缩放倍数
      this.context.beginPath();
      for(var i=0; i< this.options.data.length; i++){
        // 绘画折线代码
        ...
        // 绘制标点
        drawArc(x, y);
        // 绘制文本标签
        const textWidth = this.context.measureText(dotVal).width; // 获取文字的长度
        const textHeight = this.context.measureText(dotVal).height; // 获取文字的长度
        this.context.fillText(dotVal, x - textWidth / 2, y - textHeight / 2); // 文字
      }
    
      function drawArc( x, y ){
        this.context.beginPath();
        this.context.arc( x, y, 3, 0, Math.PI*2 );
        this.context.fill();
        this.context.closePath();
      }
      this.context.stroke();
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    最终效果如下:

    五、总结

    以上是本次技术分析,希望能对大家有所启发

    鸿蒙全栈开发全新学习指南

    也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线[包含了大APP实战项目开发]。

    本路线共分为四个阶段:

    第一阶段:鸿蒙初中级开发必备技能

    第二阶段:鸿蒙南北双向高工技能基础:https://qr21.cn/Bm8gyp

    第三阶段:应用开发中高级就业技术

    第四阶段:全网首发-工业级南向设备开发就业技术:https://qr21.cn/Bm8gyp

    《鸿蒙 (Harmony OS)开发学习手册》(共计892页)

    如何快速入门?

    1.基本概念
    2.构建第一个ArkTS应用
    3.……

    开发基础知识:https://qr21.cn/Bm8gyp

    1.应用基础知识
    2.配置文件
    3.应用数据管理
    4.应用安全管理
    5.应用隐私保护
    6.三方应用调用管控机制
    7.资源分类与访问
    8.学习ArkTS语言
    9.……

    基于ArkTS 开发

    1.Ability开发
    2.UI开发
    3.公共事件与通知
    4.窗口管理
    5.媒体
    6.安全
    7.网络与链接
    8.电话服务
    9.数据管理
    10.后台任务(Background Task)管理
    11.设备管理
    12.设备使用信息统计
    13.DFX
    14.国际化开发
    15.折叠屏系列
    16.……

    鸿蒙开发面试真题(含参考答案):https://qr21.cn/Bm8gyp

    鸿蒙入门教学视频:

    美团APP实战开发教学:https://qr21.cn/Bm8gyp

    写在最后

    • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
    • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
    • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
    • 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:https://qr21.cn/FV7h05

  • 相关阅读:
    数字菜场:智慧农贸大屏可视化大数据管理系统
    《乔布斯传》英文原著重点词汇笔记(十)【 chapter eight】
    分析Python爬虫设计
    PageHelper关联查询 统计总数问题
    IO作业:readdir、closedir、opendir、要求输入目录的路径后,能够打印出指定路径下所有文件的详细信息,类似ls -l
    java版直播商城免费搭建平台规划及常见的营销模式+电商源码+小程序+三级分销+二次开发
    解决Github Markdown图片显示残缺的问题
    DRF的认证组件(源码分析)
    使用枚举 代替简单工厂的switch或者if else
    微电网优化调度|农村农业区可再生能源微电网优化调度(Python代码实现)
  • 原文地址:https://blog.csdn.net/m0_71524094/article/details/138219232