Browse Source

Feature/add support for click in portfolio proportion chart (#729)

* Add support for click

* Update changelog
pull/731/head
Thomas Kaul 3 years ago
committed by GitHub
parent
commit
b3e58d182a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      CHANGELOG.md
  2. 16
      apps/api/src/app/admin/admin.service.ts
  3. 9
      apps/api/src/services/data-gathering.service.ts
  4. 9
      apps/api/src/services/market-data.service.ts
  5. 41
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  6. 12
      apps/client/src/app/components/positions-table/positions-table.component.ts
  7. 17
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  8. 2
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  9. 25
      apps/client/src/app/services/admin.service.ts
  10. 2
      libs/common/src/lib/interfaces/admin-data.interface.ts
  11. 2
      libs/common/src/lib/interfaces/index.ts
  12. 6
      libs/common/src/lib/interfaces/unique-asset.interface.ts
  13. 9
      libs/ui/src/lib/activities-table/activities-table.component.ts
  14. 24
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

6
CHANGELOG.md

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Added support for click in the portfolio proportion chart component
## 1.121.0 - 27.02.2022 ## 1.121.0 - 27.02.2022
### Added ### Added

16
apps/api/src/app/admin/admin.service.ts

@ -11,7 +11,8 @@ import {
AdminData, AdminData,
AdminMarketData, AdminMarketData,
AdminMarketDataDetails, AdminMarketDataDetails,
AdminMarketDataItem AdminMarketDataItem,
UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, Property } from '@prisma/client'; import { DataSource, Property } from '@prisma/client';
@ -30,13 +31,7 @@ export class AdminService {
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async deleteProfileData({ public async deleteProfileData({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
await this.symbolProfileService.delete({ dataSource, symbol }); await this.symbolProfileService.delete({ dataSource, symbol });
} }
@ -137,10 +132,7 @@ export class AdminService {
public async getMarketDataBySymbol({ public async getMarketDataBySymbol({
dataSource, dataSource,
symbol symbol
}: { }: UniqueAsset): Promise<AdminMarketDataDetails> {
dataSource: DataSource;
symbol: string;
}): Promise<AdminMarketDataDetails> {
return { return {
marketData: await this.marketDataService.marketDataItems({ marketData: await this.marketDataService.marketDataItems({
orderBy: { orderBy: {

9
apps/api/src/services/data-gathering.service.ts

@ -4,6 +4,7 @@ import {
PROPERTY_LOCKED_DATA_GATHERING PROPERTY_LOCKED_DATA_GATHERING
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { import {
@ -121,13 +122,7 @@ export class DataGatheringService {
} }
} }
public async gatherSymbol({ public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
const isDataGatheringLocked = await this.prismaService.property.findUnique({ const isDataGatheringLocked = await this.prismaService.property.findUnique({
where: { key: PROPERTY_LOCKED_DATA_GATHERING } where: { key: PROPERTY_LOCKED_DATA_GATHERING }
}); });

9
apps/api/src/services/market-data.service.ts

@ -2,6 +2,7 @@ import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-dat
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface'; import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { DataSource, MarketData, Prisma } from '@prisma/client'; import { DataSource, MarketData, Prisma } from '@prisma/client';
@ -9,13 +10,7 @@ import { DataSource, MarketData, Prisma } from '@prisma/client';
export class MarketDataService { export class MarketDataService {
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async deleteMany({ public async deleteMany({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.prismaService.marketData.deleteMany({ return this.prismaService.marketData.deleteMany({
where: { where: {
dataSource, dataSource,

41
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -8,6 +8,7 @@ import {
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@ -44,39 +45,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
this.fetchAdminMarketData(); this.fetchAdminMarketData();
} }
public onDeleteProfileData({ public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.deleteProfileData({ dataSource, symbol }) .deleteProfileData({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherProfileDataBySymbol({ public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.gatherProfileDataBySymbol({ dataSource, symbol }) .gatherProfileDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {}); .subscribe(() => {});
} }
public onGatherSymbol({ public onGatherSymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.gatherSymbol({ dataSource, symbol }) .gatherSymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -93,13 +76,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
} }
} }
public setCurrentProfile({ public setCurrentProfile({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.marketDataDetails = []; this.marketDataDetails = [];
if (this.currentSymbol === symbol) { if (this.currentSymbol === symbol) {
@ -129,13 +106,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
}); });
} }
private fetchAdminMarketDataBySymbol({ private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.adminService this.adminService
.fetchAdminMarketDataBySymbol({ dataSource, symbol }) .fetchAdminMarketDataBySymbol({ dataSource, symbol })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

12
apps/client/src/app/components/positions-table/positions-table.component.ts

@ -13,8 +13,8 @@ import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { AssetClass, DataSource, Order as OrderModel } from '@prisma/client'; import { AssetClass, Order as OrderModel } from '@prisma/client';
import { Subject, Subscription } from 'rxjs'; import { Subject, Subscription } from 'rxjs';
@Component({ @Component({
@ -75,13 +75,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
this.dataSource.filter = filterValue.trim().toLowerCase(); this.dataSource.filter = filterValue.trim().toLowerCase();
}*/ }*/
public onOpenPositionDialog({ public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}): void {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true } queryParams: { dataSource, symbol, positionDetailDialog: true }
}); });

17
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -10,6 +10,7 @@ import { prettifySymbol } from '@ghostfolio/common/helper';
import { import {
PortfolioDetails, PortfolioDetails,
PortfolioPosition, PortfolioPosition,
UniqueAsset,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -64,7 +65,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
public symbols: { public symbols: {
[name: string]: { name: string; symbol: string; value: number }; [name: string]: {
dataSource?: DataSource;
name: string;
symbol: string;
value: number;
};
}; };
public user: User; public user: User;
@ -281,6 +287,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
if (position.assetClass === AssetClass.EQUITY) { if (position.assetClass === AssetClass.EQUITY) {
this.symbols[prettifySymbol(symbol)] = { this.symbols[prettifySymbol(symbol)] = {
dataSource: position.dataSource,
name: position.name, name: position.name,
symbol: prettifySymbol(symbol), symbol: prettifySymbol(symbol),
value: aPeriod === 'original' ? position.investment : position.value value: aPeriod === 'original' ? position.investment : position.value
@ -295,6 +302,14 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.initializeAnalysisData(this.period); this.initializeAnalysisData(this.period);
} }
public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) {
if (dataSource && symbol) {
this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true }
});
}
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

2
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -89,12 +89,14 @@
<mat-card-content> <mat-card-content>
<gf-portfolio-proportion-chart <gf-portfolio-proportion-chart
class="mx-auto" class="mx-auto"
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['symbol']" [keys]="['symbol']"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[positions]="symbols" [positions]="symbols"
[showLabels]="deviceType !== 'mobile'" [showLabels]="deviceType !== 'mobile'"
(proportionChartClicked)="onProportionChartClicked($event)"
></gf-portfolio-proportion-chart> ></gf-portfolio-proportion-chart>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

25
apps/client/src/app/services/admin.service.ts

@ -3,7 +3,10 @@ import { Injectable } from '@angular/core';
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto'; import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { AdminMarketDataDetails } from '@ghostfolio/common/interfaces'; import {
AdminMarketDataDetails,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { Observable, map } from 'rxjs'; import { Observable, map } from 'rxjs';
@ -14,13 +17,7 @@ import { Observable, map } from 'rxjs';
export class AdminService { export class AdminService {
public constructor(private http: HttpClient) {} public constructor(private http: HttpClient) {}
public deleteProfileData({ public deleteProfileData({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.delete<void>( return this.http.delete<void>(
`/api/admin/profile-data/${dataSource}/${symbol}` `/api/admin/profile-data/${dataSource}/${symbol}`
); );
@ -53,13 +50,7 @@ export class AdminService {
return this.http.post<void>(`/api/admin/gather/profile-data`, {}); return this.http.post<void>(`/api/admin/gather/profile-data`, {});
} }
public gatherProfileDataBySymbol({ public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http.post<void>( return this.http.post<void>(
`/api/admin/gather/profile-data/${dataSource}/${symbol}`, `/api/admin/gather/profile-data/${dataSource}/${symbol}`,
{} {}
@ -70,10 +61,8 @@ export class AdminService {
dataSource, dataSource,
date, date,
symbol symbol
}: { }: UniqueAsset & {
dataSource: DataSource;
date?: Date; date?: Date;
symbol: string;
}) { }) {
let url = `/api/admin/gather/${dataSource}/${symbol}`; let url = `/api/admin/gather/${dataSource}/${symbol}`;

2
libs/common/src/lib/interfaces/admin-data.interface.ts

@ -1,5 +1,3 @@
import { Property } from '@prisma/client';
export interface AdminData { export interface AdminData {
dataGatheringProgress?: number; dataGatheringProgress?: number;
exchangeRates: { label1: string; label2: string; value: number }[]; exchangeRates: { label1: string; label2: string; value: number }[];

2
libs/common/src/lib/interfaces/index.ts

@ -22,6 +22,7 @@ import { PortfolioReport } from './portfolio-report.interface';
import { PortfolioSummary } from './portfolio-summary.interface'; import { PortfolioSummary } from './portfolio-summary.interface';
import { Position } from './position.interface'; import { Position } from './position.interface';
import { TimelinePosition } from './timeline-position.interface'; import { TimelinePosition } from './timeline-position.interface';
import { UniqueAsset } from './unique-asset.interface';
import { UserSettings } from './user-settings.interface'; import { UserSettings } from './user-settings.interface';
import { UserWithSettings } from './user-with-settings'; import { UserWithSettings } from './user-with-settings';
import { User } from './user.interface'; import { User } from './user.interface';
@ -49,6 +50,7 @@ export {
PortfolioSummary, PortfolioSummary,
Position, Position,
TimelinePosition, TimelinePosition,
UniqueAsset,
User, User,
UserSettings, UserSettings,
UserWithSettings UserWithSettings

6
libs/common/src/lib/interfaces/unique-asset.interface.ts

@ -0,0 +1,6 @@
import { DataSource } from '@prisma/client';
export interface UniqueAsset {
dataSource: DataSource;
symbol: string;
}

9
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -21,6 +21,7 @@ import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
@ -199,13 +200,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
this.import.emit(); this.import.emit();
} }
public onOpenPositionDialog({ public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}): void {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true } queryParams: { dataSource, symbol, positionDetailDialog: true }
}); });

24
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -3,14 +3,17 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef, ElementRef,
EventEmitter,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getTextColor } from '@ghostfolio/common/helper'; import { getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { Tooltip } from 'chart.js'; import { Tooltip } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
@ -30,6 +33,7 @@ export class PortfolioProportionChartComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
{ {
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() cursor: string;
@Input() isInPercent = false; @Input() isInPercent = false;
@Input() keys: string[] = []; @Input() keys: string[] = [];
@Input() locale = ''; @Input() locale = '';
@ -37,11 +41,14 @@ export class PortfolioProportionChartComponent
@Input() showLabels = false; @Input() showLabels = false;
@Input() positions: { @Input() positions: {
[symbol: string]: Pick<PortfolioPosition, 'type'> & { [symbol: string]: Pick<PortfolioPosition, 'type'> & {
dataSource?: DataSource;
name: string; name: string;
value: number; value: number;
}; };
} = {}; } = {};
@Output() proportionChartClicked = new EventEmitter<UniqueAsset>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart; public chart: Chart;
@ -256,6 +263,21 @@ export class PortfolioProportionChartComponent
layout: { layout: {
padding: this.showLabels === true ? 100 : 0 padding: this.showLabels === true ? 100 : 0
}, },
onClick: (event, activeElements) => {
const dataIndex = activeElements[0].index;
const symbol: string = event.chart.data.labels[dataIndex];
const dataSource = this.positions[symbol]?.dataSource;
this.proportionChartClicked.emit({ dataSource, symbol });
},
onHover: (event, chartElement) => {
if (this.cursor) {
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
}
},
plugins: { plugins: {
datalabels: { datalabels: {
color: (context) => { color: (context) => {

Loading…
Cancel
Save