1. 개요
안드로이드의 유명한 차트 라이브러리인 MPAndroidChart의 PieChart를 분석해 봤다.
아래는 라이브러리의 PieChart를 Compose로 나타낸 것이다.
AndroidView(
factory = { context ->
PieChart(context).apply {
// 기본 설정
description.isEnabled = false
isRotationEnabled = false
isHighlightPerTapEnabled = true
isDrawHoleEnabled = true
holeRadius = 50f
setDrawEntryLabels(true)
setUsePercentValues(true)
// 범례 설정
legend.isEnabled = true
}
},
modifier = modifier,
update = { chart ->
// 데이터 설정
val dataSet = PieDataSet(data, "").apply {
colors = listOf(
"#FF6B6B".toColorInt(),
"#4ECDC4".toColorInt(),
"#45B7D1".toColorInt(),
"#96CEB4".toColorInt(),
"#FFEAA7".toColorInt()
)
valueTextSize = 12f
valueTextColor = Color.BLACK
}
chart.data = PieData(dataSet)
chart.invalidate()
}
)
MPAndroidChart 라이브러리가 꽤나 오래전에 자바로 개발되었기 때문에 자바 스타일 컨벤션이 많이 남아 있었다.
이를 분석해서 나만의 PieChart를 만들 예정이기에 개요에서는 주로 보완할 내용에 집중했다.
1) 차트의 색상
먼저 차트의 색상이다.
각 데이터가 5개라면 차트 색상을 5개 넣어야 한다.
여기서 데이터 개수만큼 색상을 설정하지 않으면 누락이 발생할 수 있다.
이 문제를 방지하기 위해 색상을 필수로 넣도록 강제하는 기능이 있었으면 좋겠다고 생각했다.
두 번째로 Color의 표현 방식도 개선하고 싶었다.
기존 코드에서는 android.graphics.Color를 사용했으며, 이는 내부적으로 색상을 Int 값으로 처리한다.
반면, 현재 Compose에서는 androidx.compose.ui.graphics.Color를 사용한다.
Int 기반 색상 표현은 RGB값만 봤을 때 어떤 색깔인지 명확하지 않아 개선할 필요가 있어 보였다.
2) 접근 방식
어떤 것은 속성값으로 접근하고, 어떤 것은 메서드로 동작한다는 점이다.
예를 들면 isDrawHoleEnabled는 속성으로 접근하지만 setDrawEntryLabels나 setUsePercentValues는 메서드로 접근한다.
PieChart(context).apply {
// 기본 설정
description.isEnabled = false
isRotationEnabled = false
isHighlightPerTapEnabled = true
isDrawHoleEnabled = true
holeRadius = 50f
setDrawEntryLabels(true)
setUsePercentValues(true)
// 범례 설정
legend.isEnabled = true
}
이를 통일하는 것이 좋을 것 같다.
추가로 이 코드를 작성하면,
setDrawHoleEnabled(true)
'Use of setter method instead of property access syntax'라는 경고가 발생한다.
이를 아래처럼 수정해야 한다.
isDrawHoleEnabled = true
그렇다면 왜 setDrawEntryLabels, setUsePercentValues는 안될까?
그건 PieChart에 getter/setter 함수가 없기 때문이라고 한다.
setDrawHoleEnabled는 getter/setter가 있지만 setDrawEntryLabels와 setUsePercentValues는 없다는 게 통일이 안 되는 것 같다.
그래서 이번 커스텀 차트에서는 이를 통일해보려 한다.
2. 데이터 분석
두 번째로 파이 차트를 구성하는데 필요한 데이터를 분석해 보자.
val pieData = remember {
listOf(
PieEntry(30f, "국밥"),
PieEntry(25f, "초밥"),
PieEntry(20f, "떡볶이"),
PieEntry(15f, "치킨"),
PieEntry(10f, "연어")
)
}
파이차트에 필요한 데이터는 PieEntry를 통해 구성한다.
PieEntry는 파이차트의 각 영역에 대한 크기와 카테고리 이름을 의미한다.
PieEntry는 Entry를 상속받았고, Entry는 BaseEntry를 상속받는다.
먼저 PieEntry의 내부에 대해 알아보자.
public PieEntry(float value, String label) {
super(0f, value);
this.label = label;
}
만약 PieEntry(30f, "국밥")이라면,
첫 번째 인자값은 차트 전체에서 30만큼의 비중을 가진다는 의미로 실제 파이 차트의 크기를 결정하고,
두 번째 인자값은 해당 영역의 카테고리 이름이다.
그리고 super를 통해 부모 클래스의 생성자를 호출한 후, label을 초기화한다.
/**
* A Entry represents one single entry in the chart.
*
* @param x the x value
* @param y the y value (the actual value of the entry)
*/
public Entry(float x, float y) {
super(y);
this.x = x;
}
Entry 클래스로 왔다.
여기서 각 매개변수명은 x와 y인데 이는 x축으로 가로축을 의미하고, y는 y축으로 세로축을 의미한다.
파이차트이기 때문에 앞서 x축에 값이 있을 필요가 없다.
그렇기에 PieEntry의 x에 0f가 들어갔던 것이고, y는 value가 된다.
여기서도 Entry의 부모 클래스인 BaseEntry를 호출하고, 전달받은 x값을 초기화한다.
public BaseEntry(float y) {
this.y = y;
}
최상단 부모 클래스인 BaseEntry이다.
여기서 y 값을 초기화 함을 알 수 있다.
아마 PieEntry의 첫 번째 인자값(30f)을 토대로 데이터를 구성하는 것 같다.
정리하면 PieEntry는 x와 y값의 초기화를 Entry에 위임한 것이고, Entry는 y값의 초기화를 BaseEntry에 위임한 것이다.
PieEntry -> Entry -> BaseEntry
그렇다면 왜 이렇게 번거로운 절차를 거쳐야 했을까?
BaseEntry의 abstract class에서 첫 번째 답을 찾았다.
이유는 일관성 때문이라고 생각한다.
가장 부모 클래스인 BaseEntry는 abstract class이다.
즉, 구현체가 아닌 해당 속성과 메서드가 있어야 한다고 강제하는 것이다.
이를 구현하는 Entry 클래스는 모든 차트의 기초가 되고,
PieEntry 같은 세부 차트는 Entry를 상속받는다.
아마 차트를 만드는데 필요한 속성을 일관되게 적용하기 위해서가 아닐까라고 추측해 본다.
두 번째는 캡슐화와 책임분리라고 생각하는데 이는 BaseEntry의 y값은 private으로 선언되어 있다는 점에서 답을 찾았다.
자식 클래스는 부모의 private 필드에 접근할 수 없으므로 부모가 제공하는 생성자를 통해서만 초기화할 수 있다.
즉, super를 통해 부모 클래스에게 필드를 초기화하는 책임을 위임한 것이다.
앞서 PieEntry에서는 label만 초기화한 후, x와 y의 초기화는 Entry로 위임하였다.
Entry는 x만 초기화한 후, y의 초기화는 BaseEntry로 위임하였다.
이렇게 캡슐화와 책임분리를 했다고 생각한다.

