From f874a93555261425fb2332a7af898b9343249a62 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:11:46 +0200 Subject: [PATCH] Setup treemap chart --- .../home-holdings/home-holdings.component.ts | 18 +- .../home-holdings/home-holdings.html | 7 +- .../home-holdings/home-holdings.module.ts | 2 + .../portfolio-proportion-chart.component.ts | 3 +- libs/ui/src/lib/treemap-chart/index.ts | 1 + .../treemap-chart.component.html | 13 ++ .../treemap-chart.component.scss | 4 + .../treemap-chart/treemap-chart.component.ts | 160 ++++++++++++++++++ package.json | 2 + yarn.lock | 10 ++ 10 files changed, 217 insertions(+), 3 deletions(-) create mode 100644 libs/ui/src/lib/treemap-chart/index.ts create mode 100644 libs/ui/src/lib/treemap-chart/treemap-chart.component.html create mode 100644 libs/ui/src/lib/treemap-chart/treemap-chart.component.scss create mode 100644 libs/ui/src/lib/treemap-chart/treemap-chart.component.ts diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 5141bf9fa..fad1f9a03 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -1,7 +1,11 @@ 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 { + PortfolioPosition, + UniqueAsset, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { HoldingType, ToggleOption } from '@ghostfolio/common/types'; @@ -87,6 +91,18 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { }); } + public onSymbolClicked({ dataSource, symbol }: UniqueAsset) { + // TODO + + // if (dataSource && symbol) { + // this.router.navigate([], { + // queryParams: { dataSource, symbol, holdingDetailDialog: true } + // }); + // } + + console.log({ dataSource, symbol }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index a2bd43636..b2d9d1077 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -6,7 +6,7 @@
-
+
+ +} + diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.scss b/libs/ui/src/lib/treemap-chart/treemap-chart.component.scss new file mode 100644 index 000000000..d041372c8 --- /dev/null +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.scss @@ -0,0 +1,4 @@ +:host { + aspect-ratio: 16 / 9; + display: block; +} diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts new file mode 100644 index 000000000..d94967c44 --- /dev/null +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -0,0 +1,160 @@ +import { getLocale } from '@ghostfolio/common/helper'; +import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; + +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + ViewChild +} from '@angular/core'; +import { DataSource } from '@prisma/client'; +import { ChartConfiguration } from 'chart.js'; +import { LinearScale } from 'chart.js'; +import { Chart } from 'chart.js'; +import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +const { gray, green, red } = require('open-color'); + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [CommonModule, NgxSkeletonLoaderModule], + selector: 'gf-treemap-chart', + standalone: true, + styleUrls: ['./treemap-chart.component.scss'], + templateUrl: './treemap-chart.component.html' +}) +export class GfTreemapChartComponent + implements AfterViewInit, OnChanges, OnDestroy +{ + @Input() baseCurrency: string; + @Input() cursor: string; + @Input() holdings: PortfolioPosition[]; + @Input() isInPercent = false; + @Input() locale = getLocale(); + + @Output() treemapChartClicked = new EventEmitter(); + + @ViewChild('chartCanvas') chartCanvas: ElementRef; + + public chart: Chart<'treemap'>; + public isLoading = true; + + public constructor() { + Chart.register(LinearScale, TreemapController, TreemapElement); + } + + public ngAfterViewInit() { + if (this.holdings) { + this.initialize(); + } + } + + public ngOnChanges() { + if (this.holdings) { + this.initialize(); + } + } + + public ngOnDestroy() { + this.chart?.destroy(); + } + + private initialize() { + this.isLoading = true; + + const data: ChartConfiguration['data'] = { + datasets: [ + { + backgroundColor(ctx) { + const netPerformancePercentWithCurrencyEffect = + ctx.raw._data.netPerformancePercentWithCurrencyEffect; + + if (netPerformancePercentWithCurrencyEffect > 0.03) { + return green[9]; + } else if (netPerformancePercentWithCurrencyEffect > 0.02) { + return green[7]; + } else if (netPerformancePercentWithCurrencyEffect > 0.01) { + return green[5]; + } else if (netPerformancePercentWithCurrencyEffect > 0) { + return green[3]; + } else if (netPerformancePercentWithCurrencyEffect === 0) { + return gray[3]; + } else if (netPerformancePercentWithCurrencyEffect > -0.01) { + return red[3]; + } else if (netPerformancePercentWithCurrencyEffect > -0.02) { + return red[5]; + } else if (netPerformancePercentWithCurrencyEffect > -0.03) { + return red[7]; + } else { + return red[9]; + } + }, + key: 'allocationInPercentage', + labels: { + align: 'left', + color: ['white'], + display: true, + font: [{ size: 16, weight: 'bold' }, { size: 12 }], + formatter(ctx) { + const netPerformancePercentWithCurrencyEffect = + ctx.raw._data.netPerformancePercentWithCurrencyEffect; + + return [ + ctx.raw._data.name, + ctx.raw._data.symbol, + `${netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''}${(ctx.raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%` + ]; + }, + position: 'top' + }, + spacing: 1, + tree: this.holdings + } + ] + }; + + if (this.chartCanvas) { + if (this.chart) { + this.chart.data = data; + this.chart.update(); + } else { + this.chart = new Chart(this.chartCanvas.nativeElement, { + data, + options: { + animation: false, + onClick: (event, activeElements) => { + try { + const dataIndex = activeElements[0].index; + + const dataSource: DataSource = + event.chart.data.datasets[0].tree[dataIndex].dataSource; + const symbol: string = + event.chart.data.datasets[0].tree[dataIndex].symbol; + + this.treemapChartClicked.emit({ dataSource, symbol }); + } catch {} + }, + onHover: (event, chartElement) => { + if (this.cursor) { + event.native.target.style.cursor = chartElement[0] + ? this.cursor + : 'default'; + } + } + }, + type: 'treemap' + }); + } + } + + this.isLoading = false; + } +} diff --git a/package.json b/package.json index fca0a9a0f..49b43b303 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "cache-manager-redis-store": "2.0.0", "chart.js": "4.2.0", "chartjs-adapter-date-fns": "3.0.0", + "chartjs-chart-treemap": "2.3.1", "chartjs-plugin-annotation": "2.1.2", "chartjs-plugin-datalabels": "2.2.0", "cheerio": "1.0.0-rc.12", @@ -122,6 +123,7 @@ "ngx-markdown": "18.0.0", "ngx-skeleton-loader": "7.0.0", "ngx-stripe": "18.0.0", + "open-color": "1.9.1", "papaparse": "5.3.1", "passport": "0.7.0", "passport-google-oauth20": "2.0.0", diff --git a/yarn.lock b/yarn.lock index 8d6caaef2..e807877b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10071,6 +10071,11 @@ chartjs-adapter-date-fns@3.0.0: resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz#c25f63c7f317c1f96f9a7c44bd45eeedb8a478e5" integrity sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg== +chartjs-chart-treemap@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chartjs-chart-treemap/-/chartjs-chart-treemap-2.3.1.tgz#b0d27309ee373cb7706cabb262c48c53ffacf710" + integrity sha512-GW+iODLICIJhNZtHbTtaOjCwRIxmXcquXRKDFMsrkXyqyDeSN1aiVfzNNj6Xjy55soopqRA+YfHqjT2S2zF7lQ== + chartjs-plugin-annotation@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/chartjs-plugin-annotation/-/chartjs-plugin-annotation-2.1.2.tgz#8c307c931fda735a1acf1b606ad0e3fd7d96299b" @@ -16757,6 +16762,11 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open-color@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/open-color/-/open-color-1.9.1.tgz#a6e6328f60eff7aa60e3e8fcfa50f53ff3eece35" + integrity sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw== + open@8.4.2, open@^8.0.4, open@^8.0.9, open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9"