[LN ISP chart] Adds toogle to order by nodes/capacity and show/hide Tor
This commit is contained in:
		
							parent
							
								
									992e3fa7b9
								
							
						
					
					
						commit
						91c39b57fe
					
				@ -98,29 +98,59 @@ class NodesApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getNodesISP() {
 | 
			
		||||
  public async $getNodesISP(groupBy: string, showTor: boolean) {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
 | 
			
		||||
      const orderBy = groupBy === 'capacity' ? `CAST(SUM(capacity) as INT)` : `COUNT(DISTINCT nodes.public_key)`;
 | 
			
		||||
      
 | 
			
		||||
      // Clearnet
 | 
			
		||||
      let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names,
 | 
			
		||||
          COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
 | 
			
		||||
        FROM nodes
 | 
			
		||||
        JOIN geo_names ON geo_names.id = nodes.as_number
 | 
			
		||||
        JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
			
		||||
        GROUP BY geo_names.names
 | 
			
		||||
        ORDER BY COUNT(DISTINCT nodes.public_key) DESC
 | 
			
		||||
      `;
 | 
			
		||||
        ORDER BY ${orderBy} DESC
 | 
			
		||||
      `;      
 | 
			
		||||
      const [nodesCountPerAS]: any = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
      query = `SELECT COUNT(*) as total FROM nodes WHERE as_number IS NOT NULL`;
 | 
			
		||||
      const [nodesWithAS]: any = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
      let total = 0;
 | 
			
		||||
      const nodesPerAs: any[] = [];
 | 
			
		||||
 | 
			
		||||
      for (const asGroup of nodesCountPerAS) {
 | 
			
		||||
        if (groupBy === 'capacity') {
 | 
			
		||||
          total += asGroup.capacity;
 | 
			
		||||
        } else {
 | 
			
		||||
          total += asGroup.nodesCount;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Tor
 | 
			
		||||
      if (showTor) {
 | 
			
		||||
        query = `SELECT COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
 | 
			
		||||
          FROM nodes
 | 
			
		||||
          JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
			
		||||
          ORDER BY ${orderBy} DESC
 | 
			
		||||
        `;      
 | 
			
		||||
        const [nodesCountTor]: any = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
        total += groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount;
 | 
			
		||||
        nodesPerAs.push({
 | 
			
		||||
          ispId: null,
 | 
			
		||||
          name: 'Tor',
 | 
			
		||||
          count: nodesCountTor[0].nodesCount,
 | 
			
		||||
          share: Math.floor((groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount) / total * 10000) / 100,
 | 
			
		||||
          capacity: nodesCountTor[0].capacity,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const as of nodesCountPerAS) {
 | 
			
		||||
        nodesPerAs.push({
 | 
			
		||||
          ispId: as.ispId,
 | 
			
		||||
          name: JSON.parse(as.names),
 | 
			
		||||
          count: as.nodesCount,
 | 
			
		||||
          share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100,
 | 
			
		||||
          share: Math.floor((groupBy === 'capacity' ? as.capacity : as.nodesCount) / total * 10000) / 100,
 | 
			
		||||
          capacity: as.capacity,
 | 
			
		||||
        })
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return nodesPerAs;
 | 
			
		||||
 | 
			
		||||
@ -9,10 +9,10 @@ class NodesRoutes {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp', this.$getNodesISP)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
 | 
			
		||||
@ -63,9 +63,18 @@ class NodesRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getNodesISP(req: Request, res: Response) {
 | 
			
		||||
  private async $getISPRanking(req: Request, res: Response): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const nodesPerAs = await nodesApi.$getNodesISP();
 | 
			
		||||
      const groupBy = req.query.groupBy as string;
 | 
			
		||||
      const showTor = req.query.showTor as string === 'true' ? true : false;
 | 
			
		||||
 | 
			
		||||
      if (!['capacity', 'node-count'].includes(groupBy)) {
 | 
			
		||||
        res.status(400).send(`groupBy must be one of 'capacity' or 'node-count'`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const nodesPerAs = await nodesApi.$getNodesISP(groupBy, showTor);
 | 
			
		||||
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
 | 
			
		||||
 | 
			
		||||
@ -140,7 +140,9 @@
 | 
			
		||||
 | 
			
		||||
  <div class="d-flex justify-content-between" *ngIf="!error">
 | 
			
		||||
    <h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
 | 
			
		||||
    <app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
 | 
			
		||||
    <div class="d-flex justify-content-end">
 | 
			
		||||
      <app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <app-nodes-channels-map *ngIf="channelsListMode === 'map' && !error" [style]="'nodepage'" [publicKey]="node.public_key">
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,11 @@
 | 
			
		||||
      <div class="spinner-border text-light"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="d-flex toggle">
 | 
			
		||||
      <app-toggle [textLeft]="'Show Tor'" [textRight]="" (toggleStatusChanged)="onTorToggleStatusChanged($event)"></app-toggle>
 | 
			
		||||
      <app-toggle [textLeft]="'Nodes'" [textRight]="'Capacity'" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <table class="table table-borderless text-center m-auto" style="max-width: 900px">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
@ -34,8 +39,9 @@
 | 
			
		||||
      <tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
 | 
			
		||||
        <tr *ngFor="let asEntry of asList">
 | 
			
		||||
          <td class="rank text-left pl-0">{{ asEntry.rank }}</td>
 | 
			
		||||
          <td class="name text-left text-truncate" style="max-width: 250px">
 | 
			
		||||
          <a [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
 | 
			
		||||
          <td class="name text-left text-truncate">
 | 
			
		||||
            <a *ngIf="asEntry.ispId" [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
 | 
			
		||||
            <span *ngIf="!asEntry.ispId">{{ asEntry.name }}</span>
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="share text-right">{{ asEntry.share }}%</td>
 | 
			
		||||
          <td class="nodes text-right">{{ asEntry.count }}</td>
 | 
			
		||||
 | 
			
		||||
@ -45,7 +45,7 @@
 | 
			
		||||
.name {
 | 
			
		||||
  width: 25%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 80%;
 | 
			
		||||
    width: 70%;
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
    padding-right: 0;
 | 
			
		||||
@ -69,7 +69,17 @@
 | 
			
		||||
.capacity {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 10%;
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toggle {
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  padding-top: 15px;
 | 
			
		||||
  @media (min-width: 576px) {
 | 
			
		||||
    padding-bottom: 15px;
 | 
			
		||||
    padding-left: 105px;
 | 
			
		||||
    padding-right: 105px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
			
		||||
import { map, Observable, share, tap } from 'rxjs';
 | 
			
		||||
import { combineLatest, map, Observable, share, 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';
 | 
			
		||||
@ -17,19 +17,19 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
  miningWindowPreference: string;
 | 
			
		||||
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
  };
 | 
			
		||||
  timespan = '';
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
  chartInstance = undefined;
 | 
			
		||||
 | 
			
		||||
  @HostBinding('attr.dir') dir = 'ltr';
 | 
			
		||||
 | 
			
		||||
  nodesPerAsObservable$: Observable<any>;
 | 
			
		||||
  groupBySubject = new Subject<boolean>();
 | 
			
		||||
  showTorSubject = new Subject<boolean>();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
@ -44,23 +44,31 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`Lightning nodes per ISP`);
 | 
			
		||||
 | 
			
		||||
    this.nodesPerAsObservable$ = this.apiService.getNodesPerAs()
 | 
			
		||||
    this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
 | 
			
		||||
      .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);
 | 
			
		||||
        switchMap((selectedFilters) => {
 | 
			
		||||
          return this.apiService.getNodesPerAs(
 | 
			
		||||
            selectedFilters[0] ? 'capacity' : 'node-count',
 | 
			
		||||
            selectedFilters[1] // Show Tor nodes
 | 
			
		||||
          )
 | 
			
		||||
            .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) {
 | 
			
		||||
  generateChartSerieData(as): PieSeriesOption[] {
 | 
			
		||||
    const shareThreshold = this.isMobile() ? 2 : 0.5;
 | 
			
		||||
    const data: object[] = [];
 | 
			
		||||
    let totalShareOther = 0;
 | 
			
		||||
@ -78,6 +86,9 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      data.push({
 | 
			
		||||
        itemStyle: {
 | 
			
		||||
          color: as.ispId === null ? '#7D4698' : undefined,
 | 
			
		||||
        },
 | 
			
		||||
        value: as.share,
 | 
			
		||||
        name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
 | 
			
		||||
        label: {
 | 
			
		||||
@ -138,14 +149,14 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(as) {
 | 
			
		||||
  prepareChartOptions(as): void {
 | 
			
		||||
    let pieSize = ['20%', '80%']; // Desktop
 | 
			
		||||
    if (this.isMobile()) {
 | 
			
		||||
      pieSize = ['15%', '60%'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      color: chartColors,
 | 
			
		||||
      color: chartColors.slice(3),
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'item',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
@ -191,18 +202,18 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
  isMobile(): boolean {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
  onChartInit(ec): void {
 | 
			
		||||
    if (this.chartInstance !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.chartInstance = ec;
 | 
			
		||||
 | 
			
		||||
    this.chartInstance.on('click', (e) => {
 | 
			
		||||
      if (e.data.data === 9999) { // "Other"
 | 
			
		||||
      if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.zone.run(() => {
 | 
			
		||||
@ -212,7 +223,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
  onSaveChart(): void {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    this.chartOptions.backgroundColor = '#11131f';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
@ -224,8 +235,12 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isEllipsisActive(e) {
 | 
			
		||||
    return (e.offsetWidth < e.scrollWidth);
 | 
			
		||||
  onTorToggleStatusChanged(e): void {
 | 
			
		||||
    this.showTorSubject.next(e);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onGroupToggleStatusChanged(e): void {
 | 
			
		||||
    this.groupBySubject.next(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -255,8 +255,9 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodesPerAs(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp');
 | 
			
		||||
  getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking'
 | 
			
		||||
      + `?groupBy=${groupBy}&showTor=${showTorNodes}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodeForCountry$(country: string): Observable<any> {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div class="d-flex align-items-center justify-content-end">
 | 
			
		||||
<div class="d-flex align-items-center">
 | 
			
		||||
  <span style="margin-bottom: 0.5rem">{{ textLeft }}</span> 
 | 
			
		||||
  <label class="switch">
 | 
			
		||||
    <input type="checkbox" (change)="onToggleStatusChanged($event)">
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core';
 | 
			
		||||
import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, AfterViewInit } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-toggle',
 | 
			
		||||
@ -6,11 +6,15 @@ import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from
 | 
			
		||||
  styleUrls: ['./toggle.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class ToggleComponent {
 | 
			
		||||
export class ToggleComponent implements AfterViewInit {
 | 
			
		||||
  @Output() toggleStatusChanged = new EventEmitter<boolean>();
 | 
			
		||||
  @Input() textLeft: string;
 | 
			
		||||
  @Input() textRight: string;
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit(): void {
 | 
			
		||||
    this.toggleStatusChanged.emit(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onToggleStatusChanged(e): void {
 | 
			
		||||
    this.toggleStatusChanged.emit(e.target.checked);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user