Redesign mempool block fee distribution graph

This commit is contained in:
Mononaut 2023-03-15 11:43:18 +09:00
parent 5f787db30d
commit e4f3642082
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
5 changed files with 100 additions and 12 deletions

View File

@ -1,5 +1,8 @@
import { OnChanges } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { StateService } from '../../services/state.service';
import { VbytesPipe } from '../../shared/pipes/bytes-pipe/vbytes.pipe';
@Component({
selector: 'app-fee-distribution-graph',
@ -7,41 +10,99 @@ import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeeDistributionGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() transactions: TransactionStripped[];
@Input() height: number | string = 210;
@Input() top: number | string = 20;
@Input() right: number | string = 22;
@Input() left: number | string = 30;
@Input() numSamples: number = 200;
@Input() numLabels: number = 10;
data: number[][];
labelInterval: number = 50;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesInitOptions = {
renderer: 'svg'
};
constructor() { }
constructor(
private stateService: StateService,
private vbytesPipe: VbytesPipe,
) { }
ngOnInit() {
this.mountChart();
}
ngOnChanges() {
this.prepareChart();
this.mountChart();
}
prepareChart() {
this.data = [];
if (!this.transactions?.length) {
return;
}
const samples = [];
const txs = this.transactions.map(tx => { return { vsize: tx.vsize, rate: tx.rate || (tx.fee / tx.vsize) }; }).sort((a, b) => { return b.rate - a.rate; });
const maxBlockVSize = this.stateService.env.BLOCK_WEIGHT_UNITS / 4;
const sampleInterval = maxBlockVSize / this.numSamples;
let cumVSize = 0;
let sampleIndex = 0;
let nextSample = 0;
let txIndex = 0;
this.labelInterval = this.numSamples / this.numLabels;
while (nextSample <= maxBlockVSize) {
if (txIndex >= txs.length) {
samples.push([1 - (sampleIndex / this.numSamples), 0]);
nextSample += sampleInterval;
sampleIndex++;
break;
}
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
samples.push([1 - (sampleIndex / this.numSamples), txs[txIndex].rate]);
nextSample += sampleInterval;
sampleIndex++;
}
cumVSize += txs[txIndex].vsize;
txIndex++;
}
this.data = samples.reverse();
}
mountChart() {
this.mempoolVsizeFeesOptions = {
grid: {
height: '210',
right: '20',
top: '22',
left: '30',
left: '40',
},
xAxis: {
type: 'category',
boundaryGap: false,
name: 'MvB',
nameLocation: 'middle',
nameGap: 0,
nameTextStyle: {
verticalAlign: 'top',
padding: [30, 0, 0, 0],
},
axisLabel: {
interval: (index: number): boolean => { return index && (index % this.labelInterval === 0); },
formatter: (value: number): string => { return Number(value).toFixed(1); },
},
axisTick: {
interval: (index:number): boolean => { return (index % this.labelInterval === 0); },
},
},
yAxis: {
type: 'value',
// name: 'Effective Fee Rate s/vb',
// nameLocation: 'middle',
splitLine: {
lineStyle: {
type: 'dotted',
@ -58,14 +119,13 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges {
position: 'top',
color: '#ffffff',
textShadowBlur: 0,
formatter: (label: any) => {
return Math.floor(label.data);
},
formatter: (label: any): string => '' + Math.floor(label.data[1]),
},
showAllSymbol: false,
smooth: true,
lineStyle: {
color: '#D81B60',
width: 4,
width: 1,
},
itemStyle: {
color: '#b71c1c',

View File

@ -39,11 +39,11 @@
</tr>
</tbody>
</table>
<app-fee-distribution-graph *ngIf="webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
<app-fee-distribution-graph *ngIf="webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" ></app-fee-distribution-graph>
</div>
<div class="col-md chart-container">
<app-mempool-block-overview *ngIf="webGlEnabled" [index]="mempoolBlockIndex" (txPreviewEvent)="setTxPreview($event)"></app-mempool-block-overview>
<app-fee-distribution-graph *ngIf="!webGlEnabled" [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
<app-fee-distribution-graph *ngIf="!webGlEnabled" [transactions]="mempoolBlockTransactions$ | async" ></app-fee-distribution-graph>
</div>
</div>
</div>

View File

@ -17,6 +17,7 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
network$: Observable<string>;
mempoolBlockIndex: number;
mempoolBlock$: Observable<MempoolBlock>;
mempoolBlockTransactions$: Observable<TransactionStripped[]>;
ordinal$: BehaviorSubject<string> = new BehaviorSubject('');
previewTx: TransactionStripped | void;
webGlEnabled: boolean;
@ -62,6 +63,8 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
})
);
this.mempoolBlockTransactions$ = this.stateService.liveMempoolBlockTransactions$.pipe(map(txMap => Object.values(txMap)));
this.network$ = this.stateService.networkChanged$;
}

View File

@ -1,11 +1,11 @@
import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs';
import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs';
import { Transaction } from '../interfaces/electrs.interface';
import { IBackendInfo, MempoolBlock, MempoolBlockWithTransactions, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, TransactionStripped } from '../interfaces/websocket.interface';
import { BlockExtended, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree } from '../interfaces/node-api.interface';
import { Router, NavigationStart } from '@angular/router';
import { isPlatformBrowser } from '@angular/common';
import { map, shareReplay } from 'rxjs/operators';
import { map, scan, shareReplay, tap } from 'rxjs/operators';
import { StorageService } from './storage.service';
interface MarkBlockState {
@ -100,6 +100,7 @@ export class StateService {
mempoolBlocks$ = new ReplaySubject<MempoolBlock[]>(1);
mempoolBlockTransactions$ = new Subject<TransactionStripped[]>();
mempoolBlockDelta$ = new Subject<MempoolBlockDelta>();
liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>;
txReplaced$ = new Subject<ReplacedTransaction>();
txRbfInfo$ = new Subject<RbfTree>();
rbfLatest$ = new Subject<RbfTree[]>();
@ -166,6 +167,30 @@ export class StateService {
this.blocks$ = new ReplaySubject<[BlockExtended, string]>(this.env.KEEP_BLOCKS_AMOUNT);
this.liveMempoolBlockTransactions$ = merge(
this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })),
this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })),
).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => {
if (change.transactions) {
const txMap = {}
change.transactions.forEach(tx => {
txMap[tx.txid] = tx;
})
return txMap;
} else {
change.delta.changed.forEach(tx => {
transactions[tx.txid].rate = tx.rate;
})
change.delta.removed.forEach(txid => {
delete transactions[txid];
});
change.delta.added.forEach(tx => {
transactions[tx.txid] = tx;
});
return transactions;
}
}, {}));
if (this.env.BASE_MODULE === 'bisq') {
this.network = this.env.BASE_MODULE;
this.networkChanged$.next(this.env.BASE_MODULE);

View File

@ -500,7 +500,7 @@ html:lang(ru) .card-title {
}
.fee-distribution-chart {
height: 250px;
height: 265px;
}
.fees-wrapper-tooltip-chart {