From 7dd0173e8405dfb855ef64755f42aad01b37ae41 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 11 Apr 2022 18:17:36 +0900 Subject: [PATCH] Improve mining graphs timespan selection UX --- backend/src/api/mining.ts | 1 + .../block-fees-graph.component.html | 48 ++++++++++--------- .../block-fees-graph.component.ts | 22 +++++---- .../hashrate-chart.component.html | 29 ++++++----- .../hashrate-chart.component.scss | 2 +- .../hashrate-chart.component.ts | 23 +++++++-- .../hashrate-chart-pools.component.html | 37 +++++++------- .../hashrate-chart-pools.component.ts | 33 ++++++++----- .../pool-ranking/pool-ranking.component.html | 8 ++-- .../pool-ranking/pool-ranking.component.ts | 24 +++++----- frontend/src/app/services/mining.service.ts | 19 ++++++++ frontend/src/app/services/storage.service.ts | 8 ++-- production/nginx-cache-warmer | 1 + 13 files changed, 158 insertions(+), 97 deletions(-) diff --git a/backend/src/api/mining.ts b/backend/src/api/mining.ts index e88f21983..2829e5df1 100644 --- a/backend/src/api/mining.ts +++ b/backend/src/api/mining.ts @@ -27,6 +27,7 @@ class Mining { case '3m': timeRange = 7200; break; // 2h case '1m': timeRange = 1800; break; // 30min case '1w': timeRange = 300; break; // 5min + case '3d': timeRange = 1; break; case '24h': timeRange = 1; break; default: timeRange = 86400; break; // 24h } diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html index 88c07e208..fc811c5ea 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.html @@ -1,41 +1,43 @@ -
- -
+
+
Block fees -
+
-
-
+
diff --git a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts index 6a729d4f6..74de3c317 100644 --- a/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts +++ b/frontend/src/app/components/block-fees-graph/block-fees-graph.component.ts @@ -7,6 +7,8 @@ import { SeoService } from 'src/app/services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; import { formatterXAxisLabel } from 'src/app/shared/graphs.utils'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; @Component({ selector: 'app-block-fees-graph', @@ -24,10 +26,10 @@ import { formatterXAxisLabel } from 'src/app/shared/graphs.utils'; }) export class BlockFeesGraphComponent implements OnInit { @Input() tableOnly = false; - @Input() widget = false; @Input() right: number | string = 45; @Input() left: number | string = 75; + miningWindowPreference: string; radioGroupForm: FormGroup; chartOptions: EChartsOption = {}; @@ -44,21 +46,25 @@ export class BlockFeesGraphComponent implements OnInit { @Inject(LOCALE_ID) public locale: string, private seoService: SeoService, private apiService: ApiService, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + private storageService: StorageService, + private miningService: MiningService ) { this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - if (!this.widget) { - this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`); - } + this.seoService.setTitle($localize`:@@mining.block-fees:Block Fees`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.statsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith('1y'), + startWith(this.miningWindowPreference), switchMap((timespan) => { + this.storageService.setValue('miningWindowPreference', timespan); this.timespan = timespan; this.isLoading = true; return this.apiService.getHistoricalBlockFees$(timespan) @@ -103,7 +109,7 @@ export class BlockFeesGraphComponent implements OnInit { left: this.left, }, tooltip: { - show: !this.isMobile() || !this.widget, + show: !this.isMobile(), trigger: 'axis', axisPointer: { type: 'line' @@ -161,7 +167,7 @@ export class BlockFeesGraphComponent implements OnInit { }, }, ], - dataZoom: this.widget ? null : [{ + dataZoom: [{ type: 'inside', realtime: true, zoomLock: true, diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html index 4107f1554..49004c225 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -20,25 +20,28 @@
Hashrate & Difficulty -
+
-
diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss index 54dbe5fad..86c1f8ec3 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -132,4 +132,4 @@ display: block; max-width: 80px; margin: 15px auto 3px; -} +} \ No newline at end of file diff --git a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts index de62989bf..f4e8e871c 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -7,6 +7,8 @@ import { SeoService } from 'src/app/services/seo.service'; import { formatNumber } from '@angular/common'; import { FormBuilder, FormGroup } from '@angular/forms'; import { selectPowerOfTen } from 'src/app/bitcoin.utils'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; @Component({ selector: 'app-hashrate-chart', @@ -28,6 +30,7 @@ export class HashrateChartComponent implements OnInit { @Input() right: number | string = 45; @Input() left: number | string = 75; + miningWindowPreference: string; radioGroupForm: FormGroup; chartOptions: EChartsOption = {}; @@ -47,20 +50,32 @@ export class HashrateChartComponent implements OnInit { private apiService: ApiService, private formBuilder: FormBuilder, private cd: ChangeDetectorRef, + private storageService: StorageService, + private miningService: MiningService ) { - this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); - this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - if (!this.widget) { + let firstRun = true; + + if (this.widget) { + this.miningWindowPreference = '1y'; + } else { this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Difficulty`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); } + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith('1y'), + startWith(this.miningWindowPreference), switchMap((timespan) => { + if (!this.widget && !firstRun) { + this.storageService.setValue('miningWindowPreference', timespan); + } + firstRun = false; + this.miningWindowPreference = timespan; this.isLoading = true; return this.apiService.getHistoricalHashrate$(timespan) .pipe( diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html index 1ee088c7e..f3d547dd6 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.html @@ -1,32 +1,35 @@ -
+
-
- Mining pools dominance -
+
+ Mining pools dominance +
-
-
diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts index d664650d0..7dc12c313 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.ts @@ -6,6 +6,8 @@ import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; import { FormBuilder, FormGroup } from '@angular/forms'; import { poolsColor } from 'src/app/app.constants'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; @Component({ selector: 'app-hashrate-chart-pools', @@ -22,10 +24,10 @@ import { poolsColor } from 'src/app/app.constants'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class HashrateChartPoolsComponent implements OnInit { - @Input() widget = false; @Input() right: number | string = 45; @Input() left: number | string = 25; + miningWindowPreference: string; radioGroupForm: FormGroup; chartOptions: EChartsOption = {}; @@ -44,20 +46,29 @@ export class HashrateChartPoolsComponent implements OnInit { private apiService: ApiService, private formBuilder: FormBuilder, private cd: ChangeDetectorRef, + private storageService: StorageService, + private miningService: MiningService ) { this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); this.radioGroupForm.controls.dateSpan.setValue('1y'); } ngOnInit(): void { - if (!this.widget) { - this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`); - } + let firstRun = true; + + this.seoService.setTitle($localize`:@@mining.pools-historical-dominance:Pools Historical Dominance`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); this.hashrateObservable$ = this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith('1y'), + startWith(this.miningWindowPreference), switchMap((timespan) => { + if (!firstRun) { + this.storageService.setValue('miningWindowPreference', timespan); + } + firstRun = false; this.isLoading = true; return this.apiService.getHistoricalPoolsHashrate$(timespan) .pipe( @@ -157,11 +168,11 @@ export class HashrateChartPoolsComponent implements OnInit { grid: { right: this.right, left: this.left, - bottom: this.widget ? 30 : 70, - top: this.widget || this.isMobile() ? 10 : 50, + bottom: 70, + top: this.isMobile() ? 10 : 50, }, tooltip: { - show: !this.isMobile() || !this.widget, + show: !this.isMobile(), trigger: 'axis', axisPointer: { type: 'line' @@ -188,9 +199,9 @@ export class HashrateChartPoolsComponent implements OnInit { }, xAxis: data.series.length === 0 ? undefined : { type: 'time', - splitNumber: (this.isMobile() || this.widget) ? 5 : 10, + splitNumber: (this.isMobile()) ? 5 : 10, }, - legend: (this.isMobile() || this.widget || data.series.length === 0) ? undefined : { + legend: (this.isMobile() || data.series.length === 0) ? undefined : { data: data.legends }, yAxis: data.series.length === 0 ? undefined : { @@ -207,7 +218,7 @@ export class HashrateChartPoolsComponent implements OnInit { min: 0, }, series: data.series, - dataZoom: this.widget ? null : [{ + dataZoom: [{ type: 'inside', realtime: true, zoomLock: true, diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.html b/frontend/src/app/components/pool-ranking/pool-ranking.component.html index 1de95755c..3c038df80 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -55,7 +55,7 @@ -
@@ -79,7 +79,7 @@ Rank Pool - Hashrate + Hashrate Blocks Empty Blocks @@ -90,7 +90,7 @@ {{ pool.name }} - {{ pool.lastEstimatedHashrate }} {{ + {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool['blockText'] }} {{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%) @@ -99,7 +99,7 @@ All miners - {{ miningStats.lastEstimatedHashrate}} {{ + {{ miningStats.lastEstimatedHashrate}} {{ miningStats.miningUnits.hashrateUnit }} {{ miningStats.blockCount }} {{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts index bf78266e0..4ec12fde4 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -19,9 +19,9 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url. changeDetection: ChangeDetectionStrategy.OnPush, }) export class PoolRankingComponent implements OnInit { - @Input() widget: boolean = false; + @Input() widget = false; - poolsWindowPreference: string; + miningWindowPreference: string; radioGroupForm: FormGroup; isLoading = true; @@ -48,13 +48,13 @@ export class PoolRankingComponent implements OnInit { ngOnInit(): void { if (this.widget) { - this.poolsWindowPreference = '1w'; + this.miningWindowPreference = '1w'; } else { this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`); - this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; + this.miningWindowPreference = this.miningService.getDefaultTimespan('24h'); } - this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference }); - this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference); + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); // When... this.miningStatsObservable$ = combineLatest([ @@ -67,12 +67,12 @@ export class PoolRankingComponent implements OnInit { // ...or we change the timespan this.radioGroupForm.get('dateSpan').valueChanges .pipe( - startWith(this.poolsWindowPreference), // (trigger when the page loads) + startWith(this.miningWindowPreference), // (trigger when the page loads) tap((value) => { if (!this.widget) { - this.storageService.setValue('poolsWindowPreference', value); + this.storageService.setValue('miningWindowPreference', value); } - this.poolsWindowPreference = value; + this.miningWindowPreference = value; }) ) ]) @@ -80,7 +80,7 @@ export class PoolRankingComponent implements OnInit { .pipe( switchMap(() => { this.isLoading = true; - return this.miningService.getMiningStats(this.poolsWindowPreference) + return this.miningService.getMiningStats(this.miningWindowPreference) .pipe( catchError((e) => of(this.getEmptyMiningStat())) ); @@ -150,7 +150,7 @@ export class PoolRankingComponent implements OnInit { }, borderColor: '#000', formatter: () => { - if (this.poolsWindowPreference === '24h') { + if (this.miningWindowPreference === '24h') { return `${pool.name} (${pool.share}%)
` + pool.lastEstimatedHashrate.toString() + ' PH/s' + `
` + pool.blockCount.toString() + ` blocks`; @@ -186,7 +186,7 @@ export class PoolRankingComponent implements OnInit { }, borderColor: '#000', formatter: () => { - if (this.poolsWindowPreference === '24h') { + if (this.miningWindowPreference === '24h') { return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + totalEstimatedHashrateOther.toString() + ' PH/s' + `
` + totalBlockOther.toString() + ` blocks`; diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index 68f7e9da1..0480b09cd 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface'; import { ApiService } from '../services/api.service'; import { StateService } from './state.service'; +import { StorageService } from './storage.service'; export interface MiningUnits { hashrateDivider: number; @@ -28,8 +29,12 @@ export class MiningService { constructor( private stateService: StateService, private apiService: ApiService, + private storageService: StorageService, ) { } + /** + * Generate pool ranking stats + */ public getMiningStats(interval: string): Observable { return this.apiService.listPools$(interval).pipe( map(pools => this.generateMiningStats(pools)) @@ -63,6 +68,20 @@ export class MiningService { }; } + /** + * Get the default selection timespan, cap with `min` + */ + public getDefaultTimespan(min: string): string { + const timespans = [ + '24h', '3d', '1w', '1m', '3m', '6m', '1y', '2y', '3y', 'all' + ]; + const preference = this.storageService.getValue('miningWindowPreference') ?? '1w'; + if (timespans.indexOf(preference) < timespans.indexOf(min)) { + return min; + } + return preference; + } + private generateMiningStats(stats: PoolsStats): MiningStats { const miningUnits = this.getMiningUnits(); const hashrateDivider = miningUnits.hashrateDivider; diff --git a/frontend/src/app/services/storage.service.ts b/frontend/src/app/services/storage.service.ts index aa848a21c..f3ea694b2 100644 --- a/frontend/src/app/services/storage.service.ts +++ b/frontend/src/app/services/storage.service.ts @@ -7,21 +7,21 @@ import { Router, ActivatedRoute } from '@angular/router'; export class StorageService { constructor(private router: Router, private route: ActivatedRoute) { this.setDefaultValueIfNeeded('graphWindowPreference', '2h'); - this.setDefaultValueIfNeeded('poolsWindowPreference', '1w'); + this.setDefaultValueIfNeeded('miningWindowPreference', '1w'); } setDefaultValueIfNeeded(key: string, defaultValue: string) { - let graphWindowPreference: string = this.getValue(key); + const graphWindowPreference: string = this.getValue(key); if (graphWindowPreference === null) { // First visit to mempool.space if (this.router.url.includes('graphs') && key === 'graphWindowPreference' || - this.router.url.includes('pools') && key === 'poolsWindowPreference' + this.router.url.includes('pools') && key === 'miningWindowPreference' ) { this.setValue(key, this.route.snapshot.fragment ? this.route.snapshot.fragment : defaultValue); } else { this.setValue(key, defaultValue); } } else if (this.router.url.includes('graphs') && key === 'graphWindowPreference' || - this.router.url.includes('pools') && key === 'poolsWindowPreference' + this.router.url.includes('pools') && key === 'miningWindowPreference' ) { // Visit a different graphs#fragment from last visit if (this.route.snapshot.fragment !== null && graphWindowPreference !== this.route.snapshot.fragment) { diff --git a/production/nginx-cache-warmer b/production/nginx-cache-warmer index 8abdaddf2..21824b80b 100755 --- a/production/nginx-cache-warmer +++ b/production/nginx-cache-warmer @@ -38,6 +38,7 @@ do for url in / \ '/api/v1/mining/reward-stats/144' \ '/api/v1/mining/blocks-extras' \ '/api/v1/mining/blocks/fees/24h' \ + '/api/v1/mining/blocks/fees/3d' \ '/api/v1/mining/blocks/fees/1w' \ '/api/v1/mining/blocks/fees/1m' \ '/api/v1/mining/blocks/fees/3m' \