diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a0320ef..ddffe90a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added a vertical hover line to inspect data points in the line chart component + ### Changed +- Improved the tooltips of the chart components (content and style) - Simplified the pricing page ## 1.153.0 - 27.05.2022 diff --git a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html index 7264be84d..c3d905be0 100644 --- a/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html +++ b/apps/client/src/app/components/admin-market-data-detail/admin-market-data-detail.component.html @@ -2,8 +2,10 @@
{{ itemByMonth.key }}
diff --git a/apps/client/src/app/components/home-market/home-market.html b/apps/client/src/app/components/home-market/home-market.html index b55103bf0..d509a641d 100644 --- a/apps/client/src/app/components/home-market/home-market.html +++ b/apps/client/src/app/components/home-market/home-market.html @@ -7,11 +7,13 @@
diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index 7f804d990..e82ef7f5f 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -6,6 +6,7 @@ + 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 64bb56f3f..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,7 +23,9 @@ class="mb-4" benchmarkLabel="Average Unit Price" [benchmarkDataItems]="benchmarkDataItems" + [currency]="SymbolProfile?.currency" [historicalDataItems]="historicalDataItems" + [locale]="data.locale" [showGradient]="true" [showXAxis]="true" [showYAxis]="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..4364a8e83 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -2,21 +2,17 @@

Analysis

- - - Investment Timeline - - - - - +
+
Investment Timeline
+ +
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/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 933d1899a..00c834822 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -13,6 +13,7 @@ import { ViewChild } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; +import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb } from '@ghostfolio/common/config'; import { transformTickToAbbreviation } from '@ghostfolio/common/helper'; import { @@ -182,10 +183,7 @@ export class FireCalculatorComponent options: { plugins: { tooltip: { - itemSort: (a, b) => { - // Reverse order - return b.datasetIndex - a.datasetIndex; - }, + ...getTooltipOptions(), mode: 'index', callbacks: { footer: (items) => { 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 d2c185b15..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,8 +10,17 @@ import { OnDestroy, ViewChild } from '@angular/core'; +import { + getTooltipOptions, + getTooltipPositionerMapTop, + getVerticalHoverLinePlugin +} from '@ghostfolio/common/chart-helper'; import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; -import { getBackgroundColor } from '@ghostfolio/common/helper'; +import { + getBackgroundColor, + getDateFormatString, + getTextColor +} from '@ghostfolio/common/helper'; import { Chart, Filler, @@ -19,7 +28,8 @@ import { LineElement, LinearScale, PointElement, - TimeScale + TimeScale, + Tooltip } from 'chart.js'; import { LineChartItem } from './interfaces/line-chart.interface'; @@ -33,7 +43,9 @@ 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; @Input() showLegend = false; @Input() showLoader = true; @@ -57,8 +69,12 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { LineElement, PointElement, LinearScale, - TimeScale + TimeScale, + Tooltip ); + + Tooltip.positioners['top'] = (elements, position) => + getTooltipPositionerMapTop(this.chart, position); } public ngAfterViewInit() { @@ -142,26 +158,43 @@ 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, { data, options: { animation: false, - plugins: { + elements: { + point: { + hoverBackgroundColor: getBackgroundColor(), + hoverRadius: 2 + } + }, + interaction: { intersect: false, mode: 'index' }, + plugins: { legend: { align: 'start', display: this.showLegend, position: 'bottom' + }, + tooltip: this.getTooltipPluginConfiguration(), + verticalHoverLine: { + color: `rgba(${getTextColor()}, 0.1)` } }, scales: { x: { display: this.showXAxis, grid: { + borderColor: `rgba(${getTextColor()}, 0.1)`, + color: `rgba(${getTextColor()}, 0.8)`, display: false }, time: { + tooltipFormat: getDateFormatString(this.locale), unit: 'year' }, type: 'time' @@ -169,6 +202,8 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { y: { display: this.showYAxis, grid: { + borderColor: `rgba(${getTextColor()}, 0.1)`, + color: `rgba(${getTextColor()}, 0.8)`, display: false }, max: this.yMax, @@ -204,6 +239,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy { }, spanGaps: true }, + plugins: [getVerticalHoverLinePlugin(this.chartCanvas)], type: 'line' }); } @@ -211,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 =