차트 컴포넌트 사용법
- meta
- dev
- chart
사업 계획 문서에서 쓸 시각화 컴포넌트 모음. React나 차트 라이브러리 없이 순수 .astro + 인라인 SVG로 만들었다(GraphView.astro와 같은 방식) — 색은 전부 var(--bv-*)를 그대로 써서 라이트/다크 테마가 자동으로 따라온다.
색 규칙: 카테고리(어느 항목인지)는 --bv-series-1~8(고정 순서 8색, 색각이상·명도대역·대비 전부 검증 완료 — src/styles/global.css 참고), 증감 같은 상태는 --bv-emerald-300(좋음)/--bv-rose-400(나쁨)을 아이콘+텍스트와 함께 쓰고 색만으로 의미를 전달하지 않는다.
LineChart — 추이/성장 그래프
{ label, value }[] 배열만 넘기면 된다. 단일 시리즈라 범례 없이 제목/문맥으로 식별한다.
import LineChart from '.../components/charts/LineChart.astro';
<LineChart data={data} ariaLabel="월별 매출 추이" unit="만원" />
BarChart — 항목 비교 (경쟁사 등)
이름이 곧 식별자인 카테고리 비교라, 막대마다 고정 순서 색을 하나씩 배정하고 끝에 값을 직접 라벨링한다(범례 없이도 읽힘).
import BarChart from '.../components/charts/BarChart.astro';
<BarChart data={data} ariaLabel="경쟁사 월 매출 비교" unit="만원" />
DonutChart — 부분-전체 구조 (비용/수익 등)
세그먼트 2개 이상이라 범례가 항상 따라온다(라벨+값+퍼센트) — 도넛 위 색만으로는 구분을 강제하지 않는다.
- 임대료350만원 · 35%
- 재료비280만원 · 28%
- 인건비220만원 · 22%
- 공과금60만원 · 6%
- 마케팅50만원 · 5%
- 기타40만원 · 4%
import DonutChart from '.../components/charts/DonutChart.astro';
<DonutChart data={data} ariaLabel="월 비용 구조" unit="만원" />
KpiCard — 숫자 강조
차트가 필요 없을 만큼 단순한 “숫자 하나”에 쓴다. 증감은 화살표 아이콘 + 텍스트로 같이 보여줘서 색약이어도 좋다/나쁘다가 전달된다.
이번 달 매출
2,300만원
+12.4%전월 대비
객단가
8,500원
-3.1%전월 대비
누적 방문객
18,400명
import KpiCard from '.../components/charts/KpiCard.astro';
<KpiCard label="이번 달 매출" value={2300} unit="만원" delta={12.4} deltaLabel="전월 대비" />
Timeline — 마일스톤/로드맵
완료 여부는 색이 아니라 “채워진 점 vs 빈 점”(모양)으로 인코딩한다 — 흑백 인쇄나 색각이상에서도 구분된다.
- 2026-01시장 조사 및 경쟁사 분석 완료완료
- 2026-03입지 후보 3곳 선정완료
- 2026-05사업자 등록 및 임대차 계약예정
- 2026-07인테리어 및 장비 세팅예정
- 2026-09정식 오픈예정
import Timeline from '.../components/charts/Timeline.astro';
<Timeline items={[{ date: '2026-01', label: '...', done: true }]} ariaLabel="..." />
Sparkline — 표/카드용 인라인 미니 추이
축·격자 없이 선 하나 + 마지막 점만 찍는 극소 버전. 표 셀이나 KpiCard 옆에 자연스럽게 얹으려고 만들었다. values: number[]만 넘기면 된다(라벨은 툴팁용, 선택).
최근 매출 추이
import Sparkline from '.../components/charts/Sparkline.astro';
<Sparkline values={[820, 940, 1100, ...]} unit="만원" ariaLabel="최근 매출 추이" />
GroupedBarChart — 항목 × 지표 비교
BarChart는 “항목”별 색이지만, 이건 “지표”(맛/가격/접근성 등)별로 색을 고정하고 항목마다 그 지표들을 나란히 놓는다 — 시리즈가 2개 이상이라 범례가 항상 따라온다.
- 맛
- 가격경쟁력
- 접근성
import GroupedBarChart from '.../components/charts/GroupedBarChart.astro';
<GroupedBarChart
categories={['경쟁사 A', '경쟁사 B']}
series={[{ label: '맛', values: [8, 7] }, { label: '가격', values: [5, 8] }]}
ariaLabel="경쟁사 항목별 점수 비교"
/>
ProgressGauge — 목표 달성률
DonutChart와 같은 원 스트로크 기법이지만 세그먼트가 하나뿐이다. 항목 정체성이 아니라 “크기(달성률)“를 나타내므로 --bv-series-* 대신 브랜드 액센트(violet) 하나만 쓴다.
import ProgressGauge from '.../components/charts/ProgressGauge.astro';
<ProgressGauge value={1780} max={2000} label="이번 달 매출 목표" unit="만원" />
ScatterPlot — 포지셔닝 맵 (입지/경쟁사)
x/y 두 지표로 항목을 흩뿌려 위치를 비교한다. 점의 색은 개별 항목이 아니라 “그룹”(후보지/경쟁사)을 나타낸다 — 산점도는 어느 두 점이든 이웃할 수 있어서(all-pairs) 그룹은 2~3개까지만 안전하다. size를 주면 반지름에 반영된다(예: 평수).
- 후보지
- 경쟁사
import ScatterPlot from '.../components/charts/ScatterPlot.astro';
<ScatterPlot
data={[{ x: 42, y: 3200, label: '후보지 A', group: '후보지', size: 28 }]}
xLabel="임대료" yLabel="일 유동인구" ariaLabel="포지셔닝 맵"
/>
RadarChart — 다축 비교 (경쟁사 5각 평가 등)
여러 축을 한 폴리곤으로 겹쳐서 강점/약점을 한눈에 보여준다. 폴리곤도 산점도처럼 all-pairs 상황이라 시리즈는 3~4개까지만 권장(그 이상은 겹쳐서 어차피 안 읽힘).
- 베소 프랑세
- 와플대학
- (우리) 후보지 A
import RadarChart from '.../components/charts/RadarChart.astro';
<RadarChart
axes={['맛', '가격경쟁력', '접근성']}
series={[{ label: '경쟁사 A', values: [8, 5, 9] }]}
ariaLabel="경쟁사 다축 비교"
/>
QuadrantMatrix — 2x2 사분면 (우선순위 매트릭스 등)
ScatterPlot과 계산은 같지만 중앙에 십자선이 있고 항목 수가 적어서(보통 10개 미만) 점마다 이름을 직접 라벨링한다 — 여기서는 항목 정체성보다 “어느 사분면에 있는가”가 중요해서 점 색은 그룹 구분 없이 액센트 하나만 쓴다.
import QuadrantMatrix from '.../components/charts/QuadrantMatrix.astro';
<QuadrantMatrix
data={[{ x: 3, y: 8, label: 'SNS 마케팅 강화' }]}
xLabel="투입 노력" yLabel="기대 효과" midX={5} midY={5}
quadrantLabels={['바로 실행', '계획 필요', '여유 있을 때', '보류']}
ariaLabel="실행 우선순위 매트릭스"
/>
SWOT처럼 좌표가 아니라 텍스트 블록 4개가 필요하면 차트 컴포넌트가 아니라 그냥 .bv-card 4개를 2x2 CSS 그리드로 배치하면 된다(사실 이건 차트가 아니라 레이아웃이라 전용 컴포넌트를 안 만들었다):
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.75rem;">
<div class="bv-card"><strong>강점(S)</strong><ul><li>...</li></ul></div>
<div class="bv-card"><strong>약점(W)</strong><ul><li>...</li></ul></div>
<div class="bv-card"><strong>기회(O)</strong><ul><li>...</li></ul></div>
<div class="bv-card"><strong>위협(T)</strong><ul><li>...</li></ul></div>
</div>
StackedBarChart — 구성 비율의 시간에 따른 변화
DonutChart가 한 시점의 스냅샷이라면, 이건 그 구성이 여러 카테고리(월 등)에 걸쳐 어떻게 바뀌는지 보여준다. 구성 항목이 시리즈 정체성이라 항상 같은 색 + 범례.
- 임대료
- 재료비
- 인건비
- 기타
import StackedBarChart from '.../components/charts/StackedBarChart.astro';
<StackedBarChart
data={[{ label: '1월', stacks: [{ label: '임대료', value: 350 }, { label: '재료비', value: 180 }] }]}
ariaLabel="월별 비용 구조 변화" unit="만원"
/>
FunnelChart — 고객 전환 퍼널
순서를 바꾸면 의미가 달라지는 ordinal 데이터라 카테고리 8색이 아니라 한 색조의 명도(불투명도) 단계를 쓴다. 전환율은 이전 단계 값 대비 자동 계산 — 따로 안 넣어도 된다.
import FunnelChart from '.../components/charts/FunnelChart.astro';
<FunnelChart data={[{ label: '광고 노출', value: 12000 }, { label: '매장 방문', value: 640 }]} ariaLabel="고객 전환 퍼널" />
BreakEvenChart — 손익분기점
비용/매출 두 선을 겹쳐 그리고 교차점을 자동으로 계산해 점선으로 표시한다(“비용” vs “매출”은 상태가 아니라 서로 다른 두 측정값이라 --bv-series-1/2를 쓴다).
- 비용 (만원)
- 매출 (만원)
import BreakEvenChart from '.../components/charts/BreakEvenChart.astro';
<BreakEvenChart data={[{ label: '1월', cost: 740, revenue: 400 }]} ariaLabel="손익분기점" unit="만원" />
HeatmapGrid — 요일×시간대 등 행렬 강도 (viz/)
값이 “크기”라 카테고리 8색이 아니라 한 액센트 색의 명도(불투명도) 단계로 표시한다. 리마 상권은 시간대 장사가 중요해서 요일×시간대 매출 밀도를 한눈에 보기 좋다.
import HeatmapGrid from '.../components/viz/HeatmapGrid.astro';
<HeatmapGrid rows={['월', '화']} cols={['10시', '12시']} values={[[10, 35], [8, 32]]} ariaLabel="요일별 시간대 매출 히트맵" />
Table — 정렬 + 합계 행 (viz/, 기존 컴포넌트 확장)
기존 Table에 sortable(헤더 클릭 정렬, 최소 클라이언트 JS), showTotal(숫자 컬럼 합계 행), caption(스크린리더용 표 설명)을 추가했다.
| 매장 | 월매출_만원 | 객단가_원 | 좌석수 |
|---|---|---|---|
| 베소 프랑세 | 4200 | 9800 | 32 |
| 와플대학 | 3100 | 8500 | 24 |
| 벨지안와플하우스 | 2600 | 9200 | 18 |
| 리에주웨이브 | 2050 | 7800 | 20 |
| 합계 | 11,950 | 35,300 | 94 |
import Table from '.../components/viz/Table.astro';
<Table rows={data} sortable showTotal caption="경쟁사 비교표" />
CanvasGrid — 린 캔버스 9블록 (viz/)
사실 차트가 아니라 CSS 그리드 레이아웃이라 색 규칙이 필요 없다 — 9개 블록 다 같은 카드 톤이고 위치와 제목으로 구분한다.
문제 (Problem)
- 리마에 정통 벨기에식 와플 전문점이 적음
- 기존 카페의 와플은 디저트 부가 메뉴 수준
솔루션 (Solution)
- 현장 조리 리에주 와플 + 커스텀 토핑
- 테이크아웃 특화 포장
핵심 지표 (Key Metrics)
- 일 방문객 수
- 재구매율
- 객단가
고유 가치 제안 (UVP)
- 미라플로레스에서 가장 정통에 가까운 리에주 와플
경쟁 우위 (Unfair Advantage)
- 벨기에 현지 레시피 노하우
채널 (Channels)
- SNS(인스타그램)
- 배달앱
- 오프라인 매장
고객군 (Customer Segments)
- 20~30대 여성 직장인
- 관광객
비용 구조 (Cost Structure)
- 임대료
- 재료비(원당 반죽, 초콜릿)
- 인건비
수익원 (Revenue Streams)
- 매장 판매
- 배달앱 판매
- 시즌 한정 메뉴
import CanvasGrid from '.../components/viz/CanvasGrid.astro';
<CanvasGrid problem={['...']} solution={['...']} uvp={['...']} customerSegments={['...']} costStructure={['...']} revenueStreams={['...']} />
PersonaCard — 고객 페르소나 (viz/)
사진 없이도 바로 쓸 수 있게 이니셜 원으로 아바타를 대신한다. Pain/Gain은 좋다/나쁘다 상태가 아니라 서로 다른 두 목록이라 중립 톤으로 나란히 놓는다.
마리아(29세)
미라플로레스 거주 마케팅 직장인
"퇴근길에 줄 안 서고 빨리 달달한 걸 먹고 싶어요."
Pain
- 기존 와플집은 웨이팅이 김
- 테이크아웃 포장이 부실해서 식으면 눅눅해짐
Gain
- SNS에 올리기 좋은 비주얼
- 주문 후 5분 내 픽업
import PersonaCard from '.../components/viz/PersonaCard.astro';
<PersonaCard name="마리아" role="직장인" age={29} pains={['...']} gains={['...']} />
PricingTable — 메뉴/요금제 가격·원가·마진 비교 (viz/)
마진율은 판매가/원가에서 자동 계산한다(따로 안 넣음 — 입력받으면 원본과 어긋날 위험만 생김). 마진율이 낮으면(기본 30% 미만) 색만이 아니라 경고 아이콘과 함께 표시한다.
| 메뉴 | 판매가 | 원가 | 마진액 | 마진율 |
|---|---|---|---|---|
| 클래식 리에주 와플 | 18솔 | 6솔 | 12솔 | 66.7% |
| 누텔라 바나나 와플 | 22솔 | 9솔 | 13솔 | 59.1% |
| 프리미엄 베리 치즈 와플 | 28솔 | 21솔 | 7솔 | 25% |
import PricingTable from '.../components/viz/PricingTable.astro';
<PricingTable items={[{ name: '클래식 리에주 와플', price: 18, cost: 6 }]} unit="솔" />
WaterfallChart — 원가/손익 누적 빌드업
시작값에서 증감 단계를 거쳐 최종값까지 계단식으로 쌓는다. “증가=나쁨”인 원가 누적처럼 방향의 좋고 나쁨이 맥락마다 달라서 positiveIsGood으로 뒤집을 수 있다(기본은 증가=emerald/감소=rose, 아래 예시는 비용 누적이라 뒤집었다).
import WaterfallChart from '.../components/charts/WaterfallChart.astro';
<WaterfallChart
steps={[{ label: '재료비', value: 1.5, type: 'increase' }, { label: '원가 합계', value: 4.4, type: 'total' }]}
positiveIsGood={false} unit="솔" ariaLabel="원가 누적"
/>
WeightedScorecard — 후보지 가중치 평가표 (viz/)
LocationMap으로 지도에서 추린 후보지를 숫자로 최종 결판낼 때 쓴다. 열(후보지)은 카테고리 색, 셀 점수는 그 색의 명도로 “크기”까지 겹쳐 보여준다(숫자는 항상 텍스트로 병기).
| 평가 기준 | 후보지 A (말레콘) | 후보지 B (미라플로레스 중심가) | 후보지 C (바랑코) |
|---|---|---|---|
| 임대료 부담 (30%) | 3 / 5 | 2 / 5 | 4 / 5 |
| 유동인구 (30%) | 5 / 5 | 4 / 5 | 3 / 5 |
| 접근성 (20%) | 4 / 5 | 5 / 5 | 3 / 5 |
| 경쟁 밀도(낮을수록 좋음) (20%) | 3 / 5 | 2 / 5 | 4 / 5 |
| 가중 합계 | 3.8 | 3.2 | 3.5 |
import WeightedScorecard from '.../components/viz/WeightedScorecard.astro';
<WeightedScorecard
criteria={[{ label: '임대료 부담', weight: 30 }, { label: '유동인구', weight: 30 }]}
sites={[{ label: '후보지 A', scores: [3, 5] }, { label: '후보지 B', scores: [2, 4] }]}
/>
BulletChart — KPI 목표 대비 실적 한 줄 요약
배경의 3단계 구간(미달/양호/우수)은 순서가 있는 데이터라 카테고리색이 아니라 중립색 하나의 명도 단계를 쓰고, 실제값 막대만 액센트로 강조한다. 여러 KPI를 세로로 나열하기 좋다.
import BulletChart from '.../components/charts/BulletChart.astro';
<BulletChart label="일 매출" actual={78} target={90} unit="만원" ariaLabel="일 매출 목표 대비" />
TornadoChart — 민감도 분석
가정(임대료, 객단가 등)이 ±% 변할 때 결과가 얼마나 흔들리는지 보여준다. 여기서는 방향이 실제로 좋다/나쁘다를 의미해서(기준보다 낮아지면 나쁜 시나리오) status 색(emerald/rose)을 쓴다. 영향이 큰 요인이 자동으로 위로 정렬된다.
기준값: 460만원
import TornadoChart from '.../components/charts/TornadoChart.astro';
<TornadoChart baseValue={460} factors={[{ label: '임대료 ±10%', low: 380, high: 540 }]} unit="만원" ariaLabel="손익 민감도" />
SlopeChart — 두 시점 간 순위/값 변화
항목 식별은 카테고리 색, 상승/하락 방향은 색이 아니라 오른쪽 끝 삼각형으로 표시한다(색으로 방향까지 이중 인코딩하면 항목 색과 헷갈릴 수 있어서 분리).
import SlopeChart from '.../components/charts/SlopeChart.astro';
<SlopeChart leftLabel="2025 Q4" rightLabel="2026 Q2" items={[{ id: 'a', label: '경쟁사 A', left: 8200, right: 9500 }]} ariaLabel="변화 비교" />
GanttChart — 기간이 있는 작업 일정
Timeline은 점(마일스톤)이지만 이건 시작~종료 기간이 있는 막대다. “YYYY-MM” 문자열을 연속 월 인덱스로 바꿔 선형 축에 놓는다. 완료 여부는 색이 아니라 진하기(불투명도) + 점선 테두리로 구분한다.
- 조사
- 허가
- 공사
- 마케팅
import GanttChart from '.../components/charts/GanttChart.astro';
<GanttChart items={[{ id: 'a', label: '상권 조사', start: '2026-01', end: '2026-02', group: '조사', done: true }]} ariaLabel="오픈 로드맵" />
HistogramChart — 연속값의 분포
BarChart는 이름 있는 카테고리 비교지만, 이건 순서가 있는 “구간”의 분포라 카테고리 8색 대신 한 hue의 명도 단계로 구간 순서만 은은하게 표시한다(막대 높이=빈도는 그대로 크기 인코딩).
import HistogramChart from '.../components/charts/HistogramChart.astro';
<HistogramChart bins={[{ rangeLabel: '6-8솔', count: 4 }, { rangeLabel: '8-10솔', count: 11 }]} unit="곳" ariaLabel="가격대 분포" />
RangeBarChart — 구간(영업시간 등) 비교
GanttChart와 계산은 같지만 날짜가 아니라 숫자 축(0~24시 등)이다. 경쟁사들의 영업시간을 겹쳐서 “22시 이후 여는 곳이 우리밖에 없다” 같은 걸 바로 보여준다.
- 우리
- 경쟁사
import RangeBarChart from '.../components/charts/RangeBarChart.astro';
<RangeBarChart items={[{ label: '경쟁사 A', start: 9, end: 21, group: '경쟁사' }]} axisUnit="시" ariaLabel="영업시간 비교" />
PopulationPyramidChart — 배후 인구 피라미드
성별은 고정 카테고리 2개라 분홍/파랑 같은 고정관념 색 대신 --bv-series-1(남)/--bv-series-2(여)를 그대로 쓴다. CityMap의 POI 밀집도와 같이 쓰면 상권 분석 설득력이 올라간다.
- 남성
- 여성
import PopulationPyramidChart from '.../components/charts/PopulationPyramidChart.astro';
<PopulationPyramidChart groups={[{ ageLabel: '20-29', male: 8.9, female: 10.6 }]} unit="천명" ariaLabel="배후 인구 피라미드" />
실제 데이터로 바꾸는 법
전부 Props로 데이터를 받는 구조라 실제 데이터가 생기면 src/data/에 JSON으로 넣고 getCollection('data')로 읽어서 각 컴포넌트가 요구하는 배열 모양({ label, value }[] 또는 { date, label, done }[])으로만 맞춰 넘기면 된다. 지금 쓰는 src/data/mock/*.ts는 스타일 확인용이라, 실제 데이터로 바꾸고 나면 지워도 된다.