localgreenchain/components/analytics/charts/Heatmap.tsx
Claude 816c3b3f2e
Implement Agent 7: Advanced Analytics Dashboard
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
2025-11-23 04:02:07 +00:00

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>
);
}