diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e705c788e..642b8e4748 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- Fixed the alignment of the average price in the chart of the holding detail dialog for custom asset profiles + ## 3.74.0 - 2026-09-27 ### Added diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index d8385d70e2..02bfd25e5c 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -9,6 +9,7 @@ import { } from '@ghostfolio/common/config'; import { CreateOrderDto } from '@ghostfolio/common/dtos'; import { + alignLineChartItemsToDates, DATE_FORMAT, downloadAsFile, getCountryName @@ -68,6 +69,7 @@ import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { NavigationStart, Router, RouterModule } from '@angular/router'; +import { utc } from '@date-fns/utc'; import { IonIcon } from '@ionic/angular/standalone'; import { MarketData, Tag } from '@prisma/client'; import { isUUID } from 'class-validator'; @@ -719,12 +721,19 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.historicalDataItems = this.marketDataItems.map( ({ date, marketPrice }) => { return { - date: format(date, DATE_FORMAT), + date: format(date, DATE_FORMAT, { in: utc }), value: marketPrice }; } ); + this.benchmarkDataItems = alignLineChartItemsToDates({ + dates: this.historicalDataItems.map(({ date }) => { + return date; + }), + items: this.benchmarkDataItems + }); + this.changeDetectorRef.markForCheck(); }); } diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index d1130f0d75..cbf2bd1e99 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -3,6 +3,7 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { + alignLineChartItemsToDates, canApplyFiltersToAccess, extractNumberFromString, getCountryCodeFromCurrency, @@ -23,6 +24,66 @@ import { import { UserSettings } from '@ghostfolio/common/interfaces'; describe('Helper', () => { + describe('Align line chart items to dates', () => { + it('Dates before the first item', () => { + expect( + alignLineChartItemsToDates({ + dates: ['2025-01-02', '2025-01-03'], + items: [{ date: '2026-01-09', value: 100 }] + }) + ).toEqual([ + { date: '2025-01-02', value: null }, + { date: '2025-01-03', value: null } + ]); + }); + + it('Dates on and after the items', () => { + expect( + alignLineChartItemsToDates({ + dates: [ + '2026-01-08', + '2026-01-09', + '2026-01-10', + '2026-01-12', + '2026-01-13' + ], + items: [ + { date: '2026-01-09', value: 100 }, + { date: '2026-01-12', value: 110 } + ] + }) + ).toEqual([ + { date: '2026-01-08', value: null }, + { date: '2026-01-09', value: 100 }, + { date: '2026-01-10', value: 100 }, + { date: '2026-01-12', value: 110 }, + { date: '2026-01-13', value: 110 } + ]); + }); + + it('Null values', () => { + expect( + alignLineChartItemsToDates({ + dates: ['2026-01-09', '2026-01-10', '2026-01-11'], + items: [ + { date: '2026-01-09', value: 100 }, + { date: '2026-01-10', value: null } + ] + }) + ).toEqual([ + { date: '2026-01-09', value: 100 }, + { date: '2026-01-10', value: null }, + { date: '2026-01-11', value: null } + ]); + }); + + it('No items', () => { + expect( + alignLineChartItemsToDates({ dates: ['2026-01-09'], items: [] }) + ).toEqual([{ date: '2026-01-09', value: null }]); + }); + }); + describe('Can apply filters to access', () => { it('An access of the model context protocol', () => { expect(canApplyFiltersToAccess({ type: 'MCP' })).toEqual(false); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 6e50911322..769d693572 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -57,6 +57,7 @@ import { AssetProfileIdentifier, AssetProfileItem, Benchmark, + NullableLineChartItem, PortfolioPosition, UserSettings } from './interfaces'; @@ -91,6 +92,27 @@ const USER_SETTINGS_KEYS_OF_AUTHENTICATED_USER: (keyof UserSettings)[] = [ const VALID_COUNTRY_CODES = new Set([...Object.keys(countries), 'EU']); +export function alignLineChartItemsToDates({ + dates, + items +}: { + dates: string[]; + items: NullableLineChartItem[]; +}): NullableLineChartItem[] { + let index = -1; + + return dates.map((date) => { + while ( + index + 1 < items.length && + !isAfter(parseISO(items[index + 1].date), parseISO(date)) + ) { + index++; + } + + return { date, value: items[index]?.value ?? null }; + }); +} + export function applyAssetProfileOverrides>( assetProfile: T, assetProfileOverrides: AssetProfileOverrides | null