Merge branch 'master' into nymkappa/bugfix/skeleton-label

This commit is contained in:
wiz
2022-09-09 17:33:14 +02:00
committed by GitHub
72 changed files with 3444 additions and 463 deletions

View File

@@ -19,31 +19,31 @@
<tr>
<td i18n="address.total-sent">Fee rate</td>
<td>
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
{{ channel.fee_rate ?? '-' }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Base fee</td>
<td>
<app-sats [satoshis]="channel.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
<app-sats [valueOverride]="!channel.base_fee_mtokens ? '- ' : undefined" [satoshis]="channel.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Min HTLC</td>
<td>
<app-sats [satoshis]="channel.min_htlc_mtokens / 1000"></app-sats>
<app-sats [valueOverride]="!channel.min_htlc_mtokens ? '- ' : undefined" [satoshis]="channel.min_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Max HTLC</td>
<td>
<app-sats [satoshis]="channel.max_htlc_mtokens / 1000"></app-sats>
<app-sats [valueOverride]="!channel.max_htlc_mtokens ? '- ' : undefined" [satoshis]="channel.max_htlc_mtokens / 1000"></app-sats>
</td>
</tr>
<tr>
<td i18n="address.total-sent">Timelock delta</td>
<td>
<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta }"></ng-container>
<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta ?? '-' }"></ng-container>
</td>
</tr>
</tbody>

View File

@@ -1,9 +1,9 @@
<div class="box preview-box" *ngIf="(channel$ | async) as channel">
<h2 class="preview-header" i18n="lightning.channel">lightning channel</h2>
<div class="row d-flex justify-content-between full-width-row">
<h1 class="title">
<span i18n="lightning.channel">Channel</span>:
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> {{ channel.short_id }}</a>
</h1>
<div class="title-wrapper">
<h1 class="title">{{ channel.short_id }}</h1>
</div>
<div class="badges mb-2">
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
@@ -12,20 +12,11 @@
<app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type>
</div>
</div>
<div class="row d-flex justify-content-between full-width-row nodes">
<span class="node left">
{{ channel.node_left.alias || '?' }}
</span>
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon>
<span class="node right">
{{ channel.node_right.alias || '?' }}
</span>
</div>
<div class="row">
<div class="col-md">
<a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
<table class="table table-borderless table-striped">
<tbody>
<tr></tr>
<tr>
<td i18n="channel.created">Created</td>
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
@@ -61,6 +52,15 @@
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
</div>
</div>
<div class="row d-flex justify-content-between full-width-row nodes">
<span class="node left">
{{ channel.node_left.alias || '?' }}
</span>
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon>
<span class="node right">
{{ channel.node_right.alias || '?' }}
</span>
</div>
</div>
<ng-template [ngIf]="error">

View File

