2022-07-17 22:57:29 +02:00
|
|
|
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
|
|
|
|
import { Router } from '@angular/router';
|
2022-07-16 10:44:05 +02:00
|
|
|
import { EChartsOption, PieSeriesOption } from 'echarts';
|
2022-07-27 13:20:54 +02:00
|
|
|
import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs';
|
2022-07-16 10:44:05 +02:00
|
|
|
import { chartColors } from 'src/app/app.constants';
|
|
|
|
import { ApiService } from 'src/app/services/api.service';
|
|
|
|
import { SeoService } from 'src/app/services/seo.service';
|
2022-07-17 22:57:29 +02:00
|
|
|
import { StateService } from 'src/app/services/state.service';
|
2022-07-16 10:44:05 +02:00
|
|
|
import { download } from 'src/app/shared/graphs.utils';
|
2022-07-16 11:32:48 +02:00
|
|
|
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
2022-07-17 22:57:29 +02:00
|
|
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
2022-07-16 10:44:05 +02:00
|
|
|
|
|
|
|
@Component({
|
2022-07-17 11:46:36 +02:00
|
|
|
selector: 'app-nodes-per-isp-chart',
|
|
|
|
templateUrl: './nodes-per-isp-chart.component.html',
|
|
|
|
styleUrls: ['./nodes-per-isp-chart.component.scss'],
|
2022-07-16 10:44:05 +02:00
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
|
})
|
2022-07-17 11:46:36 +02:00
|
|
|
export class NodesPerISPChartComponent implements OnInit {
|
2022-07-16 10:44:05 +02:00
|
|
|
isLoading = true;
|
|
|
|
chartOptions: EChartsOption = {};
|
|
|
|
chartInitOptions = {
|
|
|
|
renderer: 'svg',
|
|
|
|
};
|
|
|
|
timespan = '';
|
2022-07-27 13:20:54 +02:00
|
|
|
chartInstance = undefined;
|
2022-07-16 10:44:05 +02:00
|
|
|
|
|
|
|
@HostBinding('attr.dir') dir = 'ltr';
|
|
|
|
|
|
|
|
nodesPerAsObservable$: Observable<any>;
|
2022-07-27 13:20:54 +02:00
|
|
|
groupBySubject = new Subject<boolean>();
|
|
|
|
showTorSubject = new Subject<boolean>();
|
2022-07-16 10:44:05 +02:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
private apiService: ApiService,
|
|
|
|
private seoService: SeoService,
|
2022-07-17 22:57:29 +02:00
|
|
|
private amountShortenerPipe: AmountShortenerPipe,
|
|
|
|
private router: Router,
|
|
|
|
private zone: NgZone,
|
|
|
|
private stateService: StateService,
|
2022-07-16 10:44:05 +02:00
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
2022-07-16 23:12:16 +02:00
|
|
|
this.seoService.setTitle($localize`Lightning nodes per ISP`);
|
2022-07-16 10:44:05 +02:00
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
|
2022-07-16 10:44:05 +02:00
|
|
|
.pipe(
|
2022-07-27 13:20:54 +02:00
|
|
|
switchMap((selectedFilters) => {
|
|
|
|
return this.apiService.getNodesPerAs(
|
|
|
|
selectedFilters[0] ? 'capacity' : 'node-count',
|
|
|
|
selectedFilters[1] // Show Tor nodes
|
|
|
|
)
|
|
|
|
.pipe(
|
|
|
|
tap(data => {
|
|
|
|
this.isLoading = false;
|
|
|
|
this.prepareChartOptions(data);
|
|
|
|
}),
|
|
|
|
map(data => {
|
|
|
|
for (let i = 0; i < data.length; ++i) {
|
|
|
|
data[i].rank = i + 1;
|
|
|
|
}
|
|
|
|
return data.slice(0, 100);
|
|
|
|
})
|
|
|
|
);
|
2022-07-16 10:44:05 +02:00
|
|
|
}),
|
|
|
|
share()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
generateChartSerieData(as): PieSeriesOption[] {
|
2022-07-23 23:33:13 +02:00
|
|
|
const shareThreshold = this.isMobile() ? 2 : 0.5;
|
2022-07-16 10:44:05 +02:00
|
|
|
const data: object[] = [];
|
|
|
|
let totalShareOther = 0;
|
|
|
|
let totalNodeOther = 0;
|
|
|
|
|
|
|
|
let edgeDistance: string | number = '10%';
|
|
|
|
if (this.isMobile()) {
|
|
|
|
edgeDistance = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
as.forEach((as) => {
|
|
|
|
if (as.share < shareThreshold) {
|
|
|
|
totalShareOther += as.share;
|
|
|
|
totalNodeOther += as.count;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
data.push({
|
2022-07-27 13:20:54 +02:00
|
|
|
itemStyle: {
|
|
|
|
color: as.ispId === null ? '#7D4698' : undefined,
|
|
|
|
},
|
2022-07-16 10:44:05 +02:00
|
|
|
value: as.share,
|
|
|
|
name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
|
|
|
|
label: {
|
|
|
|
overflow: 'truncate',
|
|
|
|
color: '#b1b1b1',
|
|
|
|
alignTo: 'edge',
|
|
|
|
edgeDistance: edgeDistance,
|
|
|
|
},
|
|
|
|
tooltip: {
|
|
|
|
show: !this.isMobile(),
|
|
|
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
|
|
|
borderRadius: 4,
|
|
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
|
|
textStyle: {
|
|
|
|
color: '#b1b1b1',
|
|
|
|
},
|
|
|
|
borderColor: '#000',
|
|
|
|
formatter: () => {
|
|
|
|
return `<b style="color: white">${as.name} (${as.share}%)</b><br>` +
|
2022-07-16 11:32:48 +02:00
|
|
|
$localize`${as.count.toString()} nodes<br>` +
|
|
|
|
$localize`${this.amountShortenerPipe.transform(as.capacity / 100000000, 2)} BTC capacity`
|
|
|
|
;
|
2022-07-16 10:44:05 +02:00
|
|
|
}
|
|
|
|
},
|
2022-07-17 22:57:29 +02:00
|
|
|
data: as.ispId,
|
2022-07-16 10:44:05 +02:00
|
|
|
} as PieSeriesOption);
|
|
|
|
});
|
|
|
|
|
|
|
|
// 'Other'
|
|
|
|
data.push({
|
|
|
|
itemStyle: {
|
|
|
|
color: 'grey',
|
|
|
|
},
|
|
|
|
value: totalShareOther,
|
|
|
|
name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
|
|
|
|
label: {
|
|
|
|
overflow: 'truncate',
|
|
|
|
color: '#b1b1b1',
|
|
|
|
alignTo: 'edge',
|
|
|
|
edgeDistance: edgeDistance
|
|
|
|
},
|
|
|
|
tooltip: {
|
|
|
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
|
|
|
borderRadius: 4,
|
|
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
|
|
textStyle: {
|
|
|
|
color: '#b1b1b1',
|
|
|
|
},
|
|
|
|
borderColor: '#000',
|
|
|
|
formatter: () => {
|
|
|
|
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
|
|
|
|
totalNodeOther.toString() + ` nodes`;
|
|
|
|
}
|
|
|
|
},
|
2022-07-17 22:57:29 +02:00
|
|
|
data: 9999 as any,
|
2022-07-16 10:44:05 +02:00
|
|
|
} as PieSeriesOption);
|
|
|
|
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
prepareChartOptions(as): void {
|
2022-07-16 10:44:05 +02:00
|
|
|
let pieSize = ['20%', '80%']; // Desktop
|
|
|
|
if (this.isMobile()) {
|
|
|
|
pieSize = ['15%', '60%'];
|
|
|
|
}
|
|
|
|
|
|
|
|
this.chartOptions = {
|
2022-07-27 13:20:54 +02:00
|
|
|
color: chartColors.slice(3),
|
2022-07-16 10:44:05 +02:00
|
|
|
tooltip: {
|
|
|
|
trigger: 'item',
|
|
|
|
textStyle: {
|
|
|
|
align: 'left',
|
|
|
|
}
|
|
|
|
},
|
|
|
|
series: [
|
|
|
|
{
|
|
|
|
zlevel: 0,
|
2022-07-23 23:33:13 +02:00
|
|
|
minShowLabelAngle: 1.8,
|
2022-07-17 22:57:29 +02:00
|
|
|
name: 'Lightning nodes',
|
2022-07-16 10:44:05 +02:00
|
|
|
type: 'pie',
|
|
|
|
radius: pieSize,
|
|
|
|
data: this.generateChartSerieData(as),
|
|
|
|
labelLine: {
|
|
|
|
lineStyle: {
|
|
|
|
width: 2,
|
|
|
|
},
|
|
|
|
length: this.isMobile() ? 1 : 20,
|
|
|
|
length2: this.isMobile() ? 1 : undefined,
|
|
|
|
},
|
|
|
|
label: {
|
|
|
|
fontSize: 14,
|
|
|
|
},
|
|
|
|
itemStyle: {
|
|
|
|
borderRadius: 1,
|
|
|
|
borderWidth: 1,
|
|
|
|
borderColor: '#000',
|
|
|
|
},
|
|
|
|
emphasis: {
|
|
|
|
itemStyle: {
|
|
|
|
shadowBlur: 40,
|
|
|
|
shadowColor: 'rgba(0, 0, 0, 0.75)',
|
|
|
|
},
|
|
|
|
labelLine: {
|
|
|
|
lineStyle: {
|
|
|
|
width: 4,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
isMobile(): boolean {
|
2022-07-16 10:44:05 +02:00
|
|
|
return (window.innerWidth <= 767.98);
|
|
|
|
}
|
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
onChartInit(ec): void {
|
2022-07-16 10:44:05 +02:00
|
|
|
if (this.chartInstance !== undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.chartInstance = ec;
|
2022-07-17 22:57:29 +02:00
|
|
|
|
|
|
|
this.chartInstance.on('click', (e) => {
|
2022-07-27 13:20:54 +02:00
|
|
|
if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor
|
2022-07-17 22:57:29 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.zone.run(() => {
|
|
|
|
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/isp/${e.data.data}`);
|
|
|
|
this.router.navigate([url]);
|
|
|
|
});
|
|
|
|
});
|
2022-07-16 10:44:05 +02:00
|
|
|
}
|
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
onSaveChart(): void {
|
2022-07-16 10:44:05 +02:00
|
|
|
const now = new Date();
|
|
|
|
this.chartOptions.backgroundColor = '#11131f';
|
|
|
|
this.chartInstance.setOption(this.chartOptions);
|
|
|
|
download(this.chartInstance.getDataURL({
|
|
|
|
pixelRatio: 2,
|
|
|
|
excludeComponents: ['dataZoom'],
|
|
|
|
}), `ln-nodes-per-as-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
|
|
|
this.chartOptions.backgroundColor = 'none';
|
|
|
|
this.chartInstance.setOption(this.chartOptions);
|
|
|
|
}
|
|
|
|
|
2022-07-27 13:20:54 +02:00
|
|
|
onTorToggleStatusChanged(e): void {
|
|
|
|
this.showTorSubject.next(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
onGroupToggleStatusChanged(e): void {
|
|
|
|
this.groupBySubject.next(e);
|
2022-07-16 10:44:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|