diff --git a/CHANGELOG.md b/CHANGELOG.md index f78b9dad5..fcad150f7 100644 --- a/CHANGELOG.md +++ b/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 holding detail dialog +- Refactored the line chart components to share the common chart configuration ### Fixed diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts index c474e29ab..0091ae5d7 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts @@ -1,20 +1,21 @@ import { - getTooltipOptions, - getVerticalHoverLinePlugin + getChartBorderColor, + getChartElementsOptions, + getTimeAxisOptions, + getValueAxisOptions, + getVerticalHoverLinePlugin, + getZeroLineAnnotation } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; -import { - getBackgroundColor, - getDateFormatString, - getLocale, - getTextColor, - parseDate -} from '@ghostfolio/common/helper'; +import { getLocale, parseDate } from '@ghostfolio/common/helper'; import { LineChartItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; 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 { @@ -166,28 +167,13 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { data, options: { animation: false, - elements: { - line: { - tension: 0 - }, - point: { - hoverBackgroundColor: getBackgroundColor(this.colorScheme()), - hoverRadius: 2, - radius: 0 - } - }, + elements: getChartElementsOptions(this.colorScheme()), interaction: { intersect: false, mode: 'index' }, maintainAspectRatio: true, plugins: { annotation: { annotations: { - yAxis: { - borderColor: `rgba(${getTextColor(this.colorScheme())}, 0.1)`, - borderWidth: 1, - scaleID: 'y', - type: 'line', - value: 0 - } + yAxis: getZeroLineAnnotation(this.colorScheme()) } }, legend: { @@ -195,54 +181,21 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { }, tooltip: this.getTooltipPluginConfiguration(), verticalHoverLine: { - color: `rgba(${getTextColor(this.colorScheme())}, 0.1)` + color: getChartBorderColor(this.colorScheme()) } }, responsive: true, scales: { - x: { - border: { - color: `rgba(${getTextColor(this.colorScheme())}, 0.1)`, - width: 1 - }, - display: true, - grid: { - display: false - }, - type: 'time', - time: { - tooltipFormat: getDateFormatString(this.locale()), - unit: 'year' + x: getTimeAxisOptions({ + colorScheme: this.colorScheme(), + locale: this.locale() + }), + y: getValueAxisOptions({ + colorScheme: this.colorScheme(), + tickCallback: (tickValue) => { + return `${Number(tickValue).toFixed(2)} %`; } - }, - 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: [ @@ -255,16 +208,10 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { } private getTooltipPluginConfiguration(): Partial> { - return { - ...getTooltipOptions({ - colorScheme: this.colorScheme(), - locale: this.locale(), - unit: '%' - }), - mode: 'index', - position: 'top', - xAlign: 'center', - yAlign: 'bottom' - }; + return getTimeSeriesTooltipOptions<'line'>({ + colorScheme: this.colorScheme(), + locale: this.locale(), + unit: '%' + }); } } diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 3aa65b998..2a1ffd97c 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -1,20 +1,21 @@ import { - getTooltipOptions, + getChartBorderColor, + getChartElementsOptions, + getTimeAxisOptions, + getValueAxisOptions, getVerticalHoverLinePlugin, + getZeroLineAnnotation, transformTickToAbbreviation } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; -import { - getBackgroundColor, - getDateFormatString, - getLocale, - getTextColor, - parseDate -} from '@ghostfolio/common/helper'; +import { getLocale, parseDate } from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; -import { registerChartConfiguration } from '@ghostfolio/ui/chart'; +import { + getTimeSeriesTooltipOptions, + registerChartConfiguration +} from '@ghostfolio/ui/chart'; import { ChangeDetectionStrategy, @@ -175,16 +176,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { data: chartData, options: { animation: false, - elements: { - line: { - tension: 0 - }, - point: { - hoverBackgroundColor: getBackgroundColor(this.colorScheme), - hoverRadius: 2, - radius: 0 - } - }, + elements: getChartElementsOptions(this.colorScheme), interaction: { intersect: false, mode: 'index' }, maintainAspectRatio: true, plugins: { @@ -212,13 +204,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { value: this.savingsRate } : undefined, - yAxis: { - borderColor: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, - borderWidth: 1, - scaleID: 'y', - type: 'line', - value: 0 - } + yAxis: getZeroLineAnnotation(this.colorScheme) } }, legend: { @@ -226,54 +212,23 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { }, tooltip: this.getTooltipPluginConfiguration(), verticalHoverLine: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` + color: getChartBorderColor(this.colorScheme) } }, responsive: true, scales: { - x: { - border: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)`, - width: this.groupBy ? 0 : 1 - }, - display: true, - grid: { - display: false - }, - type: 'time', - time: { - tooltipFormat: getDateFormatString(this.locale), - unit: 'year' - } - }, - y: { - border: { - display: false - }, + x: getTimeAxisOptions({ + borderWidth: this.groupBy ? 0 : 1, + colorScheme: this.colorScheme, + locale: this.locale + }), + y: getValueAxisOptions({ + colorScheme: this.colorScheme, display: !this.isInPercentage, - 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 transformTickToAbbreviation(value); - }, - display: true, - mirror: true, - z: 1 + tickCallback: (tickValue) => { + return transformTickToAbbreviation(Number(tickValue)); } - } + }) } }, plugins: [ @@ -289,19 +244,13 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { private getTooltipPluginConfiguration(): Partial< TooltipOptions<'bar' | 'line'> > { - return { - ...getTooltipOptions({ - colorScheme: this.colorScheme, - currency: this.isInPercentage ? undefined : this.currency, - groupBy: this.groupBy, - locale: this.isInPercentage ? undefined : this.locale, - unit: this.isInPercentage ? '%' : undefined - }), - mode: 'index', - position: 'top', - xAlign: 'center', - yAlign: 'bottom' - }; + return getTimeSeriesTooltipOptions<'bar' | 'line'>({ + colorScheme: this.colorScheme, + currency: this.isInPercentage ? undefined : this.currency, + groupBy: this.groupBy, + locale: this.isInPercentage ? undefined : this.locale, + unit: this.isInPercentage ? '%' : undefined + }); } private isInFuture(aContext: ScriptableLineSegmentContext, aValue: T) { diff --git a/libs/common/src/lib/chart-helper.ts b/libs/common/src/lib/chart-helper.ts index 1f385e901..23a00ca97 100644 --- a/libs/common/src/lib/chart-helper.ts +++ b/libs/common/src/lib/chart-helper.ts @@ -1,13 +1,17 @@ import type { ElementRef } from '@angular/core'; import type { Chart, + ChartOptions, ChartType, ControllerDatasetOptions, Plugin, Point, + ScaleOptions, + Tick, TooltipOptions, TooltipPosition } from 'chart.js'; +import type { AnnotationOptions } from 'chartjs-plugin-annotation'; import { format } from 'date-fns'; import { @@ -15,6 +19,7 @@ import { DATE_FORMAT_MONTHLY, DATE_FORMAT_YEARLY, getBackgroundColor, + getDateFormatString, getLocale, getTextColor } from './helper'; @@ -36,6 +41,53 @@ export function formatGroupedDate({ 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({ colorScheme, currency = '', @@ -53,7 +105,7 @@ export function getTooltipOptions({ backgroundColor: getBackgroundColor(colorScheme), bodyColor: `rgb(${getTextColor(colorScheme)})`, borderWidth: 1, - borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, + borderColor: getChartBorderColor(colorScheme), // @ts-expect-error: no need to set all attributes in callbacks callbacks: { 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( chartCanvas: ElementRef, colorScheme: ColorScheme @@ -152,6 +248,18 @@ export function getVerticalHoverLinePlugin( }; } +export function getZeroLineAnnotation( + colorScheme: ColorScheme +): AnnotationOptions<'line'> { + return { + borderColor: getChartBorderColor(colorScheme), + borderWidth: 1, + scaleID: 'y', + type: 'line', + value: 0 + }; +} + export function transformTickToAbbreviation(value: number) { if (value === 0) { return '0'; diff --git a/libs/ui/src/lib/chart/chart.options.ts b/libs/ui/src/lib/chart/chart.options.ts new file mode 100644 index 000000000..cd2802cfb --- /dev/null +++ b/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({ + colorScheme, + currency, + groupBy, + locale, + unit +}: { + colorScheme: ColorScheme; + currency?: string; + groupBy?: GroupBy; + locale?: string; + unit?: string; +}): Partial> { + return { + ...getTooltipOptions({ colorScheme, currency, groupBy, locale, unit }), + mode: 'index', + position: 'top', + xAlign: 'center', + yAlign: 'bottom' + }; +} diff --git a/libs/ui/src/lib/chart/index.ts b/libs/ui/src/lib/chart/index.ts index 2a3d3b358..01597c515 100644 --- a/libs/ui/src/lib/chart/index.ts +++ b/libs/ui/src/lib/chart/index.ts @@ -1 +1,2 @@ +export * from './chart.options'; export * from './chart.registry'; diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 92ee8e4ec..edc377fa3 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -1,14 +1,11 @@ import { - getTooltipOptions, + getChartBorderColor, + getTimeAxisOptions, + getValueAxisOptions, getVerticalHoverLinePlugin } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; -import { - getBackgroundColor, - getDateFormatString, - getLocale, - getTextColor -} from '@ghostfolio/common/helper'; +import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; @@ -38,7 +35,10 @@ import { import 'chartjs-adapter-date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { registerChartConfiguration } from '../chart'; +import { + getTimeSeriesTooltipOptions, + registerChartConfiguration +} from '../chart'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -209,49 +209,21 @@ export class GfLineChartComponent }, tooltip: this.getTooltipPluginConfiguration(), verticalHoverLine: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` + color: getChartBorderColor(this.colorScheme) } }, scales: { - x: { - border: { - color: `rgba(${getTextColor(this.colorScheme)}, 0.1)` - }, + x: getTimeAxisOptions({ + colorScheme: this.colorScheme, display: this.showXAxis, - grid: { - display: false - }, - time: { - tooltipFormat: getDateFormatString(this.locale), - unit: 'year' - }, - type: 'time' - }, + locale: this.locale + }), y: { - border: { - width: 0 - }, - display: this.showYAxis, - grid: { - 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) => { + ...getValueAxisOptions({ + colorScheme: this.colorScheme, + display: this.showYAxis, + highlightedValues: [this.yMax, this.yMin], + tickCallback: (tickValue, index, ticks) => { if (index === 0 || index === ticks.length - 1) { // Only print last and first legend entry @@ -274,11 +246,10 @@ export class GfLineChartComponent } return ''; - }, - display: this.showYAxis, - mirror: true, - z: 1 - }, + } + }), + max: this.yMax, + min: this.yMin, type: 'linear' } }, @@ -321,17 +292,11 @@ export class GfLineChartComponent } private getTooltipPluginConfiguration(): Partial> { - return { - ...getTooltipOptions({ - colorScheme: this.colorScheme, - currency: this.currency, - locale: this.locale, - unit: this.unit - }), - mode: 'index', - position: 'top', - xAlign: 'center', - yAlign: 'bottom' - }; + return getTimeSeriesTooltipOptions<'line'>({ + colorScheme: this.colorScheme, + currency: this.currency, + locale: this.locale, + unit: this.unit + }); } }