Add comprehensive analytics system with: - Analytics data layer (aggregator, metrics, trends, cache) - 6 API endpoints (overview, plants, transport, farms, sustainability, export) - 6 chart components (LineChart, BarChart, PieChart, AreaChart, Gauge, Heatmap) - 5 dashboard widgets (KPICard, TrendIndicator, DataTable, DateRangePicker, FilterPanel) - 5 dashboard pages (overview, plants, transport, farms, sustainability) - Export functionality (CSV, JSON) Dependencies added: recharts, d3, date-fns Also includes minor fixes: - Fix EnvironmentalForm spread type error - Fix AgentOrchestrator Map iteration issues - Fix next.config.js image domains undefined error - Add downlevelIteration to tsconfig
91 lines
2.4 KiB
TypeScript
91 lines
2.4 KiB
TypeScript
/**
|
|
* Gauge Chart Component
|
|
* Displays a single value as a gauge/meter
|
|
*/
|
|
|
|
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
|
|
|
|
interface GaugeProps {
|
|
value: number;
|
|
max?: number;
|
|
title?: string;
|
|
unit?: string;
|
|
size?: number;
|
|
colors?: { low: string; medium: string; high: string };
|
|
thresholds?: { low: number; high: number };
|
|
}
|
|
|
|
const DEFAULT_COLORS = {
|
|
low: '#ef4444',
|
|
medium: '#f59e0b',
|
|
high: '#10b981',
|
|
};
|
|
|
|
export default function Gauge({
|
|
value,
|
|
max = 100,
|
|
title,
|
|
unit = '%',
|
|
size = 200,
|
|
colors = DEFAULT_COLORS,
|
|
thresholds = { low: 33, high: 66 },
|
|
}: GaugeProps) {
|
|
const percentage = Math.min((value / max) * 100, 100);
|
|
|
|
// Determine color based on thresholds
|
|
let color: string;
|
|
if (percentage < thresholds.low) {
|
|
color = colors.low;
|
|
} else if (percentage < thresholds.high) {
|
|
color = colors.medium;
|
|
} else {
|
|
color = colors.high;
|
|
}
|
|
|
|
// Data for semi-circle gauge
|
|
const gaugeData = [
|
|
{ value: percentage, color },
|
|
{ value: 100 - percentage, color: '#e5e7eb' },
|
|
];
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-lg p-6 flex flex-col items-center">
|
|
{title && <h3 className="text-lg font-bold text-gray-900 mb-2">{title}</h3>}
|
|
<div className="relative" style={{ width: size, height: size / 2 + 20 }}>
|
|
<ResponsiveContainer width="100%" height={size}>
|
|
<PieChart>
|
|
<Pie
|
|
data={gaugeData}
|
|
cx="50%"
|
|
cy="100%"
|
|
startAngle={180}
|
|
endAngle={0}
|
|
innerRadius={size * 0.3}
|
|
outerRadius={size * 0.4}
|
|
paddingAngle={0}
|
|
dataKey="value"
|
|
>
|
|
{gaugeData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} stroke="none" />
|
|
))}
|
|
</Pie>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
<div
|
|
className="absolute inset-0 flex flex-col items-center justify-end pb-2"
|
|
style={{ top: size * 0.2 }}
|
|
>
|
|
<span className="text-3xl font-bold" style={{ color }}>
|
|
{value.toFixed(1)}
|
|
</span>
|
|
<span className="text-sm text-gray-500">{unit}</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-between w-full mt-2 px-4 text-xs text-gray-500">
|
|
<span>0</span>
|
|
<span>{max / 2}</span>
|
|
<span>{max}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|