@@ -57,6 +57,56 @@ const formatValue = (value: number | undefined, isMobile: boolean = false): stri
5757 return value . toLocaleString ( ) ;
5858} ;
5959
60+ const formatSignedCurrency = ( value : number | undefined , isMobile : boolean = false ) : string => {
61+ if ( value === undefined || value === null || Number . isNaN ( value ) ) {
62+ return isMobile ? '$0' : '$0.00' ;
63+ }
64+
65+ const sign = value > 0 ? '+' : value < 0 ? '-' : '' ;
66+ const absolute = Math . abs ( value ) ;
67+ const formatted = isMobile
68+ ? formatValue ( absolute , true )
69+ : absolute . toLocaleString ( undefined , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) ;
70+
71+ return `${ sign } $${ formatted } ` ;
72+ } ;
73+
74+ const formatUnsignedCurrency = ( value : number | undefined , isMobile : boolean = false ) : string => {
75+ if ( value === undefined || value === null || Number . isNaN ( value ) ) {
76+ return isMobile ? '$0' : '$0.00' ;
77+ }
78+
79+ const absolute = Math . abs ( value ) ;
80+ const formatted = isMobile
81+ ? formatValue ( absolute , true )
82+ : absolute . toLocaleString ( undefined , { minimumFractionDigits : 2 , maximumFractionDigits : 2 } ) ;
83+
84+ return `$${ formatted } ` ;
85+ } ;
86+
87+ const formatLabelForPeriod = ( date : Date , period : TimePeriod ) : string => {
88+ switch ( period ) {
89+ case '1D' :
90+ return date . toLocaleTimeString ( 'en-US' , {
91+ hour : '2-digit' ,
92+ minute : '2-digit' ,
93+ hour12 : false ,
94+ } ) ;
95+ case '1W' :
96+ return date . toLocaleDateString ( 'en-US' , { weekday : 'short' } ) ;
97+ case '1M' :
98+ case '3M' :
99+ case 'YTD' :
100+ case '1Y' :
101+ return date . toLocaleDateString ( 'en-US' , { month : 'short' , day : 'numeric' } ) ;
102+ case '5Y' :
103+ case 'All' :
104+ return date . toLocaleDateString ( 'en-US' , { month : 'short' , year : 'numeric' } ) ;
105+ default :
106+ return date . toLocaleDateString ( ) ;
107+ }
108+ } ;
109+
60110// Remove the old hardcoded function - we'll create a dynamic one in the component
61111
62112const PerformanceChart = React . memo ( ( { selectedStock : propSelectedStock , selectedStockDescription, onClearSelection } : PerformanceChartProps ) => {
@@ -384,7 +434,69 @@ const PerformanceChart = React.memo(({ selectedStock: propSelectedStock, selecte
384434 return [ ] ;
385435 } , [ selectedStock , stockData , selectedPeriod , portfolioData ] ) ;
386436
387- const currentData = useMemo ( ( ) => getCurrentData ( ) , [ getCurrentData ] ) ;
437+ const baseData = useMemo ( ( ) => getCurrentData ( ) , [ getCurrentData ] ) ;
438+
439+ const currentData = useMemo ( ( ) => {
440+ if ( ! baseData || baseData . length === 0 ) {
441+ return baseData ;
442+ }
443+
444+ if ( selectedStock || ! metrics ?. accountValue ) {
445+ return baseData ;
446+ }
447+
448+ const currentValue = Number ( metrics . accountValue ) ;
449+ if ( ! Number . isFinite ( currentValue ) || currentValue <= 0 ) {
450+ return baseData ;
451+ }
452+
453+ const referencePoint = baseData . find ( point => Number ( point ?. value ?? 0 ) > 0 ) || baseData [ 0 ] ;
454+ if ( ! referencePoint ) {
455+ return baseData ;
456+ }
457+
458+ const baselineCandidate = Number ( referencePoint . value ) - Number ( referencePoint . pnl ?? 0 ) ;
459+ const fallbackBaseline = Number ( referencePoint . value ) || Number . EPSILON ;
460+ const baseline = Number . isFinite ( baselineCandidate ) && baselineCandidate !== 0
461+ ? baselineCandidate
462+ : fallbackBaseline ;
463+
464+ const pnl = currentValue - baseline ;
465+ const pnlPercent = baseline !== 0 ? ( pnl / baseline ) * 100 : 0 ;
466+
467+ const now = new Date ( ) ;
468+ const timeLabel = formatLabelForPeriod ( now , selectedPeriod ) ;
469+ const latestTimestamp = Math . floor ( now . getTime ( ) / 1000 ) ;
470+
471+ const lastPoint = baseData [ baseData . length - 1 ] ;
472+ const timeAlreadyUsed = lastPoint ?. time === timeLabel || Number ( lastPoint ?. timestamp ?? 0 ) >= latestTimestamp ;
473+
474+ const replacePeriods = selectedPeriod === '3M'
475+ || selectedPeriod === '1Y'
476+ || selectedPeriod === '5Y'
477+ || selectedPeriod === 'All'
478+ || ( selectedPeriod === 'YTD' && baseData . length > 31 ) ;
479+
480+ const shouldReplaceLast = timeAlreadyUsed || replacePeriods ;
481+
482+ const dataCopy = [ ...baseData ] ;
483+
484+ const newPoint : PortfolioDataPoint = {
485+ time : timeLabel ,
486+ value : currentValue ,
487+ pnl,
488+ pnlPercent,
489+ timestamp : latestTimestamp
490+ } ;
491+
492+ if ( shouldReplaceLast && dataCopy . length > 0 ) {
493+ dataCopy [ dataCopy . length - 1 ] = newPoint ;
494+ } else {
495+ dataCopy . push ( newPoint ) ;
496+ }
497+
498+ return dataCopy ;
499+ } , [ baseData , metrics , selectedStock , selectedPeriod ] ) ;
388500
389501 // Custom tick formatter for X-axis based on period
390502 const formatXAxisTick = useCallback ( ( value : string ) => {
@@ -405,6 +517,22 @@ const PerformanceChart = React.memo(({ selectedStock: propSelectedStock, selecte
405517 ( firstValue . value > 0 ? ( ( totalReturn / firstValue . value ) * 100 ) . toFixed ( 2 ) : '0.00' ) ;
406518 const isPositive = totalReturn >= 0 ;
407519
520+ const cashFlowSummary = metrics ?. cashFlows ;
521+ const hasCashFlowSummary = Boolean ( cashFlowSummary ) ;
522+ const netContributions = hasCashFlowSummary ? Number ( cashFlowSummary ?. netContributions ?? 0 ) : null ;
523+ const totalDeposits = hasCashFlowSummary ? Number ( cashFlowSummary ?. totalDeposits ?? 0 ) : null ;
524+ const totalWithdrawals = hasCashFlowSummary ? Number ( cashFlowSummary ?. totalWithdrawals ?? 0 ) : null ;
525+ const activityCount = hasCashFlowSummary ? Number ( cashFlowSummary ?. activityCount ?? 0 ) : null ;
526+ const baselineValue = typeof metrics ?. baselineValue === 'number' && metrics . baselineValue > 0
527+ ? metrics . baselineValue
528+ : null ;
529+ const totalReturnSourceLabel = metrics ?. totalReturnSource === 'cash_flows'
530+ ? 'cash flow activity'
531+ : metrics ?. totalReturnSource === 'history_base'
532+ ? 'portfolio history baseline'
533+ : null ;
534+ const portfolioMetricsGridClass = `grid grid-cols-2 ${ hasCashFlowSummary ? 'sm:grid-cols-4' : 'sm:grid-cols-3' } gap-3 sm:gap-4 pt-4 border-t` ;
535+
408536 // Debug log for 1D period
409537 if ( selectedStock && selectedPeriod === '1D' && currentData . length > 0 ) {
410538 console . log ( `[PerformanceChart] ${ selectedStock } 1D data:` , {
@@ -676,7 +804,7 @@ const PerformanceChart = React.memo(({ selectedStock: propSelectedStock, selecte
676804 { ! selectedStock ? (
677805 // Portfolio metrics
678806 < div className = "space-y-4" >
679- < div className = "grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4 pt-4 border-t" >
807+ < div className = { portfolioMetricsGridClass } >
680808 < div className = "col-span-2 sm:col-span-1" >
681809 < p className = "text-xs text-muted-foreground" > Portfolio Value</ p >
682810 < p className = "text-sm sm:text-base font-semibold" >
@@ -725,7 +853,37 @@ const PerformanceChart = React.memo(({ selectedStock: propSelectedStock, selecte
725853 'Loading...'
726854 ) }
727855 </ p >
856+ { ( totalReturnSourceLabel || baselineValue ) && (
857+ < p className = "text-[11px] text-muted-foreground" >
858+ { totalReturnSourceLabel ? `Source: ${ totalReturnSourceLabel } ` : '' }
859+ { baselineValue ? `${ totalReturnSourceLabel ? ' · ' : '' } Baseline ${ formatUnsignedCurrency ( baselineValue ) } ` : '' }
860+ </ p >
861+ ) }
728862 </ div >
863+ { hasCashFlowSummary && (
864+ < div >
865+ < p className = "text-xs text-muted-foreground" > Net Contributions</ p >
866+ < div className = "space-y-1" >
867+ < p className = "text-sm sm:text-base font-semibold text-muted-foreground" >
868+ < span className = "hidden sm:inline" > { formatSignedCurrency ( netContributions ?? 0 ) } </ span >
869+ < span className = "sm:hidden" > { formatSignedCurrency ( netContributions ?? 0 , true ) } </ span >
870+ </ p >
871+ < p className = "text-[11px] text-muted-foreground" >
872+ < span className = "hidden sm:inline" >
873+ Deposits { formatUnsignedCurrency ( totalDeposits ?? 0 ) } · Withdrawals { formatUnsignedCurrency ( totalWithdrawals ?? 0 ) }
874+ </ span >
875+ < span className = "sm:hidden" >
876+ Deposits { formatUnsignedCurrency ( totalDeposits ?? 0 , true ) } · Withdrawals { formatUnsignedCurrency ( totalWithdrawals ?? 0 , true ) }
877+ </ span >
878+ </ p >
879+ { activityCount !== null && activityCount > 0 && (
880+ < p className = "text-[11px] text-muted-foreground" >
881+ { `Based on ${ activityCount } activity${ activityCount === 1 ? '' : ' entries' } ` }
882+ </ p >
883+ ) }
884+ </ div >
885+ </ div >
886+ ) }
729887 </ div >
730888 < div className = "grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 pt-4 border-t" >
731889 < div >
0 commit comments