Merge pull request #5153 from mempool/natsoni/address-history-chart-usd
Add USD to address balance history chart
This commit is contained in:
		
						commit
						8b0015b3ff
					
				| @ -2,7 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| <div [class.full-container]="!widget"> | <div [class.full-container]="!widget"> | ||||||
|   <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, paddingBottom: !widget && !allowZoom ? '10px' : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" | ||||||
|       (chartInit)="onChartInit($event)"> |       (chartInit)="onChartInit($event)"> | ||||||
|     </div> |     </div> | ||||||
|     <div class="text-center loadingGraphs" *ngIf="isLoading"> |     <div class="text-center loadingGraphs" *ngIf="isLoading"> | ||||||
|  | |||||||
| @ -46,7 +46,6 @@ | |||||||
|   display: flex; |   display: flex; | ||||||
|   flex: 1; |   flex: 1; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   padding-bottom: 10px; |  | ||||||
|   padding-right: 10px; |   padding-right: 10px; | ||||||
| } | } | ||||||
| .chart-widget { | .chart-widget { | ||||||
|  | |||||||
| @ -1,13 +1,16 @@ | |||||||
| import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; | ||||||
| import { echarts, EChartsOption } from '../../graphs/echarts'; | import { echarts, EChartsOption } from '../../graphs/echarts'; | ||||||
| import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; | import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; | ||||||
| import { catchError } from 'rxjs/operators'; | import { catchError, map, switchMap, tap } 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'; | ||||||
| import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; | import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; | ||||||
| import { Router } from '@angular/router'; | import { Router } from '@angular/router'; | ||||||
| import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; | ||||||
| import { StateService } from '../../services/state.service'; | import { StateService } from '../../services/state.service'; | ||||||
|  | import { PriceService } from '../../services/price.service'; | ||||||
|  | import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; | ||||||
|  | import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; | ||||||
| 
 | 
 | ||||||
| const periodSeconds = { | const periodSeconds = { | ||||||
|   '1d': (60 * 60 * 24), |   '1d': (60 * 60 * 24), | ||||||
| @ -44,7 +47,13 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|   @Input() widget: boolean = false; |   @Input() widget: boolean = false; | ||||||
| 
 | 
 | ||||||
|   data: any[] = []; |   data: any[] = []; | ||||||
|  |   fiatData: any[] = []; | ||||||
|   hoverData: any[] = []; |   hoverData: any[] = []; | ||||||
|  |   conversions: any; | ||||||
|  |   allowZoom: boolean = false; | ||||||
|  |   initialRight = this.right; | ||||||
|  |   initialLeft = this.left; | ||||||
|  |   selected = { [$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]: true, 'Fiat': false }; | ||||||
| 
 | 
 | ||||||
|   subscription: Subscription; |   subscription: Subscription; | ||||||
|   redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false); |   redraw$: BehaviorSubject<boolean> = new BehaviorSubject(false); | ||||||
| @ -66,6 +75,10 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|     private amountShortenerPipe: AmountShortenerPipe, |     private amountShortenerPipe: AmountShortenerPipe, | ||||||
|     private cd: ChangeDetectorRef, |     private cd: ChangeDetectorRef, | ||||||
|     private relativeUrlPipe: RelativeUrlPipe, |     private relativeUrlPipe: RelativeUrlPipe, | ||||||
|  |     private priceService: PriceService, | ||||||
|  |     private fiatCurrencyPipe: FiatCurrencyPipe, | ||||||
|  |     private fiatShortenerPipe: FiatShortenerPipe, | ||||||
|  |     private zone: NgZone, | ||||||
|   ) {} |   ) {} | ||||||
| 
 | 
 | ||||||
|   ngOnChanges(changes: SimpleChanges): void { |   ngOnChanges(changes: SimpleChanges): void { | ||||||
| @ -86,10 +99,39 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|             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(([redraw, addressSummary]) => { |         this.stateService.conversions$ | ||||||
|  |       ]).pipe( | ||||||
|  |         switchMap(([redraw, addressSummary, conversions]) => { | ||||||
|  |           this.conversions = conversions; | ||||||
|  |           if (addressSummary) { | ||||||
|  |             let extendedSummary = this.extendSummary(addressSummary); | ||||||
|  |             return this.priceService.getPriceByBulk$(extendedSummary.map(d => d.time), 'USD').pipe( | ||||||
|  |               tap((prices) => { | ||||||
|  |                 if (prices.length !== extendedSummary.length) { | ||||||
|  |                   extendedSummary = extendedSummary.map(item => ({ ...item, price: 0 })); | ||||||
|  |                 } else { | ||||||
|  |                   extendedSummary = extendedSummary.map((item, index) => { | ||||||
|  |                     let price = 0; | ||||||
|  |                     if (prices[index].price) { | ||||||
|  |                       price = prices[index].price['USD']; | ||||||
|  |                     } else if (this.conversions && this.conversions['USD']) { | ||||||
|  |                       price = this.conversions['USD']; | ||||||
|  |                     } | ||||||
|  |                     return { ...item, price: price } | ||||||
|  |                   }); | ||||||
|  |                 } | ||||||
|  |               }), | ||||||
|  |               map(() => [redraw, extendedSummary, conversions]) | ||||||
|  |             ) | ||||||
|  |           } else { | ||||||
|  |             return of([redraw, addressSummary, conversions]); | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       ).subscribe(([redraw, addressSummary, conversions]) => { | ||||||
|         if (addressSummary) { |         if (addressSummary) { | ||||||
|           this.error = null; |           this.error = null; | ||||||
|  |           this.allowZoom = addressSummary.length > 100 && !this.widget; | ||||||
|           this.prepareChartOptions(addressSummary); |           this.prepareChartOptions(addressSummary); | ||||||
|         } |         } | ||||||
|         this.isLoading = false; |         this.isLoading = false; | ||||||
| @ -101,25 +143,37 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   prepareChartOptions(summary): void { |   prepareChartOptions(summary: AddressTxSummary[]) { | ||||||
|     if (!summary || !this.stats) { |     if (!summary || !this.stats) { | ||||||
|       return; |       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 => { |     const processData = summary.map(d => { | ||||||
|       const balance = total; |         const balance = total; | ||||||
|       total -= d.value; |         const fiatBalance = total * d.price / 100_000_000; | ||||||
|       return [d.time * 1000, balance, d]; |         total -= d.value; | ||||||
|  |         return { | ||||||
|  |             time: d.time * 1000, | ||||||
|  |             balance, | ||||||
|  |             fiatBalance, | ||||||
|  |             d | ||||||
|  |         }; | ||||||
|     }).reverse(); |     }).reverse(); | ||||||
|      |      | ||||||
|  |     this.data = processData.filter(({ d }) => d.txid !== undefined).map(({ time, balance, d }) => [time, balance, d]); | ||||||
|  |     this.fiatData = processData.map(({ time, fiatBalance, balance, d }) => [time, fiatBalance, d, balance]); | ||||||
|  | 
 | ||||||
|  |     const now = Date.now(); | ||||||
|     if (this.period !== 'all') { |     if (this.period !== 'all') { | ||||||
|       const now = Date.now(); |  | ||||||
|       const start = now - (periodSeconds[this.period] * 1000); |       const start = now - (periodSeconds[this.period] * 1000); | ||||||
|       this.data = this.data.filter(d => d[0] >= start); |       this.data = this.data.filter(d => d[0] >= start); | ||||||
|       this.data.push( |       const startFiat = this.data[0]?.[0] ?? start; // Make sure USD data starts at the same time as BTC data
 | ||||||
|         {value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }} |       this.fiatData = this.fiatData.filter(d => d[0] >= startFiat); | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|  |     this.data.push( | ||||||
|  |       {value: [now, this.stats.funded_txo_sum - this.stats.spent_txo_sum], symbol: 'none', tooltip: { show: false }} | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     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); | ||||||
| @ -130,14 +184,42 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|           { offset: 0, color: '#FDD835' }, |           { offset: 0, color: '#FDD835' }, | ||||||
|           { offset: 1, color: '#FB8C00' }, |           { offset: 1, color: '#FB8C00' }, | ||||||
|         ]), |         ]), | ||||||
|  |         new echarts.graphic.LinearGradient(0, 0, 0, 1, [ | ||||||
|  |           { offset: 0, color: '#4CAF50' }, | ||||||
|  |           { offset: 1, color: '#1B5E20' }, | ||||||
|  |         ]), | ||||||
|       ], |       ], | ||||||
|       animation: false, |       animation: false, | ||||||
|       grid: { |       grid: { | ||||||
|         top: 20, |         top: 20, | ||||||
|         bottom: 20, |         bottom: this.allowZoom ? 65 : 20, | ||||||
|         right: this.right, |         right: this.right, | ||||||
|         left: this.left, |         left: this.left, | ||||||
|       }, |       }, | ||||||
|  |       legend: { | ||||||
|  |         data: [ | ||||||
|  |           { | ||||||
|  |             name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`, | ||||||
|  |             inactiveColor: 'var(--grey)', | ||||||
|  |             textStyle: { | ||||||
|  |               color: 'white', | ||||||
|  |             }, | ||||||
|  |             icon: 'roundRect', | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             name: 'Fiat', | ||||||
|  |             inactiveColor: 'var(--grey)', | ||||||
|  |             textStyle: { | ||||||
|  |               color: 'white', | ||||||
|  |             }, | ||||||
|  |             icon: 'roundRect', | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         selected: this.selected, | ||||||
|  |         formatter: function (name) { | ||||||
|  |           return name === 'Fiat' ? 'USD' : 'BTC'; | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|       tooltip: { |       tooltip: { | ||||||
|         show: !this.isMobile(), |         show: !this.isMobile(), | ||||||
|         trigger: 'axis', |         trigger: 'axis', | ||||||
| @ -152,27 +234,64 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|           align: 'left', |           align: 'left', | ||||||
|         }, |         }, | ||||||
|         borderColor: '#000', |         borderColor: '#000', | ||||||
|         formatter: function (data): string { |         formatter: function (data) { | ||||||
|           if (!data?.length || !data[0]?.data?.[2]?.txid) { |           const btcData = data.filter(d => d.seriesName !== 'Fiat'); | ||||||
|  |           const fiatData = data.filter(d => d.seriesName === 'Fiat'); | ||||||
|  |           data = btcData.length ? btcData : fiatData; | ||||||
|  |           if ((!btcData.length || !btcData[0]?.data?.[2]?.txid) && !fiatData.length) { | ||||||
|             return ''; |             return ''; | ||||||
|           } |           } | ||||||
|           const header = data.length === 1 |           let tooltip = '<div>'; | ||||||
|  | 
 | ||||||
|  |           const hasTx = data[0].data[2].txid; | ||||||
|  |           if (hasTx) { | ||||||
|  |             const header = data.length === 1 | ||||||
|             ? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}` |             ? `${data[0].data[2].txid.slice(0, 6)}...${data[0].data[2].txid.slice(-6)}` | ||||||
|             : `${data.length} transactions`; |             : `${data.length} transactions`; | ||||||
|  |             tooltip += `<span><b>${header}</b></span>`; | ||||||
|  |           } | ||||||
|  |            | ||||||
|           const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); |           const date = new Date(data[0].data[0]).toLocaleTimeString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); | ||||||
|           const val = data.reduce((total, d) => total + d.data[2].value, 0); |            | ||||||
|           const color = val === 0 ? '' : (val > 0 ? 'var(--green)' : 'var(--red)'); |           tooltip += `<div>
 | ||||||
|           const symbol = val > 0 ? '+' : ''; |             <div style="text-align: right;">`;
 | ||||||
|           return ` |            | ||||||
|             <div> |           const formatBTC = (val, decimal) => (val / 100_000_000).toFixed(decimal); | ||||||
|               <span><b>${header}</b></span> |           const formatFiat = (val) => this.fiatCurrencyPipe.transform(val, null, 'USD'); | ||||||
|               <div style="text-align: right;"> |            | ||||||
|                 <span style="color: ${color}">${symbol} ${(val / 100_000_000).toFixed(8)} BTC</span><br> |           const btcVal = btcData.reduce((total, d) => total + d.data[2].value, 0); | ||||||
|                 <span>${(data[0].data[1] / 100_000_000).toFixed(8)} BTC</span> |           const fiatVal = fiatData.reduce((total, d) => total + d.data[2].value * d.data[2].price / 100_000_000, 0); | ||||||
|               </div> |           const btcColor = btcVal === 0 ? '' : (btcVal > 0 ? 'var(--green)' : 'var(--red)'); | ||||||
|               <span>${date}</span> |           const fiatColor = fiatVal === 0 ? '' : (fiatVal > 0 ? 'var(--green)' : 'var(--red)'); | ||||||
|  |           const btcSymbol = btcVal > 0 ? '+' : ''; | ||||||
|  |           const fiatSymbol = fiatVal > 0 ? '+' : ''; | ||||||
|  | 
 | ||||||
|  |           if (btcData.length && fiatData.length) { | ||||||
|  |             tooltip += `<div style="display: flex; justify-content: space-between; color: ${btcColor}">
 | ||||||
|  |               <span style="text-align: left; margin-right: 10px;">${btcSymbol} ${formatBTC(btcVal, 4)} BTC</span> | ||||||
|  |               <span style="text-align: right;">${fiatSymbol} ${formatFiat(fiatVal)}</span> | ||||||
|             </div> |             </div> | ||||||
|           `;
 |             <div style="display: flex; justify-content: space-between;"> | ||||||
|  |               <span style="text-align: left; margin-right: 10px;">${formatBTC(btcData[0].data[1], 4)} BTC</span> | ||||||
|  |               <span style="text-align: right;">${formatFiat(fiatData[0].data[1])}</span> | ||||||
|  |             </div>`;
 | ||||||
|  |           } else if (btcData.length) { | ||||||
|  |             tooltip += `<span style="color: ${btcColor}">${btcSymbol} ${formatBTC(btcVal, 8)} BTC</span><br>
 | ||||||
|  |               <span>${formatBTC(data[0].data[1], 8)} BTC</span>`;
 | ||||||
|  |           } else { | ||||||
|  |             if (this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`]) { | ||||||
|  |               tooltip += `<div style="display: flex; justify-content: space-between;">
 | ||||||
|  |                 <span style="text-align: left; margin-right: 10px;">${formatBTC(data[0].data[3], 4)} BTC</span> | ||||||
|  |                 <span style="text-align: right;">${formatFiat(data[0].data[1])}</span> | ||||||
|  |               </div>`;
 | ||||||
|  |             } else { | ||||||
|  |               tooltip += `${hasTx ? `<span style="color: ${fiatColor}">${fiatSymbol} ${formatFiat(fiatVal)}</span><br>` : ''} | ||||||
|  |               <span>${formatFiat(data[0].data[1])}</span>`;
 | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           tooltip += `</div><span>${date}</span></div>`; | ||||||
|  |           return tooltip; | ||||||
|         }.bind(this) |         }.bind(this) | ||||||
|       }, |       }, | ||||||
|       xAxis: { |       xAxis: { | ||||||
| @ -211,10 +330,24 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|           }, |           }, | ||||||
|           min: this.period === 'all' ? 0 : 'dataMin' |           min: this.period === 'all' ? 0 : 'dataMin' | ||||||
|         }, |         }, | ||||||
|  |         { | ||||||
|  |           type: 'value', | ||||||
|  |           axisLabel: { | ||||||
|  |             color: 'rgb(110, 112, 121)', | ||||||
|  |             formatter: function(val) { | ||||||
|  |               return this.fiatShortenerPipe.transform(val, null, 'USD'); | ||||||
|  |             }.bind(this) | ||||||
|  |           }, | ||||||
|  |           splitLine: { | ||||||
|  |             show: false, | ||||||
|  |           }, | ||||||
|  |           min: this.period === 'all' ? 0 : 'dataMin' | ||||||
|  |         }, | ||||||
|       ], |       ], | ||||||
|       series: [ |       series: [ | ||||||
|         { |         { | ||||||
|           name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`, |           name: $localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`, | ||||||
|  |           yAxisIndex: 0, | ||||||
|           showSymbol: false, |           showSymbol: false, | ||||||
|           symbol: 'circle', |           symbol: 'circle', | ||||||
|           symbolSize: 8, |           symbolSize: 8, | ||||||
| @ -226,14 +359,58 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|           type: 'line', |           type: 'line', | ||||||
|           smooth: false, |           smooth: false, | ||||||
|           step: 'end' |           step: 'end' | ||||||
|  |         },  | ||||||
|  |         { | ||||||
|  |           name: 'Fiat', | ||||||
|  |           yAxisIndex: 1, | ||||||
|  |           showSymbol: false, | ||||||
|  |           symbol: 'circle', | ||||||
|  |           symbolSize: 8, | ||||||
|  |           data: this.fiatData, | ||||||
|  |           areaStyle: { | ||||||
|  |             opacity: 0.5, | ||||||
|  |           }, | ||||||
|  |           triggerLineEvent: true, | ||||||
|  |           type: 'line', | ||||||
|  |           smooth: false, | ||||||
|  |           step: 'end' | ||||||
|         } |         } | ||||||
|       ], |       ], | ||||||
|  |       dataZoom: this.allowZoom ? [{ | ||||||
|  |         type: 'inside', | ||||||
|  |         realtime: true, | ||||||
|  |         zoomLock: true, | ||||||
|  |         maxSpan: 100, | ||||||
|  |         minSpan: 5, | ||||||
|  |         moveOnMouseMove: false, | ||||||
|  |       }, { | ||||||
|  |         showDetail: false, | ||||||
|  |         show: true, | ||||||
|  |         type: 'slider', | ||||||
|  |         brushSelect: false, | ||||||
|  |         realtime: true, | ||||||
|  |         left: this.left, | ||||||
|  |         right: this.right, | ||||||
|  |         selectedDataBackground: { | ||||||
|  |           lineStyle: { | ||||||
|  |             color: '#fff', | ||||||
|  |             opacity: 0.45, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }] : undefined | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   onChartClick(e) { |   onChartClick(e) { | ||||||
|     if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) { |     if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) { | ||||||
|       this.router.navigate([this.relativeUrlPipe.transform('/tx/'), this.hoverData[0][2].txid]); |       this.zone.run(() => {  | ||||||
|  |         const url = this.relativeUrlPipe.transform(`/tx/${this.hoverData[0][2].txid}`); | ||||||
|  |         if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) { | ||||||
|  |           window.open(url); | ||||||
|  |         } else { | ||||||
|  |           this.router.navigate([url]); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -241,10 +418,38 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|     this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]); |     this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   onLegendSelectChanged(e) { | ||||||
|  |     this.selected = e.selected; | ||||||
|  |     this.right = this.selected['Fiat'] ? +this.initialRight + 40 : this.initialRight; | ||||||
|  |     this.left = this.selected[$localize`:@@7e69426bd97a606d8ae6026762858e6e7c86a1fd:Balance`] ? this.initialLeft : +this.initialLeft - 40; | ||||||
|  | 
 | ||||||
|  |     this.chartOptions = { | ||||||
|  |       grid: { | ||||||
|  |         right: this.right, | ||||||
|  |         left: this.left, | ||||||
|  |       }, | ||||||
|  |       legend: { | ||||||
|  |         selected: this.selected, | ||||||
|  |       }, | ||||||
|  |       dataZoom: this.allowZoom ? [{ | ||||||
|  |         left: this.left, | ||||||
|  |         right: this.right, | ||||||
|  |       }, { | ||||||
|  |         left: this.left, | ||||||
|  |         right: this.right, | ||||||
|  |       }] : undefined | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     if (this.chartInstance) { | ||||||
|  |       this.chartInstance.setOption(this.chartOptions); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   onChartInit(ec) { |   onChartInit(ec) { | ||||||
|     this.chartInstance = ec; |     this.chartInstance = ec; | ||||||
|     this.chartInstance.on('showTip', this.onTooltip.bind(this)); |     this.chartInstance.on('showTip', this.onTooltip.bind(this)); | ||||||
|     this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); |     this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); | ||||||
|  |     this.chartInstance.on('legendselectchanged', this.onLegendSelectChanged.bind(this)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnDestroy(): void { |   ngOnDestroy(): void { | ||||||
| @ -256,4 +461,27 @@ export class AddressGraphComponent implements OnChanges, OnDestroy { | |||||||
|   isMobile() { |   isMobile() { | ||||||
|     return (window.innerWidth <= 767.98); |     return (window.innerWidth <= 767.98); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   extendSummary(summary) { | ||||||
|  |     let extendedSummary = summary.slice(); | ||||||
|  | 
 | ||||||
|  |     // Add a point at today's date to make the graph end at the current time
 | ||||||
|  |     extendedSummary.unshift({ time: Date.now() / 1000, value: 0 }); | ||||||
|  |     extendedSummary.reverse(); | ||||||
|  |      | ||||||
|  |     let oneHour = 60 * 60; | ||||||
|  |     // Fill gaps longer than interval
 | ||||||
|  |     for (let i = 0; i < extendedSummary.length - 1; i++) { | ||||||
|  |       let hours = Math.floor((extendedSummary[i + 1].time - extendedSummary[i].time) / oneHour);       | ||||||
|  |       if (hours > 1) { | ||||||
|  |         for (let j = 1; j < hours; j++) { | ||||||
|  |           let newTime = extendedSummary[i].time + oneHour * j; | ||||||
|  |           extendedSummary.splice(i + j, 0, { time: newTime, value: 0 }); | ||||||
|  |         } | ||||||
|  |         i += hours - 1; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     return extendedSummary.reverse(); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -58,7 +58,7 @@ export class AddressTransactionsWidgetComponent implements OnInit, OnChanges, On | |||||||
|         return summary?.slice(0, 6); |         return summary?.slice(0, 6); | ||||||
|       }), |       }), | ||||||
|       switchMap(txs => { |       switchMap(txs => { | ||||||
|         return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, true, this.currency).pipe( |         return (zip(txs.map(tx => this.priceService.getBlockPrice$(tx.time, txs.length < 3, this.currency).pipe( | ||||||
|           map(price => { |           map(price => { | ||||||
|             return { |             return { | ||||||
|               ...tx, |               ...tx, | ||||||
|  | |||||||
| @ -150,7 +150,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
|       this.transactions.forEach((tx) => { |       this.transactions.forEach((tx) => { | ||||||
|         if (!this.blockTime) { |         if (!this.blockTime) { | ||||||
|           if (tx.status.block_time) { |           if (tx.status.block_time) { | ||||||
|             this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe( |             this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 3, this.currency).pipe( | ||||||
|               tap((price) => tx['price'] = price), |               tap((price) => tx['price'] = price), | ||||||
|             ).subscribe(); |             ).subscribe(); | ||||||
|           } |           } | ||||||
| @ -235,7 +235,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { | |||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (!this.blockTime && tx.status.block_time && this.currency) { |         if (!this.blockTime && tx.status.block_time && this.currency) { | ||||||
|           this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 10, this.currency).pipe( |           this.priceService.getBlockPrice$(tx.status.block_time, confirmedTxs < 3, this.currency).pipe( | ||||||
|             tap((price) => tx['price'] = price), |             tap((price) => tx['price'] = price), | ||||||
|           ).subscribe(); |           ).subscribe(); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -156,6 +156,7 @@ export interface AddressTxSummary { | |||||||
|   value: number; |   value: number; | ||||||
|   height: number; |   height: number; | ||||||
|   time: number; |   time: number; | ||||||
|  |   price?: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export interface ChainStats { | export interface ChainStats { | ||||||
|  | |||||||
| @ -249,4 +249,75 @@ export class PriceService { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   getPriceByBulk$(timestamps: number[], currency: string): Observable<Price[]> { | ||||||
|  |     if (this.stateService.env.BASE_MODULE !== 'mempool' || !this.stateService.env.HISTORICAL_PRICE) { | ||||||
|  |       return of([]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const now = new Date().getTime() / 1000; | ||||||
|  | 
 | ||||||
|  |     if (!this.priceObservable$ || (this.priceObservable$ && (now - this.lastPriceHistoryUpdate > 3600 || currency !== this.lastQueriedHistoricalCurrency || this.networkChangedSinceLastQuery))) { | ||||||
|  |       this.priceObservable$ = this.apiService.getHistoricalPrice$(undefined, currency).pipe(shareReplay()); | ||||||
|  |       this.lastPriceHistoryUpdate = new Date().getTime() / 1000; | ||||||
|  |       this.lastQueriedHistoricalCurrency = currency; | ||||||
|  |       this.networkChangedSinceLastQuery = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return this.priceObservable$.pipe( | ||||||
|  |       map((conversion) => { | ||||||
|  |         if (!conversion) { | ||||||
|  |           return undefined; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const historicalPrice = { | ||||||
|  |           prices: {}, | ||||||
|  |           exchangeRates: conversion.exchangeRates, | ||||||
|  |         }; | ||||||
|  |         for (const price of conversion.prices) { | ||||||
|  |           historicalPrice.prices[price.time] = this.stateService.env.ADDITIONAL_CURRENCIES ? { | ||||||
|  |             USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, CHF: price.CHF, AUD: price.AUD,  | ||||||
|  |             JPY: price.JPY, BGN: price.BGN, BRL: price.BRL, CNY: price.CNY, CZK: price.CZK, DKK: price.DKK, | ||||||
|  |             HKD: price.HKD, HRK: price.HRK, HUF: price.HUF, IDR: price.IDR, ILS: price.ILS, INR: price.INR, | ||||||
|  |             ISK: price.ISK, KRW: price.KRW, MXN: price.MXN, MYR: price.MYR, NOK: price.NOK, NZD: price.NZD, | ||||||
|  |             PHP: price.PHP, PLN: price.PLN, RON: price.RON, RUB: price.RUB, SEK: price.SEK, SGD: price.SGD, | ||||||
|  |             THB: price.THB, TRY: price.TRY, ZAR: price.ZAR | ||||||
|  |           } : { | ||||||
|  |             USD: price.USD, EUR: price.EUR, GBP: price.GBP, CAD: price.CAD, CHF: price.CHF, AUD: price.AUD, JPY: price.JPY | ||||||
|  |           }; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         const priceTimestamps = Object.keys(historicalPrice.prices).map(Number); | ||||||
|  |         priceTimestamps.push(Number.MAX_SAFE_INTEGER); | ||||||
|  |         priceTimestamps.sort((a, b) => b - a); | ||||||
|  | 
 | ||||||
|  |         const prices: Price[] = []; | ||||||
|  | 
 | ||||||
|  |         for (const timestamp of timestamps) { | ||||||
|  |           let left = 0; | ||||||
|  |           let right = priceTimestamps.length - 1; | ||||||
|  |           let match = -1; | ||||||
|  | 
 | ||||||
|  |           // Binary search to find the closest larger element
 | ||||||
|  |           while (left <= right) { | ||||||
|  |             const mid = Math.floor((left + right) / 2); | ||||||
|  |             if (priceTimestamps[mid] > timestamp) { | ||||||
|  |               match = mid; | ||||||
|  |               left = mid + 1; | ||||||
|  |             } else { | ||||||
|  |               right = mid - 1; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (match !== -1) { | ||||||
|  |             const priceTimestamp = priceTimestamps[match]; | ||||||
|  |             prices.push({ | ||||||
|  |               price: historicalPrice.prices[priceTimestamp], | ||||||
|  |               exchangeRates: historicalPrice.exchangeRates, | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         return prices; | ||||||
|  |       })); | ||||||
|  |   } | ||||||
| } | } | ||||||
| @ -31,7 +31,7 @@ export class FiatShortenerPipe implements PipeTransform { | |||||||
|       { value: 1, symbol: '' }, |       { value: 1, symbol: '' }, | ||||||
|       { value: 1e3, symbol: 'k' }, |       { value: 1e3, symbol: 'k' }, | ||||||
|       { value: 1e6, symbol: 'M' }, |       { value: 1e6, symbol: 'M' }, | ||||||
|       { value: 1e9, symbol: 'G' }, |       { value: 1e9, symbol: 'B' }, | ||||||
|       { value: 1e12, symbol: 'T' }, |       { value: 1e12, symbol: 'T' }, | ||||||
|       { value: 1e15, symbol: 'P' }, |       { value: 1e15, symbol: 'P' }, | ||||||
|       { value: 1e18, symbol: 'E' } |       { value: 1e18, symbol: 'E' } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user