diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 410d34a01..8d9de53c9 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -1,6 +1,5 @@ import { CpfpInfo, TransactionExtended, TransactionStripped } from '../mempool.interfaces'; import config from '../config'; -import { convertChannelId } from './lightning/clightning/clightning-convert'; export class Common { static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ? '144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49' @@ -193,16 +192,20 @@ export class Common { date.setUTCMilliseconds(0); } - static channelShortIdToIntegerId(id: string): string { - if (config.LIGHTNING.BACKEND === 'lnd') { - return id; + static channelShortIdToIntegerId(channelId: string): string { + if (channelId.indexOf('x') === -1) { // Already an integer id + return channelId; } - return convertChannelId(id); + if (channelId.indexOf('/') !== -1) { // Topology import + channelId = channelId.slice(0, -2); + } + const s = channelId.split('x').map(part => BigInt(part)); + return ((s[0] << 40n) | (s[1] << 16n) | s[2]).toString(); } /** Decodes a channel id returned by lnd as uint64 to a short channel id */ static channelIntegerIdToShortId(id: string): string { - if (config.LIGHTNING.BACKEND === 'cln') { + if (id.indexOf('x') !== -1) { // Already a short id return id; } diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index dec9af1e5..67003be57 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -448,7 +448,7 @@ class ChannelsApi { const result = await DB.query(` UPDATE channels SET status = 0 - WHERE short_id NOT IN ( + WHERE id NOT IN ( ${graphChannelsIds.map(id => `"${id}"`).join(',')} ) AND status != 2 diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index d4857a3a4..2d838524e 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -168,7 +168,7 @@ class NodesApi { } } - public async $getNodesISP(groupBy: string, showTor: boolean) { + public async $getNodesISPRanking(groupBy: string, showTor: boolean) { try { const orderBy = groupBy === 'capacity' ? `CAST(SUM(capacity) as INT)` : `COUNT(DISTINCT nodes.public_key)`; diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index a850b6a09..5e0f95acb 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -79,7 +79,7 @@ class NodesRoutes { return; } - const nodesPerAs = await nodesApi.$getNodesISP(groupBy, showTor); + const nodesPerAs = await nodesApi.$getNodesISPRanking(groupBy, showTor); res.header('Pragma', 'public'); res.header('Cache-control', 'public'); diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 5df51aadc..15d8d8766 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -1,6 +1,7 @@ import { ILightningApi } from '../lightning-api.interface'; import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher'; import logger from '../../../logger'; +import { Common } from '../../common'; /** * Convert a clightning "listnode" entry to a lnd node entry @@ -70,14 +71,6 @@ export async function convertAndmergeBidirectionalChannels(clChannels: any[]): P return consolidatedChannelList; } -export function convertChannelId(channelId): string { - if (channelId.indexOf('/') !== -1) { - channelId = channelId.slice(0, -2); - } - const s = channelId.split('x').map(part => BigInt(part)); - return ((s[0] << 40n) | (s[1] << 16n) | s[2]).toString(); -} - /** * Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format * In this case, clightning knows the channel policy for both nodes @@ -90,7 +83,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise { this.$runTasks(); }, 1000 * config.LIGHTNING.STATS_REFRESH_INTERVAL); } // This method look up the creation date of the earliest channel of the node diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index ecb056859..9e6e5bd82 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -25,7 +25,7 @@ class LightningStatsUpdater { const date = new Date(); Common.setDateMidnight(date); const networkGraph = await lightningApi.$getNetworkGraph(); - LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); + await LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); logger.info(`Updated latest network stats`); } diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 8c823e2ef..7b618e66e 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -4,6 +4,8 @@ import { XMLParser } from 'fast-xml-parser'; import logger from '../../../logger'; import fundingTxFetcher from './funding-tx-fetcher'; import config from '../../../config'; +import { ILightningApi } from '../../../api/lightning/lightning-api.interface'; +import { isIP } from 'net'; const fsPromises = promises; @@ -48,7 +50,7 @@ class LightningStatsImporter { /** * Generate LN network stats for one day */ - public async computeNetworkStats(timestamp: number, networkGraph): Promise { + public async computeNetworkStats(timestamp: number, networkGraph: ILightningApi.NetworkGraph): Promise { // Node counts and network shares let clearnetNodes = 0; let torNodes = 0; @@ -61,8 +63,8 @@ class LightningStatsImporter { let isUnnanounced = true; for (const socket of (node.addresses ?? [])) { - hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network); - hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network); + hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network) || socket.addr.indexOf('onion') !== -1; + hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network) || [4, 6].includes(isIP(socket.addr.split(':')[0])); } if (hasOnion && hasClearnet) { clearnetTorNodes++; @@ -127,22 +129,28 @@ class LightningStatsImporter { if (channel.node1_policy !== undefined) { // Coming from the node for (const policy of [channel.node1_policy, channel.node2_policy]) { - if (policy && policy.fee_rate_milli_msat < 5000) { + if (policy && parseInt(policy.fee_rate_milli_msat, 10) < 5000) { avgFeeRate += parseInt(policy.fee_rate_milli_msat, 10); feeRates.push(parseInt(policy.fee_rate_milli_msat, 10)); } - if (policy && policy.fee_base_msat < 5000) { + if (policy && parseInt(policy.fee_base_msat, 10) < 5000) { avgBaseFee += parseInt(policy.fee_base_msat, 10); baseFees.push(parseInt(policy.fee_base_msat, 10)); } } } else { // Coming from the historical import + // @ts-ignore if (channel.fee_rate_milli_msat < 5000) { + // @ts-ignore avgFeeRate += parseInt(channel.fee_rate_milli_msat, 10); + // @ts-ignore feeRates.push(parseInt(channel.fee_rate_milli_msat), 10); - } + } + // @ts-ignore if (channel.fee_base_msat < 5000) { + // @ts-ignore avgBaseFee += parseInt(channel.fee_base_msat, 10); + // @ts-ignore baseFees.push(parseInt(channel.fee_base_msat), 10); } } diff --git a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts index 0f10a106d..52da15125 100644 --- a/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts +++ b/frontend/src/app/components/bisq-master-page/bisq-master-page.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { Observable } from 'rxjs'; import { LanguageService } from 'src/app/services/language.service'; +import { EnterpriseService } from 'src/app/services/enterprise.service'; @Component({ selector: 'app-bisq-master-page', @@ -18,6 +19,7 @@ export class BisqMasterPageComponent implements OnInit { constructor( private stateService: StateService, private languageService: LanguageService, + private enterpriseService: EnterpriseService, ) { } ngOnInit() { diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts index 65f07320d..22a351068 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Env, StateService } from '../../services/state.service'; import { merge, Observable, of} from 'rxjs'; import { LanguageService } from 'src/app/services/language.service'; +import { EnterpriseService } from 'src/app/services/enterprise.service'; @Component({ selector: 'app-liquid-master-page', @@ -20,6 +21,7 @@ export class LiquidMasterPageComponent implements OnInit { constructor( private stateService: StateService, private languageService: LanguageService, + private enterpriseService: EnterpriseService, ) { } ngOnInit() { diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index ae1bb2eb2..1888b3eee 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -76,10 +76,8 @@
-
-
-
+
diff --git a/frontend/src/app/docs/api-docs/api-docs.component.html b/frontend/src/app/docs/api-docs/api-docs.component.html index bae5dfd05..f106c4bc5 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.html +++ b/frontend/src/app/docs/api-docs/api-docs.component.html @@ -42,16 +42,18 @@
Endpoint
- {{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }} + {{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }} - {{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }} + {{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }} +

{{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }}

- {{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }} + {{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }} +

{{ item.httpRequestMethod }} {{ baseNetworkUrl }}/api{{ item.urlString }}

{{ item.httpRequestMethod }} {{ item.urlString }}
diff --git a/frontend/src/app/docs/api-docs/api-docs.component.ts b/frontend/src/app/docs/api-docs/api-docs.component.ts index 260a701ea..ed0ecb0a2 100644 --- a/frontend/src/app/docs/api-docs/api-docs.component.ts +++ b/frontend/src/app/docs/api-docs/api-docs.component.ts @@ -50,9 +50,7 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { document.getElementById( this.route.snapshot.fragment ).scrollIntoView(); } } - window.addEventListener('scroll', function() { - that.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative"; - }, { passive: true} ); + window.addEventListener('scroll', that.onDocScroll, { passive: true }); }, 1 ); } @@ -87,6 +85,14 @@ export class ApiDocsComponent implements OnInit, AfterViewInit { }); } + ngOnDestroy(): void { + window.removeEventListener('scroll', this.onDocScroll); + } + + onDocScroll() { + this.desktopDocsNavPosition = ( window.pageYOffset > 182 ) ? "fixed" : "relative"; + } + anchorLinkClick( event: any ) { let targetId = ""; if( event.target.nodeName === "A" ) { diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index 999183e09..ff00f5b15 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -4,6 +4,7 @@
+
Network Statistics  @@ -17,6 +18,7 @@
+
Channels Statistics  @@ -30,29 +32,32 @@
+ +
+
+
+ + +
+
+
+
-
-
+
+
+
Lightning network history
+
-
-
-
- - -
-
-
-
Top Capacity Nodes
- +
@@ -62,7 +67,7 @@
Most Connected Nodes
- +
diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss index 4fdadd57b..303591974 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.scss @@ -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; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss index 7e6b9f050..578bffc3a 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss @@ -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; } diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 43da510f0..d8952d632 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -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', @@ -50,8 +51,15 @@ export class NodesChannelsMap implements OnInit, OnDestroy { ngOnInit(): void { this.center = this.style === 'widget' ? [0, 40] : [0, 5]; - this.zoom = this.style === 'widget' ? 3.5 : 1.3; - + 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`); } @@ -181,7 +189,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { center: this.center, zoom: this.zoom, tooltip: { - show: true + show: false }, map: 'world', roam: this.style === 'widget' ? false : true, @@ -192,18 +200,21 @@ export class NodesChannelsMap implements OnInit, OnDestroy { scaleLimit: { min: 1.3, max: 100000, + }, + emphasis: { + disabled: true, } }, series: [ { large: true, - progressive: 200, type: 'scatter', data: nodes, coordinateSystem: 'geo', geoIndex: 0, symbolSize: 4, tooltip: { + show: true, backgroundColor: 'rgba(17, 19, 31, 1)', borderRadius: 4, shadowColor: 'rgba(0, 0, 0, 0.5)', @@ -220,15 +231,15 @@ export class NodesChannelsMap implements OnInit, OnDestroy { }, itemStyle: { color: 'white', - borderColor: 'black', - borderWidth: 2, opacity: 1, + borderColor: 'black', + borderWidth: 0, }, blendMode: 'lighter', - zlevel: 1, + zlevel: 2, }, { - large: true, + large: false, progressive: 200, silent: true, type: 'lines', @@ -244,7 +255,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { tooltip: { show: false, }, - zlevel: 2, + zlevel: 1, } ] }; @@ -285,12 +296,19 @@ export class NodesChannelsMap implements OnInit, OnDestroy { series: this.chartOptions.series }; + let nodeBorder = 0; + if (this.chartInstance.getOption().geo[0].zoom > 5000) { + nodeBorder = 2; + } + + chartOptions.series[0].itemStyle.borderWidth = nodeBorder; + chartOptions.series[0].symbolSize += e.zoom > 1 ? speed * 15 : -speed * 15; + chartOptions.series[0].symbolSize = Math.max(4, Math.min(7, chartOptions.series[0].symbolSize)); + 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); }); diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.html b/frontend/src/app/lightning/nodes-list/nodes-list.component.html index 65a7a558a..d21f0b30a 100644 --- a/frontend/src/app/lightning/nodes-list/nodes-list.component.html +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.html @@ -3,18 +3,18 @@ - - + + - - diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.scss b/frontend/src/app/lightning/nodes-list/nodes-list.component.scss index e69de29bb..85a1339ea 100644 --- a/frontend/src/app/lightning/nodes-list/nodes-list.component.scss +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.scss @@ -0,0 +1,11 @@ +.capacity.mobile-channels { + @media (max-width: 767.98px) { + display: none; + } +} + +.channels.mobile-capacity { + @media (max-width: 767.98px) { + display: none; + } +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-list/nodes-list.component.ts b/frontend/src/app/lightning/nodes-list/nodes-list.component.ts index d6d05833e..9b9e2d594 100644 --- a/frontend/src/app/lightning/nodes-list/nodes-list.component.ts +++ b/frontend/src/app/lightning/nodes-list/nodes-list.component.ts @@ -9,6 +9,7 @@ import { Observable } from 'rxjs'; }) export class NodesListComponent implements OnInit { @Input() nodes$: Observable; + @Input() show: string; constructor() { } diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss index fa044a4d6..760e782ca 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss @@ -51,8 +51,7 @@ } .chart-widget { width: 100%; - height: 100%; - max-height: 270px; + height: 145px; } .formRadioGroup { diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 70d02de28..dbbb49483 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption} from 'echarts'; +import { EChartsOption, graphic} from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { formatNumber } from '@angular/common'; @@ -9,6 +9,7 @@ import { MiningService } from 'src/app/services/mining.service'; import { download } from 'src/app/shared/graphs.utils'; import { SeoService } from 'src/app/services/seo.service'; import { LightningApiService } from '../lightning-api.service'; +import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; @Component({ selector: 'app-nodes-networks-chart', @@ -26,7 +27,7 @@ import { LightningApiService } from '../lightning-api.service'; }) export class NodesNetworksChartComponent implements OnInit { @Input() right: number | string = 45; - @Input() left: number | string = 55; + @Input() left: number | string = 45; @Input() widget = false; miningWindowPreference: string; @@ -51,7 +52,8 @@ export class NodesNetworksChartComponent implements OnInit { private lightningApiService: LightningApiService, private formBuilder: FormBuilder, private storageService: StorageService, - private miningService: MiningService + private miningService: MiningService, + private amountShortenerPipe: AmountShortenerPipe, ) { } @@ -82,11 +84,17 @@ export class NodesNetworksChartComponent implements OnInit { .pipe( tap((response) => { const data = response.body; - this.prepareChartOptions({ + const chartData = { tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]), clearnet_nodes: data.map(val => [val.added * 1000, val.clearnet_nodes]), unannounced_nodes: data.map(val => [val.added * 1000, val.unannounced_nodes]), - }); + }; + let maxYAxis = 0; + for (const day of data) { + maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes); + } + maxYAxis = Math.ceil(maxYAxis / 3000) * 3000; + this.prepareChartOptions(chartData, maxYAxis); this.isLoading = false; }), map((response) => { @@ -100,7 +108,7 @@ export class NodesNetworksChartComponent implements OnInit { ); } - prepareChartOptions(data) { + prepareChartOptions(data, maxYAxis) { let title: object; if (data.tor_nodes.length === 0) { title = { @@ -110,24 +118,30 @@ export class NodesNetworksChartComponent implements OnInit { }, text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, left: 'center', - top: 'center' + top: 'top', + }; + } else if (this.widget) { + title = { + textStyle: { + color: 'grey', + fontSize: 11 + }, + text: $localize`Nodes per network`, + left: 'center', + top: 11, + zlevel: 10, }; } this.chartOptions = { title: title, animation: false, - color: [ - '#D81B60', - '#039BE5', - '#7CB342', - '#FFB300', - ], grid: { - top: 40, - bottom: this.widget ? 30 : 70, - right: this.right, - left: this.left, + height: this.widget ? 100 : undefined, + top: this.widget ? 10 : 40, + bottom: this.widget ? 0 : 70, + right: (this.isMobile() && this.widget) ? 35 : this.right, + left: (this.isMobile() && this.widget) ? 40 :this.left, }, tooltip: { show: !this.isMobile() || !this.widget, @@ -171,7 +185,7 @@ export class NodesNetworksChartComponent implements OnInit { hideOverlap: true, } }, - legend: data.tor_nodes.length === 0 ? undefined : { + legend: this.widget || data.tor_nodes.length === 0 ? undefined : { padding: 10, data: [ { @@ -207,7 +221,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, ], - selected: JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { + selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { 'Total': true, 'Tor': true, 'Clearnet': true, @@ -218,13 +232,14 @@ export class NodesNetworksChartComponent implements OnInit { { type: 'value', position: 'left', - min: (value) => { - return value.min * 0.9; - }, axisLabel: { color: 'rgb(110, 112, 121)', - formatter: (val) => { - return `${formatNumber(Math.round(val * 100) / 100, this.locale, '1.0-0')}`; + formatter: (val: number): string => { + if (this.widget) { + return `${this.amountShortenerPipe.transform(val, 0)}`; + } else { + return `${formatNumber(Math.round(val), this.locale, '1.0-0')}`; + } } }, splitLine: { @@ -232,8 +247,35 @@ export class NodesNetworksChartComponent implements OnInit { type: 'dotted', color: '#ffffff66', opacity: 0.25, + }, + }, + max: maxYAxis, + min: 0, + interval: 3000, + }, + { + type: 'value', + position: 'right', + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val: number): string => { + if (this.widget) { + return `${this.amountShortenerPipe.transform(val, 0)}`; + } else { + return `${formatNumber(Math.round(val), this.locale, '1.0-0')}`; + } } }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + }, + }, + max: maxYAxis, + min: 0, + interval: 3000, } ], series: data.tor_nodes.length === 0 ? [] : [ @@ -252,7 +294,12 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.5, }, stack: 'Total', - color: '#FDD835', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#D81B60' }, + { offset: 1, color: '#D81B60AA' }, + ]), + + smooth: true, }, { zlevel: 1, @@ -269,11 +316,15 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.5, }, stack: 'Total', - color: '#00ACC1', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#FFB300' }, + { offset: 1, color: '#FFB300AA' }, + ]), + smooth: true, }, { zlevel: 1, - yAxisIndex: 0, + yAxisIndex: 1, name: $localize`Tor`, showSymbol: false, symbol: 'none', @@ -286,7 +337,11 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.5, }, stack: 'Total', - color: '#7D4698', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#7D4698' }, + { offset: 1, color: '#7D4698AA' }, + ]), + smooth: true, }, ], dataZoom: this.widget ? null : [{ diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html index 23f54bbba..01be4f036 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html @@ -1,6 +1,29 @@ -
+
-
+
+
+
+
Tagged ISPs
+

+ {{ stats.taggedISP }} +

+
+
+
Tagged nodes
+

+ {{ stats.taggedNodeCount }} +

+
+
+
Tagged capacity
+

+ +

+
+
+
+ +
Lightning nodes per ISP
-
-
-
-
+
+
-
+
-
AliasCapacityChannelsCapacityChannels
{{ node.alias }} + + {{ node.channels | number }}
+
@@ -39,7 +60,7 @@ - +
Rank
{{ asEntry.rank }} {{ asEntry.name }} @@ -54,3 +75,26 @@ + + +
+
+
Tagged ISPs
+

+ +

+
+
+
Tagged capacity
+

+ +

+
+
+
Tagged nodes
+

+ +

+
+
+
diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss index 10ad39372..874d901b2 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss @@ -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) { diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index 6b9d41e74..cd8a72884 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -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); + } } diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss index fa044a4d6..760e782ca 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.scss @@ -51,8 +51,7 @@ } .chart-widget { width: 100%; - height: 100%; - max-height: 270px; + height: 145px; } .formRadioGroup { diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index 1727d1f68..d889dd254 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -9,6 +9,7 @@ import { StorageService } from 'src/app/services/storage.service'; import { MiningService } from 'src/app/services/mining.service'; import { download } from 'src/app/shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; +import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe'; @Component({ selector: 'app-lightning-statistics-chart', @@ -25,7 +26,7 @@ import { LightningApiService } from '../lightning-api.service'; }) export class LightningStatisticsChartComponent implements OnInit { @Input() right: number | string = 45; - @Input() left: number | string = 55; + @Input() left: number | string = 45; @Input() widget = false; miningWindowPreference: string; @@ -51,6 +52,7 @@ export class LightningStatisticsChartComponent implements OnInit { private formBuilder: FormBuilder, private storageService: StorageService, private miningService: MiningService, + private amountShortenerPipe: AmountShortenerPipe, ) { } @@ -105,24 +107,39 @@ export class LightningStatisticsChartComponent implements OnInit { color: 'grey', fontSize: 15 }, - text: `Indexing in progess`, + text: $localize`Indexing in progess`, left: 'center', top: 'center' }; + } else if (this.widget) { + title = { + textStyle: { + color: 'grey', + fontSize: 11 + }, + text: $localize`Channels & Capacity`, + left: 'center', + top: 11, + zlevel: 10, + }; } this.chartOptions = { title: title, animation: false, color: [ - '#FDD835', - '#D81B60', + '#FFB300', + new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#D81B60' }, + { offset: 1, color: '#D81B60AA' }, + ]), ], grid: { - top: 40, - bottom: this.widget ? 30 : 70, - right: this.right, - left: this.left, + height: this.widget ? 100 : undefined, + top: this.widget ? 10 : 40, + bottom: this.widget ? 0 : 70, + right: (this.isMobile() && this.widget) ? 35 : this.right, + left: (this.isMobile() && this.widget) ? 40 :this.left, }, tooltip: { show: !this.isMobile(), @@ -166,7 +183,7 @@ export class LightningStatisticsChartComponent implements OnInit { hideOverlap: true, } }, - legend: data.channel_count.length === 0 ? undefined : { + legend: this.widget || data.channel_count.length === 0 ? undefined : { padding: 10, data: [ { @@ -178,7 +195,7 @@ export class LightningStatisticsChartComponent implements OnInit { icon: 'roundRect', }, { - name: 'Capacity (BTC)', + name: 'Capacity', inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -188,17 +205,20 @@ export class LightningStatisticsChartComponent implements OnInit { ], selected: JSON.parse(this.storageService.getValue('sizes_ln_legend')) ?? { 'Channels': true, - 'Capacity (BTC)': true, + 'Capacity': true, } }, yAxis: data.channel_count.length === 0 ? undefined : [ { - min: 0, type: 'value', axisLabel: { color: 'rgb(110, 112, 121)', - formatter: (val) => { - return `${formatNumber(Math.round(val), this.locale, '1.0-0')}`; + formatter: (val: number): string => { + if (this.widget) { + return `${this.amountShortenerPipe.transform(val, 0)}`; + } else { + return `${formatNumber(Math.round(val), this.locale, '1.0-0')}`; + } } }, splitLine: { @@ -208,6 +228,7 @@ export class LightningStatisticsChartComponent implements OnInit { opacity: 0.25, } }, + minInterval: this.widget ? 20000 : undefined, }, { min: 0, @@ -215,8 +236,12 @@ export class LightningStatisticsChartComponent implements OnInit { position: 'right', axisLabel: { color: 'rgb(110, 112, 121)', - formatter: (val) => { - return `${formatNumber(Math.round(val / 100000000), this.locale, '1.0-0')}`; + formatter: (val: number): string => { + if (this.widget) { + return `${this.amountShortenerPipe.transform(Math.round(val / 100000000), 0)}`; + } else { + return `${formatNumber(Math.round(val / 100000000), this.locale, '1.0-0')}`; + } } }, splitLine: { @@ -244,20 +269,49 @@ export class LightningStatisticsChartComponent implements OnInit { opacity: 1, width: 1, }, - } + }, + smooth: true, }, { zlevel: 0, yAxisIndex: 1, - name: 'Capacity (BTC)', + name: $localize`Capacity`, showSymbol: false, symbol: 'none', stack: 'Total', data: data.capacity, - areaStyle: {}, + areaStyle: { + opacity: 0.5, + }, type: 'line', + smooth: true, } ], + dataZoom: this.widget ? null : [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 5, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + }, + }], }; } diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index 41a6194a1..bc80f337d 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -24,16 +24,16 @@ export class EnterpriseService { this.subdomain = subdomain; this.fetchSubdomainInfo(); this.disableSubnetworks(); - } else if (document.location.hostname === 'mempool.space') { + } else { this.insertMatomo(); } } - getSubdomain() { + getSubdomain(): string { return this.subdomain; } - disableSubnetworks() { + disableSubnetworks(): void { this.stateService.env.TESTNET_ENABLED = false; this.stateService.env.LIQUID_ENABLED = false; this.stateService.env.LIQUID_TESTNET_ENABLED = false; @@ -41,7 +41,7 @@ export class EnterpriseService { this.stateService.env.BISQ_ENABLED = false; } - fetchSubdomainInfo() { + fetchSubdomainInfo(): void { this.apiService.getEnterpriseInfo$(this.subdomain).subscribe((info) => { this.info = info; this.insertMatomo(info.site_id); @@ -54,14 +54,38 @@ export class EnterpriseService { }); } - insertMatomo(siteId = 5) { + insertMatomo(siteId?: number): void { let statsUrl = '//stats.mempool.space/'; - if (this.document.location.hostname === 'liquid.network') { - statsUrl = '//stats.liquid.network/'; - siteId = 8; - } else if (this.document.location.hostname === 'bisq.markets') { - statsUrl = '//stats.bisq.markets/'; - siteId = 7; + + if (!siteId) { + switch (this.document.location.hostname) { + case 'mempool.space': + statsUrl = '//stats.mempool.space/'; + siteId = 5; + break; + case 'mempool.ninja': + statsUrl = '//stats.mempool.space/'; + siteId = 4; + break; + case 'liquid.network': + siteId = 8; + statsUrl = '//stats.liquid.network/'; + break; + case 'liquid.place': + siteId = 10; + statsUrl = '//stats.liquid.network/'; + break; + case 'bisq.markets': + siteId = 7; + statsUrl = '//stats.bisq.markets/'; + break; + case 'bisq.ninja': + statsUrl = '//stats.bisq.markets/'; + siteId = 11; + break; + default: + return; + } } // @ts-ignore diff --git a/frontend/src/app/shared/pipes/amount-shortener.pipe.ts b/frontend/src/app/shared/pipes/amount-shortener.pipe.ts index a31a5712e..db3d94284 100644 --- a/frontend/src/app/shared/pipes/amount-shortener.pipe.ts +++ b/frontend/src/app/shared/pipes/amount-shortener.pipe.ts @@ -5,7 +5,7 @@ import { Pipe, PipeTransform } from '@angular/core'; }) export class AmountShortenerPipe implements PipeTransform { transform(num: number, ...args: any[]): unknown { - const digits = args[0] || 1; + const digits = args[0] ?? 1; const unit = args[1] || undefined; if (num < 1000) {