3. 파이차트 분석
이제는 파이차트를 실제로 그리는 부분에 대해 분석해 보자.
여기서는 메서드가 많아 주요한 메서드만 분석했다.
1) PieChart 내부
먼저 PieChart는 PieRadarChartBase를 상속받고, PieRadarChartBase는 Chart를 상속받는다.
PieChart 객체가 생성될 때 내부적으로 init() 메서드부터 시작해서 차트가 그려진다.
init() : 차트 초기화 및 렌더러 설정
@Override
protected void init() {
super.init();
mRenderer = new PieChartRenderer(this, mAnimator, mViewPortHandler);
mXAxis = null;
mHighlighter = new PieHighlighter(this);
}
onDraw() : 차트 그리기 (렌더러를 통해 데이터와 하이라이트, 범례 등을 화면에 그림)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mData == null)
return;
mRenderer.drawData(canvas);
if (valuesToHighlight())
mRenderer.drawHighlighted(canvas, mIndicesToHighlight);
mRenderer.drawExtras(canvas);
mRenderer.drawValues(canvas);
mLegendRenderer.renderLegend(canvas);
drawDescription(canvas);
drawMarkers(canvas);
}
calculateOffsets() : 차트 중심점, 반지름, 오프셋 계산
calcMinMax() : 데이터의 최소/최대 값 계산. 파이차트에서 내부적으로 calcAngles() 호출
calcAngles() : 각 데이터 조각이 차지할 각도 계산
getMarkerPosition() : 터치된 조각의 마커 위치 계산

