From 6b7f4093c30c757b2ecd4399e3a760e53284a64d Mon Sep 17 00:00:00 2001 From: mister-ideas <36502095+mister-ideas@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:16:24 +0200 Subject: [PATCH] Feature/show the whole current period on the time axis of the overview chart --- .../home-overview/home-overview.component.ts | 5 +- libs/common/src/lib/chart-helper.spec.ts | 53 +++++++++++++- libs/common/src/lib/chart-helper.ts | 69 ++++++++++++++++++- .../lib/line-chart/line-chart.component.ts | 2 + 4 files changed, 123 insertions(+), 6 deletions(-) diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index 1abe7aeb77..cc98251232 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -63,7 +63,10 @@ export class GfHomeOverviewComponent implements OnInit { // A line through only two points (e.g. the close of the previous day and // now) is drawn as a misleading slope across the whole width protected readonly hasEnoughChartData = computed(() => { - return (this.historicalDataItems()?.length ?? 0) >= MINIMUM_CHART_ITEMS; + const dateRange = this.user()?.settings?.dateRange; + const minimum = dateRange === '1d' ? 1 : MINIMUM_CHART_ITEMS; + + return (this.historicalDataItems()?.length ?? 0) >= minimum; }); protected readonly holdings = signal( undefined diff --git a/libs/common/src/lib/chart-helper.spec.ts b/libs/common/src/lib/chart-helper.spec.ts index 2cd77e9f96..efb66a36e9 100644 --- a/libs/common/src/lib/chart-helper.spec.ts +++ b/libs/common/src/lib/chart-helper.spec.ts @@ -1,8 +1,8 @@ -import { getTimeAxisUnit } from './chart-helper'; +import { getTimeAxisBounds, getTimeAxisUnit } from './chart-helper'; describe('getTimeAxisUnit', () => { it.each([ - ['1d', 'day'], + ['1d', 'hour'], ['wtd', 'day'], ['mtd', 'day'], ['ytd', 'month'], @@ -15,3 +15,52 @@ describe('getTimeAxisUnit', () => { expect(getTimeAxisUnit(dateRange)).toBe(unit); }); }); + +describe('getTimeAxisBounds', () => { + const reference = new Date(2026, 9, 21, 15, 30); + + it('spans the whole current year for ytd', () => { + const { max, min } = getTimeAxisBounds('ytd', reference); + + expect(new Date(min).toDateString()).toBe( + new Date(2026, 0, 1).toDateString() + ); + expect(new Date(max).toDateString()).toBe( + new Date(2026, 11, 31).toDateString() + ); + }); + + it('spans the whole current month for mtd', () => { + const { max, min } = getTimeAxisBounds('mtd', reference); + + expect(new Date(min).getDate()).toBe(1); + expect(new Date(max).getDate()).toBe(31); + }); + + it('spans the whole current week for wtd', () => { + const { max, min } = getTimeAxisBounds('wtd', reference); + + expect(new Date(min).getDay()).toBe(1); + expect(new Date(max).getDay()).toBe(0); + }); + + it('spans the whole current day for 1d', () => { + const { max, min } = getTimeAxisBounds('1d', reference); + + expect(new Date(min).getHours()).toBe(0); + expect(new Date(max).getHours()).toBe(23); + }); + + it('spans a past year', () => { + const { max, min } = getTimeAxisBounds('2024', reference); + + expect(new Date(min).getFullYear()).toBe(2024); + expect(new Date(max).getMonth()).toBe(11); + }); + + it('does not fix the bounds of rolling ranges', () => { + expect(getTimeAxisBounds('1y', reference)).toEqual({}); + expect(getTimeAxisBounds('5y', reference)).toEqual({}); + expect(getTimeAxisBounds('max', reference)).toEqual({}); + }); +}); diff --git a/libs/common/src/lib/chart-helper.ts b/libs/common/src/lib/chart-helper.ts index b4bc5f36c6..3de8ade41e 100644 --- a/libs/common/src/lib/chart-helper.ts +++ b/libs/common/src/lib/chart-helper.ts @@ -12,7 +12,17 @@ import type { TooltipPosition } from 'chart.js'; import type { AnnotationOptions } from 'chartjs-plugin-annotation'; -import { format } from 'date-fns'; +import { + endOfDay, + endOfMonth, + endOfWeek, + endOfYear, + format, + startOfDay, + startOfMonth, + startOfWeek, + startOfYear +} from 'date-fns'; import { DATE_FORMAT, @@ -67,9 +77,10 @@ export function getChartElementsOptions( */ export function getTimeAxisUnit( aDateRange?: DateRange -): 'day' | 'month' | 'year' { +): 'day' | 'hour' | 'month' | 'year' { switch (aDateRange) { case '1d': + return 'hour'; case 'mtd': case 'wtd': return 'day'; @@ -83,18 +94,68 @@ export function getTimeAxisUnit( } } +/** + * Returns the bounds of the time axis for the date ranges which have a fixed + * end (the current week, month or year), so that the chart shows the whole + * period with the part still to come left empty. Other date ranges (rolling + * periods and `max`) end at the latest data point. + */ +export function getTimeAxisBounds( + aDateRange?: DateRange, + aReferenceDate = new Date() +): { max?: number; min?: number } { + switch (aDateRange) { + case '1d': + return { + max: endOfDay(aReferenceDate).getTime(), + min: startOfDay(aReferenceDate).getTime() + }; + case 'wtd': + return { + max: endOfWeek(aReferenceDate, { weekStartsOn: 1 }).getTime(), + min: startOfWeek(aReferenceDate, { weekStartsOn: 1 }).getTime() + }; + case 'mtd': + return { + max: endOfMonth(aReferenceDate).getTime(), + min: startOfMonth(aReferenceDate).getTime() + }; + case 'ytd': + return { + max: endOfYear(aReferenceDate).getTime(), + min: startOfYear(aReferenceDate).getTime() + }; + default: { + if (typeof aDateRange === 'string' && /^\d{4}$/.test(aDateRange)) { + const year = new Date(Number(aDateRange), 0, 1); + + return { + max: endOfYear(year).getTime(), + min: startOfYear(year).getTime() + }; + } + + return {}; + } + } +} + export function getTimeAxisOptions({ borderWidth = 1, colorScheme, display = true, locale = getLocale(), + max, + min, unit = 'year' }: { borderWidth?: number; colorScheme?: ColorScheme; display?: boolean; locale?: string; - unit?: 'day' | 'month' | 'year'; + max?: number; + min?: number; + unit?: 'day' | 'hour' | 'month' | 'year'; }): ScaleOptions<'time'> { return { display, @@ -105,6 +166,8 @@ export function getTimeAxisOptions({ grid: { display: false }, + max, + min, ticks: { autoSkip: true, maxRotation: 0, diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index bda6d0a38c..7ace37da54 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -1,5 +1,6 @@ import { getChartBorderColor, + getTimeAxisBounds, getTimeAxisOptions, getTimeAxisUnit, getValueAxisOptions, @@ -239,6 +240,7 @@ export class GfLineChartComponent colorScheme: this.colorScheme, display: this.showXAxis, locale: this.locale, + ...getTimeAxisBounds(this.dateRange), unit: getTimeAxisUnit(this.dateRange) }), y: {