mirror of
https://github.com/mempool/mempool.git
synced 2025-02-23 06:35:15 +01:00
Merge branch 'master' into nymkappa/bugfix/handle-esplora-error
This commit is contained in:
commit
a30bb4e6c0
17 changed files with 361 additions and 84 deletions
|
@ -17,32 +17,60 @@ class ChannelsApi {
|
|||
}
|
||||
}
|
||||
|
||||
public async $getAllChannelsGeo(publicKey?: string): Promise<any[]> {
|
||||
public async $getAllChannelsGeo(publicKey?: string, style?: string): Promise<any[]> {
|
||||
try {
|
||||
let select: string;
|
||||
if (style === 'widget') {
|
||||
select = `
|
||||
nodes_1.latitude AS node1_latitude, nodes_1.longitude AS node1_longitude,
|
||||
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude
|
||||
`;
|
||||
} else {
|
||||
select = `
|
||||
nodes_1.public_key as node1_public_key, nodes_1.alias AS node1_alias,
|
||||
nodes_1.latitude AS node1_latitude, nodes_1.longitude AS node1_longitude,
|
||||
nodes_2.public_key as node2_public_key, nodes_2.alias AS node2_alias,
|
||||
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude
|
||||
`;
|
||||
}
|
||||
|
||||
const params: string[] = [];
|
||||
let query = `SELECT nodes_1.public_key as node1_public_key, nodes_1.alias AS node1_alias,
|
||||
nodes_1.latitude AS node1_latitude, nodes_1.longitude AS node1_longitude,
|
||||
nodes_2.public_key as node2_public_key, nodes_2.alias AS node2_alias,
|
||||
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude,
|
||||
channels.capacity
|
||||
FROM channels
|
||||
JOIN nodes AS nodes_1 on nodes_1.public_key = channels.node1_public_key
|
||||
JOIN nodes AS nodes_2 on nodes_2.public_key = channels.node2_public_key
|
||||
WHERE nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL
|
||||
AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL
|
||||
let query = `SELECT ${select}
|
||||
FROM channels
|
||||
JOIN nodes AS nodes_1 on nodes_1.public_key = channels.node1_public_key
|
||||
JOIN nodes AS nodes_2 on nodes_2.public_key = channels.node2_public_key
|
||||
WHERE nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL
|
||||
AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL
|
||||
`;
|
||||
|
||||
if (publicKey !== undefined) {
|
||||
query += ' AND (nodes_1.public_key = ? OR nodes_2.public_key = ?)';
|
||||
params.push(publicKey);
|
||||
params.push(publicKey);
|
||||
} else {
|
||||
query += ` AND channels.capacity > 1000000
|
||||
GROUP BY nodes_1.public_key, nodes_2.public_key
|
||||
ORDER BY channels.capacity DESC
|
||||
LIMIT 10000
|
||||
`;
|
||||
}
|
||||
|
||||
const [rows]: any = await DB.query(query, params);
|
||||
return rows.map((row) => [
|
||||
row.node1_public_key, row.node1_alias, row.node1_longitude, row.node1_latitude,
|
||||
row.node2_public_key, row.node2_alias, row.node2_longitude, row.node2_latitude,
|
||||
row.capacity]);
|
||||
return rows.map((row) => {
|
||||
if (style === 'widget') {
|
||||
return [
|
||||
row.node1_longitude, row.node1_latitude,
|
||||
row.node2_longitude, row.node2_latitude,
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
row.node1_public_key, row.node1_alias,
|
||||
row.node1_longitude, row.node1_latitude,
|
||||
row.node2_public_key, row.node2_alias,
|
||||
row.node2_longitude, row.node2_latitude,
|
||||
];
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
logger.err('$getAllChannelsGeo error: ' + (e instanceof Error ? e.message : e));
|
||||
throw e;
|
||||
|
@ -217,8 +245,12 @@ class ChannelsApi {
|
|||
let channelStatusFilter;
|
||||
if (status === 'open') {
|
||||
channelStatusFilter = '< 2';
|
||||
} else if (status === 'active') {
|
||||
channelStatusFilter = '= 1';
|
||||
} else if (status === 'closed') {
|
||||
channelStatusFilter = '= 2';
|
||||
} else {
|
||||
throw new Error('getChannelsForNode: Invalid status requested');
|
||||
}
|
||||
|
||||
// Channels originating from node
|
||||
|
@ -247,7 +279,12 @@ class ChannelsApi {
|
|||
allChannels.sort((a, b) => {
|
||||
return b.capacity - a.capacity;
|
||||
});
|
||||
allChannels = allChannels.slice(index, index + length);
|
||||
|
||||
if (index >= 0) {
|
||||
allChannels = allChannels.slice(index, index + length);
|
||||
} else if (index === -1) { // Node channels tree chart
|
||||
allChannels = allChannels.slice(0, 1000);
|
||||
}
|
||||
|
||||
const channels: any[] = []
|
||||
for (const row of allChannels) {
|
||||
|
|
|
@ -102,7 +102,11 @@ class ChannelsRoutes {
|
|||
|
||||
private async $getAllChannelsGeo(req: Request, res: Response) {
|
||||
try {
|
||||
const channels = await channelsApi.$getAllChannelsGeo(req.params?.publicKey);
|
||||
const style: string = typeof req.query.style === 'string' ? req.query.style : '';
|
||||
const channels = await channelsApi.$getAllChannelsGeo(req.params?.publicKey, style);
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
|
|
|
@ -98,7 +98,7 @@ class NetworkSyncService {
|
|||
const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
|
||||
const closedChannels = {};
|
||||
for (const closedChannel of closedChannelsRaw) {
|
||||
closedChannels[Common.channelShortIdToIntegerId(closedChannel.id)] = true;
|
||||
closedChannels[closedChannel.id] = true;
|
||||
}
|
||||
|
||||
let progress = 0;
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
</ng-template>
|
||||
|
||||
<ng-template #skeleton>
|
||||
<h2 class="float-left">Channels</h2>
|
||||
<h2 class="float-left" i18n="lightning.channels">Channels</h2>
|
||||
|
||||
<table class="table table-borderless">
|
||||
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||
|
|
|
@ -32,7 +32,7 @@ export class LightningApiService {
|
|||
}
|
||||
|
||||
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||
let params = new HttpParams()
|
||||
const params = new HttpParams()
|
||||
.set('public_key', publicKey)
|
||||
.set('index', index)
|
||||
.set('status', status)
|
||||
|
|
|
@ -29,6 +29,7 @@ import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-ch
|
|||
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
|
||||
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -56,6 +57,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
|
|||
TopNodesPerCapacity,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -89,6 +91,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
|
|||
TopNodesPerCapacity,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
],
|
||||
providers: [
|
||||
LightningApiService,
|
||||
|
|
|
@ -118,16 +118,23 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
||||
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart>
|
||||
<div *ngIf="!error">
|
||||
<app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
||||
|
||||
<div class="d-flex justify-content-between" *ngIf="!error">
|
||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
|
||||
<h2 i18n="lightning.node-history">Node history</h2>
|
||||
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
|
||||
|
||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||
<app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</h2>
|
||||
</div>
|
||||
|
||||
<app-channels-list [publicKey]="node.public_key"
|
||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
||||
</div>
|
||||
|
||||
<app-channels-list *ngIf="!error" [publicKey]="node.public_key"
|
||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
<div *ngIf="channelsObservable | async">
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||
<div [style]="style === 'widget' ? 'height: 250px' : ''">
|
||||
<div *ngIf="channelsObservable | async">
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
|
||||
(chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
|
||||
(chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
.full-container {
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
min-height: 600px;
|
||||
height: calc(100% - 150px);
|
||||
|
||||
@media (max-width: 992px) {
|
||||
|
@ -18,17 +18,20 @@
|
|||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.full-container.nodepage {
|
||||
min-height: 400px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.full-container.channelpage {
|
||||
min-height: 400px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.full-container.widget {
|
||||
height: 250px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.full-container.fit-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -41,25 +44,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.widget {
|
||||
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 {
|
||||
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%;
|
||||
|
@ -80,3 +64,47 @@
|
|||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
.chart.graph {
|
||||
min-height: 600px;
|
||||
}
|
||||
.chart.nodepage {
|
||||
min-height: 400px;
|
||||
}
|
||||
.chart.channelpage {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
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 {
|
||||
min-height: 250px;
|
||||
-webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%);
|
||||
@media (max-width: 767.98px) {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
.loading-spinner.widget {
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
@media (max-width: 767.98px) {
|
||||
top: 250px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export class NodesChannelsMap implements OnInit {
|
|||
channelColor = '#466d9d';
|
||||
channelCurve = 0;
|
||||
nodeSize = 4;
|
||||
isLoading = true;
|
||||
|
||||
chartInstance = undefined;
|
||||
chartOptions: EChartsOption = {};
|
||||
|
@ -74,7 +75,7 @@ export class NodesChannelsMap implements OnInit {
|
|||
switchMap((params: ParamMap) => {
|
||||
return zip(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined) : [''],
|
||||
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
|
||||
[params.get('public_key') ?? undefined]
|
||||
).pipe(tap((data) => {
|
||||
registerMap('world', data[0]);
|
||||
|
@ -93,10 +94,12 @@ export class NodesChannelsMap implements OnInit {
|
|||
}
|
||||
}
|
||||
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]];
|
||||
if (this.style === 'nodepage' && !thisNodeGPS) {
|
||||
if (data[2] === channel[0]) {
|
||||
thisNodeGPS = [channel[2], channel[3]];
|
||||
} else if (data[2] === channel[4]) {
|
||||
thisNodeGPS = [channel[6], channel[7]];
|
||||
}
|
||||
}
|
||||
|
||||
// 0 - node1 pubkey
|
||||
|
@ -105,48 +108,68 @@ export class NodesChannelsMap implements OnInit {
|
|||
// 4 - node2 pubkey
|
||||
// 5 - node2 alias
|
||||
// 6,7 - node2 GPS
|
||||
const node1PubKey = 0;
|
||||
const node1Alias = 1;
|
||||
let node1GpsLat = 2;
|
||||
let node1GpsLgt = 3;
|
||||
const node2PubKey = 4;
|
||||
const node2Alias = 5;
|
||||
let node2GpsLat = 6;
|
||||
let node2GpsLgt = 7;
|
||||
let node1UniqueId = channel[node1PubKey];
|
||||
let node2UniqueId = channel[node2PubKey];
|
||||
if (this.style === 'widget') {
|
||||
node1GpsLat = 0;
|
||||
node1GpsLgt = 1;
|
||||
node2GpsLat = 2;
|
||||
node2GpsLgt = 3;
|
||||
node1UniqueId = channel[node1GpsLat].toString() + channel[node1GpsLgt].toString();
|
||||
node2UniqueId = channel[node2GpsLat].toString() + channel[node2GpsLgt].toString();
|
||||
}
|
||||
|
||||
// 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]]) {
|
||||
if (!nodesPubkeys[node1UniqueId]) {
|
||||
nodes.push([
|
||||
channel[2] + random2 * Math.cos(random),
|
||||
channel[3] + random2 * Math.sin(random),
|
||||
channel[node1GpsLat] + random2 * Math.cos(random),
|
||||
channel[node1GpsLgt] + random2 * Math.sin(random),
|
||||
1,
|
||||
channel[0],
|
||||
channel[1]
|
||||
channel[node1PubKey],
|
||||
channel[node1Alias]
|
||||
]);
|
||||
nodesPubkeys[channel[0]] = nodes[nodes.length - 1];
|
||||
nodesPubkeys[node1UniqueId] = nodes[nodes.length - 1];
|
||||
}
|
||||
|
||||
random = Math.random() * 2 * Math.PI;
|
||||
random2 = Math.random() * 0.01;
|
||||
|
||||
if (!nodesPubkeys[channel[4]]) {
|
||||
if (!nodesPubkeys[node2UniqueId]) {
|
||||
nodes.push([
|
||||
channel[6] + random2 * Math.cos(random),
|
||||
channel[7] + random2 * Math.sin(random),
|
||||
channel[node2GpsLat] + random2 * Math.cos(random),
|
||||
channel[node2GpsLgt] + random2 * Math.sin(random),
|
||||
1,
|
||||
channel[4],
|
||||
channel[5]
|
||||
channel[node2PubKey],
|
||||
channel[node2Alias]
|
||||
]);
|
||||
nodesPubkeys[channel[4]] = nodes[nodes.length - 1];
|
||||
nodesPubkeys[node2UniqueId] = nodes[nodes.length - 1];
|
||||
}
|
||||
|
||||
const channelLoc = [];
|
||||
channelLoc.push(nodesPubkeys[channel[0]].slice(0, 2));
|
||||
channelLoc.push(nodesPubkeys[channel[4]].slice(0, 2));
|
||||
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
|
||||
channelLoc.push(nodesPubkeys[node2UniqueId].slice(0, 2));
|
||||
channelsLoc.push(channelLoc);
|
||||
}
|
||||
|
||||
if (this.style === 'nodepage' && thisNodeGPS) {
|
||||
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;
|
||||
|
@ -238,7 +261,7 @@ export class NodesChannelsMap implements OnInit {
|
|||
},
|
||||
{
|
||||
large: false,
|
||||
progressive: 200,
|
||||
progressive: this.style === 'widget' ? 500 : 200,
|
||||
silent: true,
|
||||
type: 'lines',
|
||||
coordinateSystem: 'geo',
|
||||
|
@ -266,6 +289,10 @@ export class NodesChannelsMap implements OnInit {
|
|||
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('finished', () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
|
||||
if (this.style === 'widget') {
|
||||
this.chartInstance.getZr().on('click', (e) => {
|
||||
this.zone.run(() => {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<div *ngIf="channelsObservable$ | async" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
</div>
|
|
@ -0,0 +1,138 @@
|
|||
import { formatNumber } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { lerpColor } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-channels',
|
||||
templateUrl: './node-channels.component.html',
|
||||
styleUrls: ['./node-channels.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodeChannels implements OnChanges {
|
||||
@Input() publicKey: string;
|
||||
|
||||
chartInstance: ECharts;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
|
||||
channelsObservable$: Observable<any>;
|
||||
isLoading: true;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.prepareChartOptions(null);
|
||||
|
||||
this.channelsObservable$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey, -1, 'active')
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
const biggestCapacity = response.body[0].capacity;
|
||||
this.prepareChartOptions(response.body.map(channel => {
|
||||
return {
|
||||
name: channel.node.alias,
|
||||
value: channel.capacity,
|
||||
shortId: channel.short_id,
|
||||
id: channel.id,
|
||||
itemStyle: {
|
||||
color: lerpColor('#1E88E5', '#D81B60', Math.pow(channel.capacity / biggestCapacity, 0.4)),
|
||||
}
|
||||
};
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data): void {
|
||||
this.chartOptions = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
align: 'left',
|
||||
}
|
||||
},
|
||||
series: <TreemapSeriesOption[]>[
|
||||
{
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
roam: false,
|
||||
type: 'treemap',
|
||||
data: data,
|
||||
nodeClick: 'link',
|
||||
progressive: 100,
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (value): string => {
|
||||
if (value.data.name === undefined) {
|
||||
return ``;
|
||||
}
|
||||
let capacity = '';
|
||||
if (value.data.value > 100000000) {
|
||||
capacity = formatNumber(Math.round(value.data.value / 100000000), this.locale, '1.2-2') + ' BTC';
|
||||
} else {
|
||||
capacity = <string>this.amountShortenerPipe.transform(value.data.value, 2) + ' sats';
|
||||
}
|
||||
|
||||
return `
|
||||
<b style="color: white; margin-left: 2px">${value.data.shortId}</b><br>
|
||||
<span>Node: ${value.name}</span><br>
|
||||
<span>Capacity: ${capacity}</span>
|
||||
`;
|
||||
}
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
},
|
||||
breadcrumb: {
|
||||
show: false,
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec: ECharts): void {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
//@ts-ignore
|
||||
if (!e.data.id) {
|
||||
return;
|
||||
}
|
||||
this.zone.run(() => {
|
||||
//@ts-ignore
|
||||
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/channel/${e.data.id}`);
|
||||
this.router.navigate([url]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -271,10 +271,11 @@ export class ApiService {
|
|||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries');
|
||||
}
|
||||
|
||||
getChannelsGeo$(publicKey?: string): Observable<any> {
|
||||
getChannelsGeo$(publicKey?: string, style?: 'graph' | 'nodepage' | 'widget' | 'channelpage'): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' +
|
||||
(publicKey !== undefined ? `/${publicKey}` : '')
|
||||
(publicKey !== undefined ? `/${publicKey}` : '') +
|
||||
(style !== undefined ? `?style=${style}` : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,3 +91,25 @@ export function detectWebGL() {
|
|||
return (gl && gl instanceof WebGLRenderingContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* https://gist.githubusercontent.com/rosszurowski/67f04465c424a9bc0dae/raw/90ee06c5aa84ab352eb5b233d0a8263c3d8708e5/lerp-color.js
|
||||
* A linear interpolator for hexadecimal colors
|
||||
* @param {String} a
|
||||
* @param {String} b
|
||||
* @param {Number} amount
|
||||
* @example
|
||||
* // returns #7F7F7F
|
||||
* lerpColor('#000000', '#ffffff', 0.5)
|
||||
* @returns {String}
|
||||
*/
|
||||
export function lerpColor(a: string, b: string, amount: number): string {
|
||||
const ah = parseInt(a.replace(/#/g, ''), 16),
|
||||
ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff,
|
||||
bh = parseInt(b.replace(/#/g, ''), 16),
|
||||
br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff,
|
||||
rr = ar + amount * (br - ar),
|
||||
rg = ag + amount * (bg - ag),
|
||||
rb = ab + amount * (bb - ab);
|
||||
|
||||
return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
|
||||
}
|
|
@ -77,6 +77,8 @@ do for url in / \
|
|||
'/api/v1/mining/difficulty-adjustments/2y' \
|
||||
'/api/v1/mining/difficulty-adjustments/3y' \
|
||||
'/api/v1/mining/difficulty-adjustments/all' \
|
||||
'/api/v1/lightning/channels-geo?style=widget' \
|
||||
'/api/v1/lightning/channels-geo?style=graph' \
|
||||
|
||||
do
|
||||
curl -s "https://${hostname}${url}" >/dev/null
|
||||
|
|
Loading…
Add table
Reference in a new issue