recharts 기반 막대·영역·원형 차트와 CSS BarList 조합 6종입니다.
recharts 가로 막대 — 항목이 6개 이상이라 라벨을 세로로 나열할 때 사용합니다.
const chartConfig = {
count: { label: "인원", color: "var(--chart-1)" },
} satisfies ChartConfig
<ChartContainer config={chartConfig} className="h-[180px] w-full">
<BarChart data={teamHeadcount} layout="vertical">
<CartesianGrid horizontal={false} />
<XAxis type="number" tickLine={false} axisLine={false} allowDecimals={false} />
<YAxis type="category" dataKey="name" tickLine={false} axisLine={false} width={56} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="count" radius={[0, 4, 4, 0]}>
{teamHeadcount.map((entry, i) => (
<Cell key={entry.name} fill={CHART_COLOR_VARS[i % 5]} />
))}
</Bar>
</BarChart>
</ChartContainer>recharts 세로 막대 — 월별/카테고리별 다중 시리즈 비교에 사용합니다.
const chartConfig = {
desktop: { label: "데스크톱", color: "var(--chart-1)" },
mobile: { label: "모바일", color: "var(--chart-2)" },
} satisfies ChartConfig
<ChartContainer config={chartConfig} className="h-[160px] w-full">
<BarChart data={monthlyVisits}>
<CartesianGrid vertical={false} />
<XAxis dataKey="month" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} allowDecimals={false} width={32} />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={[4, 4, 0, 0]} />
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={[4, 4, 0, 0]} />
</BarChart>
</ChartContainer>시계열 추이 — 일별/월별 누적·트렌드를 보여줄 때 사용합니다.
const chartConfig = {
count: { label: "가입", color: "var(--chart-1)" },
} satisfies ChartConfig
<ChartContainer config={chartConfig} className="h-[160px] w-full">
<AreaChart data={dailySignups}>
<defs>
<linearGradient id="fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-count)" stopOpacity={0.3} />
<stop offset="95%" stopColor="var(--color-count)" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} />
<XAxis dataKey="date" tickLine={false} axisLine={false} />
<YAxis tickLine={false} axisLine={false} allowDecimals={false} width={32} />
<ChartTooltip content={<ChartTooltipContent />} />
<Area type="monotone" dataKey="count" stroke="var(--color-count)" strokeWidth={1.5} fill="url(#fill)" />
</AreaChart>
</ChartContainer>도넛/원형 분포 — 항목이 5개 이하일 때 사용합니다.
const chartConfig = { value: { label: "건수" } } satisfies ChartConfig
<ChartContainer config={chartConfig} className="aspect-square h-[160px]">
<PieChart>
<Pie data={sentiment} dataKey="value" nameKey="name" outerRadius="80%" innerRadius="40%" paddingAngle={2}>
{sentiment.map((entry, i) => (
<Cell key={entry.name} fill={CHART_COLOR_VARS[i % 5]} />
))}
</Pie>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
</PieChart>
</ChartContainer>recharts 없이 CSS만으로 만드는 간단한 비율 막대 — 가벼운 랭킹/분포 표시에 사용합니다.
<div className="flex flex-col gap-1.5">
{regionData.map((item) => (
<div key={item.label} className="flex items-center gap-1">
<span className="min-w-[60px] max-w-[120px] truncate text-xs">{item.label}</span>
<div className="h-2.5 flex-1 rounded bg-secondary">
<div
className="h-2.5 rounded bg-primary transition-all duration-300"
style={{ width: `${(item.value / max) * 100}%` }}
/>
</div>
<span className="min-w-[32px] text-right text-xs tabular-nums">{item.value.toLocaleString()}</span>
</div>
))}
</div>여러 차트를 한 그리드에 섞어 요약 대시보드를 구성하는 예시입니다.
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-4">
<Card><CardContent>{/* 가로 막대 */}</CardContent></Card>
<Card><CardContent>{/* 도넛 */}</CardContent></Card>
<Card><CardContent>{/* 추이 */}</CardContent></Card>
<Card><CardContent>{/* 세로 막대 */}</CardContent></Card>
</div>외부 의존성 없는 순수 SVG 미니 차트 — 참조선·밴드·이벤트 마커를 겹쳐 카드/리스트 행에 인라인으로 씁니다.
상승 + 이벤트 마커
하락 + 예상 밴드
횡보 + 참조선
function Sparkline({ values, refLine, band, marker, height = 40 }: SparklineProps) {
// ...toX/toY 스케일 계산은 min/max + 10% 패딩
return (
<div className={cn("w-full", isUp ? "text-success" : "text-destructive")} style={{ height }}>
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" className="h-full w-full">
{band && <rect x={0} y={toY(band.max)} width={W} height={toY(band.min) - toY(band.max)} className="fill-primary/10" />}
{refLine && <line x1={0} y1={toY(refLine.value)} x2={W} y2={toY(refLine.value)} stroke="var(--muted-foreground)" strokeDasharray="3,3" />}
<polygon points={areaPoints} fill="currentColor" opacity={0.08} />
<polyline points={points} fill="none" stroke="currentColor" strokeWidth={1.5} />
<circle cx={lastX} cy={lastY} r={2.5} fill="currentColor" />
{marker && <circle cx={markerPoint.x} cy={markerPoint.y} r={2.5} fill="var(--chart-4)" />}
</svg>
</div>
)
}0% 기준선을 공유하는 시계열 — ResizeObserver로 컨테이너 폭을 측정해 반응형으로 그립니다.
function ReturnCurveChart({ points, height = 100 }: ReturnCurveChartProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [width, setWidth] = useState(320)
useEffect(() => {
const el = containerRef.current
if (!el || typeof ResizeObserver === "undefined") return
const observer = new ResizeObserver(([entry]) => setWidth(Math.round(entry.contentRect.width)))
observer.observe(el)
return () => observer.disconnect()
}, [])
// yMin/yMax는 0을 항상 포함하도록 계산해 0% 기준선이 항상 그려지게 한다.
return (
<div ref={containerRef} className={isUp ? "text-success" : "text-destructive"} style={{ height }}>
<svg width={W} height={H} viewBox={`0 0 ${W} ${H}`}>
<line x1={drawLeft} y1={zeroY} x2={drawRight} y2={zeroY} stroke="var(--muted-foreground)" strokeDasharray="4,3" />
<polygon points={areaPoints} fill="currentColor" opacity={0.08} />
<polyline points={linePoints} fill="none" stroke="currentColor" strokeWidth={2} />
</svg>
</div>
)
}GitHub contribution graph 스타일 — 주(週) 단위 열 × 요일 행 그리드에 primary 알파 농도로 활동량을 표현합니다.
function levelClass(n: number): string {
if (n <= 0) return "bg-muted"
if (n <= 2) return "bg-primary/25"
if (n <= 5) return "bg-primary/50"
if (n <= 9) return "bg-primary/75"
return "bg-primary"
}
<div className="flex gap-[3px] overflow-x-auto" role="img" aria-label="최근 N주 일별 활동 건수 히트맵">
{weeks.map((col) => (
<div key={col[0].week} className="flex flex-col gap-[3px]">
{col.map((cell) => (
<span key={cell.day} title={`${cell.count}건`} className={`h-2.5 w-2.5 rounded-[2px] ${levelClass(cell.count)}`} />
))}
</div>
))}
</div>여러 항목이 중앙 0축을 공유하는 막대 — 음수는 왼쪽, 양수는 오른쪽으로 뻗어 순매수/순매도 같은 대비를 보여줍니다.
const maxAbs = Math.max(...data.map((d) => Math.abs(d.value)), 1)
{data.map((item) => {
const width = item.value ? Math.max((Math.abs(item.value) / maxAbs) * 100, 2) : 0
return (
<div key={item.label} className="relative h-2.5">
<div className="absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-border" />
{item.value < 0 && <div className="absolute inset-y-0 rounded-l-sm bg-destructive" style={{ right: "50%", width: `${width / 2}%` }} />}
{item.value > 0 && <div className="absolute inset-y-0 rounded-r-sm bg-success" style={{ left: "50%", width: `${width / 2}%` }} />}
</div>
)
})}