Add fee histogram chart to the node page
This commit is contained in:
		
							parent
							
								
									f4df51dd21
								
							
						
					
					
						commit
						893aa03622
					
				| @ -15,6 +15,7 @@ import { ChannelBoxComponent } from './channel/channel-box/channel-box.component | |||||||
| import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; | import { ClosingTypeComponent } from './channel/closing-type/closing-type.component'; | ||||||
| import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; | import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; | ||||||
| import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; | import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; | ||||||
|  | import { NodeFeeChartComponent } from './node-fee-chart/node-fee-chart.component'; | ||||||
| import { GraphsModule } from '../graphs/graphs.module'; | import { GraphsModule } from '../graphs/graphs.module'; | ||||||
| import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; | import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; | ||||||
| import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component'; | import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component'; | ||||||
| @ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component'; | |||||||
|     NodesListComponent, |     NodesListComponent, | ||||||
|     NodeStatisticsComponent, |     NodeStatisticsComponent, | ||||||
|     NodeStatisticsChartComponent, |     NodeStatisticsChartComponent, | ||||||
|  |     NodeFeeChartComponent, | ||||||
|     NodeComponent, |     NodeComponent, | ||||||
|     ChannelsListComponent, |     ChannelsListComponent, | ||||||
|     ChannelComponent, |     ChannelComponent, | ||||||
| @ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component'; | |||||||
|     NodesListComponent, |     NodesListComponent, | ||||||
|     NodeStatisticsComponent, |     NodeStatisticsComponent, | ||||||
|     NodeStatisticsChartComponent, |     NodeStatisticsChartComponent, | ||||||
|  |     NodeFeeChartComponent, | ||||||
|     NodeComponent, |     NodeComponent, | ||||||
|     ChannelsListComponent, |     ChannelsListComponent, | ||||||
|     ChannelComponent, |     ChannelComponent, | ||||||
|  | |||||||
| @ -0,0 +1,7 @@ | |||||||
|  | <div class="full-container"> | ||||||
|  |   <h2 i18n="lightning.node-fee-distribution">Fee distribution</h2> | ||||||
|  |   <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div> | ||||||
|  |   <div class="text-center loadingGraphs" *ngIf="isLoading"> | ||||||
|  |     <div class="spinner-border text-light"></div>d | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | .full-container { | ||||||
|  |   margin-top: 25px; | ||||||
|  |   margin-bottom: 25px; | ||||||
|  |   min-height: 100%; | ||||||
|  | } | ||||||
| @ -0,0 +1,265 @@ | |||||||
|  | import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; | ||||||
|  | import { EChartsOption } from 'echarts'; | ||||||
|  | import { switchMap } from 'rxjs/operators'; | ||||||
|  | import { download } from '../../shared/graphs.utils'; | ||||||
|  | import { LightningApiService } from '../lightning-api.service'; | ||||||
|  | import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||||
|  | import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-node-fee-chart', | ||||||
|  |   templateUrl: './node-fee-chart.component.html', | ||||||
|  |   styleUrls: ['./node-fee-chart.component.scss'], | ||||||
|  |   styles: [` | ||||||
|  |     .loadingGraphs { | ||||||
|  |       position: absolute; | ||||||
|  |       top: 50%; | ||||||
|  |       left: calc(50% - 15px); | ||||||
|  |       z-index: 100; | ||||||
|  |     } | ||||||
|  |   `],
 | ||||||
