Browse Source

Task/refactor line charts to share chart configuration (#7438)

* Refactor line chart components to share common chart configuration

* Update changelog
pull/7456/head^2
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
6318c064af
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 109
      apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts
  3. 111
      apps/client/src/app/components/investment-chart/investment-chart.component.ts
  4. 110
      libs/common/src/lib/chart-helper.ts
  5. 28
      libs/ui/src/lib/chart/chart.options.ts
  6. 1
      libs/ui/src/lib/chart/index.ts
  7. 91
      libs/ui/src/lib/line-chart/line-chart.component.ts

1
CHANGELOG.md

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Moved the tags to the overview tab of the account detail dialog (experimental) - Moved the tags to the overview tab of the account detail dialog (experimental)
- Moved the tags to the overview tab of the holding detail dialog - Moved the tags to the overview tab of the holding detail dialog
- Refactored the line chart components to share the common chart configuration
### Fixed ### Fixed

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

@ -1,20 +1,21 @@
import { import {
getTooltipOptions, getChartBorderColor,
getVerticalHoverLinePlugin getChartElementsOptions,
getTimeAxisOptions,
getValueAxisOptions,
getVerticalHoverLinePlugin,
getZeroLineAnnotation
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { import { getLocale, parseDate } from '@ghostfolio/common/helper';
getBackgroundColor,
getDateFormatString,
getLocale,
getTextColor,
parseDate
} from '@ghostfolio/common/helper';
import { LineChartItem, User } from '@ghostfolio/common/interfaces'; import { LineChartItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { ColorScheme } from '@ghostfolio/common/types'; import { ColorScheme } from '@ghostfolio/common/types';
import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import {
getTimeSeriesTooltipOptions,
registerChartConfiguration
} from '@ghostfolio/ui/chart';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { import {
@ -166,28 +167,13 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
data, data,
options: { options: {
animation: false, animation: false,
elements: { elements: getChartElementsOptions(this.colorScheme()),
line: {
tension: 0
},
point: {
hoverBackgroundColor: getBackgroundColor(this.colorScheme()),
hoverRadius: 2,
radius: 0
}
},
interaction: { intersect: false, mode: 'index' }, interaction: { intersect: false, mode: 'index' },
maintainAspectRatio: true, maintainAspectRatio: true,
plugins: { plugins: {
annotation: { annotation: {
annotations: { annotations: {
yAxis: { yAxis: getZeroLineAnnotation(this.colorScheme())
borderColor: `rgba(${getTextColor(this.colorScheme())}, 0.1)`,
borderWidth: 1,
scaleID: 'y',
type: 'line',
value: 0
}
} }
}, },
legend: { legend: {
@ -195,54 +181,21 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme())}, 0.1)` color: getChartBorderColor(this.colorScheme())
} }
}, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: getTimeAxisOptions({
border: { colorScheme: this.colorScheme(),
color: `rgba(${getTextColor(this.colorScheme())}, 0.1)`, locale: this.locale()
width: 1 }),
}, y: getValueAxisOptions({
display: true, colorScheme: this.colorScheme(),
grid: { tickCallback: (tickValue) => {
display: false return `${Number(tickValue).toFixed(2)} %`;
},
type: 'time',
time: {
tooltipFormat: getDateFormatString(this.locale()),
unit: 'year'
} }
}, })
y: {
border: {
width: 0
},
display: true,
grid: {
color: ({ scale, tick }) => {
if (
tick.value === 0 ||
tick.value === scale.max ||
tick.value === scale.min
) {
return `rgba(${getTextColor(this.colorScheme())}, 0.1)`;
}
return 'transparent';
}
},
position: 'right',
ticks: {
callback: (value: number) => {
return `${value.toFixed(2)} %`;
},
display: true,
mirror: true,
z: 1
}
}
} }
}, },
plugins: [ plugins: [
@ -255,16 +208,10 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy {
} }
private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return { return getTimeSeriesTooltipOptions<'line'>({
...getTooltipOptions({ colorScheme: this.colorScheme(),
colorScheme: this.colorScheme(), locale: this.locale(),
locale: this.locale(), unit: '%'
unit: '%' });
}),
mode: 'index',
position: 'top',
xAlign: 'center',
yAlign: 'bottom'
};
} }
} }

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

@ -1,20 +1,21 @@
import { import {
getTooltipOptions, getChartBorderColor,
getChartElementsOptions,
getTimeAxisOptions,
getValueAxisOptions,
getVerticalHoverLinePlugin, getVerticalHoverLinePlugin,
getZeroLineAnnotation,
transformTickToAbbreviation transformTickToAbbreviation
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { import { getLocale, parseDate } from '@ghostfolio/common/helper';
getBackgroundColor,
getDateFormatString,
getLocale,
getTextColor,
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';
import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; import { ColorScheme, GroupBy } from '@ghostfolio/common/types';
import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import {
getTimeSeriesTooltipOptions,
registerChartConfiguration
} from '@ghostfolio/ui/chart';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
@ -175,16 +176,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
data: chartData, data: chartData,
options: { options: {
animation: false, animation: false,
elements: { elements: getChartElementsOptions(this.colorScheme),
line: {
tension: 0
},
point: {
hoverBackgroundColor: getBackgroundColor(this.colorScheme),
hoverRadius: 2,
radius: 0
}
},
interaction: { intersect: false, mode: 'index' }, interaction: { intersect: false, mode: 'index' },
maintainAspectRatio: true, maintainAspectRatio: true,
plugins: { plugins: {
@ -212,13 +204,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
value: this.savingsRate value: this.savingsRate
} }
: undefined, : undefined,
yAxis: { yAxis: getZeroLineAnnotation(this.colorScheme)
borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`,
borderWidth: 1,
scaleID: 'y',
type: 'line',
value: 0
}
} }
}, },
legend: { legend: {
@ -226,54 +212,23 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: getChartBorderColor(this.colorScheme)
} }
}, },
responsive: true, responsive: true,
scales: { scales: {
x: { x: getTimeAxisOptions({
border: { borderWidth: this.groupBy ? 0 : 1,
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, colorScheme: this.colorScheme,
width: this.groupBy ? 0 : 1 locale: this.locale
}, }),
display: true, y: getValueAxisOptions({
grid: { colorScheme: this.colorScheme,
display: false
},
type: 'time',
time: {
tooltipFormat: getDateFormatString(this.locale),
unit: 'year'
}
},
y: {
border: {
display: false
},
display: !this.isInPercentage, display: !this.isInPercentage,
grid: { tickCallback: (tickValue) => {
color: ({ scale, tick }) => { return transformTickToAbbreviation(Number(tickValue));
if (
tick.value === 0 ||
tick.value === scale.max ||
tick.value === scale.min
) {
return `rgba(${getTextColor(this.colorScheme)}, 0.1)`;
}
return 'transparent';
}
},
position: 'right',
ticks: {
callback: (value: number) => {
return transformTickToAbbreviation(value);
},
display: true,
mirror: true,
z: 1
} }
} })
} }
}, },
plugins: [ plugins: [
@ -289,19 +244,13 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy {
private getTooltipPluginConfiguration(): Partial< private getTooltipPluginConfiguration(): Partial<
TooltipOptions<'bar' | 'line'> TooltipOptions<'bar' | 'line'>
> { > {
return { return getTimeSeriesTooltipOptions<'bar' | 'line'>({
...getTooltipOptions({ colorScheme: this.colorScheme,
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, unit: this.isInPercentage ? '%' : undefined
unit: this.isInPercentage ? '%' : undefined });
}),
mode: 'index',
position: 'top',
xAlign: 'center',
yAlign: 'bottom'
};
} }
private isInFuture<T>(aContext: ScriptableLineSegmentContext, aValue: T) { private isInFuture<T>(aContext: ScriptableLineSegmentContext, aValue: T) {

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

@ -1,13 +1,17 @@
import type { ElementRef } from '@angular/core'; import type { ElementRef } from '@angular/core';
import type { import type {
Chart, Chart,
ChartOptions,
ChartType, ChartType,
ControllerDatasetOptions, ControllerDatasetOptions,
Plugin, Plugin,
Point, Point,
ScaleOptions,
Tick,
TooltipOptions, TooltipOptions,
TooltipPosition TooltipPosition
} from 'chart.js'; } from 'chart.js';
import type { AnnotationOptions } from 'chartjs-plugin-annotation';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { import {
@ -15,6 +19,7 @@ import {
DATE_FORMAT_MONTHLY, DATE_FORMAT_MONTHLY,
DATE_FORMAT_YEARLY, DATE_FORMAT_YEARLY,
getBackgroundColor, getBackgroundColor,
getDateFormatString,
getLocale, getLocale,
getTextColor getTextColor
} from './helper'; } from './helper';
@ -36,6 +41,53 @@ export function formatGroupedDate({
return format(date, DATE_FORMAT); return format(date, DATE_FORMAT);
} }
export function getChartBorderColor(colorScheme: ColorScheme) {
return `rgba(${getTextColor(colorScheme)}, 0.1)`;
}
export function getChartElementsOptions(
colorScheme: ColorScheme
): ChartOptions<'bar' | 'line'>['elements'] {
return {
line: {
tension: 0
},
point: {
hoverBackgroundColor: getBackgroundColor(colorScheme),
hoverRadius: 2,
radius: 0
}
};
}
export function getTimeAxisOptions({
borderWidth = 1,
colorScheme,
display = true,
locale = getLocale()
}: {
borderWidth?: number;
colorScheme: ColorScheme;
display?: boolean;
locale?: string;
}): ScaleOptions<'time'> {
return {
display,
border: {
color: getChartBorderColor(colorScheme),
width: borderWidth
},
grid: {
display: false
},
time: {
tooltipFormat: getDateFormatString(locale),
unit: 'year'
},
type: 'time'
};
}
export function getTooltipOptions<T extends ChartType>({ export function getTooltipOptions<T extends ChartType>({
colorScheme, colorScheme,
currency = '', currency = '',
@ -53,7 +105,7 @@ export function getTooltipOptions<T extends ChartType>({
backgroundColor: getBackgroundColor(colorScheme), backgroundColor: getBackgroundColor(colorScheme),
bodyColor: `rgb(${getTextColor(colorScheme)})`, bodyColor: `rgb(${getTextColor(colorScheme)})`,
borderWidth: 1, borderWidth: 1,
borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, 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
callbacks: { callbacks: {
label: (context) => { label: (context) => {
@ -116,6 +168,50 @@ export function getTooltipPositionerMapTop(
}; };
} }
export function getValueAxisOptions({
colorScheme,
display = true,
highlightedValues = [],
tickCallback
}: {
colorScheme: ColorScheme;
display?: boolean;
highlightedValues?: number[];
tickCallback: (
tickValue: number | string,
index: number,
ticks: Tick[]
) => string;
}): ScaleOptions<'linear'> {
return {
display,
border: {
display: false
},
grid: {
color: ({ scale, tick }) => {
if (
tick.value === 0 ||
tick.value === scale.max ||
tick.value === scale.min ||
highlightedValues.includes(tick.value)
) {
return getChartBorderColor(colorScheme);
}
return 'transparent';
}
},
position: 'right',
ticks: {
display,
callback: tickCallback,
mirror: true,
z: 1
}
};
}
export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>( export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
chartCanvas: ElementRef<HTMLCanvasElement>, chartCanvas: ElementRef<HTMLCanvasElement>,
colorScheme: ColorScheme colorScheme: ColorScheme
@ -152,6 +248,18 @@ export function getVerticalHoverLinePlugin<T extends 'line' | 'bar'>(
}; };
} }
export function getZeroLineAnnotation(
colorScheme: ColorScheme
): AnnotationOptions<'line'> {
return {
borderColor: getChartBorderColor(colorScheme),
borderWidth: 1,
scaleID: 'y',
type: 'line',
value: 0
};
}
export function transformTickToAbbreviation(value: number) { export function transformTickToAbbreviation(value: number) {
if (value === 0) { if (value === 0) {
return '0'; return '0';

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

@ -0,0 +1,28 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { ColorScheme, GroupBy } from '@ghostfolio/common/types';
import type { TooltipOptions } from 'chart.js';
import './chart.registry';
export function getTimeSeriesTooltipOptions<T extends 'bar' | 'line'>({
colorScheme,
currency,
groupBy,
locale,
unit
}: {
colorScheme: ColorScheme;
currency?: string;
groupBy?: GroupBy;
locale?: string;
unit?: string;
}): Partial<TooltipOptions<T>> {
return {
...getTooltipOptions<T>({ colorScheme, currency, groupBy, locale, unit }),
mode: 'index',
position: 'top',
xAlign: 'center',
yAlign: 'bottom'
};
}

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

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

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

@ -1,14 +1,11 @@
import { import {
getTooltipOptions, getChartBorderColor,
getTimeAxisOptions,
getValueAxisOptions,
getVerticalHoverLinePlugin getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper'; } from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper';
getBackgroundColor,
getDateFormatString,
getLocale,
getTextColor
} 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';
@ -38,7 +35,10 @@ import {
import 'chartjs-adapter-date-fns'; import 'chartjs-adapter-date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { registerChartConfiguration } from '../chart'; import {
getTimeSeriesTooltipOptions,
registerChartConfiguration
} from '../chart';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -209,49 +209,21 @@ export class GfLineChartComponent
}, },
tooltip: this.getTooltipPluginConfiguration(), tooltip: this.getTooltipPluginConfiguration(),
verticalHoverLine: { verticalHoverLine: {
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` color: getChartBorderColor(this.colorScheme)
} }
}, },
scales: { scales: {
x: { x: getTimeAxisOptions({
border: { colorScheme: this.colorScheme,
color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`
},
display: this.showXAxis, display: this.showXAxis,
grid: { locale: this.locale
display: false }),
},
time: {
tooltipFormat: getDateFormatString(this.locale),
unit: 'year'
},
type: 'time'
},
y: { y: {
border: { ...getValueAxisOptions({
width: 0 colorScheme: this.colorScheme,
}, display: this.showYAxis,
display: this.showYAxis, highlightedValues: [this.yMax, this.yMin],
grid: { tickCallback: (tickValue, index, ticks) => {
color: ({ scale, tick }) => {
if (
tick.value === 0 ||
tick.value === scale.max ||
tick.value === scale.min ||
tick.value === this.yMax ||
tick.value === this.yMin
) {
return `rgba(${getTextColor(this.colorScheme)}, 0.1)`;
}
return 'transparent';
}
},
max: this.yMax,
min: this.yMin,
position: 'right',
ticks: {
callback: (tickValue, index, ticks) => {
if (index === 0 || index === ticks.length - 1) { if (index === 0 || index === ticks.length - 1) {
// Only print last and first legend entry // Only print last and first legend entry
@ -274,11 +246,10 @@ export class GfLineChartComponent
} }
return ''; return '';
}, }
display: this.showYAxis, }),
mirror: true, max: this.yMax,
z: 1 min: this.yMin,
},
type: 'linear' type: 'linear'
} }
}, },
@ -321,17 +292,11 @@ export class GfLineChartComponent
} }
private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> { private getTooltipPluginConfiguration(): Partial<TooltipOptions<'line'>> {
return { return getTimeSeriesTooltipOptions<'line'>({
...getTooltipOptions({ colorScheme: this.colorScheme,
colorScheme: this.colorScheme, currency: this.currency,
currency: this.currency, locale: this.locale,
locale: this.locale, unit: this.unit
unit: this.unit });
}),
mode: 'index',
position: 'top',
xAlign: 'center',
yAlign: 'bottom'
};
} }
} }

Loading…
Cancel
Save