본문 바로가기

Android/차트그리기

2. Android Bar Chart 직접 그리기

이번엔 다음과 같이 Bar Chart를 그려볼 예정입니다. 역시 간단한 Bar Chart입니다.

 

먼저 다음과 같이 BarChartView라는 Custom View를 생성하고, onDraw함수를 override해 줍니다.

이 onDraw() 함수내에서 원하는 그림을 그리면 화면에 나타나게 됩니다.

class BarChartView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
    }

}

 

draw함수내에 다음과 같이 x, y 축을 그리는 코드를 작성해보았습니다.

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas ?: return

    val leftSpace = 90f
    val topSpace = 60f
    val rightSpace = 60f
    val bottomSpace = 90f

    // x, y 좌표축 라인 그리기
    paint.color = Color.BLACK
    val axisLineWidth = 1f
    val halfAxisLineWidth = axisLineWidth / 2f

    paint.strokeWidth = axisLineWidth
    val xAxisY = height - bottomSpace - halfAxisLineWidth
    canvas.drawLine(leftSpace, xAxisY, width - rightSpace, xAxisY, paint)
    val yAxisX = leftSpace + halfAxisLineWidth
    canvas.drawLine(yAxisX, topSpace, yAxisX, height - bottomSpace, paint)
}

다음과 같이 x, y축이 그려지는 것을 확인할 수 있습니다.

위 코드를 보시면 xAxisY, yAxisX를 계산할때 halfAxisLineWidth값을 더하고 빼는 것을 볼수 있는데요, 이는 아래 좌측 그림과 같이 drawLine()함수를 이용하여 line을 그릴때 그 기준이 되는 선이 노란색 점선이기 때문입니다. 

(노란색 점선을 중심으로 하여 paint.strokeWidth가 적용이 됩니다.)

만일, y좌표축 라인을 다음과 같이 그냥 leftSpace을 x값으로 놓고 그리게 되면, 아래 우측 그림과 같이 의도한 것과는 다르게 좌측으로 치우쳐져 그려지게 됩니다.

val yAxisX = leftSpace
canvas.drawLine(yAxisX, topSpace, yAxisX, height - bottomSpace, paint)

 

이제, 앞선 LineChartView와 마찬가지로 Int형 배열을 데이터로 전달받아 각 값을 Bar형태로 그려보겠습니다.

Bar Chart를 그리는 기본 개념은 다음과 같습니다.

- 테두리 space및 x,y좌표축 라인을 제외한 나머지 노란색 부분이 실제 Bar Chart가 그려질 영역입니다.

- 노란색 영역의 width를 data 개수로 나누면 1개 item이 차지해야 할 공간을 계산할수 있습니다. itemWidth로 표시한 부분입니다.

- 각각의 item 공간 가운데를 기준으로, lineWidth를 barWidth로 설정하여 라인을 그려줍니다.

   (이때 당연히 barWidth는 itemWidth보다 작아야 하므로 itemWidth * 0.7 과 같이 비율로 계산해주면 좋을것 같습니다.)

 

이를 코드로 작성해보면 다음과 같습니다.

override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)

    ......
    
    val dataList = dataList ?: return
    if (dataList.isEmpty()) {
        return
    }

    // View는 원점이 좌상단임을 고려하여 상/하 반전 처리
    val mappedDataList = dataList.map { value ->
        height - bottomSpace - topSpace - axisLineWidth - value.toFloat()
    }

    paint.color = Color.MAGENTA
    var x = leftSpace + axisLineWidth // 노란색 영역의 x축 시작점을 계산
    val itemWidth = (width - x - rightSpace) / mappedDataList.count() // 1개 bar item이 그려질 공간을 계산
    val barWidth = itemWidth * 0.7f // item 영역내 그려질 bar의 두께를 itemWidth보다 작게 계산
    x += itemWidth / 2 // 첫번째 bar는 item영역내 x축을 기준으로 가운데에 그려야 하므로
    paint.strokeWidth = barWidth
    mappedDataList.forEach { value ->
        canvas.drawLine(x, height - bottomSpace - axisLineWidth, x, value, paint)
        x += itemWidth
    }
}

