2023-07-20 16:25:09 +09:00
import { ChangeDetectionStrategy , ChangeDetectorRef , Component , Inject , Input , LOCALE_ID , NgZone , OnInit } from '@angular/core' ;
2024-03-20 09:57:05 +00:00
import { echarts , EChartsOption } from '../../graphs/echarts' ;
2023-07-21 14:24:36 +09:00
import { Observable , combineLatest , of } from 'rxjs' ;
2022-04-15 00:21:38 +09:00
import { map , share , startWith , switchMap , tap } from 'rxjs/operators' ;
2022-09-21 17:23:45 +02:00
import { ApiService } from '../../services/api.service' ;
import { SeoService } from '../../services/seo.service' ;
2022-04-15 00:21:38 +09:00
import { formatNumber } from '@angular/common' ;
2022-11-28 11:55:23 +09:00
import { UntypedFormBuilder , UntypedFormGroup } from '@angular/forms' ;
2022-09-21 17:23:45 +02:00
import { download , formatterXAxis , formatterXAxisLabel , formatterXAxisTimeCategory } from '../../shared/graphs.utils' ;
import { StorageService } from '../../services/storage.service' ;
import { MiningService } from '../../services/mining.service' ;
import { selectPowerOfTen } from '../../bitcoin.utils' ;
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe' ;
import { StateService } from '../../services/state.service' ;
2022-06-15 11:26:58 +02:00
import { ActivatedRoute , Router } from '@angular/router' ;
2022-04-15 00:21:38 +09:00
@Component ( {
selector : 'app-block-fee-rates-graph' ,
templateUrl : './block-fee-rates-graph.component.html' ,
styleUrls : [ './block-fee-rates-graph.component.scss' ] ,
styles : [ `
. loadingGraphs {
position : absolute ;
top : 50 % ;
left : calc ( 50 % - 15 px ) ;
2024-07-26 00:00:14 +02:00
z - index : 99 ;
2022-04-15 00:21:38 +09:00
}
` ],
changeDetection : ChangeDetectionStrategy.OnPush ,
} )
export class BlockFeeRatesGraphComponent implements OnInit {
2023-07-20 16:25:09 +09:00
@Input ( ) widget = false ;
2022-04-15 00:21:38 +09:00
@Input ( ) right : number | string = 45 ;
@Input ( ) left : number | string = 75 ;
miningWindowPreference : string ;
2022-11-28 11:55:23 +09:00
radioGroupForm : UntypedFormGroup ;
2022-04-15 00:21:38 +09:00
chartOptions : EChartsOption = { } ;
chartInitOptions = {
renderer : 'svg' ,
} ;
2023-08-30 22:25:33 +09:00
hrStatsObservable$ : Observable < any > ;
2022-04-15 00:21:38 +09:00
statsObservable$ : Observable < any > ;
isLoading = true ;
formatNumber = formatNumber ;
timespan = '' ;
2022-04-15 00:34:15 +09:00
chartInstance : any = undefined ;
2022-04-15 00:21:38 +09:00
constructor (
@Inject ( LOCALE_ID ) public locale : string ,
private seoService : SeoService ,
private apiService : ApiService ,
2022-11-28 11:55:23 +09:00
private formBuilder : UntypedFormBuilder ,
2022-04-15 00:21:38 +09:00
private storageService : StorageService ,
2022-04-15 00:34:15 +09:00
private miningService : MiningService ,
2023-11-02 01:29:55 +00:00
public stateService : StateService ,
2022-04-15 00:34:15 +09:00
private router : Router ,
private zone : NgZone ,
2022-06-15 11:26:58 +02:00
private route : ActivatedRoute ,
2023-07-20 16:25:09 +09:00
private cd : ChangeDetectorRef ,
2022-04-15 00:21:38 +09:00
) {
this . radioGroupForm = this . formBuilder . group ( { dateSpan : '1y' } ) ;
this . radioGroupForm . controls . dateSpan . setValue ( '1y' ) ;
}
ngOnInit ( ) : void {
2023-07-20 16:25:09 +09:00
if ( this . widget ) {
this . miningWindowPreference = '1m' ;
} 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' ) ;
}
2022-04-15 00:21:38 +09:00
this . radioGroupForm = this . formBuilder . group ( { dateSpan : this.miningWindowPreference } ) ;
this . radioGroupForm . controls . dateSpan . setValue ( this . miningWindowPreference ) ;
2023-07-20 16:25:09 +09:00
if ( ! this . widget ) {
this . route
. fragment
. subscribe ( ( fragment ) = > {
if ( [ '24h' , '3d' , '1w' , '1m' , '3m' , '6m' , '1y' , '2y' , '3y' , 'all' ] . indexOf ( fragment ) > - 1 ) {
this . radioGroupForm . controls . dateSpan . setValue ( fragment , { emitEvent : false } ) ;
}
} ) ;
}
2022-06-15 11:26:58 +02:00
2023-08-30 22:25:33 +09:00
this . hrStatsObservable $ = combineLatest ( [
this . apiService . getHistoricalBlockFeeRates $ ( '24h' ) ,
this . stateService . rateUnits $
] ) . pipe (
map ( ( [ response , rateUnits ] ) = > {
return {
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 ,
} ;
} ) ,
share ( ) ,
) ;
2023-06-15 18:56:34 -04:00
this . statsObservable $ = combineLatest ( [
2023-07-20 16:25:09 +09:00
this . widget ? of ( this . miningWindowPreference ) : this . radioGroupForm . get ( 'dateSpan' ) . valueChanges . pipe ( startWith ( this . radioGroupForm . controls . dateSpan . value ) ) ,
2023-06-15 18:56:34 -04:00
this . stateService . rateUnits $
] ) . pipe (
switchMap ( ( [ timespan , rateUnits ] ) = > {
2023-07-20 16:25:09 +09:00
if ( ! this . widget ) {
this . storageService . setValue ( 'miningWindowPreference' , timespan ) ;
}
2022-04-15 00:21:38 +09:00
this . timespan = timespan ;
this . isLoading = true ;
return this . apiService . getHistoricalBlockFeeRates $ ( timespan )
. pipe (
2022-05-20 10:07:57 +02:00
tap ( ( response ) = > {
2022-04-15 00:21:38 +09:00
// Group by percentile
2023-07-20 16:25:09 +09:00
const seriesData = this . widget ? { 'Median' : [ ] } : {
2022-04-15 00:21:38 +09:00
'Min' : [ ] ,
'10th' : [ ] ,
'25th' : [ ] ,
'Median' : [ ] ,
'75th' : [ ] ,
'90th' : [ ] ,
'Max' : [ ]
} ;
2022-05-20 10:07:57 +02:00
for ( const rate of response . body ) {
2022-04-15 00:21:38 +09:00
const timestamp = rate . timestamp * 1000 ;
2023-07-20 16:25:09 +09:00
if ( this . widget ) {
seriesData [ 'Median' ] . push ( [ timestamp , rate . avgFee_50 , rate . avgHeight ] ) ;
} else {
seriesData [ 'Min' ] . push ( [ timestamp , rate . avgFee_0 , rate . avgHeight ] ) ;
seriesData [ '10th' ] . push ( [ timestamp , rate . avgFee_10 , rate . avgHeight ] ) ;
seriesData [ '25th' ] . push ( [ timestamp , rate . avgFee_25 , 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 ] ) ;
}
2022-04-15 00:21:38 +09:00
}
// Prepare chart
const series = [ ] ;
const legends = [ ] ;
for ( const percentile in seriesData ) {
series . push ( {
zlevel : 0 ,
stack : 'Total' ,
name : percentile ,
data : seriesData [ percentile ] ,
type : 'bar' ,
barWidth : '100%' ,
large : true ,
} ) ;
legends . push ( {
name : percentile ,
inactiveColor : 'rgb(110, 112, 121)' ,
textStyle : {
color : 'white' ,
} ,
icon : 'roundRect' ,
enabled : false ,
selected : false ,
} ) ;
}
2023-07-20 16:25:09 +09:00
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 ;
2023-07-21 14:24:36 +09:00
medianMa . push ( [ seriesData [ 'Median' ] [ i ] [ 0 ] , avg , seriesData [ 'Median' ] [ i ] [ 2 ] ] ) ;
2023-07-20 16:25:09 +09:00
}
series . push ( {
zlevel : 1 ,
2023-07-21 14:24:36 +09:00
name : 'Moving average' ,
2023-07-20 16:25:09 +09:00
data : medianMa ,
type : 'line' ,
showSymbol : false ,
symbol : 'none' ,
lineStyle : {
width : 3 ,
}
} ) ;
}
2022-04-15 00:21:38 +09:00
this . prepareChartOptions ( {
legends : legends ,
2023-06-15 18:56:34 -04:00
series : series
} , rateUnits === 'wu' ) ;
2023-07-20 16:25:09 +09:00
2022-04-15 00:21:38 +09:00
this . isLoading = false ;
2023-07-20 16:25:09 +09:00
this . cd . markForCheck ( ) ;
2022-04-15 00:21:38 +09:00
} ) ,
2022-05-20 10:07:57 +02:00
map ( ( response ) = > {
2022-04-15 00:21:38 +09:00
return {
2022-05-20 10:07:57 +02:00
blockCount : parseInt ( response . headers . get ( 'x-total-count' ) , 10 ) ,
2023-07-20 16:25:09 +09:00
avgMedianRate : response.body.length ? response . body . reduce ( ( acc , rate ) = > acc + rate . avgFee_50 , 0 ) / response.body.length : 0 ,
2022-04-15 00:21:38 +09:00
} ;
} ) ,
) ;
} ) ,
share ( )
) ;
}
2023-06-15 18:56:34 -04:00
prepareChartOptions ( data , weightMode ) {
2022-04-15 00:21:38 +09:00
this . chartOptions = {
2024-03-20 09:57:05 +00:00
color : this.widget ? [ '#6b6b6b' , new echarts . graphic . LinearGradient ( 0 , 0 , 0 , 0.65 , [
2023-07-20 16:25:09 +09:00
{ 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' ] ,
2022-04-15 00:21:38 +09:00
animation : false ,
grid : {
right : this.right ,
left : this.left ,
2023-07-20 16:25:09 +09:00
bottom : this.widget ? 30 : 80 ,
top : this.widget ? 20 : ( this . isMobile ( ) ? 10 : 50 ) ,
2022-04-15 00:21:38 +09:00
} ,
tooltip : {
2023-07-21 14:13:36 +09:00
show : ! this . isMobile ( ) ,
2022-04-15 00:21:38 +09:00
trigger : 'axis' ,
axisPointer : {
type : 'line'
} ,
backgroundColor : 'rgba(17, 19, 31, 1)' ,
borderRadius : 4 ,
shadowColor : 'rgba(0, 0, 0, 0.5)' ,
textStyle : {
2023-01-03 05:24:14 -06:00
color : 'var(--tooltip-grey)' ,
2022-04-15 00:21:38 +09:00
align : 'left' ,
} ,
borderColor : '#000' ,
2022-07-06 22:27:45 +02:00
formatter : function ( data ) {
2022-04-15 00:21:38 +09:00
if ( data . length <= 0 ) {
return '' ;
}
2022-06-10 23:34:13 +02:00
let tooltip = ` <b style="color: white; margin-left: 2px"> ${ formatterXAxis ( this . locale , this . timespan , parseInt ( data [ 0 ] . axisValue , 10 ) ) } </b><br> ` ;
2022-04-15 00:21:38 +09:00
2022-06-06 10:14:40 +02:00
for ( const rate of data . reverse ( ) ) {
2023-06-15 18:56:34 -04:00
if ( weightMode ) {
2023-07-21 14:24:36 +09:00
tooltip += ` ${ rate . marker } ${ rate . seriesName } : ${ ( rate . data [ 1 ] / 4 ) . toFixed ( 2 ) } sats/WU<br> ` ;
2023-06-15 18:56:34 -04:00
} else {
2023-07-21 14:24:36 +09:00
tooltip += ` ${ rate . marker } ${ rate . seriesName } : ${ rate . data [ 1 ] . toFixed ( 2 ) } sats/vByte<br> ` ;
2023-06-15 18:56:34 -04:00
}
2022-04-15 00:21:38 +09:00
}
if ( [ '24h' , '3d' ] . includes ( this . timespan ) ) {
2022-06-10 23:34:13 +02:00
tooltip += ` <small> ` + $localize ` At block: ${ data [ 0 ] . data [ 2 ] } ` + ` </small> ` ;
2022-04-15 00:21:38 +09:00
} else {
2022-06-10 23:34:13 +02:00
tooltip += ` <small> ` + $localize ` Around block: ${ data [ 0 ] . data [ 2 ] } ` + ` </small> ` ;
2022-04-15 00:21:38 +09:00
}
return tooltip ;
} . bind ( this )
} ,
2022-04-15 19:39:27 +09:00
xAxis : data.series.length === 0 ? undefined :
{
2023-07-20 16:25:09 +09:00
name : this.widget ? undefined : formatterXAxisLabel ( this . locale , this . timespan ) ,
2022-04-15 19:39:27 +09:00
nameLocation : 'middle' ,
nameTextStyle : {
padding : [ 10 , 0 , 0 , 0 ] ,
} ,
2022-04-15 00:21:38 +09:00
type : 'category' ,
2022-04-15 19:39:27 +09:00
boundaryGap : false ,
axisLine : { onZero : true } ,
2022-04-15 00:21:38 +09:00
axisLabel : {
2022-04-15 19:39:27 +09:00
formatter : val = > formatterXAxisTimeCategory ( this . locale , this . timespan , parseInt ( val , 10 ) ) ,
align : 'center' ,
fontSize : 11 ,
lineHeight : 12 ,
2022-04-15 00:21:38 +09:00
hideOverlap : true ,
2022-04-15 19:39:27 +09:00
padding : [ 0 , 5 ] ,
2022-04-15 00:21:38 +09:00
} ,
} ,
2023-07-20 16:25:09 +09:00
legend : ( this . widget || data . series . length === 0 ) ? undefined : {
2023-01-18 15:54:56 -06:00
padding : [ 10 , 75 ] ,
2022-04-15 00:21:38 +09:00
data : data.legends ,
2024-03-20 09:57:05 +00:00
selected : JSON.parse ( this . storageService . getValue ( 'fee_rates_legend' ) || 'null' ) ? ? {
2022-04-15 00:21:38 +09:00
'Min' : true ,
'10th' : true ,
'25th' : true ,
'Median' : true ,
'75th' : true ,
'90th' : true ,
'Max' : false ,
2022-04-15 19:39:27 +09:00
} ,
id : 4242 ,
2022-04-15 00:21:38 +09:00
} ,
yAxis : data.series.length === 0 ? undefined : {
position : 'left' ,
axisLabel : {
color : 'rgb(110, 112, 121)' ,
formatter : ( val ) = > {
2023-06-15 18:56:34 -04:00
if ( weightMode ) {
val /= 4 ;
}
2022-04-15 00:21:38 +09:00
const selectedPowerOfTen : any = selectPowerOfTen ( val ) ;
const newVal = Math . round ( val / selectedPowerOfTen . divider ) ;
2023-06-15 18:56:34 -04:00
return ` ${ newVal } ${ selectedPowerOfTen . unit } s/ ${ weightMode ? 'WU' : 'vB' } ` ;
2022-04-15 00:21:38 +09:00
} ,
} ,
splitLine : {
lineStyle : {
type : 'dotted' ,
2024-04-04 15:36:24 +09:00
color : 'var(--transparent-fg)' ,
2022-04-15 00:21:38 +09:00
opacity : 0.25 ,
}
} ,
type : 'value' ,
2022-04-15 19:39:27 +09:00
max : ( val ) = > this . timespan === 'all' ? Math . min ( val . max , 5000 ) : undefined ,
2022-04-15 00:21:38 +09:00
} ,
series : data.series ,
2023-07-20 16:25:09 +09:00
dataZoom : this.widget ? null : [ {
2022-04-15 00:21:38 +09:00
type : 'inside' ,
realtime : true ,
zoomLock : true ,
maxSpan : 100 ,
2022-04-15 19:39:27 +09:00
minSpan : 5 ,
2022-04-15 00:21:38 +09:00
moveOnMouseMove : false ,
} , {
showDetail : false ,
show : true ,
type : 'slider' ,
brushSelect : false ,
realtime : true ,
left : 20 ,
right : 15 ,
selectedDataBackground : {
lineStyle : {
color : '#fff' ,
opacity : 0.45 ,
} ,
areaStyle : {
opacity : 0 ,
}
} ,
} ] ,
} ;
}
2022-04-15 00:34:15 +09:00
onChartInit ( ec ) {
if ( this . chartInstance !== undefined ) {
return ;
}
this . chartInstance = ec ;
2022-04-15 19:39:27 +09:00
2022-04-15 00:34:15 +09:00
this . chartInstance . on ( 'click' , ( e ) = > {
this . zone . run ( ( ) = > {
if ( [ '24h' , '3d' ] . includes ( this . timespan ) ) {
const url = new RelativeUrlPipe ( this . stateService ) . transform ( ` /block/ ${ e . data [ 2 ] } ` ) ;
this . router . navigate ( [ url ] ) ;
}
} ) ;
} ) ;
2022-04-15 19:39:27 +09:00
this . chartInstance . on ( 'legendselectchanged' , ( e ) = > {
this . storageService . setValue ( 'fee_rates_legend' , JSON . stringify ( e . selected ) ) ;
} ) ;
2022-04-15 00:34:15 +09:00
}
2022-04-15 00:21:38 +09:00
isMobile() {
return ( window . innerWidth <= 767.98 ) ;
}
2022-05-05 16:18:28 +09:00
onSaveChart() {
// @ts-ignore
const prevBottom = this . chartOptions . grid . bottom ;
const now = new Date ( ) ;
// @ts-ignore
this . chartOptions . grid . bottom = 40 ;
2024-04-04 15:36:24 +09:00
this . chartOptions . backgroundColor = 'var(--active-bg)' ;
2022-05-05 16:18:28 +09:00
this . chartInstance . setOption ( this . chartOptions ) ;
download ( this . chartInstance . getDataURL ( {
pixelRatio : 2 ,
excludeComponents : [ 'dataZoom' ] ,
2022-05-09 11:01:51 +02:00
} ) , ` block-fee-rates- ${ this . timespan } - ${ Math . round ( now . getTime ( ) / 1000 ) } .svg ` ) ;
2022-05-05 16:18:28 +09:00
// @ts-ignore
this . chartOptions . grid . bottom = prevBottom ;
2022-05-09 11:01:51 +02:00
this . chartOptions . backgroundColor = 'none' ;
2022-05-05 16:18:28 +09:00
this . chartInstance . setOption ( this . chartOptions ) ;
}
2022-04-15 00:21:38 +09:00
}