Feature/add vertical hover line to line chart component (#963)
* Add vertical hover line * Improve tooltips of charts * Update changelog
This commit is contained in:
parent
34d4212f55
commit
15dda886a0
@ -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>
|
||||
<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>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
83
libs/common/src/lib/chart-helper.ts
Normal file
83
libs/common/src/lib/chart-helper.ts
Normal file
@ -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…
x
Reference in New Issue
Block a user