一步一步来做一个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里面加一段代码就好了

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

总结

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

相关推荐
M_emory_10 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito13 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang3 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发3 小时前
解锁微前端的优秀库
前端
王解4 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁4 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis