From 59f84e82b4d0f8b792211f3f0644ccc9b96a318d Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 20 Jul 2022 11:39:51 +0200 Subject: [PATCH] Create lightning nodes world heat map (clearnet) --- frontend/proxy.conf.js | 2 +- .../components/graphs/graphs.component.html | 2 + .../src/app/graphs/graphs.routing.module.ts | 5 + .../src/app/lightning/lightning.module.ts | 2 + .../nodes-map/nodes-map.component.html | 17 ++ .../nodes-map/nodes-map.component.scss | 40 +++++ .../nodes-map/nodes-map.component.ts | 163 ++++++++++++++++++ .../nodes-per-isp/nodes-per-isp.component.ts | 2 +- frontend/src/app/services/assets.service.ts | 3 + 9 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/lightning/nodes-map/nodes-map.component.html create mode 100644 frontend/src/app/lightning/nodes-map/nodes-map.component.scss create mode 100644 frontend/src/app/lightning/nodes-map/nodes-map.component.ts 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 @@ +
+ +
+
+ Lightning nodes world heat map + +
+ (Tor nodes excluded) +
+ +
+
+ +
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()); } }