Browse Source

fix(client): treemap chart type safety

pull/6277/head
KenTandrian 7 days ago
parent
commit
0dac0b8412
  1. 2
      libs/common/src/lib/chart-helper.ts
  2. 2
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  3. 16
      libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts
  4. 48
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

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

@ -54,7 +54,7 @@ export function getTooltipOptions<T extends ChartType>({
bodyColor: `rgb(${getTextColor(colorScheme)})`, bodyColor: `rgb(${getTextColor(colorScheme)})`,
borderWidth: 1, borderWidth: 1,
borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`,
// @ts-expect-error: no need to set all attributes in callbacks. // @ts-expect-error: no need to set all attributes in callbacks
callbacks: { callbacks: {
label: (context) => { label: (context) => {
let label = (context.dataset as ControllerDatasetOptions).label ?? ''; let label = (context.dataset as ControllerDatasetOptions).label ?? '';

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

@ -434,7 +434,7 @@ export class GfPortfolioProportionChartComponent
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: {
label: (context) => { label: (context) => {
const labelIndex = const labelIndex =

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

@ -1,5 +1,21 @@
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ScriptableContext, TooltipItem } from 'chart.js';
import { TreemapDataPoint } from 'chartjs-chart-treemap';
export interface GetColorParams { export interface GetColorParams {
annualizedNetPerformancePercent: number; annualizedNetPerformancePercent: number;
negativeNetPerformancePercentsRange: { max: number; min: number }; negativeNetPerformancePercentsRange: { max: number; min: number };
positiveNetPerformancePercentsRange: { max: number; min: number }; positiveNetPerformancePercentsRange: { max: number; min: number };
} }
export interface GfTreemapScriptableContext extends ScriptableContext<'treemap'> {
raw: TreemapDataPoint & {
_data: PortfolioPosition;
};
}
export interface GfTreemapTooltipItem extends TooltipItem<'treemap'> {
raw: TreemapDataPoint & {
_data: PortfolioPosition;
};
}

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

@ -25,7 +25,7 @@ import {
} 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 { ChartConfiguration } from 'chart.js'; import type { TooltipOptions, ChartData } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
import { Chart, Tooltip } from 'chart.js'; import { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
@ -35,7 +35,11 @@ import { orderBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color'; import OpenColor from 'open-color';
import { GetColorParams } from './interfaces/interfaces'; import type {
GetColorParams,
GfTreemapScriptableContext,
GfTreemapTooltipItem
} from './interfaces/interfaces';
const { gray, green, red } = OpenColor; const { gray, green, red } = OpenColor;
@ -198,10 +202,10 @@ export class GfTreemapChartComponent
min: Math.min(...negativeNetPerformancePercents) min: Math.min(...negativeNetPerformancePercents)
}; };
const data: ChartConfiguration<'treemap'>['data'] = { const data: ChartData<'treemap'> = {
datasets: [ datasets: [
{ {
backgroundColor: (context) => { backgroundColor: (context: GfTreemapScriptableContext) => {
let annualizedNetPerformancePercent = let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
@ -232,7 +236,7 @@ export class GfTreemapChartComponent
key: 'allocationInPercentage', key: 'allocationInPercentage',
labels: { labels: {
align: 'left', align: 'left',
color: (context) => { color: (context: GfTreemapScriptableContext) => {
let annualizedNetPerformancePercent = let annualizedNetPerformancePercent =
getAnnualizedPerformancePercent({ getAnnualizedPerformancePercent({
daysInMarket: differenceInDays( daysInMarket: differenceInDays(
@ -261,7 +265,7 @@ export class GfTreemapChartComponent
}, },
display: true, display: true,
font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }], font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }],
formatter: ({ raw }) => { formatter: ({ raw }: GfTreemapScriptableContext) => {
// Round to 4 decimal places // Round to 4 decimal places
let netPerformancePercentWithCurrencyEffect = let netPerformancePercentWithCurrencyEffect =
Math.round( Math.round(
@ -286,32 +290,34 @@ export class GfTreemapChartComponent
position: 'top' position: 'top'
}, },
spacing: 1, spacing: 1,
// @ts-expect-error: should be PortfolioPosition[]
tree: this.holdings tree: this.holdings
} }
] ]
} as any; };
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.tooltip = this.chart.options.plugins.tooltip =
this.getTooltipPluginConfiguration() as unknown; this.getTooltipPluginConfiguration();
this.chart.update(); this.chart.update();
} else { } else {
this.chart = new Chart(this.chartCanvas.nativeElement, { this.chart = new Chart<'treemap'>(this.chartCanvas.nativeElement, {
data, data,
options: { options: {
animation: false, animation: false,
onClick: (event, activeElements) => { onClick: (_, activeElements, chart: Chart<'treemap'>) => {
try { try {
const dataIndex = activeElements[0].index; const dataIndex = activeElements[0].index;
const datasetIndex = activeElements[0].datasetIndex; const datasetIndex = activeElements[0].datasetIndex;
const dataset = orderBy( const dataset = orderBy(
event.chart.data.datasets[datasetIndex].tree, chart.data.datasets[datasetIndex].tree,
['allocationInPercentage'], ['allocationInPercentage'],
['desc'] ['desc']
); ) as PortfolioPosition[];
const dataSource: DataSource = dataset[dataIndex].dataSource; const dataSource: DataSource = dataset[dataIndex].dataSource;
const symbol: string = dataset[dataIndex].symbol; const symbol: string = dataset[dataIndex].symbol;
@ -321,15 +327,14 @@ export class GfTreemapChartComponent
}, },
onHover: (event, chartElement) => { onHover: (event, chartElement) => {
if (this.cursor) { if (this.cursor) {
event.native.target.style.cursor = chartElement[0] (event.native?.target as HTMLElement).style.cursor =
? this.cursor chartElement[0] ? this.cursor : 'default';
: 'default';
} }
}, },
plugins: { plugins: {
tooltip: this.getTooltipPluginConfiguration() tooltip: this.getTooltipPluginConfiguration()
} }
} as unknown, },
type: 'treemap' type: 'treemap'
}); });
} }
@ -338,16 +343,17 @@ export class GfTreemapChartComponent
this.isLoading = false; this.isLoading = false;
} }
private getTooltipPluginConfiguration() { 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
callbacks: { callbacks: {
label: ({ raw }) => { label: ({ raw }: GfTreemapTooltipItem) => {
const allocationInPercentage = `${((raw._data.allocationInPercentage as number) * 100).toFixed(2)}%`; const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`;
const name = raw._data.name; const name = raw._data.name;
const sign = const sign =
raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : '';
@ -356,11 +362,11 @@ export class GfTreemapChartComponent
const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`;
if (raw._data.valueInBaseCurrency !== null) { if (raw._data.valueInBaseCurrency !== null) {
const value = raw._data.valueInBaseCurrency as number; const value = raw._data.valueInBaseCurrency;
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}`,

Loading…
Cancel
Save