프로그래밍/Android

[안드로이드] 주식차트 그리기 (Drawing LineChart with MPAndroidChart, Differentiate line colors by limit line value)

Lou Park 2021. 1. 9. 17:03

우리가 만들 차트

위 사진에 보이는 차트를 안드로이드 주요 차트 라이브러리인 MPAndroidChart를 이용해 그려볼 것이다. 내가 영어를 그닥 잘하지 않아서 그런건지 모르겠지만, 저런 차트를 그리고 싶은데 문서나 강의 같은걸 찾지 못했다. 그래서 블로그에도 올려서 방법을 공유 해 보려고 한다!

 

 

MPAndroidChart 준비하기

MPAndroidChart 라이브러리를 사용하기 위해서 gradle에 다음과 같이 추가한다.

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    ...
    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'

최신 버전으로 받고 싶다면 github로 직접 가서 Getting Started 가이드를 보면 된다.
https://github.com/PhilJay/MPAndroidChart

 

 

차트 데이터

차트를 그리기 위해서는 자료가 필요하다. 빠른 진행을 위해, Data 자료형과 Data를 가져올 수 있는 유틸 클래스를 먼저 만들자!

 

Stock.kt

주식 데이터를 담고있다. 시간(unix timestamp)과 주가를 가짐.

data class Stock(
    var createdAt: Long = 0,
    var price: Long = 0
)

 

DataUtil.kt

DataUtil.getStockData()로 데이터 목록을 바로 가져올 수 있도록 만들었다.

object DataUtil {
    fun getStockData(): List<Stock> {
        return listOf(
            Stock(1_500_000_000, 2000),
            Stock(1_500_000_100, 2100),
            Stock(1_500_000_200, 1700),
            Stock(1_500_000_300, 1740),
            Stock(1_500_000_400, 1980),
            Stock(1_500_000_500, 2000),
            Stock(1_500_000_600, 2100),
            Stock(1_500_000_700, 2400),
            Stock(1_500_000_800, 3000),
            Stock(1_500_000_900, 1700),
            Stock(1_500_001_000, 1500),
            Stock(1_500_001_100, 1294),
            Stock(1_500_001_200, 3944),
            Stock(1_500_001_300, 4500),
            Stock(1_500_001_400, 6969),
            Stock(1_500_001_500, 8930),
            Stock(1_500_001_600, 9900),
            Stock(1_500_001_700, 7000),
            Stock(1_500_001_800, 8300),
            Stock(1_500_001_900, 4300),
            Stock(1_500_002_000, 2003),
            Stock(1_500_002_100, 5960),
            Stock(1_500_002_200, 3403),
            Stock(1_500_002_300, 3040),
            Stock(1_500_002_400, 5060),
            Stock(1_500_002_500, 2931),
            Stock(1_500_002_600, 3030),
            Stock(1_500_002_700, 6431),
            Stock(1_500_002_800, 3948),
            Stock(1_500_002_900, 2100),
            Stock(1_500_003_000, 2030),
            Stock(1_500_003_100, 2031),
            Stock(1_500_003_200, 2039),
            Stock(1_500_003_300, 4504),
            Stock(1_500_003_400, 4912),
            Stock(1_500_003_500, 7963),
            Stock(1_500_003_600, 3929),
            Stock(1_500_003_700, 7945),
            Stock(1_500_003_800, 9920),
            Stock(1_500_003_900, 1293),
            Stock(1_500_004_000, 2192),
            Stock(1_500_004_100, 2944),
            Stock(1_500_004_200, 1912),
            Stock(1_500_004_300, 2392),
            Stock(1_500_004_400, 1029),
            Stock(1_500_004_500, 4950),
            Stock(1_500_004_600, 2392),
            Stock(1_500_004_700, 3494),
            Stock(1_500_004_800, 3590),
            Stock(1_500_004_900, 3429),
            Stock(1_500_005_000, 2066),
            Stock(1_500_005_100, 6938),
            Stock(1_500_005_200, 7939),
            Stock(1_500_005_300, 8400),
            Stock(1_500_005_400, 8700),
            Stock(1_500_005_500, 8900),
            Stock(1_500_005_600, 9100),
            Stock(1_500_005_700, 1010),
            Stock(1_500_005_800, 1200),
            Stock(1_500_005_900, 1500),
            Stock(1_500_006_000, 1430),
            Stock(1_500_006_100, 1220),
            Stock(1_500_006_200, 1900),
            Stock(1_500_006_300, 2210),
            Stock(1_500_006_400, 2391),
            Stock(1_500_006_500, 2399),
            Stock(1_500_006_600, 2394),
            Stock(1_500_006_700, 3494),
            Stock(1_500_006_800, 2050),
            Stock(1_500_006_900, 2150),
            Stock(1_500_007_000, 3493),
            Stock(1_500_007_100, 2942),
            Stock(1_500_007_200, 2191),
            Stock(1_500_007_300, 3050),
            Stock(1_500_007_400, 1012),
            Stock(1_500_007_500, 2129),
            Stock(1_500_007_600, 2434),
            Stock(1_500_007_700, 2933),
            Stock(1_500_007_800, 3020),
            Stock(1_500_007_900, 3430),
            Stock(1_500_008_000, 2930),
            Stock(1_500_008_100, 2041),
            Stock(1_500_008_200, 3439),
            Stock(1_500_008_300, 2032),
            Stock(1_500_008_400, 3043),
            Stock(1_500_008_500, 1990),
            Stock(1_500_008_600, 2032),
            Stock(1_500_008_700, 1700),
            Stock(1_500_008_800, 2092),
            Stock(1_500_008_900, 2399)
        )
    }
}

 

 

 

 

레이아웃 준비하기

activity_line_chart.xml

