Browse Source

Task/improve type safety for benchmark comparator component (#6904)

* feat(client): resolve errors

* feat(client): implement view child signal

* fix(client): enforce encapsulation and immutability

* feat(client): implement output signal

* feat(client): make onChangeBenchmark protected

* feat(client): implement input signals
pull/6908/head
Kenrick Tandrian 5 days ago
committed by GitHub
parent
commit
7e9284b72b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html
  2. 81
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

12
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.html

@ -4,7 +4,7 @@
class="align-items-center d-flex flex-grow-1 h5 mb-0 py-2 text-truncate" class="align-items-center d-flex flex-grow-1 h5 mb-0 py-2 text-truncate"
> >
<span i18n>Performance</span> <span i18n>Performance</span>
@if (user?.subscription?.type === 'Basic') { @if (user()?.subscription?.type === 'Basic') {
<gf-premium-indicator class="ml-1" /> <gf-premium-indicator class="ml-1" />
} }
</div> </div>
@ -18,12 +18,12 @@
<mat-label i18n>Compare with...</mat-label> <mat-label i18n>Compare with...</mat-label>
<mat-select <mat-select
name="benchmark" name="benchmark"
[disabled]="user?.subscription?.type === 'Basic'" [disabled]="user()?.subscription?.type === 'Basic'"
[value]="benchmark?.id" [value]="benchmark()?.id"
(selectionChange)="onChangeBenchmark($event.value)" (selectionChange)="onChangeBenchmark($event.value)"
> >
<mat-option [value]="null" /> <mat-option [value]="null" />
@for (symbolProfile of benchmarks; track symbolProfile) { @for (symbolProfile of benchmarks(); track symbolProfile) {
<mat-option [value]="symbolProfile.id">{{ <mat-option [value]="symbolProfile.id">{{
symbolProfile.name symbolProfile.name
}}</mat-option> }}</mat-option>
@ -41,7 +41,7 @@
</div> </div>
</div> </div>
<div class="chart-container"> <div class="chart-container">
@if (isLoading) { @if (isLoading()) {
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
[theme]="{ [theme]="{
@ -53,6 +53,6 @@
<canvas <canvas
#chartCanvas #chartCanvas
class="h-100" class="h-100"
[ngStyle]="{ display: isLoading ? 'none' : 'block' }" [ngStyle]="{ display: isLoading() ? 'none' : 'block' }"
></canvas> ></canvas>
</div> </div>

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

@ -22,12 +22,11 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
type ElementRef, type ElementRef,
EventEmitter, input,
Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
Output, output,
ViewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
@ -68,24 +67,25 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
templateUrl: './benchmark-comparator.component.html' templateUrl: './benchmark-comparator.component.html'
}) })
export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() benchmark: Partial<SymbolProfile>; public readonly benchmark = input<Partial<SymbolProfile>>();
@Input() benchmarkDataItems: LineChartItem[] = []; public readonly benchmarkDataItems = input<LineChartItem[]>([]);
@Input() benchmarks: Partial<SymbolProfile>[]; public readonly benchmarks = input<Partial<SymbolProfile>[]>();
@Input() colorScheme: ColorScheme; public readonly colorScheme = input.required<ColorScheme>();
@Input() isLoading: boolean; public readonly isLoading = input<boolean>();
@Input() locale = getLocale(); public readonly locale = input(getLocale());
@Input() performanceDataItems: LineChartItem[]; public readonly performanceDataItems = input.required<LineChartItem[]>();
@Input() user: User; public readonly user = input<User>();
@Output() benchmarkChanged = new EventEmitter<string>(); public readonly benchmarkChanged = output<string>();
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; protected chart: Chart<'line'>;
protected hasPermissionToAccessAdminControl: boolean;
public chart: Chart<'line'>; protected readonly routerLinkAdminControlMarketData =
public hasPermissionToAccessAdminControl: boolean;
public routerLinkAdminControlMarketData =
internalRoutes.adminControl.subRoutes.marketData.routerLink; internalRoutes.adminControl.subRoutes.marketData.routerLink;
private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
public constructor() { public constructor() {
Chart.register( Chart.register(
annotationPlugin, annotationPlugin,
@ -104,27 +104,27 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
public ngOnChanges() { public ngOnChanges() {
this.hasPermissionToAccessAdminControl = hasPermission( this.hasPermissionToAccessAdminControl = hasPermission(
this.user?.permissions, this.user()?.permissions,
permissions.accessAdminControl permissions.accessAdminControl
); );
if (this.performanceDataItems) { if (this.performanceDataItems()) {
this.initialize(); this.initialize();
} }
} }
public onChangeBenchmark(symbolProfileId: string) {
this.benchmarkChanged.next(symbolProfileId);
}
public ngOnDestroy() { public ngOnDestroy() {
this.chart?.destroy(); this.chart?.destroy();
} }
protected onChangeBenchmark(symbolProfileId: string) {
this.benchmarkChanged.emit(symbolProfileId);
}
private initialize() { private initialize() {
const benchmarkDataValues: Record<string, number> = {}; const benchmarkDataValues: Record<string, number> = {};
for (const { date, value } of this.benchmarkDataItems) { for (const { date, value } of this.benchmarkDataItems()) {
benchmarkDataValues[date] = value; benchmarkDataValues[date] = value;
} }
@ -134,8 +134,11 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`, borderColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
borderWidth: 2, borderWidth: 2,
data: this.performanceDataItems.map(({ date, value }) => { data: this.performanceDataItems().map(({ date, value }) => {
return { x: parseDate(date).getTime(), y: value * 100 }; return {
x: parseDate(date)?.getTime() ?? null,
y: value * 100
};
}), }),
label: $localize`Portfolio` label: $localize`Portfolio`
}, },
@ -143,9 +146,9 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`, borderColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
borderWidth: 2, borderWidth: 2,
data: this.performanceDataItems.map(({ date }) => { data: this.performanceDataItems().map(({ date }) => {
return { return {
x: parseDate(date).getTime(), x: parseDate(date)?.getTime() ?? null,
y: benchmarkDataValues[date] y: benchmarkDataValues[date]
}; };
}), }),
@ -163,7 +166,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart<'line'>(this.chartCanvas().nativeElement, {
data, data,
options: { options: {
animation: false, animation: false,
@ -172,7 +175,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
tension: 0 tension: 0
}, },
point: { point: {
hoverBackgroundColor: getBackgroundColor(this.colorScheme), hoverBackgroundColor: getBackgroundColor(this.colorScheme()),
hoverRadius: 2, hoverRadius: 2,
radius: 0 radius: 0
} }
@ -183,7 +186,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
annotation: { annotation: {
annotations: { annotations: {
yAxis: { yAxis: {
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, borderColor: `rgba(${getTextColor(this.colorScheme())}, 0.1)`,
borderWidth: 1, borderWidth: 1,
scaleID: 'y', scaleID: 'y',
type: 'line', type: 'line',
@ -196,14 +199,14 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: `rgba(${getTextColor(this.colorScheme())}, 0.1)`
} }
}, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: {
border: { border: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, color: `rgba(${getTextColor(this.colorScheme())}, 0.1)`,
width: 1 width: 1
}, },
display: true, display: true,
@ -212,7 +215,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
}, },
type: 'time', type: 'time',
time: { time: {
tooltipFormat: getDateFormatString(this.locale), tooltipFormat: getDateFormatString(this.locale()),
unit: 'year' unit: 'year'
} }
}, },
@ -228,7 +231,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
tick.value === scale.max || tick.value === scale.max ||
tick.value === scale.min tick.value === scale.min
) { ) {
return `rgba(${getTextColor(this.colorScheme)}, 0.1)`; return `rgba(${getTextColor(this.colorScheme())}, 0.1)`;
} }
return 'transparent'; return 'transparent';
@ -247,7 +250,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
} }
}, },
plugins: [ plugins: [
getVerticalHoverLinePlugin(this.chartCanvas, this.colorScheme) getVerticalHoverLinePlugin(this.chartCanvas(), this.colorScheme())
], ],
type: 'line' type: 'line'
}); });
@ -258,8 +261,8 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme, colorScheme: this.colorScheme(),
locale: this.locale, locale: this.locale(),
unit: '%' unit: '%'
}), }),
mode: 'index', mode: 'index',

Loading…
Cancel
Save