一步一步来做一个Compose版的折线图组件

最近在尝试着做一些常用的图表组件,在上一篇文章中已经用Compose做了一个具备基础功能的饼图组件,那么今天我们继续来使用Compose做一个折线图组件

源码地址

需要实现的功能

在做之前我们来想一下,回忆一下平时见过的折线图组件都有哪些功能,是不是有几下几点

  1. 有x,y轴,x轴表示维度里面具体有哪几项,会将x轴等分,y轴表示一个个区间,区间的具体数据会根据所有数据的最大值改变而改变
  2. x轴会有文字介绍,表明x轴展示的是什么数据,同样y轴也会有文字介绍,表明y轴展示的是什么数据
  3. 在y轴上每一个区间刻度都会延伸出一条平行于x轴的直线,用处是清晰的展示折线图某一个节点位于哪个数值区间。
  4. 折线图内部某个维度的各个数据都用圆点表示,相邻数据的圆点之间用直线连接,圆点之间的连接需要自带动画效果
  5. 每个维度使用不一样的颜色来画折线
  6. 折线图顶部会有对应维度名称以及代表的折线颜色,名称与颜色的数量会随着折线数量的变化而改变

就想出来这些,可能还有别的功能,知道的可以在评论区告诉我一下,我给你们做,现在开始敲代码

准备数据

在上一篇文章中就说过,希望做出来的图表组件之间可以依靠一套数据实现互相切换展示,所以数据方面延用了上一篇,也就是数据类继续使用PieData,只是因为一些与饼图组件上的差异,我们在PieData里面新增两个字段,一个是lineColor,表示折线的颜色,另一个是groupName,表示当前折线所属维度的名称

现在我们使用PieData来创建几组数据,来模拟几个公司在近五年的营业额,数据如下

尽管数据封装的有点不规范,相同的元素其实可以拎出来,但是不影响我们来做这个折线组件

绘制x,y轴

创建个函数名叫LineChart,内部接收两个参数,一个是我们的数据源,另一个是Modifier

接着我们来做一件事情,来将整个画布分成n * n的若干个小格子,这么做的原因是由于我们绘制折线图的时候,必然会牵扯到获取坐标点Offset,所以为了方便管理这些坐标点,会将所有格子的交叉点的x,y坐标分别管理在两个数组中,这样就可以通过计算下标值从两个数组中获取任意的坐标点,比如这里将画布分成300 * 300个小格子,代码如下

Canvas中获取了整个画布的大小mSize,通过mSize可以得到了画布的宽高,我们使用得到的宽高除上count就得到了xy轴上的每个小格子的尺寸xUnityUnit,然后创建两个大小都是countxListyList,两个数组里面分别存放着画布上所有点的x,y坐标,以上都是准备工作,下面开始绘制x,y轴的两条直线,我们都知道折线图的x轴在图的最下方,y轴在最左侧,但是我们在画布里面可不能画在最边上,毕竟一个原因是不好看,另一个是后面还要把年份和营业额写在x,y轴上,所以适当的在画布四周留上个空白,比如20个格子

有了这些以后,我们xy轴的两条直线就能绘制出来了

两条直线分别使用两个drawLine函数给绘制出来了,我们可以看到两个函数内部的startend属性,都是通过计算得出下标值,然后从xListyList中取出对应的值,我们后面绘制其他元素也都是通过这种方式来获取坐标,比如现在我想平分x轴,在每个平分的位置上绘制个圆点刻度,在圆点下方将对应的年份绘制上去,那么首先需要先知道隔开多少距离画一个点,这个距离通过x轴的长度除上每个维度的数组大小得出

后面减去5个格子的目的是让最后一个点跟x轴的末尾有一点距离,有了这个值以后,就能计算出每个需要绘制的圆点刻度与年份的坐标点的下标值了,代码如下

现在运行一遍代码,就能看到我们的xy轴以及x轴上的圆点刻度和年份

