mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
329 lines
8.8 KiB
329 lines
8.8 KiB
import 'chartjs-adapter-date-fns';
|
|
|
|
import {
|
|
AfterViewInit,
|
|
ChangeDetectionStrategy,
|
|
ChangeDetectorRef,
|
|
Component,
|
|
EventEmitter,
|
|
Input,
|
|
OnChanges,
|
|
OnDestroy,
|
|
Output,
|
|
ViewChild
|
|
} from '@angular/core';
|
|
import { FormBuilder, FormControl } from '@angular/forms';
|
|
import { primaryColorRgb } from '@ghostfolio/common/config';
|
|
import { transformTickToAbbreviation } from '@ghostfolio/common/helper';
|
|
import {
|
|
BarController,
|
|
BarElement,
|
|
CategoryScale,
|
|
Chart,
|
|
LinearScale,
|
|
Tooltip
|
|
} from 'chart.js';
|
|
import * as Color from 'color';
|
|
import { isNumber } from 'lodash';
|
|
import { Subject, takeUntil } from 'rxjs';
|
|
|
|
import { FireCalculatorService } from './fire-calculator.service';
|
|
|
|
@Component({
|
|
selector: 'gf-fire-calculator',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
templateUrl: './fire-calculator.component.html',
|
|
styleUrls: ['./fire-calculator.component.scss']
|
|
})
|
|
export class FireCalculatorComponent
|
|
implements AfterViewInit, OnChanges, OnDestroy
|
|
{
|
|
@Input() currency: string;
|
|
@Input() deviceType: string;
|
|
@Input() fireWealth: number;
|
|
@Input() hasPermissionToUpdateUserSettings: boolean;
|
|
@Input() locale: string;
|
|
@Input() savingsRate = 0;
|
|
|
|
@Output() savingsRateChanged = new EventEmitter<number>();
|
|
|
|
@ViewChild('chartCanvas') chartCanvas;
|
|
|
|
public calculatorForm = this.formBuilder.group({
|
|
annualInterestRate: new FormControl(),
|
|
paymentPerPeriod: new FormControl(),
|
|
principalInvestmentAmount: new FormControl(),
|
|
time: new FormControl()
|
|
});
|
|
public chart: Chart;
|
|
public isLoading = true;
|
|
public projectedTotalAmount: number;
|
|
|
|
private unsubscribeSubject = new Subject<void>();
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
public constructor(
|
|
private changeDetectorRef: ChangeDetectorRef,
|
|
private fireCalculatorService: FireCalculatorService,
|
|
private formBuilder: FormBuilder
|
|
) {
|
|
Chart.register(
|
|
BarController,
|
|
BarElement,
|
|
CategoryScale,
|
|
LinearScale,
|
|
Tooltip
|
|
);
|
|
|
|
this.calculatorForm.setValue(
|
|
{
|
|
annualInterestRate: 5,
|
|
paymentPerPeriod: this.savingsRate,
|
|
principalInvestmentAmount: 0,
|
|
time: 10
|
|
},
|
|
{
|
|
emitEvent: false
|
|
}
|
|
);
|
|
|
|
this.calculatorForm.valueChanges
|
|
.pipe(takeUntil(this.unsubscribeSubject))
|
|
.subscribe(() => {
|
|
this.initialize();
|
|
});
|
|
|
|
this.calculatorForm
|
|
.get('paymentPerPeriod')
|
|
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
|
.subscribe((savingsRate) => {
|
|
this.savingsRateChanged.emit(savingsRate);
|
|
});
|
|
}
|
|
|
|
public ngAfterViewInit() {
|
|
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
|
setTimeout(() => {
|
|
// Wait for the chartCanvas
|
|
this.calculatorForm.patchValue(
|
|
{
|
|
principalInvestmentAmount: this.fireWealth,
|
|
paymentPerPeriod: this.savingsRate ?? 0
|
|
},
|
|
{
|
|
emitEvent: false
|
|
}
|
|
);
|
|
this.calculatorForm.get('principalInvestmentAmount').disable();
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
|
|
if (this.hasPermissionToUpdateUserSettings === true) {
|
|
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false });
|
|
} else {
|
|
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
|
}
|
|
}
|
|
|
|
public ngOnChanges() {
|
|
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
|
setTimeout(() => {
|
|
// Wait for the chartCanvas
|
|
this.calculatorForm.patchValue(
|
|
{
|
|
principalInvestmentAmount: this.fireWealth,
|
|
paymentPerPeriod: this.savingsRate ?? 0
|
|
},
|
|
{
|
|
emitEvent: false
|
|
}
|
|
);
|
|
this.calculatorForm.get('principalInvestmentAmount').disable();
|
|
|
|
this.changeDetectorRef.markForCheck();
|
|
});
|
|
}
|
|
|
|
if (this.hasPermissionToUpdateUserSettings === true) {
|
|
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false });
|
|
} else {
|
|
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
|
|
}
|
|
}
|
|
|
|
public ngOnDestroy() {
|
|
this.chart?.destroy();
|
|
|
|
this.unsubscribeSubject.next();
|
|
this.unsubscribeSubject.complete();
|
|
}
|
|
|
|
private initialize() {
|
|
this.isLoading = true;
|
|
|
|
const chartData = this.getChartData();
|
|
|
|
if (this.chartCanvas) {
|
|
if (this.chart) {
|
|
this.chart.data.labels = chartData.labels;
|
|
|
|
for (let index = 0; index < this.chart.data.datasets.length; index++) {
|
|
this.chart.data.datasets[index].data = chartData.datasets[index].data;
|
|
}
|
|
|
|
this.chart.update();
|
|
} else {
|
|
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
|
data: chartData,
|
|
options: {
|
|
plugins: {
|
|
tooltip: {
|
|
itemSort: (a, b) => {
|
|
// Reverse order
|
|
return b.datasetIndex - a.datasetIndex;
|
|
},
|
|
mode: 'index',
|
|
callbacks: {
|
|
footer: (items) => {
|
|
const totalAmount = items.reduce(
|
|
(a, b) => a + b.parsed.y,
|
|
0
|
|
);
|
|
|
|
return `Total: ${new Intl.NumberFormat(this.locale, {
|
|
currency: this.currency,
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore: Only supported from ES2020 or later
|
|
currencyDisplay: 'code',
|
|
style: 'currency'
|
|
}).format(totalAmount)}`;
|
|
},
|
|
label: (context) => {
|
|
let label = context.dataset.label || '';
|
|
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
|
|
if (context.parsed.y !== null) {
|
|
label += new Intl.NumberFormat(this.locale, {
|
|
currency: this.currency,
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore: Only supported from ES2020 or later
|
|
currencyDisplay: 'code',
|
|
style: 'currency'
|
|
}).format(context.parsed.y);
|
|
}
|
|
|
|
return label;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
responsive: true,
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
stacked: true
|
|
},
|
|
y: {
|
|
display: this.deviceType !== 'mobile',
|
|
grid: {
|
|
display: false
|
|
},
|
|
stacked: true,
|
|
ticks: {
|
|
callback: (value: number) => {
|
|
return transformTickToAbbreviation(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
type: 'bar'
|
|
});
|
|
}
|
|
}
|
|
|
|
this.isLoading = false;
|
|
}
|
|
|
|
private getChartData() {
|
|
const currentYear = new Date().getFullYear();
|
|
const labels = [];
|
|
|
|
// Principal investment amount
|
|
const P: number =
|
|
this.calculatorForm.get('principalInvestmentAmount').value || 0;
|
|
|
|
// Payment per period
|
|
const PMT: number = parseFloat(
|
|
this.calculatorForm.get('paymentPerPeriod').value
|
|
);
|
|
|
|
// Annual interest rate
|
|
const r: number = this.calculatorForm.get('annualInterestRate').value / 100;
|
|
|
|
// Time
|
|
const t: number = parseFloat(this.calculatorForm.get('time').value);
|
|
|
|
for (let year = currentYear; year < currentYear + t; year++) {
|
|
labels.push(year);
|
|
}
|
|
|
|
const datasetDeposit = {
|
|
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
|
data: [],
|
|
label: 'Deposit'
|
|
};
|
|
|
|
const datasetInterest = {
|
|
backgroundColor: Color(
|
|
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
|
|
)
|
|
.lighten(0.5)
|
|
.hex(),
|
|
data: [],
|
|
label: 'Interest'
|
|
};
|
|
|
|
const datasetSavings = {
|
|
backgroundColor: Color(
|
|
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
|
|
)
|
|
.lighten(0.25)
|
|
.hex(),
|
|
data: [],
|
|
label: 'Savings'
|
|
};
|
|
|
|
for (let period = 1; period <= t; period++) {
|
|
const { interest, principal, totalAmount } =
|
|
this.fireCalculatorService.calculateCompoundInterest({
|
|
P,
|
|
period,
|
|
PMT,
|
|
r
|
|
});
|
|
|
|
datasetDeposit.data.push(this.fireWealth);
|
|
datasetInterest.data.push(interest.toNumber());
|
|
datasetSavings.data.push(principal.minus(this.fireWealth).toNumber());
|
|
|
|
if (period === t) {
|
|
this.projectedTotalAmount = totalAmount.toNumber();
|
|
}
|
|
}
|
|
|
|
return {
|
|
labels,
|
|
datasets: [datasetDeposit, datasetSavings, datasetInterest]
|
|
};
|
|
}
|
|
}
|
|
|