Merge pull request #2139 from mempool/nymkappa/feature/ln-nodes-map

Create lightning nodes world heat map (clearnet)
This commit is contained in:
wiz 2022-07-20 09:47:27 -05:00 committed by GitHub
commit c7909a1ca8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 235 additions and 2 deletions

View file

@ -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,

View file

@ -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>

View file

@ -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',

View file

@ -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,

View file

@ -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>

View file

@ -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;
}
}

View 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);
}
}

View file

@ -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';

View file

@ -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());
}
}

File diff suppressed because one or more lines are too long