|  | }) | ||||||
|  | export class NodeFeeChartComponent implements OnInit { | ||||||
|  |   chartOptions: EChartsOption = {}; | ||||||
|  |   chartInitOptions = { | ||||||
|  |     renderer: 'svg', | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   @HostBinding('attr.dir') dir = 'ltr'; | ||||||
|  | 
 | ||||||
|  |   isLoading = true; | ||||||
|  |   chartInstance: any = undefined; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     @Inject(LOCALE_ID) public locale: string, | ||||||
|  |     private lightningApiService: LightningApiService, | ||||||
|  |     private activatedRoute: ActivatedRoute, | ||||||
|  |     private amountShortenerPipe: AmountShortenerPipe, | ||||||
|  |   ) { | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnInit(): void { | ||||||
|  | 
 | ||||||
|  |     this.activatedRoute.paramMap | ||||||
|  |       .pipe( | ||||||
|  |         switchMap((params: ParamMap) => { | ||||||
|  |           this.isLoading = true; | ||||||
|  |           return this.lightningApiService.getNodeFeeHistogram$(params.get('public_key')); | ||||||
|  |         }), | ||||||
|  |       ).subscribe((data) => { | ||||||
|  |         if (data && data.incoming && data.outgoing) { | ||||||
|  |           const outgoingHistogram = this.bucketsToHistogram(data.outgoing); | ||||||
|  |           const incomingHistogram = this.bucketsToHistogram(data.incoming); | ||||||
|  |           this.prepareChartOptions(outgoingHistogram, incomingHistogram); | ||||||
|  |         } | ||||||
|  |         this.isLoading = false; | ||||||
|  |       }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   bucketsToHistogram(buckets): { label: string, count: number, capacity: number}[] { | ||||||
|  |     const histogram = []; | ||||||
|  |     let increment = 1; | ||||||
|  |     let lower = -increment; | ||||||
|  |     let upper = 0; | ||||||
|  | 
 | ||||||
|  |     let nullBucket; | ||||||
|  |     if (buckets.length && buckets[0] && buckets[0].bucket == null) { | ||||||
|  |       nullBucket = buckets.shift(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     while (upper <= 5000) { | ||||||
|  |       let bucket; | ||||||
|  |       if (buckets.length && buckets[0] && upper >= Number(buckets[0].bucket)) { | ||||||
|  |         bucket = buckets.shift(); | ||||||
|  |       } | ||||||
|  |       histogram.push({ | ||||||
|  |         label: upper === 0 ? '0 ppm' : `${lower} - ${upper} ppm`, | ||||||
|  |         count: Number(bucket?.count || 0) + (upper === 0 ? Number(nullBucket?.count || 0) : 0), | ||||||
|  |         capacity: Number(bucket?.capacity || 0) + (upper === 0 ? Number(nullBucket?.capacity || 0) : 0), | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (upper >= increment * 10) { | ||||||
|  |         increment *= 10; | ||||||
|  |         lower = increment; | ||||||
|  |         upper = increment + increment; | ||||||
|  |       } else { | ||||||
|  |         lower += increment; | ||||||
|  |         upper += increment; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const rest = buckets.reduce((acc, bucket) => { | ||||||
|  |       acc.count += Number(bucket.count); | ||||||
|  |       acc.capacity += Number(bucket.capacity); | ||||||
|  |       return acc; | ||||||
|  |     }, { count: 0, capacity: 0 }); | ||||||
|  |     histogram.push({ | ||||||
|  |       label: `5000+ ppm`, | ||||||
|  |       count: rest.count, | ||||||
|  |       capacity: rest.capacity, | ||||||
|  |     }); | ||||||
|  |     return histogram; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   prepareChartOptions(outgoingData, incomingData): void { | ||||||
|  |     let title: object; | ||||||
|  |     if (outgoingData.length === 0) { | ||||||
|  |       title = { | ||||||
|  |         textStyle: { | ||||||
|  |           color: 'grey', | ||||||
|  |           fontSize: 15 | ||||||
|  |         }, | ||||||
|  |         text: $localize`No data to display yet. Try again later.`, | ||||||
|  |         left: 'center', | ||||||
|  |         top: 'center' | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.chartOptions = { | ||||||
|  |       title: outgoingData.length === 0 ? title : undefined, | ||||||
|  |       animation: false, | ||||||
|  |       grid: { | ||||||
|  |         top: 30, | ||||||
|  |         bottom: 20, | ||||||
|  |         right: 20, | ||||||
|  |         left: 65, | ||||||
|  |       }, | ||||||
|  |       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): string => { | ||||||
|  |           return ` | ||||||
|  |             <b style="color: white; margin-left: 2px">${ticks[0].data.label}</b><br> | ||||||
|  |             <br> | ||||||
|  |             <b style="color: white; margin-left: 2px">${ticks[0].marker} Outgoing</b><br> | ||||||
|  |             <span>Capacity: ${this.amountShortenerPipe.transform(ticks[0].data.capacity, 2, undefined, true)} sats</span><br> | ||||||
|  |             <span>Channels: ${ticks[0].data.count}</span><br> | ||||||
|  |             <br> | ||||||
|  |             <b style="color: white; margin-left: 2px">${ticks[1].marker} Incoming</b><br> | ||||||
|  |             <span>Capacity: ${this.amountShortenerPipe.transform(ticks[1].data.capacity, 2, undefined, true)} sats</span><br> | ||||||
|  |             <span>Channels: ${ticks[1].data.count}</span><br> | ||||||
|  |           `;
 | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       xAxis: outgoingData.length === 0 ? undefined : { | ||||||
|  |         type: 'category', | ||||||
|  |         axisLine: { onZero: true }, | ||||||
|  |         axisLabel: { | ||||||
|  |           align: 'center', | ||||||
|  |           fontSize: 11, | ||||||
|  |           lineHeight: 12, | ||||||
|  |           hideOverlap: true, | ||||||
|  |           padding: [0, 5], | ||||||
|  |         }, | ||||||
|  |         data: outgoingData.map(bucket => bucket.label) | ||||||
|  |       }, | ||||||
|  |       legend: outgoingData.length === 0 ? undefined : { | ||||||
|  |         padding: 10, | ||||||
|  |         data: [ | ||||||
|  |           { | ||||||
|  |             name: 'Outgoing Fees', | ||||||
|  |             inactiveColor: 'rgb(110, 112, 121)', | ||||||
|  |             textStyle: { | ||||||
|  |               color: 'white', | ||||||
|  |             }, | ||||||
|  |             icon: 'roundRect', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Incoming Fees', | ||||||
|  |             inactiveColor: 'rgb(110, 112, 121)', | ||||||
|  |             textStyle: { | ||||||
|  |               color: 'white', | ||||||
|  |             }, | ||||||
|  |             icon: 'roundRect', | ||||||
|  |           }, | ||||||
|  |         ], | ||||||
|  |       }, | ||||||
|  |       yAxis: outgoingData.length === 0 ? undefined : [ | ||||||
|  |         { | ||||||
|  |           type: 'value', | ||||||
|  |           axisLabel: { | ||||||
|  |             color: 'rgb(110, 112, 121)', | ||||||
|  |             formatter: (val) => { | ||||||
|  |               return `${this.amountShortenerPipe.transform(Math.abs(val), 2, undefined, true)} sats`; | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           splitLine: { | ||||||
|  |             lineStyle: { | ||||||
|  |               type: 'dotted', | ||||||
|  |               color: '#ffffff66', | ||||||
|  |               opacity: 0.25, | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |       series: outgoingData.length === 0 ? undefined : [ | ||||||
|  |         { | ||||||
|  |           zlevel: 0, | ||||||
|  |           name: 'Outgoing Fees', | ||||||
|  |           data: outgoingData.map(bucket => ({ | ||||||
|  |             value: bucket.capacity, | ||||||
|  |             label: bucket.label, | ||||||
|  |             capacity: bucket.capacity, | ||||||
|  |             count: bucket.count, | ||||||
|  |           })), | ||||||
|  |           type: 'bar', | ||||||
|  |           barWidth: '90%', | ||||||
|  |           barMaxWidth: 50, | ||||||
|  |           stack: 'fees', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           zlevel: 0, | ||||||
|  |           name: 'Incoming Fees', | ||||||
|  |           data: incomingData.map(bucket => ({ | ||||||
|  |             value: -bucket.capacity, | ||||||
|  |             label: bucket.label, | ||||||
|  |             capacity: bucket.capacity, | ||||||
|  |             count: bucket.count, | ||||||
|  |           })), | ||||||
|  |           type: 'bar', | ||||||
|  |           barWidth: '90%', | ||||||
|  |           barMaxWidth: 50, | ||||||
|  |           stack: 'fees', | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   onChartInit(ec) { | ||||||
|  |     if (this.chartInstance !== undefined) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.chartInstance = ec; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   isMobile() { | ||||||
|  |     return (window.innerWidth <= 767.98); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   onSaveChart() { | ||||||
|  |     // @ts-ignore
 | ||||||
|  |     const prevBottom = this.chartOptions.grid.bottom; | ||||||
|  |     // @ts-ignore
 | ||||||
|  |     this.chartOptions.grid.bottom = 40; | ||||||
|  |     this.chartOptions.backgroundColor = '#11131f'; | ||||||
|  |     this.chartInstance.setOption(this.chartOptions); | ||||||
|  |     download(this.chartInstance.getDataURL({ | ||||||
|  |       pixelRatio: 2, | ||||||
|  |     }), `node-fee-chart.svg`); | ||||||
|  |     // @ts-ignore
 | ||||||
|  |     this.chartOptions.grid.bottom = prevBottom; | ||||||
|  |     this.chartOptions.backgroundColor = 'none'; | ||||||
|  |     this.chartInstance.setOption(this.chartOptions); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -140,6 +140,8 @@ | |||||||
| 
 | 
 | ||||||
|     <app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels> |     <app-node-channels style="display:block;margin-bottom: 40px" [publicKey]="node.public_key"></app-node-channels> | ||||||
| 
 | 
 | ||||||
|  |     <app-node-fee-chart style="display:block;margin-bottom: 40px"></app-node-fee-chart> | ||||||
|  | 
 | ||||||
|     <div class="d-flex"> |     <div class="d-flex"> | ||||||
|       <h2 *ngIf="channelsListStatus === 'open'"> |       <h2 *ngIf="channelsListStatus === 'open'"> | ||||||
|         <span i18n="lightning.open-channels">Open channels</span> |         <span i18n="lightning.open-channels">Open channels</span> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user