데이터 모델
데이터 모델
차트를 고르기 전에 데이터 shape를 먼저 고정합니다. 공통 Cartesian 모델, 특수 차트 모델, formatter, empty state를 구현 기준으로 분류합니다.
reopt designUpdated
1. 공통 Cartesian 모델
LineChart, BarChart, AreaChart는 같은 데이터 모델을 공유합니다. `dataKey`는 각 행의 numeric field를 가리키고, `name`은 tooltip과 legend에 표시되는 사람이 읽는 이름입니다.
ts
export interface ChartDataPoint {
name: string;
[key: string]: string | number;
}
export interface ChartSeriesDef {
dataKey: string;
name: string;
color?: string;
type?: "line" | "bar" | "area";
stackId?: string;
}tsx
const data = [
{ name: "Enterprise", won: 42, lost: 8, pipeline: 19 },
{ name: "Mid-market", won: 31, lost: 11, pipeline: 27 },
{ name: "SMB", won: 24, lost: 14, pipeline: 32 },
];
const series = [
{ dataKey: "won", name: "Won", color: "hsl(142, 71%, 45%)" },
{ dataKey: "lost", name: "Lost", color: "hsl(349, 89%, 60%)" },
{ dataKey: "pipeline", name: "Pipeline", color: "hsl(221, 83%, 53%)" },
] satisfies ChartSeriesDef[];
<BarChart
data={data}
series={series}
stacked
showLegend
aria-label="세그먼트별 영업 상태"
/>;2. 특수 차트 모델
분석형 차트는 의미가 다르므로 공통 타입으로 억지로 맞추지 않습니다. 데이터가 이미 아래 shape 중 하나라면 해당 컴포넌트를 바로 선택합니다.
| Shape | Components | 설계 메모 |
|---|---|---|
| ChartDataPoint | LineChart, BarChart, AreaChart, ComparisonChart, TrendChart | name이 기본 x축이며 추가 key는 string 또는 number 값입니다. |
| PieChartDataPoint | PieChart | name, value, optional color로 구성비와 도넛 차트를 표현합니다. |
| ScatterSeriesDef / BubbleSeriesDef | ScatterChart, BubbleChart | series마다 numeric record 배열을 가지며 xKey, yKey, zKey/sizeKey를 지정합니다. |
| FunnelDataPoint | FunnelChart | name, value, optional color이며 최대값 대비 너비로 단계를 표현합니다. |
| RetentionRow | RetentionHeatmap | cohort와 (number | null)[] values를 쓰고 null은 빈 칸으로 표시합니다. |
| CalendarHeatmapDatum | CalendarHeatmap | YYYY-MM-DD date와 count를 날짜별 intensity로 매핑합니다. |
| SankeyNode / SankeyLink | SankeyChart | node id/label/color와 source/target/value link로 흐름을 계산합니다. |
| WaterfallDataPoint | WaterfallChart | name과 value delta를 누적 base와 delta bar로 변환합니다. |
3. Formatter와 설명 텍스트
opt-charts는 데이터 단위를 추측하지 않습니다. 통화, 퍼센트, retention period, cohort 이름은 소비자 앱에서 formatter로 주입합니다. 복잡한 차트는 시각적 label만으로 끝내지 말고 `aria-describedby`에 요약 문장을 연결합니다.
tsx
<LineChart
data={data}
series={series}
aria-label="월별 매출"
aria-describedby="revenue-chart-summary"
tooltipOptions={{
labelFormatter: (label) => `${label} 실적`,
valueFormatter: (value, name) =>
`${name}: ${Number(value).toLocaleString("ko-KR")}만원`,
}}
legendOptions={{
formatter: (value) => <span className="font-medium">{value}</span>,
}}
/>;
<p id="revenue-chart-summary" className="sr-only">
1월부터 4월까지 매출과 비용이 모두 증가했고, 4월 매출이 가장 높습니다.
</p>4. Empty state 정책
컴포넌트마다 empty 처리 방식이 다릅니다. 제품 화면에서는 차트 내부 구현에 기대지 말고 패널 레벨에서 loading, empty, error를 먼저 결정하는 것이 안정적입니다.
| 컴포넌트 | 현재 동작 | 권장 처리 |
|---|---|---|
| LineChart, BarChart, AreaChart, PieChart | 빈 ChartContainer 안에 빈 div를 렌더링합니다. | 패널 레벨에서 empty copy를 보여주는 것을 권장합니다. |
| FunnelChart, RetentionHeatmap | data.length === 0이면 null을 반환합니다. | 레이아웃 높이가 필요한 화면에서는 외부 empty state를 먼저 분기합니다. |
| ReportWidget | metric/gauge를 제외한 chart type에서 emptyChart label을 렌더링합니다. | builder나 report canvas에서는 labels.emptyChart를 제품 언어로 바꿉니다. |
tsx
function ChartPanel({ data, series }: ChartPanelProps) {
if (data.length === 0) {
return (
<div className="flex h-72 items-center justify-center rounded-lg border">
아직 표시할 데이터가 없습니다.
</div>
);
}
return <AreaChart data={data} series={series} height={288} />;
}5. 색상 할당
시리즈별 색상은 명시적 `color`가 최우선입니다. 없으면 `CHART_COLOR_PALETTE`의 5색 정적 팔레트가 순서대로 적용됩니다. 다수의 series나 segment가 필요한 분석 화면은 구현체에 있는 16색 확장 팔레트 기준으로 제품 색상표를 별도 고정하는 편이 좋습니다.