ERP-node/frontend/lib/registry/components/pivot-grid/components/PivotChart.tsx

387 lines
9.6 KiB
TypeScript

"use client";
/**
* PivotChart 컴포넌트
* 피벗 데이터를 차트로 시각화
*/
import React, { useMemo } from "react";
import { cn } from "@/lib/utils";
import { PivotResult, PivotChartConfig, PivotFieldConfig } from "../types";
import { pathToKey } from "../utils/pivotEngine";
import {
BarChart,
Bar,
LineChart,
Line,
AreaChart,
Area,
PieChart,
Pie,
Cell,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
// ==================== 타입 ====================
interface PivotChartProps {
pivotResult: PivotResult;
config: PivotChartConfig;
dataFields: PivotFieldConfig[];
className?: string;
}
// ==================== 색상 ====================
const COLORS = [
"#4472C4", // 파랑
"#ED7D31", // 주황
"#A5A5A5", // 회색
"#FFC000", // 노랑
"#5B9BD5", // 하늘
"#70AD47", // 초록
"#264478", // 진한 파랑
"#9E480E", // 진한 주황
"#636363", // 진한 회색
"#997300", // 진한 노랑
];
// ==================== 데이터 변환 ====================
function transformDataForChart(
pivotResult: PivotResult,
dataFields: PivotFieldConfig[]
): any[] {
const { flatRows, flatColumns, dataMatrix, grandTotals } = pivotResult;
// 행 기준 차트 데이터 생성
return flatRows.map((row) => {
const dataPoint: any = {
name: row.caption,
path: row.path,
};
// 각 열에 대한 데이터 추가
flatColumns.forEach((col) => {
const cellKey = `${pathToKey(row.path)}|||${pathToKey(col.path)}`;
const values = dataMatrix.get(cellKey);
if (values && values.length > 0) {
const columnName = col.caption || "전체";
dataPoint[columnName] = values[0].value;
}
});
// 총계 추가
const rowTotal = grandTotals.row.get(pathToKey(row.path));
if (rowTotal && rowTotal.length > 0) {
dataPoint["총계"] = rowTotal[0].value;
}
return dataPoint;
});
}
function transformDataForPie(
pivotResult: PivotResult,
dataFields: PivotFieldConfig[]
): any[] {
const { flatRows, grandTotals } = pivotResult;
return flatRows.map((row, idx) => {
const rowTotal = grandTotals.row.get(pathToKey(row.path));
return {
name: row.caption,
value: rowTotal?.[0]?.value || 0,
color: COLORS[idx % COLORS.length],
};
});
}
// ==================== 차트 컴포넌트 ====================
const CustomTooltip: React.FC<any> = ({ active, payload, label }) => {
if (!active || !payload || !payload.length) return null;
return (
<div className="bg-background border border-border rounded-lg shadow-lg p-2">
<p className="text-sm font-medium mb-1">{label}</p>
{payload.map((entry: any, idx: number) => (
<p key={idx} className="text-xs" style={{ color: entry.color }}>
{entry.name}: {entry.value?.toLocaleString()}
</p>
))}
</div>
);
};
// 막대 차트
const PivotBarChart: React.FC<{
data: any[];
columns: string[];
height: number;
showLegend: boolean;
stacked?: boolean;
}> = ({ data, columns, height, showLegend, stacked }) => {
return (
<ResponsiveContainer width="100%" height={height}>
<BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="name"
tick={{ fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: "#e5e5e5" }}
/>
<YAxis
tick={{ fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: "#e5e5e5" }}
tickFormatter={(value) => value.toLocaleString()}
/>
<Tooltip content={<CustomTooltip />} />
{showLegend && (
<Legend
wrapperStyle={{ fontSize: 11, paddingTop: 10 }}
iconType="square"
/>
)}
{columns.map((col, idx) => (
<Bar
key={col}
dataKey={col}
fill={COLORS[idx % COLORS.length]}
stackId={stacked ? "stack" : undefined}
radius={stacked ? 0 : [4, 4, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
};
// 선 차트
const PivotLineChart: React.FC<{
data: any[];
columns: string[];
height: number;
showLegend: boolean;
}> = ({ data, columns, height, showLegend }) => {
return (
<ResponsiveContainer width="100%" height={height}>
<LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="name"
tick={{ fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: "#e5e5e5" }}
/>
<YAxis
tick={{ fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: "#e5e5e5" }}
tickFormatter={(value) => value.toLocaleString()}
/>
<Tooltip content={<CustomTooltip />} />
{showLegend && (
<Legend
wrapperStyle={{ fontSize: 11, paddingTop: 10 }}
iconType="line"
/>
)}
{columns.map((col, idx) => (
<Line
key={col}
type="monotone"
dataKey={col}
stroke={COLORS[idx % COLORS.length]}
strokeWidth={2}
dot={{ r: 4, fill: COLORS[idx % COLORS.length] }}
activeDot={{ r: 6 }}
/>
))}
</LineChart>
</ResponsiveContainer>
);
};
// 영역 차트
const PivotAreaChart: React.FC<{
data: any[];
columns: string[];
height: number;
showLegend: boolean;
}> = ({ data, columns, height, showLegend }) => {
return (
<ResponsiveContainer width="100%" height={height}>
<AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
<XAxis
dataKey="name"
tick={{ fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: "#e5e5e5" }}
/>
<YAxis
tick={{ fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: "#e5e5e5" }}
tickFormatter={(value) => value.toLocaleString()}
/>
<Tooltip content={<CustomTooltip />} />
{showLegend && (
<Legend
wrapperStyle={{ fontSize: 11, paddingTop: 10 }}
iconType="square"
/>
)}
{columns.map((col, idx) => (
<Area
key={col}
type="monotone"
dataKey={col}
fill={COLORS[idx % COLORS.length]}
stroke={COLORS[idx % COLORS.length]}
fillOpacity={0.3}
/>
))}
</AreaChart>
</ResponsiveContainer>
);
};
// 파이 차트
const PivotPieChart: React.FC<{
data: any[];
height: number;
showLegend: boolean;
}> = ({ data, height, showLegend }) => {
return (
<ResponsiveContainer width="100%" height={height}>
<PieChart>
<Pie
data={data}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={height / 3}
label={({ name, percent }) =>
`${name} (${(percent * 100).toFixed(1)}%)`
}
labelLine
>
{data.map((entry, idx) => (
<Cell key={idx} fill={entry.color || COLORS[idx % COLORS.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
{showLegend && (
<Legend
wrapperStyle={{ fontSize: 11, paddingTop: 10 }}
iconType="circle"
/>
)}
</PieChart>
</ResponsiveContainer>
);
};
// ==================== 메인 컴포넌트 ====================
export const PivotChart: React.FC<PivotChartProps> = ({
pivotResult,
config,
dataFields,
className,
}) => {
// 차트 데이터 변환
const chartData = useMemo(() => {
if (config.type === "pie") {
return transformDataForPie(pivotResult, dataFields);
}
return transformDataForChart(pivotResult, dataFields);
}, [pivotResult, dataFields, config.type]);
// 열 이름 목록 (파이 차트 제외)
const columns = useMemo(() => {
if (config.type === "pie" || chartData.length === 0) return [];
const firstItem = chartData[0];
return Object.keys(firstItem).filter(
(key) => key !== "name" && key !== "path"
);
}, [chartData, config.type]);
const height = config.height || 300;
const showLegend = config.showLegend !== false;
if (!config.enabled) {
return null;
}
return (
<div
className={cn(
"border-t border-border bg-background p-4",
className
)}
>
{/* 차트 렌더링 */}
{config.type === "bar" && (
<PivotBarChart
data={chartData}
columns={columns}
height={height}
showLegend={showLegend}
/>
)}
{config.type === "stackedBar" && (
<PivotBarChart
data={chartData}
columns={columns}
height={height}
showLegend={showLegend}
stacked
/>
)}
{config.type === "line" && (
<PivotLineChart
data={chartData}
columns={columns}
height={height}
showLegend={showLegend}
/>
)}
{config.type === "area" && (
<PivotAreaChart
data={chartData}
columns={columns}
height={height}
showLegend={showLegend}
/>
)}
{config.type === "pie" && (
<PivotPieChart
data={chartData}
height={height}
showLegend={showLegend}
/>
)}
</div>
);
};
export default PivotChart;