Merge pull request #810 from mempool/simon/liquid-lbtc-dashboard-widget
L-BTC in circulation dashboard widgete
This commit is contained in:
		
						commit
						78b488466e
					
				| @ -37,6 +37,7 @@ import { IncomingTransactionsGraphComponent } from './components/incoming-transa | ||||
| import { TimeSpanComponent } from './components/time-span/time-span.component'; | ||||
| import { SeoService } from './services/seo.service'; | ||||
| import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component'; | ||||
| import { LbtcPegsGraphComponent } from './components/lbtc-pegs-graph/lbtc-pegs-graph.component'; | ||||
| import { AssetComponent } from './components/asset/asset.component'; | ||||
| import { AssetsComponent } from './assets/assets.component'; | ||||
| import { StatusViewComponent } from './components/status-view/status-view.component'; | ||||
| @ -84,6 +85,7 @@ import { SponsorComponent } from './components/sponsor/sponsor.component'; | ||||
|     FeeDistributionGraphComponent, | ||||
|     IncomingTransactionsGraphComponent, | ||||
|     MempoolGraphComponent, | ||||
|     LbtcPegsGraphComponent, | ||||
|     AssetComponent, | ||||
|     AssetsComponent, | ||||
|     MinerComponent, | ||||
|  | ||||
| @ -0,0 +1 @@ | ||||
| <div class="echarts" echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions"></div> | ||||
| @ -0,0 +1,137 @@ | ||||
| import { Component, OnInit, Inject, LOCALE_ID, ChangeDetectionStrategy, Output, EventEmitter, Input, OnChanges } from '@angular/core'; | ||||
| import { formatDate, formatNumber } from '@angular/common'; | ||||
| import { EChartsOption } from 'echarts'; | ||||
| import { ApiService } from 'src/app/services/api.service'; | ||||
| import { map } from 'rxjs/operators'; | ||||
| import { Observable } from 'rxjs'; | ||||
| 
 | ||||
