Feature/setup treemap chart for holdings (#3560)

* Setup treemap chart

* Update changelog
This commit is contained in:
Thomas Kaul
2024-07-12 21:02:12 +02:00
committed by GitHub
parent 890c5b986c
commit 4063c62a17
13 changed files with 297 additions and 28 deletions

View File

@@ -354,7 +354,7 @@ 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

View File

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

View File

@@ -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>

View File

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

View File

@@ -0,0 +1,168 @@
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 { orderBy } from 'lodash';
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() cursor: string;
@Input() holdings: PortfolioPosition[];
@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: 14 }, { size: 11 }, { lineHeight: 2, size: 14 }],
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 datasetIndex = activeElements[0].datasetIndex;
const dataset = orderBy(
event.chart.data.datasets[datasetIndex].tree,
['allocationInPercentage'],
['desc']
);
const dataSource: DataSource = dataset[dataIndex].dataSource;
const symbol: string = dataset[dataIndex].symbol;
this.treemapChartClicked.emit({ dataSource, symbol });
} catch {}
},
onHover: (event, chartElement) => {
if (this.cursor) {
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
}
},
plugins: {
tooltip: {
enabled: false
}
}
},
type: 'treemap'
});
}
}
this.isLoading = false;
}
}