데이터 시각화

시각화 원칙

ON:GI 차트는 D3.js로 스케일 · 축 · 도형을 계산하고 React SVG로 렌더링합니다. 사장님과 운영자가 ‘오늘 장사가 어땠는지’를 3초 안에 파악하도록, 장식 없이 수치와 추세에 집중합니다.

엔진
D3.js v7 (scale · shape · array · timer)
렌더링
React SVG (선언형)
차트
8종
접근성
aria 요약 · 키보드 탐색

Line · Area

일별 결제금액 · 이번 달 vs 지난달

자세히
  • 이번 달
  • 지난달

Bar · Stacked

요일별 채널 매출

자세히
  • POS
  • 키오스크
  • QR

Donut

결제 수단 비중

자세히
총 결제액9810만원

Combo

월별 결제금액 + 건수

자세히
  • 결제금액
  • 결제건수

Ranking

메뉴 매출 TOP 5

자세히

Heatmap

요일 × 시간대 주문량

자세히
적음
많음

Gauge · KPI

목표 달성률 · 스파크라인

자세히
목표 달성률0%

9월 목표 25건 중 19.5건

신규 가맹점
19곳
+18.2%전주 대비

5가지 원칙

원칙규칙
1. 결론 먼저차트 위에 핵심 수치(KPI)와 증감을 먼저 보여주고, 차트는 근거로 둡니다.
2. 잉크 최소화축 선 · 세로 그리드 · 3D · 그림자 없음. 가로 그리드만 1px --viz-grid.
3. 강조는 하나이번 기간은 --viz-1, 비교 기간은 --viz-7 점선. 막대 강조는 마지막(오늘) 하나만.
4. 숫자는 사람 말로축은 만 · 억 축약(1.2억), 툴팁은 정확한 원 단위(128,400원).
5. 누구나 읽게색 + 형태(점선 · 라벨)로 구분, role=img 요약, 방향키 탐색, 동작 줄이기 대응.

팔레트

Categorical — 계열 구분

순서대로 사용합니다. 계열이 6개를 넘으면 상위 5개 + ‘기타(--viz-7)’로 묶습니다.

viz-17C3AED
viz-23182F6
viz-312B886
viz-4FFB020
viz-5F0506E
viz-622B8CF
viz-78B95A1
토큰라이트용도
--viz-1#7C3AED기준 · 이번 기간 · 주요 지표
--viz-2#3182F6비교 지표 · 건수
--viz-3#12B886세 번째 계열
--viz-4#FFB020네 번째 계열
--viz-5#F0506E다섯 번째 계열
--viz-6#22B8CF여섯 번째 계열
--viz-7#8B95A1기타 · 이전 기간 · 비교 기준

Sequential — 크기 · 밀도

히트맵 · 코로플레스 등 연속 값. 7단계 퍼플, 다크 모드에서는 밝을수록 큰 값으로 반전됩니다.

0
1
2
3
4
5
6

차트 선택 가이드

질문차트예시
시간에 따라 어떻게 변했나?Line · Area일별 결제금액, 이번 달 vs 지난달
항목끼리 비교하면?Bar (세로)요일별 매출, 채널별 매출
누가 가장 많나?Ranking Bar (가로)메뉴 TOP 5, 영업자 순위
금액과 건수를 함께?Combo (이중 축)월별 결제금액 + 결제건수
전체 중 비중은?Donut (5개 이하)결제 수단 비중
언제 몰리나?Heatmap요일 × 시간대 주문량
목표까지 얼마나?Gauge · Progress영업 목표 달성률
한눈에 추세만SparklineKPI 카드 내부

숫자 포맷

위치함수결과
Y축 · KPI 요약formatCompactKRW(128400000)1.3억
툴팁 · 테이블formatKRW(128400)128,400원
증감formatDelta(12.43)+12.4%
비율formatPercent(48.2)48.2%
X축 날짜timeFormat("%-m.%-d")9.28

구현 구조

D3는 계산만, DOM은 React가 소유합니다. d3.select로 DOM을 직접 조작하지 않아 React 상태와 충돌하지 않습니다.

components/charts/line-chart.tsx (요약)
"use client";
import { scaleTime, scaleLinear, line, curveMonotoneX, bisector } from "d3";

export function LineChart({ series, height = 280 }: Props) {
  const [ref, width] = useChartWidth();        // ResizeObserver
  const t = useEnterProgress(900);             // d3.timer + easeCubicOut (0 → 1)

  const x = scaleTime().domain(extent).range([0, innerW]);
  const y = scaleLinear().domain([0, max]).nice(4).range([innerH, 0]);
  const path = line<Point>().x(d => x(d.x)).y(d => y(d.y)).curve(curveMonotoneX);

  return (
    <svg role="img" aria-label="…" tabIndex={0} onKeyDown={arrowKeyNav}>
      {y.ticks(4).map(tick => <GridLine key={tick} y={y(tick)} />)}
      <clipPath id="reveal"><rect width={innerW * t} /></clipPath>
      <path d={path(values)} clipPath="url(#reveal)" stroke="var(--viz-1)" />
    </svg>
  );
}

서버 컴포넌트에서 데이터 넘기기

차트는 클라이언트 컴포넌트입니다. 서버에서 집계한 값을 { x: Date, y: number }[] 형태로 넘기면 되고, 포맷 함수처럼 함수 props가 필요하면 클라이언트 래퍼 안에서 지정하세요.

사용 규칙

이렇게 하세요

  • y축은 0부터 (막대는 필수, 선은 권장)
  • 비교 기간은 회색 점선
  • 데이터가 없으면 차트 대신 EmptyState

이렇게 하지 마세요

  • 파이 · 도넛에 6개 이상 조각
  • 이중 축에 같은 단위 사용
  • 무지개 팔레트 · 그라데이션 막대