196 lines
5.8 KiB
TypeScript
196 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 차트 서브타입 컴포넌트
|
|
*
|
|
* Recharts 기반 막대/원형/라인 차트
|
|
* 컨테이너 크기가 너무 작으면 "차트 표시 불가" 메시지
|
|
*/
|
|
|
|
import React from "react";
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
Tooltip,
|
|
Legend,
|
|
ResponsiveContainer,
|
|
CartesianGrid,
|
|
} from "recharts";
|
|
import type { DashboardItem } from "../../types";
|
|
import { TEXT_ALIGN_CLASSES } from "../../types";
|
|
import { abbreviateNumber } from "../utils/formula";
|
|
|
|
// ===== Props =====
|
|
|
|
export interface ChartItemProps {
|
|
item: DashboardItem;
|
|
/** 차트에 표시할 데이터 행 */
|
|
rows: Record<string, unknown>[];
|
|
/** 컨테이너 너비 (px) - 최소 크기 판단용 */
|
|
containerWidth: number;
|
|
}
|
|
|
|
// ===== 기본 색상 팔레트 =====
|
|
|
|
const DEFAULT_COLORS = [
|
|
"#6366f1", // indigo
|
|
"#8b5cf6", // violet
|
|
"#06b6d4", // cyan
|
|
"#10b981", // emerald
|
|
"#f59e0b", // amber
|
|
"#ef4444", // rose
|
|
"#ec4899", // pink
|
|
"#14b8a6", // teal
|
|
];
|
|
|
|
// ===== 최소 표시 크기 =====
|
|
|
|
const MIN_CHART_WIDTH = 120;
|
|
|
|
// ===== 메인 컴포넌트 =====
|
|
|
|
export function ChartItemComponent({
|
|
item,
|
|
rows,
|
|
containerWidth,
|
|
}: ChartItemProps) {
|
|
const { chartConfig, visibility, itemStyle } = item;
|
|
const chartType = chartConfig?.chartType ?? "bar";
|
|
const colors = chartConfig?.colors?.length
|
|
? chartConfig.colors
|
|
: DEFAULT_COLORS;
|
|
const xKey = chartConfig?.xAxisColumn ?? "name";
|
|
const yKey = chartConfig?.yAxisColumn ?? "value";
|
|
|
|
// 라벨 정렬만 사용자 설정
|
|
const labelAlignClass = TEXT_ALIGN_CLASSES[itemStyle?.labelAlign ?? "center"];
|
|
|
|
// 컨테이너가 너무 작으면 메시지 표시
|
|
if (containerWidth < MIN_CHART_WIDTH) {
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center p-1">
|
|
<span className="text-[10px] text-muted-foreground">차트</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 데이터 없음
|
|
if (!rows.length) {
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-center">
|
|
<span className="text-xs text-muted-foreground">데이터 없음</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// X축 라벨이 긴지 판정 (7자 이상이면 대각선)
|
|
const hasLongLabels = rows.some(
|
|
(r) => String(r[xKey] ?? "").length > 7
|
|
);
|
|
const xAxisTickProps = hasLongLabels
|
|
? { fontSize: 10, angle: -45, textAnchor: "end" as const }
|
|
: { fontSize: 10 };
|
|
// 긴 라벨이 있으면 하단 여백 확보
|
|
const chartMargin = hasLongLabels
|
|
? { top: 5, right: 10, bottom: 40, left: 10 }
|
|
: { top: 5, right: 10, bottom: 5, left: 10 };
|
|
|
|
return (
|
|
<div className="@container flex h-full w-full flex-col p-2">
|
|
{/* 라벨 - 사용자 정렬 적용 */}
|
|
{visibility.showLabel && (
|
|
<p className={`w-full mb-1 text-muted-foreground text-xs @[250px]:text-sm ${labelAlignClass}`}>
|
|
{item.label}
|
|
</p>
|
|
)}
|
|
|
|
{/* 차트 영역 */}
|
|
<div className="min-h-0 flex-1">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
{chartType === "bar" ? (
|
|
<BarChart data={rows as Record<string, string | number>[]} margin={chartMargin}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
|
<XAxis
|
|
dataKey={xKey}
|
|
tick={xAxisTickProps}
|
|
hide={containerWidth < 200}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 10 }}
|
|
hide={containerWidth < 200}
|
|
tickFormatter={(v: number) => abbreviateNumber(v)}
|
|
/>
|
|
<Tooltip />
|
|
<Bar dataKey={yKey} fill={colors[0]} radius={[2, 2, 0, 0]} />
|
|
</BarChart>
|
|
) : chartType === "line" ? (
|
|
<LineChart data={rows as Record<string, string | number>[]} margin={chartMargin}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
|
<XAxis
|
|
dataKey={xKey}
|
|
tick={xAxisTickProps}
|
|
hide={containerWidth < 200}
|
|
/>
|
|
<YAxis
|
|
tick={{ fontSize: 10 }}
|
|
hide={containerWidth < 200}
|
|
tickFormatter={(v: number) => abbreviateNumber(v)}
|
|
/>
|
|
<Tooltip />
|
|
<Line
|
|
type="monotone"
|
|
dataKey={yKey}
|
|
stroke={colors[0]}
|
|
strokeWidth={2}
|
|
dot={containerWidth > 250}
|
|
/>
|
|
</LineChart>
|
|
) : (
|
|
/* pie - 카테고리명 + 값 라벨 표시 */
|
|
<PieChart>
|
|
<Pie
|
|
data={rows as Record<string, string | number>[]}
|
|
dataKey={yKey}
|
|
nameKey={xKey}
|
|
cx="50%"
|
|
cy="50%"
|
|
outerRadius={containerWidth > 400 ? "70%" : "80%"}
|
|
label={
|
|
containerWidth > 250
|
|
? ({ name, value, percent }: { name: string; value: number; percent: number }) =>
|
|
`${name} ${abbreviateNumber(value)} (${(percent * 100).toFixed(0)}%)`
|
|
: false
|
|
}
|
|
labelLine={containerWidth > 250}
|
|
>
|
|
{rows.map((_, index) => (
|
|
<Cell
|
|
key={`cell-${index}`}
|
|
fill={colors[index % colors.length]}
|
|
/>
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
formatter={(value: number, name: string) => [abbreviateNumber(value), name]}
|
|
/>
|
|
{containerWidth > 300 && (
|
|
<Legend
|
|
wrapperStyle={{ fontSize: 11 }}
|
|
iconSize={10}
|
|
/>
|
|
)}
|
|
</PieChart>
|
|
)}
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|