diff --git a/backend/src/routes.ts b/backend/src/routes.ts
index 83b71d19e..72bf3b483 100644
--- a/backend/src/routes.ts
+++ b/backend/src/routes.ts
@@ -686,14 +686,14 @@ class Routes {
try {
const blockSizes = await mining.$getHistoricalBlockSizes(req.params.interval ?? null);
const blockWeights = await mining.$getHistoricalBlockWeights(req.params.interval ?? null);
- const oldestIndexedBlockTimestamp = await BlocksRepository.$oldestBlockTimestamp();
+ const blockCount = await BlocksRepository.$blockCount(null, null);
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
+ res.header('X-total-count', blockCount.toString());
res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json({
- oldestIndexedBlockTimestamp: oldestIndexedBlockTimestamp,
sizes: blockSizes,
- weigths: blockWeights
+ weights: blockWeights
});
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index 13465a612..2eaf7e277 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -35,6 +35,7 @@ import { BlocksList } from './components/blocks-list/blocks-list.component';
import { BlockFeesGraphComponent } from './components/block-fees-graph/block-fees-graph.component';
import { BlockRewardsGraphComponent } from './components/block-rewards-graph/block-rewards-graph.component';
import { BlockFeeRatesGraphComponent } from './components/block-fee-rates-graph/block-fee-rates-graph.component';
+import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weights-graph/block-sizes-weights-graph.component';
let routes: Routes = [
{
@@ -131,6 +132,10 @@ let routes: Routes = [
path: 'mining/block-fee-rates',
component: BlockFeeRatesGraphComponent,
},
+ {
+ path: 'mining/block-sizes-weights',
+ component: BlockSizesWeightsGraphComponent,
+ },
],
},
{
@@ -261,6 +266,10 @@ let routes: Routes = [
path: 'mining/block-fee-rates',
component: BlockFeeRatesGraphComponent,
},
+ {
+ path: 'mining/block-sizes-weights',
+ component: BlockSizesWeightsGraphComponent,
+ },
]
},
{
@@ -389,6 +398,10 @@ let routes: Routes = [
path: 'mining/block-fee-rates',
component: BlockFeeRatesGraphComponent,
},
+ {
+ path: 'mining/block-sizes-weights',
+ component: BlockSizesWeightsGraphComponent,
+ },
]
},
{
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 09e26e906..336cfead2 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -78,6 +78,7 @@ import { BlockRewardsGraphComponent } from './components/block-rewards-graph/blo
import { BlockFeeRatesGraphComponent } from './components/block-fee-rates-graph/block-fee-rates-graph.component';
import { LoadingIndicatorComponent } from './components/loading-indicator/loading-indicator.component';
import { IndexingProgressComponent } from './components/indexing-progress/indexing-progress.component';
+import { BlockSizesWeightsGraphComponent } from './components/block-sizes-weights-graph/block-sizes-weights-graph.component';
@NgModule({
declarations: [
@@ -136,6 +137,7 @@ import { IndexingProgressComponent } from './components/indexing-progress/indexi
BlockFeeRatesGraphComponent,
LoadingIndicatorComponent,
IndexingProgressComponent,
+ BlockSizesWeightsGraphComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
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 101c27618..62b155fb5 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
@@ -278,9 +278,6 @@ export class BlockFeeRatesGraphComponent implements OnInit {
this.chartInstance = ec;
this.chartInstance.on('click', (e) => {
- if (e.data.data === 9999) { // "Other"
- return;
- }
this.zone.run(() => {
if (['24h', '3d'].includes(this.timespan)) {
const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.data[2]}`);
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 f419472c9..b92d82120 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
@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
+import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { Observable } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html
new file mode 100644
index 000000000..a02340a47
--- /dev/null
+++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.html
@@ -0,0 +1,52 @@
+
diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss
new file mode 100644
index 000000000..86c1f8ec3
--- /dev/null
+++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.scss
@@ -0,0 +1,135 @@
+.card-header {
+ border-bottom: 0;
+ font-size: 18px;
+ @media (min-width: 465px) {
+ font-size: 20px;
+ }
+}
+
+.main-title {
+ position: relative;
+ color: #ffffff91;
+ margin-top: -13px;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: 500;
+ text-align: center;
+ padding-bottom: 3px;
+}
+
+.full-container {
+ padding: 0px 15px;
+ width: 100%;
+ min-height: 500px;
+ height: calc(100% - 150px);
+ @media (max-width: 992px) {
+ height: 100%;
+ padding-bottom: 100px;
+ };
+}
+
+.chart {
+ width: 100%;
+ height: 100%;
+ padding-bottom: 20px;
+ 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 {
+ width: 100%;
+ height: 100%;
+ max-height: 270px;
+}
+
+.formRadioGroup {
+ margin-top: 6px;
+ display: flex;
+ flex-direction: column;
+ @media (min-width: 1130px) {
+ position: relative;
+ top: -65px;
+ }
+ @media (min-width: 830px) and (max-width: 1130px) {
+ position: relative;
+ top: 0px;
+ }
+ @media (min-width: 830px) {
+ flex-direction: row;
+ float: right;
+ margin-top: 0px;
+ }
+ .btn-sm {
+ font-size: 9px;
+ @media (min-width: 830px) {
+ font-size: 14px;
+ }
+ }
+}
+
+.pool-distribution {
+ 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;
+ }
+ }
+ }
+}
+
+.skeleton-loader {
+ width: 100%;
+ display: block;
+ max-width: 80px;
+ margin: 15px auto 3px;
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts
new file mode 100644
index 000000000..1f72c042b
--- /dev/null
+++ b/frontend/src/app/components/block-sizes-weights-graph/block-sizes-weights-graph.component.ts
@@ -0,0 +1,329 @@
+import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding, NgZone } from '@angular/core';
+import { EChartsOption} from 'echarts';
+import { Observable } from 'rxjs';
+import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
+import { ApiService } from 'src/app/services/api.service';
+import { SeoService } from 'src/app/services/seo.service';
+import { formatNumber } from '@angular/common';
+import { FormBuilder, FormGroup } from '@angular/forms';
+import { StorageService } from 'src/app/services/storage.service';
+import { MiningService } from 'src/app/services/mining.service';
+import { StateService } from 'src/app/services/state.service';
+import { Router } from '@angular/router';
+import { download } from 'src/app/shared/graphs.utils';
+
+@Component({
+ selector: 'app-block-sizes-weights-graph',
+ templateUrl: './block-sizes-weights-graph.component.html',
+ styleUrls: ['./block-sizes-weights-graph.component.scss'],
+ styles: [`
+ .loadingGraphs {
+ position: absolute;
+ top: 50%;
+ left: calc(50% - 15px);
+ z-index: 100;
+ }
+ `],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class BlockSizesWeightsGraphComponent implements OnInit {
+ @Input() right: number | string = 45;
+ @Input() left: number | string = 75;
+
+ miningWindowPreference: string;
+ radioGroupForm: FormGroup;
+
+ chartOptions: EChartsOption = {};
+ chartInitOptions = {
+ renderer: 'svg',
+ };
+
+ @HostBinding('attr.dir') dir = 'ltr';
+
+ blockSizesWeightsObservable$: Observable;
+ isLoading = true;
+ formatNumber = formatNumber;
+ timespan = '';
+ chartInstance: any = undefined;
+
+ constructor(
+ @Inject(LOCALE_ID) public locale: string,
+ private seoService: SeoService,
+ private apiService: ApiService,
+ private formBuilder: FormBuilder,
+ private storageService: StorageService,
+ private miningService: MiningService,
+ private stateService: StateService,
+ private router: Router,
+ private zone: NgZone,
+ ) {
+ }
+
+ ngOnInit(): void {
+ let firstRun = true;
+
+ this.seoService.setTitle($localize`:@@mining.hashrate-difficulty:Hashrate and Weight`);
+ this.miningWindowPreference = this.miningService.getDefaultTimespan('24h');
+ this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
+ this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
+
+ this.blockSizesWeightsObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
+ .pipe(
+ startWith(this.miningWindowPreference),
+ switchMap((timespan) => {
+ this.timespan = timespan;
+ if (!firstRun) {
+ this.storageService.setValue('miningWindowPreference', timespan);
+ }
+ firstRun = false;
+ this.miningWindowPreference = timespan;
+ this.isLoading = true;
+ return this.apiService.getHistoricalBlockSizesAndWeights$(timespan)
+ .pipe(
+ tap((response) => {
+ const data = response.body;
+ this.prepareChartOptions({
+ sizes: data.sizes.map(val => [val.timestamp * 1000, val.avg_size / 1000000, val.avg_height]),
+ weights: data.weights.map(val => [val.timestamp * 1000, val.avg_weight / 1000000, val.avg_height]),
+ });
+ this.isLoading = false;
+ }),
+ map((response) => {
+ return {
+ blockCount: parseInt(response.headers.get('x-total-count'), 10),
+ };
+ }),
+ );
+ }),
+ share()
+ );
+ }
+
+ prepareChartOptions(data) {
+ let title: object;
+ if (data.sizes.length === 0) {
+ title = {
+ textStyle: {
+ color: 'grey',
+ fontSize: 15
+ },
+ text: `Indexing in progess`,
+ left: 'center',
+ top: 'center'
+ };
+ }
+
+ this.chartOptions = {
+ title: title,
+ animation: false,
+ color: [
+ '#FDD835',
+ '#D81B60',
+ ],
+ grid: {
+ top: 30,
+ bottom: 70,
+ right: this.right,
+ left: this.left,
+ },
+ tooltip: {
+ show: !this.isMobile(),
+ trigger: 'axis',
+ axisPointer: {
+ type: 'line'
+ },
+ backgroundColor: 'rgba(17, 19, 31, 1)',
+ borderRadius: 4,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ textStyle: {
+ color: '#b1b1b1',
+ align: 'left',
+ },
+ borderColor: '#000',
+ formatter: (ticks) => {
+ let sizeString = '';
+ let weightString = '';
+
+ for (const tick of ticks) {
+ if (tick.seriesIndex === 0) { // Size
+ sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')} MB`;
+ } else if (tick.seriesIndex === 1) { // Weight
+ weightString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')} MWU`;
+ }
+ }
+
+ const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
+
+ let tooltip = `${date}
+ ${sizeString}
+ ${weightString}`;
+
+ if (['24h', '3d'].includes(this.timespan)) {
+ tooltip += `
At block: ${ticks[0].data[2]}`;
+ } else {
+ tooltip += `
Around block ${ticks[0].data[2]}`;
+ }
+
+ return tooltip;
+ }
+ },
+ xAxis: data.sizes.length === 0 ? undefined : {
+ type: 'time',
+ splitNumber: this.isMobile() ? 5 : 10,
+ axisLabel: {
+ hideOverlap: true,
+ }
+ },
+ legend: data.sizes.length === 0 ? undefined : {
+ padding: 10,
+ data: [
+ {
+ name: 'Size',
+ inactiveColor: 'rgb(110, 112, 121)',
+ textStyle: {
+ color: 'white',
+ },
+ icon: 'roundRect',
+ },
+ {
+ name: 'Weight',
+ inactiveColor: 'rgb(110, 112, 121)',
+ textStyle: {
+ color: 'white',
+ },
+ icon: 'roundRect',
+ },
+ ],
+ selected: JSON.parse(this.storageService.getValue('sizes_weights_legend')) ?? {
+ 'Size': true,
+ 'Weight': true,
+ }
+ },
+ yAxis: data.sizes.length === 0 ? undefined : [
+ {
+ type: 'value',
+ position: 'left',
+ min: (value) => {
+ return value.min * 0.9;
+ },
+ axisLabel: {
+ color: 'rgb(110, 112, 121)',
+ formatter: (val) => {
+ return `${Math.round(val * 100) / 100} MWU`;
+ }
+ },
+ splitLine: {
+ lineStyle: {
+ type: 'dotted',
+ color: '#ffffff66',
+ opacity: 0.25,
+ }
+ },
+ }
+ ],
+ series: data.sizes.length === 0 ? [] : [
+ {
+ zlevel: 1,
+ name: 'Size',
+ showSymbol: false,
+ symbol: 'none',
+ data: data.sizes,
+ type: 'line',
+ lineStyle: {
+ width: 2,
+ },
+ markLine: {
+ silent: true,
+ symbol: 'none',
+ lineStyle: {
+ type: 'solid',
+ color: '#ffffff66',
+ opacity: 1,
+ width: 1,
+ },
+ data: [{
+ yAxis: 1,
+ label: {
+ position: 'end',
+ show: true,
+ color: '#ffffff',
+ formatter: `1 MB`
+ }
+ }],
+ }
+ },
+ {
+ zlevel: 1,
+ yAxisIndex: 0,
+ name: 'Weight',
+ showSymbol: false,
+ symbol: 'none',
+ data: data.weights,
+ type: 'line',
+ lineStyle: {
+ width: 2,
+ }
+ }
+ ],
+ dataZoom: [{
+ type: 'inside',
+ realtime: true,
+ zoomLock: true,
+ maxSpan: 100,
+ minSpan: 5,
+ moveOnMouseMove: false,
+ }, {
+ showDetail: false,
+ show: true,
+ type: 'slider',
+ brushSelect: false,
+ realtime: true,
+ left: 20,
+ right: 15,
+ selectedDataBackground: {
+ lineStyle: {
+ color: '#fff',
+ opacity: 0.45,
+ },
+ areaStyle: {
+ opacity: 0,
+ }
+ },
+ }],
+ };
+ }
+
+ onChartInit(ec) {
+ if (this.chartInstance !== undefined) {
+ return;
+ }
+
+ this.chartInstance = ec;
+
+ this.chartInstance.on('legendselectchanged', (e) => {
+ this.storageService.setValue('sizes_weights_legend', JSON.stringify(e.selected));
+ });
+ }
+
+ isMobile() {
+ return (window.innerWidth <= 767.98);
+ }
+
+ onSaveChart() {
+ // @ts-ignore
+ const prevBottom = this.chartOptions.grid.bottom;
+ const now = new Date();
+ // @ts-ignore
+ this.chartOptions.grid.bottom = 40;
+ this.chartOptions.backgroundColor = '#11131f';
+ this.chartInstance.setOption(this.chartOptions);
+ download(this.chartInstance.getDataURL({
+ pixelRatio: 2,
+ excludeComponents: ['dataZoom'],
+ }), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
+ // @ts-ignore
+ this.chartOptions.grid.bottom = prevBottom;
+ this.chartOptions.backgroundColor = 'none';
+ this.chartInstance.setOption(this.chartOptions);
+ }
+}
diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html
index dce79ad97..a32829d6b 100644
--- a/frontend/src/app/components/graphs/graphs.component.html
+++ b/frontend/src/app/components/graphs/graphs.component.html
@@ -28,6 +28,10 @@
[routerLink]="['/graphs/mining/block-rewards' | relativeUrl]" i18n="mining.block-rewards">
Block Rewards
+
+ Block Sizes and Weights
+
diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts
index b892e16ff..982461ad1 100644
--- a/frontend/src/app/services/api.service.ts
+++ b/frontend/src/app/services/api.service.ts
@@ -189,6 +189,13 @@ export class ApiService {
);
}
+ getHistoricalBlockSizesAndWeights$(interval: string | undefined) : Observable {
+ return this.httpClient.get(
+ this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/sizes-weights` +
+ (interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
+ );
+ }
+
getRewardStats$(blockCount: number = 144): Observable {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/reward-stats/${blockCount}`);
}