From f6f6a9df43811293c9165febce0379aaef3c267c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:10:26 +0200 Subject: [PATCH] Feature/highlight negative values in performance timeline chart (#8198) * Highlight negative values in performance timeline chart * Update changelog --- CHANGELOG.md | 1 + .../portfolio/analysis/analysis-page.html | 1 + .../investment-chart.component.stories.ts | 60 +++++++++++++++++++ .../investment-chart.component.ts | 20 ++++++- 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5c182608e..a9de26bae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Highlighted the negative values in the performance timeline chart on the analysis page (experimental) - Moved the investment chart component to `@ghostfolio/ui` - Improved the matching of assets in the import of activities with _Ghostfolio AI_ to prefer the holdings of the user (experimental) - Extended the holdings table by the ISIN in the _Copy AI prompt to clipboard_ actions on the analysis page and in the tool to get the portfolio of the server of the Model Context Protocol (MCP) (experimental) diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 255ede038a..bd8451cd38 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -450,6 +450,7 @@ [benchmarkDataItems]="performancePercentagesByYear" [benchmarkDataLabel]="performanceTimelineDataLabel" [colorScheme]="user?.settings?.colorScheme" + [highlightNegativeValues]="true" [isInPercentage]="true" [isLoading]="isLoadingPerformanceTimelineChart" [locale]="user?.settings?.locale" diff --git a/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts b/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts index 225857c7da..d3c7984db5 100644 --- a/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts +++ b/libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts @@ -16,6 +16,65 @@ export default { type Story = StoryObj; +export const InvestmentsByMonth: Story = { + args: { + benchmarkDataItems: [ + { + date: '2025-01-01', + investment: 1000 + }, + { + date: '2025-02-01', + investment: 1000 + }, + { + date: '2025-03-01', + investment: 500 + }, + { + date: '2025-04-01', + investment: 1500 + }, + { + date: '2025-05-01', + investment: -2500 + }, + { + date: '2025-06-01', + investment: 1000 + }, + { + date: '2025-07-01', + investment: 0 + }, + { + date: '2025-08-01', + investment: 2000 + }, + { + date: '2025-09-01', + investment: 1000 + }, + { + date: '2025-10-01', + investment: -800 + }, + { + date: '2025-11-01', + investment: 1000 + }, + { + date: '2025-12-01', + investment: 1200 + } + ], + benchmarkDataLabel: 'Invested Capital', + currency: 'USD', + groupBy: 'month', + locale: 'en-US' + } +}; + export const PerformanceByYear: Story = { args: { benchmarkDataItems: [ @@ -70,6 +129,7 @@ export const PerformanceByYear: Story = { ], benchmarkDataLabel: 'Net Performance', groupBy: 'year', + highlightNegativeValues: true, isInPercentage: true } }; diff --git a/libs/ui/src/lib/investment-chart/investment-chart.component.ts b/libs/ui/src/lib/investment-chart/investment-chart.component.ts index 86af956d45..eeb22d2a30 100644 --- a/libs/ui/src/lib/investment-chart/investment-chart.component.ts +++ b/libs/ui/src/lib/investment-chart/investment-chart.component.ts @@ -32,6 +32,7 @@ import { LineController, LineElement, PointElement, + type ScriptableContext, type ScriptableLineSegmentContext, TimeScale, Tooltip, @@ -41,6 +42,7 @@ import 'chartjs-adapter-date-fns'; import { type AnnotationOptions } from 'chartjs-plugin-annotation'; import { isFuture } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import OpenColor from 'open-color'; import { getTimeSeriesTooltipOptions, @@ -48,6 +50,8 @@ import { registerChartConfiguration } from '../chart'; +const { orange } = OpenColor; + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [NgxSkeletonLoaderModule], @@ -61,6 +65,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { @Input() public colorScheme?: ColorScheme; @Input() public currency?: string; @Input() public groupBy?: GroupBy; + @Input() public highlightNegativeValues = false; @Input() public historicalDataItems: LineChartItem[] = []; @Input() public isInPercentage?: boolean = false; @Input() public isLoading = false; @@ -117,6 +122,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { const isBarChart = !!this.groupBy; const benchmarkColorRgb = isBarChart ? primaryColorRgb : secondaryColorRgb; + const benchmarkColor = `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`; // Create clones of the input data this.investments = this.benchmarkDataItems.map((item) => @@ -133,8 +139,18 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { }), datasets: [ { - backgroundColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`, - borderColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`, + backgroundColor: (context: ScriptableContext<'bar' | 'line'>) => { + if ( + (context.parsed?.y ?? 0) < 0 && + isBarChart && + this.highlightNegativeValues + ) { + return orange[5]; + } + + return benchmarkColor; + }, + borderColor: benchmarkColor, borderWidth: isBarChart ? 0 : 1, data: this.investments.map(({ date, investment }) => { return {