CategoryBreakdown
카테고리별 몫을 가로 막대 또는 도넛과 범례로 보여 주는 분포 컴포넌트
목차
한 시점의 합계가 카테고리별로 어떻게 나뉘는지 보여 주는 컴포넌트입니다. 기본은 항목마다 한 줄씩 라벨 · 값 · 비율을 쓰고 그 아래 합계 대비 막대를 그리는 bar
이고, donut 은 링과 범례로 같은 정보를 보여 줍니다. 데이터와 표시 문자열은 호출자가 넘기고, 비율 계산과 반올림은 컴포넌트가 양 플랫폼에서 같게 합니다.
기본 사용법 (Basic Usage)#
CategoryBreakdown(
semanticLabel: '문의 유형별 분포',
items: const [
CoreCategoryBreakdownItem(label: '결제 문의', value: 128, valueLabel: '128건'),
CoreCategoryBreakdownItem(label: '배송 문의', value: 61, valueLabel: '61건'),
CoreCategoryBreakdownItem(label: '기타', value: 12, valueLabel: '12건'),
],
)
CategoryBreakdown(
semanticLabel: '문의 유형별 분포',
items: const [
CoreCategoryBreakdownItem(label: '결제 문의', value: 128, valueLabel: '128건'),
CoreCategoryBreakdownItem(label: '배송 문의', value: 61, valueLabel: '61건'),
CoreCategoryBreakdownItem(label: '기타', value: 12, valueLabel: '12건'),
],
)
빠른 오버라이드 (Chain)#
이미 만든 CategoryBreakdown 인스턴스에 스타일을 덧붙이려면 withStyle 체인을 씁니다. categoryBreakdownStyle
슬롯에 넘기는 것과 같은 결과입니다.
class CategoryBreakdownChainExample extends StatelessWidget {
const CategoryBreakdownChainExample({super.key});
@override
Widget build(BuildContext context) {
return CategoryBreakdown(
semanticLabel: '문의 유형별 분포',
items: const [
CoreCategoryBreakdownItem(label: '결제 문의', value: 128, valueLabel: '128건'),
CoreCategoryBreakdownItem(label: '배송 문의', value: 61, valueLabel: '61건'),
CoreCategoryBreakdownItem(label: '계정 · 로그인', value: 33, valueLabel: '33건'),
CoreCategoryBreakdownItem(label: '기타', value: 12, valueLabel: '12건'),
],
).withStyle(
const CoreCategoryBreakdownStyle(
barHeight: CoreSpace.space12,
rowSpacing: CoreSpace.space16,
),
);
}
}
class CategoryBreakdownChainExample extends StatelessComponent {
const CategoryBreakdownChainExample({super.key});
@override
Component build(BuildContext context) {
return CategoryBreakdown(
semanticLabel: '문의 유형별 분포',
items: const [
CoreCategoryBreakdownItem(label: '결제 문의', value: 128, valueLabel: '128건'),
CoreCategoryBreakdownItem(label: '배송 문의', value: 61, valueLabel: '61건'),
CoreCategoryBreakdownItem(label: '계정 · 로그인', value: 33, valueLabel: '33건'),
CoreCategoryBreakdownItem(label: '기타', value: 12, valueLabel: '12건'),
],
).withStyle(
const CoreCategoryBreakdownStyle(
barHeight: CoreSpace.space12,
rowSpacing: CoreSpace.space16,
),
);
}
}
Live Preview#
코드 보기
class CategoryBreakdownDefaultExample extends StatelessComponent {
const CategoryBreakdownDefaultExample({super.key});
@override
Component build(BuildContext context) {
return CategoryBreakdown(
semanticLabel: '문의 유형별 분포',
items: const [
CoreCategoryBreakdownItem(label: '결제 문의', value: 128, valueLabel: '128건'),
CoreCategoryBreakdownItem(label: '배송 문의', value: 61, valueLabel: '61건'),
CoreCategoryBreakdownItem(label: '계정 · 로그인', value: 33, valueLabel: '33건'),
CoreCategoryBreakdownItem(label: '기타', value: 12, valueLabel: '12건'),
],
);
}
}
class CategoryBreakdownDefaultExample extends StatelessWidget {
const CategoryBreakdownDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return CategoryBreakdown(
semanticLabel: '문의 유형별 분포',
items: const [
CoreCategoryBreakdownItem(label: '결제 문의', value: 128, valueLabel: '128건'),
CoreCategoryBreakdownItem(label: '배송 문의', value: 61, valueLabel: '61건'),
CoreCategoryBreakdownItem(label: '계정 · 로그인', value: 33, valueLabel: '33건'),
CoreCategoryBreakdownItem(label: '기타', value: 12, valueLabel: '12건'),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 문의 유형 · 지출 항목 · 유입 경로 · 등급처럼 합계를 이루는 카테고리의 몫을 보여 줄 때
- 대시보드 · 통계 화면에서 구성비를 한눈에 비교할 때
대신 다른 컴포넌트를 사용하세요:
TrendChart: 값이 시간에 따라 어떻게 변했는지 보여 줄 때Progress: 단일 작업의 완료 비율Stat: 비율 없이 숫자 하나를 강조할 때
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
items |
List<CoreCategoryBreakdownItem> |
필수 | 카테고리 목록(표시 순서) |
variant |
CoreCategoryBreakdownVariant |
bar |
가로 막대(bar) 또는 도넛(donut) |
center |
Widget? / Component? |
null |
도넛 가운데 슬롯(보통 합계). bar 에서는 무시 |
empty |
Widget? / Component? |
null |
항목이 없거나 합계가 0 일 때 그릴 내용 |
semanticLabel |
String? |
null |
분포 전체의 접근 가능한 이름 |
categoryBreakdownStyle |
CoreCategoryBreakdownStyle? |
null |
인스턴스 chrome / 치수 override |
CoreCategoryBreakdownItem#
| 속성 | 타입 | 설명 |
|---|---|---|
label | String | 카테고리 이름 |
value | double | 카테고리 값. 음수는 0 으로 셉니다 |
valueLabel | String? | 호출자가 로케일로 포맷한 값. 없으면 라틴 숫자 |
color |
CoreColor? |
이 카테고리의 색. 없으면 seriesColors 를 순서대로 씁니다 |
CoreCategoryBreakdownStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
seriesColors |
List<CoreColor>? |
Colours assigned to categories in order, cycling when there are more categories than colours. An item's own
color
wins.
|
trackColor |
CoreColor? |
Colour of the empty part of each bar. |
barHeight |
double? |
Height of each bar (logical px). |
barBorderRadius |
CoreBorderRadius? |
Corner radius of the bar track and its fill. |
rowSpacing |
double? |
Vertical space between category rows (logical px). |
rowContentSpacing |
double? |
Vertical space between a row's text line and its bar (logical px). |
labelSpacing |
double? |
Horizontal space between a row's swatch, label, value and share (logical px). |
labelStyle |
CoreTextStyle? |
Typography of each category label. |
valueStyle |
CoreTextStyle? |
Typography of each category value. |
percentStyle |
CoreTextStyle? |
Typography of each category share. |
donutSize |
double? |
Diameter of the donut (logical px). |
donutThickness |
double? |
Ring thickness of the donut (logical px). |
donutSegmentSpacing |
double? |
Empty space between adjacent donut arcs, measured along the ring (logical px). |
legendSpacing |
double? |
Space between the donut and its legend — beside it, or below it once the legend wraps (logical px). |
swatchSize |
double? |
Side of the colour swatch in each donut legend row (logical px). |
swatchBorderRadius |
CoreBorderRadius? |
Corner radius of the legend swatch. |
스타일 시스템 (Style System)#
모든 chrome 은 categoryBreakdownStyle 슬롯 하나로 들어갑니다. 프로젝트 공통 값은 CoreComponentTheme.categoryBreakdown
의 style / variantStyles 에 둡니다.
CoreCategoryBreakdownStyle.default*
→ CoreCategoryBreakdownTheme.style // 프로젝트 공통
→ CoreCategoryBreakdownTheme.variantStyles[variant] // variant 별
→ widget.categoryBreakdownStyle // 인스턴스별
카테고리 색은 seriesColors(시맨틱 토큰 팔레트)를 순서대로 돌려 쓰고, 항목의 color 가 있으면 그것이 이깁니다.
변형 (Variants)#
bar#
항목마다 한 줄: 라벨과 값 · 비율, 그 아래 합계 대비 가로 막대. 라벨이 행 안에 있어 색과 범례를 오갈 필요가 없고, 좁은 폭 · 큰 글자 · 긴 라벨에서도 깨지지 않으므로 기본값입니다. 막대는 읽기 시작 쪽에서 자랍니다.
donut#
링 하나에 카테고리마다 호를 그리고 옆에 범례를 둡니다. 범례가 도넛 지름보다 좁아질 폭에서는 범례가 도넛 아래로 내려갑니다. 폭에 따라 variant 를 자동으로 바꾸지는 않습니다 — 어떤 variant 를 쓸지는 호출자가 정합니다.
동작 스펙 (Behavior)#
비율#
비율은 합계 대비 몫이고, 화면에 찍히는 정수 퍼센트는 최대 잔여 반올림으로 합이 정확히 100 이 되게 맞춥니다(세 등분은 34 · 33 · 33). 계산은
coui_core 의 CoreCategoryBreakdownGeometry 한 곳이 하고 두 플랫폼이 같은 값을 그립니다.
상태 전환#
항목이 없거나 합계가 0 이면 empty 슬롯을 그립니다(없으면 아무것도 그리지 않음). 로딩 중에는 호출자가 같은 자리에 Skeleton
을 넣습니다.
줄바꿈#
라벨과 값 · 비율이 한 줄에 들어가면 양 끝에 배치하고, 들어가지 않으면 값 · 비율이 라벨 아래로 내려갑니다.
애니메이션#
전환 애니메이션이 없습니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
좁은 화면에서는 기본 bar 를 쓰기
CategoryBreakdown(items: items) // variant 기본값 bar
이유: 막대는 라벨 · 값 · 비율을 한 행에 함께 두므로 색을 구별하지 못해도, 글자를 크게 키워도 읽힙니다.
❌ Don't#
합계를 이루지 않는 값을 섞기
// ❌ '평균 응답 시간' 은 다른 항목과 더해 합계가 되지 않는다
CoreCategoryBreakdownItem(label: '평균 응답 시간', value: 42)
이유: 비율은 모든 항목의 합 대비로 계산됩니다. 합계를 이루지 않는 값이 섞이면 모든 비율이 틀립니다.
접근성 (Accessibility)#
역할#
bar 는 semanticLabel 을 이름으로 갖는 목록이고(Web role="list"), donut
은 같은 이름의 그룹 안에 범례 목록을 둡니다. 항목마다 「라벨, 값, 비율」 한 문장을 이름으로 갖습니다(Flutter 항목별 Semantics(label:), Web
role="listitem" + aria-label).
키보드#
처리하는 키가 없습니다.
스크린 리더#
분포 이름을 읽은 뒤 항목을 「결제 문의, 128건, 58%」처럼 읽습니다. 화면의 개별 텍스트 · 막대 · 링 · 견본은 같은 내용을 두 번 읽지 않도록 보조 기술에서 숨깁니다(Flutter
ExcludeSemantics, Web aria-hidden).
포커스 관리#
포커스를 받는 요소가 없습니다.
알려진 제약#
- 항목을 눌러 선택하거나 강조하는 상호작용은 없습니다.
감속 모션 · 고대비 · 강제 색상 · 최소 터치 타겟처럼 전 컴포넌트에 공통으로 걸리는 축은 전역 접근성 축에서 다룹니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | CategoryBreakdown | CategoryBreakdown |
| 막대 | DecoratedBox + FractionallySizedBox |
<div> 트랙 + 퍼센트 폭 채움 |
| 도넛 | CustomPaint 호(arc) |
인라인 <svg> <circle> 점선 호 |
| 범례 내림 | LayoutBuilder — 폭 < 지름 × 2 + 간격 |
flex-wrap + 범례 basis = 지름 |
| 줄바꿈 | OverflowBar |
flex-wrap + justify-between |
| 오른쪽→왼쪽 | 막대 AlignmentDirectional, 도넛 반시계 |
블록 흐름, rtl:-scale-x-100 |
관련 컴포넌트 (Related Components)#
- TrendChart: 시간에 따른 추이
- Progress: 단일 진행률
- Stat: 지표 카드