互联网大数据时代,图形表格是数据分析和展示的重中之重!!!
自己用Compose实现一套图表库,你 只需要提供数据配置就可以了
一、前言
在之前的文章中重点介绍了折线图: Compose曲线图表库WXChart,你只需要提供数据配置就行了
同时重点介绍了Compose绘制的坐标体系,及相关计算
本文重点对常见的图库:折线图,曲线图,贝赛尔曲线,柱状图,圆饼图,圆环图
进行完善进行重点介绍
效果图如下:
二、基础用法
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、环形数据配置数据说明
同理,上面介绍圆饼图配置数据说明已经说过了。
总结
本文重点介绍了
折线图,贝赛尔曲线,柱状图,圆饼图,环形图的绘制,使用,及相关数据配置
同时,对点击事件进行了详细说明
总之,你只需要配置数据就可以了,不需要关心复杂的绘制逻辑。