Skip to content

Commit a208249

Browse files
feat: performance chart added (#54)
Closes #50
1 parent bff9820 commit a208249

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import React from 'react';
2+
3+
interface ChartData {
4+
date: string;
5+
value: number;
6+
}
7+
8+
interface PerformanceChartProps {
9+
portfolioData: ChartData[];
10+
benchmarkData: ChartData[];
11+
height?: number;
12+
}
13+
14+
export const PerformanceChart: React.FC<PerformanceChartProps> = ({
15+
portfolioData,
16+
benchmarkData,
17+
height = 300
18+
}) => {
19+
if (!portfolioData.length || !benchmarkData.length) {
20+
return (
21+
<div className="flex items-center justify-center h-64 bg-dark-50/50 border border-dark-300 rounded-xl">
22+
<div className="text-center text-dark-500">
23+
<div className="text-4xl mb-2">📊</div>
24+
<p>No chart data available</p>
25+
</div>
26+
</div>
27+
);
28+
}
29+
30+
// Calculate chart dimensions and scales
31+
const chartWidth = 800;
32+
const chartHeight = height;
33+
const padding = { top: 40, right: 40, bottom: 40, left: 60 };
34+
35+
// Find min and max values for scaling
36+
const allValues = [...portfolioData.map(d => d.value), ...benchmarkData.map(d => d.value)];
37+
const maxValue = Math.max(...allValues);
38+
const minValue = Math.min(...allValues);
39+
const valueRange = maxValue - minValue;
40+
41+
// Scale functions
42+
const scaleX = (index: number) =>
43+
padding.left + (index / (portfolioData.length - 1)) * (chartWidth - padding.left - padding.right);
44+
45+
const scaleY = (value: number) =>
46+
chartHeight - padding.bottom - ((value - minValue) / valueRange) * (chartHeight - padding.top - padding.bottom);
47+
48+
// Generate SVG path data
49+
const generatePath = (data: ChartData[]) => {
50+
if (data.length === 0) return '';
51+
52+
let path = `M ${scaleX(0)} ${scaleY(data[0].value)}`;
53+
54+
for (let i = 1; i < data.length; i++) {
55+
path += ` L ${scaleX(i)} ${scaleY(data[i].value)}`;
56+
}
57+
58+
return path;
59+
};
60+
61+
const portfolioPath = generatePath(portfolioData);
62+
const benchmarkPath = generatePath(benchmarkData);
63+
64+
// Generate grid lines and labels
65+
const gridLines = [];
66+
const yLabels = [];
67+
const numGridLines = 5;
68+
69+
for (let i = 0; i <= numGridLines; i++) {
70+
const value = minValue + (i / numGridLines) * valueRange;
71+
const y = scaleY(value);
72+
73+
gridLines.push(
74+
<line
75+
key={`grid-${i}`}
76+
x1={padding.left}
77+
y1={y}
78+
x2={chartWidth - padding.right}
79+
y2={y}
80+
className="stroke-dark-400/30 stroke-1"
81+
/>
82+
);
83+
84+
yLabels.push(
85+
<text
86+
key={`label-${i}`}
87+
x={padding.left - 10}
88+
y={y}
89+
className="fill-dark-500 text-xs font-inter"
90+
textAnchor="end"
91+
dominantBaseline="middle"
92+
>
93+
${Math.round(value).toLocaleString()}
94+
</text>
95+
);
96+
}
97+
98+
// Generate x-axis labels (dates)
99+
const xLabels = [];
100+
const labelInterval = Math.max(1, Math.floor(portfolioData.length / 6));
101+
102+
for (let i = 0; i < portfolioData.length; i += labelInterval) {
103+
const date = new Date(portfolioData[i].date);
104+
const label = date.toLocaleDateString('en-US', {
105+
year: '2-digit',
106+
month: 'short'
107+
});
108+
109+
xLabels.push(
110+
<text
111+
key={`xlabel-${i}`}
112+
x={scaleX(i)}
113+
y={chartHeight - padding.bottom + 20}
114+
className="fill-dark-500 text-[10px] font-inter"
115+
textAnchor="middle"
116+
>
117+
{label}
118+
</text>
119+
);
120+
}
121+
122+
// Calculate performance metrics
123+
const portfolioStart = portfolioData[0].value;
124+
const portfolioEnd = portfolioData[portfolioData.length - 1].value;
125+
const portfolioReturn = ((portfolioEnd - portfolioStart) / portfolioStart) * 100;
126+
127+
const benchmarkStart = benchmarkData[0].value;
128+
const benchmarkEnd = benchmarkData[benchmarkData.length - 1].value;
129+
const benchmarkReturn = ((benchmarkEnd - benchmarkStart) / benchmarkStart) * 100;
130+
131+
return (
132+
<div className="bg-gradient-to-br from-dark-50/95 to-dark-100/98 border border-dark-400/20 rounded-2xl p-6 backdrop-blur-xl transition-all duration-300 hover:border-blue-500/30 hover:shadow-2xl">
133+
{/* Chart Header */}
134+
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-start gap-4 mb-6">
135+
<div>
136+
<h3 className="text-white text-xl font-semibold mb-1">Portfolio Performance vs Benchmark</h3>
137+
<div className="text-dark-500 text-sm">
138+
{portfolioData[0]?.date} to {portfolioData[portfolioData.length - 1]?.date}
139+
</div>
140+
</div>
141+
142+
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6">
143+
<div className="flex flex-col gap-1">
144+
<span className="text-dark-500 text-xs uppercase tracking-wide">Portfolio Return:</span>
145+
<span className={`text-lg font-semibold ${portfolioReturn >= 0 ? 'text-green-400' : 'text-red-400'}`}>
146+
{portfolioReturn >= 0 ? '+' : ''}{portfolioReturn.toFixed(2)}%
147+
</span>
148+
</div>
149+
150+
<div className="flex flex-col gap-1">
151+
<span className="text-dark-500 text-xs uppercase tracking-wide">Benchmark Return:</span>
152+
<span className={`text-lg font-semibold ${benchmarkReturn >= 0 ? 'text-green-400' : 'text-red-400'}`}>
153+
{benchmarkReturn >= 0 ? '+' : ''}{benchmarkReturn.toFixed(2)}%
154+
</span>
155+
</div>
156+
157+
<div className="flex flex-col gap-1">
158+
<span className="text-dark-500 text-xs uppercase tracking-wide">Outperformance:</span>
159+
<span className={`text-lg font-semibold ${portfolioReturn >= benchmarkReturn ? 'text-green-400' : 'text-red-400'}`}>
160+
{(portfolioReturn - benchmarkReturn).toFixed(2)}%
161+
</span>
162+
</div>
163+
</div>
164+
</div>
165+
166+
{/* Chart Container */}
167+
<div className="bg-white/5 border border-dark-400/10 rounded-xl p-4 my-4 transition-colors duration-300 hover:border-dark-400/20">
168+
<svg
169+
width="100%"
170+
height={chartHeight}
171+
viewBox={`0 0 ${chartWidth} ${chartHeight}`}
172+
className="block max-w-full h-auto"
173+
>
174+
{/* Background */}
175+
<rect width="100%" height="100%" fill="transparent" />
176+
177+
{/* Grid lines */}
178+
{gridLines}
179+
180+
{/* Y-axis labels */}
181+
{yLabels}
182+
183+
{/* X-axis labels */}
184+
{xLabels}
185+
186+
{/* Axis lines */}
187+
<line
188+
x1={padding.left}
189+
y1={padding.top}
190+
x2={padding.left}
191+
y2={chartHeight - padding.bottom}
192+
className="stroke-dark-400/50 stroke-2"
193+
/>
194+
<line
195+
x1={padding.left}
196+
y1={chartHeight - padding.bottom}
197+
x2={chartWidth - padding.right}
198+
y2={chartHeight - padding.bottom}
199+
className="stroke-dark-400/50 stroke-2"
200+
/>
201+
202+
{/* Portfolio line with glow effect */}
203+
<defs>
204+
<filter id="portfolioGlow" x="-20%" y="-20%" width="140%" height="140%">
205+
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
206+
<feMerge>
207+
<feMergeNode in="coloredBlur"/>
208+
<feMergeNode in="SourceGraphic"/>
209+
</feMerge>
210+
</filter>
211+
</defs>
212+
213+
<path
214+
d={portfolioPath}
215+
className="stroke-blue-500 fill-none stroke-[3]"
216+
strokeLinecap="round"
217+
strokeLinejoin="round"
218+
filter="url(#portfolioGlow)"
219+
/>
220+
221+
{/* Benchmark line */}
222+
<path
223+
d={benchmarkPath}
224+
className="stroke-green-500 fill-none stroke-2"
225+
strokeLinecap="round"
226+
strokeLinejoin="round"
227+
strokeDasharray="5,5"
228+
/>
229+
230+
{/* Portfolio points */}
231+
{portfolioData.map((point, index) => (
232+
<circle
233+
key={`portfolio-${index}`}
234+
cx={scaleX(index)}
235+
cy={scaleY(point.value)}
236+
r="4"
237+
className="fill-blue-500 stroke-white stroke-2 transition-all duration-200 hover:r-6 hover:fill-white hover:stroke-blue-500"
238+
/>
239+
))}
240+
241+
{/* Benchmark points */}
242+
{benchmarkData.map((point, index) => (
243+
<circle
244+
key={`benchmark-${index}`}
245+
cx={scaleX(index)}
246+
cy={scaleY(point.value)}
247+
r="3"
248+
className="fill-green-500 stroke-white stroke-[1.5] transition-all duration-200 hover:r-5 hover:fill-white hover:stroke-green-500"
249+
/>
250+
))}
251+
</svg>
252+
</div>
253+
254+
{/* Chart Legend */}
255+
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mt-4 pt-4 border-t border-dark-400/20">
256+
<div className="flex items-center gap-3 bg-white/5 px-4 py-2 rounded-lg border border-dark-400/10">
257+
<div className="w-4 h-1 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full"></div>
258+
<span className="text-dark-500 text-sm">
259+
Strategy Portfolio (${portfolioEnd.toLocaleString()})
260+
</span>
261+
</div>
262+
263+
<div className="flex items-center gap-3 bg-white/5 px-4 py-2 rounded-lg border border-dark-400/10">
264+
<div className="w-4 h-1 bg-gradient-to-r from-green-500 to-green-600 rounded-full"></div>
265+
<span className="text-dark-500 text-sm">
266+
Benchmark - SPY (${benchmarkEnd.toLocaleString()})
267+
</span>
268+
</div>
269+
</div>
270+
</div>
271+
);
272+
};

0 commit comments

Comments
 (0)