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> | <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)"> | ||||||
|  | |||||||
| @ -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%; | ||||||
|  | |||||||
| @ -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,6 +73,12 @@ export class AddressGraphComponent implements OnChanges { | |||||||
|     if (!this.address || !this.stats) { |     if (!this.address || !this.stats) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     if (changes.address || changes.isPubkey || changes.addressSummary$) { | ||||||
|  |       if (this.subscription) { | ||||||
|  |         this.subscription.unsubscribe(); | ||||||
|  |       } | ||||||
|  |       this.subscription = combineLatest([ | ||||||
|  |         this.redraw$, | ||||||
|         (this.addressSummary$ || (this.isPubkey |         (this.addressSummary$ || (this.isPubkey | ||||||
|           ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') |           ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') | ||||||
|           : this.electrsApiService.getAddressSummary$(this.address)).pipe( |           : this.electrsApiService.getAddressSummary$(this.address)).pipe( | ||||||
| @ -77,7 +86,8 @@ export class AddressGraphComponent implements OnChanges { | |||||||
|             this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`; |             this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`; | ||||||
|             return of(null); |             return of(null); | ||||||
|           }), |           }), | ||||||
|     )).subscribe(addressSummary => { |         )) | ||||||
|  |       ]).subscribe(([redraw, addressSummary]) => { | ||||||
|         if (addressSummary) { |         if (addressSummary) { | ||||||
|           this.error = null; |           this.error = null; | ||||||
|           this.prepareChartOptions(addressSummary); |           this.prepareChartOptions(addressSummary); | ||||||
| @ -85,9 +95,16 @@ export class AddressGraphComponent implements OnChanges { | |||||||
|         this.isLoading = false; |         this.isLoading = false; | ||||||
|         this.cd.markForCheck(); |         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); | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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); | ||||||
|  | } | ||||||
| @ -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(); | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user