diff --git a/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.html b/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.html
new file mode 100644
index 000000000..2cf3fbc0b
--- /dev/null
+++ b/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.scss b/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.scss
new file mode 100644
index 000000000..2ffcc6374
--- /dev/null
+++ b/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.scss
@@ -0,0 +1,119 @@
+.card-header {
+ border-bottom: 0;
+ font-size: 18px;
+ @media (min-width: 465px) {
+ font-size: 20px;
+ }
+ @media (min-width: 992px) {
+ height: 40px;
+ }
+}
+
+.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 {
+ display: flex;
+ flex-direction: column;
+ padding: 0px 15px;
+ width: 100%;
+ height: calc(100vh - 250px);
+ @media (min-width: 992px) {
+ height: calc(100vh - 150px);
+ }
+}
+
+.chart {
+ display: flex;
+ flex: 1;
+ width: 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: 238px;
+}
+
+.acceleration-fees {
+ 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;
+}
+
+.disabled {
+ pointer-events: none;
+ opacity: 0.5;
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.ts b/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.ts
new file mode 100644
index 000000000..f666c8f31
--- /dev/null
+++ b/frontend/src/app/components/acceleration-fees-graph/acceleration-fees-graph.component.ts
@@ -0,0 +1,354 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core';
+import { EChartsOption, graphic } from 'echarts';
+import { Observable, combineLatest } from 'rxjs';
+import { map, startWith, switchMap, tap } from 'rxjs/operators';
+import { ApiService } from '../../services/api.service';
+import { SeoService } from '../../services/seo.service';
+import { formatNumber } from '@angular/common';
+import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
+import { download, formatterXAxis } from '../../shared/graphs.utils';
+import { StorageService } from '../../services/storage.service';
+import { MiningService } from '../../services/mining.service';
+import { ActivatedRoute } from '@angular/router';
+
+@Component({
+ selector: 'app-acceleration-fees-graph',
+ templateUrl: './acceleration-fees-graph.component.html',
+ styleUrls: ['./acceleration-fees-graph.component.scss'],
+ styles: [`
+ .loadingGraphs {
+ position: absolute;
+ top: 50%;
+ left: calc(50% - 15px);
+ z-index: 100;
+ }
+ `],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class AccelerationFeesGraphComponent implements OnInit {
+ @Input() widget: boolean = false;
+ @Input() right: number | string = 45;
+ @Input() left: number | string = 75;
+
+ miningWindowPreference: string;
+ radioGroupForm: UntypedFormGroup;
+
+ chartOptions: EChartsOption = {};
+ chartInitOptions = {
+ renderer: 'svg',
+ };
+
+ statsObservable$: Observable;
+ isLoading = true;
+ formatNumber = formatNumber;
+ timespan = '';
+ chartInstance: any = undefined;
+
+ currency: string;
+
+ constructor(
+ @Inject(LOCALE_ID) public locale: string,
+ private seoService: SeoService,
+ private apiService: ApiService,
+ private formBuilder: UntypedFormBuilder,
+ private storageService: StorageService,
+ private miningService: MiningService,
+ private route: ActivatedRoute,
+ private cd: ChangeDetectorRef,
+ ) {
+ this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
+ this.radioGroupForm.controls.dateSpan.setValue('1y');
+ this.currency = 'USD';
+ }
+
+ ngOnInit(): void {
+ this.seoService.setTitle($localize`:@@6c453b11fd7bd159ae30bc381f367bc736d86909:Acceleration Fees`);
+ if (this.widget) {
+ this.miningWindowPreference = '1w';
+ this.isLoading = true;
+ this.timespan = this.miningWindowPreference;
+ this.statsObservable$ = combineLatest([
+ this.apiService.getAccelerationHistory$(this.miningWindowPreference),
+ this.apiService.getHistoricalBlockFees$(this.miningWindowPreference),
+ ]).pipe(
+ tap(([accelerations, blockFeesResponse]) => {
+ console.log(accelerations, blockFeesResponse.body);
+ this.prepareChartOptions(accelerations, blockFeesResponse.body);
+ this.isLoading = false;
+ }),
+ map(([accelerations, blockFeesResponse]) => {
+ return {
+
+ };
+ }),
+ );
+ } else {
+ this.miningWindowPreference = this.miningService.getDefaultTimespan('1w');
+ 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'].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),
+ switchMap((timespan) => {
+ this.isLoading = true;
+ this.storageService.setValue('miningWindowPreference', timespan);
+ this.timespan = timespan;
+ return this.apiService.getAccelerations$();
+ })
+ ),
+ this.radioGroupForm.get('dateSpan').valueChanges.pipe(
+ startWith(this.radioGroupForm.controls.dateSpan.value),
+ switchMap((timespan) => {
+ return this.apiService.getHistoricalBlockFees$(timespan);
+ })
+ )
+ ]).pipe(
+ tap(([accelerations, blockFeesResponse]) => {
+ this.prepareChartOptions(accelerations, blockFeesResponse.body);
+ this.isLoading = false;
+ this.cd.markForCheck();
+ }),
+ map(([accelerations, blockFeesResponse]) => {
+ return {
+
+ };
+ }),
+ );
+ }
+ }
+
+ prepareChartOptions(accelerations, blockFees) {
+ let title: object;
+
+ const blockAccelerations = {};
+
+ for (const acceleration of accelerations) {
+ if (acceleration.mined) {
+ if (!blockAccelerations[acceleration.block_height]) {
+ blockAccelerations[acceleration.block_height] = [];
+ }
+ blockAccelerations[acceleration.block_height].push(acceleration);
+ }
+ }
+
+ let last = null;
+ const data = [];
+ for (const val of blockFees) {
+ if (last == null) {
+ last = val.avgHeight;
+ }
+ let totalFeeDelta = 0;
+ let totalCount = 0;
+ while (last <= val.avgHeight) {
+ totalFeeDelta += (blockAccelerations[last] || []).reduce((total, acc) => total + acc.feeDelta, 0);
+ totalCount += (blockAccelerations[last] || []).length;
+ last++;
+ }
+ data.push({
+ ...val,
+ feeDelta: totalFeeDelta,
+ accelerations: totalCount,
+ });
+ }
+
+ this.chartOptions = {
+ title: title,
+ color: [
+ 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' }
+ ]),
+ '#1E88E5',
+ ],
+ animation: false,
+ grid: {
+ right: this.right,
+ left: this.left,
+ bottom: this.widget ? 30 : 80,
+ top: this.widget ? 20 : (this.isMobile() ? 10 : 50),
+ },
+ 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: function (data) {
+ if (data.length <= 0) {
+ return '';
+ }
+ let tooltip = `
+ ${formatterXAxis(this.locale, this.timespan, parseInt(data[0].axisValue, 10))}
`;
+
+ for (const tick of data) {
+ if (tick.seriesIndex === 0) {
+ tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')} sats
`;
+ } else if (tick.seriesIndex === 1) {
+ tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}
`;
+ }
+ }
+
+ if (['24h', '3d'].includes(this.timespan)) {
+ tooltip += `` + $localize`At block: ${data[0].data[2]}` + ``;
+ } else {
+ tooltip += `` + $localize`Around block: ${data[0].data[2]}` + ``;
+ }
+
+ return tooltip;
+ }.bind(this)
+ },
+ xAxis: data.length === 0 ? undefined :
+ {
+ type: 'time',
+ splitNumber: this.isMobile() ? 5 : 10,
+ axisLabel: {
+ hideOverlap: true,
+ }
+ },
+ legend: (this.widget || data.length === 0) ? undefined : {
+ data: [
+ {
+ name: 'Total fee delta',
+ inactiveColor: 'rgb(110, 112, 121)',
+ textStyle: {
+ color: 'white',
+ },
+ icon: 'roundRect',
+ },
+ {
+ name: 'Total accelerations',
+ inactiveColor: 'rgb(110, 112, 121)',
+ textStyle: {
+ color: 'white',
+ },
+ icon: 'roundRect',
+ },
+ ],
+ },
+ yAxis: data.length === 0 ? undefined : [
+ {
+ type: 'value',
+ axisLabel: {
+ color: 'rgb(110, 112, 121)',
+ formatter: (val) => {
+ return `${val / 100_000_000} BTC`;
+ }
+ },
+ splitLine: {
+ lineStyle: {
+ type: 'dotted',
+ color: '#ffffff66',
+ opacity: 0.25,
+ }
+ },
+ },
+ {
+ type: 'value',
+ position: 'right',
+ axisLabel: {
+ color: 'rgb(110, 112, 121)',
+ formatter: function(val) {
+ return `${val}`;
+ }.bind(this)
+ },
+ splitLine: {
+ show: false,
+ },
+ },
+ ],
+ series: data.length === 0 ? undefined : [
+ {
+ legendHoverLink: false,
+ zlevel: 1,
+ yAxisIndex: 0,
+ name: 'Total fee delta',
+ data: data.map(block => [block.timestamp * 1000, block.feeDelta, block.avgHeight]),
+ type: 'line',
+ smooth: 0.25,
+ symbol: 'none',
+ lineStyle: {
+ width: 1,
+ opacity: 1,
+ }
+ },
+ {
+ legendHoverLink: false,
+ zlevel: 0,
+ yAxisIndex: 1,
+ name: 'Total accelerations',
+ data: data.map(block => [block.timestamp * 1000, block.accelerations, block.avgHeight]),
+ type: 'bar',
+ barWidth: '100%',
+ large: true,
+ },
+ ],
+ dataZoom: (this.widget || data.length === 0 )? undefined : [{
+ 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) {
+ this.chartInstance = ec;
+ }
+
+ 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'],
+ }), `acceleration-fees-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
+ // @ts-ignore
+ this.chartOptions.grid.bottom = prevBottom;
+ this.chartOptions.backgroundColor = 'none';
+ this.chartInstance.setOption(this.chartOptions);
+ }
+}