diff --git a/frontend/proxy.conf.js b/frontend/proxy.conf.js
index 77a77bb5a..05dd0411e 100644
--- a/frontend/proxy.conf.js
+++ b/frontend/proxy.conf.js
@@ -85,7 +85,7 @@ if (configContent && configContent.BASE_MODULE == "liquid") {
});
} else {
PROXY_CONFIG.push({
- context: ['/resources/pools.json', '/resources/assets.json', '/resources/assets.minimal.json'],
+ context: ['/resources/pools.json', '/resources/assets.json', '/resources/assets.minimal.json', '/ressources/worldmap.json'],
target: "https://mempool.space",
secure: false,
changeOrigin: true,
diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html
index 0849c8acd..905b2d296 100644
--- a/frontend/src/app/components/graphs/graphs.component.html
+++ b/frontend/src/app/components/graphs/graphs.component.html
@@ -38,6 +38,8 @@
i18n="lightning.nodes-per-isp">Lightning nodes per ISP
Lightning nodes per country
+ Lightning nodes world map
diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts
index a853ad576..1bed752dc 100644
--- a/frontend/src/app/graphs/graphs.routing.module.ts
+++ b/frontend/src/app/graphs/graphs.routing.module.ts
@@ -22,6 +22,7 @@ import { NodesNetworksChartComponent } from '../lightning/nodes-networks-chart/n
import { LightningStatisticsChartComponent } from '../lightning/statistics-chart/lightning-statistics-chart.component';
import { NodesPerISPChartComponent } from '../lightning/nodes-per-isp-chart/nodes-per-isp-chart.component';
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
+import { NodesMap } from '../lightning/nodes-map/nodes-map.component';
const browserWindow = window || {};
// @ts-ignore
@@ -109,6 +110,10 @@ const routes: Routes = [
path: 'lightning/nodes-per-country',
component: NodesPerCountryChartComponent,
},
+ {
+ path: 'lightning/nodes-map',
+ component: NodesMap,
+ },
{
path: '',
redirectTo: 'mempool',
diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts
index 4e2633b65..74cae756c 100644
--- a/frontend/src/app/lightning/lightning.module.ts
+++ b/frontend/src/app/lightning/lightning.module.ts
@@ -22,6 +22,7 @@ import { NodesPerISPChartComponent } from './nodes-per-isp-chart/nodes-per-isp-c
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
+import { NodesMap } from '../lightning/nodes-map/nodes-map.component';
@NgModule({
declarations: [
LightningDashboardComponent,
@@ -41,6 +42,7 @@ import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-ch
NodesPerCountry,
NodesPerISP,
NodesPerCountryChartComponent,
+ NodesMap,
],
imports: [
CommonModule,
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.html b/frontend/src/app/lightning/nodes-map/nodes-map.component.html
new file mode 100644
index 000000000..b762b2d24
--- /dev/null
+++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.html
@@ -0,0 +1,17 @@
+
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss
new file mode 100644
index 000000000..4e363a534
--- /dev/null
+++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss
@@ -0,0 +1,40 @@
+.card-header {
+ border-bottom: 0;
+ font-size: 18px;
+ @media (min-width: 465px) {
+ font-size: 20px;
+ }
+}
+
+.full-container {
+ padding: 0px 15px;
+ width: 100%;
+ min-height: 500px;
+ height: calc(100% - 150px);
+ @media (max-width: 992px) {
+ height: 100%;
+ padding-bottom: 100px;
+ };
+}
+
+.chart {
+ width: 100%;
+ height: 100%;
+ padding-bottom: 20px;
+ padding-right: 10px;
+ @media (max-width: 992px) {
+ padding-bottom: 25px;
+ }
+ @media (max-width: 829px) {
+ padding-bottom: 50px;
+ }
+ @media (max-width: 767px) {
+ padding-bottom: 25px;
+ }
+ @media (max-width: 629px) {
+ padding-bottom: 55px;
+ }
+ @media (max-width: 567px) {
+ padding-bottom: 55px;
+ }
+}
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts
new file mode 100644
index 000000000..9dd1ef8b5
--- /dev/null
+++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts
@@ -0,0 +1,163 @@
+import { ChangeDetectionStrategy, Component, NgZone, OnDestroy, OnInit } from '@angular/core';
+import { mempoolFeeColors } from 'src/app/app.constants';
+import { SeoService } from 'src/app/services/seo.service';
+import { ApiService } from 'src/app/services/api.service';
+import { combineLatest, Observable, tap } from 'rxjs';
+import { AssetsService } from 'src/app/services/assets.service';
+import { EChartsOption, MapSeriesOption, registerMap } from 'echarts';
+import { download } from 'src/app/shared/graphs.utils';
+import { Router } from '@angular/router';
+import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
+import { StateService } from 'src/app/services/state.service';
+
+@Component({
+ selector: 'app-nodes-map',
+ templateUrl: './nodes-map.component.html',
+ styleUrls: ['./nodes-map.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NodesMap implements OnInit, OnDestroy {
+ observable$: Observable;
+
+ chartInstance = undefined;
+ chartOptions: EChartsOption = {};
+ chartInitOptions = {
+ renderer: 'svg',
+ };
+
+ constructor(
+ private seoService: SeoService,
+ private apiService: ApiService,
+ private stateService: StateService,
+ private assetsService: AssetsService,
+ private router: Router,
+ private zone: NgZone,
+ ) {
+ }
+
+ ngOnDestroy(): void {}
+
+ ngOnInit(): void {
+ this.seoService.setTitle($localize`Lightning nodes world map`);
+
+ this.observable$ = combineLatest([
+ this.assetsService.getWorldMapJson$,
+ this.apiService.getNodesPerCountry()
+ ]).pipe(tap((data) => {
+ registerMap('world', data[0]);
+
+ const countries = [];
+ let max = 0;
+ for (const country of data[1]) {
+ countries.push({
+ name: country.name.en,
+ value: country.count,
+ iso: country.iso.toLowerCase(),
+ });
+ max = Math.max(max, country.count);
+ }
+
+ this.prepareChartOptions(countries, max);
+ }));
+ }
+
+ prepareChartOptions(countries, max) {
+ let title: object;
+ if (countries.length === 0) {
+ title = {
+ textStyle: {
+ color: 'grey',
+ fontSize: 15
+ },
+ text: $localize`No data to display yet`,
+ left: 'center',
+ top: 'center'
+ };
+ }
+
+ this.chartOptions = {
+ title: countries.length === 0 ? title : undefined,
+ tooltip: {
+ backgroundColor: 'rgba(17, 19, 31, 1)',
+ borderRadius: 4,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ textStyle: {
+ color: '#b1b1b1',
+ },
+ borderColor: '#000',
+ formatter: function(country) {
+ if (country.data === undefined) {
+ return `${country.name}
0 nodes
`;
+ } else {
+ return `${country.data.name}
${country.data.value} nodes
`;
+ }
+ }
+ },
+ visualMap: {
+ left: 'right',
+ show: true,
+ min: 1,
+ max: max,
+ text: ['High', 'Low'],
+ calculable: true,
+ textStyle: {
+ color: 'white',
+ },
+ inRange: {
+ color: mempoolFeeColors.map(color => `#${color}`),
+ },
+ },
+ series: {
+ type: 'map',
+ map: 'world',
+ emphasis: {
+ label: {
+ show: false,
+ },
+ itemStyle: {
+ areaColor: '#FDD835',
+ }
+ },
+ data: countries,
+ itemStyle: {
+ areaColor: '#5A6A6D'
+ },
+ }
+ };
+ }
+
+ onChartInit(ec) {
+ if (this.chartInstance !== undefined) {
+ return;
+ }
+
+ this.chartInstance = ec;
+
+ this.chartInstance.on('click', (e) => {
+ if (e.data && e.data.value > 0) {
+ this.zone.run(() => {
+ const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/country/${e.data.iso}`);
+ this.router.navigate([url]);
+ });
+ }
+ });
+ }
+
+ onSaveChart() {
+ // @ts-ignore
+ const prevBottom = this.chartOptions.grid.bottom;
+ const now = new Date();
+ // @ts-ignore
+ this.chartOptions.grid.bottom = 30;
+ this.chartOptions.backgroundColor = '#11131f';
+ this.chartInstance.setOption(this.chartOptions);
+ download(this.chartInstance.getDataURL({
+ pixelRatio: 2,
+ excludeComponents: ['dataZoom'],
+ }), `lightning-nodes-heatmap-clearnet-${Math.round(now.getTime() / 1000)}.svg`);
+ // @ts-ignore
+ this.chartOptions.grid.bottom = prevBottom;
+ this.chartOptions.backgroundColor = 'none';
+ this.chartInstance.setOption(this.chartOptions);
+ }
+}
diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts
index d29d0e67f..4c7667f5d 100644
--- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts
+++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
diff --git a/frontend/src/app/services/assets.service.ts b/frontend/src/app/services/assets.service.ts
index 880883a8c..decc8cbad 100644
--- a/frontend/src/app/services/assets.service.ts
+++ b/frontend/src/app/services/assets.service.ts
@@ -14,6 +14,7 @@ export class AssetsService {
getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
getAssetsMinimalJson$: Observable;
+ getWorldMapJson$: Observable;
constructor(
private httpClient: HttpClient,
@@ -65,5 +66,7 @@ export class AssetsService {
}),
shareReplay(1),
);
+
+ this.getWorldMapJson$ = this.httpClient.get(apiBaseUrl + '/resources/worldmap.json').pipe(shareReplay());
}
}