同样的方式我们在y轴上也将平分用的圆点刻度和每个刻度对应的平行于x轴的直线绘制出来,代码如下

yLineUnit就是y轴上每个刻度之间的距离,计算逻辑与x轴上的圆点基本相同,看下效果

第一个难点:y轴上的值

目前为止还算简单,接下来要遇到第一个难点,也就是y轴上每个刻度对应的值该怎么计算,要知道针对所有维度里面的数据,如果y轴上的数值过大,那么折线图绘制的区域会比较偏下,甚至与x轴基本贴近,如果y轴上的值过小,那么折线图很有可能有一部分是绘制不出来的,甚至是都绘制不出来,所以只有让y轴上的值跟着所展示的维度的值动态变化,才能达到折线图永远都绘制在画布的中间位置,为了做到这一点,首先需要做到的是找到所有维度里面的最大值,这里需要用到Comparator比较器,在比较之前先要将所有数据装到同一个List里面去

这里将所有数据都装到totalList里面去之后,就需要对比所有PieData里面的amount属性,找出拥有最大值amountPieData,最后将这个最大的amount取出来,代码如下

有了最大值max,是不是就可以把这个max作为我们y轴最上面刻度的值了呢,不是的,因为通常这样的刻度都是为整数,比如100,1000,1500之类的,所以我们获得了max之后,就需要计算一下比max稍微大一些的整数是多少,这里我就想不到什么比较好的方法了,只能用个笨方法,维护个大小是100的数组,里面放着100个整数,大小分别都是各自下标乘以100,当然乘之前下标可以加一,防止第一个数字为0,最后在这个数组中过滤出所有比max大的整数,取第一个作为我们y轴最顶部的刻度就好了,看代码

获得的topNum就是y轴上最顶部的刻度值了,至于y轴上任意一个刻度值都可以用下面这个公式计算出来

it就是下标值,最终完整的绘制y轴上的刻度的代码如下所示

运行一下代码看看效果

可以看到最顶部的值是1900,而我们所有数据里面最大的amount的确是公司D里面2020年的营业额1844.18,为了验证这个y轴是不是可以动态改变,我们改变一下数据源里面的值,把公司D的数据去掉,看看y轴的值是什么样的

可以看到去掉公司D的数据之后,最大的amount变成了公司C里面2023年的营业额1141.96,所以y轴最上面的刻度变成了1200,证明我们的y轴上的值的确可以动态改变,最后把x,y轴上的文字描述写上去,这两个坐标很好找,基本在绘制x,y轴的直线的时候点就已经有了,我们只需要稍作调整一下就行,这里就直接上代码了

绘制折线

接下来就是主要部分了,折线部分的绘制,折线本身就是每一个数据所代表的点连接而成,所以首先需要将这些点的坐标找出来,老规矩我们将这些点的x,y坐标的下标值分别维护在两个数组中,那么维护x坐标下标值的数组里面的数据很容易初始化,因为他们就是我们x轴上那几个刻度的横坐标,代码如下

pList就是维护着折线图上所有数据代表的点的x坐标的下标值,至于y坐标就要费点脑细胞计算一下,首先我们需要计算出以xy轴原点作为起点,往上第几格是代表着对应的数据,那么需要转换一下,使用总长度5 * yLineUnit去乘以每一个数据的amount值与topNum的比例,就得到对应的格子数,然后用count-paddingCount-计算出来的格子数,就可以获得y坐标的下标,代码如下

注意这里的pListXpListY维护的只是下标,并不是绘制时候需要用到的坐标,真正的坐标到时候还要从pListXpListY取出下标后从xListyList中获取,这里尝试一下将所有圆点绘制出来,代码就是这样的

那么现在我们的画布上面所有数据代表的点已经都绘制出来了

有了点以后,现在就要把这些点连在一起组成我们要的折线,在不要求动画的前提下,我们可以创建Path将这些点连在一起,最终绘制Path就好了

