Browse Source

feat(lib): improve chart type safety

pull/6264/head
KenTandrian 2 days ago
parent
commit
c30893bdee
  1. 16
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  2. 28
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  3. 22
      libs/common/src/lib/chart-helper.ts
  4. 67
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  5. 16
      libs/ui/src/lib/line-chart/line-chart.component.ts
  6. 45
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  7. 13
      libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts
  8. 31
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

16
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -21,6 +21,7 @@ import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
type ElementRef,
EventEmitter,
Input,
OnChanges,
@ -42,7 +43,8 @@ import {
PointElement,
TimeScale,
Tooltip,
TooltipPosition
type TooltipOptions,
type TooltipPosition
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
@ -78,7 +80,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Output() benchmarkChanged = new EventEmitter<string>();
@ViewChild('chartCanvas') chartCanvas;
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>;
public hasPermissionToAccessAdminControl: boolean;
@ -158,7 +160,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
if (this.chart) {
this.chart.data = data;
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown;
this.getTooltipPluginConfiguration();
this.chart.update();
} else {
this.chart = new Chart(this.chartCanvas.nativeElement, {
@ -193,10 +195,11 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
display: false
},
tooltip: this.getTooltipPluginConfiguration(),
// @ts-ignore
verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
}
} as unknown,
},
responsive: true,
scales: {
x: {
@ -253,7 +256,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
}
}
private getTooltipPluginConfiguration() {
private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return {
...getTooltipOptions({
colorScheme: this.colorScheme,
@ -261,7 +264,8 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
unit: '%'
}),
mode: 'index',
position: 'top' as unknown,
// @ts-ignore
position: 'top',
xAlign: 'center',
yAlign: 'bottom'
};

28
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -20,6 +20,7 @@ import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
type ElementRef,
Input,
OnChanges,
OnDestroy,
@ -36,7 +37,8 @@ import {
PointElement,
TimeScale,
Tooltip,
TooltipPosition
type TooltipOptions,
type TooltipPosition
} from 'chart.js';
import 'chartjs-adapter-date-fns';
import annotationPlugin from 'chartjs-plugin-annotation';
@ -62,7 +64,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
@Input() locale = getLocale();
@Input() savingsRate = 0;
@ViewChild('chartCanvas') chartCanvas;
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'bar' | 'line'>;
private investments: InvestmentItem[];
@ -121,12 +123,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}),
label: this.benchmarkDataLabel,
segment: {
borderColor: (context: unknown) =>
borderColor: (context) =>
this.isInFuture(
context,
`rgba(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b}, 0.67)`
),
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
borderDash: (context) => this.isInFuture(context, [2, 2])
},
stepped: true
},
@ -143,12 +145,12 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
label: $localize`Total Amount`,
pointRadius: 0,
segment: {
borderColor: (context: unknown) =>
borderColor: (context) =>
this.isInFuture(
context,
`rgba(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b}, 0.67)`
),
borderDash: (context: unknown) => this.isInFuture(context, [2, 2])
borderDash: (context) => this.isInFuture(context, [2, 2])
}
}
]
@ -158,7 +160,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
if (this.chart) {
this.chart.data = chartData;
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown;
this.getTooltipPluginConfiguration();
if (
this.savingsRate &&
@ -201,7 +203,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
color: 'white',
content: $localize`Savings Rate`,
display: true,
font: { size: '10px', weight: 'normal' },
font: { size: 10, weight: 'normal' },
padding: {
x: 4,
y: 2
@ -226,10 +228,11 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
display: false
},
tooltip: this.getTooltipPluginConfiguration(),
// @ts-ignore
verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
}
} as unknown,
},
responsive: true,
scales: {
x: {
@ -286,7 +289,9 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}
}
private getTooltipPluginConfiguration() {
private getTooltipPluginConfiguration(): Partial<
TooltipOptions<'bar' | 'line'>
> {
return {
...getTooltipOptions({
colorScheme: this.colorScheme,
@ -296,7 +301,8 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
unit: this.isInPercent ? '%' : undefined
}),
mode: 'index',
position: 'top' as unknown,
// @ts-ignore
position: 'top',
xAlign: 'center',
yAlign: 'bottom'
};

22
libs/common/src/lib/chart-helper.ts

@ -3,6 +3,7 @@ import type {
Chart,
ChartTypeRegistry,
Plugin,
TooltipOptions,
TooltipPosition
} from 'chart.js';
import { format } from 'date-fns';
@ -21,7 +22,7 @@ export function formatGroupedDate({
date,
groupBy
}: {
date: Date;
date: number;
groupBy: GroupBy;
}) {
if (groupBy === 'month') {
@ -45,34 +46,51 @@ export function getTooltipOptions({
groupBy?: GroupBy;
locale?: string;
unit?: string;
}) {
}): Partial<TooltipOptions> {
return {
backgroundColor: getBackgroundColor(colorScheme),
bodyColor: `rgb(${getTextColor(colorScheme)})`,
borderWidth: 1,
borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`,
callbacks: {
afterBody: () => '',
afterFooter: () => '',
afterLabel: () => '',
afterTitle: () => '',
beforeBody: () => '',
beforeFooter: () => '',
beforeLabel: () => '',
beforeTitle: () => '',
footer: () => '',
label: (context) => {
let label = context.dataset.label ?? '';
if (label) {
label += ': ';
}
// @ts-ignore
if (context.parsed.y !== null) {
if (currency) {
// @ts-ignore
label += `${context.parsed.y.toLocaleString(locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
})} ${currency}`;
} else if (unit) {
// @ts-ignore
label += `${context.parsed.y.toFixed(2)} ${unit}`;
} else {
// @ts-ignore
label += context.parsed.y.toFixed(2);
}
}
return label;
},
labelColor: () => {},
labelPointStyle: () => {},
labelTextColor: () => {},
title: (contexts) => {
if (groupBy) {
// @ts-ignore
return formatGroupedDate({ groupBy, date: contexts[0].parsed.x });
}

67
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -98,10 +98,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public calculatorForm = this.formBuilder.group({
// @ts-ignore
annualInterestRate: new FormControl<number>(undefined),
// @ts-ignore
paymentPerPeriod: new FormControl<number>(undefined),
// @ts-ignore
principalInvestmentAmount: new FormControl<number>(undefined),
// @ts-ignore
projectedTotalAmount: new FormControl<number>(undefined),
// @ts-ignore
retirementDate: new FormControl<Date>(undefined)
});
public chart: Chart<'bar'>;
@ -148,25 +153,25 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm
.get('annualInterestRate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((annualInterestRate) => {
this.annualInterestRateChanged.emit(annualInterestRate);
});
this.calculatorForm
.get('paymentPerPeriod')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((savingsRate) => {
this.savingsRateChanged.emit(savingsRate);
});
this.calculatorForm
.get('projectedTotalAmount')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((projectedTotalAmount) => {
this.projectedTotalAmountChanged.emit(projectedTotalAmount);
});
this.calculatorForm
.get('retirementDate')
.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
?.valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject))
.subscribe((retirementDate) => {
this.retirementDateChanged.emit(retirementDate);
});
@ -194,11 +199,11 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm.patchValue(
{
annualInterestRate:
this.calculatorForm.get('annualInterestRate').value,
this.calculatorForm.get('annualInterestRate')?.value,
paymentPerPeriod: this.getPMT(),
principalInvestmentAmount: this.calculatorForm.get(
'principalInvestmentAmount'
).value,
)?.value,
projectedTotalAmount:
Math.round(this.getProjectedTotalAmount()) || 0,
retirementDate:
@ -208,7 +213,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
emitEvent: false
}
);
this.calculatorForm.get('principalInvestmentAmount').disable();
this.calculatorForm.get('principalInvestmentAmount')?.disable();
this.changeDetectorRef.markForCheck();
});
@ -217,34 +222,36 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
if (this.hasPermissionToUpdateUserSettings === true) {
this.calculatorForm
.get('annualInterestRate')
.enable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod').enable({ emitEvent: false });
?.enable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod')?.enable({ emitEvent: false });
this.calculatorForm
.get('projectedTotalAmount')
.enable({ emitEvent: false });
?.enable({ emitEvent: false });
} else {
this.calculatorForm
.get('annualInterestRate')
.disable({ emitEvent: false });
this.calculatorForm.get('paymentPerPeriod').disable({ emitEvent: false });
?.disable({ emitEvent: false });
this.calculatorForm
.get('paymentPerPeriod')
?.disable({ emitEvent: false });
this.calculatorForm
.get('projectedTotalAmount')
.disable({ emitEvent: false });
?.disable({ emitEvent: false });
}
this.calculatorForm.get('retirementDate').disable({ emitEvent: false });
this.calculatorForm.get('retirementDate')?.disable({ emitEvent: false });
}
public setMonthAndYear(
normalizedMonthAndYear: Date,
datepicker: MatDatepicker<Date>
) {
const retirementDate = this.calculatorForm.get('retirementDate').value;
const retirementDate = this.calculatorForm.get('retirementDate')!.value;
const newRetirementDate = setMonth(
setYear(retirementDate, normalizedMonthAndYear.getFullYear()),
normalizedMonthAndYear.getMonth()
);
this.calculatorForm.get('retirementDate').setValue(newRetirementDate);
this.calculatorForm.get('retirementDate')?.setValue(newRetirementDate);
datepicker.close();
}
@ -270,7 +277,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.chart.update();
} else {
this.chart = new Chart(this.chartCanvas.nativeElement, {
this.chart = new Chart<'bar'>(this.chartCanvas.nativeElement, {
data: chartData,
options: {
plugins: {
@ -280,6 +287,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
callbacks: {
footer: (items) => {
const totalAmount = items.reduce(
// @ts-ignore
(a, b) => a + b.parsed.y,
0
);
@ -347,7 +355,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
private getChartData() {
const currentYear = new Date().getFullYear();
const labels = [];
const labels: number[] = [];
// Principal investment amount
const P: number = this.getP();
@ -360,6 +368,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
// Calculate retirement date
// if we want to retire at month x, we need the projectedTotalAmount at month x-1
// @ts-ignore
const lastPeriodDate = sub(this.getRetirementDate(), { months: 1 });
const yearsToRetire = lastPeriodDate.getFullYear() - currentYear;
@ -373,7 +382,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
const datasetDeposit = {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
data: [],
data: [] as number[],
label: $localize`Deposit`
};
@ -383,7 +392,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
)
.lighten(0.5)
.hex(),
data: [],
data: [] as number[],
label: $localize`Interest`
};
@ -393,7 +402,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
)
.lighten(0.25)
.hex(),
data: [],
data: [] as number[],
label: $localize`Savings`
};
@ -426,12 +435,12 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}
private getPeriodsToRetire(): number {
if (this.calculatorForm.get('projectedTotalAmount').value) {
if (this.calculatorForm.get('projectedTotalAmount')?.value) {
let periods = this.fireCalculatorService.calculatePeriodsToRetire({
P: this.getP(),
PMT: this.getPMT(),
r: this.getR(),
totalAmount: this.calculatorForm.get('projectedTotalAmount').value
totalAmount: this.calculatorForm.get('projectedTotalAmount')!.value
});
if (periods === Infinity) {
@ -452,13 +461,13 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}
}
private getPMT() {
return this.calculatorForm.get('paymentPerPeriod').value;
private getPMT(): number {
return this.calculatorForm.get('paymentPerPeriod')!.value;
}
private getProjectedTotalAmount() {
if (this.calculatorForm.get('projectedTotalAmount').value) {
return this.calculatorForm.get('projectedTotalAmount').value;
if (this.calculatorForm.get('projectedTotalAmount')?.value) {
return this.calculatorForm.get('projectedTotalAmount')!.value;
}
const { totalAmount } =
@ -473,10 +482,10 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}
private getR() {
return this.calculatorForm.get('annualInterestRate').value / 100;
return this.calculatorForm.get('annualInterestRate')!.value / 100;
}
private getRetirementDate(): Date {
private getRetirementDate(): Date | undefined {
if (this.periodsToRetire === Number.MAX_SAFE_INTEGER) {
return undefined;
}

16
libs/ui/src/lib/line-chart/line-chart.component.ts

@ -19,12 +19,14 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
type ElementRef,
Input,
OnChanges,
OnDestroy,
ViewChild
} from '@angular/core';
import {
type AnimationSpec,
Chart,
Filler,
LinearScale,
@ -68,7 +70,7 @@ export class GfLineChartComponent
@Input() yMin: number;
@Input() yMinLabel: string;
@ViewChild('chartCanvas') chartCanvas;
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>;
public isLoading = true;
@ -130,10 +132,11 @@ export class GfLineChartComponent
const gradient = this.chartCanvas?.nativeElement
?.getContext('2d')
.createLinearGradient(
?.createLinearGradient(
0,
0,
0,
// @ts-ignore
(this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5
);
@ -180,6 +183,7 @@ export class GfLineChartComponent
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration();
this.chart.options.animation = this.isAnimated && {
// @ts-ignore
x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }),
y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' })
};
@ -189,6 +193,7 @@ export class GfLineChartComponent
data,
options: {
animation: this.isAnimated && {
// @ts-ignore
x: this.getAnimationConfigurationForAxis({ labels, axis: 'x' }),
y: this.getAnimationConfigurationForAxis({ labels, axis: 'y' })
},
@ -207,6 +212,7 @@ export class GfLineChartComponent
position: 'bottom'
},
tooltip: this.getTooltipPluginConfiguration(),
// @ts-ignore
verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
}
@ -300,7 +306,7 @@ export class GfLineChartComponent
}: {
axis: 'x' | 'y';
labels: string[];
}) {
}): AnimationSpec<'line'> {
const delayBetweenPoints = this.ANIMATION_DURATION / labels.length;
return {
@ -310,11 +316,14 @@ export class GfLineChartComponent
}
context[`${axis}Started`] = true;
// @ts-ignore
return context.index * delayBetweenPoints;
},
duration: delayBetweenPoints,
easing: 'linear',
// @ts-ignore
from: NaN,
// @ts-ignore
type: 'number'
};
}
@ -328,6 +337,7 @@ export class GfLineChartComponent
unit: this.unit
}),
mode: 'index',
// @ts-ignore
position: 'top',
xAlign: 'center',
yAlign: 'bottom'

45
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -22,11 +22,16 @@ import {
} from '@angular/core';
import { DataSource } from '@prisma/client';
import { Big } from 'big.js';
import { ChartConfiguration, Tooltip } from 'chart.js';
import { LinearScale } from 'chart.js';
import { ArcElement } from 'chart.js';
import { DoughnutController } from 'chart.js';
import { Chart } from 'chart.js';
import {
ArcElement,
Chart,
type ChartData,
type ChartDataset,
DoughnutController,
LinearScale,
Tooltip,
type TooltipItem
} from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels';
import { isUUID } from 'class-validator';
import Color from 'color';
@ -137,14 +142,16 @@ export class GfPortfolioProportionChartComponent
chartData[this.data[symbol][this.keys[0]].toUpperCase()]
?.subCategory?.[this.data[symbol][this.keys[1]]]
) {
// @ts-ignore
chartData[
this.data[symbol][this.keys[0]].toUpperCase()
].subCategory[this.data[symbol][this.keys[1]]].value = chartData[
this.data[symbol][this.keys[0]].toUpperCase()
].subCategory[this.data[symbol][this.keys[1]]].value.plus(
].subCategory?.[this.data[symbol][this.keys[1]]].value.plus(
this.data[symbol].value || 0
);
} else {
// @ts-ignore
chartData[
this.data[symbol][this.keys[0]].toUpperCase()
].subCategory[this.data[symbol][this.keys[1]] ?? UNKNOWN_KEY] = {
@ -273,12 +280,14 @@ export class GfPortfolioProportionChartComponent
Object.keys(item.subCategory ?? {}).forEach((subCategory) => {
if (item.name === UNKNOWN_KEY) {
// @ts-ignore
backgroundColorSubCategory.push(item.color);
} else {
backgroundColorSubCategory.push(
Color(item.color).lighten(lightnessRatio).hex()
);
}
// @ts-ignore
dataSubCategory.push(item.subCategory[subCategory].value.toNumber());
labelSubCategory.push(subCategory);
@ -286,7 +295,7 @@ export class GfPortfolioProportionChartComponent
});
});
const datasets: ChartConfiguration<'doughnut'>['data']['datasets'] = [
const datasets: ChartDataset<'doughnut'>[] = [
{
backgroundColor: chartDataSorted.map(([, item]) => {
return item.color;
@ -324,7 +333,7 @@ export class GfPortfolioProportionChartComponent
datasets[1].data[1] = Number.MAX_SAFE_INTEGER;
}
const data: ChartConfiguration<'doughnut'>['data'] = {
const data: ChartData<'doughnut'> = {
datasets,
labels
};
@ -332,9 +341,13 @@ export class GfPortfolioProportionChartComponent
if (this.chartCanvas) {
if (this.chart) {
this.chart.data = data;
this.chart.options.plugins.tooltip = this.getTooltipPluginConfiguration(
data
) as unknown;
if (!this.chart.options.plugins) {
this.chart.options.plugins = {};
}
this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration(data);
this.chart.update();
} else {
this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, {
@ -348,15 +361,17 @@ export class GfPortfolioProportionChartComponent
onClick: (event, activeElements) => {
try {
const dataIndex = activeElements[0].index;
// @ts-ignore
const symbol: string = event.chart.data.labels[dataIndex];
const dataSource = this.data[symbol]?.dataSource;
const dataSource = this.data[symbol]?.dataSource!;
this.proportionChartClicked.emit({ dataSource, symbol });
} catch {}
},
onHover: (event, chartElement) => {
if (this.cursor) {
// @ts-ignore
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
@ -392,7 +407,7 @@ export class GfPortfolioProportionChartComponent
legend: { display: false },
tooltip: this.getTooltipPluginConfiguration(data)
}
} as unknown,
},
plugins: [ChartDataLabels],
type: 'doughnut'
});
@ -419,7 +434,7 @@ export class GfPortfolioProportionChartComponent
];
}
private getTooltipPluginConfiguration(data: ChartConfiguration['data']) {
private getTooltipPluginConfiguration(data: ChartData<'doughnut'>) {
return {
...getTooltipOptions({
colorScheme: this.colorScheme,
@ -427,7 +442,7 @@ export class GfPortfolioProportionChartComponent
locale: this.locale
}),
callbacks: {
label: (context) => {
label: (context: TooltipItem<'doughnut'>) => {
const labelIndex =
(data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) +
context.dataIndex;

13
libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts

@ -1,5 +1,18 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import {
TreemapDataPoint,
TreemapScriptableContext
} from 'chartjs-chart-treemap';
export interface GetColorParams {
annualizedNetPerformancePercent: number;
negativeNetPerformancePercentsRange: { max: number; min: number };
positiveNetPerformancePercentsRange: { max: number; min: number };
}
export interface GfTreemapChartTooltipContext extends TreemapScriptableContext {
raw: TreemapDataPoint & {
_data: PortfolioPosition;
};
}

31
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -25,7 +25,7 @@ import {
} from '@angular/core';
import { DataSource } from '@prisma/client';
import { Big } from 'big.js';
import type { ChartConfiguration, TooltipOptions } from 'chart.js';
import type { ChartData, TooltipOptions } from 'chart.js';
import { LinearScale } from 'chart.js';
import { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
@ -35,7 +35,10 @@ import { orderBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
import { GetColorParams } from './interfaces/interfaces';
import {
GetColorParams,
GfTreemapChartTooltipContext
} from './interfaces/interfaces';
const { gray, green, red } = OpenColor;
@ -198,10 +201,10 @@ export class GfTreemapChartComponent
min: Math.min(...negativeNetPerformancePercents)
};
const data: ChartConfiguration<'treemap'>['data'] = {
const data: ChartData<'treemap'> = {
datasets: [
{
backgroundColor: (context) => {
backgroundColor: (context: GfTreemapChartTooltipContext) => {
let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
@ -232,7 +235,7 @@ export class GfTreemapChartComponent
key: 'allocationInPercentage',
labels: {
align: 'left',
color: (context) => {
color: (context: GfTreemapChartTooltipContext) => {
let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({
daysInMarket: differenceInDays(
@ -261,7 +264,7 @@ export class GfTreemapChartComponent
},
display: true,
font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }],
formatter: ({ raw }) => {
formatter: ({ raw }: GfTreemapChartTooltipContext) => {
// Round to 4 decimal places
let netPerformancePercentWithCurrencyEffect =
Math.round(
@ -286,10 +289,11 @@ export class GfTreemapChartComponent
position: 'top'
},
spacing: 1,
// @ts-ignore
tree: this.holdings
}
]
} as any;
};
if (this.chartCanvas) {
if (this.chart) {
@ -303,7 +307,7 @@ export class GfTreemapChartComponent
this.getTooltipPluginConfiguration();
this.chart.update();
} else {
this.chart = new Chart(this.chartCanvas.nativeElement, {
this.chart = new Chart<'treemap'>(this.chartCanvas.nativeElement, {
data,
options: {
animation: false,
@ -313,6 +317,7 @@ export class GfTreemapChartComponent
const datasetIndex = activeElements[0].datasetIndex;
const dataset = orderBy(
// @ts-ignore
event.chart.data.datasets[datasetIndex].tree,
['allocationInPercentage'],
['desc']
@ -326,6 +331,7 @@ export class GfTreemapChartComponent
},
onHover: (event, chartElement) => {
if (this.cursor) {
// @ts-ignore
event.native.target.style.cursor = chartElement[0]
? this.cursor
: 'default';
@ -351,8 +357,9 @@ export class GfTreemapChartComponent
locale: this.locale
}),
callbacks: {
label: ({ raw }) => {
const allocationInPercentage = `${((raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`;
// @ts-ignore
label: ({ raw }: GfTreemapChartTooltipContext) => {
const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`;
const name = raw._data.name;
const sign =
raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : '';
@ -361,11 +368,11 @@ export class GfTreemapChartComponent
const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;
if (raw._data.valueInBaseCurrency !== null) {
const value = raw._data.valueInBaseCurrency as number;
const value = raw._data.valueInBaseCurrency;
return [
`${name ?? symbol} (${allocationInPercentage})`,
`${value.toLocaleString(this.locale, {
`${value?.toLocaleString(this.locale, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
})} ${this.baseCurrency}`,

Loading…
Cancel
Save