Chart
Data Visualization
선언적 ChartSpec 을 렌더하는 차트.
Usage
런타임 ChartSpec으로 차트 유형과 인코딩을 선언해 동적으로 렌더할 때 사용한다.
import
import
import { Chart } from '@mildang/design-system/Chart';예제를 복사해 쓸 때 필요한 준비
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
API Reference
Chart Props
Prop
Type
Default
ChartSpec
지정 안 함
string
지정 안 함
string | number
320
string | number
100%
Card
공개 Props 없음
ChartAxis
Prop
Type
Default
"bottom" | "left" | "right" | "top"
지정 안 함
any
지정 안 함
boolean
지정 안 함
boolean
지정 안 함
string
지정 안 함
number
지정 안 함
(value: unknown) => string
지정 안 함
ChartContainer
Prop
Type
Default
(dims: { innerWidth: number; innerHeight: number; }) => ReactNode
지정 안 함
string
지정 안 함
Partial<ChartMargin>
지정 안 함
ChartGrid
Prop
Type
Default
number
지정 안 함
number
지정 안 함
boolean
false
number
지정 안 함
number
지정 안 함
boolean
true
any
지정 안 함
any
지정 안 함
ChartLegend
Prop
Type
Default
LegendItem[]
지정 안 함
string
지정 안 함
"column" | "row"
row
ChartTooltip
Prop
Type
Default
boolean
지정 안 함
(data: TDatum) => ReactNode
지정 안 함
TDatum
지정 안 함
number
지정 안 함
number
지정 안 함
ChartTooltipContent
Prop
Type
Default
ChartTooltipItem[]
지정 안 함
(value: string | number) => ReactNode
지정 안 함
boolean
false
boolean
false
"line" | "dot"
dot
string
지정 안 함
예제
모든 차트
import type { ComponentProps } from 'react';
import { CHART_COLOR_PRESET, Chart, type ChartSpec } from '@mildang/design-system/Chart';
import { Box, Grid, Stack } from '@mildang/styled-system/jsx';
type Story = { args?: ComponentProps<typeof Chart> };
/* ─── 공용 샘플 데이터 (밀당 도메인, 전부 결정적) ─── */
const subjectScores = [
{ 과목: '국어', 평균점수: 82 },
{ 과목: '영어', 평균점수: 76 },
{ 과목: '수학', 평균점수: 68 },
{ 과목: '과학', 평균점수: 88 },
{ 과목: '사회', 평균점수: 74 },
];
const subjectClassScores = [
{ 과목: '국어', 반: '1반', 점수: 84 },
{ 과목: '국어', 반: '2반', 점수: 78 },
{ 과목: '영어', 반: '1반', 점수: 72 },
{ 과목: '영어', 반: '2반', 점수: 81 },
{ 과목: '수학', 반: '1반', 점수: 66 },
{ 과목: '수학', 반: '2반', 점수: 74 },
];
const monthlyMinutes = [
{ 월: '1월', 학습시간: 620 },
{ 월: '2월', 학습시간: 540 },
{ 월: '3월', 학습시간: 710 },
{ 월: '4월', 학습시간: 680 },
{ 월: '5월', 학습시간: 820 },
{ 월: '6월', 학습시간: 760 },
];
/** 월×과목 학습 시간 long 데이터 — 결정적 공식으로 생성 */
const monthlySubjectMinutes = ['1월', '2월', '3월', '4월', '5월', '6월'].flatMap((월, mi) =>
['국어', '영어', '수학'].map((과목, si) => ({
월,
과목,
학습시간: 120 + ((mi * 53 + si * 91) % 180),
})),
);
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const monthlyClassScores = [
{ 월: '3월', 반: '1반', 평균점수: 72 },
{ 월: '3월', 반: '2반', 평균점수: 68 },
{ 월: '4월', 반: '1반', 평균점수: 75 },
{ 월: '4월', 반: '2반', 평균점수: 71 },
{ 월: '5월', 반: '1반', 평균점수: 78 },
{ 월: '5월', 반: '2반', 평균점수: 76 },
{ 월: '6월', 반: '1반', 평균점수: 81 },
{ 월: '6월', 반: '2반', 평균점수: 74 },
];
/* ─── 막대 계열 ─── */
export const BarChart: Story = {
args: {
spec: {
chartType: 'Bar Chart',
data: { values: subjectScores },
semanticTypes: { 평균점수: 'Score' },
encodings: { x: '과목', y: { field: '평균점수', label: '평균 점수' } },
},
},
};
export const GroupedBarChart: Story = {
args: {
spec: {
chartType: 'Grouped Bar Chart',
data: { values: subjectClassScores },
semanticTypes: { 점수: 'Score' },
encodings: { x: '과목', y: '점수', color: '반' },
},
},
};
export const StackedBarChart: Story = {
args: {
spec: {
chartType: 'Stacked Bar Chart',
data: {
values: [
{ 월: '3월', 유형: '객관식', 풀이수: 320 },
{ 월: '3월', 유형: '주관식', 풀이수: 180 },
{ 월: '3월', 유형: '서술형', 풀이수: 60 },
{ 월: '4월', 유형: '객관식', 풀이수: 410 },
{ 월: '4월', 유형: '주관식', 풀이수: 210 },
{ 월: '4월', 유형: '서술형', 풀이수: 90 },
{ 월: '5월', 유형: '객관식', 풀이수: 380 },
{ 월: '5월', 유형: '주관식', 풀이수: 240 },
{ 월: '5월', 유형: '서술형', 풀이수: 110 },
],
},
semanticTypes: { 월: 'Month', 풀이수: 'Count' },
encodings: { x: '월', y: '풀이수', color: '유형' },
},
},
};
/* ─── 영역 계열 ─── */
export const AreaChart: Story = {
args: {
spec: {
chartType: 'Area Chart',
data: { values: monthlyMinutes },
semanticTypes: { 월: 'Month', 학습시간: 'DurationMinutes' },
encodings: { x: '월', y: { field: '학습시간', label: '학습 시간' } },
},
},
};
export const StackedAreaChart: Story = {
args: {
spec: {
chartType: 'Stacked Area Chart',
data: { values: monthlySubjectMinutes },
semanticTypes: { 월: 'Month', 학습시간: 'DurationMinutes' },
encodings: { x: '월', y: '학습시간', color: '과목' },
},
},
};
export const Streamgraph: Story = {
args: {
spec: {
chartType: 'Streamgraph',
data: { values: monthlySubjectMinutes },
semanticTypes: { 월: 'Month', 학습시간: 'DurationMinutes' },
encodings: { x: '월', y: '학습시간', color: '과목' },
},
},
};
export const RangeAreaChart: Story = {
args: {
spec: {
chartType: 'Range Area Chart',
data: {
values: [
{ 월: '1월', 최저: 52, 최고: 94, 평균: 71 },
{ 월: '2월', 최저: 48, 최고: 90, 평균: 68 },
{ 월: '3월', 최저: 55, 최고: 96, 평균: 74 },
{ 월: '4월', 최저: 58, 최고: 93, 평균: 76 },
{ 월: '5월', 최저: 61, 최고: 98, 평균: 79 },
{ 월: '6월', 최저: 57, 최고: 95, 평균: 77 },
],
},
semanticTypes: { 월: 'Month', 최저: 'Score', 최고: 'Score', 평균: 'Score' },
encodings: {
x: '월',
y0: { field: '최저', label: '최저 점수' },
y1: { field: '최고', label: '최고 점수' },
mid: { field: '평균', label: '평균 점수' },
},
},
},
};
/* ─── 격자·달력 ─── */
export const Heatmap: Story = {
args: {
spec: {
chartType: 'Heatmap',
data: {
values: ['1반', '2반', '3반'].flatMap((반, ci) =>
['1단원', '2단원', '3단원', '4단원', '5단원'].map((단원, ui) => ({
반,
단원,
정답률: 55 + ((ci * 17 + ui * 13) % 40),
})),
),
},
semanticTypes: { 정답률: 'Percent' },
encodings: { x: '단원', y: '반', value: { field: '정답률', label: '정답률' } },
chartProperties: { showValues: true, showLegend: true },
},
},
};
export const CalendarHeatmap: Story = {
args: {
spec: {
chartType: 'Calendar Heatmap',
data: {
values: Array.from({ length: 112 }, (_, i) => ({
날짜: new Date(Date.UTC(2026, 0, 5 + i)).toISOString().slice(0, 10),
풀이수: i % 6 === 5 ? 0 : ((i * 7) % 19) + (i % 4),
})),
},
semanticTypes: { 날짜: 'Date', 풀이수: 'Count' },
encodings: { date: '날짜', value: { field: '풀이수', label: '풀이 수' } },
chartProperties: { showLegend: true },
},
},
};
/* ─── 분포 계열 ─── */
export const Histogram: Story = {
args: {
spec: {
chartType: 'Histogram',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: { field: '점수', label: '점수' } },
chartProperties: { binCount: 8 },
},
},
};
export const BoxPlot: Story = {
args: {
spec: {
chartType: 'Box Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { y: { field: '점수', label: '점수' }, x: '반' },
},
},
};
export const ViolinPlot: Story = {
args: {
spec: {
chartType: 'Violin Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { y: { field: '점수', label: '점수' }, x: '반' },
},
},
};
export const DensityPlot: Story = {
args: {
spec: {
chartType: 'Density Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: { field: '점수', label: '점수' }, color: '반' },
},
},
};
export const EcdfPlot: Story = {
args: {
spec: {
chartType: 'ECDF Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: { field: '점수', label: '점수' }, color: '반' },
},
},
};
export const StripPlot: Story = {
args: {
spec: {
chartType: 'Strip Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: '반', y: { field: '점수', label: '점수' } },
},
},
};
/* ─── 카테고리 비교 ─── */
export const LollipopChart: Story = {
args: {
spec: {
chartType: 'Lollipop Chart',
data: { values: subjectScores },
semanticTypes: { 평균점수: 'Score' },
encodings: { x: '과목', y: { field: '평균점수', label: '평균 점수' } },
chartProperties: { showValueText: true },
},
},
};
export const WaterfallChart: Story = {
args: {
spec: {
chartType: 'Waterfall Chart',
data: {
values: [
{ 월: '3월', 순증감: 42 },
{ 월: '4월', 순증감: 18 },
{ 월: '5월', 순증감: -12 },
{ 월: '6월', 순증감: 25 },
{ 월: '7월', 순증감: -8 },
],
},
semanticTypes: { 월: 'Month', 순증감: 'Count' },
encodings: { x: '월', y: { field: '순증감', label: '수강생 순증감' } },
chartProperties: { totalLabel: '순증 합계' },
},
},
};
export const PyramidChart: Story = {
args: {
spec: {
chartType: 'Pyramid Chart',
data: {
values: [
{ 학년: '중1', 남학생: 120, 여학생: 132 },
{ 학년: '중2', 남학생: 145, 여학생: 138 },
{ 학년: '중3', 남학생: 160, 여학생: 155 },
{ 학년: '고1', 남학생: 130, 여학생: 148 },
{ 학년: '고2', 남학생: 110, 여학생: 118 },
{ 학년: '고3', 남학생: 88, 여학생: 95 },
],
},
semanticTypes: { 학년: 'Grade', 남학생: 'Count', 여학생: 'Count' },
encodings: { y: '학년', left: '남학생', right: '여학생' },
},
},
};
export const BulletChart: Story = {
args: {
spec: {
chartType: 'Bullet Chart',
data: {
values: [
{ 과목: '국어', 진도율: 72, 목표: 80 },
{ 과목: '영어', 진도율: 91, 목표: 85 },
{ 과목: '수학', 진도율: 58, 목표: 75 },
{ 과목: '과학', 진도율: 83, 목표: 80 },
],
},
semanticTypes: { 진도율: 'Percent', 목표: 'Percent' },
encodings: {
y: '과목',
value: { field: '진도율', label: '진도율' },
target: { field: '목표', label: '목표' },
},
chartProperties: { ranges: [50, 75, 100] },
},
},
};
/* ─── 시간·흐름 ─── */
export const GanttChart: Story = {
args: {
spec: {
chartType: 'Gantt Chart',
data: {
values: [
{ 작업: '교재 기획', 시작: '2026-03-02', 종료: '2026-03-13', 팀: '콘텐츠' },
{ 작업: '문항 집필', 시작: '2026-03-09', 종료: '2026-04-03', 팀: '콘텐츠' },
{ 작업: '검수', 시작: '2026-03-30', 종료: '2026-04-10', 팀: '편집' },
{ 작업: '조판·디자인', 시작: '2026-04-06', 종료: '2026-04-24', 팀: '편집' },
{ 작업: '베타 수업', 시작: '2026-04-20', 종료: '2026-05-08', 팀: '운영' },
],
},
semanticTypes: { 시작: 'Date', 종료: 'Date' },
encodings: { y: '작업', start: '시작', end: '종료', color: '팀' },
},
},
};
export const FunnelChart: Story = {
args: {
spec: {
chartType: 'Funnel Chart',
data: {
values: [
{ 단계: '상담 신청', 인원: 1200 },
{ 단계: '레벨 테스트', 인원: 860 },
{ 단계: '체험 수업', 인원: 540 },
{ 단계: '정규 등록', 인원: 310 },
],
},
semanticTypes: { 인원: 'Count' },
encodings: { x: '단계', y: { field: '인원', label: '인원' } },
},
},
};
export const ConnectedScatter: Story = {
args: {
spec: {
chartType: 'Connected Scatter',
data: {
values: [
{ 월: '1월', 학습시간: 480, 평균점수: 62 },
{ 월: '2월', 학습시간: 540, 평균점수: 66 },
{ 월: '3월', 학습시간: 620, 평균점수: 71 },
{ 월: '4월', 학습시간: 590, 평균점수: 74 },
{ 월: '5월', 학습시간: 720, 평균점수: 79 },
{ 월: '6월', 학습시간: 680, 평균점수: 83 },
],
},
semanticTypes: { 학습시간: 'DurationMinutes', 평균점수: 'Score' },
encodings: {
x: { field: '학습시간', label: '월 학습 시간' },
y: { field: '평균점수', label: '평균 점수' },
detail: '월',
},
},
},
};
export const RangedDotPlot: Story = {
args: {
spec: {
chartType: 'Ranged Dot Plot',
data: {
values: [
{ 과목: '국어', 중간: 74, 기말: 82 },
{ 과목: '영어', 중간: 68, 기말: 79 },
{ 과목: '수학', 중간: 61, 기말: 73 },
{ 과목: '과학', 중간: 85, 기말: 81 },
{ 과목: '사회', 중간: 70, 기말: 76 },
],
},
semanticTypes: { 중간: 'Score', 기말: 'Score' },
encodings: {
y: '과목',
start: { field: '중간', label: '중간고사' },
end: { field: '기말', label: '기말고사' },
},
},
},
};
/* ─── 계층·관계 ─── */
const studyTimeByUnit = [
{ 과목군: '언어', 과목: '국어', 학습시간: 320 },
{ 과목군: '언어', 과목: '영어', 학습시간: 410 },
{ 과목군: '수리', 과목: '수학', 학습시간: 520 },
{ 과목군: '수리', 과목: '과학', 학습시간: 180 },
{ 과목군: '탐구', 과목: '사회', 학습시간: 150 },
{ 과목군: '탐구', 과목: '한국사', 학습시간: 120 },
];
export const Treemap: Story = {
args: {
spec: {
chartType: 'Treemap',
data: { values: studyTimeByUnit },
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: {
category: '과목',
value: { field: '학습시간', label: '학습 시간' },
group: '과목군',
},
chartProperties: { showLegend: true },
},
},
};
export const Sunburst: Story = {
args: {
spec: {
chartType: 'Sunburst',
data: { values: studyTimeByUnit },
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: {
category: '과목',
value: { field: '학습시간', label: '학습 시간' },
group: '과목군',
},
chartProperties: { innerRadius: 40 },
},
},
};
export const Tree: Story = {
args: {
spec: {
chartType: 'Tree',
data: {
values: [
{ 이름: '중등 수학', 상위: null },
{ 이름: '수와 연산', 상위: '중등 수학' },
{ 이름: '문자와 식', 상위: '중등 수학' },
{ 이름: '함수', 상위: '중등 수학' },
{ 이름: '소인수분해', 상위: '수와 연산' },
{ 이름: '정수와 유리수', 상위: '수와 연산' },
{ 이름: '일차방정식', 상위: '문자와 식' },
{ 이름: '좌표평면과 그래프', 상위: '함수' },
],
},
encodings: { category: '이름', parent: '상위' },
},
},
};
export const Sankey: Story = {
args: {
spec: {
chartType: 'Sankey',
data: {
values: [
{ 출발: '중3', 도착: '고등 내신반', 인원: 120 },
{ 출발: '중3', 도착: '고등 수능반', 인원: 80 },
{ 출발: '고등 내신반', 도착: '이과', 인원: 70 },
{ 출발: '고등 내신반', 도착: '문과', 인원: 50 },
{ 출발: '고등 수능반', 도착: '이과', 인원: 45 },
{ 출발: '고등 수능반', 도착: '문과', 인원: 35 },
],
},
semanticTypes: { 인원: 'Count' },
encodings: { source: '출발', target: '도착', value: { field: '인원', label: '인원' } },
},
},
};
export const Graph: Story = {
args: {
spec: {
chartType: 'Graph',
data: {
values: [
{ 개념A: '일차방정식', 개념B: '연립방정식', 연관도: 5 },
{ 개념A: '일차방정식', 개념B: '일차함수', 연관도: 4 },
{ 개념A: '일차함수', 개념B: '이차함수', 연관도: 3 },
{ 개념A: '연립방정식', 개념B: '일차함수', 연관도: 2 },
{ 개념A: '이차함수', 개념B: '이차방정식', 연관도: 4 },
{ 개념A: '이차방정식', 개념B: '인수분해', 연관도: 5 },
],
},
encodings: { source: '개념A', target: '개념B', value: '연관도' },
chartProperties: { sizeByDegree: true },
},
},
};
/* ─── 극좌표·게이지 ─── */
export const RoseChart: Story = {
args: {
spec: {
chartType: 'Rose Chart',
data: {
values: [
{ 요일: '월', 학습시간: 95 },
{ 요일: '화', 학습시간: 110 },
{ 요일: '수', 학습시간: 85 },
{ 요일: '목', 학습시간: 120 },
{ 요일: '금', 학습시간: 70 },
{ 요일: '토', 학습시간: 140 },
{ 요일: '일', 학습시간: 60 },
],
},
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: {
category: { field: '요일', type: 'ordinal' },
value: { field: '학습시간', label: '학습 시간' },
},
},
},
};
export const Gauge: Story = {
args: {
spec: {
chartType: 'Gauge',
data: { values: [{ 진도율: 72 }] },
semanticTypes: { 진도율: 'Percent' },
encodings: { value: { field: '진도율', label: '전체 진도율' } },
},
},
};
/* ─── 변동·순위 ─── */
export const CandlestickChart: Story = {
args: {
spec: {
chartType: 'Candlestick Chart',
data: {
values: [
{ 주차: '1주', 시가: 68, 고가: 75, 저가: 64, 종가: 72 },
{ 주차: '2주', 시가: 72, 고가: 78, 저가: 69, 종가: 70 },
{ 주차: '3주', 시가: 70, 고가: 82, 저가: 70, 종가: 80 },
{ 주차: '4주', 시가: 80, 고가: 85, 저가: 74, 종가: 76 },
{ 주차: '5주', 시가: 76, 고가: 88, 저가: 75, 종가: 86 },
],
},
semanticTypes: { 시가: 'Score', 고가: 'Score', 저가: 'Score', 종가: 'Score' },
encodings: {
x: { field: '주차', type: 'ordinal' },
open: '시가',
high: '고가',
low: '저가',
close: '종가',
},
},
},
};
export const BumpChart: Story = {
args: {
spec: {
chartType: 'Bump Chart',
data: {
values: [
{ 월: '3월', 학생: '김민준', 순위: 1 },
{ 월: '3월', 학생: '이서연', 순위: 2 },
{ 월: '3월', 학생: '박지호', 순위: 3 },
{ 월: '4월', 학생: '김민준', 순위: 2 },
{ 월: '4월', 학생: '이서연', 순위: 1 },
{ 월: '4월', 학생: '박지호', 순위: 3 },
{ 월: '5월', 학생: '김민준', 순위: 3 },
{ 월: '5월', 학생: '이서연', 순위: 1 },
{ 월: '5월', 학생: '박지호', 순위: 2 },
{ 월: '6월', 학생: '김민준', 순위: 2 },
{ 월: '6월', 학생: '이서연', 순위: 3 },
{ 월: '6월', 학생: '박지호', 순위: 1 },
],
},
semanticTypes: { 월: 'Month', 순위: 'Rank' },
encodings: { x: '월', y: '순위', color: '학생' },
},
},
};
export const SlopeChart: Story = {
args: {
spec: {
chartType: 'Slope Chart',
data: {
values: [
{ 시험: '중간고사', 학생: '김민준', 점수: 72 },
{ 시험: '기말고사', 학생: '김민준', 점수: 84 },
{ 시험: '중간고사', 학생: '이서연', 점수: 88 },
{ 시험: '기말고사', 학생: '이서연', 점수: 82 },
{ 시험: '중간고사', 학생: '박지호', 점수: 65 },
{ 시험: '기말고사', 학생: '박지호', 점수: 78 },
],
},
semanticTypes: { 점수: 'Score' },
encodings: { x: '시험', y: '점수', color: '학생' },
},
},
};
/* ─── KPI·미니 차트 ─── */
export const KpiCard: Story = {
args: {
spec: {
chartType: 'KPI Card',
data: {
values: [
{ 월: '2026-01-01', 활성학습자: 1180 },
{ 월: '2026-02-01', 활성학습자: 1240 },
{ 월: '2026-03-01', 활성학습자: 1315 },
{ 월: '2026-04-01', 활성학습자: 1290 },
{ 월: '2026-05-01', 활성학습자: 1402 },
{ 월: '2026-06-01', 활성학습자: 1476 },
],
},
semanticTypes: { 월: 'Date', 활성학습자: 'Count' },
encodings: { value: { field: '활성학습자', label: '월간 활성 학습자' }, x: '월' },
chartProperties: { deltaLabel: '전월 대비', unit: '명' },
},
},
};
export const Sparkline: Story = {
args: {
spec: {
chartType: 'Sparkline',
data: { values: monthlyMinutes },
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: { y: { field: '학습시간', label: '학습 시간' } },
chartProperties: { variant: 'area' },
},
height: 60,
},
};
/* ─── 다차원 ─── */
export const ParallelCoordinates: Story = {
args: {
spec: {
chartType: 'Parallel Coordinates',
data: {
values: [
{ 학생: '김민준', 반: '1반', 국어: 82, 영어: 74, 수학: 91, 과학: 68 },
{ 학생: '이서연', 반: '1반', 국어: 76, 영어: 88, 수학: 72, 과학: 80 },
{ 학생: '박지호', 반: '2반', 국어: 64, 영어: 70, 수학: 85, 과학: 77 },
{ 학생: '최수아', 반: '2반', 국어: 90, 영어: 82, 수학: 66, 과학: 85 },
{ 학생: '정도윤', 반: '3반', 국어: 71, 영어: 65, 수학: 78, 과학: 72 },
{ 학생: '한지우', 반: '3반', 국어: 85, 영어: 79, 수학: 88, 과학: 90 },
],
},
encodings: { color: '반' },
chartProperties: { dimensions: ['국어', '영어', '수학', '과학'] },
},
},
};
/* ─── 기본 차트 ─── */
export const LineChart: Story = {
args: {
spec: {
chartType: 'Line Chart',
data: { values: monthlyClassScores },
semanticTypes: { 월: 'Month', 평균점수: 'Score' },
encodings: { x: '월', y: '평균점수', color: '반' },
},
},
};
const questionTypes = [
{ 유형: '객관식', 문항수: 420 },
{ 유형: '주관식', 문항수: 260 },
{ 유형: '서술형', 문항수: 120 },
{ 유형: 'OX', 문항수: 90 },
];
export const PieChart: Story = {
args: {
spec: {
chartType: 'Pie Chart',
data: { values: questionTypes },
semanticTypes: { 문항수: 'Count' },
encodings: { category: '유형', value: { field: '문항수', label: '문항 수' } },
},
},
};
export const DonutChart: Story = {
args: {
spec: {
chartType: 'Donut Chart',
data: { values: questionTypes },
semanticTypes: { 문항수: 'Count' },
encodings: { category: '유형', value: { field: '문항수', label: '문항 수' } },
},
},
};
export const ScatterPlot: Story = {
args: {
spec: {
chartType: 'Scatter Plot',
data: {
values: Array.from({ length: 30 }, (_, i) => ({
반: ['1반', '2반', '3반'][i % 3],
학습시간: 120 + ((i * 37) % 480),
점수: 40 + ((i * 23) % 55),
풀이수: 20 + ((i * 11) % 80),
})),
},
semanticTypes: { 학습시간: 'DurationMinutes', 점수: 'Score', 풀이수: 'Count' },
encodings: {
x: { field: '학습시간', label: '주간 학습 시간' },
y: { field: '점수', label: '점수' },
color: '반',
size: '풀이수',
},
},
},
};
export const RadarChart: Story = {
args: {
spec: {
chartType: 'Radar Chart',
data: {
values: [
{ 영역: '어휘', 학생: '김민준', 점수: 78 },
{ 영역: '문법', 학생: '김민준', 점수: 65 },
{ 영역: '독해', 학생: '김민준', 점수: 88 },
{ 영역: '듣기', 학생: '김민준', 점수: 72 },
{ 영역: '쓰기', 학생: '김민준', 점수: 60 },
{ 영역: '어휘', 학생: '이서연', 점수: 85 },
{ 영역: '문법', 학생: '이서연', 점수: 80 },
{ 영역: '독해', 학생: '이서연', 점수: 70 },
{ 영역: '듣기', 학생: '이서연', 점수: 90 },
{ 영역: '쓰기', 학생: '이서연', 점수: 76 },
],
},
semanticTypes: { 점수: 'Score' },
encodings: { x: '영역', y: '점수', color: '학생' },
},
},
};
const chartCatalog = [
{ title: 'Bar Chart', story: BarChart },
{ title: 'Grouped Bar Chart', story: GroupedBarChart },
{ title: 'Stacked Bar Chart', story: StackedBarChart },
{ title: 'Area Chart', story: AreaChart },
{ title: 'Stacked Area Chart', story: StackedAreaChart },
{ title: 'Streamgraph', story: Streamgraph },
{ title: 'Range Area Chart', story: RangeAreaChart },
{ title: 'Heatmap', story: Heatmap },
{ title: 'Calendar Heatmap', story: CalendarHeatmap },
{ title: 'Histogram', story: Histogram },
{ title: 'Box Plot', story: BoxPlot },
{ title: 'Violin Plot', story: ViolinPlot },
{ title: 'Density Plot', story: DensityPlot },
{ title: 'ECDF Plot', story: EcdfPlot },
{ title: 'Strip Plot', story: StripPlot },
{ title: 'Lollipop Chart', story: LollipopChart },
{ title: 'Waterfall Chart', story: WaterfallChart },
{ title: 'Pyramid Chart', story: PyramidChart },
{ title: 'Bullet Chart', story: BulletChart },
{ title: 'Gantt Chart', story: GanttChart },
{ title: 'Funnel Chart', story: FunnelChart },
{ title: 'Connected Scatter', story: ConnectedScatter },
{ title: 'Ranged Dot Plot', story: RangedDotPlot },
{ title: 'Treemap', story: Treemap },
{ title: 'Sunburst', story: Sunburst },
{ title: 'Tree', story: Tree },
{ title: 'Sankey', story: Sankey },
{ title: 'Graph', story: Graph },
{ title: 'Rose Chart', story: RoseChart },
{ title: 'Gauge', story: Gauge, height: 220 },
{ title: 'Candlestick Chart', story: CandlestickChart },
{ title: 'Bump Chart', story: BumpChart },
{ title: 'Slope Chart', story: SlopeChart },
{ title: 'KPI Card', story: KpiCard, height: 180 },
{ title: 'Sparkline', story: Sparkline, height: 96 },
{ title: 'Parallel Coordinates', story: ParallelCoordinates },
{ title: 'Line Chart', story: LineChart },
{ title: 'Pie Chart', story: PieChart },
{ title: 'Donut Chart', story: DonutChart },
{ title: 'Scatter Plot', story: ScatterPlot },
{ title: 'Radar Chart', story: RadarChart },
] satisfies Array<{ title: string; story: Story; height?: number }>;
function CatalogGrid({
heading,
description,
mapSpec = (spec) => spec,
}: {
heading: string;
description: string;
mapSpec?: (spec: ChartSpec) => ChartSpec;
}) {
return (
<Stack gap="24" padding="24" width="100%">
<Box>
<Box as="h2" textStyle="title-2xl" color="neutral.text.base">
{heading}
</Box>
<Box mt="4" textStyle="body-sm" color="neutral.text.low">
{description}
</Box>
</Box>
<Grid gridTemplateColumns={{ base: '1fr', xl: 'repeat(2, minmax(0, 1fr))' }} gap="24">
{chartCatalog.map(({ title, story, height }) => {
const spec = story.args?.spec as ChartSpec | undefined;
if (!spec) {
return null;
}
return (
<Stack
key={title}
as="article"
gap="12"
padding="16"
borderWidth="1px"
borderColor="neutral.border.low"
borderRadius="12"
bg="neutral.surface.low"
>
<Box>
<Box as="h3" textStyle="title-md" color="neutral.text.base">
{title}
</Box>
<Box mt="2" textStyle="caption-lg" color="neutral.text.low">
chartType: {spec.chartType}
</Box>
</Box>
<Chart spec={mapSpec(spec)} height={height ?? 280} />
</Stack>
);
})}
</Grid>
</Stack>
);
}
const ChartAllChartsExample = () => (
<CatalogGrid
heading="Chart Catalog"
description={`ChartSpec 샘플 ${chartCatalog.length}개를 한 번에 렌더링합니다.`}
/>
);
export const ChartAllChartsTremorExample = () => (
<CatalogGrid
heading="Chart Catalog — Tremor"
description={`chartProperties.colors에 TREMOR_9를 주입해 ${chartCatalog.length}개 차트를 렌더링합니다. colors prop이 없는 차트는 기본 색을 유지합니다.`}
mapSpec={(spec) => ({
...spec,
chartProperties: { ...spec.chartProperties, colors: CHART_COLOR_PRESET },
})}
/>
);
export default ChartAllChartsExample;
BarChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/* ─── 공용 샘플 데이터 (밀당 도메인, 전부 결정적) ─── */
const subjectScores = [
{ 과목: '국어', 평균점수: 82 },
{ 과목: '영어', 평균점수: 76 },
{ 과목: '수학', 평균점수: 68 },
{ 과목: '과학', 평균점수: 88 },
{ 과목: '사회', 평균점수: 74 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Bar Chart',
data: { values: subjectScores },
semanticTypes: { 평균점수: 'Score' },
encodings: { x: '과목', y: { field: '평균점수', label: '평균 점수' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartBarChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
GroupedBarChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const subjectClassScores = [
{ 과목: '국어', 반: '1반', 점수: 84 },
{ 과목: '국어', 반: '2반', 점수: 78 },
{ 과목: '영어', 반: '1반', 점수: 72 },
{ 과목: '영어', 반: '2반', 점수: 81 },
{ 과목: '수학', 반: '1반', 점수: 66 },
{ 과목: '수학', 반: '2반', 점수: 74 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Grouped Bar Chart',
data: { values: subjectClassScores },
semanticTypes: { 점수: 'Score' },
encodings: { x: '과목', y: '점수', color: '반' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartGroupedBarChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
StackedBarChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Stacked Bar Chart',
data: {
values: [
{ 월: '3월', 유형: '객관식', 풀이수: 320 },
{ 월: '3월', 유형: '주관식', 풀이수: 180 },
{ 월: '3월', 유형: '서술형', 풀이수: 60 },
{ 월: '4월', 유형: '객관식', 풀이수: 410 },
{ 월: '4월', 유형: '주관식', 풀이수: 210 },
{ 월: '4월', 유형: '서술형', 풀이수: 90 },
{ 월: '5월', 유형: '객관식', 풀이수: 380 },
{ 월: '5월', 유형: '주관식', 풀이수: 240 },
{ 월: '5월', 유형: '서술형', 풀이수: 110 },
],
},
semanticTypes: { 월: 'Month', 풀이수: 'Count' },
encodings: { x: '월', y: '풀이수', color: '유형' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartStackedBarChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
AreaChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const monthlyMinutes = [
{ 월: '1월', 학습시간: 620 },
{ 월: '2월', 학습시간: 540 },
{ 월: '3월', 학습시간: 710 },
{ 월: '4월', 학습시간: 680 },
{ 월: '5월', 학습시간: 820 },
{ 월: '6월', 학습시간: 760 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Area Chart',
data: { values: monthlyMinutes },
semanticTypes: { 월: 'Month', 학습시간: 'DurationMinutes' },
encodings: { x: '월', y: { field: '학습시간', label: '학습 시간' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartAreaChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
StackedAreaChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 월×과목 학습 시간 long 데이터 — 결정적 공식으로 생성 */
const monthlySubjectMinutes = ['1월', '2월', '3월', '4월', '5월', '6월'].flatMap((월, mi) =>
['국어', '영어', '수학'].map((과목, si) => ({
월,
과목,
학습시간: 120 + ((mi * 53 + si * 91) % 180),
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Stacked Area Chart',
data: { values: monthlySubjectMinutes },
semanticTypes: { 월: 'Month', 학습시간: 'DurationMinutes' },
encodings: { x: '월', y: '학습시간', color: '과목' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartStackedAreaChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Streamgraph
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 월×과목 학습 시간 long 데이터 — 결정적 공식으로 생성 */
const monthlySubjectMinutes = ['1월', '2월', '3월', '4월', '5월', '6월'].flatMap((월, mi) =>
['국어', '영어', '수학'].map((과목, si) => ({
월,
과목,
학습시간: 120 + ((mi * 53 + si * 91) % 180),
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Streamgraph',
data: { values: monthlySubjectMinutes },
semanticTypes: { 월: 'Month', 학습시간: 'DurationMinutes' },
encodings: { x: '월', y: '학습시간', color: '과목' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartStreamgraphExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
RangeAreaChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Range Area Chart',
data: {
values: [
{ 월: '1월', 최저: 52, 최고: 94, 평균: 71 },
{ 월: '2월', 최저: 48, 최고: 90, 평균: 68 },
{ 월: '3월', 최저: 55, 최고: 96, 평균: 74 },
{ 월: '4월', 최저: 58, 최고: 93, 평균: 76 },
{ 월: '5월', 최저: 61, 최고: 98, 평균: 79 },
{ 월: '6월', 최저: 57, 최고: 95, 평균: 77 },
],
},
semanticTypes: { 월: 'Month', 최저: 'Score', 최고: 'Score', 평균: 'Score' },
encodings: {
x: '월',
y0: { field: '최저', label: '최저 점수' },
y1: { field: '최고', label: '최고 점수' },
mid: { field: '평균', label: '평균 점수' },
},
},
}) } as ComponentProps<typeof Chart>;
export default function ChartRangeAreaChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Heatmap
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Heatmap',
data: {
values: ['1반', '2반', '3반'].flatMap((반, ci) =>
['1단원', '2단원', '3단원', '4단원', '5단원'].map((단원, ui) => ({
반,
단원,
정답률: 55 + ((ci * 17 + ui * 13) % 40),
})),
),
},
semanticTypes: { 정답률: 'Percent' },
encodings: { x: '단원', y: '반', value: { field: '정답률', label: '정답률' } },
chartProperties: { showValues: true, showLegend: true },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartHeatmapExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
CalendarHeatmap
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Calendar Heatmap',
data: {
values: Array.from({ length: 112 }, (_, i) => ({
날짜: new Date(Date.UTC(2026, 0, 5 + i)).toISOString().slice(0, 10),
풀이수: i % 6 === 5 ? 0 : ((i * 7) % 19) + (i % 4),
})),
},
semanticTypes: { 날짜: 'Date', 풀이수: 'Count' },
encodings: { date: '날짜', value: { field: '풀이수', label: '풀이 수' } },
chartProperties: { showLegend: true },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartCalendarHeatmapExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Histogram
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Histogram',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: { field: '점수', label: '점수' } },
chartProperties: { binCount: 8 },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartHistogramExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
BoxPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Box Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { y: { field: '점수', label: '점수' }, x: '반' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartBoxPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
ViolinPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Violin Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { y: { field: '점수', label: '점수' }, x: '반' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartViolinPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
DensityPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Density Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: { field: '점수', label: '점수' }, color: '반' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartDensityPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
EcdfPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'ECDF Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: { field: '점수', label: '점수' }, color: '반' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartEcdfPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
StripPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/** 반별 점수 분포 long 데이터 — 분포 계열(Box/Violin/Density/ECDF/Strip/Histogram) 공용 */
const scoreDistRows = ['1반', '2반', '3반'].flatMap((반, ci) =>
Array.from({ length: 24 }, (_, i) => ({
반,
점수: 45 + ci * 5 + ((i * 7) % 29) + (i % 4) * 2,
})),
);
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Strip Plot',
data: { values: scoreDistRows },
semanticTypes: { 점수: 'Score' },
encodings: { x: '반', y: { field: '점수', label: '점수' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartStripPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
LollipopChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/* ─── 공용 샘플 데이터 (밀당 도메인, 전부 결정적) ─── */
const subjectScores = [
{ 과목: '국어', 평균점수: 82 },
{ 과목: '영어', 평균점수: 76 },
{ 과목: '수학', 평균점수: 68 },
{ 과목: '과학', 평균점수: 88 },
{ 과목: '사회', 평균점수: 74 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Lollipop Chart',
data: { values: subjectScores },
semanticTypes: { 평균점수: 'Score' },
encodings: { x: '과목', y: { field: '평균점수', label: '평균 점수' } },
chartProperties: { showValueText: true },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartLollipopChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
WaterfallChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Waterfall Chart',
data: {
values: [
{ 월: '3월', 순증감: 42 },
{ 월: '4월', 순증감: 18 },
{ 월: '5월', 순증감: -12 },
{ 월: '6월', 순증감: 25 },
{ 월: '7월', 순증감: -8 },
],
},
semanticTypes: { 월: 'Month', 순증감: 'Count' },
encodings: { x: '월', y: { field: '순증감', label: '수강생 순증감' } },
chartProperties: { totalLabel: '순증 합계' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartWaterfallChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
PyramidChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Pyramid Chart',
data: {
values: [
{ 학년: '중1', 남학생: 120, 여학생: 132 },
{ 학년: '중2', 남학생: 145, 여학생: 138 },
{ 학년: '중3', 남학생: 160, 여학생: 155 },
{ 학년: '고1', 남학생: 130, 여학생: 148 },
{ 학년: '고2', 남학생: 110, 여학생: 118 },
{ 학년: '고3', 남학생: 88, 여학생: 95 },
],
},
semanticTypes: { 학년: 'Grade', 남학생: 'Count', 여학생: 'Count' },
encodings: { y: '학년', left: '남학생', right: '여학생' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartPyramidChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
BulletChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Bullet Chart',
data: {
values: [
{ 과목: '국어', 진도율: 72, 목표: 80 },
{ 과목: '영어', 진도율: 91, 목표: 85 },
{ 과목: '수학', 진도율: 58, 목표: 75 },
{ 과목: '과학', 진도율: 83, 목표: 80 },
],
},
semanticTypes: { 진도율: 'Percent', 목표: 'Percent' },
encodings: {
y: '과목',
value: { field: '진도율', label: '진도율' },
target: { field: '목표', label: '목표' },
},
chartProperties: { ranges: [50, 75, 100] },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartBulletChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
GanttChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Gantt Chart',
data: {
values: [
{ 작업: '교재 기획', 시작: '2026-03-02', 종료: '2026-03-13', 팀: '콘텐츠' },
{ 작업: '문항 집필', 시작: '2026-03-09', 종료: '2026-04-03', 팀: '콘텐츠' },
{ 작업: '검수', 시작: '2026-03-30', 종료: '2026-04-10', 팀: '편집' },
{ 작업: '조판·디자인', 시작: '2026-04-06', 종료: '2026-04-24', 팀: '편집' },
{ 작업: '베타 수업', 시작: '2026-04-20', 종료: '2026-05-08', 팀: '운영' },
],
},
semanticTypes: { 시작: 'Date', 종료: 'Date' },
encodings: { y: '작업', start: '시작', end: '종료', color: '팀' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartGanttChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
FunnelChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Funnel Chart',
data: {
values: [
{ 단계: '상담 신청', 인원: 1200 },
{ 단계: '레벨 테스트', 인원: 860 },
{ 단계: '체험 수업', 인원: 540 },
{ 단계: '정규 등록', 인원: 310 },
],
},
semanticTypes: { 인원: 'Count' },
encodings: { x: '단계', y: { field: '인원', label: '인원' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartFunnelChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
ConnectedScatter
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Connected Scatter',
data: {
values: [
{ 월: '1월', 학습시간: 480, 평균점수: 62 },
{ 월: '2월', 학습시간: 540, 평균점수: 66 },
{ 월: '3월', 학습시간: 620, 평균점수: 71 },
{ 월: '4월', 학습시간: 590, 평균점수: 74 },
{ 월: '5월', 학습시간: 720, 평균점수: 79 },
{ 월: '6월', 학습시간: 680, 평균점수: 83 },
],
},
semanticTypes: { 학습시간: 'DurationMinutes', 평균점수: 'Score' },
encodings: {
x: { field: '학습시간', label: '월 학습 시간' },
y: { field: '평균점수', label: '평균 점수' },
detail: '월',
},
},
}) } as ComponentProps<typeof Chart>;
export default function ChartConnectedScatterExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
RangedDotPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Ranged Dot Plot',
data: {
values: [
{ 과목: '국어', 중간: 74, 기말: 82 },
{ 과목: '영어', 중간: 68, 기말: 79 },
{ 과목: '수학', 중간: 61, 기말: 73 },
{ 과목: '과학', 중간: 85, 기말: 81 },
{ 과목: '사회', 중간: 70, 기말: 76 },
],
},
semanticTypes: { 중간: 'Score', 기말: 'Score' },
encodings: {
y: '과목',
start: { field: '중간', label: '중간고사' },
end: { field: '기말', label: '기말고사' },
},
},
}) } as ComponentProps<typeof Chart>;
export default function ChartRangedDotPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Treemap
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/* ─── 계층·관계 ─── */
const studyTimeByUnit = [
{ 과목군: '언어', 과목: '국어', 학습시간: 320 },
{ 과목군: '언어', 과목: '영어', 학습시간: 410 },
{ 과목군: '수리', 과목: '수학', 학습시간: 520 },
{ 과목군: '수리', 과목: '과학', 학습시간: 180 },
{ 과목군: '탐구', 과목: '사회', 학습시간: 150 },
{ 과목군: '탐구', 과목: '한국사', 학습시간: 120 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Treemap',
data: { values: studyTimeByUnit },
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: {
category: '과목',
value: { field: '학습시간', label: '학습 시간' },
group: '과목군',
},
chartProperties: { showLegend: true },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartTreemapExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Sunburst
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
/* ─── 계층·관계 ─── */
const studyTimeByUnit = [
{ 과목군: '언어', 과목: '국어', 학습시간: 320 },
{ 과목군: '언어', 과목: '영어', 학습시간: 410 },
{ 과목군: '수리', 과목: '수학', 학습시간: 520 },
{ 과목군: '수리', 과목: '과학', 학습시간: 180 },
{ 과목군: '탐구', 과목: '사회', 학습시간: 150 },
{ 과목군: '탐구', 과목: '한국사', 학습시간: 120 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Sunburst',
data: { values: studyTimeByUnit },
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: {
category: '과목',
value: { field: '학습시간', label: '학습 시간' },
group: '과목군',
},
chartProperties: { innerRadius: 40 },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartSunburstExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Tree
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Tree',
data: {
values: [
{ 이름: '중등 수학', 상위: null },
{ 이름: '수와 연산', 상위: '중등 수학' },
{ 이름: '문자와 식', 상위: '중등 수학' },
{ 이름: '함수', 상위: '중등 수학' },
{ 이름: '소인수분해', 상위: '수와 연산' },
{ 이름: '정수와 유리수', 상위: '수와 연산' },
{ 이름: '일차방정식', 상위: '문자와 식' },
{ 이름: '좌표평면과 그래프', 상위: '함수' },
],
},
encodings: { category: '이름', parent: '상위' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartTreeExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Sankey
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Sankey',
data: {
values: [
{ 출발: '중3', 도착: '고등 내신반', 인원: 120 },
{ 출발: '중3', 도착: '고등 수능반', 인원: 80 },
{ 출발: '고등 내신반', 도착: '이과', 인원: 70 },
{ 출발: '고등 내신반', 도착: '문과', 인원: 50 },
{ 출발: '고등 수능반', 도착: '이과', 인원: 45 },
{ 출발: '고등 수능반', 도착: '문과', 인원: 35 },
],
},
semanticTypes: { 인원: 'Count' },
encodings: { source: '출발', target: '도착', value: { field: '인원', label: '인원' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartSankeyExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Graph
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Graph',
data: {
values: [
{ 개념A: '일차방정식', 개념B: '연립방정식', 연관도: 5 },
{ 개념A: '일차방정식', 개념B: '일차함수', 연관도: 4 },
{ 개념A: '일차함수', 개념B: '이차함수', 연관도: 3 },
{ 개념A: '연립방정식', 개념B: '일차함수', 연관도: 2 },
{ 개념A: '이차함수', 개념B: '이차방정식', 연관도: 4 },
{ 개념A: '이차방정식', 개념B: '인수분해', 연관도: 5 },
],
},
encodings: { source: '개념A', target: '개념B', value: '연관도' },
chartProperties: { sizeByDegree: true },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartGraphExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
RoseChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Rose Chart',
data: {
values: [
{ 요일: '월', 학습시간: 95 },
{ 요일: '화', 학습시간: 110 },
{ 요일: '수', 학습시간: 85 },
{ 요일: '목', 학습시간: 120 },
{ 요일: '금', 학습시간: 70 },
{ 요일: '토', 학습시간: 140 },
{ 요일: '일', 학습시간: 60 },
],
},
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: {
category: { field: '요일', type: 'ordinal' },
value: { field: '학습시간', label: '학습 시간' },
},
},
}) } as ComponentProps<typeof Chart>;
export default function ChartRoseChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Gauge
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Gauge',
data: { values: [{ 진도율: 72 }] },
semanticTypes: { 진도율: 'Percent' },
encodings: { value: { field: '진도율', label: '전체 진도율' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartGaugeExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
CandlestickChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Candlestick Chart',
data: {
values: [
{ 주차: '1주', 시가: 68, 고가: 75, 저가: 64, 종가: 72 },
{ 주차: '2주', 시가: 72, 고가: 78, 저가: 69, 종가: 70 },
{ 주차: '3주', 시가: 70, 고가: 82, 저가: 70, 종가: 80 },
{ 주차: '4주', 시가: 80, 고가: 85, 저가: 74, 종가: 76 },
{ 주차: '5주', 시가: 76, 고가: 88, 저가: 75, 종가: 86 },
],
},
semanticTypes: { 시가: 'Score', 고가: 'Score', 저가: 'Score', 종가: 'Score' },
encodings: {
x: { field: '주차', type: 'ordinal' },
open: '시가',
high: '고가',
low: '저가',
close: '종가',
},
},
}) } as ComponentProps<typeof Chart>;
export default function ChartCandlestickChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
BumpChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Bump Chart',
data: {
values: [
{ 월: '3월', 학생: '김민준', 순위: 1 },
{ 월: '3월', 학생: '이서연', 순위: 2 },
{ 월: '3월', 학생: '박지호', 순위: 3 },
{ 월: '4월', 학생: '김민준', 순위: 2 },
{ 월: '4월', 학생: '이서연', 순위: 1 },
{ 월: '4월', 학생: '박지호', 순위: 3 },
{ 월: '5월', 학생: '김민준', 순위: 3 },
{ 월: '5월', 학생: '이서연', 순위: 1 },
{ 월: '5월', 학생: '박지호', 순위: 2 },
{ 월: '6월', 학생: '김민준', 순위: 2 },
{ 월: '6월', 학생: '이서연', 순위: 3 },
{ 월: '6월', 학생: '박지호', 순위: 1 },
],
},
semanticTypes: { 월: 'Month', 순위: 'Rank' },
encodings: { x: '월', y: '순위', color: '학생' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartBumpChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
SlopeChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Slope Chart',
data: {
values: [
{ 시험: '중간고사', 학생: '김민준', 점수: 72 },
{ 시험: '기말고사', 학생: '김민준', 점수: 84 },
{ 시험: '중간고사', 학생: '이서연', 점수: 88 },
{ 시험: '기말고사', 학생: '이서연', 점수: 82 },
{ 시험: '중간고사', 학생: '박지호', 점수: 65 },
{ 시험: '기말고사', 학생: '박지호', 점수: 78 },
],
},
semanticTypes: { 점수: 'Score' },
encodings: { x: '시험', y: '점수', color: '학생' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartSlopeChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
KpiCard
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'KPI Card',
data: {
values: [
{ 월: '2026-01-01', 활성학습자: 1180 },
{ 월: '2026-02-01', 활성학습자: 1240 },
{ 월: '2026-03-01', 활성학습자: 1315 },
{ 월: '2026-04-01', 활성학습자: 1290 },
{ 월: '2026-05-01', 활성학습자: 1402 },
{ 월: '2026-06-01', 활성학습자: 1476 },
],
},
semanticTypes: { 월: 'Date', 활성학습자: 'Count' },
encodings: { value: { field: '활성학습자', label: '월간 활성 학습자' }, x: '월' },
chartProperties: { deltaLabel: '전월 대비', unit: '명' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartKpiCardExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Sparkline
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const monthlyMinutes = [
{ 월: '1월', 학습시간: 620 },
{ 월: '2월', 학습시간: 540 },
{ 월: '3월', 학습시간: 710 },
{ 월: '4월', 학습시간: 680 },
{ 월: '5월', 학습시간: 820 },
{ 월: '6월', 학습시간: 760 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Sparkline',
data: { values: monthlyMinutes },
semanticTypes: { 학습시간: 'DurationMinutes' },
encodings: { y: { field: '학습시간', label: '학습 시간' } },
chartProperties: { variant: 'area' },
},
height: 60,
}) } as ComponentProps<typeof Chart>;
export default function ChartSparklineExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
ParallelCoordinates
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Parallel Coordinates',
data: {
values: [
{ 학생: '김민준', 반: '1반', 국어: 82, 영어: 74, 수학: 91, 과학: 68 },
{ 학생: '이서연', 반: '1반', 국어: 76, 영어: 88, 수학: 72, 과학: 80 },
{ 학생: '박지호', 반: '2반', 국어: 64, 영어: 70, 수학: 85, 과학: 77 },
{ 학생: '최수아', 반: '2반', 국어: 90, 영어: 82, 수학: 66, 과학: 85 },
{ 학생: '정도윤', 반: '3반', 국어: 71, 영어: 65, 수학: 78, 과학: 72 },
{ 학생: '한지우', 반: '3반', 국어: 85, 영어: 79, 수학: 88, 과학: 90 },
],
},
encodings: { color: '반' },
chartProperties: { dimensions: ['국어', '영어', '수학', '과학'] },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartParallelCoordinatesExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
LineChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const monthlyClassScores = [
{ 월: '3월', 반: '1반', 평균점수: 72 },
{ 월: '3월', 반: '2반', 평균점수: 68 },
{ 월: '4월', 반: '1반', 평균점수: 75 },
{ 월: '4월', 반: '2반', 평균점수: 71 },
{ 월: '5월', 반: '1반', 평균점수: 78 },
{ 월: '5월', 반: '2반', 평균점수: 76 },
{ 월: '6월', 반: '1반', 평균점수: 81 },
{ 월: '6월', 반: '2반', 평균점수: 74 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Line Chart',
data: { values: monthlyClassScores },
semanticTypes: { 월: 'Month', 평균점수: 'Score' },
encodings: { x: '월', y: '평균점수', color: '반' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartLineChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
PieChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const questionTypes = [
{ 유형: '객관식', 문항수: 420 },
{ 유형: '주관식', 문항수: 260 },
{ 유형: '서술형', 문항수: 120 },
{ 유형: 'OX', 문항수: 90 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Pie Chart',
data: { values: questionTypes },
semanticTypes: { 문항수: 'Count' },
encodings: { category: '유형', value: { field: '문항수', label: '문항 수' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartPieChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
DonutChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const questionTypes = [
{ 유형: '객관식', 문항수: 420 },
{ 유형: '주관식', 문항수: 260 },
{ 유형: '서술형', 문항수: 120 },
{ 유형: 'OX', 문항수: 90 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Donut Chart',
data: { values: questionTypes },
semanticTypes: { 문항수: 'Count' },
encodings: { category: '유형', value: { field: '문항수', label: '문항 수' } },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartDonutChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
ScatterPlot
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Scatter Plot',
data: {
values: Array.from({ length: 30 }, (_, i) => ({
반: ['1반', '2반', '3반'][i % 3],
학습시간: 120 + ((i * 37) % 480),
점수: 40 + ((i * 23) % 55),
풀이수: 20 + ((i * 11) % 80),
})),
},
semanticTypes: { 학습시간: 'DurationMinutes', 점수: 'Score', 풀이수: 'Count' },
encodings: {
x: { field: '학습시간', label: '주간 학습 시간' },
y: { field: '점수', label: '점수' },
color: '반',
size: '풀이수',
},
},
}) } as ComponentProps<typeof Chart>;
export default function ChartScatterPlotExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
RadarChart
import type { ComponentProps } from 'react';
import { Chart } from '@mildang/design-system/Chart';
const STORY_DEFAULT_ARGS = { ...({}), ...({
spec: {
chartType: 'Radar Chart',
data: {
values: [
{ 영역: '어휘', 학생: '김민준', 점수: 78 },
{ 영역: '문법', 학생: '김민준', 점수: 65 },
{ 영역: '독해', 학생: '김민준', 점수: 88 },
{ 영역: '듣기', 학생: '김민준', 점수: 72 },
{ 영역: '쓰기', 학생: '김민준', 점수: 60 },
{ 영역: '어휘', 학생: '이서연', 점수: 85 },
{ 영역: '문법', 학생: '이서연', 점수: 80 },
{ 영역: '독해', 학생: '이서연', 점수: 70 },
{ 영역: '듣기', 학생: '이서연', 점수: 90 },
{ 영역: '쓰기', 학생: '이서연', 점수: 76 },
],
},
semanticTypes: { 점수: 'Score' },
encodings: { x: '영역', y: '점수', color: '학생' },
},
}) } as ComponentProps<typeof Chart>;
export default function ChartRadarChartExample(props: Partial<ComponentProps<typeof Chart>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof Chart>;
return <Chart {...mergedProps} />;
}
Basic
import { Card } from '@mildang/design-system/Chart';
import { Text } from '@mildang/design-system/Text';
const CardBasicExample = () => (
<div style={{ width: 320 }}>
<Card>
<Text variant="caption-md" color="neutral.text.low">
이번 달 신규 가입
</Text>
<Text variant="title-lg">1,284명</Text>
</Card>
</div>
);
export default CardBasicExample;
영역 차트 + 요약 리스트
import { Card } from '@mildang/design-system/Chart';
import { Box, HStack, Stack } from '@mildang/styled-system/jsx';
import { AreaChart } from '@mildang/design-system/Chart';
import { getChartColor } from '@mildang/design-system/Chart';
import { css } from '@mildang/styled-system/css';
/** Card 상단 제목 + 설명 — Tremor 블록의 헤더 관례 */
function CardHeading({ title, description, action }: { title: string; description?: string; action?: React.ReactNode }) {
return (
<HStack justifyContent="space-between" alignItems="flex-start" gap="16">
<Box>
<Box as="h3" textStyle="title-md" color="primary.text.base">
{title}
</Box>
{description ? (
<Box as="p" mt="2" textStyle="caption-lg" color="primary.text.low">
{description}
</Box>
) : null}
</Box>
{action}
</HStack>
);
}
/** 색 점 + 라벨 + 값 + 비중 행 — 차트 하단/우측 요약 리스트 공용 */
function SummaryRow({ color, label, value, percent }: { color: string; label: string; value: string; percent: string }) {
return (
<div className={summaryRowClass}>
<Box flexShrink={0} width="8px" height="8px" borderRadius="full" style={{ backgroundColor: color }} />
<Box textStyle="caption-lg" color="primary.text.low" flexGrow={1}>
{label}
</Box>
<Box textStyle="caption-lg-medium" color="primary.text.base" fontVariantNumeric="tabular-nums">
{value}
</Box>
<Box textStyle="caption-lg" color="primary.text.lowest" fontVariantNumeric="tabular-nums" width="44px" textAlign="right">
{percent}
</Box>
</div>
);
}
const summaryRowClass = css({
display: 'flex',
alignItems: 'center',
gap: '8',
py: '10',
borderTopWidth: '1',
borderTopStyle: 'solid',
borderTopColor: 'primary.border.low',
});
/* ─── 공용 데이터 (밀당 도메인, 결정적) ─── */
const monthlyStudy = [
{ 월: '1월', 자기주도: 4120, 수업: 2280 },
{ 월: '2월', 자기주도: 3860, 수업: 2140 },
{ 월: '3월', 자기주도: 4510, 수업: 2620 },
{ 월: '4월', 자기주도: 4380, 수업: 2540 },
{ 월: '5월', 자기주도: 5230, 수업: 2860 },
{ 월: '6월', 자기주도: 5680, 수업: 3120 },
];
/* ─── 블록 구현 (스토리와 대시보드 조합에서 재사용) ─── */
function AreaSummaryBlock() {
const total = monthlyStudy.reduce(
(acc, d) => ({ 자기주도: acc.자기주도 + d.자기주도, 수업: acc.수업 + d.수업 }),
{ 자기주도: 0, 수업: 0 },
);
const sum = total.자기주도 + total.수업;
const series = [
{ key: '자기주도' as const, label: '자기주도 학습' },
{ key: '수업' as const, label: '수업' },
];
return (
<Card>
<Stack gap="20">
<CardHeading title="학습 시간 구성" description="최근 6개월 · 분 단위" />
<Box height="240px">
<AreaChart
data={monthlyStudy}
xKey="월"
yKeys={['자기주도', '수업']}
stackMode="stacked"
labels={{ 자기주도: '자기주도 학습', 수업: '수업' }}
showTooltip
margin={{ left: 60 }}
/>
</Box>
<Box>
{series.map(({ key, label }, i) => (
<SummaryRow
key={key}
color={getChartColor(i)}
label={label}
value={`${total[key].toLocaleString('ko-KR')}분`}
percent={`${((total[key] / sum) * 100).toFixed(1)}%`}
/>
))}
</Box>
</Stack>
</Card>
);
}
const ChartBlocksAreaWithSummaryListExample = () => <AreaSummaryBlock />;
export default ChartBlocksAreaWithSummaryListExample;
KPI 헤더 + 차트
import { Card } from '@mildang/design-system/Chart';
import { Box, HStack, Stack } from '@mildang/styled-system/jsx';
import { AreaChart } from '@mildang/design-system/Chart';
/* ─── 공용 헬퍼 ─── */
/** 증감 배지 pill — 양수 success, 음수 error */
function DeltaBadge({ delta, suffix = '%' }: { delta: number; suffix?: string }) {
const up = delta >= 0;
return (
<Box
as="span"
display="inline-flex"
alignItems="center"
px="8"
py="2"
borderRadius="6"
textStyle="caption-lg-medium"
bg={up ? 'success.bg.low' : 'error.bg.low'}
color={up ? 'success.text.base' : 'error.text.base'}
fontVariantNumeric="tabular-nums"
>
{up ? '▲' : '▼'} {Math.abs(delta).toLocaleString('ko-KR')}
{suffix}
</Box>
);
}
/** Card 상단 제목 + 설명 — Tremor 블록의 헤더 관례 */
function CardHeading({ title, description, action }: { title: string; description?: string; action?: React.ReactNode }) {
return (
<HStack justifyContent="space-between" alignItems="flex-start" gap="16">
<Box>
<Box as="h3" textStyle="title-md" color="primary.text.base">
{title}
</Box>
{description ? (
<Box as="p" mt="2" textStyle="caption-lg" color="primary.text.low">
{description}
</Box>
) : null}
</Box>
{action}
</HStack>
);
}
const weeklyActive = [
{ 주: '1주', 학생: 1820 },
{ 주: '2주', 학생: 1760 },
{ 주: '3주', 학생: 1930 },
{ 주: '4주', 학생: 2050 },
{ 주: '5주', 학생: 1990 },
{ 주: '6주', 학생: 2180 },
{ 주: '7주', 학생: 2340 },
{ 주: '8주', 학생: 2410 },
];
function KpiHeaderChartBlock() {
return (
<Card>
<Stack gap="20">
<CardHeading title="주간 활성 학생" description="최근 8주" action={<DeltaBadge delta={12.4} />} />
<HStack gap="4" alignItems="baseline">
<Box textStyle="headline-sm" color="primary.text.base" fontVariantNumeric="tabular-nums">
2,410
</Box>
<Box textStyle="contents-lg" color="primary.text.lowest">
명
</Box>
</HStack>
<Box height="180px">
<AreaChart data={weeklyActive} xKey="주" yKeys={['학생']} labels={{ 학생: '활성 학생' }} showTooltip showYAxis={false} />
</Box>
<Box pt="16" borderTopWidth="1" borderTopStyle="solid" borderTopColor="primary.border.low" textStyle="caption-lg" color="primary.text.low">
매주 월요일 00시 기준 집계
</Box>
</Stack>
</Card>
);
}
const ChartBlocksKpiHeaderChartExample = () => <KpiHeaderChartBlock />;
export default ChartBlocksKpiHeaderChartExample;
지표 전환 + 순위 리스트
import { useState } from 'react';
import { Card } from '@mildang/design-system/Chart';
import { Box, HStack, Stack } from '@mildang/styled-system/jsx';
import { SegmentedControl } from '@mildang/design-system/SegmentedControl';
import { AreaChart } from '@mildang/design-system/Chart';
import { BarList } from '@mildang/design-system/Chart';
/* ─── 공용 헬퍼 ─── */
/** 증감 배지 pill — 양수 success, 음수 error */
function DeltaBadge({ delta, suffix = '%' }: { delta: number; suffix?: string }) {
const up = delta >= 0;
return (
<Box
as="span"
display="inline-flex"
alignItems="center"
px="8"
py="2"
borderRadius="6"
textStyle="caption-lg-medium"
bg={up ? 'success.bg.low' : 'error.bg.low'}
color={up ? 'success.text.base' : 'error.text.base'}
fontVariantNumeric="tabular-nums"
>
{up ? '▲' : '▼'} {Math.abs(delta).toLocaleString('ko-KR')}
{suffix}
</Box>
);
}
/** Card 상단 제목 + 설명 — Tremor 블록의 헤더 관례 */
function CardHeading({ title, description, action }: { title: string; description?: string; action?: React.ReactNode }) {
return (
<HStack justifyContent="space-between" alignItems="flex-start" gap="16">
<Box>
<Box as="h3" textStyle="title-md" color="primary.text.base">
{title}
</Box>
{description ? (
<Box as="p" mt="2" textStyle="caption-lg" color="primary.text.low">
{description}
</Box>
) : null}
</Box>
{action}
</HStack>
);
}
/* ─── 공용 데이터 (밀당 도메인, 결정적) ─── */
const monthlyStudy = [
{ 월: '1월', 자기주도: 4120, 수업: 2280 },
{ 월: '2월', 자기주도: 3860, 수업: 2140 },
{ 월: '3월', 자기주도: 4510, 수업: 2620 },
{ 월: '4월', 자기주도: 4380, 수업: 2540 },
{ 월: '5월', 자기주도: 5230, 수업: 2860 },
{ 월: '6월', 자기주도: 5680, 수업: 3120 },
];
const metricData = {
학습시간: {
unit: '분',
total: 43340,
delta: 8.6,
trend: monthlyStudy.map(({ 월, 자기주도, 수업 }) => ({ 월, 값: 자기주도 + 수업 })),
ranking: [
{ label: '영어', value: 14200 },
{ label: '수학', value: 12800 },
{ label: '국어', value: 9340 },
{ label: '과학', value: 7000 },
],
},
문제풀이: {
unit: '문항',
total: 128400,
delta: -3.2,
trend: monthlyStudy.map(({ 월 }, i) => ({ 월, 값: 18200 + ((i * 1730) % 5200) })),
ranking: [
{ label: '수학', value: 48200 },
{ label: '영어', value: 39600 },
{ label: '과학', value: 22400 },
{ label: '국어', value: 18200 },
],
},
};
function MetricSwitcherBlock() {
const [metric, setMetric] = useState<keyof typeof metricData>('학습시간');
const current = metricData[metric];
return (
<Card>
<Stack gap="20">
<CardHeading
title="학습 지표"
description="최근 6개월 누적"
action={
<SegmentedControl size="sm" value={metric} onValueChange={(v) => setMetric(v as keyof typeof metricData)} aria-label="지표 선택">
<SegmentedControl.Item value="학습시간">학습 시간</SegmentedControl.Item>
<SegmentedControl.Item value="문제풀이">문제 풀이</SegmentedControl.Item>
</SegmentedControl>
}
/>
<HStack gap="8" alignItems="baseline">
<Box textStyle="headline-sm" color="primary.text.base" fontVariantNumeric="tabular-nums">
{current.total.toLocaleString('ko-KR')}
</Box>
<Box textStyle="contents-lg" color="primary.text.lowest">
{current.unit}
</Box>
<DeltaBadge delta={current.delta} />
</HStack>
<Box height="200px">
<AreaChart data={current.trend} xKey="월" yKeys={['값']} labels={{ 값: metric }} showTooltip showYAxis={false} />
</Box>
<Box>
<Box textStyle="caption-lg-medium" color="primary.text.low" mb="8">
과목별 순위
</Box>
<BarList data={current.ranking} ariaLabel={`${metric} 과목별 순위`} valueFormatter={(v) => `${v.toLocaleString('ko-KR')}${current.unit}`} />
</Box>
</Stack>
</Card>
);
}
const ChartBlocksMetricSwitcherExample = () => <MetricSwitcherBlock />;
export default ChartBlocksMetricSwitcherExample;
모니터링 + 기간 선택
import { useState } from 'react';
import { Card } from '@mildang/design-system/Chart';
import { Box, HStack, Stack } from '@mildang/styled-system/jsx';
import { SegmentedControl } from '@mildang/design-system/SegmentedControl';
import { AreaChart } from '@mildang/design-system/Chart';
/** Card 상단 제목 + 설명 — Tremor 블록의 헤더 관례 */
function CardHeading({ title, description, action }: { title: string; description?: string; action?: React.ReactNode }) {
return (
<HStack justifyContent="space-between" alignItems="flex-start" gap="16">
<Box>
<Box as="h3" textStyle="title-md" color="primary.text.base">
{title}
</Box>
{description ? (
<Box as="p" mt="2" textStyle="caption-lg" color="primary.text.low">
{description}
</Box>
) : null}
</Box>
{action}
</HStack>
);
}
const monitoringByPeriod = {
'1주': Array.from({ length: 7 }, (_, i) => ({ 구간: `${i + 1}일`, 요청: 4200 + ((i * 811) % 1900) })),
'4주': Array.from({ length: 4 }, (_, i) => ({ 구간: `${i + 1}주`, 요청: 30200 + ((i * 3391) % 8200) })),
'12주': Array.from({ length: 12 }, (_, i) => ({ 구간: `${i + 1}주`, 요청: 27800 + ((i * 2113) % 9600) })),
};
function MonitoringBlock() {
const [period, setPeriod] = useState<keyof typeof monitoringByPeriod>('4주');
return (
<Card>
<Stack gap="20">
<CardHeading title="AI 튜터 응답 모니터링" description="요청 수 · 성공률 · 평균 응답 시간" />
<HStack gap="24" flexWrap="wrap">
{[
{ label: '총 요청', value: '132,400건' },
{ label: '성공률', value: '99.2%' },
{ label: '평균 응답', value: '1.4초' },
].map(({ label, value }) => (
<Box key={label}>
<Box textStyle="caption-lg" color="primary.text.low">
{label}
</Box>
<Box textStyle="body-xl-semibold" color="primary.text.base" fontVariantNumeric="tabular-nums">
{value}
</Box>
</Box>
))}
</HStack>
<SegmentedControl
size="sm"
value={period}
onValueChange={(v) => setPeriod(v as keyof typeof monitoringByPeriod)}
aria-label="기간 선택"
>
<SegmentedControl.Item value="1주">1주</SegmentedControl.Item>
<SegmentedControl.Item value="4주">4주</SegmentedControl.Item>
<SegmentedControl.Item value="12주">12주</SegmentedControl.Item>
</SegmentedControl>
<Box height="200px">
<AreaChart data={monitoringByPeriod[period]} xKey="구간" yKeys={['요청']} labels={{ 요청: '요청 수' }} showTooltip margin={{ left: 60 }} />
</Box>
</Stack>
</Card>
);
}
const ChartBlocksMonitoringWithPeriodExample = () => <MonitoringBlock />;
export default ChartBlocksMonitoringWithPeriodExample;
KPI 그리드
import { Grid } from '@mildang/styled-system/jsx';
import { KpiCard } from '@mildang/design-system/Chart';
const weeklyActive = [
{ 주: '1주', 학생: 1820 },
{ 주: '2주', 학생: 1760 },
{ 주: '3주', 학생: 1930 },
{ 주: '4주', 학생: 2050 },
{ 주: '5주', 학생: 1990 },
{ 주: '6주', 학생: 2180 },
{ 주: '7주', 학생: 2340 },
{ 주: '8주', 학생: 2410 },
];
function KpiCardsBlock() {
return (
<Grid gridTemplateColumns={{ base: '1fr', sm: 'repeat(2, minmax(0, 1fr))', lg: 'repeat(4, minmax(0, 1fr))' }} gap="16">
<KpiCard label="활성 학생" value={2410} unit="명" delta={264} deltaLabel="전주 대비" sparklineData={weeklyActive.map((d) => d.학생)} />
<KpiCard label="평균 정답률" value={78.4} unit="%" delta={2.1} deltaLabel="전주 대비" format={(v) => v.toFixed(1)} />
<KpiCard label="평균 학습 시간" value={94} unit="분/일" delta={-6} deltaLabel="전주 대비" />
<KpiCard label="완료된 수업" value={1832} unit="회" delta={118} deltaLabel="전주 대비" />
</Grid>
);
}
const ChartBlocksKpiCardsGridExample = () => <KpiCardsBlock />;
export default ChartBlocksKpiCardsGridExample;
KPI + 진행률
import { Box, Grid, HStack, Stack } from '@mildang/styled-system/jsx';
import { Card } from '@mildang/design-system/Chart';
import { ProgressBar } from '@mildang/design-system/Progress';
const progressKpis = [
{ label: '이번 달 목표 수업', current: 1832, goal: 2400 },
{ label: '신규 가입 목표', current: 428, goal: 500 },
{ label: '학부모 상담 처리', current: 96, goal: 120 },
];
function KpiProgressBlock() {
return (
<Grid gridTemplateColumns={{ base: '1fr', md: 'repeat(3, minmax(0, 1fr))' }} gap="16">
{progressKpis.map(({ label, current, goal }) => {
const percent = Math.round((current / goal) * 100);
return (
<Card key={label} p="16">
<Stack gap="12">
<Box textStyle="caption-lg-medium" color="primary.text.low">
{label}
</Box>
<HStack gap="4" alignItems="baseline">
<Box textStyle="title-2xl" color="primary.text.base" fontVariantNumeric="tabular-nums">
{current.toLocaleString('ko-KR')}
</Box>
<Box textStyle="caption-lg" color="primary.text.lowest" fontVariantNumeric="tabular-nums">
/ {goal.toLocaleString('ko-KR')}
</Box>
</HStack>
<HStack gap="8" alignItems="center">
<Box flexGrow={1}>
<ProgressBar value={percent} type="conditional" />
</Box>
<Box textStyle="caption-lg-medium" color="primary.text.base" fontVariantNumeric="tabular-nums" width="36px" textAlign="right">
{percent}%
</Box>
</HStack>
</Stack>
</Card>
);
})}
</Grid>
);
}
const ChartBlocksKpiWithProgressExample = () => <KpiProgressBlock />;
export default ChartBlocksKpiWithProgressExample;
도넛 + 범례 리스트
import { Card } from '@mildang/design-system/Chart';
import { Box, Grid, HStack, Stack } from '@mildang/styled-system/jsx';
import { PieChart } from '@mildang/design-system/Chart';
import { getChartColor } from '@mildang/design-system/Chart';
import { css } from '@mildang/styled-system/css';
/** Card 상단 제목 + 설명 — Tremor 블록의 헤더 관례 */
function CardHeading({ title, description, action }: { title: string; description?: string; action?: React.ReactNode }) {
return (
<HStack justifyContent="space-between" alignItems="flex-start" gap="16">
<Box>
<Box as="h3" textStyle="title-md" color="primary.text.base">
{title}
</Box>
{description ? (
<Box as="p" mt="2" textStyle="caption-lg" color="primary.text.low">
{description}
</Box>
) : null}
</Box>
{action}
</HStack>
);
}
/** 색 점 + 라벨 + 값 + 비중 행 — 차트 하단/우측 요약 리스트 공용 */
function SummaryRow({ color, label, value, percent }: { color: string; label: string; value: string; percent: string }) {
return (
<div className={summaryRowClass}>
<Box flexShrink={0} width="8px" height="8px" borderRadius="full" style={{ backgroundColor: color }} />
<Box textStyle="caption-lg" color="primary.text.low" flexGrow={1}>
{label}
</Box>
<Box textStyle="caption-lg-medium" color="primary.text.base" fontVariantNumeric="tabular-nums">
{value}
</Box>
<Box textStyle="caption-lg" color="primary.text.lowest" fontVariantNumeric="tabular-nums" width="44px" textAlign="right">
{percent}
</Box>
</div>
);
}
const summaryRowClass = css({
display: 'flex',
alignItems: 'center',
gap: '8',
py: '10',
borderTopWidth: '1',
borderTopStyle: 'solid',
borderTopColor: 'primary.border.low',
});
const channelData = [
{ 채널: '앱', 학생수: 1240 },
{ 채널: '웹', 학생수: 680 },
{ 채널: '태블릿', 학생수: 380 },
{ 채널: '기타', 학생수: 110 },
];
function DonutLegendBlock() {
const sum = channelData.reduce((acc, d) => acc + d.학생수, 0);
return (
<Card>
<Stack gap="20">
<CardHeading title="접속 채널 분포" description="최근 30일 활성 학생 기준" />
<Grid gridTemplateColumns={{ base: '1fr', md: '200px minmax(0, 1fr)' }} gap="20" alignItems="center">
<Box height="200px">
<PieChart data={channelData} valueKey="학생수" labelKey="채널" innerRadius="donut" />
</Box>
<Box>
{channelData.map((d, i) => (
<SummaryRow
key={d.채널}
color={getChartColor(i)}
label={d.채널}
value={`${d.학생수.toLocaleString('ko-KR')}명`}
percent={`${((d.학생수 / sum) * 100).toFixed(1)}%`}
/>
))}
</Box>
</Grid>
</Stack>
</Card>
);
}
const ChartBlocksDonutWithLegendListExample = () => <DonutLegendBlock />;
export default ChartBlocksDonutWithLegendListExample;
대시보드 조합
import { Box, Grid, HStack, Stack } from '@mildang/styled-system/jsx';
import { useState } from 'react';
import { Card } from '@mildang/design-system/Chart';
import { SegmentedControl } from '@mildang/design-system/SegmentedControl';
import { AreaChart } from '@mildang/design-system/Chart';
import { BarList } from '@mildang/design-system/Chart';
import { PieChart } from '@mildang/design-system/Chart';
import { getChartColor } from '@mildang/design-system/Chart';
import { css } from '@mildang/styled-system/css';
import { ProgressBar } from '@mildang/design-system/Progress';
import { KpiCard } from '@mildang/design-system/Chart';
/* ─── 공용 헬퍼 ─── */
/** 증감 배지 pill — 양수 success, 음수 error */
function DeltaBadge({ delta, suffix = '%' }: { delta: number; suffix?: string }) {
const up = delta >= 0;
return (
<Box
as="span"
display="inline-flex"
alignItems="center"
px="8"
py="2"
borderRadius="6"
textStyle="caption-lg-medium"
bg={up ? 'success.bg.low' : 'error.bg.low'}
color={up ? 'success.text.base' : 'error.text.base'}
fontVariantNumeric="tabular-nums"
>
{up ? '▲' : '▼'} {Math.abs(delta).toLocaleString('ko-KR')}
{suffix}
</Box>
);
}
/** Card 상단 제목 + 설명 — Tremor 블록의 헤더 관례 */
function CardHeading({ title, description, action }: { title: string; description?: string; action?: React.ReactNode }) {
return (
<HStack justifyContent="space-between" alignItems="flex-start" gap="16">
<Box>
<Box as="h3" textStyle="title-md" color="primary.text.base">
{title}
</Box>
{description ? (
<Box as="p" mt="2" textStyle="caption-lg" color="primary.text.low">
{description}
</Box>
) : null}
</Box>
{action}
</HStack>
);
}
/** 색 점 + 라벨 + 값 + 비중 행 — 차트 하단/우측 요약 리스트 공용 */
function SummaryRow({ color, label, value, percent }: { color: string; label: string; value: string; percent: string }) {
return (
<div className={summaryRowClass}>
<Box flexShrink={0} width="8px" height="8px" borderRadius="full" style={{ backgroundColor: color }} />
<Box textStyle="caption-lg" color="primary.text.low" flexGrow={1}>
{label}
</Box>
<Box textStyle="caption-lg-medium" color="primary.text.base" fontVariantNumeric="tabular-nums">
{value}
</Box>
<Box textStyle="caption-lg" color="primary.text.lowest" fontVariantNumeric="tabular-nums" width="44px" textAlign="right">
{percent}
</Box>
</div>
);
}
const summaryRowClass = css({
display: 'flex',
alignItems: 'center',
gap: '8',
py: '10',
borderTopWidth: '1',
borderTopStyle: 'solid',
borderTopColor: 'primary.border.low',
});
/* ─── 공용 데이터 (밀당 도메인, 결정적) ─── */
const monthlyStudy = [
{ 월: '1월', 자기주도: 4120, 수업: 2280 },
{ 월: '2월', 자기주도: 3860, 수업: 2140 },
{ 월: '3월', 자기주도: 4510, 수업: 2620 },
{ 월: '4월', 자기주도: 4380, 수업: 2540 },
{ 월: '5월', 자기주도: 5230, 수업: 2860 },
{ 월: '6월', 자기주도: 5680, 수업: 3120 },
];
const weeklyActive = [
{ 주: '1주', 학생: 1820 },
{ 주: '2주', 학생: 1760 },
{ 주: '3주', 학생: 1930 },
{ 주: '4주', 학생: 2050 },
{ 주: '5주', 학생: 1990 },
{ 주: '6주', 학생: 2180 },
{ 주: '7주', 학생: 2340 },
{ 주: '8주', 학생: 2410 },
];
/* ─── 블록 구현 (스토리와 대시보드 조합에서 재사용) ─── */
function AreaSummaryBlock() {
const total = monthlyStudy.reduce(
(acc, d) => ({ 자기주도: acc.자기주도 + d.자기주도, 수업: acc.수업 + d.수업 }),
{ 자기주도: 0, 수업: 0 },
);
const sum = total.자기주도 + total.수업;
const series = [
{ key: '자기주도' as const, label: '자기주도 학습' },
{ key: '수업' as const, label: '수업' },
];
return (
<Card>
<Stack gap="20">
<CardHeading title="학습 시간 구성" description="최근 6개월 · 분 단위" />
<Box height="240px">
<AreaChart
data={monthlyStudy}
xKey="월"
yKeys={['자기주도', '수업']}
stackMode="stacked"
labels={{ 자기주도: '자기주도 학습', 수업: '수업' }}
showTooltip
margin={{ left: 60 }}
/>
</Box>
<Box>
{series.map(({ key, label }, i) => (
<SummaryRow
key={key}
color={getChartColor(i)}
label={label}
value={`${total[key].toLocaleString('ko-KR')}분`}
percent={`${((total[key] / sum) * 100).toFixed(1)}%`}
/>
))}
</Box>
</Stack>
</Card>
);
}
const metricData = {
학습시간: {
unit: '분',
total: 43340,
delta: 8.6,
trend: monthlyStudy.map(({ 월, 자기주도, 수업 }) => ({ 월, 값: 자기주도 + 수업 })),
ranking: [
{ label: '영어', value: 14200 },
{ label: '수학', value: 12800 },
{ label: '국어', value: 9340 },
{ label: '과학', value: 7000 },
],
},
문제풀이: {
unit: '문항',
total: 128400,
delta: -3.2,
trend: monthlyStudy.map(({ 월 }, i) => ({ 월, 값: 18200 + ((i * 1730) % 5200) })),
ranking: [
{ label: '수학', value: 48200 },
{ label: '영어', value: 39600 },
{ label: '과학', value: 22400 },
{ label: '국어', value: 18200 },
],
},
};
function MetricSwitcherBlock() {
const [metric, setMetric] = useState<keyof typeof metricData>('학습시간');
const current = metricData[metric];
return (
<Card>
<Stack gap="20">
<CardHeading
title="학습 지표"
description="최근 6개월 누적"
action={
<SegmentedControl size="sm" value={metric} onValueChange={(v) => setMetric(v as keyof typeof metricData)} aria-label="지표 선택">
<SegmentedControl.Item value="학습시간">학습 시간</SegmentedControl.Item>
<SegmentedControl.Item value="문제풀이">문제 풀이</SegmentedControl.Item>
</SegmentedControl>
}
/>
<HStack gap="8" alignItems="baseline">
<Box textStyle="headline-sm" color="primary.text.base" fontVariantNumeric="tabular-nums">
{current.total.toLocaleString('ko-KR')}
</Box>
<Box textStyle="contents-lg" color="primary.text.lowest">
{current.unit}
</Box>
<DeltaBadge delta={current.delta} />
</HStack>
<Box height="200px">
<AreaChart data={current.trend} xKey="월" yKeys={['값']} labels={{ 값: metric }} showTooltip showYAxis={false} />
</Box>
<Box>
<Box textStyle="caption-lg-medium" color="primary.text.low" mb="8">
과목별 순위
</Box>
<BarList data={current.ranking} ariaLabel={`${metric} 과목별 순위`} valueFormatter={(v) => `${v.toLocaleString('ko-KR')}${current.unit}`} />
</Box>
</Stack>
</Card>
);
}
const monitoringByPeriod = {
'1주': Array.from({ length: 7 }, (_, i) => ({ 구간: `${i + 1}일`, 요청: 4200 + ((i * 811) % 1900) })),
'4주': Array.from({ length: 4 }, (_, i) => ({ 구간: `${i + 1}주`, 요청: 30200 + ((i * 3391) % 8200) })),
'12주': Array.from({ length: 12 }, (_, i) => ({ 구간: `${i + 1}주`, 요청: 27800 + ((i * 2113) % 9600) })),
};
function MonitoringBlock() {
const [period, setPeriod] = useState<keyof typeof monitoringByPeriod>('4주');
return (
<Card>
<Stack gap="20">
<CardHeading title="AI 튜터 응답 모니터링" description="요청 수 · 성공률 · 평균 응답 시간" />
<HStack gap="24" flexWrap="wrap">
{[
{ label: '총 요청', value: '132,400건' },
{ label: '성공률', value: '99.2%' },
{ label: '평균 응답', value: '1.4초' },
].map(({ label, value }) => (
<Box key={label}>
<Box textStyle="caption-lg" color="primary.text.low">
{label}
</Box>
<Box textStyle="body-xl-semibold" color="primary.text.base" fontVariantNumeric="tabular-nums">
{value}
</Box>
</Box>
))}
</HStack>
<SegmentedControl
size="sm"
value={period}
onValueChange={(v) => setPeriod(v as keyof typeof monitoringByPeriod)}
aria-label="기간 선택"
>
<SegmentedControl.Item value="1주">1주</SegmentedControl.Item>
<SegmentedControl.Item value="4주">4주</SegmentedControl.Item>
<SegmentedControl.Item value="12주">12주</SegmentedControl.Item>
</SegmentedControl>
<Box height="200px">
<AreaChart data={monitoringByPeriod[period]} xKey="구간" yKeys={['요청']} labels={{ 요청: '요청 수' }} showTooltip margin={{ left: 60 }} />
</Box>
</Stack>
</Card>
);
}
function KpiCardsBlock() {
return (
<Grid gridTemplateColumns={{ base: '1fr', sm: 'repeat(2, minmax(0, 1fr))', lg: 'repeat(4, minmax(0, 1fr))' }} gap="16">
<KpiCard label="활성 학생" value={2410} unit="명" delta={264} deltaLabel="전주 대비" sparklineData={weeklyActive.map((d) => d.학생)} />
<KpiCard label="평균 정답률" value={78.4} unit="%" delta={2.1} deltaLabel="전주 대비" format={(v) => v.toFixed(1)} />
<KpiCard label="평균 학습 시간" value={94} unit="분/일" delta={-6} deltaLabel="전주 대비" />
<KpiCard label="완료된 수업" value={1832} unit="회" delta={118} deltaLabel="전주 대비" />
</Grid>
);
}
const progressKpis = [
{ label: '이번 달 목표 수업', current: 1832, goal: 2400 },
{ label: '신규 가입 목표', current: 428, goal: 500 },
{ label: '학부모 상담 처리', current: 96, goal: 120 },
];
function KpiProgressBlock() {
return (
<Grid gridTemplateColumns={{ base: '1fr', md: 'repeat(3, minmax(0, 1fr))' }} gap="16">
{progressKpis.map(({ label, current, goal }) => {
const percent = Math.round((current / goal) * 100);
return (
<Card key={label} p="16">
<Stack gap="12">
<Box textStyle="caption-lg-medium" color="primary.text.low">
{label}
</Box>
<HStack gap="4" alignItems="baseline">
<Box textStyle="title-2xl" color="primary.text.base" fontVariantNumeric="tabular-nums">
{current.toLocaleString('ko-KR')}
</Box>
<Box textStyle="caption-lg" color="primary.text.lowest" fontVariantNumeric="tabular-nums">
/ {goal.toLocaleString('ko-KR')}
</Box>
</HStack>
<HStack gap="8" alignItems="center">
<Box flexGrow={1}>
<ProgressBar value={percent} type="conditional" />
</Box>
<Box textStyle="caption-lg-medium" color="primary.text.base" fontVariantNumeric="tabular-nums" width="36px" textAlign="right">
{percent}%
</Box>
</HStack>
</Stack>
</Card>
);
})}
</Grid>
);
}
const channelData = [
{ 채널: '앱', 학생수: 1240 },
{ 채널: '웹', 학생수: 680 },
{ 채널: '태블릿', 학생수: 380 },
{ 채널: '기타', 학생수: 110 },
];
function DonutLegendBlock() {
const sum = channelData.reduce((acc, d) => acc + d.학생수, 0);
return (
<Card>
<Stack gap="20">
<CardHeading title="접속 채널 분포" description="최근 30일 활성 학생 기준" />
<Grid gridTemplateColumns={{ base: '1fr', md: '200px minmax(0, 1fr)' }} gap="20" alignItems="center">
<Box height="200px">
<PieChart data={channelData} valueKey="학생수" labelKey="채널" innerRadius="donut" />
</Box>
<Box>
{channelData.map((d, i) => (
<SummaryRow
key={d.채널}
color={getChartColor(i)}
label={d.채널}
value={`${d.학생수.toLocaleString('ko-KR')}명`}
percent={`${((d.학생수 / sum) * 100).toFixed(1)}%`}
/>
))}
</Box>
</Grid>
</Stack>
</Card>
);
}
const ChartBlocksDashboardExample = () => (
<Stack gap="20">
<KpiCardsBlock />
<KpiProgressBlock />
<Grid gridTemplateColumns={{ base: '1fr', lg: 'repeat(2, minmax(0, 1fr))' }} gap="20">
<AreaSummaryBlock />
<DonutLegendBlock />
</Grid>
<Grid gridTemplateColumns={{ base: '1fr', lg: 'repeat(2, minmax(0, 1fr))' }} gap="20">
<MetricSwitcherBlock />
<MonitoringBlock />
</Grid>
</Stack>
);
export default ChartBlocksDashboardExample;
색상 프리셋
import { CHART_COLOR_PRESET } from '@mildang/design-system/Chart';
const CHART_STROKE_PRESET: string[] = ['#2563eb', '#059669', '#7c3aed', '#d97706', '#4b5563', '#0891b2', '#db2777', '#65a30d', '#c026d3'];
const CHART_TEXT_COLOR_PRESET: string[] = [
'#ffffff',
'#ffffff',
'#ffffff',
'#111827',
'#ffffff',
'#ffffff',
'#ffffff',
'#111827',
'#ffffff',
];
const TREMOR_NAMES = ['blue', 'emerald', 'violet', 'amber', 'gray', 'cyan', 'pink', 'lime', 'fuchsia'];
const ChartColorsPaletteExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{CHART_COLOR_PRESET.map((fill, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{/* Fill */}
<div
style={{
width: 32,
height: 32,
borderRadius: 6,
backgroundColor: fill,
border: `2px solid ${CHART_STROKE_PRESET[i]}`,
flexShrink: 0,
}}
/>
{/* Text color sample */}
<div
style={{
width: 32,
height: 32,
borderRadius: 6,
backgroundColor: fill,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<span style={{ color: CHART_TEXT_COLOR_PRESET[i], fontSize: 11, fontWeight: 700 }}>Aa</span>
</div>
{/* Stroke swatch */}
<div
style={{
width: 12,
height: 32,
borderRadius: 3,
backgroundColor: CHART_STROKE_PRESET[i],
flexShrink: 0,
}}
/>
{/* Token name */}
<span style={{ fontFamily: 'monospace', fontSize: 12, color: '#666' }}>
{TREMOR_NAMES[i]}
<span style={{ color: '#bbb' }}> (fill / stroke / text)</span>
</span>
</div>
))}
</div>
);
export default ChartColorsPaletteExample;
기본 색상 (프리셋)
import { PieChart } from '@mildang/design-system/Chart';
import { BarChart } from '@mildang/design-system/Chart';
const sampleData = [
{ subject: '수학', score: 35 },
{ subject: '영어', score: 25 },
{ subject: '국어', score: 20 },
{ subject: '과학', score: 12 },
{ subject: '사회', score: 8 },
];
const barData = [
{ month: '1월', a: 420, b: 280 },
{ month: '2월', a: 380, b: 250 },
{ month: '3월', a: 510, b: 320 },
{ month: '4월', a: 460, b: 300 },
{ month: '5월', a: 580, b: 350 },
];
const ChartColorsDefaultColorsExample = () => (
<div style={{ display: 'flex', gap: 32, alignItems: 'flex-start' }}>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Donut</div>
<div style={{ width: 280, height: 280 }}>
<PieChart data={sampleData} valueKey="score" labelKey="subject" innerRadius="donut" showLegend />
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Bar</div>
<div style={{ width: 400, height: 280 }}>
<BarChart data={barData} xKey="month" yKeys={['a', 'b']} groupMode="grouped" labels={{ a: '계획', b: '실적' }} showLegend />
</div>
</div>
</div>
);
export default ChartColorsDefaultColorsExample;
Tableau 20 팔레트
const TABLEAU_20: string[] = ['#4E79A7', '#A0CBE8', '#F28E2B', '#FFBE7D', '#59A14F', '#8CD17D', '#B6992D', '#F1CE63', '#499894', '#86BCB6', '#E15759', '#FF9D9A', '#79706E', '#BAB0AC', '#D37295', '#FABFD2', '#B07AA1', '#D4A6C8', '#9D7660', '#D7B5A6'];
const ChartColorsTableau20PaletteExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ marginBottom: 8, fontFamily: 'monospace', fontSize: 11, color: '#888' }}>
Tableau 20 — 진한색(짝수) + 연한색(홀수) 10쌍
</div>
{Array.from({ length: 10 }, (_, i) => {
const dark = TABLEAU_20[i * 2];
const light = TABLEAU_20[i * 2 + 1];
return (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 32, height: 24, borderRadius: 4, backgroundColor: dark, flexShrink: 0 }} />
<div style={{ width: 32, height: 24, borderRadius: 4, backgroundColor: light, flexShrink: 0 }} />
<span style={{ fontFamily: 'monospace', fontSize: 11, color: '#666' }}>
{dark} · {light}
</span>
</div>
);
})}
</div>
);
export default ChartColorsTableau20PaletteExample;
Tableau 20 — 차트 적용
import { PieChart } from '@mildang/design-system/Chart';
import { BarChart } from '@mildang/design-system/Chart';
const TABLEAU_20: string[] = ['#4E79A7', '#A0CBE8', '#F28E2B', '#FFBE7D', '#59A14F', '#8CD17D', '#B6992D', '#F1CE63', '#499894', '#86BCB6', '#E15759', '#FF9D9A', '#79706E', '#BAB0AC', '#D37295', '#FABFD2', '#B07AA1', '#D4A6C8', '#9D7660', '#D7B5A6'];
const sampleData = [
{ subject: '수학', score: 35 },
{ subject: '영어', score: 25 },
{ subject: '국어', score: 20 },
{ subject: '과학', score: 12 },
{ subject: '사회', score: 8 },
];
const barData = [
{ month: '1월', a: 420, b: 280 },
{ month: '2월', a: 380, b: 250 },
{ month: '3월', a: 510, b: 320 },
{ month: '4월', a: 460, b: 300 },
{ month: '5월', a: 580, b: 350 },
];
const ChartColorsTableau20ChartsExample = () => (
<div style={{ display: 'flex', gap: 32, alignItems: 'flex-start' }}>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Donut — 진한색 5개</div>
<div style={{ width: 280, height: 280 }}>
<PieChart
data={sampleData}
valueKey="score"
labelKey="subject"
innerRadius="donut"
showLegend
colors={[0, 2, 4, 6, 8].map((i) => TABLEAU_20[i])}
/>
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Bar — 진한색 + 연한색 쌍</div>
<div style={{ width: 400, height: 280 }}>
<BarChart
data={barData}
xKey="month"
yKeys={['a', 'b']}
groupMode="grouped"
labels={{ a: '계획', b: '실적' }}
showLegend
colors={[TABLEAU_20[0], TABLEAU_20[1]]}
/>
</div>
</div>
</div>
);
export default ChartColorsTableau20ChartsExample;
Tremor 팔레트
const TREMOR_9: string[] = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#6b7280', '#06b6d4', '#ec4899', '#84cc16', '#d946ef'];
const TREMOR_NAMES = ['blue', 'emerald', 'violet', 'amber', 'gray', 'cyan', 'pink', 'lime', 'fuchsia'];
const ChartColorsTremor9PaletteExample = () => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{ marginBottom: 8, fontFamily: 'monospace', fontSize: 11, color: '#888' }}>
Tremor AvailableChartColors — Tailwind 500 계열 9색
</div>
{TREMOR_9.map((color, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 32, height: 24, borderRadius: 4, backgroundColor: color, flexShrink: 0 }} />
<span style={{ fontFamily: 'monospace', fontSize: 11, color: '#666' }}>
{color} · {TREMOR_NAMES[i]}
</span>
</div>
))}
</div>
);
export default ChartColorsTremor9PaletteExample;
Tremor — 차트 적용
import { PieChart } from '@mildang/design-system/Chart';
import { BarChart } from '@mildang/design-system/Chart';
const TREMOR_9: string[] = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#6b7280', '#06b6d4', '#ec4899', '#84cc16', '#d946ef'];
const sampleData = [
{ subject: '수학', score: 35 },
{ subject: '영어', score: 25 },
{ subject: '국어', score: 20 },
{ subject: '과학', score: 12 },
{ subject: '사회', score: 8 },
];
const barData = [
{ month: '1월', a: 420, b: 280 },
{ month: '2월', a: 380, b: 250 },
{ month: '3월', a: 510, b: 320 },
{ month: '4월', a: 460, b: 300 },
{ month: '5월', a: 580, b: 350 },
];
const ChartColorsTremor9ChartsExample = () => (
<div style={{ display: 'flex', gap: 32, alignItems: 'flex-start' }}>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Donut</div>
<div style={{ width: 280, height: 280 }}>
<PieChart
data={sampleData}
valueKey="score"
labelKey="subject"
innerRadius="donut"
showLegend
colors={TREMOR_9}
/>
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Bar</div>
<div style={{ width: 400, height: 280 }}>
<BarChart
data={barData}
xKey="month"
yKeys={['a', 'b']}
groupMode="grouped"
labels={{ a: '계획', b: '실적' }}
showLegend
colors={[TREMOR_9[0], TREMOR_9[5]]}
/>
</div>
</div>
</div>
);
export default ChartColorsTremor9ChartsExample;
커스텀 색상
import { PieChart } from '@mildang/design-system/Chart';
import { BarChart } from '@mildang/design-system/Chart';
import { token } from '@mildang/styled-system/tokens';
const sampleData = [
{ subject: '수학', score: 35 },
{ subject: '영어', score: 25 },
{ subject: '국어', score: 20 },
{ subject: '과학', score: 12 },
{ subject: '사회', score: 8 },
];
const barData = [
{ month: '1월', a: 420, b: 280 },
{ month: '2월', a: 380, b: 250 },
{ month: '3월', a: 510, b: 320 },
{ month: '4월', a: 460, b: 300 },
{ month: '5월', a: 580, b: 350 },
];
// 커스텀 팔레트 예시 — goes 계열 5색
const customColors = [
token('colors.goes.violet.light.500'),
token('colors.goes.green.light.500'),
token('colors.goes.orange.light.500'),
token('colors.goes.red.light.500'),
token('colors.goes.light.500'),
];
const customBarColors = [
token('colors.goes.violet.light.500'),
token('colors.goes.green.light.500'),
];
const ChartColorsCustomColorsExample = () => (
<div style={{ display: 'flex', gap: 32, alignItems: 'flex-start' }}>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Donut</div>
<div style={{ width: 280, height: 280 }}>
<PieChart
data={sampleData}
valueKey="score"
labelKey="subject"
innerRadius="donut"
showLegend
colors={customColors}
/>
</div>
</div>
<div>
<div style={{ marginBottom: 8, fontSize: 12, color: '#888' }}>Bar</div>
<div style={{ width: 400, height: 280 }}>
<BarChart
data={barData}
xKey="month"
yKeys={['a', 'b']}
groupMode="grouped"
labels={{ a: '계획', b: '실적' }}
showLegend
colors={customBarColors}
/>
</div>
</div>
</div>
);
export default ChartColorsCustomColorsExample;
행 방향
import type { ComponentProps } from 'react';
import { token } from '@mildang/styled-system/tokens';
import { ChartLegend } from '@mildang/design-system/Chart';
const legendItems = [
{ key: 'math', label: '수학', color: token('colors.info.border.high') },
{ key: 'english', label: '영어', color: token('colors.warning.border.high') },
{ key: 'korean', label: '국어', color: token('colors.positive.border.high') },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
items: legendItems,
}) } as ComponentProps<typeof ChartLegend>;
export default function ChartLegendRowExample(props: Partial<ComponentProps<typeof ChartLegend>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartLegend>;
return <ChartLegend {...mergedProps} />;
}
열 방향
import type { ComponentProps } from 'react';
import { token } from '@mildang/styled-system/tokens';
import { ChartLegend } from '@mildang/design-system/Chart';
const legendItems = [
{ key: 'math', label: '수학', color: token('colors.info.border.high') },
{ key: 'english', label: '영어', color: token('colors.warning.border.high') },
{ key: 'korean', label: '국어', color: token('colors.positive.border.high') },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
items: legendItems,
direction: 'column',
}) } as ComponentProps<typeof ChartLegend>;
export default function ChartLegendColumnExample(props: Partial<ComponentProps<typeof ChartLegend>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartLegend>;
return <ChartLegend {...mergedProps} />;
}
값 포함
import type { ComponentProps } from 'react';
import { token } from '@mildang/styled-system/tokens';
import { ChartLegend } from '@mildang/design-system/Chart';
const valueLegendItems = [
{ key: 'math', label: '수학', value: '35명', color: token('colors.info.border.high') },
{ key: 'english', label: '영어', value: '25명', color: token('colors.warning.border.high') },
{ key: 'korean', label: '국어', value: '20명', color: token('colors.positive.border.high') },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
items: valueLegendItems,
direction: 'column',
}) } as ComponentProps<typeof ChartLegend>;
export default function ChartLegendWithValuesExample(props: Partial<ComponentProps<typeof ChartLegend>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartLegend>;
return <ChartLegend {...mergedProps} />;
}
패턴 swatch
import type { ComponentProps } from 'react';
import { token } from '@mildang/styled-system/tokens';
import { createElement } from 'react';
import { ChartLegend } from '@mildang/design-system/Chart';
const patternId = 'chart-legend-story-pattern';
const patternLegendItems = [
{
key: 'revenue',
label: '매출',
value: '4,200',
color: token('colors.info.surface.base'),
strokeColor: token('colors.info.border.highest'),
patternId,
patternDefinition: createElement(
'pattern',
{
id: patternId,
width: 8,
height: 8,
patternUnits: 'userSpaceOnUse',
},
createElement('rect', {
width: 8,
height: 8,
fill: token('colors.info.surface.base'),
}),
createElement('path', {
d: 'M-2,2 l4,-4 M0,8 l8,-8 M6,10 l4,-4',
stroke: token('colors.info.border.highest'),
strokeWidth: 1.5,
})
),
},
{
key: 'cost',
label: '비용',
value: '2,800',
color: token('colors.warning.surface.high'),
strokeColor: token('colors.warning.border.highest'),
},
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
items: patternLegendItems,
direction: 'column',
}) } as ComponentProps<typeof ChartLegend>;
export default function ChartLegendPatternSwatchExample(props: Partial<ComponentProps<typeof ChartLegend>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartLegend>;
return <ChartLegend {...mergedProps} />;
}
방향 비교
import { css } from '@mildang/styled-system/css';
import { ChartLegend } from '@mildang/design-system/Chart';
import { token } from '@mildang/styled-system/tokens';
const legendItems = [
{ key: 'math', label: '수학', color: token('colors.info.border.high') },
{ key: 'english', label: '영어', color: token('colors.warning.border.high') },
{ key: 'korean', label: '국어', color: token('colors.positive.border.high') },
];
const valueLegendItems = [
{ key: 'math', label: '수학', value: '35명', color: token('colors.info.border.high') },
{ key: 'english', label: '영어', value: '25명', color: token('colors.warning.border.high') },
{ key: 'korean', label: '국어', value: '20명', color: token('colors.positive.border.high') },
];
const ChartLegendDirectionComparisonExample = () => (
<div
className={css({
display: 'flex',
flexDirection: 'column',
gap: '24',
})}
>
<ChartLegend items={legendItems} />
<ChartLegend items={valueLegendItems} direction="column" />
</div>
);
export default ChartLegendDirectionComparisonExample;
Basic
import { ChartTooltip, ChartTooltipContent, getChartColor } from '@mildang/design-system/Chart';
const ChartTooltipBasicExample = () => (
<div style={{ position: 'relative', width: 280, height: 120 }}>
<ChartTooltip
tooltipOpen
tooltipLeft={24}
tooltipTop={16}
tooltipData={{ label: '3월' }}
renderContent={(data) => (
<ChartTooltipContent
label={data.label}
items={[
{ name: '신규', value: 1284, color: getChartColor(0) },
{ name: '재방문', value: 842, color: getChartColor(1) },
]}
/>
)}
/>
</div>
);
export default ChartTooltipBasicExample;
Indicator — dot (기본)
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: '1월',
items: multiItems,
indicator: 'dot',
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentDotExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
Indicator — line
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: '1월',
items: multiItems,
indicator: 'line',
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentLineExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
Indicator 비교
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const ChartTooltipContentAllIndicatorsExample = () => (
<div style={{ display: 'flex', gap: 16 }}>
<ChartTooltipContent label="dot" items={multiItems} indicator="dot" />
<ChartTooltipContent label="line" items={multiItems} indicator="line" />
</div>
);
export default ChartTooltipContentAllIndicatorsExample;
단일 아이템 (파이 차트 스타일)
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const singleItem = [
{ color: '#4f86f7', name: 'Chrome', value: '275 (34.1%)' },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
items: singleItem,
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentSingleItemExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
다중 시리즈
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const manyItems = [
{ color: '#4f86f7', name: 'Desktop', value: 186 },
{ color: '#f7954f', name: 'Mobile', value: 80 },
{ color: '#6fcf97', name: 'Tablet', value: 44 },
{ color: '#bb6bd9', name: 'Smart TV', value: 12 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: 'January',
items: manyItems,
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentManyItemsExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
값 없음 (이름만)
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: '범주',
items: multiItems.map(({ color, name }) => ({ color, name })),
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentNoValueExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
hideIndicator
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: '1월',
items: multiItems,
hideIndicator: true,
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentHideIndicatorExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
hideLabel
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: '1월',
items: multiItems,
hideLabel: true,
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentHideLabelExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}
Playground
import type { ComponentProps } from 'react';
import { ChartTooltipContent } from '@mildang/design-system/Chart';
const multiItems = [
{ color: '#4f86f7', name: '매출', value: 4200 },
{ color: '#f7954f', name: '비용', value: 2800 },
];
const STORY_DEFAULT_ARGS = { ...({}), ...({
label: '1월',
items: multiItems,
indicator: 'dot',
hideLabel: false,
hideIndicator: false,
}) } as ComponentProps<typeof ChartTooltipContent>;
export default function ChartTooltipContentPlaygroundExample(props: Partial<ComponentProps<typeof ChartTooltipContent>> = {}) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as ComponentProps<typeof ChartTooltipContent>;
return <ChartTooltipContent {...mergedProps} />;
}