diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 5492ddd4c..a6925f8ff 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -61,6 +61,8 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { @Input() isLoading = false; @Input() locale = getLocale(); @Input() savingsRate = 0; + @Input() xMax: Date; + @Input() xMin: Date; @ViewChild('chartCanvas') chartCanvas; @@ -241,7 +243,13 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { grid: { display: false }, + max: this.xMax?.getTime(), + min: this.xMin?.getTime(), type: 'time', + bounds: 'data', + ticks: { + source: 'data' + }, time: { tooltipFormat: getDateFormatString(this.locale), unit: 'year' diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 63ed3569c..add29c0cb 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -73,11 +73,15 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { public deviceType: string; public dividendsByGroup: InvestmentItem[]; public dividendTimelineDataLabel = $localize`Dividend`; + public dividendTimelineXMax: Date; + public dividendTimelineXMin: Date; public firstOrderDate: Date; public hasImpersonationId: boolean; public hasPermissionToReadAiPrompt: boolean; public investments: InvestmentItem[]; public investmentTimelineDataLabel = $localize`Investment`; + public investmentTimelineXMax: Date; + public investmentTimelineXMin: Date; public investmentsByGroup: InvestmentItem[]; public isLoadingAnalysisPrompt: boolean; public isLoadingBenchmarkComparator: boolean; @@ -94,6 +98,10 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { public performanceDataItems: HistoricalDataItem[]; public performanceDataItemsInPercentage: HistoricalDataItem[]; public portfolioEvolutionDataLabel = $localize`Investment`; + public portfolioEvolutionXMax: Date; + public portfolioEvolutionXMin: Date; + public globalXMax: Date; + public globalXMin: Date; public streaks: PortfolioInvestmentsResponse['streaks']; public top3: PortfolioPosition[]; public unitCurrentStreak: string; @@ -241,6 +249,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { this.isLoadingDividendTimelineChart = false; + this.updateDateRanges(); + this.changeDetectorRef.markForCheck(); }); @@ -273,6 +283,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { this.isLoadingInvestmentTimelineChart = false; + this.updateDateRanges(); + this.changeDetectorRef.markForCheck(); }); } @@ -327,6 +339,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { this.updateBenchmarkDataItems(); + this.updateDateRanges(); + this.changeDetectorRef.markForCheck(); }); @@ -402,4 +416,79 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { } } } + + private updateDateRanges() { + // Calculate min and max dates for chart scaling based on filtered data + // This ensures charts are scaled proportionally to the selected time period + + const allDates: Date[] = []; + + if (this.performanceDataItems && this.performanceDataItems.length > 0) { + allDates.push( + ...this.performanceDataItems.map((item) => new Date(item.date)) + ); + } + + if (this.investmentsByGroup && this.investmentsByGroup.length > 0) { + allDates.push( + ...this.investmentsByGroup.map((item) => new Date(item.date)) + ); + } + + if (this.dividendsByGroup && this.dividendsByGroup.length > 0) { + allDates.push( + ...this.dividendsByGroup.map((item) => new Date(item.date)) + ); + } + + if (allDates.length > 0) { + this.globalXMin = new Date(Math.min(...allDates.map((d) => d.getTime()))); + this.globalXMax = new Date(Math.max(...allDates.map((d) => d.getTime()))); + } else { + this.globalXMin = undefined; + this.globalXMax = undefined; + } + + // Individual ranges for specific charts (fallback if needed) + if (this.performanceDataItems && this.performanceDataItems.length > 0) { + const dates = this.performanceDataItems.map( + (item) => new Date(item.date) + ); + this.portfolioEvolutionXMin = new Date( + Math.min(...dates.map((d) => d.getTime())) + ); + this.portfolioEvolutionXMax = new Date( + Math.max(...dates.map((d) => d.getTime())) + ); + } else { + this.portfolioEvolutionXMin = undefined; + this.portfolioEvolutionXMax = undefined; + } + + if (this.investmentsByGroup && this.investmentsByGroup.length > 0) { + const dates = this.investmentsByGroup.map((item) => new Date(item.date)); + this.investmentTimelineXMin = new Date( + Math.min(...dates.map((d) => d.getTime())) + ); + this.investmentTimelineXMax = new Date( + Math.max(...dates.map((d) => d.getTime())) + ); + } else { + this.investmentTimelineXMin = undefined; + this.investmentTimelineXMax = undefined; + } + + if (this.dividendsByGroup && this.dividendsByGroup.length > 0) { + const dates = this.dividendsByGroup.map((item) => new Date(item.date)); + this.dividendTimelineXMin = new Date( + Math.min(...dates.map((d) => d.getTime())) + ); + this.dividendTimelineXMax = new Date( + Math.max(...dates.map((d) => d.getTime())) + ); + } else { + this.dividendTimelineXMin = undefined; + this.dividendTimelineXMax = undefined; + } + } } diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index d33d5e570..a45bc394c 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -354,6 +354,8 @@ [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isLoading]="isLoadingInvestmentChart" [locale]="user?.settings?.locale" + [xMax]="globalXMax" + [xMin]="globalXMin" /> @@ -411,6 +413,8 @@ [isLoading]="isLoadingInvestmentTimelineChart" [locale]="user?.settings?.locale" [savingsRate]="savingsRate" + [xMax]="globalXMax" + [xMin]="globalXMin" /> @@ -445,6 +449,8 @@ [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isLoading]="isLoadingDividendTimelineChart" [locale]="user?.settings?.locale" + [xMax]="globalXMax" + [xMin]="globalXMin" /> diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 0afef5959..d42875c88 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -66,6 +66,8 @@ export class GfLineChartComponent @Input() yMaxLabel: string; @Input() yMin: number; @Input() yMinLabel: string; + @Input() xMax: Date; + @Input() xMin: Date; @ViewChild('chartCanvas') chartCanvas; @@ -218,6 +220,12 @@ export class GfLineChartComponent grid: { display: false }, + max: this.xMax?.getTime(), + min: this.xMin?.getTime(), + bounds: 'data', + ticks: { + source: 'data' + }, time: { tooltipFormat: getDateFormatString(this.locale), unit: 'year' diff --git a/test-charts.html b/test-charts.html new file mode 100644 index 000000000..7b86770af --- /dev/null +++ b/test-charts.html @@ -0,0 +1,160 @@ + + + + + + Chart UI Test - Ghostfolio #3998 + + + + + +

Mock Frontend: Chart Visual Comparability Test (#3998)

+

+ This is a simple test page to verify chart scaling with mocked data. No + server needed. +

+ + +

Portfolio Performance Chart

+
+ +
+ + +

Investment Timeline Chart

+
+ +
+ + + +