2022-07-06 13:20:37 +02:00
import { ChangeDetectionStrategy , Component , Inject , Input , LOCALE_ID , OnInit , HostBinding } from '@angular/core' ;
2022-09-22 16:09:26 +02:00
import { EChartsOption , graphic , LineSeriesOption } from 'echarts' ;
2022-07-06 13:20:37 +02:00
import { Observable } from 'rxjs' ;
2022-07-06 15:15:08 +02:00
import { map , share , startWith , switchMap , tap } from 'rxjs/operators' ;
2022-07-06 13:20:37 +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 { StorageService } from '../../services/storage.service' ;
import { MiningService } from '../../services/mining.service' ;
import { download } from '../../shared/graphs.utils' ;
import { SeoService } from '../../services/seo.service' ;
2022-07-06 13:20:37 +02:00
import { LightningApiService } from '../lightning-api.service' ;
2022-09-21 17:23:45 +02:00
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe' ;
import { isMobile } from '../../shared/common.utils' ;
2022-07-06 13:20:37 +02:00
@Component ({
selector : 'app-nodes-networks-chart' ,
templateUrl : './nodes-networks-chart.component.html' ,
styleUrls : [ './nodes-networks-chart.component.scss' ],
styles : [ `
.loadingGraphs {
position: absolute;
top: 50%;
left: calc(50% - 15px);
z-index: 100;
}
` ],
changeDetection : ChangeDetectionStrategy.OnPush ,
})
export class NodesNetworksChartComponent implements OnInit {
@Input () right : number | string = 45 ;
2022-08-12 15:35:00 +02:00
@Input () left : number | string = 45 ;
2022-07-06 13:20:37 +02:00
@Input () widget = false ;
miningWindowPreference : string ;
2022-11-28 11:55:23 +09:00
radioGroupForm : UntypedFormGroup ;
2022-07-06 13:20:37 +02:00
chartOptions : EChartsOption = {};
chartInitOptions = {
renderer : 'svg' ,
};
@HostBinding ( 'attr.dir' ) dir = 'ltr' ;
nodesNetworkObservable$ : Observable < any >;
isLoading = true ;
formatNumber = formatNumber ;
timespan = '' ;
chartInstance : any = undefined ;
constructor (
@Inject ( LOCALE_ID ) public locale : string ,
private seoService : SeoService ,
private lightningApiService : LightningApiService ,
2022-11-28 11:55:23 +09:00
private formBuilder : UntypedFormBuilder ,
2022-07-06 13:20:37 +02:00
private storageService : StorageService ,
2022-08-12 15:35:00 +02:00
private miningService : MiningService ,
private amountShortenerPipe : AmountShortenerPipe ,
2022-07-06 13:20:37 +02:00
) {
}
ngOnInit () : void {
let firstRun = true ;
if ( this . widget ) {
2022-07-26 15:18:42 +02:00
this . miningWindowPreference = '3y' ;
2022-07-06 13:20:37 +02:00
} else {
2022-10-07 00:54:33 +04:00
this . seoService . setTitle ( $localize `:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network` );
2023-08-30 20:26:07 +09:00
this . seoService . setDescription ( $localize `:@@meta.description.lightning.nodes-network:See the number of Lightning network nodes visualized over time by network: clearnet only (IPv4, IPv6), darknet (Tor, I2p, cjdns), and both.` );
2022-07-06 15:15:08 +02:00
this . miningWindowPreference = this . miningService . getDefaultTimespan ( 'all' );
2022-07-06 13:20:37 +02:00
}
this . radioGroupForm = this . formBuilder . group ({ dateSpan : this.miningWindowPreference });
this . radioGroupForm . controls . dateSpan . setValue ( this . miningWindowPreference );
this . nodesNetworkObservable$ = this . radioGroupForm . get ( 'dateSpan' ). valueChanges
. pipe (
startWith ( this . miningWindowPreference ),
switchMap (( timespan ) => {
this . timespan = timespan ;
if ( ! this . widget && ! firstRun ) {
2022-07-06 15:15:08 +02:00
this . storageService . setValue ( 'lightningWindowPreference' , timespan );
2022-07-06 13:20:37 +02:00
}
firstRun = false ;
this . miningWindowPreference = timespan ;
this . isLoading = true ;
2022-07-06 15:15:08 +02:00
return this . lightningApiService . listStatistics$ ( timespan )
2022-07-06 13:20:37 +02:00
. pipe (
2022-07-06 15:15:08 +02:00
tap (( response ) => {
const data = response . body ;
2022-08-12 15:35:00 +02:00
const chartData = {
2022-07-06 13:20:37 +02:00
tor_nodes : data.map ( val => [ val . added * 1000 , val . tor_nodes ]),
clearnet_nodes : data.map ( val => [ val . added * 1000 , val . clearnet_nodes ]),
unannounced_nodes : data.map ( val => [ val . added * 1000 , val . unannounced_nodes ]),
2022-09-06 12:05:23 +02:00
clearnet_tor_nodes : data.map ( val => [ val . added * 1000 , val . clearnet_tor_nodes ]),
2022-08-12 15:35:00 +02:00
};
let maxYAxis = 0 ;
for ( const day of data ) {
2022-09-06 12:05:23 +02:00
maxYAxis = Math . max ( maxYAxis , day . tor_nodes + day . clearnet_nodes + day . unannounced_nodes + day . clearnet_tor_nodes );
2022-08-12 15:35:00 +02:00
}
2022-08-12 15:40:19 +02:00
maxYAxis = Math . ceil ( maxYAxis / 3000 ) * 3000 ;
2022-08-12 15:35:00 +02:00
this . prepareChartOptions ( chartData , maxYAxis );
2022-07-06 13:20:37 +02:00
this . isLoading = false ;
}),
2022-07-06 15:15:08 +02:00
map (( response ) => {
return {
days : parseInt ( response . headers . get ( 'x-total-count' ), 10 ),
};
}),
2022-07-06 13:20:37 +02:00
);
}),
2022-07-06 15:15:08 +02:00
share ()
);
2022-07-06 13:20:37 +02:00
}
2022-08-18 15:09:03 +02:00
prepareChartOptions ( data , maxYAxis ) : void {
2022-07-06 13:20:37 +02:00
let title : object ;
2022-08-18 15:09:03 +02:00
if ( ! this . widget && data . tor_nodes . length === 0 ) {
2022-07-06 13:20:37 +02:00
title = {
textStyle : {
color : 'grey' ,
fontSize : 15
},
2022-12-25 23:17:04 +04:00
text : $localize `Indexing in progress` ,
2022-07-06 13:20:37 +02:00
left : 'center' ,
2022-08-18 15:09:03 +02:00
top : 'center' ,
2022-08-12 15:35:00 +02:00
};
2022-08-23 16:36:41 +02:00
} else if ( this . widget && data . tor_nodes . length > 0 ) {
2022-08-12 15:35:00 +02:00
title = {
textStyle : {
color : 'grey' ,
fontSize : 11
},
2022-10-07 00:54:33 +04:00
text : $localize `:@@b420668a91f8ebaf6e6409c4ba87f1d45961d2bd:Lightning Nodes Per Network` ,
2022-08-12 15:35:00 +02:00
left : 'center' ,
2023-02-23 16:01:56 +09:00
top : 0 ,
2022-08-12 15:35:00 +02:00
zlevel : 10 ,
2022-07-06 13:20:37 +02:00
};
}
2022-09-22 16:09:26 +02:00
const series : LineSeriesOption [] = [
{
zlevel : 1 ,
yAxisIndex : 0 ,
2022-10-07 00:54:33 +04:00
name : $localize `:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown` ,
2022-09-22 16:09:26 +02:00
showSymbol : false ,
symbol : 'none' ,
data : data.unannounced_nodes ,
type : 'line' ,
lineStyle : {
width : 2 ,
},
areaStyle : {
opacity : 0.5 ,
},
stack : 'Total' ,
color : new graphic . LinearGradient ( 0 , 0.75 , 0 , 1 , [
{ offset : 0 , color : '#D81B60' },
{ offset : 1 , color : '#D81B60AA' },
]),
smooth : false ,
},
{
zlevel : 1 ,
yAxisIndex : 0 ,
2023-02-28 00:20:30 -05:00
name : $localize `Clearnet and Darknet` ,
2022-09-22 16:09:26 +02:00
showSymbol : false ,
symbol : 'none' ,
2023-02-28 00:20:30 -05:00
data : data.clearnet_tor_nodes ,
2022-09-22 16:09:26 +02:00
type : 'line' ,
lineStyle : {
width : 2 ,
},
areaStyle : {
opacity : 0.5 ,
},
stack : 'Total' ,
color : new graphic . LinearGradient ( 0 , 0.75 , 0 , 1 , [
2023-02-28 00:20:30 -05:00
{ offset : 0 , color : '#be7d4c' },
{ offset : 1 , color : '#be7d4cAA' },
2022-09-22 16:09:26 +02:00
]),
smooth : false ,
},
{
zlevel : 1 ,
yAxisIndex : 0 ,
2023-03-05 17:30:32 +09:00
name : $localize `Clearnet Only (IPv4, IPv6)` ,
2022-09-22 16:09:26 +02:00
showSymbol : false ,
symbol : 'none' ,
2023-02-28 00:20:30 -05:00
data : data.clearnet_nodes ,
2022-09-22 16:09:26 +02:00
type : 'line' ,
lineStyle : {
width : 2 ,
},
areaStyle : {
opacity : 0.5 ,
},
stack : 'Total' ,
color : new graphic . LinearGradient ( 0 , 0.75 , 0 , 1 , [
2023-02-28 00:20:30 -05:00
{ offset : 0 , color : '#FFB300' },
{ offset : 1 , color : '#FFB300AA' },
2022-09-22 16:09:26 +02:00
]),
smooth : false ,
},
{
zlevel : 1 ,
yAxisIndex : 0 ,
2023-02-28 00:20:30 -05:00
name : $localize `Darknet Only (Tor, I2P, cjdns)` ,
2022-09-22 16:09:26 +02:00
showSymbol : false ,
symbol : 'none' ,
data : data.tor_nodes ,
type : 'line' ,
lineStyle : {
width : 2 ,
},
areaStyle : {
opacity : 0.5 ,
},
stack : 'Total' ,
color : new graphic . LinearGradient ( 0 , 0.75 , 0 , 1 , [
{ offset : 0 , color : '#7D4698' },
{ offset : 1 , color : '#7D4698AA' },
]),
smooth : false ,
},
];
2022-07-06 13:20:37 +02:00
this . chartOptions = {
title : title ,
animation : false ,
grid : {
2023-02-23 16:01:56 +09:00
height : this.widget ? 90 : undefined ,
top : this.widget ? 20 : 40 ,
2022-08-12 15:35:00 +02:00
bottom : this.widget ? 0 : 70 ,
2022-08-18 15:09:03 +02:00
right : ( isMobile () && this . widget ) ? 35 : this.right ,
left : ( isMobile () && this . widget ) ? 40 :this.left ,
2022-07-06 13:20:37 +02:00
},
tooltip : {
2022-08-18 15:09:03 +02:00
show : ! isMobile () || ! this . widget ,
2022-07-06 13:20:37 +02: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' ,
2022-08-18 15:09:03 +02:00
formatter : ( ticks ) : string => {
2022-07-28 10:01:04 +02:00
let total = 0 ;
2022-07-06 13:20:37 +02:00
const date = new Date ( ticks [ 0 ]. data [ 0 ]). toLocaleDateString ( this . locale , { year : 'numeric' , month : 'short' , day : 'numeric' });
let tooltip = `<b style="color: white; margin-left: 2px"> ${ date } </b><br>` ;
2022-09-22 16:09:26 +02:00
for ( const tick of ticks . reverse ()) {
if ( tick . seriesName . indexOf ( 'ignored' ) !== - 1 ) {
continue ;
}
2022-07-28 10:01:04 +02:00
if ( tick . seriesIndex === 0 ) { // Tor
2022-07-06 13:20:37 +02:00
tooltip += ` ${ tick . marker } ${ tick . seriesName } : ${ formatNumber ( tick . data [ 1 ], this . locale , '1.0-0' ) } ` ;
2022-07-28 10:01:04 +02:00
} else if ( tick . seriesIndex === 1 ) { // Clearnet
2022-07-06 13:20:37 +02:00
tooltip += ` ${ tick . marker } ${ tick . seriesName } : ${ formatNumber ( tick . data [ 1 ], this . locale , '1.0-0' ) } ` ;
2022-07-28 10:01:04 +02:00
} else if ( tick . seriesIndex === 2 ) { // Unannounced
2022-07-06 13:20:37 +02:00
tooltip += ` ${ tick . marker } ${ tick . seriesName } : ${ formatNumber ( tick . data [ 1 ], this . locale , '1.0-0' ) } ` ;
2022-09-06 12:05:23 +02:00
} else if ( tick . seriesIndex === 3 ) { // Tor + Clearnet
tooltip += ` ${ tick . marker } ${ tick . seriesName } : ${ formatNumber ( tick . data [ 1 ], this . locale , '1.0-0' ) } ` ;
2022-07-06 13:20:37 +02:00
}
tooltip += `<br>` ;
2022-07-28 10:01:04 +02:00
total += tick . data [ 1 ];
2022-07-06 13:20:37 +02:00
}
2022-07-28 10:01:04 +02:00
tooltip += `<b>Total:</b> ${ formatNumber ( total , this . locale , '1.0-0' ) } nodes` ;
2022-07-06 13:20:37 +02:00
return tooltip ;
}
},
2022-07-28 10:01:04 +02:00
xAxis : data.tor_nodes.length === 0 ? undefined : {
2022-07-06 13:20:37 +02:00
type : 'time' ,
2022-08-18 15:09:03 +02:00
splitNumber : ( isMobile () || this . widget ) ? 5 : 10 ,
2022-07-06 13:20:37 +02:00
axisLabel : {
hideOverlap : true ,
}
},
2022-08-12 15:35:00 +02:00
legend : this.widget || data . tor_nodes . length === 0 ? undefined : {
2022-07-06 13:20:37 +02:00
padding : 10 ,
data : [
{
2023-02-28 00:20:30 -05:00
name : $localize `Darknet Only (Tor, I2P, cjdns)` ,
2022-07-06 13:20:37 +02:00
inactiveColor : 'rgb(110, 112, 121)' ,
textStyle : {
color : 'white' ,
},
icon : 'roundRect' ,
},
{
2023-03-05 17:34:30 +09:00
name : $localize `Clearnet Only (IPv4, IPv6)` ,
2022-07-06 13:20:37 +02:00
inactiveColor : 'rgb(110, 112, 121)' ,
textStyle : {
color : 'white' ,
},
icon : 'roundRect' ,
},
2022-09-08 19:03:37 +02:00
{
2023-02-28 00:20:30 -05:00
name : $localize `Clearnet and Darknet` ,
2022-09-08 19:03:37 +02:00
inactiveColor : 'rgb(110, 112, 121)' ,
textStyle : {
color : 'white' ,
},
icon : 'roundRect' ,
},
2022-07-06 13:20:37 +02:00
{
2022-10-07 00:54:33 +04:00
name : $localize `:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown` ,
2022-07-06 13:20:37 +02:00
inactiveColor : 'rgb(110, 112, 121)' ,
textStyle : {
color : 'white' ,
},
icon : 'roundRect' ,
},
],
2022-08-12 15:35:00 +02:00
selected : this.widget ? undefined : JSON . parse ( this . storageService . getValue ( 'nodes_networks_legend' )) ?? {
2023-02-28 00:20:30 -05:00
'$localize`Darknet Only (Tor, I2P, cjdns)`' : true ,
2023-03-05 17:34:30 +09:00
'$localize`Clearnet Only (IPv4, IPv6)`' : true ,
2023-02-28 00:20:30 -05:00
'$localize`Clearnet and Darknet`' : true ,
2022-10-07 00:54:33 +04:00
'$localize`:@@e5d8bb389c702588877f039d72178f219453a72d:Unknown`' : true ,
2022-07-06 13:20:37 +02:00
}
},
2022-07-28 10:01:04 +02:00
yAxis : data.tor_nodes.length === 0 ? undefined : [
2022-07-06 13:20:37 +02:00
{
type : 'value' ,
position : 'left' ,
axisLabel : {
color : 'rgb(110, 112, 121)' ,
2022-08-12 15:35:00 +02:00
formatter : ( val : number ) : string => {
if ( this . widget ) {
return ` ${ this . amountShortenerPipe . transform ( val , 0 ) } ` ;
} else {
return ` ${ formatNumber ( Math . round ( val ), this . locale , '1.0-0' ) } ` ;
}
2022-07-06 13:20:37 +02:00
}
},
splitLine : {
lineStyle : {
type : 'dotted' ,
color : '#ffffff66' ,
opacity : 0.25 ,
2022-08-12 15:35:00 +02:00
},
},
min : 0 ,
2022-08-12 15:40:19 +02:00
interval : 3000 ,
2022-08-12 15:35:00 +02:00
},
{
type : 'value' ,
position : 'right' ,
axisLabel : {
color : 'rgb(110, 112, 121)' ,
formatter : ( val : number ) : string => {
if ( this . widget ) {
return ` ${ this . amountShortenerPipe . transform ( val , 0 ) } ` ;
} else {
return ` ${ formatNumber ( Math . round ( val ), this . locale , '1.0-0' ) } ` ;
}
2022-07-06 13:20:37 +02:00
}
},
2022-08-12 15:35:00 +02:00
splitLine : {
lineStyle : {
type : 'dotted' ,
color : '#ffffff66' ,
opacity : 0.25 ,
},
},
min : 0 ,
2022-08-12 15:40:19 +02:00
interval : 3000 ,
2022-07-06 13:20:37 +02:00
}
],
2022-09-22 16:09:26 +02:00
series : data.tor_nodes.length === 0 ? [] : series . concat ( series . map (( serie ) => {
// We create dummy duplicated series so when we use the data zoom, the y axis
// both scales properly
const invisibleSerie = {... serie };
2023-08-30 20:26:07 +09:00
invisibleSerie . name = 'ignored' + Math . random (). toString ();
2022-09-22 16:09:26 +02:00
invisibleSerie . stack = 'ignored' ;
invisibleSerie . yAxisIndex = 1 ;
invisibleSerie . lineStyle = {
opacity : 0 ,
};
invisibleSerie . areaStyle = {
opacity : 0 ,
};
return invisibleSerie ;
})),
2022-07-06 13:20:37 +02:00
dataZoom : this.widget ? null : [{
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 ,
}
},
}],
};
2022-09-22 16:09:26 +02:00
2023-01-26 23:18:12 +04:00
if ( isMobile () && this . chartOptions . legend ) {
2022-09-22 16:09:26 +02:00
// @ts-ignore
this . chartOptions . legend . left = 50 ;
}
2022-07-06 13:20:37 +02:00
}
2022-08-18 15:09:03 +02:00
onChartInit ( ec ) : void {
2022-07-06 13:20:37 +02:00
if ( this . chartInstance !== undefined ) {
return ;
}
this . chartInstance = ec ;
this . chartInstance . on ( 'legendselectchanged' , ( e ) => {
this . storageService . setValue ( 'nodes_networks_legend' , JSON . stringify ( e . selected ));
});
}
2022-08-18 15:09:03 +02:00
onSaveChart () : void {
2022-07-06 13:20:37 +02:00
// @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
}), `lightning-nodes-per-network- ${ Math . round ( now . getTime () / 1000 ) } .svg` );
2022-07-06 13:20:37 +02:00
// @ts-ignore
this . chartOptions . grid . bottom = prevBottom ;
this . chartOptions . backgroundColor = 'none' ;
this . chartInstance . setOption ( this . chartOptions );
}
}