diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 9d82dc83d..cbd70a34f 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -434,12 +434,14 @@ class NodesApi { SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels, nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, geo_names_city.names as city, geo_names_country.names as country, - geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision + geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision, + nodes.longitude, nodes.latitude, nodes.as_number, geo_names_isp.names as isp FROM nodes LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division' + LEFT JOIN geo_names geo_names_isp on geo_names_isp.id = nodes.as_number AND geo_names_isp.type = 'as_organization' WHERE geo_names_country.id = ? ORDER BY capacity DESC `; @@ -449,6 +451,7 @@ class NodesApi { rows[i].country = JSON.parse(rows[i].country); rows[i].city = JSON.parse(rows[i].city); rows[i].subdivision = JSON.parse(rows[i].subdivision); + rows[i].isp = JSON.parse(rows[i].isp); } return rows; } catch (e) { @@ -463,7 +466,8 @@ class NodesApi { SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels, nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, geo_names_city.names as city, geo_names_country.names as country, - geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision + geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision, + nodes.longitude, nodes.latitude FROM nodes LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' diff --git a/frontend/src/app/lightning/channel/channel.component.html b/frontend/src/app/lightning/channel/channel.component.html index 9a0c424fb..3942cda6e 100644 --- a/frontend/src/app/lightning/channel/channel.component.html +++ b/frontend/src/app/lightning/channel/channel.component.html @@ -16,7 +16,8 @@
- +
@@ -51,38 +52,57 @@
- - - -
- -
-
- +
+
+ + + + + + + +
Capacity + + +
-
- -
-
- -
- - -
-

Opening transaction

- -
- -
- -
-

Closing transaction

   - -
- -
-
+
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + + +
+

Opening transaction

+ +
+ + +
+ +
+

Closing transaction

   + + +
+ + +
+
@@ -108,7 +128,7 @@
- +
@@ -152,4 +172,4 @@
- \ No newline at end of file + diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.html b/frontend/src/app/lightning/nodes-map/nodes-map.component.html index 75f8aeb08..d739dd2c9 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.html +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.html @@ -1,13 +1,13 @@ -
+
-
+
Lightning nodes world map
(Tor nodes excluded)
-
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss index 4e363a534..d7ad42b46 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss @@ -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; +} diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts index 6c809916e..b783e225a 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core'; +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 { Observable, tap, zip } from 'rxjs'; @@ -18,6 +18,10 @@ import { getFlagEmoji } from 'src/app/shared/common.utils'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class NodesMap implements OnInit { + @Input() widget: boolean = false; + @Input() nodes: any[] | undefined = undefined; + @Input() type: 'none' | 'isp' | 'country' = 'none'; + observable$: Observable; chartInstance = undefined; @@ -43,13 +47,48 @@ export class NodesMap implements OnInit { this.observable$ = zip( this.assetsService.getWorldMapJson$, - this.apiService.getWorldNodes$() + this.nodes ? [this.nodes] : this.apiService.getWorldNodes$() ).pipe(tap((data) => { registerMap('world', data[0]); + 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]; + } + + 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[] = []; - console.log(data[1].nodes[0]); - for (const node of data[1].nodes) { + 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; @@ -66,11 +105,12 @@ export class NodesMap implements OnInit { ]); } - this.prepareChartOptions(nodes, data[1].maxLiquidity); + maxLiquidity = Math.max(1, maxLiquidity); + this.prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom); })); } - prepareChartOptions(nodes, maxLiquidity) { + prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) { let title: object; if (nodes.length === 0) { title = { @@ -91,8 +131,8 @@ export class NodesMap implements OnInit { geo: { animation: false, silent: true, - center: [0, 5], - zoom: 1.3, + center: mapCenter, + zoom: mapZoom, tooltip: { show: false }, @@ -122,10 +162,13 @@ export class NodesMap implements OnInit { 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: 4, + borderRadius: 0, shadowColor: 'rgba(0, 0, 0, 0.5)', textStyle: { color: '#b1b1b1', @@ -155,7 +198,6 @@ export class NodesMap implements OnInit { borderColor: 'black', borderWidth: 0, }, - blendMode: 'lighter', zlevel: 2, }, ] diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html index 190cf6219..543cf951c 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html @@ -1,9 +1,58 @@
-

+

Lightning nodes in {{ country?.name }} {{ country?.flag }}

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Nodes{{ countryNodes.nodes.length }}
Liquidity + + + {{ countryNodes.sumLiquidity | amountShortener: 1 }} + sats + +   + + +
Channels{{ countryNodes.sumChannels }}
ISP Count{{ countryNodes.ispCount }}
Top ISP + + {{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}] + +
+
+
+
+ +
+
+
+
+
@@ -15,9 +64,8 @@ - - - + + diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts index e0bf5eb66..19394a828 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts @@ -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'; @@ -32,6 +32,8 @@ export class NodesPerCountry implements OnInit { 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) @@ -45,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; } } diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html index 4ea3c2d11..441dc429e 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html @@ -1,5 +1,54 @@
-

Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]

+

Lightning nodes on ISP: {{ isp?.name }}

+ +
+
+
+
Channels Location
{{ node.alias }}
+ + + + + + + + + + + + + + + + + + + + + + +
ASN{{ isp?.id }}
Nodes{{ ispNodes.nodes.length }}
Liquidity + + + {{ ispNodes.sumLiquidity | amountShortener: 1 }} + sats + +   + + +
Channels{{ ispNodes.sumChannels }}
Top country + + {{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }} + +
+
+
+
+ +
+
+
+
@@ -12,9 +61,8 @@ - - - + + diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss index 02b47e8be..b829c5b59 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.scss @@ -59,4 +59,4 @@ @media (max-width: 576px) { display: none } -} \ No newline at end of file +} diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts index f7edf783a..24664aab0 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts @@ -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({ @@ -33,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}]`); @@ -46,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; } }
Channels Location
{{ node.alias }}