Merge pull request #5026 from mempool/mononaut/balance-graph-span-toggle

Polish address balance graph, add period toggle
This commit is contained in:
softsimon 2024-05-04 14:31:26 +07:00 committed by GitHub
commit 8ec5dd70e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 86 additions and 42 deletions

View File

@ -1,12 +1,6 @@
<app-indexing-progress *ngIf="!widget"></app-indexing-progress> <app-indexing-progress *ngIf="!widget"></app-indexing-progress>
<div [class.full-container]="!widget"> <div [class.full-container]="!widget">
<div *ngIf="!widget" class="card-header mb-0 mb-md-2">
<div class="d-flex d-md-block align-items-baseline">
<span i18n="address.balance-history">Balance History</span>
</div>
</div>
<ng-container *ngIf="!error"> <ng-container *ngIf="!error">
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" <div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)"> (chartInit)="onChartInit($event)">

View File

@ -45,23 +45,8 @@
display: flex; display: flex;
flex: 1; flex: 1;
width: 100%; width: 100%;
padding-bottom: 20px; padding-bottom: 10px;
padding-right: 10px; padding-right: 10px;
@media (max-width: 992px) {
padding-bottom: 25px;
}
@media (max-width: 829px) {
padding-bottom: 50px;
}
@media (max-width: 767px) {
padding-bottom: 25px;
}
@media (max-width: 629px) {
padding-bottom: 55px;
}
@media (max-width: 567px) {
padding-bottom: 55px;
}
} }
.chart-widget { .chart-widget {
width: 100%; width: 100%;

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts'; import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable, of } from 'rxjs'; import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service'; import { ElectrsApiService } from '../../services/electrs-api.service';
@ -32,7 +32,7 @@ const periodSeconds = {
`], `],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AddressGraphComponent implements OnChanges { export class AddressGraphComponent implements OnChanges, OnDestroy {
@Input() address: string; @Input() address: string;
@Input() isPubkey: boolean = false; @Input() isPubkey: boolean = false;
@Input() stats: ChainStats; @Input() stats: ChainStats;
@ -46,6 +46,9 @@ export class AddressGraphComponent implements OnChanges {
data: any[] = []; data: any[] = [];
hoverData: any[] = []; hoverData: any[] = [];
subscription: Subscription;
redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false);
chartOptions: EChartsOption = {}; chartOptions: EChartsOption = {};
chartInitOptions = { chartInitOptions = {
renderer: 'svg', renderer: 'svg',
@ -70,24 +73,38 @@ export class AddressGraphComponent implements OnChanges {
if (!this.address || !this.stats) { if (!this.address || !this.stats) {
return; return;
} }
(this.addressSummary$ || (this.isPubkey if (changes.address || changes.isPubkey || changes.addressSummary$) {
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') if (this.subscription) {
: this.electrsApiService.getAddressSummary$(this.address)).pipe( this.subscription.unsubscribe();
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
)).subscribe(addressSummary => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
} }
this.isLoading = false; this.subscription = combineLatest([
this.cd.markForCheck(); this.redraw$,
}); (this.addressSummary$ || (this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
catchError(e => {
this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`;
return of(null);
}),
))
]).subscribe(([redraw, addressSummary]) => {
if (addressSummary) {
this.error = null;
this.prepareChartOptions(addressSummary);
}
this.isLoading = false;
this.cd.markForCheck();
});
} else {
// re-trigger subscription
this.redraw$.next(true);
}
} }
prepareChartOptions(summary): void { prepareChartOptions(summary): void {
if (!summary || !this.stats) {
return;
}
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum);
this.data = summary.map(d => { this.data = summary.map(d => {
const balance = total; const balance = total;
@ -104,8 +121,8 @@ export class AddressGraphComponent implements OnChanges {
); );
} }
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] || d.value[1])), 0); const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0);
const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue); const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue);
this.chartOptions = { this.chartOptions = {
color: [ color: [
@ -230,6 +247,10 @@ export class AddressGraphComponent implements OnChanges {
this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
} }
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
isMobile() { isMobile() {
return (window.innerWidth <= 767.98); return (window.innerWidth <= 767.98);
} }

View File

@ -53,10 +53,20 @@
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2"> <ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br> <br>
<div class="title-tx">
<h2 class="text-left" i18n="address.balance-history">Balance History</h2>
</div>
<div class="box"> <div class="box">
<div class="widget-toggler" *ngIf="showBalancePeriod()">
<a href="" (click)="setBalancePeriod('all')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === 'all'}"><small i18n="all">all</small></a>
<span style="color: var(--transparent-fg); font-size: 8px"> | </span>
<a href="" (click)="setBalancePeriod('1m')" class="toggler-option"
[ngClass]="{'inactive': balancePeriod === '1m'}"><small i18n="recent">recent</small></a>
</div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" /> <app-address-graph [address]="addressString" [isPubkey]="address?.is_pubkey" [stats]="address.chain_stats" [period]="balancePeriod" />
</div> </div>
</div> </div>
</div> </div>

View File

@ -109,3 +109,19 @@ h1 {
flex-grow: 0.5; flex-grow: 0.5;
} }
} }
.widget-toggler {
font-size: 12px;
position: absolute;
top: -20px;
right: 3px;
text-align: right;
}
.toggler-option {
text-decoration: none;
}
.inactive {
color: var(--transparent-fg);
}

View File

@ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy {
txCount = 0; txCount = 0;
received = 0; received = 0;
sent = 0; sent = 0;
now = Date.now() / 1000;
balancePeriod: 'all' | '1m' = 'all';
private tempTransactions: Transaction[]; private tempTransactions: Transaction[];
private timeTxIndexes: number[]; private timeTxIndexes: number[];
@ -174,6 +176,10 @@ export class AddressComponent implements OnInit, OnDestroy {
this.transactions = this.tempTransactions; this.transactions = this.tempTransactions;
this.isLoadingTransactions = false; this.isLoadingTransactions = false;
if (!this.showBalancePeriod()) {
this.setBalancePeriod('all');
}
}, },
(error) => { (error) => {
console.log(error); console.log(error);
@ -296,6 +302,18 @@ export class AddressComponent implements OnInit, OnDestroy {
this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
} }
setBalancePeriod(period: 'all' | '1m'): boolean {
this.balancePeriod = period;
return false;
}
showBalancePeriod(): boolean {
return this.transactions?.length && (
!this.transactions[0].status?.confirmed
|| this.transactions[0].status.block_time > (this.now - (60 * 60 * 24 * 30))
);
}
ngOnDestroy() { ngOnDestroy() {
this.mainSubscription.unsubscribe(); this.mainSubscription.unsubscribe();
this.mempoolTxSubscription.unsubscribe(); this.mempoolTxSubscription.unsubscribe();