mempool/frontend/src/app/components/pool-ranking/pool-ranking.component.ts

312 lines
9.1 KiB
TypeScript

import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { EChartsOption, PieSeriesOption } from 'echarts';
import { combineLatest, Observable, of } from 'rxjs';
import { catchError, map, share, skip, startWith, switchMap, tap } from 'rxjs/operators';
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from '../..//services/storage.service';
import { MiningService, MiningStats } from '../../services/mining.service';
import { StateService } from '../../services/state.service';
import { chartColors, poolsColor } from 'src/app/app.constants';
@Component({
selector: 'app-pool-ranking',
templateUrl: './pool-ranking.component.html',
styleUrls: ['./pool-ranking.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PoolRankingComponent implements OnInit {
@Input() widget: boolean = false;
poolsWindowPreference: string;
radioGroupForm: FormGroup;
isLoading = true;
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
width: 'auto',
height: 'auto',
};
chartInstance: any = undefined;
miningStatsObservable$: Observable<MiningStats>;
constructor(
private stateService: StateService,
private storageService: StorageService,
private formBuilder: FormBuilder,
private miningService: MiningService,
private seoService: SeoService,
private router: Router,
) {
}
ngOnInit(): void {
if (this.widget) {
this.poolsWindowPreference = '1w';
} else {
this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`);
this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w';
}
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference });
this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference);
// When...
this.miningStatsObservable$ = combineLatest([
// ...a new block is mined
this.stateService.blocks$
.pipe(
// (we always receives some blocks at start so only trigger for the last one)
skip(this.stateService.env.MEMPOOL_BLOCKS_AMOUNT - 1),
),
// ...or we change the timespan
this.radioGroupForm.get('dateSpan').valueChanges
.pipe(
startWith(this.poolsWindowPreference), // (trigger when the page loads)
tap((value) => {
if (!this.widget) {
this.storageService.setValue('poolsWindowPreference', value);
}
this.poolsWindowPreference = value;
})
)
])
// ...then refresh the mining stats
.pipe(
switchMap(() => {
this.isLoading = true;
return this.miningService.getMiningStats(this.poolsWindowPreference)
.pipe(
catchError((e) => of(this.getEmptyMiningStat()))
);
}),
map(data => {
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
return data;
}),
tap(data => {
this.isLoading = false;
this.prepareChartOptions(data);
}),
share()
);
}
formatPoolUI(pool: SinglePoolStats) {
pool['blockText'] = pool.blockCount.toString() + ` (${pool.share}%)`;
return pool;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
generatePoolsChartSerieData(miningStats) {
const poolShareThreshold = this.isMobile() ? 2 : 1; // Do not draw pools which hashrate share is lower than that
const data: object[] = [];
let totalShareOther = 0;
let totalBlockOther = 0;
let totalEstimatedHashrateOther = 0;
let edgeDistance: any = '20%';
if (this.isMobile() && this.widget) {
edgeDistance = 0;
} else if (this.isMobile() && !this.widget || this.widget) {
edgeDistance = 20;
}
miningStats.pools.forEach((pool) => {
if (parseFloat(pool.share) < poolShareThreshold) {
totalShareOther += parseFloat(pool.share);
totalBlockOther += pool.blockCount;
totalEstimatedHashrateOther += pool.lastEstimatedHashrate;
return;
}
data.push({
itemStyle: {
color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()],
},
value: pool.share,
name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
label: {
overflow: 'none',
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: () => {
if (this.poolsWindowPreference === '24h') {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s' +
`<br>` + pool.blockCount.toString() + ` blocks`;
} else {
return `<b style="color: white">${pool.name} (${pool.share}%)</b><br>` +
pool.blockCount.toString() + ` blocks`;
}
}
},
data: pool.poolId,
} as PieSeriesOption);
});
// 'Other'
data.push({
itemStyle: {
color: 'grey',
},
value: totalShareOther,
name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`),
label: {
overflow: 'none',
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: () => {
if (this.poolsWindowPreference === '24h') {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalEstimatedHashrateOther.toString() + ' PH/s' +
`<br>` + totalBlockOther.toString() + ` blocks`;
} else {
return `<b style="color: white">${'Other'} (${totalShareOther.toFixed(2)}%)</b><br>` +
totalBlockOther.toString() + ` blocks`;
}
}
},
data: 9999 as any,
} as PieSeriesOption);
return data;
}
prepareChartOptions(miningStats) {
let network = this.stateService.network;
if (network === '') {
network = 'bitcoin';
}
network = network.charAt(0).toUpperCase() + network.slice(1);
let radius: any[] = ['20%', '80%'];
let top: any = undefined; let bottom = undefined; let height = undefined;
if (this.isMobile() && this.widget) {
radius = ['10%', '50%'];
} else if (this.isMobile() && !this.widget) {
top = 0;
height = 300;
radius = ['10%', '50%'];
} else if (this.widget) {
radius = ['15%', '60%'];
} else {
top = 35;
}
this.chartOptions = {
color: chartColors,
title: {
text: this.widget ? '' : $localize`:@@mining.pool-chart-title:${network}:NETWORK: mining pools share`,
left: 'center',
textStyle: {
color: '#FFF',
},
subtextStyle: {
color: '#CCC',
fontStyle: 'italic',
}
},
tooltip: {
trigger: 'item',
textStyle: {
align: 'left',
}
},
series: [
{
minShowLabelAngle: 3.6,
top: top,
bottom: bottom,
height: height,
name: 'Mining pool',
type: 'pie',
radius: radius,
data: this.generatePoolsChartSerieData(miningStats),
labelLine: {
lineStyle: {
width: 2,
},
},
label: {
fontSize: 14,
},
itemStyle: {
borderRadius: 2,
borderWidth: 2,
borderColor: '#000',
},
emphasis: {
itemStyle: {
shadowBlur: 40,
shadowColor: 'rgba(0, 0, 0, 0.75)',
},
labelLine: {
lineStyle: {
width: 3,
}
}
}
}
],
};
}
onChartInit(ec) {
if (this.chartInstance !== undefined) {
return;
}
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
if (e.data.data === 9999) { // "Other"
return;
}
this.router.navigate(['/mining/pool/', e.data.data]);
});
}
/**
* Default mining stats if something goes wrong
*/
getEmptyMiningStat() {
return {
lastEstimatedHashrate: 'Error',
blockCount: 0,
totalEmptyBlock: 0,
totalEmptyBlockRatio: '',
pools: [],
availableTimespanDay: 0,
miningUnits: {
hashrateDivider: 1,
hashrateUnit: '',
},
};
}
}