【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

陈杨
• 阅读 21

简介

大家好,我是陈杨。今天主要是分享一下McCharts组件库中的折线图实现过程,记录并分享自己的一些开发经验,感兴趣的可以互相学习。

McCharts组件库是基于鸿蒙ArkTS语法开发,支持API9以上的版本。图表组件已经开源了,大家可以一起参与进来,不管是新手还是大佬都可以,一起互相学习,开发出更多的组件库,丰富ArkTS的生态库。开源地址:https://github.com/Yuan-Mr/mcCharts

大家可以通过文章地址去看看,觉得不错的记得给开源项目点一个小星星哦。还可以加入交流群,互相学习。回归正题,

功能结构

实现一个公共组件的时候,我都会先分析一下大概的实现结构以及开发思路,方便我们少走一点歪路;也可以使组件更加容易拓展,维护性更强。不过我不会直接讲所有封装后的,我会把功能一个一个拆开来讲。这样大家才能学习到更多一点东西。下面我简单列一下折线图组件的功能结构:

· 公共属性

  1. 宽高

  2. 上下左右间距

  3. 字体大小

  4. 字体颜色

  5. 数据

· 绘画坐标轴

  1. 绘画X轴

  2. 绘画轴线

  3. 绘画分割线

  4. 绘画刻度线

  5. 绘画文本标签

  6. 绘画Y轴

  7. 绘画轴线

  8. 绘画分割线

  9. 绘画刻度线

  10. 绘画文本标签

· 绘画折线区

  1. 绘画折线

  2. 绘画标点、文本标签

这就是我大概画的一个功能结构了,可是说是比较简单的一些基础功能了,后续还有点击触发呀、动画呀等等功能也会规划进去。这一期我们先实现上面这些基础的功能,后续在慢慢拓展。

公共属性

一个组件肯定会有一些公共的属性作为动态参数,便于组件之间的信息传递,我们分别讲解一下五个公共属性的一些作用:

  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(() => {



      })

  }

}

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

绘画坐标轴

讲完画布基本的一些属性之后,我们开始绘画图表内容区吧。首先是绘画坐标轴,坐标轴分为X轴跟Y轴,我们要先开始画Y轴,为什么?因为我已经帮大家踩坑了,原因是:y轴上要显示文本标签,如果一开始没有得到文本标签对应的宽度最大值,那么Y轴跟X轴的起点坐标就会有偏差,会导致绘画全部错位。我先给大家看完整的坐标轴的效果

【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

绘画Y轴

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

【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

我用了一个500*500的矩形作为我们这次的画布,我们可以图上看到Y轴整体包含了文本标签、Y轴线、分割线、刻度线四个部分。了解canvas的都知道,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 // 每次进行最大值的匹配

        }

      })

}

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

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

   }

})

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

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

  }

})

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

4、绘画y轴线。接着就是我们的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轴

})

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)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);

  }

})

![转存失败,建议直接上传图片文件](<转存失败,建议直接上传图片文件 >)绘画X轴

绘画完Y轴之后,我们接着绘画X轴,其实X轴跟Y差不多,只是方向不一样而已。具体的算法我就不一一讲了,我画一下概览图,大家一看就知道了。

【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

大致算法是跟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();  
})

绘画折线区****

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

绘画折线****

老方法,先给大家画个概览图,好理解一点。

【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

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

【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

首先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();  
})

绘画标点 文本标签****

画完折线我们基本能得到很多东西,比如折线上每个转折点的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();  
})

最终效果如下:

【McCharts】基于鸿蒙ArkTS语法开发的图表组件--折线图

总结****

感谢大家的观看,这次技术分析就到此为止,希望大家能够学习到很多东西。也能自己开发想要开发的效果,后续我们会把chart相关系列的组件封装到组件库发布到市场上,这样可以直接开箱即用了。敬请期待吧,关注我;后续还有很多技术的分享,不要错过!!!!也可以技术交流交流。

