2025-10-31 17:48:20 +01:00
|
|
|
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;
|
2025-10-31 17:57:32 +01:00
|
|
|
const radius = outerRadius + 35;
|
2025-10-31 17:48:20 +01:00
|
|
|
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
|
2025-10-31 17:57:32 +01:00
|
|
|
const lineEndX = x > cx ? x + 30 : x - 30;
|
2025-10-31 17:48:20 +01:00
|
|
|
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}
|
2025-10-31 17:57:32 +01:00
|
|
|
strokeWidth="1.5"
|
2025-10-31 17:48:20 +01:00
|
|
|
className="print:stroke-gray-400"
|
|
|
|
|
/>
|
|
|
|
|
<line
|
|
|
|
|
x1={x}
|
|
|
|
|
y1={y}
|
|
|
|
|
x2={lineEndX}
|
|
|
|
|
y2={y}
|
|
|
|
|
stroke={fill}
|
2025-10-31 17:57:32 +01:00
|
|
|
strokeWidth="1.5"
|
2025-10-31 17:48:20 +01:00
|
|
|
className="print:stroke-gray-400"
|
|
|
|
|
/>
|
|
|
|
|
|
2025-10-31 18:10:41 +01:00
|
|
|
{/* Single-line label text - project name with percentage */}
|
2025-10-31 17:48:20 +01:00
|
|
|
<text
|
|
|
|
|
x={lineEndX}
|
2025-10-31 18:10:41 +01:00
|
|
|
y={y}
|
2025-10-31 17:48:20 +01:00
|
|
|
textAnchor={textAnchor}
|
2025-10-31 17:57:32 +01:00
|
|
|
className="fill-slate-700 print:text-[7px]"
|
|
|
|
|
style={{ fontSize: '10px', fontWeight: 600 }}
|
2025-10-31 17:48:20 +01:00
|
|
|
>
|
2025-10-31 18:10:41 +01:00
|
|
|
{name} ({percentage.toFixed(1)}%)
|
2025-10-31 17:48:20 +01:00
|
|
|
</text>
|
|
|
|
|
</g>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-10-31 17:57:32 +01:00
|
|
|
<div className="flex flex-col items-center w-full max-w-4xl mx-auto">
|
2025-10-31 18:37:27 +01:00
|
|
|
{/* SVG Chart - Responsive container (hidden in print) */}
|
|
|
|
|
<div className="w-full print:hidden" style={{ maxWidth: '700px' }}>
|
|
|
|
|
<ResponsiveContainer width="100%" height={400}>
|
2025-10-31 17:57:32 +01:00
|
|
|
<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"
|
2025-10-31 18:37:27 +01:00
|
|
|
className="text-2xl font-bold fill-emerald-600"
|
2025-10-31 17:57:32 +01:00
|
|
|
style={{ fontSize: '20px', fontWeight: 700, fill: '#059669' }}
|
|
|
|
|
/>
|
|
|
|
|
<Label
|
|
|
|
|
value="tons CO₂"
|
|
|
|
|
position="center"
|
|
|
|
|
dy={18}
|
2025-10-31 18:37:27 +01:00
|
|
|
className="text-sm fill-slate-600"
|
2025-10-31 17:57:32 +01:00
|
|
|
style={{ fontSize: '12px', fill: '#475569' }}
|
|
|
|
|
/>
|
|
|
|
|
</Pie>
|
|
|
|
|
</PieChart>
|
|
|
|
|
</ResponsiveContainer>
|
|
|
|
|
</div>
|
2025-10-31 17:48:20 +01:00
|
|
|
|
2025-10-31 18:37:27 +01:00
|
|
|
{/* Print-optimized table (visible only in print) */}
|
|
|
|
|
<div className="hidden print:block w-full mb-6">
|
2025-10-31 18:46:27 +01:00
|
|
|
<h3 className="text-xl font-bold mb-3 text-center text-slate-800">Portfolio Distribution</h3>
|
|
|
|
|
<p className="text-center mb-4 text-lg font-semibold text-emerald-700">
|
2025-10-31 18:37:27 +01:00
|
|
|
Total: {totalTons} tons CO₂
|
|
|
|
|
</p>
|
2025-10-31 18:46:27 +01:00
|
|
|
<table className="w-full border-collapse shadow-md rounded-lg overflow-hidden">
|
2025-10-31 18:37:27 +01:00
|
|
|
<thead>
|
2025-10-31 18:46:27 +01:00
|
|
|
<tr className="bg-gradient-to-r from-cyan-500 to-blue-500 text-white">
|
|
|
|
|
<th className="border border-blue-400 px-4 py-3 text-left font-bold">Project Name</th>
|
|
|
|
|
<th className="border border-blue-400 px-4 py-3 text-center font-bold">Percentage</th>
|
|
|
|
|
<th className="border border-blue-400 px-4 py-3 text-right font-bold">Tons CO₂</th>
|
2025-10-31 18:37:27 +01:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{chartData.map((item, index) => (
|
2025-10-31 18:46:27 +01:00
|
|
|
<tr key={index} className={index % 2 === 0 ? 'bg-slate-50' : 'bg-white'}>
|
|
|
|
|
<td className="border border-slate-200 px-4 py-2 text-slate-800">{item.name}</td>
|
|
|
|
|
<td className="border border-slate-200 px-4 py-2 text-center text-emerald-700 font-semibold">{item.percentage.toFixed(1)}%</td>
|
|
|
|
|
<td className="border border-slate-200 px-4 py-2 text-right text-slate-800 font-medium">{item.tons}</td>
|
2025-10-31 18:37:27 +01:00
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Legend below chart (for screen clarity, hidden in print) */}
|
|
|
|
|
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-3 w-full max-w-2xl print:hidden">
|
2025-10-31 17:48:20 +01:00
|
|
|
{chartData.map((item, index) => (
|
2025-10-31 18:37:27 +01:00
|
|
|
<div key={index} className="flex items-center gap-3">
|
2025-10-31 17:48:20 +01:00
|
|
|
<div
|
2025-10-31 18:37:27 +01:00
|
|
|
className="w-4 h-4 rounded-sm flex-shrink-0"
|
2025-10-31 17:48:20 +01:00
|
|
|
style={{ backgroundColor: item.fill }}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
2025-10-31 18:37:27 +01:00
|
|
|
<p className="text-sm font-medium text-slate-800 truncate">
|
2025-10-31 17:48:20 +01:00
|
|
|
{item.name}
|
|
|
|
|
</p>
|
2025-10-31 18:37:27 +01:00
|
|
|
<p className="text-xs text-slate-600">
|
2025-10-31 17:48:20 +01:00
|
|
|
{item.tons} tons ({item.percentage.toFixed(1)}%)
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|