From 35f6e15fec96715959fc590018bbeb88ea1e45d1 Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sat, 11 Oct 2025 21:22:18 +0530 Subject: [PATCH 1/3] Fix chart scaling for #3998 --- .../investment-chart.component.ts | 4 ++ .../analysis/analysis-page.component.ts | 59 +++++++++++++++++++ .../portfolio/analysis/analysis-page.html | 6 ++ .../lib/line-chart/line-chart.component.ts | 4 ++ 4 files changed, 73 insertions(+) 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..469d29f7c 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,6 +243,8 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { grid: { display: false }, + max: this.xMax?.getTime(), + min: this.xMin?.getTime(), type: 'time', time: { tooltipFormat: getDateFormatString(this.locale), 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 26d474f73..c8a7ed4b7 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,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { public performanceDataItems: HistoricalDataItem[]; public performanceDataItemsInPercentage: HistoricalDataItem[]; public portfolioEvolutionDataLabel = $localize`Investment`; + public portfolioEvolutionXMax: Date; + public portfolioEvolutionXMin: Date; public streaks: PortfolioInvestments['streaks']; public top3: PortfolioPosition[]; public unitCurrentStreak: string; @@ -241,6 +247,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { this.isLoadingDividendTimelineChart = false; + this.updateDateRanges(); + this.changeDetectorRef.markForCheck(); }); @@ -273,6 +281,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { this.isLoadingInvestmentTimelineChart = false; + this.updateDateRanges(); + this.changeDetectorRef.markForCheck(); }); } @@ -327,6 +337,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { this.updateBenchmarkDataItems(); + this.updateDateRanges(); + this.changeDetectorRef.markForCheck(); }); @@ -402,4 +414,51 @@ 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 + + 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 { + // Fallback to undefined if no data, allowing chart to use default scaling + 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..a0a878e49 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]="portfolioEvolutionXMax" + [xMin]="portfolioEvolutionXMin" /> @@ -411,6 +413,8 @@ [isLoading]="isLoadingInvestmentTimelineChart" [locale]="user?.settings?.locale" [savingsRate]="savingsRate" + [xMax]="investmentTimelineXMax" + [xMin]="investmentTimelineXMin" /> @@ -445,6 +449,8 @@ [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isLoading]="isLoadingDividendTimelineChart" [locale]="user?.settings?.locale" + [xMax]="dividendTimelineXMax" + [xMin]="dividendTimelineXMin" /> 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..bad9975fc 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,8 @@ export class GfLineChartComponent grid: { display: false }, + max: this.xMax?.getTime(), + min: this.xMin?.getTime(), time: { tooltipFormat: getDateFormatString(this.locale), unit: 'year' From 64c480da39cbc30d227f2d79d281271a9a0bc26d Mon Sep 17 00:00:00 2001 From: adityagarud Date: Mon, 13 Oct 2025 19:42:45 +0530 Subject: [PATCH 2/3] Fix chart X-axis synchronization for #3998 --- .../analysis/analysis-page.component.ts | 32 +++- .../portfolio/analysis/analysis-page.html | 12 +- test-charts.html | 160 ++++++++++++++++++ 3 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 test-charts.html 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 c8a7ed4b7..3a3ad9056 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 @@ -100,6 +100,8 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { public portfolioEvolutionDataLabel = $localize`Investment`; public portfolioEvolutionXMax: Date; public portfolioEvolutionXMin: Date; + public globalXMax: Date; + public globalXMin: Date; public streaks: PortfolioInvestments['streaks']; public top3: PortfolioPosition[]; public unitCurrentStreak: string; @@ -419,6 +421,35 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { // 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) @@ -430,7 +461,6 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit { Math.max(...dates.map((d) => d.getTime())) ); } else { - // Fallback to undefined if no data, allowing chart to use default scaling this.portfolioEvolutionXMin = undefined; this.portfolioEvolutionXMax = 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 a0a878e49..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,8 +354,8 @@ [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isLoading]="isLoadingInvestmentChart" [locale]="user?.settings?.locale" - [xMax]="portfolioEvolutionXMax" - [xMin]="portfolioEvolutionXMin" + [xMax]="globalXMax" + [xMin]="globalXMin" /> @@ -413,8 +413,8 @@ [isLoading]="isLoadingInvestmentTimelineChart" [locale]="user?.settings?.locale" [savingsRate]="savingsRate" - [xMax]="investmentTimelineXMax" - [xMin]="investmentTimelineXMin" + [xMax]="globalXMax" + [xMin]="globalXMin" /> @@ -449,8 +449,8 @@ [isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isLoading]="isLoadingDividendTimelineChart" [locale]="user?.settings?.locale" - [xMax]="dividendTimelineXMax" - [xMin]="dividendTimelineXMin" + [xMax]="globalXMax" + [xMin]="globalXMin" /> 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

+
+ +
+ + + + From 33737c18a3b8b99f5a61e4425517b5c022c3dae5 Mon Sep 17 00:00:00 2001 From: adityagarud Date: Sun, 19 Oct 2025 09:30:47 +0530 Subject: [PATCH 3/3] Fix Chart.js axis rounding to respect exact min/max dates for #3998 --- .../components/investment-chart/investment-chart.component.ts | 4 ++++ libs/ui/src/lib/line-chart/line-chart.component.ts | 4 ++++ 2 files changed, 8 insertions(+) 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 469d29f7c..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 @@ -246,6 +246,10 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { 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/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index bad9975fc..d42875c88 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -222,6 +222,10 @@ export class GfLineChartComponent }, max: this.xMax?.getTime(), min: this.xMin?.getTime(), + bounds: 'data', + ticks: { + source: 'data' + }, time: { tooltipFormat: getDateFormatString(this.locale), unit: 'year'