Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果

1,129 阅读8分钟

333.jpg

互联网大数据时代,图形表格是数据分析和展示的重中之重!!!

自己用Compose实现一套图表库,你 只需要提供数据配置就可以了

一、前言

在之前的文章中重点介绍了折线图: Compose曲线图表库WXChart,你只需要提供数据配置就行了
同时重点介绍了Compose绘制的坐标体系,及相关计算
本文重点对常见的图库:折线图,曲线图,贝赛尔曲线,柱状图,圆饼图,圆环图进行完善进行重点介绍 效果图如下:

1111.gif

2222.gif

3333.gif

4444.gif

5555.gif

二、基础用法

1、repositories中添加如下maven
    repositories {
        maven { url 'https://repo1.maven.org/maven2/' }
        maven { url 'https://s01.oss.sonatype.org/content/repositories/releases/' }
    }
}

2、 dependencies中添加依赖

implementation("io.github.wgllss:Wgllss-WXChart:1.0.11")

3、基础坐标体系,X,Y,视图长,宽等,在前面文章已经介绍过了,请参考:

Compose曲线图表库WXChart,你只需要提供数据配置就行了

三、折线图,曲线图,贝赛尔曲线

1、线条绘制使用:

如下面代码:
touchIndex:点击的横向哪一条
isTouchLast:控制点击到右边了,展示说明浮层要放左 如果想绘制出贝赛尔曲线: realDrawLineChart(modifier, textMeasurer, it, touchIndex, isTouchLast, false)的最后一个参数传true,默认也是true


@Composable
fun LineChart(innerPadding: PaddingValues, viewModel: SampleViewModel = SampleViewModel().apply { setData() }) {
    var touchIndex by remember { mutableStateOf(-1) }//点击的横向哪一条
    var isTouchLast by remember { mutableStateOf(false) }//控制点击到右边了,展示说明浮层要放左边

    val textMeasurer = rememberTextMeasurer()

    val chatModel by viewModel.chatLineModel.observeAsState()
    chatModel?.let {
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .background(Color.White)
                .fillMaxWidth()
                .fillMaxHeight()
        ) {

            val modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
                .background(Color.White)
                .graphicsLayer()      //监听手势缩放
                .pointerInput(Unit) {
                    detectTapGestures(onTap = {
                        touchIndex = getTouchIndex(chatModel, size.width.toFloat(), it.x, it.y)
                        isTouchLast = if (chatModel != null) {
                            chatModel!!.xCount - 3 <= touchIndex
                        } else false
                    })
                }
                //贝赛尔曲线,最后一个值传true,默认是true
            realDrawLineChart(modifier, textMeasurer, it, touchIndex, isTouchLast, false)
        }
    }
}

2、点击计算出哪一条数据

点击能拿到触摸touch的x,y值,
X轴上面能够平分出绘制的条数的单元间隔值:xValue,
能够判断出x坐标是否位于,某一条坐标值的单元间隔值:xValue一半的,左边一半到右边一半这个区域内,边可以得出点击的列表索引。

fun getTouchIndex(chatModel: ChatLineModel?, width: Float, x: Float, y: Float): Int {
    chatModel?.let {
        val xValue = (width - it.offsetx) / it.xCount
        for (chatLinebean in it.datas) {
            for (i in 0 until chatLinebean.listY.size) {
                if (x > it.offsetx + i * xValue - xValue / 2 && x < it.offsetx + i * xValue + xValue / 2) {
                    return i
                }
            }
        }
    }
    return -1
}

3、曲线数据配置 chatLineModel

前面文章已经介绍过了,请参考: Compose曲线图表库WXChart,你只需要提供数据配置就行了

四、柱状图

1、使用介绍:

如下代码:
touchIndex:点击的横向哪一条
isTouchLast:控制点击到右边了,展示说明浮层要放左
调用: realDrawBarChart(modifier, textMeasurer, it, touchIndex, isTouchLast)


@Composable
fun BarChart(innerPadding: PaddingValues = PaddingValues(0.dp), viewModel: SampleViewModel = SampleViewModel().apply { setData() }) {
    var touchIndex by remember { mutableStateOf(-1) }
    var isTouchLast by remember { mutableStateOf(false) }

    val textMeasurer = rememberTextMeasurer()

    val chatModel by viewModel.chatBarModel.observeAsState()
    chatModel?.let {
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .background(Color.White)
                .fillMaxWidth()
                .fillMaxHeight()
        ) {

            val modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
                .background(Color.White)
                .graphicsLayer()      //监听手势缩放
                .pointerInput(Unit) {
                    detectTapGestures(onTap = {
                        touchIndex = getTouchBarIndex(chatModel, size.width.toFloat(), it.x, it.y)
                        isTouchLast = if (chatModel != null) {
                            chatModel!!.xCount - 3 <= touchIndex
                        } else false
                    })
                }
            realDrawBarChart(modifier, textMeasurer, it, touchIndex, isTouchLast)
        }
    }
}

2、点击计算出哪一条数据

