From b3e58d182ab6c0c2b181679a9e38c00df27fbd46 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 28 Feb 2022 21:35:52 +0100 Subject: [PATCH] Feature/add support for click in portfolio proportion chart (#729) * Add support for click * Update changelog --- CHANGELOG.md | 6 +++ apps/api/src/app/admin/admin.service.ts | 16 ++------ .../src/services/data-gathering.service.ts | 9 +--- apps/api/src/services/market-data.service.ts | 9 +--- .../admin-market-data.component.ts | 41 +++---------------- .../positions-table.component.ts | 12 ++---- .../allocations/allocations-page.component.ts | 17 +++++++- .../allocations/allocations-page.html | 2 + apps/client/src/app/services/admin.service.ts | 25 ++++------- .../lib/interfaces/admin-data.interface.ts | 2 - libs/common/src/lib/interfaces/index.ts | 2 + .../lib/interfaces/unique-asset.interface.ts | 6 +++ .../activities-table.component.ts | 9 +--- .../portfolio-proportion-chart.component.ts | 24 ++++++++++- 14 files changed, 81 insertions(+), 99 deletions(-) create mode 100644 libs/common/src/lib/interfaces/unique-asset.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b6bfa09..267584c2b 100644 --- a/CHANGELOG.md +++ b/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/), 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 ### Added diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 9c1de05b0..c1be0bedb 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -11,7 +11,8 @@ import { AdminData, AdminMarketData, AdminMarketDataDetails, - AdminMarketDataItem + AdminMarketDataItem, + UniqueAsset } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { DataSource, Property } from '@prisma/client'; @@ -30,13 +31,7 @@ export class AdminService { private readonly symbolProfileService: SymbolProfileService ) {} - public async deleteProfileData({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public async deleteProfileData({ dataSource, symbol }: UniqueAsset) { await this.marketDataService.deleteMany({ dataSource, symbol }); await this.symbolProfileService.delete({ dataSource, symbol }); } @@ -137,10 +132,7 @@ export class AdminService { public async getMarketDataBySymbol({ dataSource, symbol - }: { - dataSource: DataSource; - symbol: string; - }): Promise { + }: UniqueAsset): Promise { return { marketData: await this.marketDataService.marketDataItems({ orderBy: { diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index f1757e462..9292709c3 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -4,6 +4,7 @@ import { PROPERTY_LOCKED_DATA_GATHERING } from '@ghostfolio/common/config'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { DataSource } from '@prisma/client'; import { @@ -121,13 +122,7 @@ export class DataGatheringService { } } - public async gatherSymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public async gatherSymbol({ dataSource, symbol }: UniqueAsset) { const isDataGatheringLocked = await this.prismaService.property.findUnique({ where: { key: PROPERTY_LOCKED_DATA_GATHERING } }); diff --git a/apps/api/src/services/market-data.service.ts b/apps/api/src/services/market-data.service.ts index 582ee2593..0afb5a811 100644 --- a/apps/api/src/services/market-data.service.ts +++ b/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 { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { resetHours } from '@ghostfolio/common/helper'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { Injectable } from '@nestjs/common'; import { DataSource, MarketData, Prisma } from '@prisma/client'; @@ -9,13 +10,7 @@ import { DataSource, MarketData, Prisma } from '@prisma/client'; export class MarketDataService { public constructor(private readonly prismaService: PrismaService) {} - public async deleteMany({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public async deleteMany({ dataSource, symbol }: UniqueAsset) { return this.prismaService.marketData.deleteMany({ where: { dataSource, diff --git a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts b/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts index 5dd3a3fd2..a2900ae6b 100644 --- a/apps/client/src/app/components/admin-market-data/admin-market-data.component.ts +++ b/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 { DataService } from '@ghostfolio/client/services/data.service'; 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 { DataSource, MarketData } from '@prisma/client'; import { Subject } from 'rxjs'; @@ -44,39 +45,21 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { this.fetchAdminMarketData(); } - public onDeleteProfileData({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public onDeleteProfileData({ dataSource, symbol }: UniqueAsset) { this.adminService .deleteProfileData({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => {}); } - public onGatherProfileDataBySymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public onGatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { this.adminService .gatherProfileDataBySymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => {}); } - public onGatherSymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public onGatherSymbol({ dataSource, symbol }: UniqueAsset) { this.adminService .gatherSymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) @@ -93,13 +76,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { } } - public setCurrentProfile({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public setCurrentProfile({ dataSource, symbol }: UniqueAsset) { this.marketDataDetails = []; if (this.currentSymbol === symbol) { @@ -129,13 +106,7 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit { }); } - private fetchAdminMarketDataBySymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + private fetchAdminMarketDataBySymbol({ dataSource, symbol }: UniqueAsset) { this.adminService .fetchAdminMarketDataBySymbol({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) diff --git a/apps/client/src/app/components/positions-table/positions-table.component.ts b/apps/client/src/app/components/positions-table/positions-table.component.ts index d338244da..9dadb27df 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.ts +++ b/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 { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; -import { PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { AssetClass, DataSource, Order as OrderModel } from '@prisma/client'; +import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; +import { AssetClass, Order as OrderModel } from '@prisma/client'; import { Subject, Subscription } from 'rxjs'; @Component({ @@ -75,13 +75,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { this.dataSource.filter = filterValue.trim().toLowerCase(); }*/ - public onOpenPositionDialog({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }): void { + public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void { this.router.navigate([], { queryParams: { dataSource, symbol, positionDetailDialog: true } }); diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 5cf555049..2ca5f6930 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -10,6 +10,7 @@ import { prettifySymbol } from '@ghostfolio/common/helper'; import { PortfolioDetails, PortfolioPosition, + UniqueAsset, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -64,7 +65,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { [name: string]: { name: string; value: number }; }; public symbols: { - [name: string]: { name: string; symbol: string; value: number }; + [name: string]: { + dataSource?: DataSource; + name: string; + symbol: string; + value: number; + }; }; public user: User; @@ -281,6 +287,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (position.assetClass === AssetClass.EQUITY) { this.symbols[prettifySymbol(symbol)] = { + dataSource: position.dataSource, name: position.name, symbol: prettifySymbol(symbol), value: aPeriod === 'original' ? position.investment : position.value @@ -295,6 +302,14 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.initializeAnalysisData(this.period); } + public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) { + if (dataSource && symbol) { + this.router.navigate([], { + queryParams: { dataSource, symbol, positionDetailDialog: true } + }); + } + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 21b5618d5..276de32ce 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -89,12 +89,14 @@ diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index ff6e624d0..cf9b36b1f 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/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 { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; 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 { format, parseISO } from 'date-fns'; import { Observable, map } from 'rxjs'; @@ -14,13 +17,7 @@ import { Observable, map } from 'rxjs'; export class AdminService { public constructor(private http: HttpClient) {} - public deleteProfileData({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public deleteProfileData({ dataSource, symbol }: UniqueAsset) { return this.http.delete( `/api/admin/profile-data/${dataSource}/${symbol}` ); @@ -53,13 +50,7 @@ export class AdminService { return this.http.post(`/api/admin/gather/profile-data`, {}); } - public gatherProfileDataBySymbol({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }) { + public gatherProfileDataBySymbol({ dataSource, symbol }: UniqueAsset) { return this.http.post( `/api/admin/gather/profile-data/${dataSource}/${symbol}`, {} @@ -70,10 +61,8 @@ export class AdminService { dataSource, date, symbol - }: { - dataSource: DataSource; + }: UniqueAsset & { date?: Date; - symbol: string; }) { let url = `/api/admin/gather/${dataSource}/${symbol}`; diff --git a/libs/common/src/lib/interfaces/admin-data.interface.ts b/libs/common/src/lib/interfaces/admin-data.interface.ts index a061269e7..ce90dccc5 100644 --- a/libs/common/src/lib/interfaces/admin-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-data.interface.ts @@ -1,5 +1,3 @@ -import { Property } from '@prisma/client'; - export interface AdminData { dataGatheringProgress?: number; exchangeRates: { label1: string; label2: string; value: number }[]; diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 5a0c71590..feeaaabd4 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -22,6 +22,7 @@ import { PortfolioReport } from './portfolio-report.interface'; import { PortfolioSummary } from './portfolio-summary.interface'; import { Position } from './position.interface'; import { TimelinePosition } from './timeline-position.interface'; +import { UniqueAsset } from './unique-asset.interface'; import { UserSettings } from './user-settings.interface'; import { UserWithSettings } from './user-with-settings'; import { User } from './user.interface'; @@ -49,6 +50,7 @@ export { PortfolioSummary, Position, TimelinePosition, + UniqueAsset, User, UserSettings, UserWithSettings diff --git a/libs/common/src/lib/interfaces/unique-asset.interface.ts b/libs/common/src/lib/interfaces/unique-asset.interface.ts new file mode 100644 index 000000000..745a0d9a7 --- /dev/null +++ b/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; +} diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index 4c19c73ff..9fce3f703 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/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 { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; +import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { DataSource } from '@prisma/client'; import Big from 'big.js'; @@ -199,13 +200,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.import.emit(); } - public onOpenPositionDialog({ - dataSource, - symbol - }: { - dataSource: DataSource; - symbol: string; - }): void { + public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void { this.router.navigate([], { queryParams: { dataSource, symbol, positionDetailDialog: true } }); diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index 1bcc0d373..4fe51497d 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -3,14 +3,17 @@ import { ChangeDetectionStrategy, Component, ElementRef, + EventEmitter, Input, OnChanges, OnDestroy, + Output, ViewChild } from '@angular/core'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; 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 { Tooltip } from 'chart.js'; import { LinearScale } from 'chart.js'; @@ -30,6 +33,7 @@ export class PortfolioProportionChartComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() baseCurrency: string; + @Input() cursor: string; @Input() isInPercent = false; @Input() keys: string[] = []; @Input() locale = ''; @@ -37,11 +41,14 @@ export class PortfolioProportionChartComponent @Input() showLabels = false; @Input() positions: { [symbol: string]: Pick & { + dataSource?: DataSource; name: string; value: number; }; } = {}; + @Output() proportionChartClicked = new EventEmitter(); + @ViewChild('chartCanvas') chartCanvas: ElementRef; public chart: Chart; @@ -256,6 +263,21 @@ export class PortfolioProportionChartComponent layout: { 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: { datalabels: { color: (context) => {