Browse Source

Feature/performance timeline on analysis page (#8103)

* Add performance timeline

* Update changelog
pull/8070/head^2
Thomas Kaul 11 hours ago
committed by GitHub
parent
commit
b60e7acde0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 27
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 42
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  4. 29
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  5. 7
      libs/ui/src/lib/services/data.service.ts

1
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

27
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,

42
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<GroupBy>('month');
protected readonly modeOptions: ToggleOption<GroupBy>[] = [
@ -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();
}

29
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -430,6 +430,35 @@
</div>
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div class="mb-5 row">
<div class="col-lg">
<div class="align-items-center d-flex mb-4">
<div
class="align-items-center d-flex flex-grow-1 h5 mb-0 text-truncate"
>
<span i18n>Performance Timeline</span>
@if (user?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" />
}
</div>
</div>
<div class="chart-container">
<gf-investment-chart
class="h-100"
groupBy="year"
[benchmarkDataItems]="performancePercentagesByYear"
[benchmarkDataLabel]="performanceTimelineDataLabel"
[colorScheme]="user?.settings?.colorScheme"
[isInPercentage]="true"
[isLoading]="isLoadingPerformanceTimelineChart"
[locale]="user?.settings?.locale"
/>
</div>
</div>
</div>
}
<div class="mb-5 row">
<div class="col-lg">
<div class="align-items-center d-flex mb-4">

7
libs/ui/src/lib/services/data.service.ts

@ -728,14 +728,21 @@ export class DataService {
public fetchPortfolioPerformance({
filters,
groupBy,
range,
withExcludedAccounts = false
}: {
filters?: Filter[];
groupBy?: Extract<GroupBy, 'year'>;
range: DateRange;
withExcludedAccounts?: boolean;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
if (groupBy) {
params = params.append('groupBy', groupBy);
}
params = params.append('range', range);
if (withExcludedAccounts) {

Loading…
Cancel
Save