mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
7 months ago
committed by
GitHub
13 changed files with 297 additions and 28 deletions
@ -0,0 +1 @@ |
|||
export type HoldingViewMode = 'CHART' | 'TABLE'; |
@ -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,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; |
|||
} |
|||
} |
Loading…
Reference in new issue