mirror of https://github.com/ghostfolio/ghostfolio
10 changed files with 217 additions and 3 deletions
@ -0,0 +1 @@ |
|||||
|
export * from './treemap-chart.component'; |
@ -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> |
@ -0,0 +1,4 @@ |
|||||
|
:host { |
||||
|
aspect-ratio: 16 / 9; |
||||
|
display: block; |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue