2022-07-06 22:27:45 +02:00
import { ChangeDetectionStrategy , Component , Inject , Input , LOCALE_ID , NgZone , OnInit } from '@angular/core' ;
import { EChartsOption } from 'echarts' ;
import { Observable } from 'rxjs' ;
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-07-06 22:27:45 +02: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' ;
2022-07-06 22:27:45 +02:00
import { ActivatedRoute , Router } from '@angular/router' ;
2022-09-21 17:23:45 +02:00
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe' ;
import { StateService } from '../../services/state.service' ;
2022-07-06 22:27:45 +02:00
@Component ( {
2023-03-16 17:03:32 +09:00
selector : 'app-block-health-graph' ,
templateUrl : './block-health-graph.component.html' ,
styleUrls : [ './block-health-graph.component.scss' ] ,
2022-07-06 22:27:45 +02:00
styles : [ `
. loadingGraphs {
position : absolute ;
top : 50 % ;
left : calc ( 50 % - 15 px ) ;
z - index : 100 ;
}
` ],
changeDetection : ChangeDetectionStrategy.OnPush ,
} )
2023-03-16 17:03:32 +09:00
export class BlockHealthGraphComponent implements OnInit {
2022-07-06 22:27:45 +02:00
@Input ( ) right : number | string = 45 ;
@Input ( ) left : number | string = 75 ;
miningWindowPreference : string ;
2022-11-28 11:55:23 +09:00
radioGroupForm : UntypedFormGroup ;
2022-07-06 22:27:45 +02:00
chartOptions : EChartsOption = { } ;
chartInitOptions = {
renderer : 'svg' ,
} ;
statsObservable$ : Observable < any > ;
isLoading = true ;
formatNumber = formatNumber ;
timespan = '' ;
chartInstance : any = undefined ;
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-07-06 22:27:45 +02:00
private storageService : StorageService ,
private zone : NgZone ,
private route : ActivatedRoute ,
private stateService : StateService ,
private router : Router ,
) {
this . radioGroupForm = this . formBuilder . group ( { dateSpan : '1y' } ) ;
this . radioGroupForm . controls . dateSpan . setValue ( '1y' ) ;
}
ngOnInit ( ) : void {
2023-03-16 17:03:32 +09:00
this . seoService . setTitle ( $localize ` :@@d7d5fcf50179ad70c938491c517efb82de2c8146:Block Health ` ) ;
2023-08-30 20:26:07 +09:00
this . seoService . setDescription ( $localize ` :@@meta.description.bitcoin.graphs.block-health:See Bitcoin block health visualized over time. Block health is a measure of how many expected transactions were included in an actual mined block. Expected transactions are determined using Mempool's re-implementation of Bitcoin Core's transaction selection algorithm. ` ) ;
2022-07-06 22:27:45 +02:00
this . miningWindowPreference = '24h' ; //this.miningService.getDefaultTimespan('24h');
this . radioGroupForm = this . formBuilder . group ( { dateSpan : this.miningWindowPreference } ) ;
this . radioGroupForm . controls . dateSpan . setValue ( this . miningWindowPreference ) ;
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 } ) ;
}
} ) ;
this . statsObservable $ = this . radioGroupForm . get ( 'dateSpan' ) . valueChanges
. pipe (
startWith ( this . radioGroupForm . controls . dateSpan . value ) ,
switchMap ( ( timespan ) = > {
this . storageService . setValue ( 'miningWindowPreference' , timespan ) ;
this . timespan = timespan ;
this . isLoading = true ;
2023-03-16 17:03:32 +09:00
return this . apiService . getHistoricalBlocksHealth $ ( timespan )
2022-07-06 22:27:45 +02:00
. pipe (
tap ( ( response ) = > {
this . prepareChartOptions ( response . body ) ;
this . isLoading = false ;
} ) ,
map ( ( response ) = > {
return {
blockCount : parseInt ( response . headers . get ( 'x-total-count' ) , 10 ) ,
} ;
} ) ,
) ;
} ) ,
share ( )
) ;
}
prepareChartOptions ( data ) {
2022-07-15 12:01:21 +02:00
let title : object ;
if ( data . length === 0 ) {
title = {
textStyle : {
color : 'grey' ,
fontSize : 15
} ,
text : $localize ` No data to display yet. Try again later. ` ,
left : 'center' ,
top : 'center'
} ;
}
2022-07-06 22:27:45 +02:00
this . chartOptions = {
2022-07-15 12:01:21 +02:00
title : data.length === 0 ? title : undefined ,
2022-07-06 22:27:45 +02:00
animation : false ,
grid : {
top : 30 ,
bottom : 80 ,
right : this.right ,
left : this.left ,
} ,
tooltip : {
show : ! this . isMobile ( ) ,
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 : ( ticks ) = > {
let tooltip = ` <b style="color: white; margin-left: 2px"> ${ formatterXAxis ( this . locale , this . timespan , parseInt ( ticks [ 0 ] . axisValue , 10 ) * 1000 ) } </b><br> ` ;
tooltip += ` ${ ticks [ 0 ] . marker } ${ ticks [ 0 ] . seriesName } : ${ formatNumber ( ticks [ 0 ] . data . value , this . locale , '1.2-2' ) } %<br> ` ;
if ( [ '24h' , '3d' ] . includes ( this . timespan ) ) {
tooltip += ` <small> ` + $localize ` At block: ${ ticks [ 0 ] . data . block } ` + ` </small> ` ;
} else {
tooltip += ` <small> ` + $localize ` Around block: ${ ticks [ 0 ] . data . block } ` + ` </small> ` ;
}
return tooltip ;
}
} ,
2022-07-15 12:01:21 +02:00
xAxis : data.length === 0 ? undefined : {
2022-07-06 22:27:45 +02:00
name : formatterXAxisLabel ( this . locale , this . timespan ) ,
nameLocation : 'middle' ,
nameTextStyle : {
padding : [ 10 , 0 , 0 , 0 ] ,
} ,
type : 'category' ,
axisLine : { onZero : true } ,
axisLabel : {
2022-07-19 08:00:11 +02:00
formatter : val = > formatterXAxisTimeCategory ( this . locale , this . timespan , parseInt ( val , 10 ) * 1000 ) ,
2022-07-06 22:27:45 +02:00
align : 'center' ,
fontSize : 11 ,
lineHeight : 12 ,
hideOverlap : true ,
padding : [ 0 , 5 ] ,
} ,
2023-03-16 17:03:32 +09:00
data : data.map ( health = > health [ 0 ] )
2022-07-06 22:27:45 +02:00
} ,
2022-07-15 12:01:21 +02:00
yAxis : data.length === 0 ? undefined : [
2022-07-06 22:27:45 +02:00
{
type : 'value' ,
axisLabel : {
color : 'rgb(110, 112, 121)' ,
formatter : ( val ) = > {
return ` ${ val } % ` ;
}
} ,
splitLine : {
lineStyle : {
type : 'dotted' ,
color : '#ffffff66' ,
opacity : 0.25 ,
}
} ,
} ,
] ,
2022-07-15 12:01:21 +02:00
series : data.length === 0 ? undefined : [
2022-07-06 22:27:45 +02:00
{
zlevel : 0 ,
2023-03-16 17:03:32 +09:00
name : $localize ` Health ` ,
data : data.map ( health = > ( {
value : health [ 2 ] ,
block : health [ 1 ] ,
2022-07-06 22:27:45 +02:00
itemStyle : {
2023-03-16 17:03:32 +09:00
color : this.getHealthColor ( health [ 2 ] )
2022-07-06 22:27:45 +02:00
}
} ) ) ,
type : 'bar' ,
barWidth : '90%' ,
2022-07-15 12:01:21 +02:00
barMaxWidth : 50 ,
2022-07-06 22:27:45 +02:00
} ,
] ,
2022-07-15 12:01:21 +02:00
dataZoom : data.length === 0 ? undefined : [ {
2022-07-06 22:27:45 +02:00
type : 'inside' ,
realtime : true ,
zoomLock : true ,
maxSpan : 100 ,
minSpan : 5 ,
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 ,
}
} ,
} ] ,
} ;
}
colorGradient ( fadeFraction , rgbColor1 , rgbColor2 , rgbColor3 ) {
let color1 = rgbColor1 ;
let color2 = rgbColor2 ;
let fade = fadeFraction ;
// Do we have 3 colors for the gradient? Need to adjust the params.
if ( rgbColor3 ) {
fade = fade * 2 ;
// Find which interval to use and adjust the fade percentage
if ( fade >= 1 ) {
fade -= 1 ;
color1 = rgbColor2 ;
color2 = rgbColor3 ;
}
}
const diffRed = color2 . red - color1 . red ;
const diffGreen = color2 . green - color1 . green ;
const diffBlue = color2 . blue - color1 . blue ;
const gradient = {
red : Math.floor ( color1 . red + ( diffRed * fade ) ) ,
green : Math.floor ( color1 . green + ( diffGreen * fade ) ) ,
blue : Math.floor ( color1 . blue + ( diffBlue * fade ) ) ,
} ;
return 'rgb(' + gradient . red + ',' + gradient . green + ',' + gradient . blue + ')' ;
}
2023-03-16 17:03:32 +09:00
getHealthColor ( matchRate ) {
2022-07-06 22:27:45 +02:00
return this . colorGradient (
Math . pow ( ( 100 - matchRate ) / 100 , 0.5 ) ,
{ red : 67 , green : 171 , blue : 71 } ,
{ red : 253 , green : 216 , blue : 53 } ,
{ red : 244 , green : 0 , blue : 0 } ,
) ;
}
onChartInit ( ec ) {
this . chartInstance = ec ;
this . chartInstance . on ( 'click' , ( e ) = > {
this . zone . run ( ( ) = > {
if ( [ '24h' , '3d' ] . includes ( this . timespan ) ) {
const url = new RelativeUrlPipe ( this . stateService ) . transform ( ` /block/ ${ e . data . block } ` ) ;
this . router . navigate ( [ url ] ) ;
}
} ) ;
} ) ;
}
isMobile() {
return ( window . innerWidth <= 767.98 ) ;
}
onSaveChart() {
// @ts-ignore
const prevBottom = this . chartOptions . grid . bottom ;
const now = new Date ( ) ;
// @ts-ignore
this . chartOptions . grid . bottom = 40 ;
this . chartOptions . backgroundColor = '#11131f' ;
this . chartInstance . setOption ( this . chartOptions ) ;
download ( this . chartInstance . getDataURL ( {
pixelRatio : 2 ,
excludeComponents : [ 'dataZoom' ] ,
2023-03-16 17:03:32 +09:00
} ) , ` block-health- ${ this . timespan } - ${ Math . round ( now . getTime ( ) / 1000 ) } .svg ` ) ;
2022-07-06 22:27:45 +02:00
// @ts-ignore
this . chartOptions . grid . bottom = prevBottom ;
this . chartOptions . backgroundColor = 'none' ;
this . chartInstance . setOption ( this . chartOptions ) ;
}
}