Redesign accelerator dashboard
This commit is contained in:
		
							parent
							
								
									8599876b26
								
							
						
					
					
						commit
						42f6f0c122
					
				| @ -1,20 +1,18 @@ | |||||||
| <app-indexing-progress *ngIf="!widget"></app-indexing-progress> |  | ||||||
| 
 |  | ||||||
| <div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget"> | <div class="container-xl" style="min-height: 335px" [class.widget]="widget" [class.full-height]="!widget"> | ||||||
|   <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1> |   <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1> | ||||||
|   <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> |   <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> | ||||||
| 
 | 
 | ||||||
|   <div class="clearfix"></div> |   <div class="clearfix"></div> | ||||||
| 
 | 
 | ||||||
|   <div style="min-height: 295px"> |   <div style="min-height: 295px" *ngIf="accelerationList$ | async as accelerations"> | ||||||
|     <table class="table table-borderless table-fixed"> |     <table *ngIf="!accelerations || accelerations.length; else noData" class="table table-borderless table-fixed"> | ||||||
|       <thead> |       <thead> | ||||||
|         <th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th> |         <th class="txid text-left" i18n="dashboard.latest-transactions.txid">TXID</th> | ||||||
|         <th class="fee text-right" i18n="transaction.fee|Transaction fee">Final Fee</th> |         <th class="fee text-right" i18n="transaction.fee|Transaction fee">Final Fee</th> | ||||||
|         <th class="fee-delta text-right" i18n="accelerator.fee-delta">Max Bid</th> |         <th class="fee-delta text-right" i18n="accelerator.fee-delta">Max Bid</th> | ||||||
|         <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> |         <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> | ||||||
|       </thead> |       </thead> | ||||||
|       <tbody *ngIf="accelerationList$ | async as accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> |       <tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> | ||||||
|         <tr *ngFor="let acceleration of accelerations; let i= index;"> |         <tr *ngFor="let acceleration of accelerations; let i= index;"> | ||||||
|           <td class="txid text-left"> |           <td class="txid text-left"> | ||||||
|             <a [routerLink]="['/tx' | relativeUrl, acceleration.txid]"> |             <a [routerLink]="['/tx' | relativeUrl, acceleration.txid]"> | ||||||
| @ -63,4 +61,10 @@ | |||||||
|     </ng-template> |     </ng-template> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|  |   <ng-template #noData> | ||||||
|  |     <div class="no-data"> | ||||||
|  |       <span i18n="accelerations.no-accelerations-yet">There are no accelerations show here yet!</span> | ||||||
|  |     </div> | ||||||
|  |   </ng-template> | ||||||
|  |    | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -105,3 +105,13 @@ tr, td, th { | |||||||
| 	max-width: 50vw; | 	max-width: 50vw; | ||||||
|   text-align: left; |   text-align: left; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .no-data { | ||||||
|  |   color: rgba(255, 255, 255, 0.4); | ||||||
|  |   display: flex; | ||||||
|  |   height: 280px; | ||||||
|  |   width: 100%; | ||||||
|  |   flex-direction: row; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  | } | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import { WebsocketService } from '../../../services/websocket.service'; | |||||||
| }) | }) | ||||||
| export class AccelerationsListComponent implements OnInit { | export class AccelerationsListComponent implements OnInit { | ||||||
|   @Input() widget: boolean = false; |   @Input() widget: boolean = false; | ||||||
|  |   @Input() pending: boolean = false; | ||||||
|   @Input() accelerations$: Observable<Acceleration[]>; |   @Input() accelerations$: Observable<Acceleration[]>; | ||||||
| 
 | 
 | ||||||
|   accelerationList$: Observable<Acceleration[]> = undefined; |   accelerationList$: Observable<Acceleration[]> = undefined; | ||||||
| @ -40,8 +41,14 @@ export class AccelerationsListComponent implements OnInit { | |||||||
|     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; |     this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; | ||||||
|     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; |     this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; | ||||||
| 
 | 
 | ||||||
|     this.accelerationList$ = (this.apiService.getAccelerationHistory$({ timeframe: '1m' }) || this.accelerations$).pipe( |     const accelerationObservable$ = this.accelerations$ || (this.pending ? this.apiService.getAccelerations$() : this.apiService.getAccelerationHistory$({ timeframe: '1m' })); | ||||||
|  |     this.accelerationList$ = accelerationObservable$.pipe( | ||||||
|       switchMap(accelerations => { |       switchMap(accelerations => { | ||||||
|  |         if (this.pending) { | ||||||
|  |           for (const acceleration of accelerations) { | ||||||
|  |             acceleration.status = acceleration.status || 'accelerating'; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|         if (this.widget) { |         if (this.widget) { | ||||||
|           return of(accelerations.slice(-6).reverse()); |           return of(accelerations.slice(-6).reverse()); | ||||||
|         } else { |         } else { | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ | |||||||
|       <div class="card-wrapper"> |       <div class="card-wrapper"> | ||||||
|         <div class="card"> |         <div class="card"> | ||||||
|           <div class="card-body more-padding"> |           <div class="card-body more-padding"> | ||||||
|             <app-pending-stats></app-pending-stats> |             <app-pending-stats [accelerations$]="pendingAccelerations$"></app-pending-stats> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -27,7 +27,18 @@ | |||||||
|       <div class="card-wrapper"> |       <div class="card-wrapper"> | ||||||
|         <div class="card"> |         <div class="card"> | ||||||
|           <div class="card-body more-padding"> |           <div class="card-body more-padding"> | ||||||
|             <app-acceleration-stats timespan="1w" [accelerations$]="accelerations$"></app-acceleration-stats> |             <app-acceleration-stats timespan="1w" [accelerations$]="minedAccelerations$"></app-acceleration-stats> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <!-- Next block visualization --> | ||||||
|  |     <div class="col" style="margin-bottom: 1.47rem"> | ||||||
|  |       <div class="card"> | ||||||
|  |         <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> | ||||||
|  |           <div class="mempool-block-wrapper"> | ||||||
|  |             <app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @ -43,64 +54,28 @@ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- Block fee rates --> |     <!-- Pending List --> | ||||||
|     <div class="col" style="margin-bottom: 1.47rem"> |     <div class="col"> | ||||||
|       <div class="card"> |       <div class="card list-card"> | ||||||
|         <div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2"> |         <div class="card-body"> | ||||||
|           <app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph> |           <h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Pending Accelerations</h5> | ||||||
|           <div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more »</a></div> |           <app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 | 
 | ||||||
|     <!-- Recent accelerations --> |     <!-- Confirmed List --> | ||||||
|     <div class="col"> |     <div class="col"> | ||||||
|       <div class="card"> |       <div class="card list-card"> | ||||||
|         <div class="card-body"> |         <div class="card-body"> | ||||||
|           <a class="title-link" href="" [routerLink]="['/acceleration-list' | relativeUrl]"> |           <a class="title-link" href="" [routerLink]="['/acceleration-list' | relativeUrl]"> | ||||||
|             <h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5> |             <h5 class="card-title d-inline" i18n="dashboard.recent-accelerations">Recent Accelerations</h5> | ||||||
|             <span> </span> |             <span> </span> | ||||||
|             <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon> |             <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon> | ||||||
|           </a> |           </a> | ||||||
|           <app-accelerations-list [attr.data-cy]="'recent-accelerations'" [widget]=true [accelerations$]="accelerations$"></app-accelerations-list> |           <app-accelerations-list [attr.data-cy]="'recent-accelerations'" [widget]=true [accelerations$]="minedAccelerations$"></app-accelerations-list> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| 
 |  | ||||||
|     <!-- Recent blocks --> |  | ||||||
|     <div class="col"> |  | ||||||
|       <div class="card"> |  | ||||||
|         <div class="card-body"> |  | ||||||
|           <a class="title-link" href="" [routerLink]="['/blocks' | relativeUrl]"> |  | ||||||
|             <h5 class="card-title d-inline" i18n="dashboard.recent-blocks">Recent Blocks</h5> |  | ||||||
|             <span> </span> |  | ||||||
|             <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon> |  | ||||||
|           </a> |  | ||||||
|           <table class="table lastest-blocks-table"> |  | ||||||
|             <thead> |  | ||||||
|               <th class="table-cell-height" i18n="dashboard.latest-blocks.height">Height</th> |  | ||||||
|               <th class="table-cell-pool" i18n="mining.pool-name">Pool</th> |  | ||||||
|               <th class="table-cell-fee" i18n="block.median-fee">Median fee</th> |  | ||||||
|               <th class="table-cell-acceleration-count" i18n="accelerator.transaction-count">Accelerations</th> |  | ||||||
|             </thead> |  | ||||||
|             <tbody> |  | ||||||
|               <tr *ngFor="let block of blocks$ | async; let i = index"> |  | ||||||
|                 <td class="table-cell-height" ><a [routerLink]="['/block' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height }}</a></td> |  | ||||||
|                 <td class="table-cell-pool"> |  | ||||||
|                   <a class="clear-link" [routerLink]="[('/mining/pool/' + block.extras.pool.slug) | relativeUrl]"> |  | ||||||
|                     <img width="22" height="22" src="{{ block.extras.pool['logo'] }}" |  | ||||||
|                       onError="this.src = '/resources/mining-pools/default.svg'"> |  | ||||||
|                     <span class="pool-name">{{ block.extras.pool.name }}</span> |  | ||||||
|                   </a> |  | ||||||
|                 </td> |  | ||||||
|                 <td class="table-cell-fee" ><app-fee-rate [fee]="block?.extras?.medianFee" rounding="1.0-0"></app-fee-rate></td> |  | ||||||
|                 <td class="table-cell-acceleration-count">{{ block.accelerationCount | number }}</td> |  | ||||||
|               </tr> |  | ||||||
|             </tbody> |  | ||||||
|           </table> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     </div> |  | ||||||
| 
 |  | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -133,3 +133,16 @@ | |||||||
|     width: 20%; |     width: 20%; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .card { | ||||||
|  |   height: 385px; | ||||||
|  | } | ||||||
|  | .list-card { | ||||||
|  |   height: 410px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .mempool-block-wrapper { | ||||||
|  |   max-height: 380px; | ||||||
|  |   max-width: 380px; | ||||||
|  |   margin: auto; | ||||||
|  | } | ||||||
| @ -3,8 +3,15 @@ import { SeoService } from '../../../services/seo.service'; | |||||||
| import { WebsocketService } from '../../../services/websocket.service'; | import { WebsocketService } from '../../../services/websocket.service'; | ||||||
| import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; | ||||||
| import { StateService } from '../../../services/state.service'; | import { StateService } from '../../../services/state.service'; | ||||||
| import { Observable, Subject, catchError, combineLatest, distinctUntilChanged, of, share, switchMap, tap } from 'rxjs'; | import { Observable, Subject, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs'; | ||||||
| import { ApiService } from '../../../services/api.service'; | import { ApiService } from '../../../services/api.service'; | ||||||
|  | import { Color } from '../../block-overview-graph/sprite-types'; | ||||||
|  | import { hexToColor } from '../../block-overview-graph/utils'; | ||||||
|  | import TxView from '../../block-overview-graph/tx-view'; | ||||||
|  | import { feeLevels, mempoolFeeColors } from '../../../app.constants'; | ||||||
|  | 
 | ||||||
|  | const acceleratedColor: Color = hexToColor('8F5FF6'); | ||||||
|  | const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F')); | ||||||
| 
 | 
 | ||||||
| interface AccelerationBlock extends BlockExtended { | interface AccelerationBlock extends BlockExtended { | ||||||
|   accelerationCount: number, |   accelerationCount: number, | ||||||
| @ -19,6 +26,8 @@ interface AccelerationBlock extends BlockExtended { | |||||||
| export class AcceleratorDashboardComponent implements OnInit { | export class AcceleratorDashboardComponent implements OnInit { | ||||||
|   blocks$: Observable<AccelerationBlock[]>; |   blocks$: Observable<AccelerationBlock[]>; | ||||||
|   accelerations$: Observable<Acceleration[]>; |   accelerations$: Observable<Acceleration[]>; | ||||||
|  |   pendingAccelerations$: Observable<Acceleration[]>; | ||||||
|  |   minedAccelerations$: Observable<Acceleration[]>; | ||||||
|   loadingBlocks: boolean = true; |   loadingBlocks: boolean = true; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
| @ -33,6 +42,17 @@ export class AcceleratorDashboardComponent implements OnInit { | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); |     this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); | ||||||
| 
 | 
 | ||||||
|  |     this.pendingAccelerations$ = interval(30000).pipe( | ||||||
|  |       startWith(true), | ||||||
|  |       switchMap(() => { | ||||||
|  |         return this.apiService.getAccelerations$(); | ||||||
|  |       }), | ||||||
|  |       catchError((e) => { | ||||||
|  |         return of([]); | ||||||
|  |       }), | ||||||
|  |       share(), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|     this.accelerations$ = this.stateService.chainTip$.pipe( |     this.accelerations$ = this.stateService.chainTip$.pipe( | ||||||
|       distinctUntilChanged(), |       distinctUntilChanged(), | ||||||
|       switchMap((chainTip) => { |       switchMap((chainTip) => { | ||||||
| @ -44,6 +64,12 @@ export class AcceleratorDashboardComponent implements OnInit { | |||||||
|       share(), |       share(), | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     this.minedAccelerations$ = this.accelerations$.pipe( | ||||||
|  |       map(accelerations => { | ||||||
|  |         return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status)) | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|     this.blocks$ = combineLatest([ |     this.blocks$ = combineLatest([ | ||||||
|       this.accelerations$, |       this.accelerations$, | ||||||
|       this.stateService.blocks$.pipe( |       this.stateService.blocks$.pipe( | ||||||
| @ -83,4 +109,14 @@ export class AcceleratorDashboardComponent implements OnInit { | |||||||
|       }) |       }) | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getAcceleratorColor(tx: TxView): Color { | ||||||
|  |     if (tx.status === 'accelerated' || tx.acc) { | ||||||
|  |       return acceleratedColor; | ||||||
|  |     } else { | ||||||
|  |       const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
 | ||||||
|  |       const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; | ||||||
|  |       return normalColors[feeLevelIndex] || normalColors[mempoolFeeColors.length - 1]; | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ | |||||||
|       <h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5> |       <h5 class="card-title" i18n="accelerator.total-vsize">Total vsize</h5> | ||||||
|       <div class="card-text"> |       <div class="card-text"> | ||||||
|         <div [innerHTML]="'‎' + (stats.totalVsize * 4 | vbytes: 2)"></div> |         <div [innerHTML]="'‎' + (stats.totalVsize * 4 | vbytes: 2)"></div> | ||||||
|         <div class="symbol">{{ (stats.totalVsize / 1_000_000).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div> |         <div class="symbol">{{ (stats.totalVsize / 1_000_000 * 100).toFixed(2) }}% <span i18n="accelerator.percent-of-next-block"> of next block</span></div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core | |||||||
| import { Observable, of } from 'rxjs'; | import { Observable, of } from 'rxjs'; | ||||||
| import { switchMap } from 'rxjs/operators'; | import { switchMap } from 'rxjs/operators'; | ||||||
| import { ApiService } from '../../../services/api.service'; | import { ApiService } from '../../../services/api.service'; | ||||||
|  | import { Acceleration } from '../../../interfaces/node-api.interface'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-pending-stats', |   selector: 'app-pending-stats', | ||||||
| @ -10,6 +11,7 @@ import { ApiService } from '../../../services/api.service'; | |||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class PendingStatsComponent implements OnInit { | export class PendingStatsComponent implements OnInit { | ||||||
|  |   @Input() accelerations$: Observable<Acceleration[]>; | ||||||
|   public accelerationStats$: Observable<any>; |   public accelerationStats$: Observable<any>; | ||||||
| 
 | 
 | ||||||
|   constructor( |   constructor( | ||||||
| @ -17,7 +19,7 @@ export class PendingStatsComponent implements OnInit { | |||||||
|   ) { } |   ) { } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.accelerationStats$ = this.apiService.getAccelerations$().pipe( |     this.accelerationStats$ = (this.accelerations$ || this.apiService.getAccelerations$()).pipe( | ||||||
|       switchMap(accelerations => { |       switchMap(accelerations => { | ||||||
|         let totalAccelerations = 0; |         let totalAccelerations = 0; | ||||||
|         let totalFeeDelta = 0; |         let totalFeeDelta = 0; | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ import { FastVertexArray } from './fast-vertex-array'; | |||||||
| import BlockScene from './block-scene'; | import BlockScene from './block-scene'; | ||||||
| import TxSprite from './tx-sprite'; | import TxSprite from './tx-sprite'; | ||||||
| import TxView from './tx-view'; | import TxView from './tx-view'; | ||||||
| import { Position } from './sprite-types'; | import { Color, Position } from './sprite-types'; | ||||||
| import { Price } from '../../services/price.service'; | import { Price } from '../../services/price.service'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
| import { Subscription } from 'rxjs'; | import { Subscription } from 'rxjs'; | ||||||
| @ -27,6 +27,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|   @Input() unavailable: boolean = false; |   @Input() unavailable: boolean = false; | ||||||
|   @Input() auditHighlighting: boolean = false; |   @Input() auditHighlighting: boolean = false; | ||||||
|   @Input() blockConversion: Price; |   @Input() blockConversion: Price; | ||||||
|  |   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||||
|   @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); |   @Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>(); | ||||||
|   @Output() txHoverEvent = new EventEmitter<string>(); |   @Output() txHoverEvent = new EventEmitter<string>(); | ||||||
|   @Output() readyEvent = new EventEmitter(); |   @Output() readyEvent = new EventEmitter(); | ||||||
| @ -91,6 +92,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     if (changes.auditHighlighting) { |     if (changes.auditHighlighting) { | ||||||
|       this.setHighlightingEnabled(this.auditHighlighting); |       this.setHighlightingEnabled(this.auditHighlighting); | ||||||
|     } |     } | ||||||
|  |     if (changes.overrideColor) { | ||||||
|  |       this.scene.setColorFunction(this.overrideColors); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnDestroy(): void { |   ngOnDestroy(): void { | ||||||
| @ -228,7 +232,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On | |||||||
|     } else { |     } else { | ||||||
|       this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, |       this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution, | ||||||
|         blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, |         blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray, | ||||||
|         highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset }); |         highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset, | ||||||
|  |         colorFunction: this.overrideColors }); | ||||||
|       this.start(); |       this.start(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -1,12 +1,26 @@ | |||||||
| import { FastVertexArray } from './fast-vertex-array'; | import { FastVertexArray } from './fast-vertex-array'; | ||||||
| import TxView from './tx-view'; | import TxView from './tx-view'; | ||||||
| import { TransactionStripped } from '../../interfaces/websocket.interface'; | import { TransactionStripped } from '../../interfaces/websocket.interface'; | ||||||
| import { Position, Square, ViewUpdateParams } from './sprite-types'; | import { Color, Position, Square, ViewUpdateParams } from './sprite-types'; | ||||||
|  | import { feeLevels, mempoolFeeColors } from '../../app.constants'; | ||||||
|  | import { darken, desaturate, hexToColor } from './utils'; | ||||||
|  | 
 | ||||||
|  | const feeColors = mempoolFeeColors.map(hexToColor); | ||||||
|  | const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); | ||||||
|  | const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); | ||||||
|  | const auditColors = { | ||||||
|  |   censored: hexToColor('f344df'), | ||||||
|  |   missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), | ||||||
|  |   added: hexToColor('0099ff'), | ||||||
|  |   selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), | ||||||
|  |   accelerated: hexToColor('8F5FF6'), | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| export default class BlockScene { | export default class BlockScene { | ||||||
|   scene: { count: number, offset: { x: number, y: number}}; |   scene: { count: number, offset: { x: number, y: number}}; | ||||||
|   vertexArray: FastVertexArray; |   vertexArray: FastVertexArray; | ||||||
|   txs: { [key: string]: TxView }; |   txs: { [key: string]: TxView }; | ||||||
|  |   getColor: ((tx: TxView) => Color) = defaultColorFunction; | ||||||
|   orientation: string; |   orientation: string; | ||||||
|   flip: boolean; |   flip: boolean; | ||||||
|   animationDuration: number = 1000; |   animationDuration: number = 1000; | ||||||
| @ -26,11 +40,11 @@ export default class BlockScene { | |||||||
|   animateUntil = 0; |   animateUntil = 0; | ||||||
|   dirty: boolean; |   dirty: boolean; | ||||||
| 
 | 
 | ||||||
|   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: |   constructor({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }: | ||||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, |       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } |         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||||
|   ) { |   ) { | ||||||
|     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }); |     this.init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { |   resize({ width = this.width, height = this.height, animate = true }: { width?: number, height?: number, animate: boolean }): void { | ||||||
| @ -63,6 +77,14 @@ export default class BlockScene { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { | ||||||
|  |     this.getColor = colorFunction; | ||||||
|  |     this.dirty = true; | ||||||
|  |     if (this.initialised && this.scene) { | ||||||
|  |       this.updateColors(performance.now(), 50); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   // Destroy the current layout and clean up graphics sprites without any exit animation
 |   // Destroy the current layout and clean up graphics sprites without any exit animation
 | ||||||
|   destroy(): void { |   destroy(): void { | ||||||
|     Object.values(this.txs).forEach(tx => tx.destroy()); |     Object.values(this.txs).forEach(tx => tx.destroy()); | ||||||
| @ -86,7 +108,7 @@ export default class BlockScene { | |||||||
|       this.applyTxUpdate(txView, { |       this.applyTxUpdate(txView, { | ||||||
|         display: { |         display: { | ||||||
|           position: txView.screenPosition, |           position: txView.screenPosition, | ||||||
|           color: txView.getColor() |           color: this.getColor(txView) | ||||||
|         }, |         }, | ||||||
|         duration: 0 |         duration: 0 | ||||||
|       }); |       }); | ||||||
| @ -217,9 +239,9 @@ export default class BlockScene { | |||||||
|     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); |     this.animateUntil = Math.max(this.animateUntil, tx.setHighlight(value)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting }: |   private init({ width, height, resolution, blockLimit, animationDuration, animationOffset, orientation, flip, vertexArray, highlighting, colorFunction }: | ||||||
|       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, |       { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, | ||||||
|         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean } |         orientation: string, flip: boolean, vertexArray: FastVertexArray, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } | ||||||
|   ): void { |   ): void { | ||||||
|     this.animationDuration = animationDuration || 1000; |     this.animationDuration = animationDuration || 1000; | ||||||
|     this.configAnimationOffset = animationOffset; |     this.configAnimationOffset = animationOffset; | ||||||
| @ -228,6 +250,7 @@ export default class BlockScene { | |||||||
|     this.flip = flip; |     this.flip = flip; | ||||||
|     this.vertexArray = vertexArray; |     this.vertexArray = vertexArray; | ||||||
|     this.highlightingEnabled = highlighting; |     this.highlightingEnabled = highlighting; | ||||||
|  |     this.getColor = colorFunction || defaultColorFunction; | ||||||
| 
 | 
 | ||||||
|     this.scene = { |     this.scene = { | ||||||
|       count: 0, |       count: 0, | ||||||
| @ -261,9 +284,23 @@ export default class BlockScene { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private updateColor(tx: TxView, startTime: number, delay: number, animate: boolean = true, duration: number = 500): void { | ||||||
|  |     if (tx.dirty || this.dirty) { | ||||||
|  |       const txColor = this.getColor(tx); | ||||||
|  |       this.applyTxUpdate(tx, { | ||||||
|  |         display: { | ||||||
|  |           color: txColor, | ||||||
|  |         }, | ||||||
|  |         start: startTime, | ||||||
|  |         delay, | ||||||
|  |         duration: animate ? duration : 0, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void { |   private setTxOnScreen(tx: TxView, startTime: number, delay: number = 50, direction: string = 'left', animate: boolean = true): void { | ||||||
|     if (!tx.initialised) { |     if (!tx.initialised) { | ||||||
|       const txColor = tx.getColor(); |       const txColor = this.getColor(tx); | ||||||
|       this.applyTxUpdate(tx, { |       this.applyTxUpdate(tx, { | ||||||
|         display: { |         display: { | ||||||
|           position: { |           position: { | ||||||
| @ -321,6 +358,15 @@ export default class BlockScene { | |||||||
|     this.dirty = false; |     this.dirty = false; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   private updateColors(startTime: number, delay: number = 50, animate: boolean = true, duration: number = 500): void { | ||||||
|  |     const ids = this.getTxList(); | ||||||
|  |     startTime = startTime || performance.now(); | ||||||
|  |     for (const id of ids) { | ||||||
|  |       this.updateColor(this.txs[id], startTime, delay, animate, duration); | ||||||
|  |     } | ||||||
|  |     this.dirty = false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   private remove(id: string, startTime: number, direction: string = 'left'): TxView | void { |   private remove(id: string, startTime: number, direction: string = 'left'): TxView | void { | ||||||
|     const tx = this.txs[id]; |     const tx = this.txs[id]; | ||||||
|     if (tx) { |     if (tx) { | ||||||
| @ -858,3 +904,48 @@ class BlockLayout { | |||||||
| function feeRateDescending(a: TxView, b: TxView) { | function feeRateDescending(a: TxView, b: TxView) { | ||||||
|   return b.feerate - a.feerate; |   return b.feerate - a.feerate; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | function defaultColorFunction(tx: TxView): Color { | ||||||
|  |   const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
 | ||||||
|  |   const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; | ||||||
|  |   const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; | ||||||
|  |   // Normal mode
 | ||||||
|  |   if (!tx.scene?.highlightingEnabled) { | ||||||
|  |     if (tx.acc) { | ||||||
|  |       return auditColors.accelerated; | ||||||
|  |     } else { | ||||||
|  |       return feeLevelColor; | ||||||
|  |     } | ||||||
|  |     return feeLevelColor; | ||||||
|  |   } | ||||||
|  |   // Block audit
 | ||||||
|  |   switch(tx.status) { | ||||||
|  |     case 'censored': | ||||||
|  |       return auditColors.censored; | ||||||
|  |     case 'missing': | ||||||
|  |     case 'sigop': | ||||||
|  |     case 'rbf': | ||||||
|  |       return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||||
|  |     case 'fresh': | ||||||
|  |     case 'freshcpfp': | ||||||
|  |       return auditColors.missing; | ||||||
|  |     case 'added': | ||||||
|  |       return auditColors.added; | ||||||
|  |     case 'selected': | ||||||
|  |       return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; | ||||||
|  |     case 'accelerated': | ||||||
|  |       return auditColors.accelerated; | ||||||
|  |     case 'found': | ||||||
|  |       if (tx.context === 'projected') { | ||||||
|  |         return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; | ||||||
|  |       } else { | ||||||
|  |         return feeLevelColor; | ||||||
|  |       } | ||||||
|  |     default: | ||||||
|  |       if (tx.acc) { | ||||||
|  |         return auditColors.accelerated; | ||||||
|  |       } else { | ||||||
|  |         return feeLevelColor; | ||||||
|  |       } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -2,24 +2,13 @@ import TxSprite from './tx-sprite'; | |||||||
| import { FastVertexArray } from './fast-vertex-array'; | import { FastVertexArray } from './fast-vertex-array'; | ||||||
| import { TransactionStripped } from '../../interfaces/websocket.interface'; | import { TransactionStripped } from '../../interfaces/websocket.interface'; | ||||||
| import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; | import { SpriteUpdateParams, Square, Color, ViewUpdateParams } from './sprite-types'; | ||||||
| import { feeLevels, mempoolFeeColors } from '../../app.constants'; | import { hexToColor } from './utils'; | ||||||
| import BlockScene from './block-scene'; | import BlockScene from './block-scene'; | ||||||
| 
 | 
 | ||||||
| const hoverTransitionTime = 300; | const hoverTransitionTime = 300; | ||||||
| const defaultHoverColor = hexToColor('1bd8f4'); | const defaultHoverColor = hexToColor('1bd8f4'); | ||||||
| const defaultHighlightColor = hexToColor('800080'); | const defaultHighlightColor = hexToColor('800080'); | ||||||
| 
 | 
 | ||||||
| const feeColors = mempoolFeeColors.map(hexToColor); |  | ||||||
| const auditFeeColors = feeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); |  | ||||||
| const marginalFeeColors = feeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); |  | ||||||
| const auditColors = { |  | ||||||
|   censored: hexToColor('f344df'), |  | ||||||
|   missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), |  | ||||||
|   added: hexToColor('0099ff'), |  | ||||||
|   selected: darken(desaturate(hexToColor('0099ff'), 0.3), 0.7), |  | ||||||
|   accelerated: hexToColor('8F5FF6'), |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| // convert from this class's update format to TxSprite's update format
 | // convert from this class's update format to TxSprite's update format
 | ||||||
| function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams { | function toSpriteUpdate(params: ViewUpdateParams): SpriteUpdateParams { | ||||||
|   return { |   return { | ||||||
| @ -195,77 +184,4 @@ export default class TxView implements TransactionStripped { | |||||||
|     this.dirty = false; |     this.dirty = false; | ||||||
|     return performance.now() + hoverTransitionTime; |     return performance.now() + hoverTransitionTime; | ||||||
|   } |   } | ||||||
| 
 |  | ||||||
|   getColor(): Color { |  | ||||||
|     const rate = this.fee / this.vsize; // color by simple single-tx fee rate
 |  | ||||||
|     const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; |  | ||||||
|     const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; |  | ||||||
|     // Normal mode
 |  | ||||||
|     if (!this.scene?.highlightingEnabled) { |  | ||||||
|       if (this.acc) { |  | ||||||
|         return auditColors.accelerated; |  | ||||||
|       } else { |  | ||||||
|         return feeLevelColor; |  | ||||||
|       } |  | ||||||
|       return feeLevelColor; |  | ||||||
|     } |  | ||||||
|     // Block audit
 |  | ||||||
|     switch(this.status) { |  | ||||||
|       case 'censored': |  | ||||||
|         return auditColors.censored; |  | ||||||
|       case 'missing': |  | ||||||
|       case 'sigop': |  | ||||||
|       case 'rbf': |  | ||||||
|         return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; |  | ||||||
|       case 'fresh': |  | ||||||
|       case 'freshcpfp': |  | ||||||
|         return auditColors.missing; |  | ||||||
|       case 'added': |  | ||||||
|         return auditColors.added; |  | ||||||
|       case 'selected': |  | ||||||
|         return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; |  | ||||||
|       case 'accelerated': |  | ||||||
|         return auditColors.accelerated; |  | ||||||
|       case 'found': |  | ||||||
|         if (this.context === 'projected') { |  | ||||||
|           return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; |  | ||||||
|         } else { |  | ||||||
|           return feeLevelColor; |  | ||||||
|         } |  | ||||||
|       default: |  | ||||||
|         if (this.acc) { |  | ||||||
|           return auditColors.accelerated; |  | ||||||
|         } else { |  | ||||||
|           return feeLevelColor; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function hexToColor(hex: string): Color { |  | ||||||
|   return { |  | ||||||
|     r: parseInt(hex.slice(0, 2), 16) / 255, |  | ||||||
|     g: parseInt(hex.slice(2, 4), 16) / 255, |  | ||||||
|     b: parseInt(hex.slice(4, 6), 16) / 255, |  | ||||||
|     a: 1 |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function desaturate(color: Color, amount: number): Color { |  | ||||||
|   const gray = (color.r + color.g + color.b) / 6; |  | ||||||
|   return { |  | ||||||
|     r: color.r + ((gray - color.r) * amount), |  | ||||||
|     g: color.g + ((gray - color.g) * amount), |  | ||||||
|     b: color.b + ((gray - color.b) * amount), |  | ||||||
|     a: color.a, |  | ||||||
|   }; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function darken(color: Color, amount: number): Color { |  | ||||||
|   return { |  | ||||||
|     r: color.r * amount, |  | ||||||
|     g: color.g * amount, |  | ||||||
|     b: color.b * amount, |  | ||||||
|     a: color.a, |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										29
									
								
								frontend/src/app/components/block-overview-graph/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/app/components/block-overview-graph/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | |||||||
|  | import { Color } from './sprite-types'; | ||||||
|  | 
 | ||||||
|  | export function hexToColor(hex: string): Color { | ||||||
|  |   return { | ||||||
|  |     r: parseInt(hex.slice(0, 2), 16) / 255, | ||||||
|  |     g: parseInt(hex.slice(2, 4), 16) / 255, | ||||||
|  |     b: parseInt(hex.slice(4, 6), 16) / 255, | ||||||
|  |     a: hex.length > 6 ? parseInt(hex.slice(6, 8), 16) / 255 : 1 | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function desaturate(color: Color, amount: number): Color { | ||||||
|  |   const gray = (color.r + color.g + color.b) / 6; | ||||||
|  |   return { | ||||||
|  |     r: color.r + ((gray - color.r) * amount), | ||||||
|  |     g: color.g + ((gray - color.g) * amount), | ||||||
|  |     b: color.b + ((gray - color.b) * amount), | ||||||
|  |     a: color.a, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function darken(color: Color, amount: number): Color { | ||||||
|  |   return { | ||||||
|  |     r: color.r * amount, | ||||||
|  |     g: color.g * amount, | ||||||
|  |     b: color.b * amount, | ||||||
|  |     a: color.a, | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -5,5 +5,6 @@ | |||||||
|   [blockLimit]="stateService.blockVSize" |   [blockLimit]="stateService.blockVSize" | ||||||
|   [orientation]="timeLtr ? 'right' : 'left'" |   [orientation]="timeLtr ? 'right' : 'left'" | ||||||
|   [flip]="true" |   [flip]="true" | ||||||
|  |   [overrideColors]="overrideColors" | ||||||
|   (txClickEvent)="onTxClick($event)" |   (txClickEvent)="onTxClick($event)" | ||||||
| ></app-block-overview-graph> | ></app-block-overview-graph> | ||||||
|  | |||||||
| @ -8,6 +8,8 @@ import { switchMap, filter } from 'rxjs/operators'; | |||||||
| import { WebsocketService } from '../../services/websocket.service'; | import { WebsocketService } from '../../services/websocket.service'; | ||||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
| import { Router } from '@angular/router'; | import { Router } from '@angular/router'; | ||||||
|  | import { Color } from '../block-overview-graph/sprite-types'; | ||||||
|  | import TxView from '../block-overview-graph/tx-view'; | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-mempool-block-overview', |   selector: 'app-mempool-block-overview', | ||||||
| @ -16,6 +18,7 @@ import { Router } from '@angular/router'; | |||||||
| }) | }) | ||||||
| export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { | export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit { | ||||||
|   @Input() index: number; |   @Input() index: number; | ||||||
|  |   @Input() overrideColors: ((tx: TxView) => Color) | null = null; | ||||||
|   @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); |   @Output() txPreviewEvent = new EventEmitter<TransactionStripped | void>(); | ||||||
| 
 | 
 | ||||||
|   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; |   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user