Add LN node per country graph
This commit is contained in:
		
							parent
							
								
									75f1b52a2a
								
							
						
					
					
						commit
						63ebace378
					
				@ -186,6 +186,39 @@ class NodesApi {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getNodesCountries() {
 | 
			
		||||
    try {
 | 
			
		||||
      let query = `SELECT geo_names.names as names, geo_names_iso.names as iso_code, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
 | 
			
		||||
        FROM nodes
 | 
			
		||||
        JOIN geo_names ON geo_names.id = nodes.country_id AND geo_names.type = 'country'
 | 
			
		||||
        JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
 | 
			
		||||
        JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
			
		||||
        GROUP BY country_id
 | 
			
		||||
        ORDER BY COUNT(DISTINCT nodes.public_key) DESC
 | 
			
		||||
      `;
 | 
			
		||||
      const [nodesCountPerCountry]: any = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
      query = `SELECT COUNT(*) as total FROM nodes WHERE country_id IS NOT NULL`;
 | 
			
		||||
      const [nodesWithAS]: any = await DB.query(query);
 | 
			
		||||
 | 
			
		||||
      const nodesPerCountry: any[] = [];
 | 
			
		||||
      for (const country of nodesCountPerCountry) {
 | 
			
		||||
        nodesPerCountry.push({
 | 
			
		||||
          name: JSON.parse(country.names),
 | 
			
		||||
          iso: country.iso_code, 
 | 
			
		||||
          count: country.nodesCount,
 | 
			
		||||
          share: Math.floor(country.nodesCount / nodesWithAS[0].total * 10000) / 100,
 | 
			
		||||
          capacity: country.capacity,
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return nodesPerCountry;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err(`Cannot get nodes grouped by AS. Reason: ${e instanceof Error ? e.message : e}`);
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NodesApi();
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ class NodesRoutes {
 | 
			
		||||
      .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/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)
 | 
			
		||||
    ;
 | 
			
		||||
@ -128,6 +129,18 @@ class NodesRoutes {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getNodesCountries(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const nodesPerAs = await nodesApi.$getNodesCountries();
 | 
			
		||||
      res.header('Pragma', 'public');
 | 
			
		||||
      res.header('Cache-control', 'public');
 | 
			
		||||
      res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
 | 
			
		||||
      res.json(nodesPerAs);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new NodesRoutes();
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,8 @@
 | 
			
		||||
        i18n="lightning.capacity">Network capacity</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.nodes-per-isp">Lightning nodes per ISP</a>
 | 
			
		||||
      <a class="dropdown-item" routerLinkActive="active" [routerLink]="['/graphs/lightning/nodes-per-country' | relativeUrl]"
 | 
			
		||||
        i18n="lightning.nodes-per-isp">Lightning nodes per country</a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ 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 { NodesPerISPChartComponent } from '../lightning/nodes-per-isp-chart/nodes-per-isp-chart.component';
 | 
			
		||||
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
 | 
			
		||||
 | 
			
		||||
const browserWindow = window || {};
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
@ -104,6 +105,10 @@ const routes: Routes = [
 | 
			
		||||
            path: 'lightning/nodes-per-isp',
 | 
			
		||||
            component: NodesPerISPChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'lightning/nodes-per-country',
 | 
			
		||||
            component: NodesPerCountryChartComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: '',
 | 
			
		||||
            redirectTo: 'mempool',
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,7 @@ import { ChannelsStatisticsComponent } from './channels-statistics/channels-stat
 | 
			
		||||
import { NodesPerISPChartComponent } from './nodes-per-isp-chart/nodes-per-isp-chart.component';
 | 
			
		||||
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
 | 
			
		||||
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
 | 
			
		||||
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    LightningDashboardComponent,
 | 
			
		||||
@ -39,6 +40,7 @@ import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
 | 
			
		||||
    NodesPerISPChartComponent,
 | 
			
		||||
    NodesPerCountry,
 | 
			
		||||
    NodesPerISP,
 | 
			
		||||
    NodesPerCountryChartComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,52 @@
 | 
			
		||||
<div class="full-container h-100">
 | 
			
		||||
 | 
			
		||||
  <div class="card-header">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
      <span i18n="lightning.nodes-per-country">Lightning nodes per country</span>
 | 
			
		||||
      <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
 | 
			
		||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div class="container pb-lg-0 bottom-padding">
 | 
			
		||||
    <div class="pb-lg-5">
 | 
			
		||||
      <div class="chart w-100" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
			
		||||
        (chartInit)="onChartInit($event)">
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="text-center loadingGraphs" *ngIf="isLoading">
 | 
			
		||||
      <div class="spinner-border text-light"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <table class="table table-borderless text-center m-auto" style="max-width: 900px">
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th class="text-left rank" *ngIf="!isMobile()" i18n="mining.rank">Rank</th>
 | 
			
		||||
          <th class="text-left name" i18n="lightning.as-name">Name</th>
 | 
			
		||||
          <th class="text-right share" *ngIf="!isMobile()" i18n="lightning.share">Share</th>
 | 
			
		||||
          <th class="text-right nodes" i18n="lightning.nodes-count">Nodes</th>
 | 
			
		||||
          <th class="text-right capacity" i18n="lightning.capacity">Capacity</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerCountryObservable$ | async) as countries">
 | 
			
		||||
        <tr *ngFor="let country of countries">
 | 
			
		||||
          <td class="text-left rank" *ngIf="!isMobile()">{{ country.rank }}</td>
 | 
			
		||||
          <td class="text-left text-truncate name">{{ country.name.en }}</td>
 | 
			
		||||
          <td class="text-right share" *ngIf="!isMobile()">{{ country.share }}%</td>
 | 
			
		||||
          <td class="text-right nodes">{{ country.count }}</td>
 | 
			
		||||
          <td class="text-right capacity">
 | 
			
		||||
            <app-amount *ngIf="country.capacity > 100000000; else smallchannel" [satoshis]="country.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
			
		||||
            <ng-template #smallchannel>
 | 
			
		||||
              {{ country.capacity | amountShortener: 1 }}
 | 
			
		||||
              <span class="sats" i18n="shared.sats">sats</span>
 | 
			
		||||
            </ng-template>
 | 
			
		||||
          </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
.sats {
 | 
			
		||||
  color: #ffffff66;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rank {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.name {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 80%;
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
    padding-left: 0;
 | 
			
		||||
    padding-right: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.share {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nodes {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 10%;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.capacity {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 10%;
 | 
			
		||||
    max-width: 100px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,231 @@
 | 
			
		||||
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 { 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 { 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';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-country-chart',
 | 
			
		||||
  templateUrl: './nodes-per-country-chart.component.html',
 | 
			
		||||
  styleUrls: ['./nodes-per-country-chart.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesPerCountryChartComponent implements OnInit {
 | 
			
		||||
  miningWindowPreference: string;
 | 
			
		||||
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
  chartInitOptions = {
 | 
			
		||||
    renderer: 'svg',
 | 
			
		||||
  };
 | 
			
		||||
  timespan = '';
 | 
			
		||||
  chartInstance: any = undefined;
 | 
			
		||||
 | 
			
		||||
  @HostBinding('attr.dir') dir = 'ltr';
 | 
			
		||||
 | 
			
		||||
  nodesPerCountryObservable$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private amountShortenerPipe: AmountShortenerPipe,
 | 
			
		||||
    private zone: NgZone,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`Lightning nodes per country`);
 | 
			
		||||
 | 
			
		||||
    this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry()
 | 
			
		||||
      .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(country) {
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    country.forEach((country) => {
 | 
			
		||||
      if (country.share < shareThreshold) {
 | 
			
		||||
        totalShareOther += country.share;
 | 
			
		||||
        totalNodeOther += country.count;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      data.push({
 | 
			
		||||
        value: country.share,
 | 
			
		||||
        name: country.name.en + (this.isMobile() ? `` : ` (${country.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 `<b style="color: white">${country.name.en} (${country.share}%)</b><br>` +
 | 
			
		||||
              $localize`${country.count.toString()} nodes<br>` +
 | 
			
		||||
              $localize`${this.amountShortenerPipe.transform(country.capacity / 100000000, 2)} BTC capacity`
 | 
			
		||||
            ;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        data: country.iso,
 | 
			
		||||
      } 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 `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
 | 
			
		||||
            totalNodeOther.toString() + ` nodes`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    } as PieSeriesOption);
 | 
			
		||||
 | 
			
		||||
    return data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(country) {
 | 
			
		||||
    let pieSize = ['20%', '80%']; // Desktop
 | 
			
		||||
    if (this.isMobile()) {
 | 
			
		||||
      pieSize = ['15%', '60%'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
      animation: false,
 | 
			
		||||
      color: chartColors,
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        trigger: 'item',
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      series: [
 | 
			
		||||
        {
 | 
			
		||||
          zlevel: 0,
 | 
			
		||||
          minShowLabelAngle: 3.6,
 | 
			
		||||
          name: 'Mining pool',
 | 
			
		||||
          type: 'pie',
 | 
			
		||||
          radius: pieSize,
 | 
			
		||||
          data: this.generateChartSerieData(country),
 | 
			
		||||
          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;
 | 
			
		||||
 | 
			
		||||
    this.chartInstance.on('click', (e) => {
 | 
			
		||||
      if (e.data.data === 9999) { // "Other"
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.zone.run(() => {
 | 
			
		||||
        const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/country/${e.data.data}`);
 | 
			
		||||
        this.router.navigate([url]);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
    this.chartOptions.backgroundColor = '#11131f';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
    download(this.chartInstance.getDataURL({
 | 
			
		||||
      pixelRatio: 2,
 | 
			
		||||
      excludeComponents: ['dataZoom'],
 | 
			
		||||
    }), `lightning-nodes-per-country-${Math.round(now.getTime() / 1000)}.svg`);
 | 
			
		||||
    this.chartOptions.backgroundColor = 'none';
 | 
			
		||||
    this.chartInstance.setOption(this.chartOptions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isEllipsisActive(e) {
 | 
			
		||||
    return (e.offsetWidth < e.scrollWidth);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,6 @@
 | 
			
		||||
  width: 30%;
 | 
			
		||||
  max-width: 400px;
 | 
			
		||||
  padding-right: 70px;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 50%;
 | 
			
		||||
    max-width: 150px;
 | 
			
		||||
@ -23,7 +22,6 @@
 | 
			
		||||
 | 
			
		||||
.timestamp-first {
 | 
			
		||||
  width: 20%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
@ -31,7 +29,6 @@
 | 
			
		||||
 | 
			
		||||
.timestamp-update {
 | 
			
		||||
  width: 16%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
@ -39,7 +36,6 @@
 | 
			
		||||
 | 
			
		||||
.capacity {
 | 
			
		||||
  width: 10%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
@ -47,7 +43,6 @@
 | 
			
		||||
 | 
			
		||||
.channels {
 | 
			
		||||
  width: 10%;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    width: 25%;
 | 
			
		||||
  }
 | 
			
		||||
@ -55,7 +50,6 @@
 | 
			
		||||
 | 
			
		||||
.city {
 | 
			
		||||
  max-width: 150px;
 | 
			
		||||
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -262,4 +262,8 @@ export class ApiService {
 | 
			
		||||
  getNodeForISP$(isp: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp/' + isp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNodesPerCountry(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user