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