diff --git a/frontend/src/app/components/difficulty/difficulty.component.html b/frontend/src/app/components/difficulty/difficulty.component.html index fc3030286..3684b8de4 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.html +++ b/frontend/src/app/components/difficulty/difficulty.component.html @@ -1,4 +1,4 @@ -
Difficulty Adjustment
+
Difficulty Adjustment
@@ -47,7 +47,7 @@
-
Next halving
+
Next Halving
{{ i }} blocks diff --git a/frontend/src/app/components/difficulty/difficulty.component.ts b/frontend/src/app/components/difficulty/difficulty.component.ts index ff44e5aeb..b22001ef1 100644 --- a/frontend/src/app/components/difficulty/difficulty.component.ts +++ b/frontend/src/app/components/difficulty/difficulty.component.ts @@ -28,8 +28,9 @@ export class DifficultyComponent implements OnInit { isLoadingWebSocket$: Observable; difficultyEpoch$: Observable; - @Input() showProgress: boolean = true; - @Input() showHalving: boolean = false; + @Input() showProgress = true; + @Input() showHalving = false; + @Input() showTitle = true; constructor( public stateService: StateService, @@ -97,7 +98,7 @@ export class DifficultyComponent implements OnInit { colorPreviousAdjustments = '#ffffff66'; } - const blocksUntilHalving = block.height % 210000; + const blocksUntilHalving = 210000 - (block.height % 210000); const timeUntilHalving = (blocksUntilHalving * timeAvgMins * 60 * 1000) + (now * 1000); return { 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 ea5c5a2a7..eaa9fa809 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.html @@ -1,6 +1,6 @@
-
+
-
- +
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 316f0fc47..4d9e0e5fa 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.scss @@ -29,7 +29,7 @@ .chart-widget { width: 100%; height: 100%; - max-height: 275px; + max-height: 293px; } .formRadioGroup { @@ -48,3 +48,8 @@ } } } + +.compact td { + padding: 0 !important; + margin: 0.15rem !important; +} \ 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 7efb83098..a5f2b63b8 100644 --- a/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts +++ b/frontend/src/app/components/hashrate-chart/hashrate-chart.component.ts @@ -22,7 +22,8 @@ import { selectPowerOfTen } from 'src/app/bitcoin.utils'; `], }) export class HashrateChartComponent implements OnInit { - @Input() widget: boolean = false; + @Input() tableOnly = false; + @Input() widget = false; @Input() right: number | string = 45; @Input() left: number | string = 75; @@ -114,7 +115,7 @@ export class HashrateChartComponent implements OnInit { } return { availableTimespanDay: availableTimespanDay, - difficulty: tableData + difficulty: this.tableOnly ? (this.isMobile() ? tableData.slice(0, 12) : tableData.slice(0, 9)) : tableData }; }), ); @@ -141,6 +142,7 @@ export class HashrateChartComponent implements OnInit { bottom: this.widget ? 30 : 60, }, tooltip: { + show: !this.isMobile() || !this.widget, trigger: 'axis', axisPointer: { type: 'line' 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 93cec63ca..8750caa56 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 @@ -27,7 +27,7 @@
-
+
diff --git a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss index 06a3eeb25..4f15e51d6 100644 --- a/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss +++ b/frontend/src/app/components/hashrates-chart-pools/hashrate-chart-pools.component.scss @@ -29,7 +29,7 @@ .chart-widget { width: 100%; height: 100%; - max-height: 275px; + max-height: 293px; } .formRadioGroup { 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 3d7935e3d..7e3f081a6 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 @@ -78,7 +78,7 @@ export class HashrateChartPoolsComponent implements OnInit { name: name, showSymbol: false, symbol: 'none', - data: grouped[name].map((val) => [val.timestamp * 1000, (val.share * 100).toFixed(2)]), + data: grouped[name].map((val) => [val.timestamp * 1000, val.share * 100]), type: 'line', lineStyle: { width: 0 }, areaStyle: { opacity: 1 }, @@ -132,6 +132,7 @@ export class HashrateChartPoolsComponent implements OnInit { top: this.widget ? 10 : 40, }, tooltip: { + show: !this.isMobile() || !this.widget, trigger: 'axis', axisPointer: { type: 'line' @@ -149,7 +150,7 @@ export class HashrateChartPoolsComponent implements OnInit { data.sort((a, b) => b.data[1] - a.data[1]); for (const pool of data) { if (pool.data[1] > 0) { - tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1]}%
` + tooltip += `${pool.marker} ${pool.seriesName}: ${pool.data[1].toFixed(2)}%
`; } } return tooltip; diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html index 4d811fc9a..6dbb541c3 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.html @@ -3,35 +3,97 @@
-
-
- -
- - Mining Pools Share (1w) - -
- +
Reward stats
+
+
+
+
+
Miners Reward
+
+ +
in the last 8 blocks
+
+
+
+
Reward Per Tx
+
+ {{ rewardStats.rewardPerTx }} + sats/tx +
in the last 8 blocks
+
+
+
+
Average Fee
+
+ {{ rewardStats.feePerTx }} + sats/tx +
in the last 8 blocks
+
+
+
+
+
+
- + +
+
Difficulty Adjustment
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+
+
+ Hashrate (1y) +
+ + +
+
+
+ + +
+
+
+
+ Mining Pools Dominance (1y) +
+
-
+
-
- - Hashrate (1y) - + Adjusments
- + +
-
+
\ No newline at end of file diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss index 4c75e9ea6..828ee7ed0 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.scss @@ -12,14 +12,11 @@ .card { background-color: #1d1f31; - height: 340px; -} -.card.double { - height: 620px; } .card-title { font-size: 1rem; + color: #4a68b9; } .card-title > a { color: #4a68b9; @@ -58,3 +55,91 @@ text-align: center; padding-bottom: 3px; } + +.general-stats { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + margin: 0px auto 10px; + display: inline-block; + @media (min-width: 485px) { + margin: 0px auto 10px; + } + @media (min-width: 785px) { + margin: 0px auto 0px; + } + &:last-child { + margin: 0px auto 0px; + } + &: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; + } + } + } +} + +.difficulty-adjustment-container { + display: flex; + flex-direction: row; + justify-content: space-around; + height: 76px; + .shared-block { + color: #ffffff66; + font-size: 12px; + } + .item { + padding: 0 5px; + width: 100%; + &:nth-child(1) { + display: none; + @media (min-width: 485px) { + display: table-cell; + } + @media (min-width: 768px) { + display: none; + } + @media (min-width: 992px) { + display: table-cell; + } + } + } + .card-text { + font-size: 22px; + margin-top: -9px; + position: relative; + } +} diff --git a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts index aac546ca1..b4cc95a1e 100644 --- a/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts +++ b/frontend/src/app/components/mining-dashboard/mining-dashboard.component.ts @@ -1,5 +1,10 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, OnDestroy, OnInit } from '@angular/core'; +import { map } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; +import { StateService } from 'src/app/services/state.service'; +import { formatNumber } from '@angular/common'; +import { WebsocketService } from 'src/app/services/websocket.service'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-mining-dashboard', @@ -8,12 +13,36 @@ import { SeoService } from 'src/app/services/seo.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class MiningDashboardComponent implements OnInit { + private blocks = []; - constructor(private seoService: SeoService) { + public $rewardStats: Observable; + public totalReward = 0; + public rewardPerTx = '~'; + public feePerTx = '~'; + + constructor(private seoService: SeoService, + public stateService: StateService, + private websocketService: WebsocketService, + @Inject(LOCALE_ID) private locale: string, + ) { this.seoService.setTitle($localize`:@@mining.mining-dashboard:Mining Dashboard`); } ngOnInit(): void { - } + this.$rewardStats = this.stateService.blocks$.pipe( + map(([block]) => { + this.blocks.push(block); + this.blocks = this.blocks.slice(0, 8); + const totalTx = this.blocks.reduce((acc, block) => acc + block.tx_count, 0); + const totalFee = this.blocks.reduce((acc, block) => acc + block.extras?.totalFees ?? 0, 0); + const totalReward = this.blocks.reduce((acc, block) => acc + block.extras?.reward ?? 0, 0); + return { + 'totalReward': totalReward, + 'rewardPerTx': formatNumber(totalReward / totalTx, this.locale, '1.0-0'), + 'feePerTx': formatNumber(totalFee / totalTx, this.locale, '1.0-0'), + } + }) + ); + } } 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 067c61646..6bba39df4 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.html +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.html @@ -1,8 +1,29 @@
+
+
+
Pools luck (1w)
+

+ {{ miningStats['minersLuck'] }}% +

+
+
+
Blocks (1w)
+

+ {{ miningStats.blockCount }} +

+
+
+
Pools count (1w)
+

+ {{ miningStats.pools.length }} +

+
+
+
-
+
@@ -59,7 +80,7 @@ {{ pool.rank }} {{ pool.name }} - {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} + {{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }} {{ pool['blockText'] }} {{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%) diff --git a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss index a41891a8f..f73486395 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.scss +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.scss @@ -1,13 +1,16 @@ .chart { max-height: 400px; @media (max-width: 767.98px) { - max-height: 300px; + max-height: 270px; } } .chart-widget { width: 100%; height: 100%; - max-height: 275px; + max-height: 270px; + @media (max-width: 767.98px) { + max-height: 200px; + } } .formRadioGroup { @@ -44,3 +47,59 @@ .loadingGraphs.widget { top: 25%; } + +.pool-distribution { + min-height: 56px; + display: block; + @media (min-width: 485px) { + display: flex; + flex-direction: row; + } + h5 { + margin-bottom: 10px; + } + .item { + width: 50%; + margin: 0px auto 10px; + display: inline-block; + @media (min-width: 485px) { + margin: 0px auto 10px; + } + @media (min-width: 785px) { + margin: 0px auto 0px; + } + &:last-child { + margin: 0px auto 0px; + } + &: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; + } + } + } +} \ No newline at end of file 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 cef04cca5..64641c31d 100644 --- a/frontend/src/app/components/pool-ranking/pool-ranking.component.ts +++ b/frontend/src/app/components/pool-ranking/pool-ranking.component.ts @@ -49,7 +49,7 @@ export class PoolRankingComponent implements OnInit { this.poolsWindowPreference = '1w'; } else { this.seoService.setTitle($localize`:@@mining.mining-pools:Mining Pools`); - this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; + this.poolsWindowPreference = this.storageService.getValue('poolsWindowPreference') ? this.storageService.getValue('poolsWindowPreference') : '1w'; } this.radioGroupForm = this.formBuilder.group({ dateSpan: this.poolsWindowPreference }); this.radioGroupForm.controls.dateSpan.setValue(this.poolsWindowPreference); @@ -85,6 +85,7 @@ export class PoolRankingComponent implements OnInit { }), map(data => { data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool)); + data['minersLuck'] = (100 * (data.blockCount / 1008)).toFixed(2); // luck 1w return data; }), tap(data => { @@ -105,24 +106,40 @@ export class PoolRankingComponent implements OnInit { } generatePoolsChartSerieData(miningStats) { - const poolShareThreshold = this.isMobile() ? 1 : 0.5; // Do not draw pools which hashrate share is lower than that + const poolShareThreshold = this.isMobile() ? 2 : 1; // Do not draw pools which hashrate share is lower than that const data: object[] = []; + let totalShareOther = 0; + let totalBlockOther = 0; + let totalEstimatedHashrateOther = 0; + + let edgeDistance: any = '20%'; + if (this.isMobile() && this.widget) { + edgeDistance = 0; + } else if (this.isMobile() && !this.widget || this.widget) { + edgeDistance = 35; + } miningStats.pools.forEach((pool) => { if (parseFloat(pool.share) < poolShareThreshold) { + totalShareOther += parseFloat(pool.share); + totalBlockOther += pool.blockCount; + totalEstimatedHashrateOther += pool.lastEstimatedHashrate; return; } data.push({ itemStyle: { - color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase()], + color: poolsColor[pool.name.replace(/[^a-zA-Z0-9]/g, '').toLowerCase()], }, value: pool.share, - name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`), + name: pool.name + ((this.isMobile() || this.widget) ? `` : ` (${pool.share}%)`), label: { + overflow: 'none', color: '#b1b1b1', - overflow: 'break', + alignTo: 'edge', + edgeDistance: edgeDistance, }, tooltip: { + show: !this.isMobile() || !this.widget, backgroundColor: 'rgba(17, 19, 31, 1)', borderRadius: 4, shadowColor: 'rgba(0, 0, 0, 0.5)', @@ -144,6 +161,42 @@ export class PoolRankingComponent implements OnInit { data: pool.poolId, } as PieSeriesOption); }); + + // 'Other' + data.push({ + itemStyle: { + color: 'grey', + }, + value: totalShareOther, + name: 'Other' + (this.isMobile() ? `` : ` (${totalShareOther.toFixed(2)}%)`), + label: { + overflow: 'none', + color: '#b1b1b1', + alignTo: 'edge', + edgeDistance: edgeDistance + }, + tooltip: { + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: () => { + if (this.poolsWindowPreference === '24h') { + return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + + totalEstimatedHashrateOther.toString() + ' PH/s' + + `
` + totalBlockOther.toString() + ` blocks`; + } else { + return `${'Other'} (${totalShareOther.toFixed(2)}%)
` + + totalBlockOther.toString() + ` blocks`; + } + } + }, + data: 9999 as any, + } as PieSeriesOption); + return data; } @@ -154,9 +207,22 @@ export class PoolRankingComponent implements OnInit { } network = network.charAt(0).toUpperCase() + network.slice(1); - let radius: any[] = ['20%', '70%']; - if (this.isMobile() || this.widget) { - radius = ['20%', '60%']; + let radius: any[] = ['20%', '80%']; + let top: any = undefined; let bottom = undefined; let height = undefined; + if (this.isMobile() && this.widget) { + top = -30; + height = 270; + radius = ['10%', '50%']; + } else if (this.isMobile() && !this.widget) { + top = 0; + height = 300; + radius = ['10%', '50%']; + } else if (this.widget) { + radius = ['15%', '60%']; + top = -20; + height = 330; + } else { + top = 35; } this.chartOptions = { @@ -180,14 +246,15 @@ export class PoolRankingComponent implements OnInit { }, series: [ { - top: this.widget ? 0 : 35, + minShowLabelAngle: 3.6, + top: top, + bottom: bottom, + height: height, name: 'Mining pool', type: 'pie', radius: radius, data: this.generatePoolsChartSerieData(miningStats), labelLine: { - length: this.isMobile() ? 10 : 15, - length2: this.isMobile() ? 0 : 15, lineStyle: { width: 2, }, @@ -223,6 +290,9 @@ export class PoolRankingComponent implements OnInit { this.chartInstance = ec; this.chartInstance.on('click', (e) => { + if (e.data.data === 9999) { // "Other" + return; + } this.router.navigate(['/mining/pool/', e.data.data]); }); } @@ -230,7 +300,7 @@ export class PoolRankingComponent implements OnInit { /** * Default mining stats if something goes wrong */ - getEmptyMiningStat() { + getEmptyMiningStat(): MiningStats { return { lastEstimatedHashrate: 'Error', blockCount: 0, diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index d8760d1f0..a05671257 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -64,7 +64,7 @@ export interface SinglePoolStats { blockCount: number; emptyBlocks: number; rank: number; - share: string; + share: number; lastEstimatedHashrate: string; emptyBlockRatio: string; logo: string; @@ -75,13 +75,6 @@ export interface PoolsStats { oldestIndexedBlockTimestamp: number; pools: SinglePoolStats[]; } -export interface MiningStats { - lastEstimatedHashrate: string; - blockCount: number; - totalEmptyBlock: number; - totalEmptyBlockRatio: string; - pools: SinglePoolStats[]; -} /** * Pool component diff --git a/frontend/src/app/services/mining.service.ts b/frontend/src/app/services/mining.service.ts index c216515b0..68f7e9da1 100644 --- a/frontend/src/app/services/mining.service.ts +++ b/frontend/src/app/services/mining.service.ts @@ -73,7 +73,7 @@ export class MiningService { const totalEmptyBlockRatio = (totalEmptyBlock / stats.blockCount * 100).toFixed(2); const poolsStats = stats.pools.map((poolStat) => { return { - share: (poolStat.blockCount / stats.blockCount * 100).toFixed(2), + share: parseFloat((poolStat.blockCount / stats.blockCount * 100).toFixed(2)), lastEstimatedHashrate: (poolStat.blockCount / stats.blockCount * stats.lastEstimatedHashrate / hashrateDivider).toFixed(2), emptyBlockRatio: (poolStat.emptyBlocks / poolStat.blockCount * 100).toFixed(2), logo: `./resources/mining-pools/` + poolStat.name.toLowerCase().replace(' ', '').replace('.', '') + '.svg',