Feature/improve analysis page (#609)

* Improve analysis page (show y-axis, extend chart in relation to days in market)

* Update changelog
This commit is contained in:
Thomas Kaul 2022-01-01 12:09:49 +01:00 committed by GitHub
parent 0179823ad9
commit e54638a684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 122 additions and 80 deletions

View File

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the portfolio analysis page: show the y-axis and extend the chart in relation to the days in market
- Restructured the about page
- Start refactoring _transactions_ to _activities_
- Refactored the demo user id

View File

@ -10,12 +10,12 @@ import { baseCurrency } from '@ghostfolio/common/config';
import {
PortfolioChart,
PortfolioDetails,
PortfolioInvestments,
PortfolioPerformance,
PortfolioPublicDetails,
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
@ -48,42 +48,6 @@ export class PortfolioController {
private readonly userService: UserService
) {}
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async findAll(
@Headers('impersonation-id') impersonationId: string,
@Res() res: Response
): Promise<InvestmentItem[]> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json([]);
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const maxInvestment = investments.reduce(
(investment, item) => Math.max(investment, item.investment),
1
);
investments = investments.map((item) => ({
date: item.date,
investment: item.investment / maxInvestment
}));
}
return <any>res.json(investments);
}
@Get('chart')
@UseGuards(AuthGuard('jwt'))
public async getChart(
@ -200,6 +164,42 @@ export class PortfolioController {
return <any>res.json({ accounts, hasError, holdings });
}
@Get('investments')
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string,
@Res() res: Response
): Promise<PortfolioInvestments> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
) {
res.status(StatusCodes.FORBIDDEN);
return <any>res.json({});
}
let investments = await this.portfolioService.getInvestments(
impersonationId
);
if (
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const maxInvestment = investments.reduce(
(investment, item) => Math.max(investment, item.investment),
1
);
investments = investments.map((item) => ({
date: item.date,
investment: item.investment / maxInvestment
}));
}
return <any>res.json({ firstOrderDate: investments[0]?.date, investments });
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
public async getPerformance(

View File

@ -55,7 +55,7 @@ import {
subDays,
subYears
} from 'date-fns';
import { isEmpty } from 'lodash';
import { isEmpty, sortBy } from 'lodash';
import {
HistoricalDataContainer,
@ -150,12 +150,33 @@ export class PortfolioService {
return [];
}
return portfolioCalculator.getInvestments().map((item) => {
const investments = portfolioCalculator.getInvestments().map((item) => {
return {
date: item.date,
investment: item.investment.toNumber()
};
});
// Add investment of today
const investmentOfToday = investments.filter((investment) => {
return investment.date === format(new Date(), DATE_FORMAT);
});
if (investmentOfToday.length <= 0) {
const pastInvestments = investments.filter((investment) => {
return isBefore(parseDate(investment.date), new Date());
});
const lastInvestment = pastInvestments[pastInvestments.length - 1];
investments.push({
date: format(new Date(), DATE_FORMAT),
investment: lastInvestment?.investment ?? 0
});
}
return sortBy(investments, (investment) => {
return investment.date;
});
}
public async getChart(

View File

@ -10,6 +10,7 @@ import {
ViewChild
} from '@angular/core';
import { primaryColorRgb } from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import {
Chart,
@ -19,7 +20,7 @@ import {
PointElement,
TimeScale
} from 'chart.js';
import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
@Component({
selector: 'gf-investment-chart',
@ -27,8 +28,10 @@ import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
templateUrl: './investment-chart.component.html',
styleUrls: ['./investment-chart.component.scss']
})
export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() daysInMarket: number;
@Input() investments: InvestmentItem[];
@Input() isInPercent = false;
@ViewChild('chartCanvas') chartCanvas;
@ -45,8 +48,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
);
}
public ngOnInit() {}
public ngOnChanges() {
if (this.investments) {
this.initialize();
@ -61,19 +62,25 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
this.isLoading = true;
if (this.investments?.length > 0) {
// Extend chart by three months (before)
// Extend chart by 5% of days in market (before)
const firstItem = this.investments[0];
this.investments.unshift({
...firstItem,
date: subMonths(parseISO(firstItem.date), 3).toISOString(),
date: subDays(
parseISO(firstItem.date),
this.daysInMarket * 0.05 || 90
).toISOString(),
investment: 0
});
// Extend chart by three months (after)
// Extend chart by 5% of days in market (after)
const lastItem = this.investments[this.investments.length - 1];
this.investments.push({
...lastItem,
date: addMonths(new Date(), 3).toISOString()
date: addDays(
parseDate(lastItem.date),
this.daysInMarket * 0.05 || 90
).toISOString()
});
}
@ -136,12 +143,26 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
}
},
y: {
display: false,
display: !this.isInPercent,
grid: {
display: false
},
ticks: {
display: false
display: true,
callback: (tickValue, index, ticks) => {
if (index === 0 || index === ticks.length - 1) {
// Only print last and first legend entry
if (typeof tickValue === 'number') {
return tickValue.toFixed(2);
}
return tickValue;
}
return '';
},
mirror: true,
z: 1
}
}
}

View File

@ -7,7 +7,6 @@ import { InvestmentChartComponent } from './investment-chart.component';
@NgModule({
declarations: [InvestmentChartComponent],
exports: [InvestmentChartComponent],
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: []
imports: [CommonModule, NgxSkeletonLoaderModule]
})
export class GfInvestmentChartModule {}

View File

@ -2,9 +2,9 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
import { User } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { ToggleOption } from '@ghostfolio/common/types';
import { differenceInDays } from 'date-fns';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -16,28 +16,10 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './analysis-page.html'
})
export class AnalysisPageComponent implements OnDestroy, OnInit {
public accounts: {
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
};
public continents: {
[code: string]: { name: string; value: number };
};
public countries: {
[code: string]: { name: string; value: number };
};
public daysInMarket: number;
public deviceType: string;
public hasImpersonationId: boolean;
public period = 'current';
public periodOptions: ToggleOption[] = [
{ label: 'Initial', value: 'original' },
{ label: 'Current', value: 'current' }
];
public investments: InvestmentItem[];
public portfolioPositions: { [symbol: string]: PortfolioPosition };
public positions: { [symbol: string]: any };
public sectors: {
[name: string]: { name: string; value: number };
};
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -69,8 +51,9 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchInvestments()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.investments = response;
.subscribe(({ firstOrderDate, investments }) => {
this.daysInMarket = differenceInDays(new Date(), firstOrderDate);
this.investments = investments;
this.changeDetectorRef.markForCheck();
});

View File

@ -11,6 +11,8 @@
<mat-card-content>
<gf-investment-chart
class="h-100"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
></gf-investment-chart>
</mat-card-content>

View File

@ -5,7 +5,6 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
@ -23,13 +22,13 @@ import {
InfoItem,
PortfolioChart,
PortfolioDetails,
PortfolioInvestments,
PortfolioPerformance,
PortfolioPublicDetails,
PortfolioReport,
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { DataSource, Order as OrderModel } from '@prisma/client';
@ -124,6 +123,18 @@ export class DataService {
return info;
}
public fetchInvestments(): Observable<PortfolioInvestments> {
return this.http.get<any>('/api/portfolio/investments').pipe(
map((response) => {
if (response.firstOrderDate) {
response.firstOrderDate = parseISO(response.firstOrderDate);
}
return response;
})
);
}
public fetchSymbolItem({
dataSource,
includeHistoricalData = false,
@ -170,10 +181,6 @@ export class DataService {
);
}
public fetchInvestments() {
return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
}
public fetchPortfolioDetails(aParams: { [param: string]: any }) {
return this.http.get<PortfolioDetails>('/api/portfolio/details', {
params: aParams

View File

@ -8,6 +8,7 @@ import { Export } from './export.interface';
import { InfoItem } from './info-item.interface';
import { PortfolioChart } from './portfolio-chart.interface';
import { PortfolioDetails } from './portfolio-details.interface';
import { PortfolioInvestments } from './portfolio-investments.interface';
import { PortfolioItem } from './portfolio-item.interface';
import { PortfolioOverview } from './portfolio-overview.interface';
import { PortfolioPerformance } from './portfolio-performance.interface';
@ -33,6 +34,7 @@ export {
InfoItem,
PortfolioChart,
PortfolioDetails,
PortfolioInvestments,
PortfolioItem,
PortfolioOverview,
PortfolioPerformance,

View File

@ -0,0 +1,6 @@
import { InvestmentItem } from './investment-item.interface';
export interface PortfolioInvestments {
firstOrderDate: Date;
investments: InvestmentItem[];
}