ERP-node/frontend/lib/registry/pop-components/pop-dashboard/items/ChartItem.tsx

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>
);
}