Create stacked pools historical hashrates to see dominance over time
This commit is contained in:
@@ -29,6 +29,7 @@ import { AssetsComponent } from './components/assets/assets.component';
|
||||
import { PoolComponent } from './components/pool/pool.component';
|
||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
||||
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
|
||||
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
|
||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
|
||||
|
||||
let routes: Routes = [
|
||||
@@ -78,6 +79,10 @@ let routes: Routes = [
|
||||
path: 'hashrate',
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'hashrate/pools',
|
||||
component: HashrateChartPoolsComponent,
|
||||
},
|
||||
{
|
||||
path: 'pools',
|
||||
component: PoolRankingComponent,
|
||||
@@ -89,10 +94,6 @@ let routes: Routes = [
|
||||
path: ':poolId',
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: ':poolId/hashrate',
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -193,6 +194,10 @@ let routes: Routes = [
|
||||
path: 'hashrate',
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'hashrate/pools',
|
||||
component: HashrateChartPoolsComponent,
|
||||
},
|
||||
{
|
||||
path: 'pools',
|
||||
component: PoolRankingComponent,
|
||||
@@ -204,10 +209,6 @@ let routes: Routes = [
|
||||
path: ':poolId',
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: ':poolId/hashrate',
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -302,6 +303,10 @@ let routes: Routes = [
|
||||
path: 'hashrate',
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
{
|
||||
path: 'hashrate/pools',
|
||||
component: HashrateChartPoolsComponent,
|
||||
},
|
||||
{
|
||||
path: 'pools',
|
||||
component: PoolRankingComponent,
|
||||
@@ -313,10 +318,6 @@ let routes: Routes = [
|
||||
path: ':poolId',
|
||||
component: PoolComponent,
|
||||
},
|
||||
{
|
||||
path: ':poolId/hashrate',
|
||||
component: HashrateChartComponent,
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
@@ -71,7 +71,23 @@ export const chartColors = [
|
||||
"#263238",
|
||||
];
|
||||
|
||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
export const poolsColor = {
|
||||
'foundryusa': '#D81B60',
|
||||
'antpool': '#8E24AA',
|
||||
'f2pool': '#5E35B1',
|
||||
'poolin': '#3949AB',
|
||||
'binancepool': '#1E88E5',
|
||||
'viabtc': '#039BE5',
|
||||
'btccom': '#00ACC1',
|
||||
'slushpool': '#00897B',
|
||||
'sbicrypto': '#43A047',
|
||||
'marapool': '#7CB342',
|
||||
'luxor': '#C0CA33',
|
||||
'unknown': '#FDD835',
|
||||
'okkong': '#FFB300',
|
||||
}
|
||||
|
||||
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
|
||||
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
|
||||
|
||||
export interface Language {
|
||||
|
||||
@@ -71,6 +71,7 @@ import { AssetGroupComponent } from './components/assets/asset-group/asset-group
|
||||
import { AssetCirculationComponent } from './components/asset-circulation/asset-circulation.component';
|
||||
import { MiningDashboardComponent } from './components/mining-dashboard/mining-dashboard.component';
|
||||
import { HashrateChartComponent } from './components/hashrate-chart/hashrate-chart.component';
|
||||
import { HashrateChartPoolsComponent } from './components/hashrates-chart-pools/hashrate-chart-pools.component';
|
||||
import { MiningStartComponent } from './components/mining-start/mining-start.component';
|
||||
import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
|
||||
|
||||
@@ -126,6 +127,7 @@ import { AmountShortenerPipe } from './shared/pipes/amount-shortener.pipe';
|
||||
AssetCirculationComponent,
|
||||
MiningDashboardComponent,
|
||||
HashrateChartComponent,
|
||||
HashrateChartPoolsComponent,
|
||||
MiningStartComponent,
|
||||
AmountShortenerPipe,
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div [class]="widget === false ? 'full-container' : ''">
|
||||
|
||||
<div class="card-header mb-0 mb-lg-4" [style]="widget ? 'display:none' : ''">
|
||||
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<div [class]="widget === false ? 'full-container' : ''">
|
||||
|
||||
<div class="card-header mb-0 mb-md-4" [style]="widget ? 'display:none' : ''">
|
||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(hashrateObservable$ | async) as hashrates">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 90">
|
||||
<input ngbButton type="radio" [value]="'3m'"> 3M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 180">
|
||||
<input ngbButton type="radio" [value]="'6m'"> 6M
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 365">
|
||||
<input ngbButton type="radio" [value]="'1y'"> 1Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 730">
|
||||
<input ngbButton type="radio" [value]="'2y'"> 2Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm" *ngIf="hashrates.availableTimespanDay >= 1095">
|
||||
<input ngbButton type="radio" [value]="'3y'"> 3Y
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="'all'"> ALL
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div *ngIf="hashrateObservable$ | async" [class]="!widget ? 'chart' : 'chart-widget'"
|
||||
echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="mt-3" *ngIf="!widget">
|
||||
<table class="table table-borderless table-sm text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th i18n="mining.rank">Block</th>
|
||||
<th class="d-none d-md-block" i18n="block.timestamp">Timestamp</th>
|
||||
<th i18n="mining.adjusted">Adjusted</th>
|
||||
<th i18n="mining.difficulty">Difficulty</th>
|
||||
<th i18n="mining.change">Change</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody *ngIf="(hashrateObservable$ | async) as data">
|
||||
<tr *ngFor="let diffChange of data.difficulty">
|
||||
<td><a [routerLink]="['/block' | relativeUrl, diffChange.height]">{{ diffChange.height }}</a></td>
|
||||
<td class="d-none d-md-block">‎{{ diffChange.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||
<td><app-time-since [time]="diffChange.timestamp" [fastRender]="true"></app-time-since></td>
|
||||
<td class="d-none d-md-block">{{ formatNumber(diffChange.difficulty, locale, '1.2-2') }}</td>
|
||||
<td class="d-block d-md-none">{{ diffChange.difficultyShorten }}</td>
|
||||
<td [style]="diffChange.change >= 0 ? 'color: #42B747' : 'color: #B74242'">{{ formatNumber(diffChange.change, locale, '1.2-2') }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
.main-title {
|
||||
position: relative;
|
||||
color: #ffffff91;
|
||||
margin-top: -13px;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.full-container {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
@media (max-width: 992px) {
|
||||
height: calc(100% - 110px);
|
||||
};
|
||||
@media (max-width: 576px) {
|
||||
height: calc(100% - 130px);
|
||||
};
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 275px;
|
||||
}
|
||||
|
||||
.formRadioGroup {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 830px) {
|
||||
flex-direction: row;
|
||||
float: right;
|
||||
margin-top: 0px;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 9px;
|
||||
@media (min-width: 830px) {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { poolsColor } from 'src/app/app.constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashrate-chart-pools',
|
||||
templateUrl: './hashrate-chart-pools.component.html',
|
||||
styleUrls: ['./hashrate-chart-pools.component.scss'],
|
||||
styles: [`
|
||||
.loadingGraphs {
|
||||
position: absolute;
|
||||
top: 38%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class HashrateChartPoolsComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() right: number | string = 40;
|
||||
@Input() left: number | string = 25;
|
||||
|
||||
radioGroupForm: FormGroup;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
|
||||
hashrateObservable$: Observable<any>;
|
||||
isLoading = true;
|
||||
formatNumber = formatNumber;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`);
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith('1y'),
|
||||
switchMap((timespan) => {
|
||||
return this.apiService.getHistoricalPoolsHashrate$(timespan)
|
||||
.pipe(
|
||||
tap((data: any) => {
|
||||
const grouped = {};
|
||||
for (const hashrate of data.hashrates) {
|
||||
if (!grouped.hasOwnProperty(hashrate.poolName)) {
|
||||
grouped[hashrate.poolName] = [];
|
||||
}
|
||||
grouped[hashrate.poolName].push(hashrate);
|
||||
}
|
||||
|
||||
const series = [];
|
||||
const legends = [];
|
||||
|
||||
for (const name in grouped) {
|
||||
series.push({
|
||||
stack: 'Total',
|
||||
name: name,
|
||||
showSymbol: false,
|
||||
data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]),
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 0,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 1,
|
||||
},
|
||||
smooth: true,
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
emphasis: {
|
||||
disabled: true,
|
||||
scale: false,
|
||||
},
|
||||
});
|
||||
|
||||
legends.push({
|
||||
name: name,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
},
|
||||
icon: 'roundRect',
|
||||
itemStyle: {
|
||||
color: poolsColor[name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions({
|
||||
legends: legends,
|
||||
series: series
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((data: any) => {
|
||||
const availableTimespanDay = (
|
||||
(new Date().getTime() / 1000) - (data.oldestIndexedBlockTimestamp)
|
||||
) / 3600 / 24;
|
||||
return {
|
||||
availableTimespanDay: availableTimespanDay,
|
||||
hashrates: data
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(data) {
|
||||
this.chartOptions = {
|
||||
grid: {
|
||||
right: this.right,
|
||||
left: this.left,
|
||||
bottom: this.widget ? 30 : 60,
|
||||
top: this.widget ? 10 : 40,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'line'
|
||||
},
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
align: 'left',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: function (data) {
|
||||
let tooltip = `<b style="color: white; margin-left: 18px">${data[0].axisValueLabel}</b><br>`;
|
||||
data.sort((a, b) => b.data[1] - a.data[1]);
|
||||
for (const pool of data) {
|
||||
if (pool.data[1] > 0) {
|
||||
tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%<br>`
|
||||
}
|
||||
}
|
||||
return tooltip;
|
||||
}.bind(this)
|
||||
},
|
||||
xAxis: {
|
||||
type: 'time',
|
||||
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
|
||||
},
|
||||
legend: (this.isMobile() || this.widget) ? undefined : {
|
||||
data: data.legends
|
||||
},
|
||||
yAxis: {
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
color: 'rgb(110, 112, 121)',
|
||||
formatter: (val) => `${val}%`,
|
||||
},
|
||||
splitLine: {
|
||||
show: false,
|
||||
},
|
||||
type: 'value',
|
||||
max: 100,
|
||||
min: 0,
|
||||
},
|
||||
series: data.series,
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
}
|
||||
@@ -30,5 +30,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- pools hashrate -->
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<a class="link" href="" [routerLink]="['/mining/hashrate/pools' | relativeUrl]" i18n="mining.pools-hashrate">
|
||||
Pools Hashrate Share (1y)
|
||||
</a>
|
||||
</h5>
|
||||
<app-hashrate-chart-pools [widget]=true></app-hashrate-chart-pools>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -163,4 +163,11 @@ export class ApiService {
|
||||
(interval !== undefined ? `/${interval}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
getHistoricalPoolsHashrate$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/hashrate/pools` +
|
||||
(interval !== undefined ? `/${interval}` : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user