Browse Source

Task/improve type safety in treemap chart component (#7325)

* fix(ui): resolve type errors

* feat(ui): enforce encapsulation

* feat(ui): implement viewChild signal

* feat(ui): implement output signal

* feat(ui): implement input signal
pull/7317/head^2
Kenrick Tandrian 2 days ago
committed by GitHub
parent
commit
b4a12341c5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  2. 74
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

8
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -58,7 +58,7 @@ export class GfHomeHoldingsComponent implements OnInit {
protected hasImpersonationId: boolean; protected hasImpersonationId: boolean;
protected hasPermissionToAccessHoldingsChart: boolean; protected hasPermissionToAccessHoldingsChart: boolean;
protected hasPermissionToCreateActivity: boolean; protected hasPermissionToCreateActivity: boolean;
protected holdings: PortfolioPosition[]; protected holdings: PortfolioPosition[] | undefined;
protected holdingType: HoldingType = 'ACTIVE'; protected holdingType: HoldingType = 'ACTIVE';
protected readonly holdingTypeOptions: ToggleOption[] = [ protected readonly holdingTypeOptions: ToggleOption[] = [
{ label: $localize`Active`, value: 'ACTIVE' }, { label: $localize`Active`, value: 'ACTIVE' },
@ -181,8 +181,8 @@ export class GfHomeHoldingsComponent implements OnInit {
this.viewModeFormControl.setValue( this.viewModeFormControl.setValue(
this.deviceType === 'mobile' this.deviceType === 'mobile'
? GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE ? GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
: this.user?.settings?.holdingsViewMode || : (this.user?.settings?.holdingsViewMode ??
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE, GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE),
{ emitEvent: false } { emitEvent: false }
); );
} else if (this.holdingType === 'CLOSED') { } else if (this.holdingType === 'CLOSED') {
@ -192,7 +192,7 @@ export class GfHomeHoldingsComponent implements OnInit {
); );
} }
this.holdings = []; this.holdings = undefined;
this.fetchHoldings() this.fetchHoldings()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))

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

@ -15,18 +15,16 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
ElementRef, ElementRef,
EventEmitter, input,
Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output, output,
ViewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import type { ChartData, TooltipOptions } from 'chart.js'; import type { ChartData, TooltipOptions } from 'chart.js';
import { LinearScale } from 'chart.js'; import { Chart, LinearScale, Tooltip } from 'chart.js';
import { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { differenceInDays, max } from 'date-fns'; import { differenceInDays, max } from 'date-fns';
@ -52,33 +50,31 @@ const { gray, green, red } = OpenColor;
export class GfTreemapChartComponent export class GfTreemapChartComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
{ {
@Input() baseCurrency: string; public readonly baseCurrency = input.required<string>();
@Input() colorScheme: ColorScheme; public readonly colorScheme = input.required<ColorScheme>();
@Input() cursor: string; public readonly cursor = input.required<string>();
@Input() dateRange: DateRange; public readonly dateRange = input.required<DateRange>();
@Input() holdings: PortfolioPosition[]; public readonly holdings = input<PortfolioPosition[]>();
@Input() locale = getLocale(); public readonly locale = input<string>(getLocale());
@Output() treemapChartClicked = new EventEmitter<AssetProfileIdentifier>(); public readonly treemapChartClicked = output<AssetProfileIdentifier>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; protected isLoading = true;
public chart: Chart<'treemap'>; private chart: Chart<'treemap'>;
public isLoading = true; private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
public constructor() { public constructor() {
Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement); Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement);
} }
public ngAfterViewInit() { public ngAfterViewInit() {
if (this.holdings) { this.initialize();
this.initialize();
}
} }
public ngOnChanges() { public ngOnChanges() {
if (this.holdings) { this.initialize();
this.initialize();
}
} }
public ngOnDestroy() { public ngOnDestroy() {
@ -159,13 +155,19 @@ export class GfTreemapChartComponent
} }
private initialize() { private initialize() {
const holdings = this.holdings();
if (!holdings) {
return;
}
this.isLoading = true; this.isLoading = true;
const { endDate, startDate } = getIntervalFromDateRange({ const { endDate, startDate } = getIntervalFromDateRange({
dateRange: this.dateRange dateRange: this.dateRange()
}); });
const netPerformancePercentsWithCurrencyEffect = this.holdings.map( const netPerformancePercentsWithCurrencyEffect = holdings.map(
({ dateOfFirstActivity, netPerformancePercentWithCurrencyEffect }) => { ({ dateOfFirstActivity, netPerformancePercentWithCurrencyEffect }) => {
return getAnnualizedPerformancePercent({ return getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
@ -301,12 +303,12 @@ export class GfTreemapChartComponent
}, },
spacing: 1, spacing: 1,
// @ts-expect-error: should be PortfolioPosition[] // @ts-expect-error: should be PortfolioPosition[]
tree: this.holdings tree: this.holdings()
} }
] ]
}; };
if (this.chartCanvas) { if (this.chartCanvas()) {
if (this.chart) { if (this.chart) {
this.chart.data = data; this.chart.data = data;
this.chart.options.plugins ??= {}; this.chart.options.plugins ??= {};
@ -315,7 +317,7 @@ export class GfTreemapChartComponent
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart<'treemap'>(this.chartCanvas.nativeElement, { this.chart = new Chart<'treemap'>(this.chartCanvas().nativeElement, {
data, data,
options: { options: {
animation: false, animation: false,
@ -339,9 +341,9 @@ export class GfTreemapChartComponent
} catch {} } catch {}
}, },
onHover: (event, chartElement) => { onHover: (event, chartElement) => {
if (this.cursor) { if (this.cursor()) {
(event.native?.target as HTMLElement).style.cursor = (event.native?.target as HTMLElement).style.cursor =
chartElement[0] ? this.cursor : 'default'; chartElement[0] ? this.cursor() : 'default';
} }
}, },
plugins: { plugins: {
@ -359,9 +361,9 @@ export class GfTreemapChartComponent
private getTooltipPluginConfiguration(): Partial<TooltipOptions<'treemap'>> { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'treemap'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme(),
currency: this.baseCurrency, currency: this.baseCurrency(),
locale: this.locale locale: this.locale()
}), }),
// @ts-expect-error: no need to set all attributes in callbacks // @ts-expect-error: no need to set all attributes in callbacks
callbacks: { callbacks: {
@ -381,19 +383,19 @@ export class GfTreemapChartComponent
return [ return [
`${name ?? symbol} (${allocationInPercentage})`, `${name ?? symbol} (${allocationInPercentage})`,
`${value?.toLocaleString(this.locale, { `${value?.toLocaleString(this.locale(), {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
})} ${this.baseCurrency}`, })} ${this.baseCurrency()}`,
'', '',
$localize`Change` + ' (' + $localize`Performance` + ')', $localize`Change` + ' (' + $localize`Performance` + ')',
`${sign}${raw._data.netPerformanceWithCurrencyEffect.toLocaleString( `${sign}${raw._data.netPerformanceWithCurrencyEffect.toLocaleString(
this.locale, this.locale(),
{ {
maximumFractionDigits: 2, maximumFractionDigits: 2,
minimumFractionDigits: 2 minimumFractionDigits: 2
} }
)} ${this.baseCurrency} (${netPerformanceInPercentageWithSign})` )} ${this.baseCurrency()} (${netPerformanceInPercentageWithSign})`
]; ];
} else { } else {
return [ return [

Loading…
Cancel
Save