puffin-app/src/components/RechartsPortfolioPieChart.tsx

161 lines
4.8 KiB
TypeScript
Raw Normal View History

import React from 'react';
import { PieChart, Pie, Cell, ResponsiveContainer, Label } from 'recharts';
import type { OffsetProject } from '../types';
import { getProjectColor } from '../utils/portfolioColors';
interface RechartsPortfolioPieChartProps {
projects: OffsetProject[];
totalTons: number;
size?: number;
}
interface ChartDataPoint {
name: string;
value: number;
tons: number;
percentage: number;
fill: string;
}
export function RechartsPortfolioPieChart({
projects,
totalTons,
size = 280,
}: RechartsPortfolioPieChartProps) {
// Transform data to Recharts format
const chartData: ChartDataPoint[] = projects
.filter(project => project.percentage && project.percentage > 0)
.map((project, index) => {
const tons = totalTons * (project.percentage || 0);
return {
name: project.name,
value: tons,
tons: parseFloat(tons.toFixed(2)),
percentage: (project.percentage || 0) * 100,
fill: getProjectColor(index, projects.length),
};
});
// Custom label component for multi-line labels
const renderCustomLabel = (props: any) => {
const {
cx,
cy,
midAngle,
outerRadius,
name,
tons,
percentage,
fill,
} = props;
const RADIAN = Math.PI / 180;
const radius = outerRadius + 35;
const x = cx + radius * Math.cos(-midAngle * RADIAN);
const y = cy + radius * Math.sin(-midAngle * RADIAN);
const textAnchor = x > cx ? 'start' : 'end';
// Calculate line endpoint for label connector
const lineEndX = x > cx ? x + 30 : x - 30;
const labelStartX = cx + (outerRadius + 10) * Math.cos(-midAngle * RADIAN);
const labelStartY = cy + (outerRadius + 10) * Math.sin(-midAngle * RADIAN);
return (
<g>
{/* Connector line from pie edge to label */}
<line
x1={labelStartX}
y1={labelStartY}
x2={x}
y2={y}
stroke={fill}
strokeWidth="1.5"
className="print:stroke-gray-400"
/>
<line
x1={x}
y1={y}
x2={lineEndX}
y2={y}
stroke={fill}
strokeWidth="1.5"
className="print:stroke-gray-400"
/>
{/* Single-line label text - project name with percentage */}
<text
x={lineEndX}
y={y}
textAnchor={textAnchor}
className="fill-slate-700 print:text-[7px]"
style={{ fontSize: '10px', fontWeight: 600 }}
>
{name} ({percentage.toFixed(1)}%)
</text>
</g>
);
};
return (
<div className="flex flex-col items-center w-full max-w-4xl mx-auto">
{/* SVG Chart - Responsive container */}
<div className="w-full print:max-w-[600px]" style={{ maxWidth: '700px' }}>
<ResponsiveContainer width="100%" height={400} className="print:h-[350px]">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
labelLine={false}
label={renderCustomLabel}
outerRadius={80}
innerRadius={50}
paddingAngle={2}
dataKey="value"
isAnimationActive={false}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} stroke="white" strokeWidth={2} />
))}
{/* Center label showing total */}
<Label
value={totalTons.toString()}
position="center"
className="text-2xl font-bold fill-emerald-600 print:text-xl"
style={{ fontSize: '20px', fontWeight: 700, fill: '#059669' }}
/>
<Label
value="tons CO₂"
position="center"
dy={18}
className="text-sm fill-slate-600 print:text-xs"
style={{ fontSize: '12px', fill: '#475569' }}
/>
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Legend below chart (for print clarity) */}
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-3 w-full max-w-2xl print:mt-4 print:gap-2">
{chartData.map((item, index) => (
<div key={index} className="flex items-center gap-3 print:gap-2">
<div
className="w-4 h-4 rounded-sm flex-shrink-0 print:w-3 print:h-3"
style={{ backgroundColor: item.fill }}
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800 truncate print:text-xs">
{item.name}
</p>
<p className="text-xs text-slate-600 print:text-[10px]">
{item.tons} tons ({item.percentage.toFixed(1)}%)
</p>
</div>
</div>
))}
</div>
</div>
);
}