|
|
@ -2,11 +2,13 @@ import { |
|
|
|
getAnnualizedPerformancePercent, |
|
|
|
getIntervalFromDateRange |
|
|
|
} from '@ghostfolio/common/calculation-helper'; |
|
|
|
import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; |
|
|
|
import { getLocale } from '@ghostfolio/common/helper'; |
|
|
|
import { |
|
|
|
AssetProfileIdentifier, |
|
|
|
PortfolioPosition |
|
|
|
} from '@ghostfolio/common/interfaces'; |
|
|
|
import { DateRange } from '@ghostfolio/common/types'; |
|
|
|
import { ColorScheme, DateRange } from '@ghostfolio/common/types'; |
|
|
|
|
|
|
|
import { CommonModule } from '@angular/common'; |
|
|
|
import { |
|
|
@ -25,7 +27,7 @@ import { DataSource } from '@prisma/client'; |
|
|
|
import { Big } from 'big.js'; |
|
|
|
import { ChartConfiguration } from 'chart.js'; |
|
|
|
import { LinearScale } from 'chart.js'; |
|
|
|
import { Chart } from 'chart.js'; |
|
|
|
import { Chart, Tooltip } from 'chart.js'; |
|
|
|
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; |
|
|
|
import { differenceInDays, max } from 'date-fns'; |
|
|
|
import { orderBy } from 'lodash'; |
|
|
@ -44,9 +46,12 @@ const { gray, green, red } = require('open-color'); |
|
|
|
export class GfTreemapChartComponent |
|
|
|
implements AfterViewInit, OnChanges, OnDestroy |
|
|
|
{ |
|
|
|
@Input() baseCurrency: string; |
|
|
|
@Input() colorScheme: ColorScheme; |
|
|
|
@Input() cursor: string; |
|
|
|
@Input() dateRange: DateRange; |
|
|
|
@Input() holdings: PortfolioPosition[]; |
|
|
|
@Input() locale = getLocale(); |
|
|
|
|
|
|
|
@Output() treemapChartClicked = new EventEmitter<AssetProfileIdentifier>(); |
|
|
|
|
|
|
@ -58,7 +63,7 @@ export class GfTreemapChartComponent |
|
|
|
public isLoading = true; |
|
|
|
|
|
|
|
public constructor() { |
|
|
|
Chart.register(LinearScale, TreemapController, TreemapElement); |
|
|
|
Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement); |
|
|
|
} |
|
|
|
|
|
|
|
public ngAfterViewInit() { |
|
|
@ -86,7 +91,7 @@ export class GfTreemapChartComponent |
|
|
|
datasets: [ |
|
|
|
{ |
|
|
|
backgroundColor(ctx) { |
|
|
|
const annualizedNetPerformancePercentWithCurrencyEffect = |
|
|
|
let annualizedNetPerformancePercentWithCurrencyEffect = |
|
|
|
getAnnualizedPerformancePercent({ |
|
|
|
daysInMarket: differenceInDays( |
|
|
|
endDate, |
|
|
@ -100,6 +105,12 @@ export class GfTreemapChartComponent |
|
|
|
) |
|
|
|
}).toNumber(); |
|
|
|
|
|
|
|
// Round to 2 decimal places
|
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect = |
|
|
|
Math.round( |
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect * 100 |
|
|
|
) / 100; |
|
|
|
|
|
|
|
if ( |
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect > |
|
|
|
0.03 * GfTreemapChartComponent.HEAT_MULTIPLIER |
|
|
@ -118,8 +129,11 @@ export class GfTreemapChartComponent |
|
|
|
} else if (annualizedNetPerformancePercentWithCurrencyEffect > 0) { |
|
|
|
return green[3]; |
|
|
|
} else if ( |
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect === 0 |
|
|
|
Math.abs(annualizedNetPerformancePercentWithCurrencyEffect) === 0 |
|
|
|
) { |
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect = Math.abs( |
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect |
|
|
|
); |
|
|
|
return gray[3]; |
|
|
|
} else if ( |
|
|
|
annualizedNetPerformancePercentWithCurrencyEffect > |
|
|
@ -146,13 +160,12 @@ export class GfTreemapChartComponent |
|
|
|
align: 'left', |
|
|
|
color: ['white'], |
|
|
|
display: true, |
|
|
|
font: [{ size: 14 }, { size: 11 }, { lineHeight: 2, size: 14 }], |
|
|
|
font: [{ size: 16 }, { lineHeight: 1.5, 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)}%` |
|
|
|
]; |
|
|
@ -168,6 +181,9 @@ export class GfTreemapChartComponent |
|
|
|
if (this.chartCanvas) { |
|
|
|
if (this.chart) { |
|
|
|
this.chart.data = data; |
|
|
|
this.chart.options.plugins.tooltip = <unknown>( |
|
|
|
this.getTooltipPluginConfiguration() |
|
|
|
); |
|
|
|
this.chart.update(); |
|
|
|
} else { |
|
|
|
this.chart = new Chart(this.chartCanvas.nativeElement, { |
|
|
@ -199,9 +215,7 @@ export class GfTreemapChartComponent |
|
|
|
} |
|
|
|
}, |
|
|
|
plugins: { |
|
|
|
tooltip: { |
|
|
|
enabled: false |
|
|
|
} |
|
|
|
tooltip: this.getTooltipPluginConfiguration() |
|
|
|
} |
|
|
|
}, |
|
|
|
type: 'treemap' |
|
|
@ -211,4 +225,42 @@ export class GfTreemapChartComponent |
|
|
|
|
|
|
|
this.isLoading = false; |
|
|
|
} |
|
|
|
|
|
|
|
private getTooltipPluginConfiguration() { |
|
|
|
return { |
|
|
|
...getTooltipOptions({ |
|
|
|
colorScheme: this.colorScheme, |
|
|
|
currency: this.baseCurrency, |
|
|
|
locale: this.locale |
|
|
|
}), |
|
|
|
callbacks: { |
|
|
|
label: (context) => { |
|
|
|
const name = context.raw._data.name; |
|
|
|
const symbol = context.raw._data.symbol; |
|
|
|
|
|
|
|
if (context.raw._data.valueInBaseCurrency !== null) { |
|
|
|
const value = <number>context.raw._data.valueInBaseCurrency; |
|
|
|
|
|
|
|
return [ |
|
|
|
`${name ?? symbol}`, |
|
|
|
`${value.toLocaleString(this.locale, { |
|
|
|
maximumFractionDigits: 2, |
|
|
|
minimumFractionDigits: 2 |
|
|
|
})} ${this.baseCurrency}` |
|
|
|
]; |
|
|
|
} else { |
|
|
|
const percentage = |
|
|
|
<number>context.raw._data.allocationInPercentage * 100; |
|
|
|
|
|
|
|
return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; |
|
|
|
} |
|
|
|
}, |
|
|
|
title: () => { |
|
|
|
return ''; |
|
|
|
} |
|
|
|
}, |
|
|
|
xAlign: 'center', |
|
|
|
yAlign: 'center' |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|