点赞
收藏
评论区
推荐文章
使用 Taro 开发鸿蒙原生应用 —— 探秘适配鸿蒙 ArkTS 的工作原理
背景在上一篇文章中,我们已经了解到华为即将发布的鸿蒙操作系统,以及各个互联网厂商开展鸿蒙应用开发的消息。其中,Taro作为一个重要的前端开发框架,也积极适配鸿蒙的新一代语言框架——ArkTS。本文将深入探讨Taro适配鸿蒙ArkTS框架的工作原理,接下来我
爱学it学无止境 爱学it学无止境
8个月前
2024 鸿蒙零基础快速实战-仿抖音App开发( ArkTS版 )|完结
ArkTS:鸿蒙应用开发的高效利器ArkTS,作为HarmonyOS4.0优选的主力应用开发语言,正逐步成为鸿蒙生态系统中的核心开发工具。它基于TypeScript进行扩展,不仅继承了TypeScript的静态类型优势,还通过一系列创新设计,进一步提升了
少湖说 少湖说
4个月前
鸿蒙Flutter实战:06-使用ArkTs开发Flutter鸿蒙插件
使用ArkTs开发Flutter鸿蒙平台插件本文讲述如何开发一个Flutter鸿蒙插件,如何实现Flutter与鸿蒙的混合开发,以及双端消息通信。Flutter侧,编写MethodChanneldartconstMethodChannelmethodCha
京东云开发者 京东云开发者
4个月前
Taro 鸿蒙技术内幕系列(三) - 多语言场景下的通用事件系统设计
作者:京东零售朱天健基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景在鸿蒙生态系统中,虽然原生应用通常基于ArkTS实现,但在实际研发过程中发现,使用C可以显
陈杨 陈杨
4天前
开源啦!!!基于鸿蒙ArkTS封装的图表组件《McCharts》,大家快来一起共创
Hello;大家好,我是陈杨。好久没更新了,首先是自己本职工作比较忙,基本没时间写作。其次就是学习技术,自学鸿蒙ArkTS语言已经接近半年了,也算半路出师了,这次将分享我封装的组件库,所以有啥讲错的地方请大家高抬贵手,宽容一下,谢谢。这次主要是给大家带来一
陈杨 陈杨
4天前
McCharts 2.0来了,完美适配HarmonyOS NEXT最新版本,可轻松迁移Echarts图表项目
大家好,我是陈杨。终于有时间来分享一些技术文章了,自从McCharts组件上线第一期之后,就开始忙碌鸿蒙创新赛与极客马拉松比赛。在比赛的过程一直收到很多Issues,但是由于腾不出时间来维护,导致大家以为我们不维护了。在这里给大家说一声对不起。现在两个比赛
陈杨 陈杨
3天前
McCharts 2.0来了,完美适配HarmonyOS NEXT最新版本,可轻松迁移Echarts图表项目
大家好,我是陈杨。终于有时间来分享一些技术文章了,自从McCharts组件上线第一期之后,就开始忙碌鸿蒙创新赛与极客马拉松比赛。在比赛的过程一直收到很多Issues,但是由于腾不出时间来维护,导致大家以为我们不维护了。在这里给大家说一声对不起。现在两个比赛
陈杨 陈杨
4天前
分享之前使用HarmonyOS NEXT Canvas做的动态GIF视频的一个案例,没有感情,全是技术。
theme:fancyhello,大家好,我是莓创陈杨。最近忙着改图表组件的BUG,还有定制化开发一些图表。没啥时间写新东西,草稿里面放了十几个要实现的案例分享,欠的实在太多了,后面再慢慢还吧。这次分享一下之前使用HarmonyOSNEXTCanvas做的
陈杨 陈杨
4天前
【莓创Chart】基于鸿蒙原生ArkTS语法开发的图表组件--柱状图
大家好,我是陈杨。在上一篇文章中,我简要介绍了折线图的实现逻辑,并解释了整体图表的绘制规则。根据这些规则,我们还可以绘制更多种类的图表组件。在本期中,我将讲解如何实现柱状图,并引入了一个新的功能。鉴于柱状图跟折线图可共用的基础配置很多,我将不再重复介绍基础
陈杨 陈杨
4天前
鸿蒙原生绘图API:从基础到高阶的绘制之旅(基础版)
theme:hydrogen大家好,欢迎来到莓创IT技术分享频道,我是陈杨。由于经常有小伙伴一直给我反馈说莓创图表(mccharts)数据多的时候经常卡顿,很无奈之前做动画的时候没考虑ArkTs的性能瓶颈,导致现在又要重构开发。于是我重新翻阅文档,看看有没