본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] Compose Vico 라이브러리로 ComposedChart 만들기 (5)

by immgga 2023. 10. 20.

지난 포스팅에서 Compose Chart 라이브러리인 Vico 사용법과 Vico로 Column Chart 만드는 것까지 해보았다.

이번 포스팅에서는 지난 포스팅에서 언급했다시피, Vico에서 Composed Chart(혼합 차트)를 만들어 보도록 하겠다.

 

1. Composed Chart 만들기

Composed Chart는 혼합 차트를 뜻하는데, line chart와 column chart를 혼합해서 사용할 수 있다.

실제로 Vico wiki에서도 두 차트를 동시에 사용한 예제가 있었다.

하지만 필자는 Column Chart를 두 개를 이용해서 데이터를 표시하고 싶었기에, column 차트 2개를 사용했다.

Vico 라이브러리에 대한 설명과 Column Chart 구현에 관한 자세한 설명이 필요하면 이전 포스팅을 참고하자.

https://rkdrkd-history.tistory.com/72

 

[Android, Kotlin] Chart를 Compose로 구현하기 - (4)

이전 포스팅에서는 compose에서 calendar를 만들어서 Plan Screen의 UI를 구현해 보았다. 이번 포스팅에서는 필자가 compose로 chart를 만들면서 생겼던 일을 설명해 줄까 한다. 3일 동안의 공백 기한 중 2일

rkdrkd-history.tistory.com

 

그러면 이제 본격적으로 구현을 해보도록 하자.

ComposedChart를 구현하기 위해서는 ChartStyle이라는 것이 필요하다. ChartStyle은 말 그대로 차트의 스타일을 변경해 주는 기능이다.

ChartStyle에서는 축, 차트 데이터 UI를 변경, 마커 설정 등등을 할 수 있다.

 

그러면 바로 필자가 만든 차트 스타일을 코드로 확인해 보자.

@Composable
fun rememberChartStyle(columnChartColors: List<Color>): ChartStyle {
    val isSystemInDarkTheme = isSystemInDarkTheme()
    return remember(columnChartColors, isSystemInDarkTheme) {
        val defaultColors = if (isSystemInDarkTheme) DefaultColors.Dark else DefaultColors.Light

        ChartStyle(
            axis = ChartStyle.Axis(
                axisLabelColor = Color(defaultColors.axisLabelColor),
                axisGuidelineColor = Color(defaultColors.axisGuidelineColor),
                axisLineColor = Color(defaultColors.axisLineColor)
            ),
            columnChart = ChartStyle.ColumnChart(
                columns = columnChartColors.map { columnColor ->
                    LineComponent(
                        color = columnColor.toArgb(),
                        thicknessDp = 25f,
                        shape = Shapes.cutCornerShape(topRightPercent = 20, topLeftPercent = 20)
                    )
                },
                dataLabel = TextComponent.Builder().build()
            ),
            lineChart = ChartStyle.LineChart(lines = emptyList()),
            marker = ChartStyle.Marker(),
            elevationOverlayColor = Color(defaultColors.elevationOverlayColor)
        )
    }
}

차트 스타일을 따로 함수로 빼서 제작했다. param으로는 color list를 가져왔다.

color list를 가져온 이유는 column chart를 혼합시킬 건데, 각 columns의 색을 다르게 하고 싶었기 때문이다.

이제 ChartStyle의 param들을 하나하나 확인해 보면서 어떤 기능들을 사용했는지 확인해 보자.

 

1. axis

axis에서는 축의 색을 변경할 수 있다.

아래에 따로 axis 쪽 코드만 빼서 살펴보자.

axis = ChartStyle.Axis(
    axisLabelColor = Color(defaultColors.axisLabelColor),
    axisGuidelineColor = Color(defaultColors.axisGuidelineColor),
    axisLineColor = Color(defaultColors.axisLineColor)
),

axis 쪽에 defaultColors라고 위에 필자가 따로 변수를 생성해 준 것을 보았을 것이다.

이 변수를 이용해 축들의 color를 설정해 주었다.

defaultColors 변수로 Vico의 DefaultColors 인터페이스를 가져와서 default 색상을 설정해 주었다.

필자는 axis 쪽 색은 건드릴 생각이 없었기에, 그냥 default 색상으로 설정해 주었다.

 

2. columnChart, LineChart

