안드로이드 - MPAndroidChart로 그래프 그리기

이우건·2023년 12월 19일
0

안드로이드

목록 보기
9/20

개요

카페 주문, 메뉴, 매출을 관리하는 관리자 앱을 만들었는데 매출을 수치로 표현하기보다는 그래프로 표현하는 것이 한 눈에 보기 편할 것 같아 MPAndroidChart 라이브러리를 사용해서 그래프를 구현하였다.

링크 : https://github.com/PhilJay/MPAndroidChart

MPAndroidChart 라이브러리에서는 많은 그래프 모양을 지원한다. 이 중에서 필자는 한달 매출을 LineChart를 사용하여 꺾은선 그래프 형태로 구현하였고, 많이 팔린 음료 Top3는 HorizontalBarChart를 이용하여 수평으로 누운 그래프를 구현하였다.

XML 파일

한달 매출을 위한 LineChart

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.github.mikephil.charting.charts.LineChart
            android:id="@+id/line_chart"
            android:layout_width="match_parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            android:layout_marginStart="10dp"
            android:layout_marginEnd="10dp"
            android:layout_height="300dp" />



    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Kotlin 코드

MonthChartFragment

class MonthChartFragment : Fragment() {

    private var _binding : FragmentMonthChartBinding? = null
    private val binding get() = _binding!!

    private lateinit var lineChart : LineChart
    private var maxSales = 0

    // 부모 Fragment viewModel과 공유
    private val salesViewModel : SalesViewModel by viewModels(
        ownerProducer = {requireParentFragment()}
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        maxSales = ApplicationClass.sharedPreferenceUtil.getMaxSales()!!
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        _binding = FragmentMonthChartBinding.inflate(inflater, container, false)
        return binding.root
    }

    @RequiresApi(Build.VERSION_CODES.O)
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lineChart = binding.lineChart

        setView()

        val markerView = CustomMarkerView(requireContext(), R.layout.custom_marker_view)
        lineChart.marker = markerView // MarkerView를 Chart에 설정

        lineChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
            override fun onValueSelected(e: Entry?, h: Highlight?) {
                if (e != null) {
                    markerView.refreshContent(e, h!!) // 데이터 포인트를 클릭하면 MarkerView에 값을 표시
                    markerView.visibility = View.VISIBLE // MarkerView 표시
                }
            }

            override fun onNothingSelected() {
                markerView.visibility = View.GONE // 아무것도 선택되지 않으면 MarkerView 숨김
            }
        })

        lineChart.setOnClickListener {
            markerView.visibility = View.GONE // 차트를 클릭하면 MarkerView 숨김
        }


    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun setView() {
        salesViewModel.monthPrice.observe(viewLifecycleOwner){
            Log.d(TAG, "그래프 쪽 관찰: $it")
            // y축의 최댓값을 결정하기 위해 설정
            var maxPrice = 0
            val entries: ArrayList<Entry> = ArrayList()

            val entryListData = salesViewModel.createMonthGraphData(it)

            entries.add(Entry(0f, 0f))
            entryListData.forEachIndexed { index, price ->
                maxPrice = kotlin.math.max(maxPrice, price)
                entries.add(Entry((index + 1).toFloat(), price.toFloat()))
            }

            // 최댓값 갱신
            if (maxPrice > maxSales){
                maxSales = maxPrice
                ApplicationClass.sharedPreferenceUtil.setMaxSales(maxPrice)
            }

            val dataSet = LineDataSet(entries, "일별 매출")
            dataSet.color = Color.RED
            dataSet.valueTextColor = Color.BLACK
            dataSet.circleRadius = 4f // 데이터 포인트의 원 크기 설정
            dataSet.setCircleColor(ContextCompat.getColor(requireContext(), R.color.coffee_brown)) // 데이터 포인트의 색상 설정
            dataSet.setDrawValues(false)

            val lineData = LineData(dataSet)

            lineChart.apply {
                description.isEnabled = false
                xAxis.position = XAxis.XAxisPosition.BOTTOM
                xAxis.labelCount = entries.size // x축 레이블 수
                xAxis.setDrawGridLines(false)

                axisLeft.apply {
                    setDrawGridLines(false)
                    axisLineColor = Color.BLACK // Y축 라인 색상 설정
                    textColor = Color.BLACK // Y축 텍스트 색상 설정
                    setDrawAxisLine(true) // y축 선 그리기
                    axisLineWidth = 1f // Y축 라인 굵기 설정
                    axisMaximum = maxSales.toFloat() // 왼쪽 Y축 최댓값 설정

                }
                axisLeft.setDrawGridLines(false)
                axisRight.isEnabled = false

                data = lineData
                invalidate()
            }
        }
    }


    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

CustomMarkerView

class CustomMarkerView(context : Context, layoutResource : Int) : MarkerView(context, layoutResource) {
    val textView = findViewById<TextView>(R.id.tvContent)

    override fun refreshContent(entry : Entry, highlight: Highlight) {
        textView.text = "${makeComma(entry.y.toInt())}"
        super.refreshContent(entry, highlight)
    }
    override fun getOffset(): MPPointF {
        return MPPointF(-width.toFloat(), -height.toFloat()) // 마커 뷰가 데이터 포인트의 좌하단에 표시되도록 설정
    }


    private fun makeComma(price : Int) : String{
        //천단위 콤마
        val comma = DecimalFormat("#,###")
        val priceString = comma.format(price) + "원"

        return priceString
    }

}

LineChart를 구현하기 위해서는
1. Entry 속성의 ArrayList
2. xAxis, aAxis 속성(x축, y축 속성)
3. dataSet 속성
이 필요하다.

entries 리스트에는 그래프에 표시 할 데이터 (x값, y값)를 넣어주면 된다.

필자는 해당 Fragment viewModel에서 그래프를 그릴 데이터를 가공하고 MarkerView 객체를 통해 그래프의 데이터 포인트 클릭 시 값을 불러오는 식으로 구현하였다.

x축, y축 속성은 xAxis, aAxis의 속성을 정의하여 색깔, 선 굵기 등을 정의하였다.

마지막으로 apply 함수에 Invalidate() 함수를 호출하여 그림을 그려주었다.

CustomMarkerView를 구현하기 위해서는
MarkerView 클래스를 상속받아 refreshContent 함수를 override하여 데이터 포인트의 값을 textView로설정하였다.
textView의 위치는 getOffset() 함수로 조정하였다.

결과

Top3 음료도 HorizontalBarChart로 이와 같이 구현하면 된다.

profile
머리가 나쁘면 기록이라도 잘하자

0개의 댓글