Merge pull request #5026 from mempool/mononaut/balance-graph-span-toggle
Polish address balance graph, add period toggle
This commit is contained in:
		
						commit
						8ec5dd70e0
					
				| @ -1,12 +1,6 @@ | ||||
| <app-indexing-progress *ngIf="!widget"></app-indexing-progress> | ||||
| 
 | ||||
| <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"> | ||||
|     <div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||
|       (chartInit)="onChartInit($event)"> | ||||
|  | ||||
| @ -45,23 +45,8 @@ | ||||
|   display: flex; | ||||
|   flex: 1; | ||||
|   width: 100%; | ||||
|   padding-bottom: 20px; | ||||
|   padding-bottom: 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 { | ||||
|   width: 100%; | ||||
|  | ||||
| @ -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 { Observable, of } from 'rxjs'; | ||||
| import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; | ||||
| import { catchError } from 'rxjs/operators'; | ||||
| import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; | ||||
| import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||
| @ -32,7 +32,7 @@ const periodSeconds = { | ||||
|   `],
 | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class AddressGraphComponent implements OnChanges { | ||||
| export class AddressGraphComponent implements OnChanges, OnDestroy { | ||||
|   @Input() address: string; | ||||
|   @Input() isPubkey: boolean = false; | ||||
|   @Input() stats: ChainStats; | ||||
| @ -46,6 +46,9 @@ export class AddressGraphComponent implements OnChanges { | ||||
|   data: any[] = []; | ||||
|   hoverData: any[] = []; | ||||
| 
 | ||||
|   subscription: Subscription; | ||||
|   redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false); | ||||
| 
 | ||||
|   chartOptions: EChartsOption = {}; | ||||
|   chartInitOptions = { | ||||
|     renderer: 'svg', | ||||
| @ -70,24 +73,38 @@ export class AddressGraphComponent implements OnChanges { | ||||
|     if (!this.address || !this.stats) { | ||||
|       return; | ||||
|     } | ||||
|     (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(addressSummary => { | ||||
|       if (addressSummary) { | ||||
|         this.error = null; | ||||
|         this.prepareChartOptions(addressSummary); | ||||
|     if (changes.address || changes.isPubkey || changes.addressSummary$) { | ||||
|       if (this.subscription) { | ||||
|         this.subscription.unsubscribe(); | ||||
|       } | ||||
|       this.isLoading = false; | ||||
|       this.cd.markForCheck(); | ||||
|     }); | ||||
|       this.subscription = combineLatest([ | ||||
|         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 { | ||||
|     if (!summary || !this.stats) { | ||||
|       return; | ||||
|     } | ||||
|     let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); | ||||
|     this.data = summary.map(d => { | ||||
|       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 minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue); | ||||
|     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); | ||||
| 
 | ||||
|     this.chartOptions = { | ||||
|       color: [ | ||||
| @ -230,6 +247,10 @@ export class AddressGraphComponent implements OnChanges { | ||||
|     this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); | ||||
|   } | ||||
| 
 | ||||
|   ngOnDestroy(): void { | ||||
|     this.subscription.unsubscribe(); | ||||
|   } | ||||
| 
 | ||||
|   isMobile() { | ||||
|     return (window.innerWidth <= 767.98); | ||||
|   } | ||||
|  | ||||
| @ -53,10 +53,20 @@ | ||||
| 
 | ||||
|     <ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2"> | ||||
|       <br> | ||||
|       <div class="title-tx"> | ||||
|         <h2 class="text-left" i18n="address.balance-history">Balance History</h2> | ||||
|       </div> | ||||
|       <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="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> | ||||
|  | ||||
| @ -109,3 +109,19 @@ h1 { | ||||
|     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); | ||||
| } | ||||
| @ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
|   txCount = 0; | ||||
|   received = 0; | ||||
|   sent = 0; | ||||
|   now = Date.now() / 1000; | ||||
|   balancePeriod: 'all' | '1m' = 'all'; | ||||
| 
 | ||||
|   private tempTransactions: Transaction[]; | ||||
|   private timeTxIndexes: number[]; | ||||
| @ -174,6 +176,10 @@ export class AddressComponent implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         this.transactions = this.tempTransactions; | ||||
|         this.isLoadingTransactions = false; | ||||
| 
 | ||||
|         if (!this.showBalancePeriod()) { | ||||
|           this.setBalancePeriod('all'); | ||||
|         } | ||||
|       }, | ||||
|       (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; | ||||
|   } | ||||
| 
 | ||||
|   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() { | ||||
|     this.mainSubscription.unsubscribe(); | ||||
|     this.mempoolTxSubscription.unsubscribe(); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user