Browse Source

Bugfix/light color scheme in chart helpers (#7975)

* Fix colors of charts if appearance is set to light and OS uses dark mode

* Update changelog
pull/7983/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
435aeeb1f7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 8
      apps/client/src/app/app.component.ts
  3. 7
      apps/client/src/app/components/admin-market-data/asset-profile-dialog-host/asset-profile-dialog-host.component.ts
  4. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts
  5. 22
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  6. 14
      libs/common/src/lib/chart-helper.ts
  7. 3
      libs/common/src/lib/config.ts
  8. 35
      libs/common/src/lib/helper.spec.ts
  9. 20
      libs/common/src/lib/helper.ts
  10. 2
      libs/ui/src/lib/chart/chart.options.ts
  11. 8
      libs/ui/src/lib/line-chart/line-chart.component.ts
  12. 6
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  13. 4
      libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts

1
CHANGELOG.md

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed the colors of the investment chart component (dark mode) - Fixed the colors of the investment chart component (dark mode)
- Fixed the colors of the charts if the appearance is set to light and the operating system uses dark mode
- Fixed the allocation column of the top holdings of ETFs on the allocations page after a change of the user settings - Fixed the allocation column of the top holdings of ETFs on the allocations page after a change of the user settings
- Fixed the value column of the top holdings of ETFs on the allocations page in the _Presenter View_ - Fixed the value column of the top holdings of ETFs on the allocations page in the _Presenter View_
- Fixed the projected sustainable retirement income on the _FIRE_ page for a projected total amount of zero (experimental) - Fixed the projected sustainable retirement income on the _FIRE_ page for a projected total amount of zero (experimental)

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

@ -1,4 +1,4 @@
import { getCssVariable } from '@ghostfolio/common/helper'; import { getCssVariable, isDarkColorScheme } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
InfoItem, InfoItem,
@ -260,11 +260,7 @@ export class GfAppComponent implements OnInit {
} }
private initializeTheme(userPreferredColorScheme?: ColorScheme) { private initializeTheme(userPreferredColorScheme?: ColorScheme) {
const isDarkTheme = userPreferredColorScheme this.toggleTheme(isDarkColorScheme(userPreferredColorScheme));
? userPreferredColorScheme === 'DARK'
: window.matchMedia('(prefers-color-scheme: dark)').matches;
this.toggleTheme(isDarkTheme);
// Default chart styles // Default chart styles
Chart.defaults.font.family = getCssVariable('--font-family-sans-serif'); Chart.defaults.font.family = getCssVariable('--font-family-sans-serif');

7
apps/client/src/app/components/admin-market-data/asset-profile-dialog-host/asset-profile-dialog-host.component.ts

@ -1,8 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
DEFAULT_COLOR_SCHEME,
DEFAULT_LOCALE
} from '@ghostfolio/common/config';
import { AssetProfileIdentifier, User } from '@ghostfolio/common/interfaces'; import { AssetProfileIdentifier, User } from '@ghostfolio/common/interfaces';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { DataSource } from '@ghostfolio/prisma/enums'; import { DataSource } from '@ghostfolio/prisma/enums';
@ -150,7 +147,7 @@ export class GfAssetProfileDialogHostComponent implements OnDestroy, OnInit {
data: { data: {
dataSource, dataSource,
symbol, symbol,
colorScheme: user?.settings.colorScheme ?? DEFAULT_COLOR_SCHEME, colorScheme: user?.settings?.colorScheme,
deviceType: this.deviceType(), deviceType: this.deviceType(),
locale: user?.settings?.locale ?? DEFAULT_LOCALE locale: user?.settings?.locale ?? DEFAULT_LOCALE
} satisfies AssetProfileDialogParams, } satisfies AssetProfileDialogParams,

2
apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts

@ -2,7 +2,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
import { DataSource } from '@ghostfolio/prisma/enums'; import { DataSource } from '@ghostfolio/prisma/enums';
export interface AssetProfileDialogParams { export interface AssetProfileDialogParams {
colorScheme: ColorScheme; colorScheme?: ColorScheme;
dataSource: DataSource; dataSource: DataSource;
deviceType: string; deviceType: string;
locale: string; locale: string;

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

@ -7,11 +7,7 @@ import {
getZeroLineAnnotation, getZeroLineAnnotation,
transformTickToAbbreviation transformTickToAbbreviation
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
DEFAULT_COLOR_SCHEME,
primaryColorRgb,
secondaryColorRgb
} from '@ghostfolio/common/config';
import { getLocale, parseDate } from '@ghostfolio/common/helper'; import { getLocale, parseDate } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces'; import { LineChartItem } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
@ -174,15 +170,13 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
this.chart.update(); this.chart.update();
} else { } else {
const colorScheme = this.colorScheme ?? DEFAULT_COLOR_SCHEME;
this.chart = new Chart<'bar' | 'line'>( this.chart = new Chart<'bar' | 'line'>(
this.chartCanvas().nativeElement, this.chartCanvas().nativeElement,
{ {
data: chartData, data: chartData,
options: { options: {
animation: false, animation: false,
elements: getChartElementsOptions(colorScheme), elements: getChartElementsOptions(this.colorScheme),
interaction: { intersect: false, mode: 'index' }, interaction: { intersect: false, mode: 'index' },
maintainAspectRatio: true, maintainAspectRatio: true,
plugins: { plugins: {
@ -210,7 +204,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
value: this.savingsRate value: this.savingsRate
} }
: undefined, : undefined,
yAxis: getZeroLineAnnotation(colorScheme) yAxis: getZeroLineAnnotation(this.colorScheme)
} }
}, },
legend: { legend: {
@ -218,18 +212,18 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
verticalHoverLine: { verticalHoverLine: {
color: getChartBorderColor(colorScheme) color: getChartBorderColor(this.colorScheme)
} }
}, },
responsive: true, responsive: true,
scales: { scales: {
x: getTimeAxisOptions({ x: getTimeAxisOptions({
colorScheme,
borderWidth: this.groupBy ? 0 : 1, borderWidth: this.groupBy ? 0 : 1,
colorScheme: this.colorScheme,
locale: this.locale locale: this.locale
}), }),
y: getValueAxisOptions({ y: getValueAxisOptions({
colorScheme, colorScheme: this.colorScheme,
display: !this.isInPercentage, display: !this.isInPercentage,
tickCallback: (tickValue) => { tickCallback: (tickValue) => {
return transformTickToAbbreviation(Number(tickValue)); return transformTickToAbbreviation(Number(tickValue));
@ -238,7 +232,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
} }
}, },
plugins: [ plugins: [
getVerticalHoverLinePlugin(this.chartCanvas(), colorScheme) getVerticalHoverLinePlugin(this.chartCanvas(), this.colorScheme)
], ],
type: this.groupBy ? 'bar' : 'line' type: this.groupBy ? 'bar' : 'line'
} }
@ -251,7 +245,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
TooltipOptions<'bar' | 'line'> TooltipOptions<'bar' | 'line'>
> { > {
return getTimeSeriesTooltipOptions<'bar' | 'line'>({ return getTimeSeriesTooltipOptions<'bar' | 'line'>({
colorScheme: this.colorScheme ?? DEFAULT_COLOR_SCHEME, colorScheme: this.colorScheme,
currency: this.isInPercentage ? undefined : this.currency, currency: this.isInPercentage ? undefined : this.currency,
groupBy: this.groupBy, groupBy: this.groupBy,
locale: this.isInPercentage ? undefined : this.locale, locale: this.isInPercentage ? undefined : this.locale,

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

@ -41,12 +41,12 @@ export function formatGroupedDate({
return format(date, DATE_FORMAT); return format(date, DATE_FORMAT);
} }
export function getChartBorderColor(colorScheme: ColorScheme) { export function getChartBorderColor(colorScheme?: ColorScheme) {
return `rgba(${getTextColor(colorScheme)}, 0.1)`; return `rgba(${getTextColor(colorScheme)}, 0.1)`;
} }
export function getChartElementsOptions( export function getChartElementsOptions(
colorScheme: ColorScheme colorScheme?: ColorScheme
): ChartOptions<'bar' | 'line'>['elements'] { ): ChartOptions<'bar' | 'line'>['elements'] {
return { return {
line: { line: {
@ -67,7 +67,7 @@ export function getTimeAxisOptions({
locale = getLocale() locale = getLocale()
}: { }: {
borderWidth?: number; borderWidth?: number;
colorScheme: ColorScheme; colorScheme?: ColorScheme;
display?: boolean; display?: boolean;
locale?: string; locale?: string;
}): ScaleOptions<'time'> { }): ScaleOptions<'time'> {
@ -95,7 +95,7 @@ export function getTooltipOptions<T extends ChartType>({
locale = getLocale(), locale = getLocale(),
unit = '' unit = ''
}: { }: {
colorScheme: ColorScheme; colorScheme?: ColorScheme;
currency?: string; currency?: string;
groupBy?: GroupBy; groupBy?: GroupBy;
locale?: string; locale?: string;
@ -174,7 +174,7 @@ export function getValueAxisOptions({
highlightedValues = [], highlightedValues = [],
tickCallback tickCallback
}: { }: {
colorScheme: ColorScheme; colorScheme?: ColorScheme;
display?: boolean; display?: boolean;
highlightedValues?: number[]; highlightedValues?: number[];
tickCallback: ( tickCallback: (
@ -214,7 +214,7 @@ export function getValueAxisOptions({
export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>( export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
chartCanvas: ElementRef<HTMLCanvasElement>, chartCanvas: ElementRef<HTMLCanvasElement>,
colorScheme: ColorScheme colorScheme?: ColorScheme
): Plugin<T, { color: string; width: number }> { ): Plugin<T, { color: string; width: number }> {
return { return {
afterDatasetsDraw: (chart, _, options) => { afterDatasetsDraw: (chart, _, options) => {
@ -249,7 +249,7 @@ export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
} }
export function getZeroLineAnnotation( export function getZeroLineAnnotation(
colorScheme: ColorScheme colorScheme?: ColorScheme
): AnnotationOptions<'line'> { ): AnnotationOptions<'line'> {
return { return {
borderColor: getChartBorderColor(colorScheme), borderColor: getChartBorderColor(colorScheme),

3
libs/common/src/lib/config.ts

@ -8,7 +8,7 @@ import {
import { JobOptions, JobStatus } from 'bull'; import { JobOptions, JobStatus } from 'bull';
import ms from 'ms'; import ms from 'ms';
import { ColorScheme, DateRange } from './types'; import { DateRange } from './types';
export const ghostfolioPrefix = 'GF'; export const ghostfolioPrefix = 'GF';
@ -100,7 +100,6 @@ export const PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW =
export const STATISTICS_GATHERING_QUEUE = 'STATISTICS_GATHERING_QUEUE'; export const STATISTICS_GATHERING_QUEUE = 'STATISTICS_GATHERING_QUEUE';
export const DEFAULT_COLOR_SCHEME: ColorScheme = 'LIGHT';
export const DEFAULT_CURRENCY = 'USD'; export const DEFAULT_CURRENCY = 'USD';
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy'; export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
export const DEFAULT_DATE_RANGE: DateRange = 'max'; export const DEFAULT_DATE_RANGE: DateRange = 'max';

35
libs/common/src/lib/helper.spec.ts

@ -13,6 +13,7 @@ import {
isAccountExcluded, isAccountExcluded,
isCurrency, isCurrency,
isCurrencySymbol, isCurrencySymbol,
isDarkColorScheme,
isSplitRatio, isSplitRatio,
isValidCurrencyCode, isValidCurrencyCode,
isValidCustomAssetProfileSymbol, isValidCustomAssetProfileSymbol,
@ -348,6 +349,40 @@ describe('Helper', () => {
}); });
}); });
describe('Is dark color scheme', () => {
const mockPrefersColorSchemeDark = (matches: boolean) => {
window.matchMedia = jest.fn(() => {
return { matches } as MediaQueryList;
});
};
afterEach(() => {
Reflect.deleteProperty(window, 'matchMedia');
});
it('Dark color scheme with operating system in light mode', () => {
mockPrefersColorSchemeDark(false);
expect(isDarkColorScheme('DARK')).toEqual(true);
});
it('Light color scheme with operating system in dark mode', () => {
mockPrefersColorSchemeDark(true);
expect(isDarkColorScheme('LIGHT')).toEqual(false);
});
it('Auto color scheme with operating system in dark mode', () => {
mockPrefersColorSchemeDark(true);
expect(isDarkColorScheme()).toEqual(true);
expect(isDarkColorScheme(null)).toEqual(true);
});
it('Auto color scheme with operating system in light mode', () => {
mockPrefersColorSchemeDark(false);
expect(isDarkColorScheme()).toEqual(false);
expect(isDarkColorScheme(null)).toEqual(false);
});
});
describe('Is split ratio', () => { describe('Is split ratio', () => {
it('Forward split', () => { it('Forward split', () => {
expect(isSplitRatio({ denominator: 1, numerator: 2 })).toEqual(true); expect(isSplitRatio({ denominator: 1, numerator: 2 })).toEqual(true);

20
libs/common/src/lib/helper.ts

@ -345,12 +345,9 @@ export function getAssetProfileIdentifier({
return `${dataSource}-${symbol}`; return `${dataSource}-${symbol}`;
} }
export function getBackgroundColor(aColorScheme: ColorScheme) { export function getBackgroundColor(aColorScheme?: ColorScheme) {
return getCssVariable( return getCssVariable(
aColorScheme === 'DARK' || isDarkColorScheme(aColorScheme) ? '--dark-background' : '--light-background'
window.matchMedia('(prefers-color-scheme: dark)').matches
? '--dark-background'
: '--light-background'
); );
} }
@ -531,10 +528,9 @@ export function getSum(aArray: Big[]) {
return new Big(0); return new Big(0);
} }
export function getTextColor(aColorScheme: ColorScheme) { export function getTextColor(aColorScheme?: ColorScheme) {
const cssVariable = getCssVariable( const cssVariable = getCssVariable(
aColorScheme === 'DARK' || isDarkColorScheme(aColorScheme)
window.matchMedia('(prefers-color-scheme: dark)').matches
? '--light-primary-text' ? '--light-primary-text'
: '--dark-primary-text' : '--dark-primary-text'
); );
@ -612,6 +608,14 @@ export function isCurrencySymbol(aSymbol: string) {
); );
} }
export function isDarkColorScheme(aColorScheme?: ColorScheme | null) {
if (aColorScheme) {
return aColorScheme === 'DARK';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
export function isDerivedCurrency(aCurrency: string) { export function isDerivedCurrency(aCurrency: string) {
if (aCurrency === 'USX') { if (aCurrency === 'USX') {
return true; return true;

2
libs/ui/src/lib/chart/chart.options.ts

@ -12,7 +12,7 @@ export function getTimeSeriesTooltipOptions<T extends 'bar' | 'line'>({
locale, locale,
unit unit
}: { }: {
colorScheme: ColorScheme; colorScheme?: ColorScheme;
currency?: string; currency?: string;
groupBy?: GroupBy; groupBy?: GroupBy;
locale?: string; locale?: string;

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

@ -4,11 +4,7 @@ import {
getValueAxisOptions, getValueAxisOptions,
getVerticalHoverLinePlugin getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
DEFAULT_COLOR_SCHEME,
primaryColorRgb,
secondaryColorRgb
} from '@ghostfolio/common/config';
import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper'; import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces'; import { LineChartItem } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
@ -56,7 +52,7 @@ export class GfLineChartComponent
{ {
@Input() benchmarkDataItems: LineChartItem[] = []; @Input() benchmarkDataItems: LineChartItem[] = [];
@Input() benchmarkLabel = ''; @Input() benchmarkLabel = '';
@Input() colorScheme: ColorScheme = DEFAULT_COLOR_SCHEME; @Input() colorScheme?: ColorScheme;
@Input() currency: string; @Input() currency: string;
@Input() historicalDataItems: LineChartItem[]; @Input() historicalDataItems: LineChartItem[];
@Input() isAnimated = false; @Input() isAnimated = false;

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

@ -1,5 +1,5 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { DEFAULT_COLOR_SCHEME, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale, getSum, getTextColor } from '@ghostfolio/common/helper'; import { getLocale, getSum, getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
@ -124,7 +124,7 @@ export class GfPortfolioProportionChartComponent
}; };
} = {}; } = {};
const textColor = getTextColor(this.colorScheme ?? DEFAULT_COLOR_SCHEME); const textColor = getTextColor(this.colorScheme);
this.colorMap = { this.colorMap = {
[this.OTHER_KEY]: `rgba(${textColor}, 0.24)`, [this.OTHER_KEY]: `rgba(${textColor}, 0.24)`,
@ -448,7 +448,7 @@ export class GfPortfolioProportionChartComponent
): Partial<TooltipOptions<'doughnut'>> { ): Partial<TooltipOptions<'doughnut'>> {
return { return {
...getTooltipOptions({ ...getTooltipOptions({
colorScheme: this.colorScheme ?? DEFAULT_COLOR_SCHEME, colorScheme: this.colorScheme,
currency: this.baseCurrency, currency: this.baseCurrency,
locale: this.locale locale: this.locale
}), }),

4
libs/ui/src/lib/treemap-chart/treemap-chart.component.stories.ts

@ -1,5 +1,3 @@
import { DEFAULT_COLOR_SCHEME } from '@ghostfolio/common/config';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { moduleMetadata } from '@storybook/angular'; import { moduleMetadata } from '@storybook/angular';
import type { Meta, StoryObj } from '@storybook/angular'; import type { Meta, StoryObj } from '@storybook/angular';
@ -38,7 +36,7 @@ export const Default: Story = {
args: { args: {
holdings, holdings,
baseCurrency: 'USD', baseCurrency: 'USD',
colorScheme: DEFAULT_COLOR_SCHEME, colorScheme: 'LIGHT',
cursor: undefined, cursor: undefined,
dateRange: 'mtd', dateRange: 'mtd',
locale: 'en-US' locale: 'en-US'

Loading…
Cancel
Save