Compare commits

..

9 Commits

Author SHA1 Message Date
0cee7a0b35 Release 1.197.0 (#1287) 2022-09-24 13:16:47 +02:00
f3d337b044 Feature/add value of active filters on allocations page (#1286)
* Add value

* Update changelog
2022-09-24 13:15:16 +02:00
7667af059c Feature/combine performance and chart calculation (#1285)
* Combine performance and chart calculation endpoints

* Update changelog
2022-09-24 13:12:40 +02:00
1095b47f45 Feature/add multi language support to feature overview (#1284)
* Add multi-language support

* Update changelog
2022-09-24 12:29:36 +02:00
dacd7271eb Feature/improve density of various selectors (#1283)
* Improve density

* Update changelog
2022-09-24 09:58:09 +02:00
e093041184 Release 1.196.0 (#1281) 2022-09-22 21:05:05 +02:00
8f2caa508a Feature/extend landing page (#1279)
* Extend landing page

* Update changelog
2022-09-22 20:52:46 +02:00
862f670ccf Feature/setup italiano (#1276)
* Setup italiano

* Update changelog
2022-09-22 20:52:03 +02:00
54bf4c7a43 Update messages.it.xlf (#1280) 2022-09-22 20:51:31 +02:00
26 changed files with 693 additions and 110 deletions

View File

@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.197.0 - 24.09.2022
### Added
- Added the value of the active filter in percentage on the allocations page
- Extended the feature overview page by multi-language support (English, German, Italian)
### Changed
- Combined the performance and chart calculation
- Improved the style of various selectors (density)
## 1.196.0 - 22.09.2022
### Added
- Set up the language localization for Italiano (`it`)
- Extended the landing page
## 1.195.0 - 20.09.2022
### Changed
@ -195,7 +214,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Set up `ng-extract-i18n-merge` to improve the i18n extraction and merge workflow
- Set up language localization for German (`de`)
- Set up the language localization for German (`de`)
- Resolved the feature graphic of the blog post
### Changed

View File

@ -164,7 +164,7 @@ export class BenchmarkService {
);
const marketPriceAtStartDate = marketDataItems?.[0]?.marketPrice ?? 0;
return {
const response = {
marketData: [
...marketDataItems
.filter((marketDataItem, index) => {
@ -181,17 +181,22 @@ export class BenchmarkService {
marketDataItem.marketPrice
) * 100
};
}),
{
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
}
})
]
};
if (currentSymbolItem?.marketPrice) {
response.marketData.push({
date: format(new Date(), DATE_FORMAT),
value:
this.calculateChangeInPercentage(
marketPriceAtStartDate,
currentSymbolItem.marketPrice
) * 100
});
}
return response;
}
private getMarketCondition(aPerformanceInPercent: number) {

View File

@ -11,6 +11,7 @@ import { NextFunction, Request, Response } from 'express';
export class FrontendMiddleware implements NestMiddleware {
public indexHtmlDe = '';
public indexHtmlEn = '';
public indexHtmlIt = '';
public isProduction: boolean;
public constructor(
@ -32,6 +33,10 @@ export class FrontendMiddleware implements NestMiddleware {
this.getPathOfIndexHtmlFile(DEFAULT_LANGUAGE_CODE),
'utf8'
);
this.indexHtmlIt = fs.readFileSync(
this.getPathOfIndexHtmlFile('it'),
'utf8'
);
} catch {}
}
@ -61,6 +66,15 @@ export class FrontendMiddleware implements NestMiddleware {
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else if (req.path === '/it' || req.path.startsWith('/it/')) {
res.send(
this.interpolate(this.indexHtmlIt, {
featureGraphicPath,
languageCode: 'it',
path: req.path,
rootUrl: this.configurationService.get('ROOT_URL')
})
);
} else {
res.send(
this.interpolate(this.indexHtmlEn, {

View File

@ -272,23 +272,20 @@ export class PortfolioCalculator {
}
}
const isInPercentage = true;
return Object.keys(totalNetPerformanceValues).map((date) => {
return isInPercentage
? {
date,
value: totalInvestmentValues[date].eq(0)
? 0
: totalNetPerformanceValues[date]
.div(totalInvestmentValues[date])
.mul(100)
.toNumber()
}
: {
date,
value: totalNetPerformanceValues[date].toNumber()
};
const netPerformanceInPercentage = totalInvestmentValues[date].eq(0)
? 0
: totalNetPerformanceValues[date]
.div(totalInvestmentValues[date])
.mul(100)
.toNumber();
return {
date,
netPerformanceInPercentage,
netPerformance: totalNetPerformanceValues[date].toNumber(),
value: netPerformanceInPercentage
};
});
}

View File

@ -110,26 +110,6 @@ export class PortfolioController {
};
}
@Get('chart')
@UseGuards(AuthGuard('jwt'))
@Version('2')
public async getChartV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range
): Promise<PortfolioChart> {
const historicalDataContainer = await this.portfolioService.getChartV2(
impersonationId,
range
);
return {
chart: historicalDataContainer.items,
hasError: false,
isAllTimeHigh: false,
isAllTimeLow: false
};
}
@Get('details')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(RedactValuesInResponseInterceptor)
@ -319,6 +299,35 @@ export class PortfolioController {
return performanceInformation;
}
@Get('performance')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange
): Promise<PortfolioPerformanceResponse> {
const performanceInformation = await this.portfolioService.getPerformanceV2(
{
dateRange,
impersonationId
}
);
if (
impersonationId ||
this.request.user.Settings.settings.viewMode === 'ZEN' ||
this.userService.isRestrictedView(this.request.user)
) {
performanceInformation.performance = nullifyValuesInObject(
performanceInformation.performance,
['currentGrossPerformance', 'currentNetPerformance', 'currentValue']
);
}
return performanceInformation;
}
@Get('positions')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInResponseInterceptor)

View File

@ -355,11 +355,14 @@ export class PortfolioService {
};
}
public async getChartV2(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
public async getChartV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<HistoricalDataContainer> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
@ -383,7 +386,7 @@ export class PortfolioService {
const endDate = new Date();
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const startDate = this.getStartDate(dateRange, portfolioStart);
const daysInMarket = differenceInDays(new Date(), startDate);
const step = Math.round(
@ -987,6 +990,105 @@ export class PortfolioService {
};
}
public async getPerformanceV2({
dateRange = 'max',
impersonationId
}: {
dateRange?: DateRange;
impersonationId: string;
}): Promise<PortfolioPerformanceResponse> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
userId
});
const portfolioCalculator = new PortfolioCalculator({
currency: this.request.user.Settings.settings.baseCurrency,
currentRateService: this.currentRateService,
orders: portfolioOrders
});
if (transactionPoints?.length <= 0) {
return {
chart: [],
hasErrors: false,
performance: {
currentGrossPerformance: 0,
currentGrossPerformancePercent: 0,
currentNetPerformance: 0,
currentNetPerformancePercent: 0,
currentValue: 0
}
};
}
portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate
);
const hasErrors = currentPositions.hasErrors;
const currentValue = currentPositions.currentValue.toNumber();
const currentGrossPerformance = currentPositions.grossPerformance;
const currentGrossPerformancePercent =
currentPositions.grossPerformancePercentage;
let currentNetPerformance = currentPositions.netPerformance;
let currentNetPerformancePercent =
currentPositions.netPerformancePercentage;
// if (currentGrossPerformance.mul(currentGrossPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentGrossPerformancePercent = currentGrossPerformancePercent.mul(-1);
// }
// if (currentNetPerformance.mul(currentNetPerformancePercent).lt(0)) {
// // If algebraic sign is different, harmonize it
// currentNetPerformancePercent = currentNetPerformancePercent.mul(-1);
// }
const historicalDataContainer = await this.getChartV2({
dateRange,
impersonationId
});
const itemOfToday = historicalDataContainer.items.find((item) => {
return item.date === format(new Date(), DATE_FORMAT);
});
if (itemOfToday) {
currentNetPerformance = new Big(itemOfToday.netPerformance);
currentNetPerformancePercent = new Big(
itemOfToday.netPerformanceInPercentage
).div(100);
}
return {
chart: historicalDataContainer.items.map(
({ date, netPerformanceInPercentage }) => {
return {
date,
value: netPerformanceInPercentage
};
}
),
errors: currentPositions.errors,
hasErrors: currentPositions.hasErrors || hasErrors,
performance: {
currentValue,
currentGrossPerformance: currentGrossPerformance.toNumber(),
currentGrossPerformancePercent:
currentGrossPerformancePercent.toNumber(),
currentNetPerformance: currentNetPerformance.toNumber(),
currentNetPerformancePercent: currentNetPerformancePercent.toNumber()
}
};
}
public async getReport(impersonationId: string): Promise<PortfolioReport> {
const currency = this.request.user.Settings.settings.baseCurrency;
const userId = await this.getUserId(impersonationId, this.request.user.id);

View File

@ -2,7 +2,10 @@
<div class="row">
<div class="col">
<form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field appearance="outline" class="flex-grow-1">
<mat-form-field
appearance="outline"
class="compact-with-outline flex-grow-1 mr-2 without-hint"
>
<mat-select formControlName="status">
<mat-option></mat-option>
<mat-option
@ -13,7 +16,7 @@
</mat-select>
</mat-form-field>
<button
class="ml-1"
class="mt-1"
color="warn"
mat-flat-button
(click)="onDeleteJobs()"

View File

@ -162,8 +162,11 @@
</button>
</div>
<div class="mt-2">
<form #couponForm="ngForm">
<mat-form-field appearance="outline" class="mr-2">
<form #couponForm="ngForm" class="align-items-center d-flex">
<mat-form-field
appearance="outline"
class="compact-with-outline mr-2 without-hint"
>
<mat-select
name="duration"
[value]="couponDuration"
@ -176,6 +179,7 @@
</mat-select>
</mat-form-field>
<button
class="mt-1"
color="primary"
mat-flat-button
(click)="onAddCoupon()"

View File

@ -10,7 +10,11 @@
</div>
</div>
<div class="col-md-6 col-xs-12 d-flex justify-content-end">
<mat-form-field appearance="outline" class="w-100" color="accent">
<mat-form-field
appearance="outline"
class="w-100 without-hint"
color="accent"
>
<mat-label i18n>Compare with...</mat-label>
<mat-select
name="benchmark"
@ -26,7 +30,7 @@
</mat-form-field>
</div>
</div>
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="mb-3 text-center">
<div *ngIf="user.settings.viewMode !== 'ZEN'" class="my-2 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoading"

View File

@ -76,8 +76,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
!this.hasImpersonationId &&
!this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN';
this.update();
}
public onChangeDateRange(dateRange: DateRange) {
@ -104,36 +102,51 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
}
private update() {
this.historicalDataItems = null;
this.isLoadingPerformance = true;
this.dataService
.fetchChart({
.fetchPortfolioPerformance({
range: this.user?.settings?.dateRange,
version: this.user?.settings?.isExperimentalFeatures ? 2 : 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map((chartDataItem) => {
return {
date: chartDataItem.date,
value: chartDataItem.value
};
});
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchPortfolioPerformance({ range: this.user?.settings?.dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((response) => {
this.errors = response.errors;
this.hasError = response.hasErrors;
this.performance = response.performance;
this.isLoadingPerformance = false;
if (this.user?.settings?.isExperimentalFeatures) {
this.historicalDataItems = response.chart.map(({ date, value }) => {
return {
date,
value
};
});
} else {
this.dataService
.fetchChart({
range: this.user?.settings?.dateRange,
version: 1
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((chartData) => {
this.historicalDataItems = chartData.chart.map(
({ date, value }) => {
return {
date,
value
};
}
);
this.isAllTimeHigh = chartData.isAllTimeHigh;
this.isAllTimeLow = chartData.isAllTimeLow;
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck();
});

View File

@ -54,7 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public hasPermissionToUpdateViewMode: boolean;
public hasPermissionToUpdateUserSettings: boolean;
public language = document.documentElement.lang;
public locales = ['de', 'de-CH', 'en-GB', 'en-US'];
public locales = ['de', 'de-CH', 'en-GB', 'en-US', 'it'];
public price: number;
public priceId: string;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;

View File

@ -94,7 +94,10 @@
<ng-container i18n>Base Currency</ng-container>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -116,7 +119,10 @@
<div class="hint-text text-muted" i18n>Beta</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -126,6 +132,7 @@
<mat-option [value]="null"></mat-option>
<mat-option value="de">Deutsch</mat-option>
<mat-option value="en">English</mat-option>
<mat-option value="it">Italiano</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -138,7 +145,10 @@
</div>
</div>
<div class="pl-1 w-50">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="locale"
[disabled]="!hasPermissionToUpdateUserSettings"
@ -161,7 +171,10 @@
</div>
<div class="pl-1 w-50">
<div class="align-items-center d-flex overflow-hidden">
<mat-form-field appearance="outline" class="w-100">
<mat-form-field
appearance="outline"
class="compact-with-outline w-100 without-hint"
>
<mat-select
name="viewMode"
[disabled]="!hasPermissionToUpdateViewMode"

View File

@ -192,6 +192,17 @@
</div>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1">
<h4>Multi-Language</h4>
<p class="m-0">
Use Ghostfolio in multiple languages: English, German and
Italian are currently supported.
</p>
</div>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card class="d-flex flex-column h-100">
<div class="flex-grow-1">

View File

@ -55,6 +55,28 @@
</div>
</div>
<div class="row my-3">
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">360° View</mat-card-title>
Get the full picture of your personal finances across multiple
platforms.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">Web3 Ready</mat-card-title>
Use Ghostfolio anonymously and own your financial data.
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card>
<mat-card-title class="text-center">Open Source</mat-card-title>
Benefit from continuous improvements through a strong community.
</mat-card>
</div>
</div>
<div class="row my-5">
<div class="col-md-6 offset-md-3">
<h2 class="h4 mb-1 text-center">Why <strong>Ghostfolio</strong>?</h2>
@ -133,19 +155,43 @@
</div>
</div>
<div class="row my-5">
<div class="col-md-6 offset-md-3">
<div class="row my-3">
<div class="col-12">
<h2 class="h4 mb-1 text-center">
How does <strong>Ghostfolio</strong> work?
</h2>
<p class="lead mb-3 text-center">Get started in only 3 steps</p>
<ol class="m-0 pl-3">
<li class="mb-2">
Sign up anonymously<br />(no e-mail address nor credit card required)
</li>
<li class="mb-2">Add any of your historical transactions</li>
<li>Get valuable insights of your portfolio composition</li>
</ol>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">Sign up anonymously*</div>
<div class="text-muted">
<small>* no e-mail address nor credit card required</small>
</div>
</div>
<div class="pl-2 text-muted text-right">1</div>
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">
Add any of your historical transactions
</div>
</div>
<div class="pl-2 text-muted text-right">2</div>
</mat-card>
</div>
<div class="col-md-4 my-2">
<mat-card class="d-flex flex-row h-100">
<div class="flex-grow-1">
<div class="font-weight-bold">
Get valuable insights of your portfolio composition
</div>
</div>
<div class="pl-2 text-muted text-right">3</div>
</mat-card>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfLogoModule } from '@ghostfolio/ui/logo';
@ -14,6 +15,7 @@ import { LandingPageComponent } from './landing-page.component';
GfLogoModule,
LandingPageRoutingModule,
MatButtonModule,
MatCardModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@ -13,8 +13,16 @@
<div class="row">
<div class="col">
<mat-card class="mb-3">
<mat-card-header>
<mat-card-title i18n>Proportion of Net Worth</mat-card-title>
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n
>Proportion of Net Worth</mat-card-title
>
<gf-value
class="align-items-end flex-grow-1 ml-2"
size="medium"
[isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
></gf-value>
</mat-card-header>
<mat-card-content>
<mat-progress-bar

View File

@ -20,6 +20,7 @@
::ng-deep {
.mat-card-header-text {
flex: 1 1 auto;
overflow: hidden;
}
}

View File

@ -126,7 +126,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.isLoadingBenchmarkComparator = true;
this.dataService
.fetchChart({ range: this.user?.settings?.dateRange, version: 2 })
.fetchPortfolioPerformance({
range: this.user?.settings?.dateRange,
version: 2
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {
this.firstOrderDate = new Date(chart?.[0]?.date ?? new Date());

View File

@ -353,12 +353,16 @@ export class DataService {
});
}
public fetchPortfolioPerformance(params: { [param: string]: any }) {
public fetchPortfolioPerformance({
range,
version
}: {
range: DateRange;
version: number;
}) {
return this.http.get<PortfolioPerformanceResponse>(
'/api/v1/portfolio/performance',
{
params
}
`/api/v${version}/portfolio/performance`,
{ params: { range } }
);
}

File diff suppressed because it is too large Load Diff

View File

@ -146,13 +146,6 @@ ngx-skeleton-loader {
@include gf-table;
}
.mat-fab,
.mat-flat-button {
&.mat-primary {
color: rgba(var(--light-primary-text)) !important;
}
}
.mat-card {
&:not([class*='mat-elevation-z']) {
border: 1px solid rgba(var(--dark-dividers));
@ -164,6 +157,49 @@ ngx-skeleton-loader {
margin: 0 !important;
}
.mat-fab,
.mat-flat-button {
&.mat-primary {
color: rgba(var(--light-primary-text)) !important;
}
}
.mat-form-field {
&.compact-with-outline {
.mat-form-field-wrapper {
margin: 0.5rem 0 0.25rem;
padding-bottom: 1rem;
.mat-form-field-infix {
border-top-width: 0;
padding: 1rem 0 0.75rem;
.mat-form-field-label {
margin-top: 0.1rem;
}
.mat-select-arrow-wrapper {
transform: none;
}
}
.mat-form-field-prefix {
top: 0;
}
.mat-form-field-suffix {
top: 0;
}
}
}
&.without-hint {
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
}
.no-min-width {
min-width: unset !important;
}

View File

@ -1,7 +1,7 @@
import * as currencies from '@dinero.js/currencies';
import { DataSource } from '@prisma/client';
import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
import { de } from 'date-fns/locale';
import { de, it } from 'date-fns/locale';
import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark } from './interfaces';
@ -75,6 +75,8 @@ export function getCssVariable(aCssVariable: string) {
export function getDateFnsLocale(aLanguageCode: string) {
if (aLanguageCode === 'de') {
return de;
} else if (aLanguageCode === 'it') {
return it;
}
return undefined;

View File

@ -2,5 +2,7 @@ export interface HistoricalDataItem {
averagePrice?: number;
date: string;
grossPerformancePercent?: number;
netPerformance?: number;
netPerformanceInPercentage?: number;
value: number;
}

View File

@ -1,6 +1,8 @@
import { HistoricalDataItem } from '../historical-data-item.interface';
import { PortfolioPerformance } from '../portfolio-performance.interface';
import { ResponseError } from './errors.interface';
export interface PortfolioPerformanceResponse extends ResponseError {
chart?: HistoricalDataItem[];
performance: PortfolioPerformance;
}

View File

@ -93,7 +93,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
}
public ngOnChanges() {
if (this.historicalDataItems) {
if (this.historicalDataItems || this.historicalDataItems === null) {
setTimeout(() => {
// Wait for the chartCanvas
this.initialize();

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.195.0",
"version": "1.197.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {