Feature/add vertical hover line to line chart component (#963)

* Add vertical hover line

* Improve tooltips of charts

* Update changelog
pull/973/head
Thomas Kaul 2 years ago committed by GitHub
parent 34d4212f55
commit 15dda886a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -2,8 +2,10 @@
<gf-line-chart
class="mb-4"
[historicalDataItems]="historicalDataItems"
[locale]="locale"
[showXAxis]="true"
[showYAxis]="true"
[symbol]="symbol"
></gf-line-chart>
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex">
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>

@ -7,11 +7,13 @@
</div>
<gf-line-chart
class="mb-3"
symbol="Fear & Greed Index"
yMax="100"
yMaxLabel="Greed"
yMin="0"
yMinLabel="Fear"
[historicalDataItems]="historicalData"
[locale]="user?.settings?.locale"
[showXAxis]="true"
[showYAxis]="true"
></gf-line-chart>

@ -6,6 +6,7 @@
<gf-line-chart
symbol="Performance"
[historicalDataItems]="historicalDataItems"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"

@ -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 = <unknown>(
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: <unknown>{
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: <unknown>'top',
xAlign: 'center',
yAlign: 'bottom'
};
}
private isInFuture<T>(aContext: any, aValue: T) {
return isAfter(new Date(aContext?.p1?.parsed?.x), new Date())
? aValue

@ -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"

@ -2,21 +2,17 @@
<div class="investment-chart row">
<div class="col-lg">
<h3 class="d-flex justify-content-center mb-3" i18n>Analysis</h3>
<mat-card class="mb-3">
<mat-card-header>
<mat-card-title class="align-items-center d-flex" i18n
>Investment Timeline</mat-card-title
>
</mat-card-header>
<mat-card-content>
<gf-investment-chart
class="h-100"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
></gf-investment-chart>
</mat-card-content>
</mat-card>
<div class="mb-3">
<div class="h5 mb-3" i18n>Investment Timeline</div>
<gf-investment-chart
class="h-100"
[currency]="user?.settings?.baseCurrency"
[daysInMarket]="daysInMarket"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[investments]="investments"
[locale]="user?.settings?.locale"
></gf-investment-chart>
</div>
</div>
</div>

@ -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'
};
}

@ -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) => {

@ -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 = <unknown>(
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: <unknown>{
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: <unknown>'top',
xAlign: 'center',
yAlign: 'bottom'
};
}
}

@ -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 = <unknown>(
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 =

Loading…
Cancel
Save