Browse Source

Setup treemap chart

pull/3560/head
Thomas Kaul 1 year ago
parent
commit
f874a93555
  1. 18
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  2. 7
      apps/client/src/app/components/home-holdings/home-holdings.html
  3. 2
      apps/client/src/app/components/home-holdings/home-holdings.module.ts
  4. 3
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  5. 1
      libs/ui/src/lib/treemap-chart/index.ts
  6. 13
      libs/ui/src/lib/treemap-chart/treemap-chart.component.html
  7. 4
      libs/ui/src/lib/treemap-chart/treemap-chart.component.scss
  8. 160
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
  9. 2
      package.json
  10. 10
      yarn.lock

18
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();

7
apps/client/src/app/components/home-holdings/home-holdings.html

@ -6,7 +6,7 @@
</div>
<div class="row">
<div class="col-lg">
<div class="d-flex justify-content-end">
<div class="d-flex justify-content-end mb-3">
<gf-toggle
class="d-none d-lg-block"
[defaultValue]="holdingType"
@ -15,6 +15,11 @@
(change)="onChangeHoldingType($event.value)"
/>
</div>
<gf-treemap-chart
cursor="pointer"
[holdings]="holdings"
(treemapChartClicked)="onSymbolClicked($event)"
/>
<gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"

2
apps/client/src/app/components/home-holdings/home-holdings.module.ts

@ -1,5 +1,6 @@
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfTreemapChartComponent } from '@ghostfolio/ui/treemap-chart';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
@ -14,6 +15,7 @@ import { HomeHoldingsComponent } from './home-holdings.component';
CommonModule,
GfHoldingsTableComponent,
GfToggleModule,
GfTreemapChartComponent,
MatButtonModule,
RouterModule
],

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

@ -352,9 +352,10 @@ export class GfPortfolioProportionChartComponent
/**
* Color palette, inspired by https://yeun.github.io/open-color
*
*/
private getColorPalette() {
//
// TODO: Reuse require('open-color')
return [
'#329af0', // blue 5
'#20c997', // teal 5

1
libs/ui/src/lib/treemap-chart/index.ts

@ -0,0 +1 @@
export * from './treemap-chart.component';

13
libs/ui/src/lib/treemap-chart/treemap-chart.component.html

@ -0,0 +1,13 @@
@if (isLoading) {
<ngx-skeleton-loader
animation="pulse"
class="h-100"
[theme]="{
height: '100%'
}"
/>
}
<canvas
#chartCanvas
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
></canvas>

4
libs/ui/src/lib/treemap-chart/treemap-chart.component.scss

@ -0,0 +1,4 @@
:host {
aspect-ratio: 16 / 9;
display: block;
}

160
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<UniqueAsset>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
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'] = <any>{
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: <unknown>{
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;
}
}

2
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",

10
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"

Loading…
Cancel
Save