急に要件が出てきたら困るかもなので、グラフの表示試してみます
Vicoなどサードベンダーのライブラリを使うのもありだけど、保守性を考えるとあまり依存したくないのでCanvasに描画&アニメーションで独自に作ってみました
https://developer.android.com/develop/ui/compose/graphics/draw/overview
https://developer.android.com/develop/ui/compose/animation/quick-guide
折れ線グラフ
ただの直線じゃ面白くないので、よくある?滑らかな曲線で作ってみようと思います
こちらが参考になりました
https://medium.com/@kezhang404/either-compose-is-elegant-or-if-you-want-to-draw-something-with-an-android-view-you-have-to-7ce00dc7cc1
// ↑の実装を流用
fun buildCurveLine(path: Path, startPoint: Offset, endPoint: Offset) {
val firstControlPoint = Offset(
x = startPoint.x + (endPoint.x - startPoint.x) / 2F,
y = startPoint.y,
)
val secondControlPoint = Offset(
x = startPoint.x + (endPoint.x - startPoint.x) / 2F,
y = endPoint.y,
)
path.cubicTo(
x1 = firstControlPoint.x,
y1 = firstControlPoint.y,
x2 = secondControlPoint.x,
y2 = secondControlPoint.y,
x3 = endPoint.x,
y3 = endPoint.y,
)
}data class Item (
val title: String,
val amount: Float
)
@Composable
fun LineChart(chart: List<Item>, modifier: Modifier = Modifier) {
val max = remember {
chart.map { it.amount }.max()
}
Canvas(modifier) {
val moveX = this.size.width / chart.size
val drawH = this.size.height
var current = Offset(0.0f, 0.0f)
val path = Path().apply {
chart.forEachIndexed { i, it ->
val x = moveX * i
val y = drawH - drawH * (it.amount / max)
if (i == 0) moveTo(x, y)
else buildCurveLine(this, current, Offset(x, y))
current = Offset(x, y)
}
}
repeat((max / 500).toInt()) {
if (it != 0) {
drawLine(
Color.Gray,
start = Offset(0.0f, drawH - drawH * (it * 500 / max)),
end = Offset(this.size.width, drawH - drawH * (it * 500 / max))
)
}
}
drawPath(
path,
Color.Black,
1.0f,
style = Stroke(width = 4.0f)
)
path.lineTo(current.x, drawH)
path.lineTo(0.0f, drawH)
path.close()
drawPath(
path,
Brush.verticalGradient(listOf(Color(1.0f, 0.2f, 0.2f, 1.0f), Color(1.0f, 0.4f, 0.4f, 1.0f), Color(1.0f, 0.95f, 0.95f, 1.0f))),
0.75f
)
}
}
@Composable
fun Screen(modifier: Modifier = Modifier) {
val chart = remember {
listOf(
Item("2010", 1000f),
Item("2015", 700f),
Item("2020", 0f),
Item("2025", 100f),
Item("2030", 500f),
Item("2030", 1500f),
Item("2030", 1200f),
Item("2030", 900f),
)
}
Column(
modifier
.fillMaxWidth()
.padding(top = 44.dp), horizontalAlignment = Alignment.CenterHorizontally) {
LineChart(chart, Modifier.fillMaxSize().padding(44.dp))
}
}
ドーナツグラフ
表示にアニメーションするドーナツグラフを作ってみました
@Composable
fun DonutChart(
chart: List<Item>,
startColor: Color = Color(0, 79, 70),
endColor: Color = Color(178, 255, 255)
) {
val sum = remember {
chart.map { it.amount }.sum()
}
val color = remember {
var i = 0
chart.map {
val r = startColor.red - ((startColor.red - endColor.red) / chart.size * i)
val g = startColor.green - ((startColor.green - endColor.green) / chart.size * i)
val b = startColor.blue - ((startColor.blue - endColor.blue) / chart.size * i)
i++
Color(r, g, b, 1.0f)
}
}
val startAngle = remember {
var angle = 270f
chart.map {
val value = angle
angle += 360f * (it.amount / sum)
if (angle > 360f) angle -= 360f
value
}
}
val animationAngle = remember {
chart.map { Animatable(0f) }
}
LaunchedEffect(Unit) {
animationAngle.forEachIndexed { i, it ->
it.animateTo(360f * (chart[i].amount / sum) - 1f, tween(1000 / chart.size, easing = LinearEasing))
}
}
Column {
Canvas(Modifier.size(200.dp, 200.dp)) {
chart.forEachIndexed { i, value ->
drawArc(
color = color[i],
startAngle = startAngle[i],
sweepAngle = animationAngle[i].value,
useCenter = false,
style = Stroke(width = 64f),
)
}
}
Spacer(Modifier.height(44.dp))
Column(Modifier.width(200.dp)) {
Text("資産合計 ${sum.toInt()} 万円", style = MaterialTheme.typography.labelSmall)
HorizontalDivider()
chart.forEachIndexed { i, it ->
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(Modifier.size(8.dp).background(color[i]))
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(it.title, style = MaterialTheme.typography.labelSmall)
Text("${it.amount.toInt()} 万円", style = MaterialTheme.typography.labelSmall)
}
}
}
}
}
}
@Composable
fun Screen(modifier: Modifier = Modifier) {
val chart = remember {
listOf(
Item("現金", 1000f),
Item("株式", 500f),
Item("債権", 0f),
Item("保険", 50f),
Item("不動産", 2500f),
)
.filter { it.amount != 0f }
.sortedByDescending { it.amount }
}
Column(modifier.fillMaxWidth().padding(top = 44.dp), horizontalAlignment = Alignment.CenterHorizontally) {
DonutChart(chart)
}
}Android Studio Koala 2024.1.1 Patch 1 built on July 11, 2024
