diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 3492114b5..b6ce3ba70 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -24,6 +24,7 @@ class MiningRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees', this.$getBlockFeesTimespan) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) @@ -217,6 +218,26 @@ class MiningRoutes { } } + private async $getBlockFeesTimespan(req: Request, res: Response) { + try { + if (!parseInt(req.query.from as string, 10) || !parseInt(req.query.to as string, 10)) { + throw new Error('Invalid timestamp range'); + } + if (parseInt(req.query.from as string, 10) > parseInt(req.query.to as string, 10)) { + throw new Error('from must be less than to'); + } + const blockFees = await mining.$getBlockFeesTimespan(parseInt(req.query.from as string, 10), parseInt(req.query.to as string, 10)); + 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 * 60).toUTCString()); + res.json(blockFees); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getHistoricalBlockRewards(req: Request, res: Response) { try { const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval); diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index 85554db2d..21ee4b35a 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -45,11 +45,22 @@ class Mining { */ public async $getHistoricalBlockFees(interval: string | null = null): Promise { return await BlocksRepository.$getHistoricalBlockFees( - this.getTimeRange(interval, 5), + this.getTimeRange(interval), Common.getSqlInterval(interval) ); } + /** + * Get timespan block total fees + */ + public async $getBlockFeesTimespan(from: number, to: number): Promise { + return await BlocksRepository.$getHistoricalBlockFees( + this.getTimeRangeFromTimespan(from, to), + null, + {from, to} + ); + } + /** * Get historical block rewards */ @@ -646,6 +657,24 @@ class Mining { } } + private getTimeRangeFromTimespan(from: number, to: number, scale = 1): number { + const timespan = to - from; + switch (true) { + case timespan > 3600 * 24 * 365 * 4: return 86400 * scale; // 24h + case timespan > 3600 * 24 * 365 * 3: return 43200 * scale; // 12h + case timespan > 3600 * 24 * 365 * 2: return 43200 * scale; // 12h + case timespan > 3600 * 24 * 365: return 28800 * scale; // 8h + case timespan > 3600 * 24 * 30 * 6: return 28800 * scale; // 8h + case timespan > 3600 * 24 * 30 * 3: return 10800 * scale; // 3h + case timespan > 3600 * 24 * 30: return 7200 * scale; // 2h + case timespan > 3600 * 24 * 7: return 1800 * scale; // 30min + case timespan > 3600 * 24 * 3: return 300 * scale; // 5min + case timespan > 3600 * 24: return 1 * scale; + default: return 1 * scale; + } + } + + // Finds the oldest block in a consecutive chain back from the tip // assumes `blocks` is sorted in ascending height order private getOldestConsecutiveBlock(blocks: DifficultyBlock[]): DifficultyBlock { diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index e6e92d60f..0077a0b79 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -663,7 +663,7 @@ class BlocksRepository { /** * Get the historical averaged block fees */ - public async $getHistoricalBlockFees(div: number, interval: string | null): Promise { + public async $getHistoricalBlockFees(div: number, interval: string | null, timespan?: {from: number, to: number}): Promise { try { let query = `SELECT CAST(AVG(blocks.height) as INT) as avgHeight, @@ -677,6 +677,8 @@ class BlocksRepository { if (interval !== null) { query += ` WHERE blockTimestamp BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; + } else if (timespan) { + query += ` WHERE blockTimestamp BETWEEN FROM_UNIXTIME(${timespan.from}) AND FROM_UNIXTIME(${timespan.to})`; } query += ` GROUP BY UNIX_TIMESTAMP(blockTimestamp) DIV ${div}`; diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html new file mode 100644 index 000000000..ee1d78f35 --- /dev/null +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html @@ -0,0 +1,55 @@ + + +
+
+
+ Block Fees Vs Subsidy + +
+ +
+
+ + + + + + + + + + +
+
+
+ +
+
+
+
+
+ +
diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss new file mode 100644 index 000000000..645605751 --- /dev/null +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss @@ -0,0 +1,66 @@ +.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 - 225px); + min-height: 400px; + @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: 270px; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts new file mode 100644 index 000000000..88d27033f --- /dev/null +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts @@ -0,0 +1,510 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core'; +import { EChartsOption } from '../../graphs/echarts'; +import { Observable } from 'rxjs'; +import { catchError, map, share, 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 { ActivatedRoute, Router } from '@angular/router'; +import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; +import { StateService } from '../../services/state.service'; +import { MiningService } from '../../services/mining.service'; +import { StorageService } from '../../services/storage.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; + +@Component({ + selector: 'app-block-fees-subsidy-graph', + templateUrl: './block-fees-subsidy-graph.component.html', + styleUrls: ['./block-fees-subsidy-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockFeesSubsidyGraphComponent implements OnInit { + @Input() right: number | string = 45; + @Input() left: number | string = 75; + + miningWindowPreference: string; + radioGroupForm: UntypedFormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + statsObservable$: Observable; + data: any; + subsidies: { [key: number]: number } = {}; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + chartInstance: any = undefined; + showFiat = false; + updateZoom = false; + zoomSpan = 100; + zoomTimeSpan = ''; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: UntypedFormBuilder, + public stateService: StateService, + private storageService: StorageService, + private miningService: MiningService, + private route: ActivatedRoute, + private router: Router, + private zone: NgZone, + private fiatShortenerPipe: FiatShortenerPipe, + private fiatCurrencyPipe: FiatCurrencyPipe, + private cd: ChangeDetectorRef, + ) { + this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' }); + this.radioGroupForm.controls.dateSpan.setValue('1y'); + + this.subsidies = this.initSubsidies(); + } + + ngOnInit(): void { + this.seoService.setTitle($localize`:@@mining.block-fees-subsidy:Block Fees Vs Subsidy`); + this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees-subsidy:See the mining fees earned per Bitcoin block compared to the Bitcoin block subsidy, visualized in BTC and USD over time.`); + + 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 }); + } + }); + + this.statsObservable$ = 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; + this.zoomTimeSpan = timespan; + this.isLoading = true; + return this.apiService.getHistoricalBlockFees$(timespan) + .pipe( + tap((response) => { + this.data = { + timestamp: response.body.map(val => val.timestamp * 1000), + blockHeight: response.body.map(val => val.avgHeight), + blockFees: response.body.map(val => val.avgFees / 100_000_000), + blockFeesFiat: response.body.filter(val => val['USD'] > 0).map(val => val.avgFees / 100_000_000 * val['USD']), + blockSubsidy: response.body.map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000), + blockSubsidyFiat: response.body.filter(val => val['USD'] > 0).map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000 * val['USD']), + }; + + this.prepareChartOptions(); + this.isLoading = false; + }), + map((response) => { + return { + blockCount: parseInt(response.headers.get('x-total-count'), 10), + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions() { + let title: object; + if (this.data.blockFees.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + color: [ + '#ff9f00', + '#0aab2f', + ], + animation: false, + grid: { + top: 80, + bottom: 80, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)', + borderRadius: 4, + shadowColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)', + textStyle: { + color: 'var(--tooltip-grey)', + align: 'left', + }, + borderColor: 'var(--active-bg)', + formatter: function (data) { + if (data.length <= 0) { + return ''; + } + let tooltip = `${formatterXAxis(this.locale, this.zoomTimeSpan, parseInt(this.data.timestamp[data[0].dataIndex], 10))}
`; + for (let i = data.length - 1; i >= 0; i--) { + const tick = data[i]; + if (!this.showFiat) tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data, this.locale, '1.0-3')} BTC
`; + else tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data, null, 'USD') }
`; + } + if (!this.showFiat) tooltip += `
${formatNumber(data.reduce((acc, val) => acc + val.data, 0), this.locale, '1.0-3')} BTC
`; + else tooltip += `
${this.fiatCurrencyPipe.transform(data.reduce((acc, val) => acc + val.data, 0), null, 'USD')}
`; + if (['24h', '3d'].includes(this.zoomTimeSpan)) { + tooltip += `` + $localize`At block ${data[0].axisValue}` + ``; + } else { + tooltip += `` + $localize`Around block ${data[0].axisValue}` + ``; + } + return tooltip; + }.bind(this) + }, + xAxis: this.data.blockFees.length === 0 ? undefined : [ + { + type: 'category', + data: this.data.blockHeight, + show: false, + axisLabel: { + hideOverlap: true, + } + }, + { + type: 'category', + data: this.data.timestamp, + show: true, + position: 'bottom', + axisLabel: { + color: 'var(--grey)', + formatter: (val) => { + return formatterXAxis(this.locale, this.timespan, parseInt(val, 10)); + } + }, + axisTick: { + show: false, + }, + axisLine: { + show: false, + }, + splitLine: { + show: false, + }, + } + ], + legend: this.data.blockFees.length === 0 ? undefined : { + data: [ + { + name: 'Subsidy', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Fees', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Subsidy (USD)', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Fees (USD)', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], + selected: { + 'Subsidy (USD)': this.showFiat, + 'Fees (USD)': this.showFiat, + 'Subsidy': !this.showFiat, + 'Fees': !this.showFiat, + }, + }, + yAxis: this.data.blockFees.length === 0 ? undefined : [ + { + type: 'value', + axisLabel: { + color: 'var(--grey)', + formatter: (val) => { + return `${val} BTC`; + } + }, + min: 0, + splitLine: { + lineStyle: { + type: 'dotted', + color: 'var(--transparent-fg)', + opacity: 0.25, + } + }, + }, + { + type: 'value', + position: 'right', + axisLabel: { + color: 'var(--grey)', + formatter: function(val) { + return this.fiatShortenerPipe.transform(val, null, 'USD'); + }.bind(this) + }, + splitLine: { + show: false, + }, + }, + ], + series: this.data.blockFees.length === 0 ? undefined : [ + { + name: 'Subsidy', + yAxisIndex: 0, + type: 'bar', + stack: 'total', + data: this.data.blockSubsidy, + }, + { + name: 'Fees', + yAxisIndex: 0, + type: 'bar', + stack: 'total', + data: this.data.blockFees, + }, + { + name: 'Subsidy (USD)', + yAxisIndex: 1, + type: 'bar', + stack: 'total', + data: this.data.blockSubsidyFiat, + }, + { + name: 'Fees (USD)', + yAxisIndex: 1, + type: 'bar', + stack: 'total', + data: this.data.blockFeesFiat, + }, + ], + dataZoom: this.data.blockFees.length === 0 ? undefined : [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 1, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + }, + }], + }; + } + + onChartInit(ec) { + this.chartInstance = ec; + + this.chartInstance.on('legendselectchanged', (params) => { + const isFiat = params.name.includes('USD'); + if (isFiat === this.showFiat) return; + + const isActivation = params.selected[params.name]; + if (isFiat === isActivation) { + this.showFiat = true; + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy (USD)' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees (USD)' }); + } else { + this.showFiat = false; + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy (USD)' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees (USD)' }); + } + }); + + this.chartInstance.on('datazoom', (params) => { + if (params.silent || this.isLoading || ['24h', '3d'].includes(this.timespan)) { + return; + } + this.updateZoom = true; + }); + + this.chartInstance.on('click', (e) => { + this.zone.run(() => { + if (['24h', '3d'].includes(this.zoomTimeSpan)) { + const url = new RelativeUrlPipe(this.stateService).transform(`/block/${e.name}`); + if (e.event.event.shiftKey || e.event.event.ctrlKey || e.event.event.metaKey) { + window.open(url); + } else { + this.router.navigate([url]); + } + } + }); + }); + } + + @HostListener('document:pointerup', ['$event']) + onPointerUp(event: PointerEvent) { + if (this.updateZoom) { + this.onZoom(); + this.updateZoom = false; + } + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + initSubsidies(): { [key: number]: number } { + let blockReward = 50 * 100_000_000; + const subsidies = {}; + for (let i = 0; i <= 33; i++) { + subsidies[i] = blockReward; + blockReward = Math.floor(blockReward / 2); + } + return subsidies; + } + + onZoom() { + const option = this.chartInstance.getOption(); + const timestamps = option.xAxis[1].data; + const startTimestamp = timestamps[option.dataZoom[0].startValue]; + const endTimestamp = timestamps[option.dataZoom[0].endValue]; + + this.isLoading = true; + this.cd.detectChanges(); + + const subscription = this.apiService.getBlockFeesFromTimespan$(Math.floor(startTimestamp / 1000), Math.floor(endTimestamp / 1000)) + .pipe( + tap((response) => { + const startIndex = option.dataZoom[0].startValue; + const endIndex = option.dataZoom[0].endValue; + + // Update series with more granular data + const lengthBefore = this.data.timestamp.length; + this.data.timestamp.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.timestamp * 1000)); + this.data.blockHeight.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgHeight)); + this.data.blockFees.splice(startIndex, endIndex - startIndex, ...response.body.map(val => val.avgFees / 100_000_000)); + this.data.blockFeesFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => val.avgFees / 100_000_000 * val['USD'])); + this.data.blockSubsidy.splice(startIndex, endIndex - startIndex, ...response.body.map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000)); + this.data.blockSubsidyFiat.splice(startIndex, endIndex - startIndex, ...response.body.filter(val => val['USD'] > 0).map(val => this.subsidies[Math.floor(Math.min(val.avgHeight / 210000, 33))] / 100_000_000 * val['USD'])); + option.series[0].data = this.data.blockSubsidy; + option.series[1].data = this.data.blockFees; + option.series[2].data = this.data.blockSubsidyFiat; + option.series[3].data = this.data.blockFeesFiat; + option.xAxis[0].data = this.data.blockHeight; + option.xAxis[1].data = this.data.timestamp; + this.chartInstance.setOption(option, true); + const lengthAfter = this.data.timestamp.length; + + // Update the zoom to keep the same range after the update + this.chartInstance.dispatchAction({ + type: 'dataZoom', + startValue: startIndex, + endValue: endIndex + lengthAfter - lengthBefore, + silent: true, + }); + + // Update the chart + const newOption = this.chartInstance.getOption(); + this.zoomSpan = newOption.dataZoom[0].end - newOption.dataZoom[0].start; + this.zoomTimeSpan = this.getTimeRangeFromTimespan(Math.floor(this.data.timestamp[newOption.dataZoom[0].startValue] / 1000), Math.floor(this.data.timestamp[newOption.dataZoom[0].endValue] / 1000)); + this.isLoading = false; + }), + catchError(() => { + const newOption = this.chartInstance.getOption(); + this.zoomSpan = newOption.dataZoom[0].end - newOption.dataZoom[0].start; + this.zoomTimeSpan = this.getTimeRangeFromTimespan(Math.floor(this.data.timestamp[newOption.dataZoom[0].startValue] / 1000), Math.floor(this.data.timestamp[newOption.dataZoom[0].endValue] / 1000)); + this.isLoading = false; + this.cd.detectChanges(); + return []; + }) + ).subscribe(() => { + subscription.unsubscribe(); + this.cd.detectChanges(); + }); + } + + getTimeRangeFromTimespan(from: number, to: number): string { + const timespan = to - from; + switch (true) { + case timespan >= 3600 * 24 * 365 * 4: return 'all'; + case timespan >= 3600 * 24 * 365 * 3: return '4y'; + case timespan >= 3600 * 24 * 365 * 2: return '3y'; + case timespan >= 3600 * 24 * 365: return '2y'; + case timespan >= 3600 * 24 * 30 * 6: return '1y'; + case timespan >= 3600 * 24 * 30 * 3: return '6m'; + case timespan >= 3600 * 24 * 30: return '3m'; + case timespan >= 3600 * 24 * 7: return '1m'; + case timespan >= 3600 * 24 * 3: return '1w'; + case timespan >= 3600 * 24: return '3d'; + default: return '24h'; + } + } + + onSaveChart() { + // @ts-ignore + const prevBottom = this.chartOptions.grid.bottom; + const now = new Date(); + // @ts-ignore + this.chartOptions.grid.bottom = 40; + this.chartOptions.backgroundColor = 'var(--active-bg)'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + excludeComponents: ['dataZoom'], + }), `block-fees-subsidy-${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 94241b825..53d18bbc2 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -1,9 +1,9 @@ -