@@ -1,15 +1,17 @@
.title {
font-size: 52px;
margin: 0;
}
.table {
font-size: 32px;
margin-top: 36px;
margin-top: 10px;
}
.badges {
font-size: 28px;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: baseline;
justify-content: flex-end;
::ng-deep .badge {
margin-left: 0.5em;
@@ -23,11 +25,12 @@
.full-width-row {
padding-left: 15px;
padding-right: 15px;
flex-wrap: nowrap;
}
&:nth-child(even) {
background: #181b2d;
margin: 15px 0;
}
.row.nodes {
background: #181b2d;
margin: 15px 0 0;
}
.nodes {
@@ -46,7 +49,7 @@
min-width: 470px;
padding: 0;
background: #181b2d;
max-height: 470px;
max-height: 350px;
overflow: hidden;
}

View File

@@ -16,7 +16,8 @@
<div class="clearfix"></div>
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'"
[channel]="channelGeo"></app-nodes-channels-map>
<div class="box">
@@ -25,13 +26,17 @@
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-sent">Created</td>
<td i18n="lightning.created">Created</td>
<td><app-timestamp [dateString]="channel.created"></app-timestamp></td>
</tr>
<tr>
<td i18n="address.total-sent">Last update</td>
<tr *ngIf="channel.status !== 2">
<td i18n="lightning.last-update">Last update</td>
<td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td>
</tr>
<tr *ngIf="channel.status === 2">
<td i18n="lightning.closing_date">Closing date</td>
<td><app-timestamp [dateString]="channel.closing_date"></app-timestamp></td>
</tr>
</tbody>
</table>
</div>
@@ -47,38 +52,57 @@
</table>
</div>
</div>
</div>
<br>
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<app-channel-box [channel]="channel.node_left"></app-channel-box>
<div class="w-100 d-block d-md-none"></div>
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-received">Capacity</td>
<td>
<app-sats [satoshis]="channel.capacity"></app-sats>
<app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<app-channel-box [channel]="channel.node_right"></app-channel-box>
</div>
</div>
<br>
<ng-container *ngIf="transactions$ | async as transactions">
<ng-template [ngIf]="transactions[0]">
<div class="d-flex">
<h3>Opening transaction</h3>
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
</div>
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
</ng-template>
<ng-template [ngIf]="transactions[1]">
<div class="closing-header d-flex">
<h3 style="margin: 0;">Closing transaction</h3>&nbsp;&nbsp;<app-closing-type [type]="channel.closing_reason"></app-closing-type>
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
</div>
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
</ng-template>
</ng-container>
</div>
<br>
<div class="row row-cols-1 row-cols-md-2">
<div class="col">
<app-channel-box [channel]="channel.node_left"></app-channel-box>
</div>
<div class="col">
<app-channel-box [channel]="channel.node_right"></app-channel-box>
</div>
</div>
<br>
<ng-container *ngIf="transactions$ | async as transactions">
<ng-template [ngIf]="transactions[0]">
<div class="d-flex">
<h3>Opening transaction</h3>
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()"
i18n="transaction.details|Transaction Details">Details</button>
</div>
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
</app-transactions-list>
</ng-template>
<ng-template [ngIf]="transactions[1]">
<div class="closing-header d-flex">
<h3 style="margin: 0;">Closing transaction</h3>&nbsp;&nbsp;<app-closing-type [type]="channel.closing_reason">
</app-closing-type>
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()"
i18n="transaction.details|Transaction Details">Details</button>
</div>
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
</app-transactions-list>
</ng-template>
</ng-container>
</div>
@@ -104,7 +128,7 @@
<div class="badges mb-2">
<span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span>
</div>
<div class="clearfix"></div>
<div style="height: 413px; padding: 15px;">
@@ -148,4 +172,4 @@
</div>
</div>
</ng-template>
</ng-template>

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, of, zip } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { IChannel } from 'src/app/interfaces/node-api.interface';
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { SeoService } from 'src/app/services/seo.service';
@@ -31,9 +31,11 @@ export class ChannelComponent implements OnInit {
.pipe(
switchMap((params: ParamMap) => {
this.error = null;
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
return this.lightningApiService.getChannel$(params.get('short_id'))
.pipe(
tap((value) => {
this.seoService.setTitle(`Channel: ${value.short_id}`);
}),
catchError((err) => {
this.error = err;
return of(null);

View File

@@ -35,7 +35,8 @@
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction">&nbsp;</th>
<th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
<th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
<th *ngIf="status === 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.closing_date">Closing date</th>
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
</thead>
@@ -71,9 +72,12 @@
</ng-template>
</ng-template>
</td>
<td class="capacity text-left d-none d-md-table-cell">
<td *ngIf="status !== 'closed'" class="capacity text-left d-none d-md-table-cell">
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
</td>
<td *ngIf="status === 'closed'" class="capacity text-left d-none d-md-table-cell">
<app-timestamp [unixTime]="channel.closing_date"></app-timestamp>
</td>
<td class="capacity text-right d-none d-md-table-cell">
<app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallchannel>

View File

@@ -21,7 +21,7 @@ export class LightningDashboardComponent implements OnInit {
) { }
ngOnInit(): void {
this.seoService.setTitle($localize`Lightning Dashboard`);
this.seoService.setTitle($localize`Lightning Network`);
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());

View File

@@ -1,15 +1,17 @@
<div class="box preview-box" *ngIf="(node$ | async) as node">
<h2 class="preview-header" i18n="lightning.node">lightning node</h2>
<div class="row d-flex justify-content-between full-width-row">
<h1 class="title">
<span i18n="lightning.node">Node</span>:
<a [routerLink]="['/lightning/node' | relativeUrl, node.id]"> {{ node.alias }}</a>
</h1>
<h1 class="title"></h1>
<div class="title-wrapper">
<h1 class="title">{{ node.alias }}</h1>
</div>
<div class="badges mb-2">
<span class="badge rounded-pill badge-success" *ngFor="let socketType of socketTypes">{{ socketType }}</span>
</div>
</div>
<div class="row">
<div class="col-md">
<a class="subtitle" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.public_key }}</a>
<table class="table table-borderless table-striped">
<tbody>
<tr>

View File

@@ -1,15 +1,17 @@
.title {
font-size: 52px;
margin-bottom: 0;
}
.table {
margin-top: 48px;
margin-top: 6px;
font-size: 32px;
}
.badges {
font-size: 28px;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: baseline;
justify-content: flex-end;
::ng-deep .badge {
margin-left: 0.5em;
@@ -20,14 +22,14 @@
flex-grow: 0;
flex-shrink: 0;
width: 470px;
height: 390px;
height: 408px;
min-width: 470px;
min-height: 390px;
max-height: 390px;
min-height: 408px;
max-height: 408px;
padding: 0;
background: #181b2d;
overflow: hidden;
margin-top: 18px;
margin-top: 6px;
}
.row {
@@ -36,6 +38,7 @@
.full-width-row {
padding-left: 15px;
flex-wrap: nowrap;
}
::ng-deep .symbol {

View File

@@ -120,7 +120,7 @@
</div>
<div *ngIf="!error">
<div class="row" *ngIf="node.as_number">
<div class="row" *ngIf="node.as_number && node.active_channel_count">
<div class="col-sm">
<app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key" [hasLocation]="!!node.as_number"></app-nodes-channels-map>
</div>
@@ -128,7 +128,7 @@
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
</div>
</div>
<div *ngIf="!node.as_number">
<div *ngIf="!node.as_number || !node.active_channel_count">
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
</div>

View File

@@ -1,16 +1,13 @@
<div class="full-container">
<div class="full-container" [class]="widget ? 'widget' : ''">
<div class="card-header">
<div *ngIf="!widget" 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>
<span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
</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"
<div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>

View File

@@ -16,6 +16,11 @@
padding-bottom: 100px;
};
}
.full-container.widget {
min-height: 240px;
height: 240px;
padding: 0px;
}
.chart {
width: 100%;
@@ -38,3 +43,6 @@
padding-bottom: 55px;
}
}
.chart.widget {
padding: 0px;
}

View File

@@ -1,14 +1,15 @@
import { ChangeDetectionStrategy, Component, NgZone, OnDestroy, OnInit } from '@angular/core';
import { mempoolFeeColors } from 'src/app/app.constants';
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service';
import { ApiService } from 'src/app/services/api.service';
import { combineLatest, Observable, tap } from 'rxjs';
import { Observable, tap, zip } from 'rxjs';
import { AssetsService } from 'src/app/services/assets.service';
import { EChartsOption, registerMap } from 'echarts';
import { download } from 'src/app/shared/graphs.utils';
import { lerpColor } 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';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { getFlagEmoji } from 'src/app/shared/common.utils';
@Component({
selector: 'app-nodes-map',
@@ -16,7 +17,11 @@ import { StateService } from 'src/app/services/state.service';
styleUrls: ['./nodes-map.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodesMap implements OnInit, OnDestroy {
export class NodesMap implements OnInit {
@Input() widget: boolean = false;
@Input() nodes: any[] | undefined = undefined;
@Input() type: 'none' | 'isp' | 'country' = 'none';
observable$: Observable<any>;
chartInstance = undefined;
@@ -26,44 +31,88 @@ export class NodesMap implements OnInit, OnDestroy {
};
constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService,
private apiService: ApiService,
private stateService: StateService,
private assetsService: AssetsService,
private router: Router,
private zone: NgZone,
private amountShortenerPipe: AmountShortenerPipe
) {
}
ngOnDestroy(): void {}
ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes world map`);
this.observable$ = combineLatest([
this.observable$ = zip(
this.assetsService.getWorldMapJson$,
this.apiService.getNodesPerCountry()
]).pipe(tap((data) => {
this.nodes ? [this.nodes] : this.apiService.getWorldNodes$()
).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);
let maxLiquidity = data[1].maxLiquidity;
let inputNodes: any[] = data[1].nodes;
let mapCenter: number[] = [0, 5];
if (this.type === 'country') {
mapCenter = [0, 0];
} else if (this.type === 'isp') {
mapCenter = [0, 10];
}
this.prepareChartOptions(countries, max);
let mapZoom = 1.3;
if (!inputNodes) {
inputNodes = [];
for (const node of data[1]) {
if (this.type === 'country') {
mapCenter[0] += node.longitude;
mapCenter[1] += node.latitude;
}
inputNodes.push([
node.longitude,
node.latitude,
node.public_key,
node.alias,
node.capacity,
node.channels,
node.country,
node.iso_code,
]);
maxLiquidity = Math.max(maxLiquidity ?? 0, node.capacity);
}
if (this.type === 'country') {
mapCenter[0] /= data[1].length;
mapCenter[1] /= data[1].length;
mapZoom = 6;
}
}
const nodes: any[] = [];
for (const node of inputNodes) {
// We add a bit of noise so nodes at the same location are not all
// on top of each other
const random = Math.random() * 2 * Math.PI;
const random2 = Math.random() * 0.01;
nodes.push([
node[0] + random2 * Math.cos(random),
node[1] + random2 * Math.sin(random),
node[4], // Liquidity
node[3], // Alias
node[2], // Public key
node[5], // Channels
node[6].en, // Country
node[7], // ISO Code
]);
}
maxLiquidity = Math.max(1, maxLiquidity);
this.prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom);
}));
}
prepareChartOptions(countries, max) {
prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) {
let title: object;
if (countries.length === 0) {
if (nodes.length === 0) {
title = {
textStyle: {
color: 'grey',
@@ -76,53 +125,82 @@ export class NodesMap implements OnInit, OnDestroy {
}
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',
silent: false,
title: title ?? undefined,
tooltip: {},
geo: {
animation: false,
silent: true,
center: mapCenter,
zoom: mapZoom,
tooltip: {
show: false
},
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>`;
}
map: 'world',
roam: true,
itemStyle: {
borderColor: 'black',
color: '#272b3f'
},
scaleLimit: {
min: 1.3,
max: 100000,
},
emphasis: {
disabled: true,
}
},
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,
series: [
{
large: false,
type: 'scatter',
data: nodes,
coordinateSystem: 'geo',
geoIndex: 0,
progressive: 500,
symbolSize: function (params) {
return 10 * Math.pow(params[2] / maxLiquidity, 0.2) + 3;
},
tooltip: {
position: function(point, params, dom, rect, size) {
return point;
},
trigger: 'item',
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',
borderRadius: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
textStyle: {
color: '#b1b1b1',
align: 'left',
},
borderColor: '#000',
formatter: (value) => {
const data = value.data;
const alias = data[3].length > 0 ? data[3] : data[4].slice(0, 20);
const liquidity = data[2] >= 100000000 ?
`${this.amountShortenerPipe.transform(data[2] / 100000000)} BTC` :
`${this.amountShortenerPipe.transform(data[2], 2)} sats`;
return `
<b style="color: white">${alias}</b><br>
${liquidity}<br>
${data[5]} channels<br>
${getFlagEmoji(data[7])} ${data[6]}
`;
}
},
itemStyle: {
areaColor: '#FDD835',
}
color: function (params) {
return `${lerpColor('#1E88E5', '#D81B60', Math.pow(params.data[2] / maxLiquidity, 0.2))}`;
},
opacity: 1,
borderColor: 'black',
borderWidth: 0,
},
zlevel: 2,
},
data: countries,
itemStyle: {
areaColor: '#5A6A6D'
},
}
]
};
}
@@ -134,30 +212,16 @@ export class NodesMap implements OnInit, OnDestroy {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
if (e.data && e.data.value > 0) {
if (e.data) {
this.zone.run(() => {
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/country/${e.data.iso}`);
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data[4]}`);
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);
this.chartInstance.on('georoam', (e) => {
this.chartInstance.resize();
});
}
}

View File

@@ -46,7 +46,7 @@
<td class="text-right capacity">
<app-amount *ngIf="country.capacity > 100000000; else smallchannel" [satoshis]="country.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallchannel>
{{ country.capacity | amountShortener: 1 }}
{{ country.capacity ?? 0 | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span>
</ng-template>
</td>

View File

@@ -45,7 +45,7 @@ export class NodesPerCountryChartComponent implements OnInit {
ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes per country`);
this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry()
this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry$()
.pipe(
map(data => {
for (let i = 0; i < data.length; ++i) {

View File

@@ -1,21 +1,71 @@
<div class="container-xl full-height" style="min-height: 335px">
<h1 class="float-left" i18n="lightning.nodes-in-country">
<h1 i18n="lightning.nodes-in-country">
<span>Lightning nodes in {{ country?.name }}</span>
<span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
</h1>
<div class="box">
<div class="row" *ngIf="nodes$ | async as countryNodes">
<div class="col-12 col-md-6">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="lightning.node-count">Nodes</td>
<td>{{ countryNodes.nodes.length }}</td>
</tr>
<tr>
<td i18n="lightning.liquidity">Liquidity</td>
<td>
<app-amount *ngIf="countryNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="countryNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
<ng-template #smallnode>
{{ countryNodes.sumLiquidity | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span>
</ng-template>
<span class="d-none d-md-inline-block">&nbsp;</span>
<span class="d-block d-md-none"></span>
<app-fiat [value]="countryNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
<tr>
<td i18n="lightning.channels">Channels</td>
<td>{{ countryNodes.sumChannels }}</td>
</tr>
<tr>
<td i18n="lightning.isp-count">ISP Count</td>
<td>{{ countryNodes.ispCount }}</td>
</tr>
<tr>
<td i18n="lightning.top-isp">Top ISP</td>
<td class="text-truncate">
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, countryNodes.topIsp.id]">
{{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}]
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
<div style="background-color: #181b2d">
<app-nodes-map [widget]="true" [nodes]="countryNodes.nodes" type="country"></app-nodes-map>
</div>
</div>
</div>
</div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
<th class="channels text-right" i18n="lightning.channels">Channels</th>
<th class="city text-right" i18n="lightning.city">City</th>
<th class="city text-right" i18n="lightning.location">Location</th>
</thead>
<tbody *ngIf="nodes$ | async as nodes">
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
<tbody *ngIf="nodes$ | async as countryNodes; else skeleton">
<tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
<td class="alias text-left text-truncate">
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
</td>
@@ -39,6 +89,32 @@
<app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
</td>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="alias text-left text-truncate">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-first text-left">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-update text-left">
<span class="skeleton-loader"></span>
</td>
<td class="capacity text-right">
<span class="skeleton-loader"></span>
</td>
<td class="channels text-right">
<span class="skeleton-loader"></span>
</td>
<td class="city text-right text-truncate">
<span class="skeleton-loader"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>
</div>

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { map, Observable, share } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { getFlagEmoji } from 'src/app/shared/common.utils';
@@ -16,16 +16,24 @@ export class NodesPerCountry implements OnInit {
nodes$: Observable<any>;
country: {name: string, flag: string};
skeletonLines: number[] = [];
constructor(
private apiService: ApiService,
private seoService: SeoService,
private route: ActivatedRoute,
) { }
) {
for (let i = 0; i < 20; ++i) {
this.skeletonLines.push(i);
}
}
ngOnInit(): void {
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
.pipe(
map(response => {
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
this.country = {
name: response.country.en,
flag: getFlagEmoji(this.route.snapshot.params.country)
@@ -39,14 +47,50 @@ export class NodesPerCountry implements OnInit {
iso: response.nodes[i].iso_code,
};
}
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
return response.nodes;
})
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
const isps = {};
const topIsp = {
count: 0,
id: '',
name: '',
};
for (const node of response.nodes) {
if (!node.isp) {
continue;
}
if (!isps[node.isp]) {
isps[node.isp] = {
count: 0,
asns: [],
};
}
if (isps[node.isp].asns.indexOf(node.as_number) === -1) {
isps[node.isp].asns.push(node.as_number);
}
isps[node.isp].count++;
if (isps[node.isp].count > topIsp.count) {
topIsp.count = isps[node.isp].count;
topIsp.id = isps[node.isp].asns.join(',');
topIsp.name = node.isp;
}
}
return {
nodes: response.nodes,
sumLiquidity: sumLiquidity,
sumChannels: sumChannels,
topIsp: topIsp,
ispCount: Object.keys(isps).length
};
}),
share()
);
}
trackByPublicKey(index: number, node: any) {
trackByPublicKey(index: number, node: any): string {
return node.public_key;
}
}

View File

@@ -47,7 +47,9 @@ export class NodesPerISPChartComponent implements OnInit {
}
ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes per ISP`);
if (!this.widget) {
this.seoService.setTitle($localize`Lightning nodes per ISP`);
}
this.nodesPerAsObservable$ = combineLatest([
this.sortBySubject.pipe(startWith(true)),
@@ -105,7 +107,7 @@ export class NodesPerISPChartComponent implements OnInit {
}
generateChartSerieData(ispRanking): PieSeriesOption[] {
let shareThreshold = 0.5;
let shareThreshold = 0.4;
if (this.widget && isMobile() || isMobile()) {
shareThreshold = 1;
} else if (this.widget) {
@@ -132,9 +134,6 @@ export class NodesPerISPChartComponent implements OnInit {
return;
}
data.push({
itemStyle: {
color: isp[0] === null ? '#7D4698' : undefined,
},
value: this.sortBy === 'capacity' ? isp[7] : isp[6],
name: isp[1].replace('&', '') + (isMobile() || this.widget ? `` : ` (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)`),
label: {
@@ -204,7 +203,7 @@ export class NodesPerISPChartComponent implements OnInit {
}
this.chartOptions = {
color: chartColors.slice(3),
color: chartColors.filter((color) => color != '#5E35B1'), // Remove color that looks like Tor
tooltip: {
trigger: 'item',
textStyle: {

View File

@@ -1,18 +1,68 @@
<div class="container-xl full-height" style="min-height: 335px">
<h1 class="float-left" i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]</h1>
<h1 i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }}</h1>
<div class="box">
<div class="row" *ngIf="nodes$ | async as ispNodes">
<div class="col-12 col-md-6">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="lightning.asn">ASN</td>
<td>{{ isp?.id }}</td>
</tr>
<tr>
<td i18n="lightning.node-count">Nodes</td>
<td>{{ ispNodes.nodes.length }}</td>
</tr>
<tr>
<td i18n="lightning.liquidity">Liquidity</td>
<td>
<app-amount *ngIf="ispNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="ispNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
<ng-template #smallnode>
{{ ispNodes.sumLiquidity | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span>
</ng-template>
<span class="d-none d-md-inline-block">&nbsp;</span>
<span class="d-block d-md-none"></span>
<app-fiat [value]="ispNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
<tr>
<td i18n="lightning.channels">Channels</td>
<td>{{ ispNodes.sumChannels }}</td>
</tr>
<tr>
<td i18n="lightning.top-country">Top country</td>
<td class="text-truncate">
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/country' | relativeUrl, ispNodes.topCountry.iso]">
<span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
<div style="background-color: #181b2d">
<app-nodes-map [widget]="true" [nodes]="ispNodes.nodes" type="isp"></app-nodes-map>
</div>
</div>
</div>
</div>
<div style="min-height: 295px">
<table class="table table-borderless">
<thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
<th class="channels text-right" i18n="lightning.channels">Channels</th>
<th class="city text-right" i18n="lightning.city">City</th>
<th class="city text-right" i18n="lightning.location">Location</th>
</thead>
<tbody *ngIf="nodes$ | async as nodes">
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
<tbody *ngIf="nodes$ | async as ispNodes; else skeleton">
<tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
<td class="alias text-left text-truncate">
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
</td>
@@ -36,6 +86,32 @@
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="alias text-left text-truncate">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-first text-left">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-update text-left">
<span class="skeleton-loader"></span>
</td>
<td class="capacity text-right">
<span class="skeleton-loader"></span>
</td>
<td class="channels text-right">
<span class="skeleton-loader"></span>
</td>
<td class="city text-right text-truncate">
<span class="skeleton-loader"></span>
</td>
</tr>
</tbody>
</ng-template>
</table>
</div>

View File

@@ -59,4 +59,4 @@
@media (max-width: 576px) {
display: none
}
}
}

View File

@@ -1,8 +1,9 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { map, Observable, share } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { getFlagEmoji } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
@@ -15,11 +16,17 @@ export class NodesPerISP implements OnInit {
nodes$: Observable<any>;
isp: {name: string, id: number};
skeletonLines: number[] = [];
constructor(
private apiService: ApiService,
private seoService: SeoService,
private route: ActivatedRoute,
) { }
) {
for (let i = 0; i < 20; ++i) {
this.skeletonLines.push(i);
}
}
ngOnInit(): void {
this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
@@ -27,7 +34,7 @@ export class NodesPerISP implements OnInit {
map(response => {
this.isp = {
name: response.isp,
id: this.route.snapshot.params.isp
id: this.route.snapshot.params.isp.split(',').join(', ')
};
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
@@ -40,12 +47,40 @@ export class NodesPerISP implements OnInit {
};
}
return response.nodes;
})
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
const countries = {};
const topCountry = {
count: 0,
country: '',
iso: '',
flag: '',
};
for (const node of response.nodes) {
if (!node.geolocation.iso) {
continue;
}
countries[node.geolocation.iso] = countries[node.geolocation.iso] ?? 0 + 1;
if (countries[node.geolocation.iso] > topCountry.count) {
topCountry.count = countries[node.geolocation.iso];
topCountry.country = node.geolocation.country;
topCountry.iso = node.geolocation.iso;
}
}
topCountry.flag = getFlagEmoji(topCountry.iso);
return {
nodes: response.nodes,
sumLiquidity: sumLiquidity,
sumChannels: sumChannels,
topCountry: topCountry,
};
}),
share()
);
}
trackByPublicKey(index: number, node: any) {
trackByPublicKey(index: number, node: any): string {
return node.public_key;
}
}

View File

@@ -9,7 +9,7 @@
<th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th>
<th *ngIf="!widget" class="capacity text-right" i18n="node.capacity">Capacity</th>
<th *ngIf="!widget" class="capacity text-right" i18n="node.liquidity">Liquidity</th>
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
@@ -35,7 +35,7 @@
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
</td>
<td *ngIf="!widget" class="location text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td>
</tr>
</tbody>

View File

@@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
import { SeoService } from 'src/app/services/seo.service';
import { IOldestNodes } from '../../../interfaces/node-api.interface';
import { LightningApiService } from '../../lightning-api.service';
@@ -15,19 +17,38 @@ export class OldestNodes implements OnInit {
oldestNodes$: Observable<IOldestNodes[]>;
skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {}
constructor(
private apiService: LightningApiService,
private seoService: SeoService
) {}
ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`Oldest lightning nodes`);
}
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
this.skeletonRows.push(i);
}
if (this.widget === false) {
this.oldestNodes$ = this.apiService.getOldestNodes$();
this.oldestNodes$ = this.apiService.getOldestNodes$().pipe(
map((ranking) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
city: ranking[i].city?.en,
subdivision: ranking[i].subdivision?.en,
iso: ranking[i].iso_code,
};
}
return ranking;
})
);
} else {
this.oldestNodes$ = this.apiService.getOldestNodes$().pipe(
map((nodes: IOldestNodes[]) => {
return nodes.slice(0, 10);
return nodes.slice(0, 7);
})
);
}

View File

@@ -8,7 +8,7 @@
<thead>
<th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="capacity text-right" i18n="node.capacity">Capacity</th>
<th class="capacity text-right" i18n="node.liquidity">Liquidity</th>
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
@@ -35,7 +35,7 @@
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
</td>
<td *ngIf="!widget" class="location text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td>
</tr>
</tbody>

View File

@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { isMobile } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
import { LightningApiService } from '../../lightning-api.service';
@Component({
@@ -17,15 +19,34 @@ export class TopNodesPerCapacity implements OnInit {
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {}
constructor(
private apiService: LightningApiService,
private seoService: SeoService
) {}
ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`Liquidity Ranking`);
}
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
this.skeletonRows.push(i);
}
if (this.widget === false) {
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$();
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe(
map((ranking) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
city: ranking[i].city?.en,
subdivision: ranking[i].subdivision?.en,
iso: ranking[i].iso_code,
};
}
return ranking;
})
);
} else {
this.topNodesPerCapacity$ = this.nodes$.pipe(
map((ranking) => {

View File

@@ -9,7 +9,7 @@
<th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="channels text-right" i18n="node.channels">Channels</th>
<th *ngIf="!widget" class="capacity text-right" i18n="lightning.capacity">Capacity</th>
<th *ngIf="!widget" class="capacity text-right" i18n="lightning.liquidity">Liquidity</th>
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
@@ -35,9 +35,9 @@
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
</td>
<td *ngIf="!widget" class="location text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td>
</tr>
</tr>
</tbody>
<ng-template #skeleton>
<tbody>

View File

@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { isMobile } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
import { LightningApiService } from '../../lightning-api.service';
@Component({
@@ -17,15 +19,34 @@ export class TopNodesPerChannels implements OnInit {
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {}
constructor(
private apiService: LightningApiService,
private seoService: SeoService
) {}
ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`Connectivity Ranking`);
}
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
this.skeletonRows.push(i);
}
if (this.widget === false) {
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$();
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
map((ranking) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
city: ranking[i].city?.en,
subdivision: ranking[i].subdivision?.en,
iso: ranking[i].iso_code,
};
}
return ranking;
})
);
} else {
this.topNodesPerChannels$ = this.nodes$.pipe(
map((ranking) => {