| @Component({ | ||||
|   selector: 'app-lbtc-pegs-graph', | ||||
|   templateUrl: './lbtc-pegs-graph.component.html', | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class LbtcPegsGraphComponent implements OnChanges { | ||||
|   @Input() data: any; | ||||
|   pegsChartOptions: EChartsOption; | ||||
| 
 | ||||
|   height: number | string = '200'; | ||||
|   right: number | string = '10'; | ||||
|   top: number | string = '20'; | ||||
|   left: number | string = '50'; | ||||
|   template: ('widget' | 'advanced') = 'widget'; | ||||
| 
 | ||||
|   pegsChartOption: EChartsOption = {}; | ||||
|   pegsChartInitOption = { | ||||
|     renderer: 'svg' | ||||
|   }; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
|   ) { } | ||||
| 
 | ||||
|   ngOnChanges() { | ||||
|     this.pegsChartOptions = this.createChartOptions(this.data.series, this.data.labels); | ||||
|   } | ||||
| 
 | ||||
|   createChartOptions(series: number[], labels: string[]): EChartsOption { | ||||
|     return { | ||||
|       grid: { | ||||
|         height: this.height, | ||||
|         right: this.right, | ||||
|         top: this.top, | ||||
|         left: this.left, | ||||
|       }, | ||||
|       animation: false, | ||||
|       dataZoom: [{ | ||||
|         type: 'inside', | ||||
|         realtime: true, | ||||
|         zoomOnMouseWheel: (this.template === 'advanced') ? true : false, | ||||
|         maxSpan: 100, | ||||
|         minSpan: 10, | ||||
|       }, { | ||||
|         show: (this.template === 'advanced') ? true : false, | ||||
|         type: 'slider', | ||||
|         brushSelect: false, | ||||
|         realtime: true, | ||||
|         selectedDataBackground: { | ||||
|           lineStyle: { | ||||
|             color: '#fff', | ||||
|             opacity: 0.45, | ||||
|           }, | ||||
|           areaStyle: { | ||||
|             opacity: 0, | ||||
|           } | ||||
|         } | ||||
|       }], | ||||
|       tooltip: { | ||||
|         trigger: 'axis', | ||||
|         position: (pos, params, el, elRect, size) => { | ||||
|           const obj = { top: -20 }; | ||||
|           obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80; | ||||
|           return obj; | ||||
|         }, | ||||
|         extraCssText: `width: ${(this.template === 'widget') ? '125px' : '135px'};
 | ||||
|                       background: transparent; | ||||
|                       border: none; | ||||
|                       box-shadow: none;`,
 | ||||
|         axisPointer: { | ||||
|           type: 'line', | ||||
|         }, | ||||
|         formatter: (params: any) => { | ||||
|           const colorSpan = (color: string) => `<span class="indicator" style="background-color: #116761;"></span>`; | ||||
|           let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>'; | ||||
|           params.map((item: any, index: number) => { | ||||
|             if (index < 26) { | ||||
|               itemFormatted += `<div class="item">
 | ||||
|                 <div class="indicator-container">${colorSpan(item.color)}</div> | ||||
|                 <div class="grow"></div> | ||||
|                 <div class="value">${formatNumber(item.value, this.locale, '1.2-2')} <span class="symbol">L-BTC</span></div> | ||||
|               </div>`;
 | ||||
|             } | ||||
|           }); | ||||
|           return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`; | ||||
|         } | ||||
|       }, | ||||
|       xAxis: { | ||||
|         type: 'category', | ||||
|         axisLabel: { | ||||
|           align: 'center', | ||||
|           fontSize: 11, | ||||
|           lineHeight: 12 | ||||
|         }, | ||||
|         data: labels.map((value: any) => `${formatDate(value, 'MMM\ny', this.locale)}`), | ||||
|       }, | ||||
|       yAxis: { | ||||
|         type: 'value', | ||||
|         axisLabel: { | ||||
|           fontSize: 11, | ||||
|         }, | ||||
|         splitLine: { | ||||
|           lineStyle: { | ||||
|             type: 'dotted', | ||||
|             color: '#ffffff66', | ||||
|             opacity: 0.25, | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       series: [ | ||||
|         { | ||||
|           data: series, | ||||
|           type: 'line', | ||||
|           stack: 'total', | ||||
|           smooth: false, | ||||
|           showSymbol: false, | ||||
|           areaStyle: { | ||||
|             opacity: 0.2, | ||||
|             color: '#116761', | ||||
|           }, | ||||
|           lineStyle: { | ||||
|             width: 3, | ||||
|             color: '#116761', | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @ -16,7 +16,7 @@ | ||||
|       <div class="col"> | ||||
|         <div class="card"> | ||||
|           <div class="card-body"> | ||||
|             <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
|             <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -44,16 +44,21 @@ | ||||
|         <div class="card graph-card"> | ||||
|           <div class="card-body pl-0"> | ||||
|             <div style="padding-left: 1.25rem;"> | ||||
|               <ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
|               <ng-container *ngTemplateOutlet="stateService.network === 'liquid' ? lbtcPegs : mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container> | ||||
|               <hr> | ||||
|             </div> | ||||
|             <div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats; else loadingSpinner"> | ||||
|               <app-mempool-graph | ||||
|                 [template]="'widget'" | ||||
|                 [limitFee]="150" | ||||
|                 [data]="mempoolStats.mempool" | ||||
|               ></app-mempool-graph> | ||||
|             <div class="mempool-graph" *ngIf="stateService.network === 'liquid'; else mempoolGraph"> | ||||
|               <app-lbtc-pegs-graph [data]="liquidPegsMonth$ | async"></app-lbtc-pegs-graph> | ||||
|             </div> | ||||
|             <ng-template #mempoolGraph> | ||||
|               <div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats; else loadingSpinner"> | ||||
|                 <app-mempool-graph | ||||
|                   [template]="'widget'" | ||||
|                   [limitFee]="150" | ||||
|                   [data]="mempoolStats.mempool" | ||||
|                 ></app-mempool-graph> | ||||
|               </div> | ||||
|             </ng-template> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
| @ -192,6 +197,17 @@ | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #lbtcPegs let-mempoolInfoData> | ||||
|   <div class="mempool-info-data"> | ||||
|     <div class="item"> | ||||
|       <h5 class="card-title" i18n="dashboard.lbtc-pegs-in-circulation">L-BTC in circulation</h5> | ||||
|       <ng-container *ngIf="(liquidPegsMonth$ | async) as liquidPegsMonth; else loadingTransactions"> | ||||
|         <p class="card-text">{{ liquidPegsMonth.series.slice(-1)[0] | number: '1.2-2' }} <span>L-BTC</span></p> | ||||
|       </ng-container> | ||||
|     </div> | ||||
|   </div> | ||||
| </ng-template> | ||||
| 
 | ||||
| <ng-template #txPerSecond let-mempoolInfoData> | ||||
|   <h5 class="card-title" i18n="dashboard.incoming-transactions">Incoming transactions</h5> | ||||
|   <ng-template [ngIf]="(isLoadingWebSocket$ | async) === false && mempoolInfoData.value" [ngIfElse]="loadingTransactions"> | ||||
| @ -207,7 +223,6 @@ | ||||
|   </ng-template> | ||||
| </ng-template> | ||||
| 
 | ||||
| 
 | ||||
| <ng-template #difficultyEpoch> | ||||
|   <div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div> | ||||
|   <div class="card-wrapper"> | ||||
|  | ||||
| @ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnInit } from '@ | ||||
| import { combineLatest, merge, Observable, of, timer } from 'rxjs'; | ||||
| import { filter, map, scan, share, switchMap, tap } from 'rxjs/operators'; | ||||
| import { Block } from '../interfaces/electrs.interface'; | ||||
| import { OptimizedMempoolStats } from '../interfaces/node-api.interface'; | ||||
| import { LiquidPegs, OptimizedMempoolStats } from '../interfaces/node-api.interface'; | ||||
| import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface'; | ||||
| import { ApiService } from '../services/api.service'; | ||||
| import { StateService } from '../services/state.service'; | ||||
| @ -63,6 +63,7 @@ export class DashboardComponent implements OnInit { | ||||
|   mempoolStats$: Observable<MempoolStatsData>; | ||||
|   transactionsWeightPerSecondOptions: any; | ||||
|   isLoadingWebSocket$: Observable<boolean>; | ||||
|   liquidPegsMonth$: Observable<any>; | ||||
| 
 | ||||
|   constructor( | ||||
|     @Inject(LOCALE_ID) private locale: string, | ||||
| @ -261,6 +262,22 @@ export class DashboardComponent implements OnInit { | ||||
|         }), | ||||
|         share(), | ||||
|       ); | ||||
| 
 | ||||
|     if (this.stateService.network === 'liquid') { | ||||
|       this.liquidPegsMonth$ = this.apiService.listLiquidPegsMonth$() | ||||
|         .pipe( | ||||
|           map((pegs) => { | ||||
|             const labels = pegs.map(stats => stats.date); | ||||
|             const series = pegs.map(stats => parseFloat(stats.amount) / 100000000); | ||||
|             series.reduce((prev, curr, i) => series[i] = prev + curr, 0); | ||||
|             return { | ||||
|               series, | ||||
|               labels | ||||
|             }; | ||||
|           }), | ||||
|           share(), | ||||
|         ); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) { | ||||
|  | ||||
| @ -47,3 +47,8 @@ export interface AddressInformation { | ||||
|   confidential_key?: string;       //  (string) Elements only
 | ||||
|   unconfidential?: string;         //  (string) Elements only
 | ||||
| } | ||||
| 
 | ||||
| export interface LiquidPegs { | ||||
|   amount: string; | ||||
|   date: string; | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { HttpClient, HttpParams } from '@angular/common/http'; | ||||
| import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation } from '../interfaces/node-api.interface'; | ||||
| import { CpfpInfo, OptimizedMempoolStats, DifficultyAdjustment, AddressInformation, LiquidPegs } from '../interfaces/node-api.interface'; | ||||
| import { Observable } from 'rxjs'; | ||||
| import { StateService } from './state.service'; | ||||
| import { WebsocketResponse } from '../interfaces/websocket.interface'; | ||||
| @ -100,4 +100,8 @@ export class ApiService { | ||||
|   validateAddress$(address: string): Observable<AddressInformation> { | ||||
|     return this.httpClient.get<AddressInformation>(this.apiBaseUrl + this.apiBasePath + '/api/v1/validate-address/' + address); | ||||
|   } | ||||
| 
 | ||||
|   listLiquidPegsMonth$(): Observable<LiquidPegs[]> { | ||||
|     return this.httpClient.get<LiquidPegs[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegs/month'); | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user