Browse Source

Feature/highlight negative values in performance timeline chart (#8198)

* Highlight negative values in performance timeline chart

* Update changelog
pull/8205/head
Thomas Kaul 12 hours ago
committed by GitHub
parent
commit
f6f6a9df43
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 1
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  3. 60
      libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts
  4. 20
      libs/ui/src/lib/investment-chart/investment-chart.component.ts

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Highlighted the negative values in the performance timeline chart on the analysis page (experimental)
- Moved the investment chart component to `@ghostfolio/ui` - 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) - 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) - 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)

1
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -450,6 +450,7 @@
[benchmarkDataItems]="performancePercentagesByYear" [benchmarkDataItems]="performancePercentagesByYear"
[benchmarkDataLabel]="performanceTimelineDataLabel" [benchmarkDataLabel]="performanceTimelineDataLabel"
[colorScheme]="user?.settings?.colorScheme" [colorScheme]="user?.settings?.colorScheme"
[highlightNegativeValues]="true"
[isInPercentage]="true" [isInPercentage]="true"
[isLoading]="isLoadingPerformanceTimelineChart" [isLoading]="isLoadingPerformanceTimelineChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

60
libs/ui/src/lib/investment-chart/investment-chart.component.stories.ts

@ -16,6 +16,65 @@ export default {
type Story = StoryObj<GfInvestmentChartComponent>; type Story = StoryObj<GfInvestmentChartComponent>;
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 = { export const PerformanceByYear: Story = {
args: { args: {
benchmarkDataItems: [ benchmarkDataItems: [
@ -70,6 +129,7 @@ export const PerformanceByYear: Story = {
], ],
benchmarkDataLabel: 'Net Performance', benchmarkDataLabel: 'Net Performance',
groupBy: 'year', groupBy: 'year',
highlightNegativeValues: true,
isInPercentage: true isInPercentage: true
} }
}; };

20
libs/ui/src/lib/investment-chart/investment-chart.component.ts

@ -32,6 +32,7 @@ import {
LineController, LineController,
LineElement, LineElement,
PointElement, PointElement,
type ScriptableContext,
type ScriptableLineSegmentContext, type ScriptableLineSegmentContext,
TimeScale, TimeScale,
Tooltip, Tooltip,
@ -41,6 +42,7 @@ import 'chartjs-adapter-date-fns';
import { type AnnotationOptions } from 'chartjs-plugin-annotation'; import { type AnnotationOptions } from 'chartjs-plugin-annotation';
import { isFuture } from 'date-fns'; import { isFuture } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color';
import { import {
getTimeSeriesTooltipOptions, getTimeSeriesTooltipOptions,
@ -48,6 +50,8 @@ import {
registerChartConfiguration registerChartConfiguration
} from '../chart'; } from '../chart';
const { orange } = OpenColor;
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgxSkeletonLoaderModule], imports: [NgxSkeletonLoaderModule],
@ -61,6 +65,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
@Input() public colorScheme?: ColorScheme; @Input() public colorScheme?: ColorScheme;
@Input() public currency?: string; @Input() public currency?: string;
@Input() public groupBy?: GroupBy; @Input() public groupBy?: GroupBy;
@Input() public highlightNegativeValues = false;
@Input() public historicalDataItems: LineChartItem[] = []; @Input() public historicalDataItems: LineChartItem[] = [];
@Input() public isInPercentage?: boolean = false; @Input() public isInPercentage?: boolean = false;
@Input() public isLoading = false; @Input() public isLoading = false;
@ -117,6 +122,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
const isBarChart = !!this.groupBy; const isBarChart = !!this.groupBy;
const benchmarkColorRgb = isBarChart ? primaryColorRgb : secondaryColorRgb; const benchmarkColorRgb = isBarChart ? primaryColorRgb : secondaryColorRgb;
const benchmarkColor = `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`;
// Create clones of the input data // Create clones of the input data
this.investments = this.benchmarkDataItems.map((item) => this.investments = this.benchmarkDataItems.map((item) =>
@ -133,8 +139,18 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}), }),
datasets: [ datasets: [
{ {
backgroundColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`, backgroundColor: (context: ScriptableContext<'bar' | 'line'>) => {
borderColor: `rgb(${benchmarkColorRgb.r}, ${benchmarkColorRgb.g}, ${benchmarkColorRgb.b})`, if (
(context.parsed?.y ?? 0) < 0 &&
isBarChart &&
this.highlightNegativeValues
) {
return orange[5];
}
return benchmarkColor;
},
borderColor: benchmarkColor,
borderWidth: isBarChart ? 0 : 1, borderWidth: isBarChart ? 0 : 1,
data: this.investments.map(({ date, investment }) => { data: this.investments.map(({ date, investment }) => {
return { return {

Loading…
Cancel
Save