다음은 차트 데이터가 어떻게 보이게 할 것인지 설정할 수 있는 곳인데, 그냥 차트에서 중복으로 들어가는 데이터들을 모두 여기에 작성해 놓아도 될 듯하다.

columnChart = ChartStyle.ColumnChart(
    columns = columnChartColors.map { columnColor ->
        LineComponent(
            color = columnColor.toArgb(),
            thicknessDp = 25f,
            shape = Shapes.cutCornerShape(topRightPercent = 20, topLeftPercent = 20)
        )
    },
    dataLabel = TextComponent.Builder().build()
),

여기에서 param으로 가져온 color list를 사용했다.

columns paramter에서 columnChartColors List<Color>를 받아와서 사용했는데, 사용한 이유는 2개의 차트가 서로 다른 색상으로 표시되도록 하고 싶었기 때문이다.

LineComponent() 안에 color에서는 toArgb()를 사용했는데 간단히 설명하면 compose의 Color를 xml의 int Color로 변환해 주는 함수라고 생각하면 되겠다.

다음으로 thicknessDp(데이터의 두께)와 shape(데이터의 모양 설정)을 간단히 설정해 주었다.

마지막으로 차트 두 개에 공통으로 들어갈 dataLabel을 설정했다.

 

3. marker, elevationOverlayColor

marker = ChartStyle.Marker(),
elevationOverlayColor = Color(defaultColors.elevationOverlayColor)

이 부분은 필요한 사람들도 있을 것이고, 필요 없는 사람들도 있을 것이라고 생각한다.

marker는 특정 데이터에 점을 찍어주고 데이터의 크기를 볼 수 있게 하는 기능이다.

출처: vico wiki

elevationOverlayColor는 아직 제대로 알지는 못하지만, marker와 비슷한 역할을 하는 것으로 추측해 본다.

출처: vico wiki

위에 언급한 모든 parameter들은 ChartStyle에 필수로 들어가기 때문에 필요하지 않더라도 넣어 주어야 한다.

모든 ChartStyle 세팅을 마쳤으니, 바로 ComposedChart를 만들어 보자.

 

차트를 만들기 전에 ChartStyle을 적용해 준다.

ProvideChartStyle(rememberChartStyle(columnChartColors = colorList)) {
    // add chart
}

ProvideChartStyle을 이용해 차트 스타일을 적용해 주었다.

ProvideChartStyle() 안에 자신이 만든 ChartStyle을 넣어주면 된다.

 

다음으로는 이제 차트 종류를 설정해 주면 되는데 지난 포스팅이랑은 약간 다르게 해야 한다.

val completedPlanChart = columnChart(
    mergeMode = ColumnChart.MergeMode.Grouped,
    axisValuesOverrider = AxisValuesOverrider.fixed(
        minY = 0f,
        maxY = maxYRange.toFloat()
    ),
    spacing = 100.dp
)
val completedRateChart = columnChart(
    mergeMode = ColumnChart.MergeMode.Grouped,
    axisValuesOverrider = AxisValuesOverrider.fixed(
        minY = 0f,
        maxY = maxYRange.toFloat()
    ),
    spacing = 100.dp
)

전 포스팅처럼 columnChart()를 사용하는 것은 동일하지만 새로운 parameter들을 사용하는 모습이다.

하나하나 설명해 주자면

  • mergeMode: 다중 차트의 모드를 설정할 수 있다.
    Grouped: 각각 따로, Stack: 하나로 합치기
  • spacing: 각 차트 아이템들의 너비를 설정할 수 있다.

다음으로 각 차트의 EntryModelProducer도 생성해 준다.

val completedPlanEntry = ChartEntryModelProducer(intListAsFloatEntryList(completedPlanList))
val completedRateEntry = ChartEntryModelProducer(intListAsFloatEntryList(completedRateList))

initListAsFloatEntryList()는 필자가 사용한 기본 list를 floatEntry형의 list로 변경해 주는 함수이다.

private fun intListAsFloatEntryList(list: List<Int>): List<FloatEntry> {
    val floatEntryList = arrayListOf<FloatEntry>()
    floatEntryList.clear()

    list.forEachIndexed { index, item ->
        floatEntryList.add(entryOf(x = index.toFloat(), y = item.toFloat()))
    }

    return floatEntryList
}

 

마지막으로 차트를 만들어 주겠다.

