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 { LightningStatisticsChartComponent } from './statistics-chart/lightning-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 { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; | ||||
| import { ChannelsStatisticsComponent } from './channels-statistics/channels-statistics.component'; | ||||
| @ -38,6 +39,7 @@ import { GroupComponent } from './group/group.component'; | ||||
|     NodesListComponent, | ||||
|     NodeStatisticsComponent, | ||||
|     NodeStatisticsChartComponent, | ||||
|     NodeFeeChartComponent, | ||||
|     NodeComponent, | ||||
|     ChannelsListComponent, | ||||
|     ChannelComponent, | ||||
| @ -73,6 +75,7 @@ import { GroupComponent } from './group/group.component'; | ||||
|     NodesListComponent, | ||||
|     NodeStatisticsComponent, | ||||
|     NodeStatisticsChartComponent, | ||||
|     NodeFeeChartComponent, | ||||
|     NodeComponent, | ||||
|     ChannelsListComponent, | ||||
|     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-fee-chart style="display:block;margin-bottom: 40px"></app-node-fee-chart> | ||||
| 
 | ||||
|     <div class="d-flex"> | ||||
|       <h2 *ngIf="channelsListStatus === 'open'"> | ||||
|         <span i18n="lightning.open-channels">Open channels</span> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user