widgetify block fee rates chart
This commit is contained in:
parent
4169e1053f
commit
42a3a380d5
@ -1,13 +1,13 @@
|
|||||||
<app-indexing-progress></app-indexing-progress>
|
<app-indexing-progress *ngIf="!widget"></app-indexing-progress>
|
||||||
|
|
||||||
<div class="full-container">
|
<div [class.full-container]="!widget">
|
||||||
<div class="card-header mb-0 mb-md-4">
|
<div *ngIf="!widget" class="card-header mb-0 mb-md-4">
|
||||||
<div class="d-flex d-md-block align-items-baseline">
|
<div class="d-flex d-md-block align-items-baseline">
|
||||||
<span i18n="mining.block-fee-rates">Block Fee Rates</span>
|
<span i18n="mining.block-fee-rates">Block Fee Rates</span>
|
||||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px" (click)="onSaveChart()">
|
||||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
<form [formGroup]="radioGroupForm" class="formRadioGroup" *ngIf="(statsObservable$ | async) as stats">
|
||||||
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
|
<div class="btn-group btn-group-toggle" name="radioBasic" [class]="{'disabled': isLoading}">
|
||||||
@ -45,11 +45,45 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
<div *ngIf="widget">
|
||||||
|
<div class="block-fee-rates" *ngIf="(statsObservable$ | async) as stats; else loadingStats">
|
||||||
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="mining.avg-block-fee-1m">Avg Block Fee (1m)</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<app-fee-rate [fee]="stats.avgMedianRate"></app-fee-rate>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="block.???">???</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
???
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||||
(chartInit)="onChartInit($event)">
|
(chartInit)="onChartInit($event)">
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||||
<div class="spinner-border text-light"></div>
|
<div class="spinner-border text-light"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #loadingStats>
|
||||||
|
<div class="block-fee-rates">
|
||||||
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="mining.avg-block-fee">Avg Block Fee</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<h5 class="card-title" i18n="block.???">???</h5>
|
||||||
|
<p class="card-text">
|
||||||
|
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -57,7 +57,54 @@
|
|||||||
.chart-widget {
|
.chart-widget {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-height: 270px;
|
max-height: 238px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-fee-rates {
|
||||||
|
min-height: 56px;
|
||||||
|
display: block;
|
||||||
|
@media (min-width: 485px) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
width: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0px auto 20px;
|
||||||
|
&:nth-child(2) {
|
||||||
|
order: 2;
|
||||||
|
@media (min-width: 485px) {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
order: 3;
|
||||||
|
@media (min-width: 485px) {
|
||||||
|
order: 2;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #4a68b9;
|
||||||
|
}
|
||||||
|
.card-text {
|
||||||
|
font-size: 18px;
|
||||||
|
span {
|
||||||
|
color: #ffffff66;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.formRadioGroup {
|
.formRadioGroup {
|
||||||
@ -85,6 +132,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-loader {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
max-width: 80px;
|
||||||
|
margin: 15px auto 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.disabled {
|
.disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||||
import { EChartsOption } from '../../graphs/echarts';
|
import { EChartsOption, graphic } from 'echarts';
|
||||||
import { Observable, Subscription, combineLatest } from 'rxjs';
|
import { Observable, Subscription, combineLatest, of } from 'rxjs';
|
||||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||||
import { ApiService } from '../../services/api.service';
|
import { ApiService } from '../../services/api.service';
|
||||||
import { SeoService } from '../../services/seo.service';
|
import { SeoService } from '../../services/seo.service';
|
||||||
@ -29,6 +29,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class BlockFeeRatesGraphComponent implements OnInit {
|
export class BlockFeeRatesGraphComponent implements OnInit {
|
||||||
|
@Input() widget = false;
|
||||||
@Input() right: number | string = 45;
|
@Input() right: number | string = 45;
|
||||||
@Input() left: number | string = 75;
|
@Input() left: number | string = 75;
|
||||||
|
|
||||||
@ -57,39 +58,48 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private zone: NgZone,
|
private zone: NgZone,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
) {
|
) {
|
||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||||
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
this.radioGroupForm.controls.dateSpan.setValue('1y');
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
|
if (this.widget) {
|
||||||
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
|
this.miningWindowPreference = '1m';
|
||||||
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
} else {
|
||||||
|
this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`);
|
||||||
|
this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`);
|
||||||
|
this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
|
||||||
|
}
|
||||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||||
|
|
||||||
this.route
|
if (!this.widget) {
|
||||||
.fragment
|
this.route
|
||||||
.subscribe((fragment) => {
|
.fragment
|
||||||
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
|
.subscribe((fragment) => {
|
||||||
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) {
|
||||||
}
|
this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false });
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.statsObservable$ = combineLatest([
|
this.statsObservable$ = combineLatest([
|
||||||
this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
|
this.widget ? of(this.miningWindowPreference) : this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)),
|
||||||
this.stateService.rateUnits$
|
this.stateService.rateUnits$
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(([timespan, rateUnits]) => {
|
switchMap(([timespan, rateUnits]) => {
|
||||||
this.storageService.setValue('miningWindowPreference', timespan);
|
if (!this.widget) {
|
||||||
|
this.storageService.setValue('miningWindowPreference', timespan);
|
||||||
|
}
|
||||||
this.timespan = timespan;
|
this.timespan = timespan;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
return this.apiService.getHistoricalBlockFeeRates$(timespan)
|
return this.apiService.getHistoricalBlockFeeRates$(timespan)
|
||||||
.pipe(
|
.pipe(
|
||||||
tap((response) => {
|
tap((response) => {
|
||||||
// Group by percentile
|
// Group by percentile
|
||||||
const seriesData = {
|
const seriesData = this.widget ? { 'Median': [] } : {
|
||||||
'Min': [],
|
'Min': [],
|
||||||
'10th': [],
|
'10th': [],
|
||||||
'25th': [],
|
'25th': [],
|
||||||
@ -100,13 +110,17 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
};
|
};
|
||||||
for (const rate of response.body) {
|
for (const rate of response.body) {
|
||||||
const timestamp = rate.timestamp * 1000;
|
const timestamp = rate.timestamp * 1000;
|
||||||
seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
|
if (this.widget) {
|
||||||
seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
|
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
|
||||||
seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
|
} else {
|
||||||
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
|
seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]);
|
||||||
seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
|
seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]);
|
||||||
seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
|
seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]);
|
||||||
seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
|
seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]);
|
||||||
|
seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]);
|
||||||
|
seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]);
|
||||||
|
seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare chart
|
// Prepare chart
|
||||||
@ -135,15 +149,42 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.widget) {
|
||||||
|
let maResolution = 30;
|
||||||
|
const medianMa = [];
|
||||||
|
for (let i = maResolution - 1; i < seriesData['Median'].length; ++i) {
|
||||||
|
let avg = 0;
|
||||||
|
for (let y = maResolution - 1; y >= 0; --y) {
|
||||||
|
avg += seriesData['Median'][i - y][1];
|
||||||
|
}
|
||||||
|
avg /= maResolution;
|
||||||
|
medianMa.push([seriesData['Median'][i][0], avg]);
|
||||||
|
}
|
||||||
|
series.push({
|
||||||
|
zlevel: 1,
|
||||||
|
name: 'MA',
|
||||||
|
data: medianMa,
|
||||||
|
type: 'line',
|
||||||
|
showSymbol: false,
|
||||||
|
symbol: 'none',
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.prepareChartOptions({
|
this.prepareChartOptions({
|
||||||
legends: legends,
|
legends: legends,
|
||||||
series: series
|
series: series
|
||||||
}, rateUnits === 'wu');
|
}, rateUnits === 'wu');
|
||||||
|
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
this.cd.markForCheck();
|
||||||
}),
|
}),
|
||||||
map((response) => {
|
map((response) => {
|
||||||
return {
|
return {
|
||||||
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
blockCount: parseInt(response.headers.get('x-total-count'), 10),
|
||||||
|
avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -154,16 +195,22 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
|
|
||||||
prepareChartOptions(data, weightMode) {
|
prepareChartOptions(data, weightMode) {
|
||||||
this.chartOptions = {
|
this.chartOptions = {
|
||||||
color: ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
|
color: this.widget ? ['#6b6b6b', new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||||
|
{ offset: 0, color: '#F4511E' },
|
||||||
|
{ offset: 0.25, color: '#FB8C00' },
|
||||||
|
{ offset: 0.5, color: '#FFB300' },
|
||||||
|
{ offset: 0.75, color: '#FDD835' },
|
||||||
|
{ offset: 1, color: '#7CB342' }
|
||||||
|
])] : ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'],
|
||||||
animation: false,
|
animation: false,
|
||||||
grid: {
|
grid: {
|
||||||
right: this.right,
|
right: this.right,
|
||||||
left: this.left,
|
left: this.left,
|
||||||
bottom: 80,
|
bottom: this.widget ? 30 : 80,
|
||||||
top: this.isMobile() ? 10 : 50,
|
top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: !this.isMobile(),
|
show: !this.isMobile() && !this.widget,
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
axisPointer: {
|
axisPointer: {
|
||||||
type: 'line'
|
type: 'line'
|
||||||
@ -201,7 +248,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
},
|
},
|
||||||
xAxis: data.series.length === 0 ? undefined :
|
xAxis: data.series.length === 0 ? undefined :
|
||||||
{
|
{
|
||||||
name: formatterXAxisLabel(this.locale, this.timespan),
|
name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan),
|
||||||
nameLocation: 'middle',
|
nameLocation: 'middle',
|
||||||
nameTextStyle: {
|
nameTextStyle: {
|
||||||
padding: [10, 0, 0, 0],
|
padding: [10, 0, 0, 0],
|
||||||
@ -218,7 +265,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
padding: [0, 5],
|
padding: [0, 5],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
legend: (data.series.length === 0) ? undefined : {
|
legend: (this.widget || data.series.length === 0) ? undefined : {
|
||||||
padding: [10, 75],
|
padding: [10, 75],
|
||||||
data: data.legends,
|
data: data.legends,
|
||||||
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
||||||
@ -256,7 +303,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||||||
max: (val) => this.timespan === 'all' ? Math.min(val.max, 5000) : undefined,
|
max: (val) => this.timespan === 'all' ? Math.min(val.max, 5000) : undefined,
|
||||||
},
|
},
|
||||||
series: data.series,
|
series: data.series,
|
||||||
dataZoom: [{
|
dataZoom: this.widget ? null : [{
|
||||||
type: 'inside',
|
type: 'inside',
|
||||||
realtime: true,
|
realtime: true,
|
||||||
zoomLock: true,
|
zoomLock: true,
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
|
||||||
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
|
||||||
|
<<<<<<< HEAD
|
||||||
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
|
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
|
||||||
|
=======
|
||||||
|
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration } from '../interfaces/node-api.interface';
|
||||||
|
>>>>>>> 9b9adcd43 (widgetify block fee rates chart)
|
||||||
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
|
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';
|
import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user