Feature/add world map (#150)
* Add a global heat map * Update changelog
This commit is contained in:
parent
74954bc51d
commit
2eafc042ad
@ -5,6 +5,10 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Added a global heat map to visualize positions by country
|
||||||
|
|
||||||
## 1.12.0 - 06.06.2021
|
## 1.12.0 - 06.06.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
class="h-100"
|
||||||
|
[theme]="{
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
|
||||||
|
<div class="align-items-center d-flex h-100 w-100" id="svgMap"></div>
|
@ -0,0 +1,24 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.loader {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svgMap-map-wrapper {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.svgMap-map-controls-wrapper {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
.svgMap-tooltip {
|
||||||
|
background: var(--dark-background);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { primaryColorHex } from '@ghostfolio/common/config';
|
||||||
|
import { getCssVariable, getTextColor } from '@ghostfolio/common/helper';
|
||||||
|
import { Currency } from '@prisma/client';
|
||||||
|
import svgMap from 'svgmap';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-world-map-chart',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './world-map-chart.component.html',
|
||||||
|
styleUrls: ['./world-map-chart.component.scss']
|
||||||
|
})
|
||||||
|
export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
|
@Input() baseCurrency: Currency;
|
||||||
|
@Input() countries: { [code: string]: { name: string; value: number } };
|
||||||
|
|
||||||
|
public isLoading = true;
|
||||||
|
public svgMapElement;
|
||||||
|
|
||||||
|
public constructor(private changeDetectorRef: ChangeDetectorRef) {}
|
||||||
|
|
||||||
|
public ngOnInit() {}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
if (this.countries) {
|
||||||
|
this.destroySvgMap();
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.destroySvgMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize() {
|
||||||
|
this.svgMapElement = new svgMap({
|
||||||
|
colorMax: primaryColorHex,
|
||||||
|
colorMin: '#d3f4f3',
|
||||||
|
colorNoData: `rgba(${getTextColor()}, ${getCssVariable(
|
||||||
|
'--palette-foreground-divider-alpha'
|
||||||
|
)})`,
|
||||||
|
data: {
|
||||||
|
applyData: 'value',
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
format: `{0} ${this.baseCurrency}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
values: this.countries
|
||||||
|
},
|
||||||
|
hideFlag: true,
|
||||||
|
minZoom: 1.06,
|
||||||
|
maxZoom: 1.06,
|
||||||
|
targetElementID: 'svgMap'
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroySvgMap() {
|
||||||
|
this.svgMapElement?.mapWrapper?.remove();
|
||||||
|
this.svgMapElement?.tooltip?.remove();
|
||||||
|
|
||||||
|
this.svgMapElement = null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { WorldMapChartComponent } from './world-map-chart.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [WorldMapChartComponent],
|
||||||
|
exports: [WorldMapChartComponent],
|
||||||
|
imports: [CommonModule, NgxSkeletonLoaderModule],
|
||||||
|
providers: []
|
||||||
|
})
|
||||||
|
export class GfWorldMapChartModule {}
|
@ -102,50 +102,6 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
|
||||||
<mat-card class="mb-3">
|
|
||||||
<mat-card-header class="w-100">
|
|
||||||
<mat-card-title i18n>By Continent</mat-card-title>
|
|
||||||
<gf-toggle
|
|
||||||
[defaultValue]="period"
|
|
||||||
[isLoading]="false"
|
|
||||||
[options]="periodOptions"
|
|
||||||
(change)="onChangePeriod($event.value)"
|
|
||||||
></gf-toggle>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
<gf-portfolio-proportion-chart
|
|
||||||
key="name"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
|
||||||
[isInPercent]="false"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[positions]="continents"
|
|
||||||
></gf-portfolio-proportion-chart>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<mat-card class="mb-3">
|
|
||||||
<mat-card-header class="w-100">
|
|
||||||
<mat-card-title i18n>By Country</mat-card-title>
|
|
||||||
<gf-toggle
|
|
||||||
[defaultValue]="period"
|
|
||||||
[isLoading]="false"
|
|
||||||
[options]="periodOptions"
|
|
||||||
(change)="onChangePeriod($event.value)"
|
|
||||||
></gf-toggle>
|
|
||||||
</mat-card-header>
|
|
||||||
<mat-card-content>
|
|
||||||
<gf-portfolio-proportion-chart
|
|
||||||
key="name"
|
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
|
||||||
[isInPercent]="false"
|
|
||||||
[locale]="user?.settings?.locale"
|
|
||||||
[positions]="countries"
|
|
||||||
></gf-portfolio-proportion-chart>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header class="w-100">
|
<mat-card-header class="w-100">
|
||||||
@ -190,6 +146,50 @@
|
|||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="w-100">
|
||||||
|
<mat-card-title i18n>By Continent</mat-card-title>
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="period"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="periodOptions"
|
||||||
|
(change)="onChangePeriod($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-portfolio-proportion-chart
|
||||||
|
key="name"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isInPercent]="false"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[positions]="continents"
|
||||||
|
></gf-portfolio-proportion-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="w-100">
|
||||||
|
<mat-card-title i18n>By Country</mat-card-title>
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="period"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="periodOptions"
|
||||||
|
(change)="onChangePeriod($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-portfolio-proportion-chart
|
||||||
|
key="name"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[isInPercent]="false"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[positions]="countries"
|
||||||
|
></gf-portfolio-proportion-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-block d-sm-none row">
|
<div class="d-block d-sm-none row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
@ -211,7 +211,28 @@
|
|||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-none d-sm-block row">
|
<div class="row world-map-chart">
|
||||||
|
<div class="col-lg">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header class="w-100">
|
||||||
|
<mat-card-title i18n>Global Heat Map</mat-card-title>
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="period"
|
||||||
|
[isLoading]="false"
|
||||||
|
[options]="periodOptions"
|
||||||
|
(change)="onChangePeriod($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<gf-world-map-chart
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[countries]="countries"
|
||||||
|
></gf-world-map-chart>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
|
@ -6,6 +6,7 @@ import { PortfolioPositionsChartModule } from '@ghostfolio/client/components/por
|
|||||||
import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
import { PortfolioProportionChartModule } from '@ghostfolio/client/components/portfolio-proportion-chart/portfolio-proportion-chart.module';
|
||||||
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
|
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
|
||||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
|
||||||
|
|
||||||
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
||||||
import { AnalysisPageComponent } from './analysis-page.component';
|
import { AnalysisPageComponent } from './analysis-page.component';
|
||||||
@ -19,6 +20,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
|||||||
GfInvestmentChartModule,
|
GfInvestmentChartModule,
|
||||||
GfPositionsTableModule,
|
GfPositionsTableModule,
|
||||||
GfToggleModule,
|
GfToggleModule,
|
||||||
|
GfWorldMapChartModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
PortfolioPositionsChartModule,
|
PortfolioPositionsChartModule,
|
||||||
PortfolioProportionChartModule
|
PortfolioProportionChartModule
|
||||||
|
@ -7,6 +7,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.world-map-chart {
|
||||||
|
.mat-card {
|
||||||
|
.mat-card-content {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-card {
|
.mat-card {
|
||||||
.mat-card-header {
|
.mat-card-header {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
@import '~angular-material-css-vars/main';
|
@import '~angular-material-css-vars/main';
|
||||||
|
|
||||||
|
@import '~svgmap/dist/svgMap';
|
||||||
|
|
||||||
$mat-css-dark-theme-selector: '.is-dark-theme';
|
$mat-css-dark-theme-selector: '.is-dark-theme';
|
||||||
$mat-css-light-theme-selector: '.is-light-theme';
|
$mat-css-light-theme-selector: '.is-light-theme';
|
||||||
|
|
||||||
|
@ -97,6 +97,7 @@
|
|||||||
"reflect-metadata": "0.1.13",
|
"reflect-metadata": "0.1.13",
|
||||||
"round-to": "5.0.0",
|
"round-to": "5.0.0",
|
||||||
"rxjs": "6.6.7",
|
"rxjs": "6.6.7",
|
||||||
|
"svgmap": "2.1.1",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"yahoo-finance": "0.3.6",
|
"yahoo-finance": "0.3.6",
|
||||||
"zone.js": "0.11.4"
|
"zone.js": "0.11.4"
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -12382,6 +12382,18 @@ supports-hyperlinks@^2.0.0:
|
|||||||
has-flag "^4.0.0"
|
has-flag "^4.0.0"
|
||||||
supports-color "^7.0.0"
|
supports-color "^7.0.0"
|
||||||
|
|
||||||
|
svg-pan-zoom@^3.6.1:
|
||||||
|
version "3.6.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/svg-pan-zoom/-/svg-pan-zoom-3.6.1.tgz#f880a1bb32d18e9c625d7715350bebc269b450cf"
|
||||||
|
integrity sha512-JaKkGHHfGvRrcMPdJWkssLBeWqM+Isg/a09H7kgNNajT1cX5AztDTNs+C8UzpCxjCTRrG34WbquwaovZbmSk9g==
|
||||||
|
|
||||||
|
svgmap@2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/svgmap/-/svgmap-2.1.1.tgz#355c259cf4e04b20d2d39bab05d0e718ade942ff"
|
||||||
|
integrity sha512-1blZYMYDXq8H3xykzgBJRh5q+XPd5JLOJ8K7UuZI6ab2D3hngiVcr+Z1olfy7DH9Xf9AOCTpt4Id7iVD8cKD0A==
|
||||||
|
dependencies:
|
||||||
|
svg-pan-zoom "^3.6.1"
|
||||||
|
|
||||||
svgo@^1.0.0:
|
svgo@^1.0.0:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
|
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user