2022-04-05 20:37:18 +02:00
import { ChangeDetectionStrategy , ChangeDetectorRef , Component , Inject , Input , LOCALE_ID , OnInit , HostBinding } from '@angular/core' ;
2023-11-06 18:19:54 +00:00
import { EChartsOption } from '../../graphs/echarts' ;
2022-02-24 16:55:18 +09:00
import { Observable } from 'rxjs' ;
2022-03-09 21:21:44 +01:00
import { delay , map , retryWhen , 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-11-28 11:55:23 +09:00
import { UntypedFormBuilder , UntypedFormGroup } from '@angular/forms' ;
2023-02-21 17:51:46 +09:00
import { chartColors , poolsColor } from '../../app.constants' ;
2022-09-21 17:23:45 +02:00
import { StorageService } from '../../services/storage.service' ;
import { MiningService } from '../../services/mining.service' ;
import { download } from '../../shared/graphs.utils' ;
2022-06-15 11:26:58 +02:00
import { ActivatedRoute } from '@angular/router' ;
2023-11-02 01:29:55 +00:00
import { StateService } from '../../services/state.service' ;
2022-02-24 16:55:18 +09:00
2024-01-27 22:10:18 +00:00
interface Hashrate {
timestamp : number ;
avgHashRate : number ;
share : number ;
poolName : string ;
}
2022-02-24 16:55:18 +09:00
@Component ( {
selector : 'app-hashrate-chart-pools' ,
templateUrl : './hashrate-chart-pools.component.html' ,
styleUrls : [ './hashrate-chart-pools.component.scss' ] ,
styles : [ `
. loadingGraphs {
position : absolute ;
2022-02-24 20:20:18 +09:00
top : 50 % ;
2022-02-24 16:55:18 +09:00
left : calc ( 50 % - 15 px ) ;
z - index : 100 ;
}
` ],
2022-02-24 20:20:18 +09:00
changeDetection : ChangeDetectionStrategy.OnPush ,
2022-02-24 16:55:18 +09:00
} )
export class HashrateChartPoolsComponent implements OnInit {
2022-03-14 18:06:54 +01:00
@Input ( ) right : number | string = 45 ;
2022-02-24 16:55:18 +09:00
@Input ( ) left : number | string = 25 ;
2022-04-11 18:17:36 +09:00
miningWindowPreference : string ;
2022-11-28 11:55:23 +09:00
radioGroupForm : UntypedFormGroup ;
2022-02-24 16:55:18 +09:00
2024-01-27 22:10:18 +00:00
hashrates : Hashrate [ ] ;
2022-02-24 16:55:18 +09:00
chartOptions : EChartsOption = { } ;
chartInitOptions = {
renderer : 'svg' ,
} ;
2022-04-05 20:37:18 +02:00
@HostBinding ( 'attr.dir' ) dir = 'ltr' ;
2022-02-24 16:55:18 +09:00
hashrateObservable$ : Observable < any > ;
isLoading = true ;
2022-05-05 16:18:28 +09:00
timespan = '' ;
chartInstance : any = undefined ;
2022-02-24 16:55:18 +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-03-09 21:21:44 +01:00
private cd : ChangeDetectorRef ,
2022-04-11 18:17:36 +09:00
private storageService : StorageService ,
2022-06-15 11:26:58 +02:00
private miningService : MiningService ,
2023-11-02 01:29:55 +00:00
public stateService : StateService ,
2022-06-15 11:26:58 +02:00
private route : ActivatedRoute ,
2022-02-24 16:55:18 +09:00
) {
this . radioGroupForm = this . formBuilder . group ( { dateSpan : '1y' } ) ;
this . radioGroupForm . controls . dateSpan . setValue ( '1y' ) ;
}
ngOnInit ( ) : void {
2022-04-11 18:17:36 +09:00
let firstRun = true ;
this . seoService . setTitle ( $localize ` :@@mining.pools-historical-dominance:Pools Historical Dominance ` ) ;
2023-09-06 22:42:33 +09:00
this . seoService . setDescription ( $localize ` :@@meta.descriptions.bitcoin.graphs.hashrate-pools:See Bitcoin mining pool dominance visualized over time: see how top mining pools' share of total hashrate has fluctuated over time. ` ) ;
2022-06-15 11:26:58 +02:00
this . miningWindowPreference = this . miningService . getDefaultTimespan ( '6m' ) ;
2022-04-11 18:17:36 +09:00
this . radioGroupForm = this . formBuilder . group ( { dateSpan : this.miningWindowPreference } ) ;
this . radioGroupForm . controls . dateSpan . setValue ( this . miningWindowPreference ) ;
2022-02-24 20:20:18 +09:00
2022-06-15 11:26:58 +02:00
this . route
. fragment
. subscribe ( ( fragment ) = > {
if ( [ '6m' , '1y' , '2y' , '3y' , 'all' ] . indexOf ( fragment ) > - 1 ) {
2022-06-23 15:30:42 +02:00
this . radioGroupForm . controls . dateSpan . setValue ( fragment , { emitEvent : false } ) ;
2022-06-15 11:26:58 +02:00
}
} ) ;
2022-02-24 16:55:18 +09:00
this . hashrateObservable $ = this . radioGroupForm . get ( 'dateSpan' ) . valueChanges
. pipe (
2022-06-15 11:26:58 +02:00
startWith ( this . radioGroupForm . controls . dateSpan . value ) ,
2022-02-24 16:55:18 +09:00
switchMap ( ( timespan ) = > {
2022-04-11 18:17:36 +09:00
if ( ! firstRun ) {
this . storageService . setValue ( 'miningWindowPreference' , timespan ) ;
}
2022-05-05 16:18:28 +09:00
this . timespan = timespan ;
2022-04-11 18:17:36 +09:00
firstRun = false ;
2022-02-24 20:20:18 +09:00
this . isLoading = true ;
2022-02-24 16:55:18 +09:00
return this . apiService . getHistoricalPoolsHashrate $ ( timespan )
. pipe (
2022-04-15 20:43:10 +09:00
tap ( ( response ) = > {
2024-01-27 22:10:18 +00:00
this . hashrates = response . body ;
2022-02-24 20:20:18 +09:00
// Prepare series (group all hashrates data point by pool)
2024-01-27 22:10:18 +00:00
const series = this . applyHashrates ( ) ;
2022-03-09 21:21:44 +01:00
if ( series . length === 0 ) {
this . cd . markForCheck ( ) ;
throw new Error ( ) ;
}
2022-02-24 16:55:18 +09:00
} ) ,
2022-04-15 20:43:10 +09:00
map ( ( response ) = > {
2022-02-24 16:55:18 +09:00
return {
2022-04-15 20:43:10 +09:00
blockCount : parseInt ( response . headers . get ( 'x-total-count' ) , 10 ) ,
}
2022-02-24 16:55:18 +09:00
} ) ,
2022-03-09 21:21:44 +01:00
retryWhen ( ( errors ) = > errors . pipe (
delay ( 60000 )
) )
2022-02-24 16:55:18 +09:00
) ;
} ) ,
share ( )
) ;
}
2024-01-27 22:10:18 +00:00
applyHashrates ( ) : any [ ] {
const times : { [ time : number ] : { hashrates : { [ pool : string ] : Hashrate } } } = { } ;
const pools = { } ;
for ( const hashrate of this . hashrates ) {
if ( ! times [ hashrate . timestamp ] ) {
times [ hashrate . timestamp ] = { hashrates : { } } ;
}
times [ hashrate . timestamp ] . hashrates [ hashrate . poolName ] = hashrate ;
if ( ! pools [ hashrate . poolName ] ) {
2024-01-29 16:59:13 +00:00
pools [ hashrate . poolName ] = true ;
2024-01-27 22:10:18 +00:00
}
}
const sortedTimes = Object . keys ( times ) . sort ( ( a , b ) = > parseInt ( a ) - parseInt ( b ) ) . map ( time = > ( { time : parseInt ( time ) , hashrates : times [ time ] . hashrates } ) ) ;
2024-01-29 16:59:13 +00:00
const lastHashrates = sortedTimes [ sortedTimes . length - 1 ] . hashrates ;
const sortedPools = Object . keys ( pools ) . sort ( ( a , b ) = > {
if ( lastHashrates [ b ] ? . share ? ? lastHashrates [ a ] ? . share ? ? false ) {
// sort by descending share of hashrate in latest period
return ( lastHashrates [ b ] ? . share || 0 ) - ( lastHashrates [ a ] ? . share || 0 ) ;
} else {
// tiebreak by pool name
b < a ;
}
} ) ;
2024-01-27 22:10:18 +00:00
const series = [ ] ;
const legends = [ ] ;
for ( const name of sortedPools ) {
const data = sortedTimes . map ( ( { time , hashrates } ) = > {
return [ time * 1000 , ( hashrates [ name ] ? . share || 0 ) * 100 ] ;
} ) ;
series . push ( {
zlevel : 0 ,
stack : 'Total' ,
name : name ,
showSymbol : false ,
symbol : 'none' ,
data ,
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 ;
return series ;
}
2022-02-24 16:55:18 +09:00
prepareChartOptions ( data ) {
2022-03-09 20:10:51 +01:00
let title : object ;
2022-03-09 12:15:10 +01:00
if ( data . series . length === 0 ) {
title = {
textStyle : {
2022-03-09 21:21:44 +01:00
color : 'grey' ,
fontSize : 15
2022-03-09 12:15:10 +01:00
} ,
2022-06-10 23:29:27 +02:00
text : $localize ` :@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks ` ,
2022-03-09 20:10:51 +01:00
left : 'center' ,
top : 'center' ,
2022-03-09 12:15:10 +01:00
} ;
}
2022-02-24 16:55:18 +09:00
this . chartOptions = {
2022-03-09 12:15:10 +01:00
title : title ,
2022-03-14 18:06:54 +01:00
animation : false ,
2023-02-22 14:06:08 +09:00
color : chartColors.filter ( color = > color !== '#FDD835' ) ,
2022-02-24 16:55:18 +09:00
grid : {
right : this.right ,
left : this.left ,
2022-04-11 18:17:36 +09:00
bottom : 70 ,
top : this.isMobile ( ) ? 10 : 50 ,
2022-02-24 16:55:18 +09:00
} ,
tooltip : {
2022-04-11 18:17:36 +09:00
show : ! this . isMobile ( ) ,
2022-02-24 16:55:18 +09:00
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 ) {
2022-03-10 11:48:21 +01:00
const date = new Date ( data [ 0 ] . data [ 0 ] ) . toLocaleDateString ( this . locale , { year : 'numeric' , month : 'short' , day : 'numeric' } ) ;
2022-06-10 23:34:13 +02:00
let tooltip = ` <b style="color: white; margin-left: 2px"> ${ date } </b><br> ` ;
2022-02-24 16:55:18 +09:00
data . sort ( ( a , b ) = > b . data [ 1 ] - a . data [ 1 ] ) ;
for ( const pool of data ) {
if ( pool . data [ 1 ] > 0 ) {
2022-03-05 18:47:21 +01:00
tooltip += ` ${ pool . marker } ${ pool . seriesName } : ${ pool . data [ 1 ] . toFixed ( 2 ) } %<br> ` ;
2022-02-24 16:55:18 +09:00
}
}
return tooltip ;
} . bind ( this )
} ,
2022-03-09 20:10:51 +01:00
xAxis : data.series.length === 0 ? undefined : {
2022-02-24 16:55:18 +09:00
type : 'time' ,
2022-04-11 21:17:15 +09:00
splitNumber : this.isMobile ( ) ? 5 : 10 ,
axisLabel : {
hideOverlap : true ,
}
2022-02-24 16:55:18 +09:00
} ,
2022-04-11 18:17:36 +09:00
legend : ( this . isMobile ( ) || data . series . length === 0 ) ? undefined : {
2022-02-24 16:55:18 +09:00
data : data.legends
} ,
2022-03-09 20:10:51 +01:00
yAxis : data.series.length === 0 ? undefined : {
2022-02-24 16:55:18 +09:00
position : 'right' ,
axisLabel : {
color : 'rgb(110, 112, 121)' ,
formatter : ( val ) = > ` ${ val } % ` ,
} ,
splitLine : {
show : false ,
} ,
type : 'value' ,
max : 100 ,
min : 0 ,
} ,
series : data.series ,
2022-04-11 18:17:36 +09:00
dataZoom : [ {
2022-03-14 18:06:54 +01:00
type : 'inside' ,
realtime : true ,
zoomLock : true ,
maxSpan : 100 ,
minSpan : 10 ,
2022-03-14 18:25:16 +01:00
moveOnMouseMove : false ,
2022-03-14 18:06:54 +01:00
} , {
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-02-24 16:55:18 +09:00
} ;
2024-01-27 22:10:18 +00:00
this . cd . markForCheck ( ) ;
2022-02-24 16:55:18 +09:00
}
2022-05-05 16:18:28 +09:00
onChartInit ( ec ) {
this . chartInstance = ec ;
}
2022-02-24 16:55:18 +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 = 30 ;
2022-05-09 11:01:51 +02:00
this . chartOptions . backgroundColor = '#11131f' ;
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
} ) , ` pools-dominance- ${ 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-02-24 16:55:18 +09:00
}