diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts
index f76975d0a..3bfa4d50e 100644
--- a/backend/src/api/explorer/nodes.api.ts
+++ b/backend/src/api/explorer/nodes.api.ts
@@ -100,7 +100,6 @@ class NodesApi {
JOIN geo_names ON geo_names.id = nodes.as_number
GROUP BY as_number
ORDER BY COUNT(*) DESC
- LIMIT 20
`;
const [nodesCountPerAS]: any = await DB.query(query);
diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html
index 6f93676f6..c75aac232 100644
--- a/frontend/src/app/components/graphs/graphs.component.html
+++ b/frontend/src/app/components/graphs/graphs.component.html
@@ -34,6 +34,8 @@
i18n="lightning.nodes-networks">Nodes per network
Network capacity
+ Nodes per AS
diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts
index 8bf531d80..193c6ab61 100644
--- a/frontend/src/app/graphs/graphs.routing.module.ts
+++ b/frontend/src/app/graphs/graphs.routing.module.ts
@@ -20,6 +20,7 @@ import { TelevisionComponent } from '../components/television/television.compone
import { DashboardComponent } from '../dashboard/dashboard.component';
import { NodesNetworksChartComponent } from '../lightning/nodes-networks-chart/nodes-networks-chart.component';
import { LightningStatisticsChartComponent } from '../lightning/statistics-chart/lightning-statistics-chart.component';
+import { NodesPerAsChartComponent } from '../lightning/nodes-per-as-chart/nodes-per-as-chart.component';
const browserWindow = window || {};
// @ts-ignore
@@ -99,6 +100,10 @@ const routes: Routes = [
path: 'lightning/capacity',
component: LightningStatisticsChartComponent,
},
+ {
+ path: 'lightning/nodes-per-as',
+ component: NodesPerAsChartComponent,
+ },
{
path: '',
redirectTo: 'mempool',
diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts
index 9146975e4..1cf9992f6 100644
--- a/frontend/src/app/lightning/lightning.module.ts
+++ b/frontend/src/app/lightning/lightning.module.ts
@@ -18,6 +18,7 @@ import { NodeStatisticsChartComponent } from './node-statistics-chart/node-stati
import { GraphsModule } from '../graphs/graphs.module';
import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component';
import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component';
+import { NodesPerAsChartComponent } from '../lightning/nodes-per-as-chart/nodes-per-as-chart.component';
@NgModule({
declarations: [
LightningDashboardComponent,
@@ -33,6 +34,7 @@ import { ChannelsStatisticsComponent } from './channels-statistics/channels-stat
LightningStatisticsChartComponent,
NodesNetworksChartComponent,
ChannelsStatisticsComponent,
+ NodesPerAsChartComponent,
],
imports: [
CommonModule,
diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html
new file mode 100644
index 000000000..16ba4fea6
--- /dev/null
+++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Rank |
+ Name |
+ Hashrate |
+ Nodes |
+
+
+
+
+ {{ asEntry.rank }} |
+ {{ asEntry.name }} |
+ {{ asEntry.share }}% |
+ {{ asEntry.count }} |
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss
new file mode 100644
index 000000000..832880122
--- /dev/null
+++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.scss
@@ -0,0 +1,36 @@
+.card-header {
+ border-bottom: 0;
+ font-size: 18px;
+ @media (min-width: 465px) {
+ font-size: 20px;
+ }
+}
+
+.full-container {
+ padding: 0px 15px;
+ width: 100%;
+ height: calc(100% - 140px);
+ @media (max-width: 992px) {
+ height: calc(100% - 190px);
+ };
+ @media (max-width: 575px) {
+ height: calc(100% - 230px);
+ };
+}
+
+.chart {
+ max-height: 400px;
+ @media (max-width: 767.98px) {
+ max-height: 230px;
+ margin-top: -35px;
+ }
+}
+
+.bottom-padding {
+ @media (max-width: 992px) {
+ padding-bottom: 65px
+ };
+ @media (max-width: 576px) {
+ padding-bottom: 65px
+ };
+}
diff --git a/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts
new file mode 100644
index 000000000..cc8d5e759
--- /dev/null
+++ b/frontend/src/app/lightning/nodes-per-as-chart/nodes-per-as-chart.component.ts
@@ -0,0 +1,210 @@
+import { ChangeDetectionStrategy, Component, OnInit, HostBinding } from '@angular/core';
+import { EChartsOption, PieSeriesOption } from 'echarts';
+import { map, Observable, share, 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 { download } from 'src/app/shared/graphs.utils';
+
+@Component({
+ selector: 'app-nodes-per-as-chart',
+ templateUrl: './nodes-per-as-chart.component.html',
+ styleUrls: ['./nodes-per-as-chart.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NodesPerAsChartComponent implements OnInit {
+ miningWindowPreference: string;
+
+ isLoading = true;
+ chartOptions: EChartsOption = {};
+ chartInitOptions = {
+ renderer: 'svg',
+ };
+ timespan = '';
+ chartInstance: any = undefined;
+
+ @HostBinding('attr.dir') dir = 'ltr';
+
+ nodesPerAsObservable$: Observable;
+
+ constructor(
+ private apiService: ApiService,
+ private seoService: SeoService,
+ ) {
+ }
+
+ ngOnInit(): void {
+ this.seoService.setTitle($localize`Nodes per AS`);
+
+ this.nodesPerAsObservable$ = this.apiService.getNodesPerAs()
+ .pipe(
+ tap(data => {
+ this.isLoading = false;
+ this.prepareChartOptions(data);
+ }),
+ map(data => {
+ for (let i = 0; i < data.length; ++i) {
+ data[i].rank = i + 1;
+ }
+ return data.slice(0, 100);
+ }),
+ share()
+ );
+ }
+
+ generateChartSerieData(as) {
+ const shareThreshold = this.isMobile() ? 2 : 1;
+ const data: object[] = [];
+ let totalShareOther = 0;
+ let totalNodeOther = 0;
+
+ let edgeDistance: string | number = '10%';
+ if (this.isMobile()) {
+ edgeDistance = 0;
+ }
+
+ as.forEach((as) => {
+ if (as.share < shareThreshold) {
+ totalShareOther += as.share;
+ totalNodeOther += as.count;
+ return;
+ }
+ data.push({
+ value: as.share,
+ name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
+ label: {
+ overflow: 'truncate',
+ color: '#b1b1b1',
+ alignTo: 'edge',
+ edgeDistance: edgeDistance,
+ },
+ tooltip: {
+ show: !this.isMobile(),
+ backgroundColor: 'rgba(17, 19, 31, 1)',
+ borderRadius: 4,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ textStyle: {
+ color: '#b1b1b1',
+ },
+ borderColor: '#000',
+ formatter: () => {
+ return `${as.name} (${as.share}%)
` +
+ $localize`${as.count.toString()} nodes`;
+ }
+ },
+ data: as.slug,
+ } as PieSeriesOption);
+ });
+
+ // 'Other'
+ data.push({
+ itemStyle: {
+ color: 'grey',
+ },
+ value: totalShareOther,
+ name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
+ label: {
+ overflow: 'truncate',
+ color: '#b1b1b1',
+ alignTo: 'edge',
+ edgeDistance: edgeDistance
+ },
+ tooltip: {
+ backgroundColor: 'rgba(17, 19, 31, 1)',
+ borderRadius: 4,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ textStyle: {
+ color: '#b1b1b1',
+ },
+ borderColor: '#000',
+ formatter: () => {
+ return `${'Other'} (${totalShareOther.toFixed(2)}%)
` +
+ totalNodeOther.toString() + ` nodes`;
+ }
+ },
+ } as PieSeriesOption);
+
+ return data;
+ }
+
+ prepareChartOptions(as) {
+ let pieSize = ['20%', '80%']; // Desktop
+ if (this.isMobile()) {
+ pieSize = ['15%', '60%'];
+ }
+
+ this.chartOptions = {
+ color: chartColors,
+ tooltip: {
+ trigger: 'item',
+ textStyle: {
+ align: 'left',
+ }
+ },
+ series: [
+ {
+ zlevel: 0,
+ minShowLabelAngle: 3.6,
+ name: 'Mining pool',
+ type: 'pie',
+ radius: pieSize,
+ data: this.generateChartSerieData(as),
+ labelLine: {
+ lineStyle: {
+ width: 2,
+ },
+ length: this.isMobile() ? 1 : 20,
+ length2: this.isMobile() ? 1 : undefined,
+ },
+ label: {
+ fontSize: 14,
+ },
+ itemStyle: {
+ borderRadius: 1,
+ borderWidth: 1,
+ borderColor: '#000',
+ },
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 40,
+ shadowColor: 'rgba(0, 0, 0, 0.75)',
+ },
+ labelLine: {
+ lineStyle: {
+ width: 4,
+ }
+ }
+ }
+ }
+ ],
+ };
+ }
+
+ isMobile() {
+ return (window.innerWidth <= 767.98);
+ }
+
+ onChartInit(ec) {
+ if (this.chartInstance !== undefined) {
+ return;
+ }
+ this.chartInstance = ec;
+ }
+
+ onSaveChart() {
+ const now = new Date();
+ this.chartOptions.backgroundColor = '#11131f';
+ this.chartInstance.setOption(this.chartOptions);
+ download(this.chartInstance.getDataURL({
+ pixelRatio: 2,
+ excludeComponents: ['dataZoom'],
+ }), `ln-nodes-per-as-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
+ this.chartOptions.backgroundColor = 'none';
+ this.chartInstance.setOption(this.chartOptions);
+ }
+
+ isEllipsisActive(e) {
+ return (e.offsetWidth < e.scrollWidth);
+ }
+}
+
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts
index 8e3da7e09..48f23a94f 100644
--- a/frontend/src/app/services/api.service.ts
+++ b/frontend/src/app/services/api.service.ts
@@ -251,4 +251,7 @@ export class ApiService {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
}
+ getNodesPerAs(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/asShare');
+ }
}