Spaces:
Sleeping
Sleeping
File size: 6,550 Bytes
dbc70ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | import React, { useMemo } from 'react';
import { Treemap, ResponsiveContainer, Tooltip } from 'recharts';
import { getHistoricalData } from '../data/historicalData';
import { motion } from 'framer-motion';
import { X, Info } from 'lucide-react';
const HeatmapContent = (props) => {
const { x, y, width, height, name, performance } = props;
if (width < 5 || height < 5) return null;
const color = performance > 0
? `rgba(0, 200, 5, ${Math.min(0.2 + Math.abs(performance) / 5, 0.9)})`
: `rgba(255, 80, 0, ${Math.min(0.2 + Math.abs(performance) / 5, 0.9)})`;
return (
<g>
<rect
x={x}
y={y}
width={width}
height={height}
style={{
fill: color,
stroke: '#fff',
strokeWidth: 1,
strokeOpacity: 0.2,
}}
/>
{width > 40 && height > 30 && (
<text
x={x + width / 2}
y={y + height / 2 + 5}
textAnchor="middle"
fill="#fff"
fontSize={Math.min(width / 6, 12)}
fontWeight="bold"
className="pointer-events-none select-none"
>
{name}
</text>
)}
</g>
);
};
export default function PortfolioHeatmap({ onClose, allocation }) {
const heatmapData = useMemo(() => {
if (!allocation) return [];
return allocation.map(asset => {
try {
const hist = getHistoricalData(asset.ticker);
return {
name: asset.ticker,
size: asset.value || 1,
performance: hist?.percentChange || 0,
fullName: asset.name
};
} catch (e) {
return {
name: asset.ticker,
size: asset.value || 1,
performance: 0,
fullName: asset.name
};
}
});
}, [allocation]);
return (
<div className="fixed inset-0 z-[70] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-gs-navy/60 backdrop-blur-md"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative bg-[#111111] rounded-3xl shadow-2xl w-full max-w-5xl overflow-hidden border border-white/10 flex flex-col max-h-[90vh]"
>
{/* Header */}
<div className="p-6 md:p-8 border-b border-white/5 flex justify-between items-center bg-black/20">
<div>
<h2 className="text-2xl font-bold text-white flex items-center">
Portfolio Heatmap
<span className="ml-3 px-2 py-1 bg-white/10 rounded text-[10px] uppercase tracking-widest text-white/60 font-medium">
Live Analysis
</span>
</h2>
<p className="text-gray-400 text-sm mt-1 font-light">
Box size: Allocation % | Color: Daily Performance
</p>
</div>
<button
onClick={onClose}
className="p-2 rounded-full bg-white/5 text-white/40 hover:text-white hover:bg-white/10 transition-colors"
>
<X size={24} />
</button>
</div>
<div className="p-6 md:p-8 overflow-y-auto">
<div className="h-[400px] md:h-[500px] w-full bg-black/40 rounded-2xl overflow-hidden border border-white/5">
<ResponsiveContainer width="100%" height="100%">
<Treemap
data={heatmapData}
dataKey="size"
aspectRatio={4 / 3}
stroke="#fff"
content={<HeatmapContent />}
>
<Tooltip
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-[#222] border border-white/10 p-4 rounded-xl shadow-2xl min-w-[180px]">
<p className="text-white font-bold text-base">{data.name}</p>
<p className="text-gray-400 text-[10px] mb-3 truncate max-w-[160px]">{data.fullName}</p>
<div className="flex justify-between items-center border-t border-white/5 pt-2">
<div className="text-left">
<span className="text-[9px] text-gray-500 uppercase block">Allocation</span>
<span className="text-white text-sm font-medium">{data.size}%</span>
</div>
<div className="text-right">
<span className="text-[9px] text-gray-500 uppercase block">Day Change</span>
<span className={`text-sm font-bold ${data.performance > 0 ? 'text-[#00C805]' : 'text-[#FF5000]'}`}>
{data.performance > 0 ? '+' : ''}{data.performance}%
</span>
</div>
</div>
</div>
);
}
return null;
}}
/>
</Treemap>
</ResponsiveContainer>
</div>
{/* Legend */}
<div className="mt-8 flex flex-wrap items-center justify-between gap-6">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-[#FF5000] rounded-sm shadow-[0_0_10px_rgba(255,80,0,0.3)]"></div>
<span className="text-[11px] text-gray-400 font-medium">Down</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-[#00C805] rounded-sm shadow-[0_0_10px_rgba(0,200,5,0.3)]"></div>
<span className="text-[11px] text-gray-400 font-medium">Up</span>
</div>
</div>
<div className="flex items-center gap-2 text-gray-500 bg-white/5 px-4 py-2 rounded-lg border border-white/5">
<Info size={14} className="text-gs-gold" />
<span className="text-[10px] uppercase tracking-wider font-medium">Color intensity scales with volatility</span>
</div>
</div>
</div>
</motion.div>
</div>
);
}
|