最终得到的效果图里面我们的折线图已经出来了

但是这个肯定不是我们最终想要的,我们还需要让折线图出来的时候,有个从第一个点逐渐连线至第二个点,然后慢慢将所有点连起来的过程,明显使用drawPath这个函数不能实现我们这个需求,得找找其他办法

第二个难点:让折线动起来

drawPath既然不行那么只能使用drawLine了,但是用了那么久的drawLine也没听过怎么让drawLine动起来啊,基本也是靠着start点和end点绘制出来的一根静态线,琢磨了半天最终想到个办法,虽然startend之间是一根静态线,但是如果我们让end点每个单位时间不一样,那么最终呈现的效果不就是这根线在逐渐变化吗,至于如何变化,那肯定是让end的x,y坐标点分别使用animateFloatAsState函数来计算,这里再创建个数据类LineModel用来封装绘制每一根直线需要用到的数据

新建这么个LineModel的原因主要是我们需要将endXendYanimateFloatAsState函数来生成,而在Canvas中是无法直接使用animateFloatAsState来创建动画变量的,所以我们需要一个以LineModel为元素的数组

可以看到这里创建了一个LineModel的二维数组,主要是因为折线图中所有折线都是同时绘制的,所以这里准备将所有折线里面第一根直线维护在单个数组中,有几个这样的数组取决于单个维度中有多少个数据,初始化这个二维数组的代码如下

这里首先创建了一个计数器,每过200毫秒loadPos加一,loadPos这个变量就代表着绘制折线里面的第几根直线,之所以初始值为-2是因为需要在第一次加一的时候,可以让函数animateFloatAsStatestart值是从第一个点的坐标开始,而当绘制到最后一根直线的时候,animateFloatAsState里面的start值与end值又变成了同一个点,表示动画结束,其他情况下,动画的目标值坐标就是下一个动画的起始坐标,我们在看下绘制部分

绘制的逻辑就很简单了,每条折线绘制直线的数量永远都比loadPos这个变量要小,每次都是先画圆点再画直线,在loadPos之后的圆点和直线都是暂时不绘制的,运行下代码看看现在折线的绘制效果

动态更改折线数量

现在折线在绘制的时候已经有了动画效果了,最后我们再实现一下让折线数量与传进来的维度数量相对应,并且在折线图顶部显示折线颜色与对应的公司名称,顶部的这些数据我们就用LazyRow组件来展示,内部遍历一下payload数组,渲染数据从payload数组里面每一个子数组中取出,代码如下

我们看到呈现出来的效果就是上面这样,然后将这个LazyRow与我们折线图的Canvas放置在一个Box组件里面,并且调整下LazyRow的位置,让它顶部居中

接下来就是动态改变payload的大小,现在默认是四个维度的数据都塞进来了,这里稍作修改,在折线图LineChart组件顶部放三个CheckBox,默认是加载一个维度数据,每个CheckBox被勾选之后,动态再添加一个维度的数据进去

现在看下每次添加新数据后折线图是一个什么样子的展现效果

发现有点不对劲,添加新的数据之后,折线图并没有重新开始绘制的动画,而是直接把完整的折线展现出来了,造成这个的原因是主要是因为loadPos这个变量,它在第一个折线显示完以后,大小已经变成单个维度数组的最大值了,所以必须让每次payload大小改变的时候,重置一下loadPos的大小,我们在LineChart里面加一段代码就好了

我们最终效果就完成了,一起来看下

总结

这个折线图组件算是已经完成了,但还是存在着一个比较大的缺点,就是太受限于数据结构,折线图函数的数据源入参必须是个二维数组才可以,不过问题也不大,可以多暴露出一点主要的参数供调用方来定制,这样子无论什么样子的数据结构,只需要将这些参数提取出来,就能绘制这个折线图,这些细节也会后续在源码中逐步完善

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
peakmain94 小时前
Jetpack Compose UI组件封装(一)
android jetpack
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试