From e8624ab0613a280ac88581d57f8d27f64527fefb Mon Sep 17 00:00:00 2001 From: mister-ideas <36502095+mister-ideas@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:55:41 +0200 Subject: [PATCH] Feature/draw a baseline in the performance charts --- CHANGELOG.md | 1 + .../home-overview/home-overview.html | 1 + libs/common/src/lib/chart-helper.spec.ts | 14 ++++ libs/common/src/lib/chart-helper.ts | 77 +++++++++++++++++-- .../lib/line-chart/line-chart.component.ts | 9 +++ 5 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 libs/common/src/lib/chart-helper.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 297677b966..8eea452e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added a dashed baseline (0 %) to the performance chart on the overview page and made the zero line of the benchmark comparator and the investment chart dashed and more visible - Added a link to the holdings tab in the holdings table on the overview tab of the home page (experimental) ### Changed diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index 0f9e1bcfae..28416faeae 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -70,6 +70,7 @@ { + it('returns a dashed horizontal line at the value', () => { + expect( + getBaselineAnnotation({ colorScheme: 'LIGHT', value: 0 }) + ).toMatchObject({ + borderDash: [4, 4], + scaleID: 'y', + type: 'line', + value: 0 + }); + }); +}); diff --git a/libs/common/src/lib/chart-helper.ts b/libs/common/src/lib/chart-helper.ts index 72b629556a..175b83cca5 100644 --- a/libs/common/src/lib/chart-helper.ts +++ b/libs/common/src/lib/chart-helper.ts @@ -217,6 +217,50 @@ export function getValueAxisOptions({ }; } +/** + * Returns a plugin which draws a dashed horizontal line at the baseline of a + * chart above the datasets. Unlike an annotation, it does not follow the + * custom animations of the data points + */ +export function getBaselinePlugin({ + colorScheme, + getValue +}: { + colorScheme?: ColorScheme; + getValue: () => number | undefined; +}): Plugin { + return { + afterDatasetsDraw: (chart) => { + const value = getValue(); + const yScale = chart.scales['y']; + + if (value === undefined || !yScale) { + return; + } + + const y = yScale.getPixelForValue(value); + const { bottom, left, right, top } = chart.chartArea; + + if (!Number.isFinite(y) || y < top || y > bottom) { + return; + } + + const context = chart.ctx; + + context.save(); + context.beginPath(); + context.lineWidth = 1; + context.setLineDash(BASELINE_DASH); + context.strokeStyle = getBaselineColor(colorScheme); + context.moveTo(left, y); + context.lineTo(right, y); + context.stroke(); + context.restore(); + }, + id: 'baseline' + }; +} + export function getVerticalHoverLinePlugin( chartCanvas: ElementRef, colorScheme?: ColorScheme @@ -253,18 +297,41 @@ export function getVerticalHoverLinePlugin( }; } -export function getZeroLineAnnotation( - colorScheme?: ColorScheme -): AnnotationOptions<'line'> { +const BASELINE_DASH = [4, 4]; + +function getBaselineColor(colorScheme?: ColorScheme) { + return `rgba(${getTextColor(colorScheme)}, 0.4)`; +} + +/** + * Returns a dashed horizontal line at the baseline of a chart (e.g. 0 % for + * the performance), which stands out from the grid lines + */ +export function getBaselineAnnotation({ + colorScheme, + value = 0 +}: { + colorScheme?: ColorScheme; + value?: number; +}): AnnotationOptions<'line'> { return { - borderColor: getChartBorderColor(colorScheme), + borderColor: getBaselineColor(colorScheme), + borderDash: BASELINE_DASH, borderWidth: 1, + // Above the area of a filled line + drawTime: 'afterDatasetsDraw', scaleID: 'y', type: 'line', - value: 0 + value }; } +export function getZeroLineAnnotation( + colorScheme?: ColorScheme +): AnnotationOptions<'line'> { + return getBaselineAnnotation({ colorScheme }); +} + export function transformTickToAbbreviation(value: number) { if (value === 0) { return '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 d910033bc0..61c0988033 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -1,4 +1,5 @@ import { + getBaselinePlugin, getChartBorderColor, getTimeAxisOptions, getValueAxisOptions, @@ -58,6 +59,8 @@ export class GfLineChartComponent { @Input() benchmarkDataItems: NullableLineChartItem[] = []; @Input() benchmarkLabel = ''; + /** Value of a horizontal reference line (e.g. 0 for a change in percent) */ + @Input() baseline?: number; @Input() colorScheme?: ColorScheme; @Input() currency?: string; @Input() historicalDataItems: LineChartItem[] | null; @@ -277,6 +280,12 @@ export class GfLineChartComponent spanGaps: true }, plugins: [ + getBaselinePlugin({ + colorScheme: this.colorScheme, + getValue: () => { + return this.baseline; + } + }), getVerticalHoverLinePlugin(this.chartCanvas, this.colorScheme) ], type: 'line'