MPAndroidChart의 LineChart를 사용하기 위해서 다음과 같이 xml을 구성 해 주었다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">
    <com.github.mikephil.charting.charts.LineChart
        android:id="@+id/chart"
        android:layout_width="match_parent"
        android:layout_height="400dp"/>
</LinearLayout>

 

 

 

 

LineChart 그리기

처음에 MPAndroidChart에서 제공하는 라인차트를 그리게 되면 너무 기능상 충실한 나머지 디자인이 눈뜨고 못봐줄 정도라 가리고 싶은 부분이 많은데, 원하는 부분을 가리고 커스터마이징 할 수 있다. 우선은 모든- 거추장스러운 부분을 가리고 라인 차트를 하나 그려보자!

 

LineChartActivity.kt

kotlin의 viewBinding을 이용할 것이다. (사용하기 위해서는 app 수준 gradle에 아래 코드 추가해주면됨)

android {
    viewBinding {
        enabled = true
    }
}

LineChart - 1

다음 코드를 이용하면 사진과 같은 깔끔한 차트 하나를 그릴 수 있다. 데이터의 평균 값으로 LimitLine을 그리고, Color.RED로 그냥 빨갛게만 칠했다. 코드를 만져보면서 눈치챈 사람들도 있겠지만, LineDataSet에서는 setColor()외에도 setColors()라는 메소드가 있어서 색상값을 배열로 전달 할 수 있다.

class LineChartActivity : AppCompatActivity() {

    private lateinit var binding: ActivityLineChartBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLineChartBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // 평균 값 구하기
        var average = 0F
        for (stock in DataUtil.getStockData()) {
            average += stock.price.toFloat()
        }
        average /= DataUtil.getStockData().size

        // 그래프에 들어갈 데이터 준비
        val entries = ArrayList<Entry>()
        for (stock in DataUtil.getStockData()) {
            entries.add(Entry(stock.createdAt.toFloat(), stock.price.toFloat()))
        }

        val dataSet = LineDataSet(entries, "").apply {
            setDrawCircles(false)
            color = Color.RED
            highLightColor = Color.TRANSPARENT
            valueTextSize = 0F
            lineWidth = 1.5F
        }

        val lineData = LineData(dataSet)
        binding.chart.run {
            data = lineData
            description.isEnabled = false // 하단 Description Label 제거함
            invalidate() // refresh
        }

        val averageLine = LimitLine(average).apply {
            lineWidth = 1F
            enableDashedLine(4F, 10F, 10F)
            lineColor = Color.DKGRAY
        }

        // 범례
        binding.chart.legend.apply {
            isEnabled = false // 사용하지 않음
        }
        // Y 축
        binding.chart.axisLeft.apply {
            // 라벨, 축라인, 그리드 사용하지 않음
            setDrawLabels(false)
            setDrawAxisLine(false)
            setDrawGridLines(false)

            // 한계선 추가
            removeAllLimitLines()
            addLimitLine(averageLine)
        }
        binding.chart.axisRight.apply {
            // 우측 Y축은 사용하지 않음
            isEnabled = false
        }
        // X 축
        binding.chart.xAxis.apply {
            // x축 값은 투명으로
            textColor = Color.TRANSPARENT
            // 축라인, 그리드 사용하지 않음
            setDrawAxisLine(false)
            setDrawGridLines(false)
        }
    }
}

그렇다면! 평균값을 기준으로 평균값 >= 가격이라면 Color.RED, 평균값 < 가격이라면 Color.BLUE로 해서 표를 그려보면 어떨까?! 그래서 colors라는 ArrayList<Int>를 추가하고 적용시켜보았다.

val entries = ArrayList<Entry>()
val colors = ArrayList<Int>()
for (stock in DataUtil.getStockData()) {
    entries.add(Entry(stock.createdAt.toFloat(), stock.price.toFloat()))
    colors.add(if (stock.price >= average) {
        Color.RED
    } else {
        Color.BLUE
    })
}

val dataSet = LineDataSet(entries, "").apply {
    setDrawCircles(false)
    this.colors = colors
    highLightColor = Color.TRANSPARENT
    valueTextSize = 0F
    lineWidth = 1.5F
}

LineChart - 2

결과는 다소 당황스러웠는데, 값과 값 사이의 색깔을 칠해주는거라서 정확하게 평균값을 기준으로 두 색깔을 가르지 못했다. 이를 해결하기 위해서는 가짜 값들을 넣어주어야 한다. 이전 색깔을 검사하고 색깔이 달라질때 투명이면서 평균값을 가지는 데이터 하나를 추가하고, 다음 색깔을 가지면서 평균값을 가지는 데이터 하나를 추가하면 우리가 원하는 차트를 얻을 수 있다.

가짜 데이터가 평균값 사이를 오갈때마다 추가된다.

val entries = ArrayList<Entry>()
val colors = Stack<Int>()
for (stock in DataUtil.getStockData()) {
    if (stock.price >= average) {
        if (colors.isNotEmpty() && colors.peek() == Color.BLUE) {
            entries.add(Entry(stock.createdAt.toFloat(), average))
            colors.add(Color.TRANSPARENT)
            entries.add(Entry(stock.createdAt.toFloat(), average))
            colors.add(Color.RED)
        }
        entries.add(Entry(stock.createdAt.toFloat(), stock.price.toFloat()))
        colors.add(Color.RED)
    } else {
        if (colors.isNotEmpty() && colors.peek() == Color.RED) {
            entries.add(Entry(stock.createdAt.toFloat(), average))
            colors.add(Color.TRANSPARENT)
            entries.add(Entry(stock.createdAt.toFloat(), average))
            colors.add(Color.BLUE)
        }
        entries.add(Entry(stock.createdAt.toFloat(), stock.price.toFloat()))
        colors.add(Color.BLUE)
    }
}