+
Platform
@if (selectedPlatform) {
@@ -51,8 +51,8 @@
class="ml-3"
matPrefix
[hasPlaceholder]="true"
- [tooltip]="selectedPlatform?.name"
- [url]="selectedPlatform?.url"
+ [tooltip]="selectedPlatform.name ?? ''"
+ [url]="selectedPlatform.url"
/>
}
{{ platformEntry.name }}
diff --git a/apps/client/src/app/util/date.util.ts b/apps/client/src/app/util/date.util.ts
index a7dd0afd8c..02e42c529a 100644
--- a/apps/client/src/app/util/date.util.ts
+++ b/apps/client/src/app/util/date.util.ts
@@ -10,16 +10,17 @@ export function formatRelativeTime({
date,
language
}: {
- date: string;
+ date: Date | string | null | undefined;
language?: string;
}) {
if (date) {
- const distanceString = formatDistanceToNowStrict(parseISO(date), {
+ const parsedDate = typeof date === 'string' ? parseISO(date) : date;
+ const distanceString = formatDistanceToNowStrict(parsedDate, {
addSuffix: true,
locale: getDateFnsLocale(language)
});
- return Math.abs(differenceInSeconds(parseISO(date), new Date())) < 60
+ return Math.abs(differenceInSeconds(parsedDate, new Date())) < 60
? $localize`just now`
: distanceString;
}
From f22e896c34e781306759e41f2eb5c21f035db184 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 10 Oct 2026 13:25:33 +0200
Subject: [PATCH 5/6] Task/harmonize colors in investment chart component
(#8105)
* Harmonize colors
* Update changelog
---
CHANGELOG.md | 1 +
.../investment-chart.component.ts | 23 +++++++++++--------
2 files changed, 15 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92c970aa6a..6cc09560af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
+- Harmonized the colors in the investment chart component
- Improved the language localization for Chinese (`zh`)
## 3.82.0 - 2026-10-09
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 34500422ed..8b74a74d4c 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
@@ -113,10 +113,15 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}
private initialize() {
- // Create a clone
+ const isBarChart = !!this.groupBy;
+
+ const benchmarkColorRgb = isBarChart ? primaryColorRgb : secondaryColorRgb;
+
+ // Create clones of the input data
this.investments = this.benchmarkDataItems.map((item) =>
Object.assign({}, item)
);
+
this.values = this.historicalDataItems.map((item) =>
Object.assign({}, item)
);
@@ -127,9 +132,9 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}),
datasets: [
{
- backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
- borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
- borderWidth: this.groupBy ? 0 : 1,
+ backgroundColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`,
+ borderColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`,
+ borderWidth: isBarChart ? 0 : 1,
data: this.investments.map(({ date, investment }) => {
return {
x: parseDate(date)?.getTime() ?? null,
@@ -141,7 +146,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
borderColor: (context) =>
this.isInFuture(
context,
- `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)`
+ `rgba(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b}, 0.67)`
),
borderDash: (context) => this.isInFuture(context, [2, 2])
},
@@ -200,10 +205,10 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
annotations: {
savingsRate: this.savingsRate
? {
- borderColor: `rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.75)`,
+ borderColor: `rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.75)`,
borderWidth: 1,
label: {
- backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
+ backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderRadius: 2,
color: 'white',
content: $localize`Savings Rate`,
@@ -234,7 +239,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
responsive: true,
scales: {
x: getTimeAxisOptions({
- borderWidth: this.groupBy ? 0 : 1,
+ borderWidth: isBarChart ? 0 : 1,
colorScheme: this.colorScheme,
locale: this.locale
}),
@@ -250,7 +255,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
plugins: [
getVerticalHoverLinePlugin(this.chartCanvas(), this.colorScheme)
],
- type: this.groupBy ? 'bar' : 'line'
+ type: isBarChart ? 'bar' : 'line'
}
);
}
From b60e7acde04baf0c597677ee5d6e7f3e6cb75658 Mon Sep 17 00:00:00 2001
From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
Date: Sat, 10 Oct 2026 13:26:36 +0200
Subject: [PATCH 6/6] Feature/performance timeline on analysis page (#8103)
* Add performance timeline
* Update changelog
---
CHANGELOG.md | 1 +
.../src/app/portfolio/portfolio.controller.ts | 27 ++++++++----
.../analysis/analysis-page.component.ts | 42 +++++++++++++++++++
.../portfolio/analysis/analysis-page.html | 29 +++++++++++++
libs/ui/src/lib/services/data.service.ts | 7 ++++
5 files changed, 97 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6cc09560af..b9cbcd7e21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+- Added the performance timeline grouped by year to the analysis page (experimental)
- Extended the `GET api/v2/portfolio/performance` endpoint by the `groupBy` query parameter (`year`)
### Changed
diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts
index 109b5cca59..72468f1d6e 100644
--- a/apps/api/src/app/portfolio/portfolio.controller.ts
+++ b/apps/api/src/app/portfolio/portfolio.controller.ts
@@ -4,7 +4,8 @@ import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorato
import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator';
import {
hasNotDefinedValuesInObject,
- nullifyValuesInObject
+ nullifyValuesInObject,
+ nullifyValuesInObjects
} from '@ghostfolio/api/helper/object.helper';
import { convertValuesToPercentages } from '@ghostfolio/api/helper/portfolio.helper';
import { PerformanceLoggingInterceptor } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
@@ -583,14 +584,22 @@ export class PortfolioController {
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription?.type === SubscriptionType.Basic
) {
- performanceInformation.chart = performanceInformation.chart.map(
- (item) => {
- return nullifyValuesInObject(item, [
- 'dividendInBaseCurrency',
- 'totalInvestment',
- 'value'
- ]);
- }
+ performanceInformation.chart = nullifyValuesInObjects(
+ performanceInformation.chart,
+ [
+ 'dividendInBaseCurrency',
+ 'totalInvestment',
+ 'value',
+ ...(groupBy === 'year'
+ ? [
+ 'investmentValueWithCurrencyEffect',
+ 'netPerformance',
+ 'netPerformanceInPercentage',
+ 'netPerformanceInPercentageWithCurrencyEffect',
+ 'netPerformanceWithCurrencyEffect'
+ ]
+ : [])
+ ]
);
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
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 460ac9de8e..26e52678c7 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
@@ -5,6 +5,7 @@ import {
DEFAULT_DATE_RANGE,
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config';
+import { SubscriptionType } from '@ghostfolio/common/enums';
import { canOpenHoldingDetail } from '@ghostfolio/common/helper';
import {
InvestmentItem,
@@ -93,6 +94,7 @@ export class GfAnalysisPageComponent implements OnInit {
protected isLoadingDividendTimelineChart: boolean;
protected isLoadingInvestmentChart: boolean;
protected isLoadingInvestmentTimelineChart: boolean;
+ protected isLoadingPerformanceTimelineChart: boolean;
protected isLoadingPortfolioPrompt: boolean;
protected readonly mode = signal('month');
protected readonly modeOptions: ToggleOption[] = [
@@ -103,6 +105,8 @@ export class GfAnalysisPageComponent implements OnInit {
protected readonly PerformanceCalculationType = PerformanceCalculationType;
protected performanceDataItems: LineChartItem[];
protected performanceDataItemsInPercentage: LineChartItem[];
+ protected performancePercentagesByYear: InvestmentItem[];
+ protected readonly performanceTimelineDataLabel = $localize`Net Performance`;
protected readonly portfolioEvolutionDataLabel = $localize`Invested Capital`;
protected precision = 2;
protected savingsRatePerMonth: number | undefined;
@@ -312,6 +316,37 @@ export class GfAnalysisPageComponent implements OnInit {
);
}
+ private fetchPerformanceByYear() {
+ this.isLoadingPerformanceTimelineChart = true;
+
+ this.dataService
+ .fetchPortfolioPerformance({
+ filters: this.userService.getFilters(),
+ groupBy: 'year',
+ range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE
+ })
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(({ chart }) => {
+ this.performancePercentagesByYear = [];
+
+ for (const {
+ date,
+ netPerformanceInPercentageWithCurrencyEffect
+ } of chart ?? []) {
+ if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) {
+ this.performancePercentagesByYear.push({
+ date,
+ investment: netPerformanceInPercentageWithCurrencyEffect
+ });
+ }
+ }
+
+ this.isLoadingPerformanceTimelineChart = false;
+
+ this.changeDetectorRef.markForCheck();
+ });
+ }
+
private update() {
this.isLoadingInvestmentChart = true;
@@ -420,6 +455,13 @@ export class GfAnalysisPageComponent implements OnInit {
this.fetchDividendsAndInvestments();
+ if (
+ this.user?.settings?.isExperimentalFeatures &&
+ this.user?.subscription?.type !== SubscriptionType.Basic
+ ) {
+ this.fetchPerformanceByYear();
+ }
+
this.changeDetectorRef.markForCheck();
}
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 c39b774f4b..6f05dae069 100644
--- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
+++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html
@@ -430,6 +430,35 @@
+ @if (user?.settings?.isExperimentalFeatures) {
+