Chart(
    modifier = Modifier
        .fillMaxWidth()
        .height(400.dp)
        .padding(horizontal = 15.dp, vertical = 5.dp),
    chart = remember(completedPlanChart, completedRateChart) {
        completedPlanChart + completedRateChart
    },
    legend = rememberLegend(colors = colorList),
    chartModelProducer = ComposedChartEntryModelProducer(completedPlanEntry.plus(completedRateEntry)),
    startAxis = rememberStartAxis(
        itemPlacer = AxisItemPlacer.Vertical.default(maxItemCount = maxYRange / 10 + 1)
    ),
    bottomAxis = rememberBottomAxis(
        valueFormatter = { value, _ ->
            ("${value.toInt()+1}주 전")
        }
    ),
    runInitialAnimation = true,
    chartScrollState = rememberChartScrollState()
)

chart 설정 부분에서 이전에 설정한 ChartEntryModelProducer를 remember를 이용해 넣어 준다.

함수 안에는 각 ChartEntryModelProducer의 + 한 값을 넣어주도록 하자. + 가 작동하지 않으면. plus()를 이용해 보자.

 

chartModelProdecer에는 ComposedChartEntryModelProducer를 넣어 준다. 이 클래스 안에는 두 개의 ChartEntryModelProducer를. plus() 또는 + 를 이용해 넣어 준다.

 

legend는 차트의 범례를 설정하는 곳이다. 필자는 rememberLegend() Composable을 만들어서 설정해 주었다.

@Composable
fun rememberLegend(colors: List<Color>): HorizontalLegend {
    val labelTextList = listOf("완료한 일정(개)", "일정 완료율(%)")

    return horizontalLegend(
        items = List(labelTextList.size) { index ->
            legendItem(
                icon = shapeComponent(
                    shape = Shapes.pillShape,
                    color = colors[index]
                ),
                label = textComponent(),
                labelText = labelTextList[index]
            )
        },
        iconSize = 10.dp,
        iconPadding = 8.dp,
        spacing = 10.dp,
        padding = dimensionsOf(top = 8.dp)
    )
}

horizontalLegend()는 범례를 가로로 정렬하고 싶을 때 사용하고 세로로 정렬하는 것을 원한다면 verticalLegend()를 사용하면 되겠다.

 

chartScrollState를 이용해서 Chart를 scroll 할 수 있게 해 주었다.

 

나머지 param들은 이전에 설명했기에 생략하겠다.

전체 차트 코드

ProvideChartStyle(rememberChartStyle(columnChartColors = colorList)) {
    val completedPlanChart = columnChart(
        mergeMode = ColumnChart.MergeMode.Grouped,
        axisValuesOverrider = AxisValuesOverrider.fixed(
            minY = 0f,
            maxY = maxYRange.toFloat()
        ),
        spacing = 100.dp
    )
    val completedRateChart = columnChart(
        mergeMode = ColumnChart.MergeMode.Grouped,
        axisValuesOverrider = AxisValuesOverrider.fixed(
            minY = 0f,
            maxY = maxYRange.toFloat()
        ),
        spacing = 100.dp
    )

    val completedPlanEntry = ChartEntryModelProducer(intListAsFloatEntryList(completedPlanList))
    val completedRateEntry = ChartEntryModelProducer(intListAsFloatEntryList(completedRateList))

    Chart(
        modifier = Modifier
            .fillMaxWidth()
            .height(400.dp)
            .padding(horizontal = 15.dp, vertical = 5.dp),
        chart = remember(completedPlanChart, completedRateChart) {
            completedPlanChart + completedRateChart
        },
        legend = rememberLegend(colors = colorList),
        chartModelProducer = ComposedChartEntryModelProducer(completedPlanEntry.plus(completedRateEntry)),
        startAxis = rememberStartAxis(
            itemPlacer = AxisItemPlacer.Vertical.default(maxItemCount = maxYRange / 10 + 1)
        ),
        bottomAxis = rememberBottomAxis(
            valueFormatter = { value, _ ->
                ("${value.toInt()+1}주 전")
            }
        ),
        runInitialAnimation = true,
        chartScrollState = rememberChartScrollState()
    )
}

차트 코드를 완성했으면 테스트를 해보자.

 

2. 테스트

이제 만든 것을 테스트해 보고 끝내자.

정상적으로 작동하는 모습니다 :)

 

정리

  • 두 번째 혼합 차트를 만들 때 더 힘들었다 >:(
  • 각각의 차트를 Compose로 만들어 보았는데, 이제 다시 사용할 때가 되면 유용하게 사용할 수 있을 듯하다.
728x90