"use client"; import React, { useEffect, useRef } from "react"; import * as d3 from "d3"; import { ChartConfig, ChartData } from "../types"; interface PieChartProps { data: ChartData; config: ChartConfig; width?: number; height?: number; isDonut?: boolean; } /** * D3 원형 차트 / 도넛 차트 컴포넌트 */ export function PieChart({ data, config, width = 500, height = 500, isDonut = false }: PieChartProps) { const svgRef = useRef(null); useEffect(() => { if (!svgRef.current || !data.labels.length || !data.datasets.length) return; const svg = d3.select(svgRef.current); svg.selectAll("*").remove(); // 범례를 위한 여백 확보 (아래 80px) const legendHeight = config.showLegend !== false ? 80 : 0; const margin = { top: 20, right: 20, bottom: 20 + legendHeight, left: 20 }; const chartWidth = width - margin.left - margin.right; const chartHeight = height - margin.top - margin.bottom - legendHeight; const radius = Math.min(chartWidth, chartHeight) / 2; // 차트를 위쪽에 배치 (범례 공간 확보) const centerX = width / 2; const centerY = margin.top + radius + 20; const g = svg.append("g").attr("transform", `translate(${centerX},${centerY})`); // 색상 팔레트 const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899"]; // 첫 번째 데이터셋 사용 const dataset = data.datasets[0]; const pieData = data.labels.map((label, i) => ({ label, value: dataset.data[i], })); // 파이 생성기 const pie = d3 .pie<{ label: string; value: number }>() .value((d) => d.value) .sort(null); // 아크 생성기 const innerRadius = isDonut ? radius * (config.pieInnerRadius || 0.5) : 0; const arc = d3.arc>().innerRadius(innerRadius).outerRadius(radius); // 툴팁용 확대 아크 const arcHover = d3 .arc>() .innerRadius(innerRadius) .outerRadius(radius + 10); // 파이 조각 그리기 const arcs = g.selectAll(".arc").data(pie(pieData)).enter().append("g").attr("class", "arc"); const paths = arcs .append("path") .attr("fill", (d, i) => colors[i % colors.length]) .attr("stroke", "white") .attr("stroke-width", 2); // 애니메이션 if (config.enableAnimation !== false) { paths .transition() .duration(config.animationDuration || 750) .attrTween("d", function (d) { const interpolate = d3.interpolate({ startAngle: 0, endAngle: 0 }, d); return function (t) { return arc(interpolate(t)) || ""; }; }); } else { paths.attr("d", arc); } // 툴팁 if (config.showTooltip !== false) { paths .on("mouseover", function (event, d) { d3.select(this).transition().duration(200).attr("d", arcHover); const tooltip = g .append("g") .attr("class", "tooltip") .attr("transform", `translate(${arc.centroid(d)[0]},${arc.centroid(d)[1]})`); tooltip .append("rect") .attr("x", -50) .attr("y", -40) .attr("width", 100) .attr("height", 35) .attr("fill", "rgba(0,0,0,0.8)") .attr("rx", 4); tooltip .append("text") .attr("text-anchor", "middle") .attr("fill", "white") .attr("font-size", "12px") .attr("y", -25) .text(d.data.label); tooltip .append("text") .attr("text-anchor", "middle") .attr("fill", "white") .attr("font-size", "14px") .attr("font-weight", "bold") .attr("y", -10) .text(`${d.data.value} (${((d.data.value / d3.sum(dataset.data)) * 100).toFixed(1)}%)`); }) .on("mouseout", function (event, d) { d3.select(this).transition().duration(200).attr("d", arc); g.selectAll(".tooltip").remove(); }); } // 차트 제목 if (config.title) { svg .append("text") .attr("x", width / 2) .attr("y", 20) .attr("text-anchor", "middle") .style("font-size", "16px") .style("font-weight", "bold") .text(config.title); } // 범례 (차트 아래, 가로 배치, 중앙 정렬) if (config.showLegend !== false) { const itemSpacing = 100; // 각 범례 항목 사이 간격 (줄임) const totalWidth = pieData.length * itemSpacing; const legendStartX = (width - totalWidth) / 2; // 시작 위치 const legendY = centerY + radius + 40; // 차트 아래 40px const legend = svg.append("g").attr("class", "legend"); pieData.forEach((d, i) => { const legendItem = legend .append("g") .attr("transform", `translate(${legendStartX + i * itemSpacing + itemSpacing / 2}, ${legendY})`); legendItem .append("rect") .attr("x", -6) // 사각형을 중앙 기준으로 .attr("y", -6) .attr("width", 12) .attr("height", 12) .attr("fill", colors[i % colors.length]) .attr("rx", 2); legendItem .append("text") .attr("x", 0) .attr("y", 18) .attr("text-anchor", "middle") // 텍스트 중앙 정렬 .style("font-size", "8px") .style("fill", "#333") .text(`${d.label} (${d.value})`); }); } // 도넛 차트 중앙 텍스트 if (isDonut) { const total = d3.sum(dataset.data); g.append("text") .attr("text-anchor", "middle") .attr("y", -10) .style("font-size", "24px") .style("font-weight", "bold") .style("fill", "#333") .text(total.toLocaleString()); g.append("text") .attr("text-anchor", "middle") .attr("y", 15) .style("font-size", "14px") .style("fill", "#666") .text("Total"); } }, [data, config, width, height, isDonut]); return ; }