Merge branch 'master' into nofollow-api-links
This commit is contained in:
@@ -76,10 +76,8 @@
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'bottom-padding' : 'pb-0'" class="container pb-lg-0">
|
||||
<div>
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="box">
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
|
||||
|
||||
<div class="box">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
export class ChannelComponent implements OnInit {
|
||||
channel$: Observable<any>;
|
||||
error: any = null;
|
||||
channelGeo: number[] = [];
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
@@ -29,9 +30,23 @@ export class ChannelComponent implements OnInit {
|
||||
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||
.pipe(
|
||||
tap((data) => {
|
||||
if (!data.node_left.longitude || !data.node_left.latitude ||
|
||||
!data.node_right.longitude || !data.node_right.latitude) {
|
||||
this.channelGeo = [];
|
||||
} else {
|
||||
this.channelGeo = [
|
||||
data.node_left.public_key,
|
||||
data.node_left.alias,
|
||||
data.node_left.longitude, data.node_left.latitude,
|
||||
data.node_right.public_key,
|
||||
data.node_right.alias,
|
||||
data.node_right.longitude, data.node_right.latitude,
|
||||
];
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
console.log(this.error);
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
|
||||
<!-- Network capacity/channels/nodes -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span i18n="lightning.statistics-title">Network Statistics</span>
|
||||
@@ -17,6 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Channels stats -->
|
||||
<div class="col">
|
||||
<div class="main-title">
|
||||
<span i18n="lightning.statistics-title">Channels Statistics</span>
|
||||
@@ -30,18 +32,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<!-- ISP pie chart -->
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<app-nodes-per-isp-chart [widget]="true"></app-nodes-per-isp-chart>
|
||||
<div class="mt-1"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<app-nodes-networks-chart [widget]=true></app-nodes-networks-chart>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<app-lightning-statistics-chart [widget]=true></app-lightning-statistics-chart>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/lightning/capacity' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
@@ -52,7 +64,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Top Capacity Nodes</h5>
|
||||
<app-nodes-list [nodes$]="nodesByCapacity$"></app-nodes-list>
|
||||
<app-nodes-list [nodes$]="nodesByCapacity$" [show]="'mobile-capacity'"></app-nodes-list>
|
||||
<!-- <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> -->
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,7 +74,7 @@
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Most Connected Nodes</h5>
|
||||
<app-nodes-list [nodes$]="nodesByChannels$"></app-nodes-list>
|
||||
<app-nodes-list [nodes$]="nodesByChannels$" [show]="'mobile-channels'"></app-nodes-list>
|
||||
<!-- <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,13 @@
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.graph-card {
|
||||
height: 100%;
|
||||
@media (min-width: 992px) {
|
||||
height: 385px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
@@ -22,9 +29,6 @@
|
||||
color: #4a68b9;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem 1rem 0.75rem 1rem;
|
||||
}
|
||||
.card-body.pool-ranking {
|
||||
padding: 1.25rem 0.25rem 0.75rem 0.25rem;
|
||||
}
|
||||
@@ -32,6 +36,21 @@
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
#blockchain-container {
|
||||
position: relative;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
#blockchain-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fade-border {
|
||||
-webkit-mask-image: linear-gradient(to right, transparent 0%, black 10%, black 80%, transparent 100%)
|
||||
}
|
||||
|
||||
.main-title {
|
||||
position: relative;
|
||||
@@ -45,7 +64,7 @@
|
||||
}
|
||||
|
||||
.more-padding {
|
||||
padding: 18px;
|
||||
padding: 24px 20px !important;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
@@ -78,3 +97,10 @@
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -30,21 +30,28 @@
|
||||
}
|
||||
|
||||
.widget {
|
||||
width: 99vw;
|
||||
width: 90vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 250px;
|
||||
-webkit-mask: linear-gradient(0deg, #11131f00 5%, #11131fff 25%);
|
||||
@media (max-width: 767.98px) {
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
.widget > .chart {
|
||||
-webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%);
|
||||
min-height: 250px;
|
||||
-webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%);
|
||||
@media (max-width: 767.98px) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { EChartsOption, registerMap } from 'echarts';
|
||||
import 'echarts-gl';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-channels-map',
|
||||
@@ -16,11 +17,18 @@ import 'echarts-gl';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
@Input() style: 'graph' | 'nodepage' | 'widget' = 'graph';
|
||||
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
||||
@Input() publicKey: string | undefined;
|
||||
@Input() channel: any[] = [];
|
||||
|
||||
observable$: Observable<any>;
|
||||
center: number[] | undefined = undefined;
|
||||
|
||||
center: number[] | undefined;
|
||||
zoom: number | undefined;
|
||||
channelWidth = 0.6;
|
||||
channelOpacity = 0.1;
|
||||
channelColor = '#466d9d';
|
||||
channelCurve = 0;
|
||||
|
||||
chartInstance = undefined;
|
||||
chartOptions: EChartsOption = {};
|
||||
@@ -42,8 +50,16 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.center = this.style === 'widget' ? [0, 0, -10] : undefined;
|
||||
|
||||
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
|
||||
this.zoom = 1.3;
|
||||
if (this.style === 'widget' && !isMobile()) {
|
||||
this.zoom = 3.5;
|
||||
}
|
||||
if (this.style === 'widget' && isMobile()) {
|
||||
this.zoom = 1.4;
|
||||
this.center = [0, 10];
|
||||
}
|
||||
|
||||
if (this.style === 'graph') {
|
||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||
}
|
||||
@@ -62,36 +78,85 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
const nodes = [];
|
||||
const nodesPubkeys = {};
|
||||
let thisNodeGPS: number[] | undefined = undefined;
|
||||
for (const channel of data[1]) {
|
||||
|
||||
let geoloc = data[1];
|
||||
if (this.style === 'channelpage') {
|
||||
if (this.channel.length === 0) {
|
||||
geoloc = [];
|
||||
} else {
|
||||
geoloc = [this.channel];
|
||||
}
|
||||
}
|
||||
for (const channel of geoloc) {
|
||||
if (!thisNodeGPS && data[2] === channel[0]) {
|
||||
thisNodeGPS = [channel[2], channel[3]];
|
||||
} else if (!thisNodeGPS && data[2] === channel[4]) {
|
||||
thisNodeGPS = [channel[6], channel[7]];
|
||||
}
|
||||
|
||||
channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]);
|
||||
// 0 - node1 pubkey
|
||||
// 1 - node1 alias
|
||||
// 2,3 - node1 GPS
|
||||
// 4 - node2 pubkey
|
||||
// 5 - node2 alias
|
||||
// 6,7 - node2 GPS
|
||||
|
||||
// We add a bit of noise so nodes at the same location are not all
|
||||
// on top of each other
|
||||
let random = Math.random() * 2 * Math.PI;
|
||||
let random2 = Math.random() * 0.01;
|
||||
|
||||
if (!nodesPubkeys[channel[0]]) {
|
||||
nodes.push({
|
||||
publicKey: channel[0],
|
||||
name: channel[1],
|
||||
value: [channel[2], channel[3]],
|
||||
});
|
||||
nodesPubkeys[channel[0]] = true;
|
||||
nodes.push([
|
||||
channel[2] + random2 * Math.cos(random),
|
||||
channel[3] + random2 * Math.sin(random),
|
||||
1,
|
||||
channel[0],
|
||||
channel[1]
|
||||
]);
|
||||
nodesPubkeys[channel[0]] = nodes[nodes.length - 1];
|
||||
}
|
||||
|
||||
random = Math.random() * 2 * Math.PI;
|
||||
random2 = Math.random() * 0.01;
|
||||
|
||||
if (!nodesPubkeys[channel[4]]) {
|
||||
nodes.push({
|
||||
publicKey: channel[4],
|
||||
name: channel[5],
|
||||
value: [channel[6], channel[7]],
|
||||
});
|
||||
nodesPubkeys[channel[4]] = true;
|
||||
nodes.push([
|
||||
channel[6] + random2 * Math.cos(random),
|
||||
channel[7] + random2 * Math.sin(random),
|
||||
1,
|
||||
channel[4],
|
||||
channel[5]
|
||||
]);
|
||||
nodesPubkeys[channel[4]] = nodes[nodes.length - 1];
|
||||
}
|
||||
|
||||
const channelLoc = [];
|
||||
channelLoc.push(nodesPubkeys[channel[0]].slice(0, 2));
|
||||
channelLoc.push(nodesPubkeys[channel[4]].slice(0, 2));
|
||||
channelsLoc.push(channelLoc);
|
||||
}
|
||||
if (this.style === 'nodepage' && thisNodeGPS) {
|
||||
// 1ML 0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266
|
||||
// New York GPS [-74.0068, 40.7123]
|
||||
// Map center [-20.55, 0, -9.85]
|
||||
this.center = [thisNodeGPS[0] * -20.55 / -74.0068, 0, thisNodeGPS[1] * -9.85 / 40.7123];
|
||||
this.center = [thisNodeGPS[0], thisNodeGPS[1]];
|
||||
this.zoom = 10;
|
||||
this.channelWidth = 1;
|
||||
this.channelOpacity = 1;
|
||||
}
|
||||
if (this.style === 'channelpage' && this.channel.length > 0) {
|
||||
this.channelWidth = 2;
|
||||
this.channelOpacity = 1;
|
||||
this.channelColor = '#bafcff';
|
||||
this.channelCurve = 0.1;
|
||||
this.center = [
|
||||
(this.channel[2] + this.channel[6]) / 2,
|
||||
(this.channel[3] + this.channel[7]) / 2
|
||||
];
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(this.channel[7] - this.channel[3], 2) +
|
||||
Math.pow(this.channel[6] - this.channel[2], 2)
|
||||
);
|
||||
|
||||
this.zoom = -0.05 * distance + 8;
|
||||
}
|
||||
|
||||
this.prepareChartOptions(nodes, channelsLoc);
|
||||
@@ -115,87 +180,83 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
silent: this.style === 'widget' ? true : false,
|
||||
silent: this.style === 'widget',
|
||||
title: title ?? undefined,
|
||||
geo3D: {
|
||||
map: 'world',
|
||||
shading: 'color',
|
||||
tooltip: {},
|
||||
geo: {
|
||||
animation: false,
|
||||
silent: true,
|
||||
postEffect: {
|
||||
enable: true,
|
||||
bloom: {
|
||||
intensity: 0.1,
|
||||
}
|
||||
},
|
||||
viewControl: {
|
||||
center: this.center,
|
||||
minDistance: 1,
|
||||
maxDistance: 60,
|
||||
distance: this.style === 'widget' ? 22 : this.style === 'nodepage' ? 22 : 60,
|
||||
alpha: 90,
|
||||
rotateSensitivity: 0,
|
||||
panSensitivity: this.style === 'widget' ? 0 : 1,
|
||||
zoomSensitivity: this.style === 'widget' ? 0 : 0.5,
|
||||
panMouseButton: this.style === 'widget' ? null : 'left',
|
||||
rotateMouseButton: undefined,
|
||||
center: this.center,
|
||||
zoom: this.zoom,
|
||||
tooltip: {
|
||||
show: true
|
||||
},
|
||||
map: 'world',
|
||||
roam: this.style === 'widget' ? false : true,
|
||||
itemStyle: {
|
||||
color: 'white',
|
||||
opacity: 0.02,
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
color: '#ffffff44'
|
||||
},
|
||||
regionHeight: 0.01,
|
||||
scaleLimit: {
|
||||
min: 1.3,
|
||||
max: 100000,
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
// @ts-ignore
|
||||
type: 'lines3D',
|
||||
coordinateSystem: 'geo3D',
|
||||
blendMode: 'lighter',
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
opacity: ['widget', 'graph'].includes(this.style) ? 0.025 : 1,
|
||||
large: true,
|
||||
type: 'scatter',
|
||||
data: nodes,
|
||||
coordinateSystem: 'geo',
|
||||
geoIndex: 0,
|
||||
symbolSize: 4,
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (value) => {
|
||||
const data = value.data;
|
||||
const alias = data[4].length > 0 ? data[4] : data[3].slice(0, 20);
|
||||
return `<b style="color: white">${alias}</b>`;
|
||||
}
|
||||
},
|
||||
data: channels
|
||||
itemStyle: {
|
||||
color: 'white',
|
||||
borderColor: 'black',
|
||||
borderWidth: 2,
|
||||
opacity: 1,
|
||||
},
|
||||
blendMode: 'lighter',
|
||||
zlevel: 1,
|
||||
},
|
||||
{
|
||||
// @ts-ignore
|
||||
type: 'scatter3D',
|
||||
symbol: 'circle',
|
||||
blendMode: 'lighter',
|
||||
coordinateSystem: 'geo3D',
|
||||
symbolSize: 3,
|
||||
itemStyle: {
|
||||
color: '#BBFFFF',
|
||||
opacity: 1,
|
||||
borderColor: '#FFFFFF00',
|
||||
large: false,
|
||||
progressive: 200,
|
||||
silent: true,
|
||||
type: 'lines',
|
||||
coordinateSystem: 'geo',
|
||||
data: channels,
|
||||
lineStyle: {
|
||||
opacity: this.channelOpacity,
|
||||
width: this.channelWidth,
|
||||
curveness: this.channelCurve,
|
||||
color: this.channelColor,
|
||||
},
|
||||
data: nodes,
|
||||
emphasis: {
|
||||
label: {
|
||||
position: 'top',
|
||||
color: 'white',
|
||||
fontSize: 16,
|
||||
formatter: function(value) {
|
||||
return value.name;
|
||||
},
|
||||
show: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
blendMode: 'lighter',
|
||||
tooltip: {
|
||||
show: false,
|
||||
},
|
||||
zlevel: 2,
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
@HostListener('window:wheel', ['$event'])
|
||||
onWindowScroll(e): void {
|
||||
// Not very smooth when using the mouse
|
||||
if (this.style === 'widget' && e.target.tagName === 'CANVAS') {
|
||||
window.scrollBy({left: 0, top: e.deltaY, behavior: 'auto'});
|
||||
}
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
@@ -211,14 +272,34 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
if (e.data && e.data.publicKey) {
|
||||
if (e.data) {
|
||||
this.zone.run(() => {
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data.publicKey}`);
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data[3]}`);
|
||||
this.router.navigate([url]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.chartInstance.on('georoam', (e) => {
|
||||
if (!e.zoom || this.style === 'nodepage') {
|
||||
return;
|
||||
}
|
||||
|
||||
const speed = 0.005;
|
||||
const chartOptions = {
|
||||
series: this.chartOptions.series
|
||||
};
|
||||
|
||||
chartOptions.series[1].lineStyle.opacity += e.zoom > 1 ? speed : -speed;
|
||||
chartOptions.series[1].lineStyle.width += e.zoom > 1 ? speed : -speed;
|
||||
chartOptions.series[0].symbolSize += e.zoom > 1 ? speed * 10 : -speed * 10;
|
||||
chartOptions.series[1].lineStyle.opacity = Math.max(0.05, Math.min(0.5, chartOptions.series[1].lineStyle.opacity));
|
||||
chartOptions.series[1].lineStyle.width = Math.max(0.5, Math.min(1, chartOptions.series[1].lineStyle.width));
|
||||
chartOptions.series[0].symbolSize = Math.max(4, Math.min(5.5, chartOptions.series[0].symbolSize));
|
||||
|
||||
this.chartInstance.setOption(chartOptions);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="capacity text-right" i18n="node.capacity">Capacity</th>
|
||||
<th class="channels text-right" i18n="node.channels">Channels</th>
|
||||
<th class="capacity text-right" [class]="show" i18n="node.capacity">Capacity</th>
|
||||
<th class="channels text-right" [class]="show" i18n="node.channels">Channels</th>
|
||||
</thead>
|
||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i = index;">
|
||||
<td class="alias text-left">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
<td class="capacity text-right" [class]="show">
|
||||
<app-amount [satoshis]="node.capacity" digitsInfo="1.2-2"></app-amount>
|
||||
</td>
|
||||
<td class="channels text-right">
|
||||
<td class="channels text-right" [class]="show">
|
||||
{{ node.channels | number }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.capacity.mobile-channels {
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.channels.mobile-capacity {
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { Observable } from 'rxjs';
|
||||
})
|
||||
export class NodesListComponent implements OnInit {
|
||||
@Input() nodes$: Observable<any>;
|
||||
@Input() show: string;
|
||||
|
||||
constructor() { }
|
||||
|
||||
|
||||
@@ -1,6 +1,29 @@
|
||||
<div class="full-container h-100">
|
||||
<div [class]="widget === false ? 'full-container' : ''">
|
||||
|
||||
<div class="card-header">
|
||||
<div *ngIf="widget">
|
||||
<div class="pool-distribution" *ngIf="(nodesPerAsObservable$ | async) as stats; else loadingReward">
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tagged-isp">Tagged ISPs</h5>
|
||||
<p class="card-text">
|
||||
{{ stats.taggedISP }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tagged-capacity">Tagged capacity</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="mining.blocks-count-desc">
|
||||
<app-amount [satoshis]="stats.taggedCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tagged-nodes">Tagged nodes</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="mining.pools-count-desc">
|
||||
{{ stats.taggedNodeCount }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-header" *ngIf="!widget">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-per-isp">Lightning nodes per ISP</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||
@@ -12,23 +35,21 @@
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="container pb-lg-0 bottom-padding">
|
||||
<div class="pb-lg-5" *ngIf="nodesPerAsObservable$ | async">
|
||||
<div class="chart w-100" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div [class]="!widget ? 'bottom-padding' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex toggle">
|
||||
<div class="d-flex toggle" *ngIf="!widget">
|
||||
<app-toggle [textLeft]="'Show Tor'" [textRight]="" (toggleStatusChanged)="onTorToggleStatusChanged($event)"></app-toggle>
|
||||
<app-toggle [textLeft]="'Nodes'" [textRight]="'Capacity'" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless text-center m-auto" style="max-width: 900px">
|
||||
<table class="table table-borderless text-center m-auto" style="max-width: 900px" *ngIf="!widget">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="rank text-left pl-0" i18n="mining.rank">Rank</th>
|
||||
@@ -39,7 +60,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
|
||||
<tr *ngFor="let asEntry of asList">
|
||||
<tr *ngFor="let asEntry of asList.data">
|
||||
<td class="rank text-left pl-0">{{ asEntry.rank }}</td>
|
||||
<td class="name text-left text-truncate">
|
||||
<a *ngIf="asEntry.ispId" [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
|
||||
@@ -54,3 +75,26 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ng-template #loadingReward>
|
||||
<div class="pool-distribution">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.tagged-isp">Tagged ISPs</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.tagged-capacity">Tagged capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.tagged-nodes">Tagged nodes</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -22,7 +22,40 @@
|
||||
max-height: 400px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-height: 230px;
|
||||
margin-top: -35px;
|
||||
margin-top: -40px;
|
||||
}
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 240px;
|
||||
@media (max-width: 485px) {
|
||||
max-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 991px) {
|
||||
position: relative;
|
||||
top: -65px;
|
||||
}
|
||||
@media (min-width: 830px) and (max-width: 991px) {
|
||||
position: relative;
|
||||
top: 0px;
|
||||
}
|
||||
@media (min-width: 830px) {
|
||||
flex-direction: row;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
@media (min-width: 830px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +68,79 @@
|
||||
};
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.pools-table th,
|
||||
.pools-table td {
|
||||
padding: .3em !important;
|
||||
}
|
||||
}
|
||||
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
min-height: 56px;
|
||||
display: block;
|
||||
@media (min-width: 485px) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
h5 {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.item {
|
||||
max-width: 160px;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
margin: 0px auto 20px;
|
||||
&:nth-child(2) {
|
||||
order: 2;
|
||||
@media (min-width: 485px) {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
&:nth-child(3) {
|
||||
width: 50%;
|
||||
order: 3;
|
||||
@media (min-width: 485px) {
|
||||
order: 2;
|
||||
display: block;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.card-text {
|
||||
font-size: 18px;
|
||||
span {
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-loader {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-width: 80px;
|
||||
margin: 15px auto 3px;
|
||||
}
|
||||
|
||||
.rank {
|
||||
width: 15%;
|
||||
@media (max-width: 576px) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs';
|
||||
import { combineLatest, map, Observable, share, startWith, Subject, switchMap, tap } from 'rxjs';
|
||||
import { chartColors } from 'src/app/app.constants';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
@@ -17,6 +18,8 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesPerISPChartComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
isLoading = true;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
@@ -46,7 +49,11 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
this.seoService.setTitle($localize`Lightning nodes per ISP`);
|
||||
|
||||
this.showTorObservable$ = this.showTorSubject.asObservable();
|
||||
this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
|
||||
|
||||
this.nodesPerAsObservable$ = combineLatest([
|
||||
this.groupBySubject.pipe(startWith(false)),
|
||||
this.showTorSubject.pipe(startWith(false)),
|
||||
])
|
||||
.pipe(
|
||||
switchMap((selectedFilters) => {
|
||||
return this.apiService.getNodesPerAs(
|
||||
@@ -62,23 +69,41 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
data[i].rank = i + 1;
|
||||
}
|
||||
return data.slice(0, 100);
|
||||
return {
|
||||
taggedISP: data.length,
|
||||
taggedCapacity: data.reduce((partialSum, isp) => partialSum + isp.capacity, 0),
|
||||
taggedNodeCount: data.reduce((partialSum, isp) => partialSum + isp.count, 0),
|
||||
data: data.slice(0, 100),
|
||||
};
|
||||
})
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
if (this.widget) {
|
||||
this.showTorSubject.next(false);
|
||||
this.groupBySubject.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
generateChartSerieData(as): PieSeriesOption[] {
|
||||
const shareThreshold = this.isMobile() ? 2 : 0.5;
|
||||
let shareThreshold = 0.5;
|
||||
if (this.widget && isMobile() || isMobile()) {
|
||||
shareThreshold = 1;
|
||||
} else if (this.widget) {
|
||||
shareThreshold = 0.75;
|
||||
}
|
||||
|
||||
const data: object[] = [];
|
||||
let totalShareOther = 0;
|
||||
let totalNodeOther = 0;
|
||||
|
||||
let edgeDistance: string | number = '10%';
|
||||
if (this.isMobile()) {
|
||||
if (isMobile() && this.widget) {
|
||||
edgeDistance = 0;
|
||||
} else if (isMobile() && !this.widget || this.widget) {
|
||||
edgeDistance = 10;
|
||||
}
|
||||
|
||||
as.forEach((as) => {
|
||||
@@ -92,15 +117,16 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
color: as.ispId === null ? '#7D4698' : undefined,
|
||||
},
|
||||
value: as.share,
|
||||
name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
|
||||
name: as.name + (isMobile() || this.widget ? `` : ` (${as.share}%)`),
|
||||
label: {
|
||||
overflow: 'truncate',
|
||||
width: isMobile() ? 75 : this.widget ? 125 : 250,
|
||||
color: '#b1b1b1',
|
||||
alignTo: 'edge',
|
||||
edgeDistance: edgeDistance,
|
||||
},
|
||||
tooltip: {
|
||||
show: !this.isMobile(),
|
||||
show: !isMobile(),
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
@@ -125,7 +151,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
color: 'grey',
|
||||
},
|
||||
value: totalShareOther,
|
||||
name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
|
||||
name: 'Other' + (isMobile() || this.widget ? `` : ` (${totalShareOther.toFixed(2)}%)`),
|
||||
label: {
|
||||
overflow: 'truncate',
|
||||
color: '#b1b1b1',
|
||||
@@ -153,7 +179,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
|
||||
prepareChartOptions(as): void {
|
||||
let pieSize = ['20%', '80%']; // Desktop
|
||||
if (this.isMobile()) {
|
||||
if (isMobile() && !this.widget) {
|
||||
pieSize = ['15%', '60%'];
|
||||
}
|
||||
|
||||
@@ -177,8 +203,8 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
length: this.isMobile() ? 1 : 20,
|
||||
length2: this.isMobile() ? 1 : undefined,
|
||||
length: isMobile() ? 1 : 20,
|
||||
length2: isMobile() ? 1 : undefined,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
@@ -204,10 +230,6 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
isMobile(): boolean {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onChartInit(ec): void {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
@@ -244,5 +266,9 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
onGroupToggleStatusChanged(e): void {
|
||||
this.groupBySubject.next(e);
|
||||
}
|
||||
|
||||
isEllipsisActive(e) {
|
||||
return (e.offsetWidth < e.scrollWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,7 @@
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 270px;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
|
||||
Reference in New Issue
Block a user