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
134 lines
4.1 KiB
TypeScript
134 lines
4.1 KiB
TypeScript
/**
|
|
* Heatmap Component
|
|
* Displays data intensity across a grid
|
|
*/
|
|
|
|
interface HeatmapCell {
|
|
x: string;
|
|
y: string;
|
|
value: number;
|
|
}
|
|
|
|
interface HeatmapProps {
|
|
data: HeatmapCell[];
|
|
title?: string;
|
|
xLabels: string[];
|
|
yLabels: string[];
|
|
colorRange?: { min: string; max: string };
|
|
height?: number;
|
|
showValues?: boolean;
|
|
}
|
|
|
|
function interpolateColor(color1: string, color2: string, factor: number): string {
|
|
const hex = (c: string) => parseInt(c, 16);
|
|
const r1 = hex(color1.slice(1, 3));
|
|
const g1 = hex(color1.slice(3, 5));
|
|
const b1 = hex(color1.slice(5, 7));
|
|
const r2 = hex(color2.slice(1, 3));
|
|
const g2 = hex(color2.slice(3, 5));
|
|
const b2 = hex(color2.slice(5, 7));
|
|
|
|
const r = Math.round(r1 + (r2 - r1) * factor);
|
|
const g = Math.round(g1 + (g2 - g1) * factor);
|
|
const b = Math.round(b1 + (b2 - b1) * factor);
|
|
|
|
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
}
|
|
|
|
export default function Heatmap({
|
|
data,
|
|
title,
|
|
xLabels,
|
|
yLabels,
|
|
colorRange = { min: '#fee2e2', max: '#10b981' },
|
|
height = 300,
|
|
showValues = true,
|
|
}: HeatmapProps) {
|
|
const maxValue = Math.max(...data.map((d) => d.value));
|
|
const minValue = Math.min(...data.map((d) => d.value));
|
|
const range = maxValue - minValue || 1;
|
|
|
|
const getColor = (value: number): string => {
|
|
const factor = (value - minValue) / range;
|
|
return interpolateColor(colorRange.min, colorRange.max, factor);
|
|
};
|
|
|
|
const getValue = (x: string, y: string): number | undefined => {
|
|
const cell = data.find((d) => d.x === x && d.y === y);
|
|
return cell?.value;
|
|
};
|
|
|
|
const cellWidth = `${100 / xLabels.length}%`;
|
|
const cellHeight = (height - 40) / yLabels.length;
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-lg p-6">
|
|
{title && <h3 className="text-lg font-bold text-gray-900 mb-4">{title}</h3>}
|
|
<div style={{ height }}>
|
|
{/* X Labels */}
|
|
<div className="flex mb-1" style={{ paddingLeft: '80px' }}>
|
|
{xLabels.map((label) => (
|
|
<div
|
|
key={label}
|
|
className="text-xs text-gray-500 text-center truncate"
|
|
style={{ width: cellWidth }}
|
|
>
|
|
{label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Grid */}
|
|
{yLabels.map((yLabel) => (
|
|
<div key={yLabel} className="flex">
|
|
<div
|
|
className="flex items-center justify-end pr-2 text-xs text-gray-500"
|
|
style={{ width: '80px' }}
|
|
>
|
|
{yLabel}
|
|
</div>
|
|
{xLabels.map((xLabel) => {
|
|
const value = getValue(xLabel, yLabel);
|
|
const bgColor = value !== undefined ? getColor(value) : '#f3f4f6';
|
|
const textColor =
|
|
value !== undefined && (value - minValue) / range > 0.5
|
|
? '#fff'
|
|
: '#374151';
|
|
|
|
return (
|
|
<div
|
|
key={`${xLabel}-${yLabel}`}
|
|
className="flex items-center justify-center border border-white rounded-sm transition-all hover:ring-2 hover:ring-gray-400"
|
|
style={{
|
|
width: cellWidth,
|
|
height: cellHeight,
|
|
backgroundColor: bgColor,
|
|
}}
|
|
title={`${xLabel}, ${yLabel}: ${value ?? 'N/A'}`}
|
|
>
|
|
{showValues && value !== undefined && (
|
|
<span className="text-xs font-medium" style={{ color: textColor }}>
|
|
{value.toFixed(0)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
|
|
{/* Legend */}
|
|
<div className="flex items-center justify-center mt-4 space-x-4">
|
|
<span className="text-xs text-gray-500">Low</span>
|
|
<div
|
|
className="w-24 h-3 rounded"
|
|
style={{
|
|
background: `linear-gradient(to right, ${colorRange.min}, ${colorRange.max})`,
|
|
}}
|
|
/>
|
|
<span className="text-xs text-gray-500">High</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|