Merge pull request #5294 from mempool/mononaut/acc-fee-graph-fixes

[accelerator] improve rendering of acceleration fee rate graph
This commit is contained in:
softsimon 2024-07-09 01:01:27 +09:00 committed by GitHub
commit 2c81ebb637
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 82 additions and 29 deletions

View File

@ -1,4 +1,4 @@
<div class="fee-graph" *ngIf="tx && estimate"> <div class="fee-graph" *ngIf="tx && estimate" #feeGraph>
<div class="column"> <div class="column">
<ng-container *ngFor="let bar of bars"> <ng-container *ngFor="let bar of bars">
<div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);"> <div class="bar {{ bar.class }}" [class.active]="bar.active" [style]="bar.style" (click)="onClick($event, bar);">

View File

@ -1,20 +1,16 @@
import { Component, OnInit, Input, Output, OnChanges, EventEmitter, HostListener, Inject, LOCALE_ID } from '@angular/core'; import { Component, Input, Output, OnChanges, EventEmitter, HostListener, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
import { StateService } from '../../services/state.service'; import { Transaction } from '../../interfaces/electrs.interface';
import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.interface';
import { Router } from '@angular/router';
import { ReplaySubject, merge, Subscription, of } from 'rxjs';
import { tap, switchMap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
import { AccelerationEstimate, RateOption } from './accelerate-checkout.component'; import { AccelerationEstimate, RateOption } from './accelerate-checkout.component';
interface GraphBar { interface GraphBar {
rate: number; rate: number;
style: any; style?: Record<string,string>;
class: 'tx' | 'target' | 'max'; class: 'tx' | 'target' | 'max';
label: string; label: string;
active?: boolean; active?: boolean;
rateIndex?: number; rateIndex?: number;
fee?: number; fee?: number;
height?: number;
} }
@Component({ @Component({
@ -22,7 +18,7 @@ interface GraphBar {
templateUrl: './accelerate-fee-graph.component.html', templateUrl: './accelerate-fee-graph.component.html',
styleUrls: ['./accelerate-fee-graph.component.scss'], styleUrls: ['./accelerate-fee-graph.component.scss'],
}) })
export class AccelerateFeeGraphComponent implements OnInit, OnChanges { export class AccelerateFeeGraphComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() estimate: AccelerationEstimate; @Input() estimate: AccelerationEstimate;
@Input() showEstimate = false; @Input() showEstimate = false;
@ -30,13 +26,37 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
@Input() maxRateIndex: number = 0; @Input() maxRateIndex: number = 0;
@Output() setUserBid = new EventEmitter<{ fee: number, index: number }>(); @Output() setUserBid = new EventEmitter<{ fee: number, index: number }>();
@ViewChild('feeGraph')
container: ElementRef<HTMLDivElement>;
height: number;
observer: ResizeObserver;
stopResizeLoop = false;
bars: GraphBar[] = []; bars: GraphBar[] = [];
tooltipPosition = { x: 0, y: 0 }; tooltipPosition = { x: 0, y: 0 };
constructor(
private cd: ChangeDetectorRef,
) {}
ngOnInit(): void { ngOnInit(): void {
this.initGraph(); this.initGraph();
} }
ngAfterViewInit(): void {
if (ResizeObserver) {
this.observer = new ResizeObserver(entries => {
for (const entry of entries) {
this.height = entry.contentRect.height;
this.initGraph();
}
});
this.observer.observe(this.container.nativeElement);
} else {
this.startResizeFallbackLoop();
}
}
ngOnChanges(): void { ngOnChanges(): void {
this.initGraph(); this.initGraph();
} }
@ -45,44 +65,61 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
if (!this.tx || !this.estimate) { if (!this.tx || !this.estimate) {
return; return;
} }
const hasNextBlockRate = (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee);
const numBars = hasNextBlockRate ? 4 : 3;
const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate)); const maxRate = Math.max(...this.maxRateOptions.map(option => option.rate));
const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize; const baseRate = this.estimate.txSummary.effectiveFee / this.estimate.txSummary.effectiveVsize;
const baseHeight = baseRate / maxRate; let baseHeight = Math.max(this.height - (numBars * 30), this.height * (baseRate / maxRate));
const bars: GraphBar[] = this.maxRateOptions.slice().reverse().map(option => { const bars: GraphBar[] = [];
return { let lastHeight = 0;
rate: option.rate, if (hasNextBlockRate) {
style: this.getStyle(option.rate, maxRate, baseHeight), lastHeight = Math.max(lastHeight + 30, (this.height * ((this.estimate.targetFeeRate - baseRate) / maxRate)));
class: 'max',
label: this.showEstimate ? $localize`maximum` : $localize`:@@25fbf6e80a945703c906a5a7d8c92e8729c7ab21:accelerated`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
}
});
if (this.estimate.nextBlockFee > this.estimate.txSummary.effectiveFee) {
bars.push({ bars.push({
rate: this.estimate.targetFeeRate, rate: this.estimate.targetFeeRate,
style: this.getStyle(this.estimate.targetFeeRate, maxRate, baseHeight), height: lastHeight,
class: 'target', class: 'target',
label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(), label: $localize`:@@bdf0e930eb22431140a2eaeacd809cc5f8ebd38c:Next Block`.toLowerCase(),
fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee fee: this.estimate.nextBlockFee - this.estimate.txSummary.effectiveFee
}); });
} }
this.maxRateOptions.forEach((option, index) => {
lastHeight = Math.max(lastHeight + 30, (this.height * ((option.rate - baseRate) / maxRate)));
bars.push({
rate: option.rate,
height: lastHeight,
class: 'max',
label: this.showEstimate ? $localize`maximum` : $localize`:@@25fbf6e80a945703c906a5a7d8c92e8729c7ab21:accelerated`,
active: option.index === this.maxRateIndex,
rateIndex: option.index,
fee: option.fee,
})
})
bars.reverse();
baseHeight = this.height - lastHeight;
for (const bar of bars) {
bar.style = this.getStyle(bar.height, baseHeight);
}
bars.push({ bars.push({
rate: baseRate, rate: baseRate,
style: this.getStyle(baseRate, maxRate, 0), style: this.getStyle(baseHeight, 0),
height: baseHeight,
class: 'tx', class: 'tx',
label: '', label: '',
fee: this.estimate.txSummary.effectiveFee, fee: this.estimate.txSummary.effectiveFee,
}); });
this.bars = bars; this.bars = bars;
this.cd.detectChanges();
} }
getStyle(rate, maxRate, base) { getStyle(height: number, base: number): Record<string,string> {
const top = (rate / maxRate);
return { return {
height: `${(top - base) * 100}%`, height: `${height}px`,
bottom: base ? `${base * 100}%` : '0', bottom: base ? `${base}px` : '0',
} }
} }
@ -96,4 +133,20 @@ export class AccelerateFeeGraphComponent implements OnInit, OnChanges {
onPointerMove(event) { onPointerMove(event) {
this.tooltipPosition = { x: event.offsetX, y: event.offsetY }; this.tooltipPosition = { x: event.offsetX, y: event.offsetY };
} }
startResizeFallbackLoop(): void {
if (this.stopResizeLoop) {
return;
}
requestAnimationFrame(() => {
this.height = this.container?.nativeElement?.clientHeight || 0;
this.initGraph();
this.startResizeFallbackLoop();
});
}
ngOnDestroy(): void {
this.stopResizeLoop = true;
this.observer.disconnect();
}
} }