From 42a3a380d5e56ed33a13d5db99757c8ff8e105ec Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 20 Jul 2023 16:25:09 +0900 Subject: [PATCH] widgetify block fee rates chart --- .../block-fee-rates-graph.component.html | 46 +++++++- .../block-fee-rates-graph.component.scss | 56 ++++++++- .../block-fee-rates-graph.component.ts | 107 +++++++++++++----- frontend/src/app/services/api.service.ts | 4 + 4 files changed, 176 insertions(+), 37 deletions(-) diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html index 77c35cea8..f5eb5d1cf 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.html @@ -1,13 +1,13 @@ - + -
-
+
+
Block Fee Rates -
+
@@ -45,11 +45,45 @@
-
+
+
+
Avg Block Fee (1m)
+

+ +

+
+
+
???
+

+ ??? +

+
+
+
+ +
-
\ No newline at end of file +
+ + +
+
+
Avg Block Fee
+

+ +

+
+
+
???
+

+ +

+
+
+
\ No newline at end of file diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss index f4f4dcc77..21dd458b5 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.scss @@ -57,7 +57,54 @@ .chart-widget { width: 100%; height: 100%; - max-height: 270px; + max-height: 238px; +} + +.block-fee-rates { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + display: inline-block; + margin: 0px auto 20px; + &:nth-child(2) { + order: 2; + @media (min-width: 485px) { + order: 3; + } + } + &:nth-child(3) { + order: 3; + @media (min-width: 485px) { + order: 2; + display: block; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: block; + } + } + .card-title { + font-size: 1rem; + color: #4a68b9; + } + .card-text { + font-size: 18px; + span { + color: #ffffff66; + font-size: 12px; + } + } + } } .formRadioGroup { @@ -85,6 +132,13 @@ } } +.skeleton-loader { + width: 100%; + display: block; + max-width: 80px; + margin: 15px auto 3px; +} + .disabled { pointer-events: none; opacity: 0.5; diff --git a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts index c4d061927..19c51527f 100644 --- a/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts +++ b/frontend/src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; -import { EChartsOption } from '../../graphs/echarts'; -import { Observable, Subscription, combineLatest } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; +import { EChartsOption, graphic } from 'echarts'; +import { Observable, Subscription, combineLatest, of } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { ApiService } from '../../services/api.service'; import { SeoService } from '../../services/seo.service'; @@ -29,6 +29,7 @@ import { ActivatedRoute, Router } from '@angular/router'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class BlockFeeRatesGraphComponent implements OnInit { + @Input() widget = false; @Input() right: number | string = 45; @Input() left: number | string = 75; @@ -57,39 +58,48 @@ export class BlockFeeRatesGraphComponent implements OnInit { private router: Router, private zone: NgZone, private route: ActivatedRoute, + private cd: ChangeDetectorRef, ) { this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`); - this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`); - this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); + if (this.widget) { + this.miningWindowPreference = '1m'; + } else { + this.seoService.setTitle($localize`:@@ed8e33059967f554ff06b4f5b6049c465b92d9b3:Block Fee Rates`); + this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fee-rates:See Bitcoin feerates visualized over time, including minimum and maximum feerates per block along with feerates at various percentiles.`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); + } this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); - this.route - .fragment - .subscribe((fragment) => { - if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) { - this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); - } - }); + if (!this.widget) { + this.route + .fragment + .subscribe((fragment) => { + if (['24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all'].indexOf(fragment) > -1) { + this.radioGroupForm.controls.dateSpan.setValue(fragment, { emitEvent: false }); + } + }); + } this.statsObservable$ = combineLatest([ - this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)), + this.widget ? of(this.miningWindowPreference) : this.radioGroupForm.get('dateSpan').valueChanges.pipe(startWith(this.radioGroupForm.controls.dateSpan.value)), this.stateService.rateUnits$ ]).pipe( switchMap(([timespan, rateUnits]) => { - this.storageService.setValue('miningWindowPreference', timespan); + if (!this.widget) { + this.storageService.setValue('miningWindowPreference', timespan); + } this.timespan = timespan; this.isLoading = true; return this.apiService.getHistoricalBlockFeeRates$(timespan) .pipe( tap((response) => { // Group by percentile - const seriesData = { + const seriesData = this.widget ? { 'Median': [] } : { 'Min': [], '10th': [], '25th': [], @@ -100,13 +110,17 @@ export class BlockFeeRatesGraphComponent implements OnInit { }; for (const rate of response.body) { const timestamp = rate.timestamp * 1000; - seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]); - seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]); - seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]); - seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]); - seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]); - seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]); - seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]); + if (this.widget) { + seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]); + } else { + seriesData['Min'].push([timestamp, rate.avgFee_0, rate.avgHeight]); + seriesData['10th'].push([timestamp, rate.avgFee_10, rate.avgHeight]); + seriesData['25th'].push([timestamp, rate.avgFee_25, rate.avgHeight]); + seriesData['Median'].push([timestamp, rate.avgFee_50, rate.avgHeight]); + seriesData['75th'].push([timestamp, rate.avgFee_75, rate.avgHeight]); + seriesData['90th'].push([timestamp, rate.avgFee_90, rate.avgHeight]); + seriesData['Max'].push([timestamp, rate.avgFee_100, rate.avgHeight]); + } } // Prepare chart @@ -135,15 +149,42 @@ export class BlockFeeRatesGraphComponent implements OnInit { }); } + if (this.widget) { + let maResolution = 30; + const medianMa = []; + for (let i = maResolution - 1; i < seriesData['Median'].length; ++i) { + let avg = 0; + for (let y = maResolution - 1; y >= 0; --y) { + avg += seriesData['Median'][i - y][1]; + } + avg /= maResolution; + medianMa.push([seriesData['Median'][i][0], avg]); + } + series.push({ + zlevel: 1, + name: 'MA', + data: medianMa, + type: 'line', + showSymbol: false, + symbol: 'none', + lineStyle: { + width: 3, + } + }); + } + this.prepareChartOptions({ legends: legends, series: series }, rateUnits === 'wu'); + this.isLoading = false; + this.cd.markForCheck(); }), map((response) => { return { blockCount: parseInt(response.headers.get('x-total-count'), 10), + avgMedianRate: response.body.length ? response.body.reduce((acc, rate) => acc + rate.avgFee_50, 0) / response.body.length : 0, }; }), ); @@ -154,16 +195,22 @@ export class BlockFeeRatesGraphComponent implements OnInit { prepareChartOptions(data, weightMode) { this.chartOptions = { - color: ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'], + color: this.widget ? ['#6b6b6b', new graphic.LinearGradient(0, 0, 0, 0.65, [ + { offset: 0, color: '#F4511E' }, + { offset: 0.25, color: '#FB8C00' }, + { offset: 0.5, color: '#FFB300' }, + { offset: 0.75, color: '#FDD835' }, + { offset: 1, color: '#7CB342' } + ])] : ['#D81B60', '#8E24AA', '#1E88E5', '#7CB342', '#FDD835', '#6D4C41', '#546E7A'], animation: false, grid: { right: this.right, left: this.left, - bottom: 80, - top: this.isMobile() ? 10 : 50, + bottom: this.widget ? 30 : 80, + top: this.widget ? 20 : (this.isMobile() ? 10 : 50), }, tooltip: { - show: !this.isMobile(), + show: !this.isMobile() && !this.widget, trigger: 'axis', axisPointer: { type: 'line' @@ -201,7 +248,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { }, xAxis: data.series.length === 0 ? undefined : { - name: formatterXAxisLabel(this.locale, this.timespan), + name: this.widget ? undefined : formatterXAxisLabel(this.locale, this.timespan), nameLocation: 'middle', nameTextStyle: { padding: [10, 0, 0, 0], @@ -218,7 +265,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { padding: [0, 5], }, }, - legend: (data.series.length === 0) ? undefined : { + legend: (this.widget || data.series.length === 0) ? undefined : { padding: [10, 75], data: data.legends, selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? { @@ -256,7 +303,7 @@ export class BlockFeeRatesGraphComponent implements OnInit { max: (val) => this.timespan === 'all' ? Math.min(val.max, 5000) : undefined, }, series: data.series, - dataZoom: [{ + dataZoom: this.widget ? null : [{ type: 'inside', realtime: true, zoomLock: true, diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 854d15c2a..e379a8626 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,7 +1,11 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, +<<<<<<< HEAD PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; +======= + PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration } from '../interfaces/node-api.interface'; +>>>>>>> 9b9adcd43 (widgetify block fee rates chart) import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from './state.service'; import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';