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 e77c5b362..4fae46dbb 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 @@ -6,11 +6,18 @@ import { Input, OnChanges, OnDestroy, - OnInit, ViewChild } from '@angular/core'; +import { + getTooltipOptions, + getTooltipPositionerMapTop, + getVerticalHoverLinePlugin +} from '@ghostfolio/common/chart-helper'; import { primaryColorRgb } from '@ghostfolio/common/config'; import { + getBackgroundColor, + getDateFormatString, + getTextColor, parseDate, transformTickToAbbreviation } from '@ghostfolio/common/helper'; @@ -21,7 +28,8 @@ import { LineElement, LinearScale, PointElement, - TimeScale + TimeScale, + Tooltip } from 'chart.js'; import { addDays, isAfter, parseISO, subDays } from 'date-fns'; @@ -32,9 +40,11 @@ import { addDays, isAfter, parseISO, subDays } from 'date-fns'; styleUrls: ['./investment-chart.component.scss'] }) export class InvestmentChartComponent implements OnChanges, OnDestroy { + @Input() currency: string; @Input() daysInMarket: number; @Input() investments: InvestmentItem[]; @Input() isInPercent = false; + @Input() locale: string; @ViewChild('chartCanvas') chartCanvas; @@ -47,8 +57,12 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { LineController, LineElement, PointElement, - TimeScale + TimeScale, + Tooltip ); + + Tooltip.positioners['top'] = (elements, position) => + getTooltipPositionerMapTop(this.chart, position); } public ngOnChanges() { @@ -98,6 +112,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { data: this.investments.map((position) => { return position.investment; }), + label: 'Investment', segment: { borderColor: (context: unknown) => this.isInFuture( @@ -114,6 +129,9 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins.tooltip = ( + this.getTooltipPluginConfiguration() + ); this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -124,13 +142,20 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { tension: 0 }, point: { + hoverBackgroundColor: getBackgroundColor(), + hoverRadius: 2, radius: 0 } }, + interaction: { intersect: false, mode: 'index' }, maintainAspectRatio: true, - plugins: { + plugins: { legend: { display: false + }, + tooltip: this.getTooltipPluginConfiguration(), + verticalHoverLine: { + color: `rgba(${getTextColor()}, 0.1)` } }, responsive: true, @@ -138,16 +163,21 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { x: { display: true, grid: { + borderColor: `rgba(${getTextColor()}, 0.1)`, + color: `rgba(${getTextColor()}, 0.8)`, display: false }, type: 'time', time: { + tooltipFormat: getDateFormatString(this.locale), unit: 'year' } }, y: { display: !this.isInPercent, grid: { + borderColor: `rgba(${getTextColor()}, 0.1)`, + color: `rgba(${getTextColor()}, 0.8)`, display: false }, ticks: { @@ -161,6 +191,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { } } }, + plugins: [getVerticalHoverLinePlugin(this.chartCanvas)], type: 'line' }); @@ -169,6 +200,19 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy { } } + private getTooltipPluginConfiguration() { + return { + ...getTooltipOptions( + this.isInPercent ? undefined : this.currency, + this.isInPercent ? undefined : this.locale + ), + mode: 'index', + position: 'top', + xAlign: 'center', + yAlign: 'bottom' + }; + } + private isInFuture(aContext: any, aValue: T) { return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) ? aValue diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 5bda278c4..59a8e4e16 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -23,6 +23,7 @@ class="mb-4" benchmarkLabel="Average Unit Price" [benchmarkDataItems]="benchmarkDataItems" + [currency]="SymbolProfile?.currency" [historicalDataItems]="historicalDataItems" [locale]="data.locale" [showGradient]="true" diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 361110d76..e6b61bff7 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -11,9 +11,11 @@ diff --git a/libs/common/src/lib/chart-helper.ts b/libs/common/src/lib/chart-helper.ts new file mode 100644 index 000000000..d2c68af26 --- /dev/null +++ b/libs/common/src/lib/chart-helper.ts @@ -0,0 +1,83 @@ +import { Chart, TooltipPosition } from 'chart.js'; + +import { getBackgroundColor, getTextColor } from './helper'; + +export function getTooltipOptions(currency = '', locale = '') { + return { + backgroundColor: getBackgroundColor(), + bodyColor: `rgb(${getTextColor()})`, + borderWidth: 1, + borderColor: `rgba(${getTextColor()}, 0.1)`, + callbacks: { + label: (context) => { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + if (currency) { + label += `${context.parsed.y.toLocaleString(locale, { + maximumFractionDigits: 2, + minimumFractionDigits: 2 + })} ${currency}`; + } else { + label += context.parsed.y.toFixed(2); + } + } + return label; + } + }, + caretSize: 0, + cornerRadius: 2, + footerColor: `rgb(${getTextColor()})`, + itemSort: (a, b) => { + // Reverse order + return b.datasetIndex - a.datasetIndex; + }, + titleColor: `rgb(${getTextColor()})`, + usePointStyle: true + }; +} + +export function getTooltipPositionerMapTop( + chart: Chart, + position: TooltipPosition +) { + if (!position) { + return false; + } + return { + x: position.x, + y: chart.chartArea.top + }; +} + +export function getVerticalHoverLinePlugin(chartCanvas) { + return { + afterDatasetsDraw: (chart, x, options) => { + const active = chart.getActiveElements(); + + if (!active || active.length === 0) { + return; + } + + const color = options.color || `rgb(${getTextColor()})`; + const width = options.width || 1; + + const { + chartArea: { bottom, top } + } = chart; + const xValue = active[0].element.x; + + const context = chartCanvas.nativeElement.getContext('2d'); + context.lineWidth = width; + context.strokeStyle = color; + + context.beginPath(); + context.moveTo(xValue, top); + context.lineTo(xValue, bottom); + context.stroke(); + }, + id: 'verticalHoverLine' + }; +} 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 65fadcb4d..bcf004ed0 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -10,6 +10,11 @@ import { OnDestroy, ViewChild } from '@angular/core'; +import { + getTooltipOptions, + getTooltipPositionerMapTop, + getVerticalHoverLinePlugin +} from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; import { getBackgroundColor, @@ -38,6 +43,7 @@ import { LineChartItem } from './interfaces/line-chart.interface'; export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { @Input() benchmarkDataItems: LineChartItem[] = []; @Input() benchmarkLabel = ''; + @Input() currency: string; @Input() historicalDataItems: LineChartItem[]; @Input() locale: string; @Input() showGradient = false; @@ -67,15 +73,8 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { Tooltip ); - Tooltip.positioners['top'] = (elements, position) => { - if (position === false) { - return false; - } - return { - x: position.x, - y: this.chart.chartArea.top - }; - }; + Tooltip.positioners['top'] = (elements, position) => + getTooltipPositionerMapTop(this.chart, position); } public ngAfterViewInit() { @@ -159,6 +158,9 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins.tooltip = ( + this.getTooltipPluginConfiguration() + ); this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -172,22 +174,13 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { } }, interaction: { intersect: false, mode: 'index' }, - plugins: { + plugins: { legend: { align: 'start', display: this.showLegend, position: 'bottom' }, - tooltip: { - itemSort: (a, b) => { - // Reverse order - return b.datasetIndex - a.datasetIndex; - }, - mode: 'index', - position: 'top', - xAlign: 'center', - yAlign: 'bottom' - }, + tooltip: this.getTooltipPluginConfiguration(), verticalHoverLine: { color: `rgba(${getTextColor()}, 0.1)` } @@ -196,7 +189,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { x: { display: this.showXAxis, grid: { - borderColor: `rgba(${getTextColor()}, 0.2)`, + borderColor: `rgba(${getTextColor()}, 0.1)`, color: `rgba(${getTextColor()}, 0.8)`, display: false }, @@ -209,7 +202,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { y: { display: this.showYAxis, grid: { - borderColor: `rgba(${getTextColor()}, 0.2)`, + borderColor: `rgba(${getTextColor()}, 0.1)`, color: `rgba(${getTextColor()}, 0.8)`, display: false }, @@ -246,35 +239,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { }, spanGaps: true }, - plugins: [ - { - afterDatasetsDraw: (chart, x, options) => { - const active = chart.getActiveElements(); - - if (!active || active.length === 0) { - return; - } - - const color = options.color || `rgb(${getTextColor()})`; - const width = options.width || 1; - - const { - chartArea: { bottom, top } - } = chart; - const xValue = active[0].element.x; - - const context = this.chartCanvas.nativeElement.getContext('2d'); - context.lineWidth = width; - context.strokeStyle = color; - - context.beginPath(); - context.moveTo(xValue, top); - context.lineTo(xValue, bottom); - context.stroke(); - }, - id: 'verticalHoverLine' - } - ], + plugins: [getVerticalHoverLinePlugin(this.chartCanvas)], type: 'line' }); } @@ -282,4 +247,14 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { this.isLoading = false; } + + private getTooltipPluginConfiguration() { + return { + ...getTooltipOptions(this.currency, this.locale), + mode: 'index', + position: 'top', + xAlign: 'center', + yAlign: 'bottom' + }; + } } diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index 78e56a8cc..389e09676 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -10,6 +10,7 @@ import { Output, ViewChild } from '@angular/core'; +import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { getTextColor } from '@ghostfolio/common/helper'; import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces'; @@ -255,8 +256,9 @@ export class PortfolioProportionChartComponent if (this.chartCanvas) { if (this.chart) { this.chart.data = data; - this.chart.options.plugins.tooltip = - this.getTooltipPluginConfiguration(data); + this.chart.options.plugins.tooltip = ( + this.getTooltipPluginConfiguration(data) + ); this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { @@ -339,6 +341,7 @@ export class PortfolioProportionChartComponent private getTooltipPluginConfiguration(data: ChartConfiguration['data']) { return { + ...getTooltipOptions(this.baseCurrency, this.locale), callbacks: { label: (context) => { const labelIndex =