2) Renderer 내부
init메서드 내부를 보면 PieChartRenderer 객체를 생성한다.
실제로 차트는 Renderer 객체를 통해 그려지며, Renderer가 데이터를 해석해서 Canvas에 그려줘야 화면에 보인다.
PieChartRenderer는 DataRenderer를 상속받고, DataRenderer는 Renderer를 상속받는다.
이번엔 Renderer부터 확인해 보자.
Renderer는 모든 Rendere의 최상위 추상 클래스로 공통 속성인 ViewPortHandler를 관리한다.
ViewPortHandler는 차트의 화면 영역, 크기, 스케일, 오프셋 등을 관리하는 유틸리티 클래스로 차트가 그려질 수 있는 Canvas 영역을 정의한다.
public abstract class Renderer {
/**
* the component that handles the drawing area of the chart and it's offsets
*/
protected ViewPortHandler mViewPortHandler;
public Renderer(ViewPortHandler viewPortHandler) {
this.mViewPortHandler = viewPortHandler;
}
}
DataRenderer는 모든 차트에서 공통으로 사용하는 렌더링의 부모 클래스로 관련 속성과 메서드를 제공한다.
즉, 데이터 중심의 차트를 그리는 Renderer의 기본 골격을 잡아준다.
public DataRenderer(ChartAnimator animator, ViewPortHandler viewPortHandler) {
super(viewPortHandler);
this.mAnimator = animator;
mRenderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mRenderPaint.setStyle(Style.FILL);
mDrawPaint = new Paint(Paint.DITHER_FLAG);
mValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mValuePaint.setColor(Color.rgb(63, 63, 63));
mValuePaint.setTextAlign(Align.CENTER);
mValuePaint.setTextSize(Utils.convertDpToPixel(9f));
mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHighlightPaint.setStyle(Paint.Style.STROKE);
mHighlightPaint.setStrokeWidth(2f);
mHighlightPaint.setColor(Color.rgb(255, 187, 115));
}
마지막으로 PieChartRenderer는 파이차트만 그리는 Renderer로 파이차트에 특화된 페인트들이 추가된다.
public PieChartRenderer(PieChart chart, ChartAnimator animator,
ViewPortHandler viewPortHandler) {
super(animator, viewPortHandler);
mChart = chart;
mHolePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mHolePaint.setColor(Color.WHITE);
mHolePaint.setStyle(Style.FILL);
mTransparentCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mTransparentCirclePaint.setColor(Color.WHITE);
mTransparentCirclePaint.setStyle(Style.FILL);
mTransparentCirclePaint.setAlpha(105);
mCenterTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCenterTextPaint.setColor(Color.BLACK);
mCenterTextPaint.setTextSize(Utils.convertDpToPixel(12f));
mValuePaint.setTextSize(Utils.convertDpToPixel(13f));
mValuePaint.setColor(Color.WHITE);
mValuePaint.setTextAlign(Align.CENTER);
mEntryLabelsPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mEntryLabelsPaint.setColor(Color.WHITE);
mEntryLabelsPaint.setTextAlign(Align.CENTER);
mEntryLabelsPaint.setTextSize(Utils.convertDpToPixel(13f));
mValueLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mValueLinePaint.setStyle(Style.STROKE);
}
정리하면 Renderer는 ViewPortHandler만 관리하여 그릴 영역을 관리하고,
DataRender는 페인트, 애니메이션 값, 그리기 기능의 공통 로직을 제공한다.
마지막으로 PieChartRenderer는 파이차트 전용 페인트, 비트맵, 실게 그리기를 구현한다.

4. 마무리
처음으로 라이브러리의 구조에 대한 분석을 해봤다.
생각보다 복잡하고 다양한 경우의 수를 고려하여 설계한 것이 느껴졌다.
특히 상속구조를 통한 일관성 유지, 캡슐화, 책임 분리 등 많은 것을 배울 수 있었다.
이번 분석을 바탕으로 Compose로 나만의 PieChart를 구현해 볼 계획이다.
구현했습니다. 링크는 여기.
[Android] MPAndroidChart의 PieChart를 분석해서 나만의 CustomPieChart 만들기 (2)
1. 개요앞서 MPAndroidChart를 분석했다. 이번에는 분석한 내용을 바탕으로 나만의 PieChart를 구현해보고자 한다. 2. 데이터 정의MPAndroidChart에서는 PieEntry를 통해 파이 차트를 그리는데 필요한 데이터
sfida.tistory.com
그리고 관련 GitHub Repository입니다.
GitHub - Meezzi/custom-pie-chart-android: Compose로 직접 구현한 Pie Chart
Compose로 직접 구현한 Pie Chart. Contribute to Meezzi/custom-pie-chart-android development by creating an account on GitHub.
github.com
'Android' 카테고리의 다른 글
| [Android] ViewModel에서 Context 없이 예외 처리 하기 (1) | 2025.08.01 |
|---|---|
| [Android] MPAndroidChart의 PieChart를 분석해서 나만의 CustomPieChart 만들기 (2) (4) | 2025.07.18 |