与曲线图不同的是
曲线图X刻度上面哪一条对应的X上竖线刻度是一条线,柱状图是有宽度的,而且,第一条起点不能从X轴0开始,不然与Y轴线得加在一起了不美观,最后一条起始点不能在X轴线最右边,否则加上柱状宽度就超出控件了,数据那边配置需要设置柱状宽度,第一个起始点位置左边要留一个柱状宽度,最后一个要一个柱状宽度,柱状从起点开始还要向右占一个宽度.
所以,每个柱状左下角的起始点之间的间隔单位,需要控件所有宽度减去掉左右偏移(margin),还要再减去3个柱状图宽度后,再去按照数据个数平均。之后判断点击索引基本就与折线图,曲线度逻辑一样了。

fun getTouchBarIndex(chatModel: ChartBarModel?, width: Float, x: Float, y: Float): Int {
    chatModel?.let {
        val xValue = (width - 1.5f * it.offsetx - 3 * it.barWidth) / (it.xCount - 1)
        for (i in 0 until it.list.size) {
            if (x > it.offsetx + it.barWidth + i * xValue - xValue / 2 && x < it.offsetx + it.barWidth + i * xValue + xValue / 2) {
                return i
            }
        }
    }
    return -1
}

3、柱状图数据配置说明:

ChartBarModel内:
barWidth:柱状图宽度
list: MutableList<ChartBarBean>:柱状图数据
class ChartBarBean(val value: Float, @Stable val color: Color, val title: String)内:
value:数据值
color:柱状图颜色
title:柱状图标题

class ChartBarModel(
    val list: MutableList<ChartBarBean>,
    val barWidth: Float = 40f,
) : ChartBaseModel()
class ChartBarBean(val value: Float, @Stable val color: Color, val title: String)

五、圆饼图

1、使用

如下代码:
touchPieData:点击touch时候返回内容Triple对象,其中:
first:点击索引
second:点击后计算出对应扇形外面展示当前的X坐标值
third:点击后计算出对应扇形外面展示当前的Y坐标值


@Composable
fun PieChart(innerPadding: PaddingValues = PaddingValues(100.dp), viewModel: SampleViewModel = SampleViewModel().apply { setData() }) {
    var touchPieData by remember { mutableStateOf(Triple(-1, 0f, 0f)) }
    val textMeasurer = rememberTextMeasurer()
    val chatModel by viewModel.chatPieModel.observeAsState()
    chatModel?.let {
        Column(
            modifier = Modifier
                .padding(innerPadding)
                .background(Color.White)
                .fillMaxWidth()
                .fillMaxHeight()
        ) {
            val modifier3 = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
                .background(Color(0x20000000))
                .graphicsLayer()      //监听手势缩放
                .pointerInput(Unit) {
                    detectTapGestures(onTap = { t ->
                        touchPieData = getTouchPieIndex(it, size.center.x.toFloat(), size.center.y.toFloat(), t.x, t.y)
                    })
                }
            realDrawPieChart(modifier3, textMeasurer, it, touchPieData)
        }
    }
}

2、点击计算出哪一条数据

与折线图,曲线图,柱状图不一样的是,该计算相对复杂: 半径:为视图中心centerX,centerY 最小的那一个(即:视图长和宽各一半最小的那个为半径),如果想跳小半径,需要数据那边配置radiusOffset,即视图长和宽各一半最小的那个为半径再减去radiusOffset为半径:

需要根据点击的x,y值判断出点击的哪一个扇形内,
需要计算出,点击的位置,距离圆心的X距离,Y距离,然后X的平方 + Y的平方要小于,半径的平方 如果是圆环:然后X的平方 + Y的平方要小于 环外边缘半径平方,同时还要 大于 环内边缘半径平方

判断了点击区域,需要找到 以圆心为坐标体系象限:1,2,3,4象限哪一个,
计算出弧度,弧度累计依次哪两条弧度之间就是当前索引

然后根据当前弧度找到当前弧度最中间的哪个弧度
然后根据弧度计算出角度
然后根据角度和半径计算出距离,再往圆外面加一些,再换成坐标计算法,得出显示当前点击的内容说明


//得到第一 第二 第三 第四象限,以圆心坐标为原点计算,数学公式
fun touchOnWhichPart(centerX: Float, centerY: Float, x: Float, y: Float): Int {
    return if (x > centerX) {
        if (y > centerY) 4 else 1
    } else if (y > centerY) 3 else 2
}

