CategoryBreakdown | CoUI
본문으로 건너뛰기
LogoCoUI

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#

사용 시기 (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#

속성타입설명
labelString카테고리 이름
valuedouble카테고리 값. 음수는 0 으로 셉니다
valueLabelString?호출자가 로케일로 포맷한 값. 없으면 라틴 숫자
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)#

항목FlutterWeb
클래스명CategoryBreakdownCategoryBreakdown
막대 DecoratedBox + FractionallySizedBox <div> 트랙 + 퍼센트 폭 채움
도넛 CustomPaint 호(arc) 인라인 <svg> <circle> 점선 호
범례 내림 LayoutBuilder — 폭 < 지름 × 2 + 간격 flex-wrap + 범례 basis = 지름
줄바꿈 OverflowBar flex-wrap + justify-between
오른쪽→왼쪽 막대 AlignmentDirectional, 도넛 반시계 블록 흐름, rtl:-scale-x-100