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
- 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 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_

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 {
AssetProfileIdentifier,
InfoItem,
@ -227,7 +231,7 @@ export class GfAppComponent implements OnInit {
: !!this.info?.subscriptionOffer?.coupon ||
!!this.info?.subscriptionOffer?.durationExtension;
this.initializeTheme(this.user?.settings.colorScheme);
this.toggleTheme(this.user?.settings.colorScheme);
this.changeDetectorRef.markForCheck();
});
@ -259,17 +263,19 @@ export class GfAppComponent implements OnInit {
document.location.href = `/${document.documentElement.lang}`;
}
private initializeTheme(userPreferredColorScheme?: ColorScheme) {
this.toggleTheme(isDarkColorScheme(userPreferredColorScheme));
private initializeTheme() {
this.toggleTheme();
// Default chart styles
Chart.defaults.font.family = getCssVariable('--font-family-sans-serif');
window.matchMedia('(prefers-color-scheme: dark)').addListener((event) => {
if (!this.user?.settings.colorScheme) {
this.toggleTheme(event.matches);
}
});
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', () => {
if (!this.user?.settings.colorScheme) {
this.toggleTheme();
}
});
}
private openHoldingDetailDialog({
@ -340,12 +346,8 @@ export class GfAppComponent implements OnInit {
});
}
private toggleTheme(isDarkTheme: boolean) {
const themeColor = getCssVariable(
isDarkTheme ? '--dark-background' : '--light-background'
);
if (isDarkTheme) {
private toggleTheme(userPreferredColorScheme?: ColorScheme) {
if (isDarkColorScheme(userPreferredColorScheme)) {
this.document.body.classList.add('theme-dark');
this.document.body.classList.remove('theme-light');
} else {
@ -355,6 +357,6 @@ export class GfAppComponent implements OnInit {
this.document
.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 {
getTimeSeriesTooltipOptions,
onPrefersColorSchemeChange,
registerChartConfiguration
} from '@ghostfolio/ui/chart';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -27,6 +28,7 @@ import {
OnChanges,
OnDestroy,
output,
SimpleChanges,
viewChild
} from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@ -68,7 +70,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
public readonly benchmark = input<Partial<SymbolProfile>>();
public readonly benchmarkDataItems = input<LineChartItem[]>([]);
public readonly benchmarks = input<Partial<SymbolProfile>[]>();
public readonly colorScheme = input.required<ColorScheme>();
public readonly colorScheme = input<ColorScheme>();
public readonly isLoading = input<boolean>();
public readonly locale = input(getLocale());
public readonly performanceDataItems = input.required<LineChartItem[]>();
@ -76,7 +78,7 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
public readonly benchmarkChanged = output<string>();
protected chart: Chart<'line'>;
protected chart?: Chart<'line'>;
protected hasPermissionToAccessAdminControl: boolean;
protected readonly routerLinkAdminControlMarketData =
internalRoutes.adminControl.subRoutes.marketData.routerLink;
@ -96,10 +98,24 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
registerChartConfiguration();
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme()) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
});
addIcons({ arrowForwardOutline });
}
public ngOnChanges() {
public ngOnChanges(changes: SimpleChanges) {
if (changes.colorScheme && this.chart) {
this.chart.destroy();
this.chart = undefined;
}
this.hasPermissionToAccessAdminControl = hasPermission(
this.user()?.permissions,
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 {
baseCurrency: string;
colorScheme: ColorScheme;
colorScheme?: ColorScheme;
dataSource: DataSource;
deviceType: string;
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 {
getTimeSeriesTooltipOptions,
onPrefersColorSchemeChange,
registerChartConfiguration
} from '@ghostfolio/ui/chart';
@ -24,6 +25,7 @@ import {
Input,
OnChanges,
OnDestroy,
SimpleChanges,
viewChild
} from '@angular/core';
import {
@ -67,7 +69,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
private readonly chartCanvas =
viewChild.required<ElementRef<HTMLCanvasElement>>('chartCanvas');
private chart: Chart<'bar' | 'line'>;
private chart?: Chart<'bar' | 'line'>;
private investments: InvestmentItem[];
private values: LineChartItem[];
@ -84,9 +86,23 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
);
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) {
this.initialize();
}

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

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

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

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

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

@ -18,6 +18,7 @@ import {
Input,
OnChanges,
OnDestroy,
SimpleChanges,
ViewChild
} from '@angular/core';
import {
@ -37,6 +38,7 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import {
getTimeSeriesTooltipOptions,
onPrefersColorSchemeChange,
registerChartConfiguration
} from '../chart';
@ -71,7 +73,7 @@ export class GfLineChartComponent
@ViewChild('chartCanvas') chartCanvas: ElementRef<HTMLCanvasElement>;
public chart: Chart<'line'>;
public chart?: Chart<'line'>;
public isLoading = true;
private readonly ANIMATION_DURATION = 1200;
@ -88,6 +90,15 @@ export class GfLineChartComponent
);
registerChartConfiguration();
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme) {
this.chart.destroy();
this.chart = undefined;
this.initialize();
}
});
}
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) {
setTimeout(() => {
// 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 OpenColor from 'open-color';
import { onPrefersColorSchemeChange } from '../chart';
import { PortfolioProportionChartClickEvent } from './interfaces/interfaces';
const {
@ -94,6 +95,12 @@ export class GfPortfolioProportionChartComponent
public constructor() {
Chart.register(ArcElement, DoughnutController, LinearScale, Tooltip);
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme) {
this.initialize();
}
});
}
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 OpenColor from 'open-color';
import { onPrefersColorSchemeChange } from '../chart';
import type {
GetColorParams,
GfTreemapDataPoint,
@ -55,7 +56,7 @@ export class GfTreemapChartComponent
implements AfterViewInit, OnChanges, OnDestroy
{
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 dateRange = input.required<DateRange>();
public readonly holdings = input<PortfolioPosition[]>();
@ -71,6 +72,12 @@ export class GfTreemapChartComponent
public constructor() {
Chart.register(LinearScale, Tooltip, TreemapController, TreemapElement);
onPrefersColorSchemeChange(() => {
if (this.chart && !this.colorScheme()) {
this.initialize();
}
});
}
public ngAfterViewInit() {

Loading…
Cancel
Save