fun setDataList(dataList: ArrayList<Int>) {
    if (dataList.isEmpty()) {
        return
    }
    this.dataList = dataList
    invalidate()
}

데이터는 다음과 같이 설정하여 차트를 그렸습니다.

chartView.setDataList(arrayListOf(0, 30, 45, 150, 600, 270, 300, 210, 90))

 

x, y좌표축 라인 두께가 얇아서 티가 잘 나지 않는데요, 라인 두께를 좀 두껍게 해서 다시 그려보면 다음과 같습니다.

좌표축 라인과 bar가 서로 겹치지 않고 그려지는 것을 확인할수 있습니다.

헌데, 아직 문제가 남아있습니다. 앞서 Line Chart도 마찬가지였는데요, 전달되는 data값이 Bar Chart영역의 높이를 벗어나는 경우에 대한 대비가 이루어지지 않았습니다.

 

이를 위해 data목록 중 가장 큰값이 위에 "Bar Chart를 그리는 기본 개념"에서 보여드린 그림의 노란색 영역의 높이가 될수 있도록 보정하는 코드를 추가한 전체 코드는 다음과 같습니다.

class BarChartView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {

    private var dataList: ArrayList<Int>? = null
    private val paint = Paint().apply {
        isAntiAlias = true
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas ?: return

        val leftSpace = 90f
        val topSpace = 60f
        val rightSpace = 60f
        val bottomSpace = 90f

        // x, y 좌표축 라인 그리기
        paint.color = Color.BLACK
        val axisLineWidth = 1f
        val halfAxisLineWidth = axisLineWidth / 2f

        paint.strokeWidth = axisLineWidth
        val xAxisY = height - bottomSpace - halfAxisLineWidth
        canvas.drawLine(leftSpace, xAxisY, width - rightSpace, xAxisY, paint)
        val yAxisX = leftSpace + halfAxisLineWidth
        canvas.drawLine(yAxisX, topSpace, yAxisX, height - bottomSpace, paint)

        val dataList = dataList ?: return
        if (dataList.isEmpty()) {
            return
        }
        val maxValue = dataList.maxOrNull() ?: return

        var scale = 1f
        // 값이 뷰 영역을 벗어나지 않고 화면내에 그려질수 있도록 비율 계산
        val chartAreaHeight = height - bottomSpace - topSpace - axisLineWidth
        if (maxValue > chartAreaHeight) {
            scale = chartAreaHeight / maxValue
        }

        // View는 원점이 좌상단임을 고려하여 상/하 반전 처리
        val mappedDataList = dataList.map { value ->
            height - bottomSpace - topSpace - axisLineWidth - value.toFloat() * scale
        }

        paint.color = Color.MAGENTA
        var x = leftSpace + axisLineWidth // 노란색 영역의 x축 시작점을 계산
        val itemWidth = (width - x - rightSpace) / mappedDataList.count() // 1개 bar item이 그려질 공간을 계산
        val barWidth = itemWidth * 0.7f // item 영역내 그려질 bar의 두께를 itemWidth보다 작게 계산
        x += itemWidth / 2 // 첫번째 bar는 item영역내 x축을 기준으로 가운데에 그려야 하므로
        paint.strokeWidth = barWidth
        mappedDataList.forEach { value ->
            canvas.drawLine(x, height - bottomSpace - axisLineWidth, x, value + topSpace, paint)
            x += itemWidth
        }
    }

    fun setDataList(dataList: ArrayList<Int>) {
        if (dataList.isEmpty()) {
            return
        }
        this.dataList = dataList
        invalidate()
    }

}

데이터를 다음과 같이 화면을 벗어나는 값을 설정하여, 값 보정전과 후가 어떻게 다르게 표시되는지 확인해 보았습니다.

chartView.setDataList(arrayListOf(0, 30, 45, 150, 1500, 270, 300, 210, 90))

좌측의 경우 5번째 항목이 Chart영역 밖을 벗어났지만, 우측의 경우 Chart영역 내에서만 그려지는 것을 볼수 있습니다.

 

이렇게 간단한 Bar Chart를 그리는 방법을 알아보았습니다.

 
반응형