mirror of
https://github.com/mempool/mempool.git
synced 2024-11-20 10:21:52 +01:00
Merge pull request #2366 from mempool/nymkappa/feature/channel-tree-map
Create active channel tree map component
This commit is contained in:
commit
7babc82f5d
@ -245,8 +245,12 @@ class ChannelsApi {
|
|||||||
let channelStatusFilter;
|
let channelStatusFilter;
|
||||||
if (status === 'open') {
|
if (status === 'open') {
|
||||||
channelStatusFilter = '< 2';
|
channelStatusFilter = '< 2';
|
||||||
|
} else if (status === 'active') {
|
||||||
|
channelStatusFilter = '= 1';
|
||||||
} else if (status === 'closed') {
|
} else if (status === 'closed') {
|
||||||
channelStatusFilter = '= 2';
|
channelStatusFilter = '= 2';
|
||||||
|
} else {
|
||||||
|
throw new Error('getChannelsForNode: Invalid status requested');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channels originating from node
|
// Channels originating from node
|
||||||
@ -275,7 +279,12 @@ class ChannelsApi {
|
|||||||
allChannels.sort((a, b) => {
|
allChannels.sort((a, b) => {
|
||||||
return b.capacity - a.capacity;
|
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[] = []
|
const channels: any[] = []
|
||||||
for (const row of allChannels) {
|
for (const row of allChannels) {
|
||||||
|
@ -188,4 +188,4 @@ export interface IOldestNodes {
|
|||||||
updatedAt?: number,
|
updatedAt?: number,
|
||||||
city?: any,
|
city?: any,
|
||||||
country?: any,
|
country?: any,
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #skeleton>
|
<ng-template #skeleton>
|
||||||
<h2 class="float-left">Channels</h2>
|
<h2 class="float-left" i18n="lightning.channels">Channels</h2>
|
||||||
|
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||||
|
@ -32,7 +32,7 @@ export class LightningApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||||
let params = new HttpParams()
|
const params = new HttpParams()
|
||||||
.set('public_key', publicKey)
|
.set('public_key', publicKey)
|
||||||
.set('index', index)
|
.set('index', index)
|
||||||
.set('status', status)
|
.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 { 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 { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||||
|
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -56,6 +57,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
|
|||||||
TopNodesPerCapacity,
|
TopNodesPerCapacity,
|
||||||
OldestNodes,
|
OldestNodes,
|
||||||
NodesRankingsDashboard,
|
NodesRankingsDashboard,
|
||||||
|
NodeChannels,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@ -89,6 +91,7 @@ import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/no
|
|||||||
TopNodesPerCapacity,
|
TopNodesPerCapacity,
|
||||||
OldestNodes,
|
OldestNodes,
|
||||||
NodesRankingsDashboard,
|
NodesRankingsDashboard,
|
||||||
|
NodeChannels,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LightningApiService,
|
LightningApiService,
|
||||||
|
@ -118,15 +118,22 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
<div *ngIf="!error">
|
||||||
<app-node-statistics-chart [publicKey]="node.public_key" *ngIf="!error"></app-node-statistics-chart>
|
<app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
||||||
|
|
||||||
<div class="d-flex justify-content-between" *ngIf="!error">
|
<h2 i18n="lightning.node-history">Node history</h2>
|
||||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})</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>
|
</div>
|
||||||
|
|
||||||
<app-channels-list *ngIf="!error" [publicKey]="node.public_key"
|
|
||||||
(channelsStatusChangedEvent)="onChannelsListStatusChanged($event)"></app-channels-list>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -91,3 +91,25 @@ export function detectWebGL() {
|
|||||||
return (gl && gl instanceof WebGLRenderingContext);
|
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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user