mempool/frontend/src/app/components/fee-distribution-graph/fee-distribution-graph.component.ts

201 lines
6.2 KiB
TypeScript
Raw Normal View History

import { HostListener, OnChanges, OnDestroy } 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';
import { selectPowerOfTen } from '../../bitcoin.utils';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-fee-distribution-graph',
templateUrl: './fee-distribution-graph.component.html',
styleUrls: ['./fee-distribution-graph.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestroy {
@Input() feeRange: number[];
@Input() vsize: number;
@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;
simple: boolean = false;
data: number[][];
labelInterval: number = 50;
smallScreen: boolean = window.innerWidth < 450;
rateUnitSub: Subscription;
weightMode: boolean = false;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesInitOptions = {
renderer: 'svg'
};
constructor(
2023-11-02 01:29:55 +00:00
public stateService: StateService,
private vbytesPipe: VbytesPipe,
) { }
ngOnInit() {
this.rateUnitSub = this.stateService.rateUnits$.subscribe(rateUnits => {
this.weightMode = rateUnits === 'wu';
if (this.data) {
this.mountChart();
}
});
}
ngOnChanges(): void {
this.simple = !!this.feeRange?.length;
this.prepareChart();
this.mountChart();
}
prepareChart(): void {
if (this.simple) {
this.data = this.feeRange.map((rate, index) => [index * 10, rate]);
this.labelInterval = 1;
return;
}
this.data = [];
if (!this.transactions?.length) {
return;
}
const samples = [];
2023-07-18 15:05:44 +09:00
const txs = this.transactions.filter(tx => !tx.acc).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)) * 100, 0.000001]);
nextSample += sampleInterval;
sampleIndex++;
2023-06-12 11:54:58 -04:00
continue;
}
while (txs[txIndex] && nextSample < cumVSize + txs[txIndex].vsize) {
samples.push([(1 - (sampleIndex / this.numSamples)) * 100, txs[txIndex].rate || 0.000001]);
nextSample += sampleInterval;
sampleIndex++;
}
cumVSize += txs[txIndex].vsize;
txIndex++;
}
this.data = samples.reverse();
}
mountChart(): void {
this.mempoolVsizeFeesOptions = {
grid: {
height: '210',
right: this.smallScreen ? '10' : '20',
top: '22',
left: this.smallScreen ? '10' : '40',
},
xAxis: {
type: 'category',
boundaryGap: false,
name: '% Weight',
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(0); },
},
axisTick: {
interval: (index:number): boolean => { return (index % this.labelInterval === 0); },
},
},
yAxis: {
type: 'log',
min: 1,
max: this.data.reduce((min, val) => Math.max(min, val[1]), 1),
// name: 'Effective Fee Rate s/vb',
// nameLocation: 'middle',
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
},
axisLabel: {
show: !this.smallScreen,
formatter: (value: number): string => {
const unitValue = this.weightMode ? value / 4 : value;
const selectedPowerOfTen = selectPowerOfTen(unitValue);
const scaledValue = unitValue / selectedPowerOfTen.divider;
const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3);
return `${newVal}${selectedPowerOfTen.unit}`;
},
},
axisTick: {
show: !this.smallScreen,
}
},
series: [{
data: this.data,
type: 'line',
label: {
show: true,
position: 'top',
color: '#ffffff',
textShadowBlur: 0,
fontSize: this.smallScreen ? 10 : 12,
formatter: (label: { data: number[] }): string => {
const value = label.data[1];
const unitValue = this.weightMode ? value / 4 : value;
const selectedPowerOfTen = selectPowerOfTen(unitValue);
const scaledValue = unitValue / selectedPowerOfTen.divider;
const newVal = scaledValue >= 100 ? Math.round(scaledValue) : scaledValue.toPrecision(3);
return `${newVal}${selectedPowerOfTen.unit}`;
}
},
showAllSymbol: false,
smooth: true,
lineStyle: {
color: '#D81B60',
width: 1,
},
itemStyle: {
color: '#b71c1c',
borderWidth: 10,
borderMiterLimit: 10,
opacity: 1,
},
areaStyle: {
color: '#D81B60',
opacity: 1,
}
}]
};
}
@HostListener('window:resize', ['$event'])
onResize(): void {
const isSmallScreen = window.innerWidth < 450;
if (this.smallScreen !== isSmallScreen) {
this.smallScreen = isSmallScreen;
this.prepareChart();
this.mountChart();
}
}
ngOnDestroy(): void {
this.rateUnitSub.unsubscribe();
}
}