fun getTouchPieIndex(chatModel: ChartPieModel?, centerX: Float, centerY: Float, x: Float, y: Float): Triple<Int, Float, Float> {
    chatModel?.let {
        val radius = min(centerX - it.radiusOffset, centerY - it.radiusOffset) //饼图半径
        val hoopSize = if (it.isHoop) radius else 0f
        val distance = (x - (centerX + it.offsetx)) * (x - (centerX + it.offsetx)) + (y - (centerY + it.offsety)) * (y - (centerY + it.offsety))
        val outDistance = (radius + hoopSize / 2) * (radius + hoopSize / 2)
        val interDistance = if (it.isHoop) (radius - hoopSize / 2) * (radius - hoopSize / 2) else 0f
        var alfa = 0.00
        if (distance < outDistance && distance > interDistance) {
            val type = touchOnWhichPart(centerX + it.offsetx, centerY + it.offsety, x, y)
            when (type) {
                1 -> {
                    alfa = Math.atan2((x - (centerX + it.offsetx)).toDouble(), ((centerY + it.offsety) - y).toDouble()) * 180 / PI;
                }

                2 -> {
                    alfa = Math.atan2(((centerY + it.offsety) - y).toDouble(), ((centerX + it.offsetx) - x).toDouble()) * 180 / PI + 270;
                }

                3 -> {
                    alfa = Math.atan2(((centerX + it.offsetx) - x).toDouble(), (y - (centerY + it.offsety)).toDouble()) * 180 / PI + 180;
                }

                4 -> {
                    alfa = Math.atan2((y - (centerY + it.offsety)).toDouble(), (x - (centerX + it.offsetx)).toDouble()) * 180 / PI + 90;
                }

                else -> {

                }
            }
            val rateAbs = 360f / it.total
            val listSize = it.list.size
            var prefixAngle = 0f
            if (alfa >= 0) {
                for (i in 0 until listSize) {
                    val g = prefixAngle + rateAbs * it.list[i].value / 2//角度
                    val addDif = if (it.isHoop) it.hoopSize / 2 + 2 * it.scaleOffset else 2 * it.scaleOffset

                    prefixAngle += rateAbs * it.list[i].value
                    if (alfa > prefixAngle) {

                    } else {
                        var offsetX = 0f
                        var offsetY = 0f
                        when (type) {
                            1 -> {
                                val q = Math.toRadians(g.toDouble())//角度
                                offsetX = centerX + it.offsetx + (radius + addDif) * sin(q).toFloat()
                                offsetY = centerY + it.offsety - (radius + addDif) * cos(q).toFloat()
                            }

                            2 -> {
                                val q = Math.toRadians((g - 270f).toDouble())//角度
                                offsetX = centerX + it.offsetx - (radius + addDif) * cos(q).toFloat() - 2 * it.scaleOffset - it.lableLeftOffetx
                                offsetY = centerY + it.offsety - (radius + addDif) * sin(q).toFloat()
                            }

                            3 -> {
                                val q = Math.toRadians((g - 180f).toDouble())//角度
                                offsetX = centerX + it.offsetx - (radius + addDif) * sin(q).toFloat() - 2 * it.scaleOffset - it.lableLeftOffetx
                                offsetY = centerY + it.offsety + (radius + addDif) * cos(q).toFloat()
                            }

                            4 -> {
                                val q = Math.toRadians((g - 90f).toDouble())//角度
                                offsetX = centerX + it.offsetx + (radius + addDif) * cos(q).toFloat()
                                offsetY = centerY + it.offsety + (radius + addDif) * sin(q).toFloat()
                            }
                        }
                        return Triple(i, offsetX, offsetY)
                    }
                }
            }
        }
    }
    return Triple(-1, 0f, 0f)
}

3、圆饼图配置数据说明

ChartPieModel

radiusOffset:调小半径配置
hoopSize: 圆环环宽度
isHoop:是否是圆环
scaleOffset: 点击放大的半径值
lableLeftOffetx:点击到圆左边时候,显示文案太长,可控制该文案向左偏移

class ChartPieModel(val list: MutableList<ChartPieBean>, val radiusOffset: Float) : ChartBaseModel() {

    var hoopSize: Float = 50f //圆环环宽度
    var isHoop: Boolean = false //是否是圆环
    var scaleOffset: Float = 50f //点击放大的半径值
    var lableLeftOffetx: Float = 50f //点击到圆左边时候,显示文案太长,可控制该文案向左偏移

ChartPieBean:

name:圆饼扇形或者环形上名字
value:数据值
color:圆饼扇形或者环形上颜色

data class ChartPieBean(val name: String, val value: Float, @Stable val color: Color)

六、圆环图

1、使用

和圆饼图使用方法一样
数据配置需要将 isHoop:设置为true
hoopSize: 需要设置圆环环宽度
radiusOffset:调小半径配置
需要注意的是: 绘制环形,是绘制圆外边缘边线,设置了边线宽度,不填边线内部区域: 环的宽度设置为 hoopSize,显示出来理论是,hoopSize的一半在边线之外,一半在边线之内,
需要调整环大小需要 radiusOffset 和 hoopSize进行综合计算

2、点击计算出哪一条数据

这里逻辑和圆饼图一样

3、环形数据配置数据说明

同理,上面介绍圆饼图配置数据说明已经说过了。

总结

本文重点介绍了
折线图,贝赛尔曲线,柱状图,圆饼图,环形图的绘制,使用,及相关数据配置
同时,对点击事件进行了详细说明

总之,你只需要配置数据就可以了,不需要关心复杂的绘制逻辑。

github地址
gitee地址

感谢阅读:

欢迎 关注,点赞、收藏

这里你会学到不一样的东西