mirror of
https://github.com/mempool/mempool.git
synced 2025-02-23 06:35:15 +01:00
Merge pull request #2139 from mempool/nymkappa/feature/ln-nodes-map
Create lightning nodes world heat map (clearnet)
This commit is contained in:
commit
c7909a1ca8
10 changed files with 235 additions and 2 deletions
|
@ -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', '/resources/worldmap.json'],
|
||||
target: "https://mempool.space",
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
i18n="lightning.nodes-per-isp">Lightning nodes per ISP</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-country' | relativeUrl]"
|
||||
i18n="lightning.nodes-per-isp">Lightning nodes per country</a>
|
||||
<a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-map' | relativeUrl]"
|
||||
i18n="lightning.nodes-per-isp">Lightning nodes world map</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<div class="full-container">
|
||||
|
||||
<div class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-heatmap">Lightning nodes world heat map</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px">
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true" (click)="onSaveChart()"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -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;
|
||||
}
|
||||
}
|
163
frontend/src/app/lightning/nodes-map/nodes-map.component.ts
Normal file
163
frontend/src/app/lightning/nodes-map/nodes-map.component.ts
Normal file
|
@ -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<any>;
|
||||
|
||||
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 `<b style="color: white">${country.name}<br>0 nodes</b><br>`;
|
||||
} else {
|
||||
return `<b style="color: white">${country.data.name}<br>${country.data.value} nodes</b><br>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -14,6 +14,7 @@ export class AssetsService {
|
|||
|
||||
getAssetsJson$: Observable<{ array: AssetExtended[]; objects: any}>;
|
||||
getAssetsMinimalJson$: Observable<any>;
|
||||
getWorldMapJson$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
|
@ -65,5 +66,7 @@ export class AssetsService {
|
|||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
this.getWorldMapJson$ = this.httpClient.get(apiBaseUrl + '/resources/worldmap.json').pipe(shareReplay());
|
||||
}
|
||||
}
|
||||
|
|
1
frontend/src/resources/worldmap.json
Normal file
1
frontend/src/resources/worldmap.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue