Browse Source

Task/update chart colors on theme change (#7981)

* Update chart colors on change of color scheme

* Update changelog
pull/7962/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
4d63d5f4d6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 34
      apps/client/src/app/app.component.ts
  3. 22
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  4. 2
      apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts
  5. 20
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  6. 12
      libs/common/src/lib/chart-helper.ts
  7. 8
      libs/ui/src/lib/chart/chart.color-scheme.ts
  8. 1
      libs/ui/src/lib/chart/index.ts
  9. 24
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  10. 20
      libs/ui/src/lib/line-chart/line-chart.component.ts
  11. 7
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  12. 9
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

1
CHANGELOG.md

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the charts to update their colors if the appearance is set to auto and the operating system switches between light and dark mode
- Extended the emergency fund dialog in the portfolio summary to include the currency suffix - Extended the emergency fund dialog in the portfolio summary to include the currency suffix
- Extended the user detail dialog in the users section of the admin control panel to include the last request - Extended the user detail dialog in the users section of the admin control panel to include the last request
- Migrated the client to the code generated by the new `prisma-client` generator of _Prisma_ - Migrated the client to the code generated by the new `prisma-client` generator of _Prisma_

34
apps/client/src/app/app.component.ts

@ -1,4 +1,8 @@
import { getCssVariable, isDarkColorScheme } from '@ghostfolio/common/helper'; import {
getBackgroundColor,
getCssVariable,
isDarkColorScheme
} from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
InfoItem, InfoItem,
@ -227,7 +231,7 @@ export class GfAppComponent implements OnInit {
: !!this.info?.subscriptionOffer?.coupon || : !!this.info?.subscriptionOffer?.coupon ||
!!this.info?.subscriptionOffer?.durationExtension; !!this.info?.subscriptionOffer?.durationExtension;
this.initializeTheme(this.user?.settings.colorScheme); this.toggleTheme(this.user?.settings.colorScheme);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -259,17 +263,19 @@ export class GfAppComponent implements OnInit {
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }
private initializeTheme(userPreferredColorScheme?: ColorScheme) { private initializeTheme() {
this.toggleTheme(isDarkColorScheme(userPreferredColorScheme)); this.toggleTheme();
// Default chart styles // Default chart styles
Chart.defaults.font.family = getCssVariable('--font-family-sans-serif'); Chart.defaults.font.family = getCssVariable('--font-family-sans-serif');
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => { window
if (!this.user?.settings.colorScheme) { .matchMedia('(prefers-color-scheme: dark)')
this.toggleTheme(event.matches); .addEventListener('change', () => {
} if (!this.user?.settings.colorScheme) {
}); this.toggleTheme();
}
});
} }
private openHoldingDetailDialog({ private openHoldingDetailDialog({
@ -340,12 +346,8 @@ export class GfAppComponent implements OnInit {
}); });
} }
private toggleTheme(isDarkTheme: boolean) { private toggleTheme(userPreferredColorScheme?: ColorScheme) {
const themeColor = getCssVariable( if (isDarkColorScheme(userPreferredColorScheme)) {
isDarkTheme ? '--dark-background' : '--light-background'
);
if (isDarkTheme) {
this.document.body.classList.add('theme-dark'); this.document.body.classList.add('theme-dark');
this.document.body.classList.remove('theme-light'); this.document.body.classList.remove('theme-light');
} else { } else {
@ -355,6 +357,6 @@ export class GfAppComponent implements OnInit {
this.document this.document
.querySelector('meta[name="theme-color"]') .querySelector('meta[name="theme-color"]')
?.setAttribute('content', themeColor); ?.setAttribute('content', getBackgroundColor(userPreferredColorScheme));
} }
} }

22
apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts

@ -15,6 +15,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
import type { SymbolProfile } from '@ghostfolio/prisma/browser'; import type { SymbolProfile } from '@ghostfolio/prisma/browser';
import { import {
getTimeSeriesTooltipOptions, getTimeSeriesTooltipOptions,
onPrefersColorSchemeChange,
registerChartConfiguration registerChartConfiguration
} from '@ghostfolio/ui/chart'; } from '@ghostfolio/ui/chart';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -27,6 +28,7 @@ import {
OnChanges, OnChanges,
OnDestroy, OnDestroy,
output, output,
SimpleChanges,
viewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -68,7 +70,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
public readonly benchmark = input<Partial<SymbolProfile>>(); public readonly benchmark = input<Partial<SymbolProfile>>();
public readonly benchmarkDataItems = input<LineChartItem[]>([]); public readonly benchmarkDataItems = input<LineChartItem[]>([]);
public readonly benchmarks = input<Partial<SymbolProfile>[]>(); public readonly benchmarks = input<Partial<SymbolProfile>[]>();
public readonly colorScheme = input.required<ColorScheme>(); public readonly colorScheme = input<ColorScheme>();
public readonly isLoading = input<boolean>(); public readonly isLoading = input<boolean>();
public readonly locale = input(getLocale()); public readonly locale = input(getLocale());
public readonly performanceDataItems = input.required<LineChartItem[]>(); public readonly performanceDataItems = input.required<LineChartItem[]>();
@ -76,7 +78,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
public readonly benchmarkChanged = output<string>(); public readonly benchmarkChanged = output<string>();
protected chart: Chart<'line'>; protected chart?: Chart<'line'>;
protected hasPermissionToAccessAdminControl: boolean; protected hasPermissionToAccessAdminControl: boolean;
protected readonly routerLinkAdminControlMarketData = protected readonly routerLinkAdminControlMarketData =
internalRoutes.adminControl.subRoutes.marketData.routerLink; internalRoutes.adminControl.subRoutes.marketData.routerLink;
@ -96,10 +98,24 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
registerChartConfiguration(); registerChartConfiguration();
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme()) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
});
addIcons({ arrowForwardOutline }); addIcons({ arrowForwardOutline });
} }
public ngOnChanges() { public ngOnChanges(changes: SimpleChanges) {
if (changes.colorScheme && this.chart) {
this.chart.destroy();
this.chart = undefined;
}
this.hasPermissionToAccessAdminControl = hasPermission( this.hasPermissionToAccessAdminControl = hasPermission(
this.user()?.permissions, this.user()?.permissions,
permissions.accessAdminControl permissions.accessAdminControl

2
apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts

@ -3,7 +3,7 @@ import { DataSource } from '@ghostfolio/prisma/enums';
export interface HoldingDetailDialogParams { export interface HoldingDetailDialogParams {
baseCurrency: string; baseCurrency: string;
colorScheme: ColorScheme; colorScheme?: ColorScheme;
dataSource: DataSource; dataSource: DataSource;
deviceType: string; deviceType: string;
hasPermissionToAccessAdminControl: boolean; hasPermissionToAccessAdminControl: boolean;

20
apps/client/src/app/components/investment-chart/investment-chart.component.ts

@ -14,6 +14,7 @@ import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.in
import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; import { ColorScheme, GroupBy } from '@ghostfolio/common/types';
import { import {
getTimeSeriesTooltipOptions, getTimeSeriesTooltipOptions,
onPrefersColorSchemeChange,
registerChartConfiguration registerChartConfiguration
} from '@ghostfolio/ui/chart'; } from '@ghostfolio/ui/chart';
@ -24,6 +25,7 @@ import {
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
SimpleChanges,
viewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { import {
@ -67,7 +69,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
private readonly chartCanvas = private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas'); viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
private chart: Chart<'bar' | 'line'>; private chart?: Chart<'bar' | 'line'>;
private investments: InvestmentItem[]; private investments: InvestmentItem[];
private values: LineChartItem[]; private values: LineChartItem[];
@ -84,9 +86,23 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
); );
registerChartConfiguration(); registerChartConfiguration();
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
});
} }
public ngOnChanges() { public ngOnChanges(changes: SimpleChanges) {
if (changes.colorScheme && this.chart) {
this.chart.destroy();
this.chart = undefined;
}
if (this.benchmarkDataItems && this.historicalDataItems) { if (this.benchmarkDataItems && this.historicalDataItems) {
this.initialize(); this.initialize();
} }

12
libs/common/src/lib/chart-helper.ts

@ -101,9 +101,11 @@ export function getTooltipOptions<T extends ChartType>({
locale?: string; locale?: string;
unit?: string; unit?: string;
}): Partial<TooltipOptions<T>> { }): Partial<TooltipOptions<T>> {
const textColor = getTextColor(colorScheme);
return { return {
backgroundColor: getBackgroundColor(colorScheme), backgroundColor: getBackgroundColor(colorScheme),
bodyColor: `rgb(${getTextColor(colorScheme)})`, bodyColor: `rgb(${textColor})`,
borderWidth: 1, borderWidth: 1,
borderColor: getChartBorderColor(colorScheme), borderColor: getChartBorderColor(colorScheme),
// @ts-expect-error: no need to set all attributes in callbacks // @ts-expect-error: no need to set all attributes in callbacks
@ -144,12 +146,12 @@ export function getTooltipOptions<T extends ChartType>({
}, },
caretSize: 0, caretSize: 0,
cornerRadius: 2, cornerRadius: 2,
footerColor: `rgb(${getTextColor(colorScheme)})`, footerColor: `rgb(${textColor})`,
itemSort: (a, b) => { itemSort: (a, b) => {
// Reverse order // Reverse order
return b.datasetIndex - a.datasetIndex; return b.datasetIndex - a.datasetIndex;
}, },
titleColor: `rgb(${getTextColor(colorScheme)})`, titleColor: `rgb(${textColor})`,
usePointStyle: true usePointStyle: true
}; };
} }
@ -183,6 +185,8 @@ export function getValueAxisOptions({
ticks: Tick[] ticks: Tick[]
) => string; ) => string;
}): ScaleOptions<'linear'> { }): ScaleOptions<'linear'> {
const borderColor = getChartBorderColor(colorScheme);
return { return {
display, display,
border: { border: {
@ -196,7 +200,7 @@ export function getValueAxisOptions({
tick.value === scale.min || tick.value === scale.min ||
highlightedValues.includes(tick.value) highlightedValues.includes(tick.value)
) { ) {
return getChartBorderColor(colorScheme); return borderColor;
} }
return 'transparent'; return 'transparent';

8
libs/ui/src/lib/chart/chart.color-scheme.ts

@ -0,0 +1,8 @@
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { fromEvent } from 'rxjs';
export function onPrefersColorSchemeChange(callback: () => void) {
fromEvent(window.matchMedia('(prefers-color-scheme: dark)'), 'change')
.pipe(takeUntilDestroyed())
.subscribe(callback);
}

1
libs/ui/src/lib/chart/index.ts

@ -1,2 +1,3 @@
export * from './chart.color-scheme';
export * from './chart.options'; export * from './chart.options';
export * from './chart.registry'; export * from './chart.registry';

24
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -18,6 +18,7 @@ import {
OnChanges, OnChanges,
OnDestroy, OnDestroy,
output, output,
SimpleChanges,
viewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -63,6 +64,7 @@ import { isNumber } from 'lodash-es';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { debounceTime } from 'rxjs'; import { debounceTime } from 'rxjs';
import { onPrefersColorSchemeChange } from '../chart';
import { FireCalculatorService } from './fire-calculator.service'; import { FireCalculatorService } from './fire-calculator.service';
@Component({ @Component({
@ -85,7 +87,7 @@ import { FireCalculatorService } from './fire-calculator.service';
}) })
export class GfFireCalculatorComponent implements OnChanges, OnDestroy { export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() annualInterestRate = 0; @Input() annualInterestRate = 0;
@Input() colorScheme: ColorScheme; @Input() colorScheme?: ColorScheme;
@Input() currency: string; @Input() currency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() fireWealth = 0; @Input() fireWealth = 0;
@ -103,7 +105,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
retirementDate: new FormControl<Date | null>(null) retirementDate: new FormControl<Date | null>(null)
}); });
public chart: Chart<'bar'>; public chart?: Chart<'bar'>;
public isLoading = true; public isLoading = true;
public minDate = addDays(new Date(), 1); public minDate = addDays(new Date(), 1);
public periodsToRetire = 0; public periodsToRetire = 0;
@ -142,6 +144,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
addIcons({ calendarClearOutline }); addIcons({ calendarClearOutline });
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
});
this.calculatorForm.valueChanges this.calculatorForm.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
@ -221,7 +232,14 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}); });
} }
public ngOnChanges() { public ngOnChanges(changes: SimpleChanges) {
if (changes.colorScheme && this.chart) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
if (isNumber(this.fireWealth) && this.fireWealth >= 0) { if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
this.calculatorForm.setValue( this.calculatorForm.setValue(
{ {

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

@ -18,6 +18,7 @@ import {
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy,
SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { import {
@ -37,6 +38,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { import {
getTimeSeriesTooltipOptions, getTimeSeriesTooltipOptions,
onPrefersColorSchemeChange,
registerChartConfiguration registerChartConfiguration
} from '../chart'; } from '../chart';
@ -71,7 +73,7 @@ export class GfLineChartComponent
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>; @ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>; public chart?: Chart<'line'>;
public isLoading = true; public isLoading = true;
private readonly ANIMATION_DURATION = 1200; private readonly ANIMATION_DURATION = 1200;
@ -88,6 +90,15 @@ export class GfLineChartComponent
); );
registerChartConfiguration(); registerChartConfiguration();
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
});
} }
public ngAfterViewInit() { public ngAfterViewInit() {
@ -101,7 +112,12 @@ export class GfLineChartComponent
} }
} }
public ngOnChanges() { public ngOnChanges(changes: SimpleChanges) {
if (changes.colorScheme && this.chart) {
this.chart.destroy();
this.chart = undefined;
}
if (this.historicalDataItems || this.historicalDataItems === null) { if (this.historicalDataItems || this.historicalDataItems === null) {
setTimeout(() => { setTimeout(() => {
// Wait for the chartCanvas // Wait for the chartCanvas

7
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -33,6 +33,7 @@ import Color from 'color';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color'; import OpenColor from 'open-color';
import { onPrefersColorSchemeChange } from '../chart';
import { PortfolioProportionChartClickEvent } from './interfaces/interfaces'; import { PortfolioProportionChartClickEvent } from './interfaces/interfaces';
const { const {
@ -94,6 +95,12 @@ export class GfPortfolioProportionChartComponent
public constructor() { public constructor() {
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip); Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme) {
this.initialize();
}
});
} }
public ngAfterViewInit() { public ngAfterViewInit() {

9
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -36,6 +36,7 @@ import { orderBy, round } from 'lodash-es';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import OpenColor from 'open-color'; import OpenColor from 'open-color';
import { onPrefersColorSchemeChange } from '../chart';
import type { import type {
GetColorParams, GetColorParams,
GfTreemapDataPoint, GfTreemapDataPoint,
@ -55,7 +56,7 @@ export class GfTreemapChartComponent
implements AfterViewInit, OnChanges, OnDestroy implements AfterViewInit, OnChanges, OnDestroy
{ {
public readonly baseCurrency = input.required<string>(); public readonly baseCurrency = input.required<string>();
public readonly colorScheme = input.required<ColorScheme>(); public readonly colorScheme = input<ColorScheme>();
public readonly cursor = input.required<string>(); public readonly cursor = input.required<string>();
public readonly dateRange = input.required<DateRange>(); public readonly dateRange = input.required<DateRange>();
public readonly holdings = input<PortfolioPosition[]>(); public readonly holdings = input<PortfolioPosition[]>();
@ -71,6 +72,12 @@ export class GfTreemapChartComponent
public constructor() { public constructor() {
Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement); Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement);
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme()) {
this.initialize();
}
});
} }
public ngAfterViewInit() { public ngAfterViewInit() {

Loading…
Cancel
Save