mirror of
https://github.com/mempool/mempool.git
synced 2025-02-28 16:58:32 +01:00
390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
|
|
import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe';
|
|
import { formatNumber } from "@angular/common";
|
|
import { OptimizedMempoolStats } from 'src/app/interfaces/node-api.interface';
|
|
import { StateService } from 'src/app/services/state.service';
|
|
import { StorageService } from 'src/app/services/storage.service';
|
|
import { EChartsOption } from 'echarts';
|
|
import { feeLevels, chartColors } from 'src/app/app.constants';
|
|
import { formatterXAxis, formatterXAxisLabel } from 'src/app/shared/graphs.utils';
|
|
|
|
@Component({
|
|
selector: 'app-mempool-graph',
|
|
templateUrl: './mempool-graph.component.html',
|
|
styles: [`
|
|
.loadingGraphs {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: calc(50% - 16px);
|
|
z-index: 100;
|
|
}
|
|
`],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class MempoolGraphComponent implements OnInit, OnChanges {
|
|
@Input() data: any[];
|
|
@Input() limitFee = 350;
|
|
@Input() limitFilterFee = 1;
|
|
@Input() height: number | string = 200;
|
|
@Input() top: number | string = 20;
|
|
@Input() right: number | string = 10;
|
|
@Input() left: number | string = 75;
|
|
@Input() template: ('widget' | 'advanced') = 'widget';
|
|
@Input() showZoom = true;
|
|
@Input() windowPreferenceOverride: string;
|
|
|
|
isLoading = true;
|
|
mempoolVsizeFeesData: any;
|
|
mempoolVsizeFeesOptions: EChartsOption;
|
|
mempoolVsizeFeesInitOptions = {
|
|
renderer: 'svg',
|
|
};
|
|
windowPreference: string;
|
|
hoverIndexSerie = 0;
|
|
feeLimitIndex: number;
|
|
feeLevelsOrdered = [];
|
|
chartColorsOrdered = chartColors;
|
|
inverted: boolean;
|
|
|
|
constructor(
|
|
private vbytesPipe: VbytesPipe,
|
|
private stateService: StateService,
|
|
private storageService: StorageService,
|
|
@Inject(LOCALE_ID) private locale: string,
|
|
) { }
|
|
|
|
ngOnInit(): void {
|
|
this.isLoading = true;
|
|
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
|
|
}
|
|
|
|
ngOnChanges() {
|
|
if (!this.data) {
|
|
return;
|
|
}
|
|
this.windowPreference = this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference');
|
|
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
|
|
this.mountFeeChart();
|
|
}
|
|
|
|
rendered() {
|
|
if (!this.data) {
|
|
return;
|
|
}
|
|
this.isLoading = false;
|
|
}
|
|
|
|
onChartReady(myChart: any) {
|
|
myChart.getZr().on('mousemove', (e: any) => {
|
|
if (e.target !== undefined &&
|
|
e.target.parent !== undefined &&
|
|
e.target.parent.parent !== null &&
|
|
e.target.parent.parent.__ecComponentInfo !== undefined) {
|
|
this.hoverIndexSerie = e.target.parent.parent.__ecComponentInfo.index;
|
|
}
|
|
});
|
|
}
|
|
|
|
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
|
|
mempoolStats.reverse();
|
|
const labels = mempoolStats.map(stats => stats.added);
|
|
const finalArrayVByte = this.generateArray(mempoolStats);
|
|
|
|
return {
|
|
labels: labels,
|
|
series: finalArrayVByte
|
|
};
|
|
}
|
|
|
|
generateArray(mempoolStats: OptimizedMempoolStats[]) {
|
|
const finalArray: number[][][] = [];
|
|
let feesArray: number[][] = [];
|
|
let limitFeesTemplate = this.template === 'advanced' ? 26 : 20;
|
|
for (let index = limitFeesTemplate; index > -1; index--) {
|
|
feesArray = [];
|
|
mempoolStats.forEach((stats) => {
|
|
feesArray.push([stats.added * 1000, stats.vsizes[index] ? stats.vsizes[index] : 0]);
|
|
});
|
|
finalArray.push(feesArray);
|
|
}
|
|
finalArray.reverse();
|
|
return finalArray;
|
|
}
|
|
|
|
mountFeeChart() {
|
|
this.orderLevels();
|
|
const { series } = this.mempoolVsizeFeesData;
|
|
|
|
const seriesGraph = [];
|
|
const newColors = [];
|
|
for (let index = 0; index < series.length; index++) {
|
|
const value = series[index];
|
|
if (index >= this.feeLimitIndex) {
|
|
newColors.push(this.chartColorsOrdered[index]);
|
|
seriesGraph.push({
|
|
name: this.feeLevelsOrdered[index],
|
|
type: 'line',
|
|
stack: 'fees',
|
|
smooth: false,
|
|
markPoint: {
|
|
symbol: 'rect',
|
|
},
|
|
lineStyle: {
|
|
width: 0,
|
|
opacity: 0,
|
|
},
|
|
symbol: 'none',
|
|
emphasis: {
|
|
focus: 'none',
|
|
areaStyle: {
|
|
opacity: 0.85,
|
|
},
|
|
},
|
|
markLine: {
|
|
silent: true,
|
|
symbol: 'none',
|
|
lineStyle: {
|
|
color: '#fff',
|
|
opacity: 1,
|
|
width: this.inverted ? 2 : 0,
|
|
},
|
|
data: [{
|
|
yAxis: '1000000',
|
|
label: {
|
|
show: false,
|
|
color: '#ffffff',
|
|
}
|
|
}],
|
|
},
|
|
areaStyle: {
|
|
color: this.chartColorsOrdered[index],
|
|
opacity: 1,
|
|
},
|
|
data: value
|
|
});
|
|
}
|
|
}
|
|
|
|
this.mempoolVsizeFeesOptions = {
|
|
series: this.inverted ? [...seriesGraph].reverse() : seriesGraph,
|
|
hover: true,
|
|
color: this.inverted ? [...newColors].reverse() : newColors,
|
|
tooltip: {
|
|
show: !this.isMobile(),
|
|
trigger: 'axis',
|
|
alwaysShowContent: false,
|
|
position: (pos, params, el, elRect, size) => {
|
|
const positions = { top: (this.template === 'advanced') ? 0 : -30 };
|
|
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 60;
|
|
return positions;
|
|
},
|
|
extraCssText: `width: ${(this.template === 'advanced') ? '275px' : '200px'};
|
|
background: transparent;
|
|
border: none;
|
|
box-shadow: none;`,
|
|
axisPointer: {
|
|
type: 'line',
|
|
},
|
|
formatter: (params: any) => {
|
|
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, params[0].axisValue);
|
|
const { totalValue, totalValueArray } = this.getTotalValues(params);
|
|
const itemFormatted = [];
|
|
let totalParcial = 0;
|
|
let progressPercentageText = '';
|
|
const items = this.inverted ? [...params].reverse() : params;
|
|
items.map((item: any, index: number) => {
|
|
totalParcial += item.value[1];
|
|
const progressPercentage = (item.value[1] / totalValue) * 100;
|
|
const progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
|
|
let activeItemClass = '';
|
|
let hoverActive = 0;
|
|
if (this.inverted) {
|
|
hoverActive = Math.abs(this.feeLevelsOrdered.length - item.seriesIndex - this.feeLevelsOrdered.length);
|
|
} else {
|
|
hoverActive = item.seriesIndex;
|
|
}
|
|
if (this.hoverIndexSerie === hoverActive) {
|
|
progressPercentageText = `<div class="total-parcial-active">
|
|
<span class="progress-percentage">
|
|
${formatNumber(progressPercentage, this.locale, '1.2-2')}
|
|
<span class="symbol">%</span>
|
|
</span>
|
|
<span class="total-parcial-vbytes">
|
|
${this.vbytesPipe.transform(totalParcial, 2, 'vB', 'MvB', false)}
|
|
</span>
|
|
<div class="total-percentage-bar">
|
|
<span class="total-percentage-bar-background">
|
|
<span style="
|
|
width: ${progressPercentage}%;
|
|
background: ${item.color}
|
|
"></span>
|
|
</span>
|
|
</div>
|
|
</div>`;
|
|
activeItemClass = 'active';
|
|
}
|
|
itemFormatted.push(`<tr class="item ${activeItemClass}">
|
|
<td class="indicator-container">
|
|
<span class="indicator" style="
|
|
background-color: ${item.color}
|
|
"></span>
|
|
<span>
|
|
${item.seriesName}
|
|
</span>
|
|
</td>
|
|
<td class="total-progress-sum">
|
|
<span>
|
|
${this.vbytesPipe.transform(item.value[1], 2, 'vB', 'MvB', false)}
|
|
</span>
|
|
</td>
|
|
<td class="total-progress-sum">
|
|
<span>
|
|
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
|
|
</span>
|
|
</td>
|
|
<td class="total-progress-sum-bar">
|
|
<span class="total-percentage-bar-background">
|
|
<span style="
|
|
width: ${progressPercentageSum.toFixed(2)}%;
|
|
background-color: ${this.chartColorsOrdered[3]}
|
|
"></span>
|
|
</span>
|
|
</td>
|
|
</tr>`);
|
|
});
|
|
const classActive = (this.template === 'advanced') ? 'fees-wrapper-tooltip-chart-advanced' : '';
|
|
const titleRange = $localize`Range`;
|
|
const titleSize = $localize`:@@7faaaa08f56427999f3be41df1093ce4089bbd75:Size`;
|
|
const titleSum = $localize`Sum`;
|
|
return `<div class="fees-wrapper-tooltip-chart ${classActive}">
|
|
<div class="title">
|
|
${axisValueLabel}
|
|
<span class="total-value">
|
|
${this.vbytesPipe.transform(totalValue, 2, 'vB', 'MvB', false)}
|
|
</span>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>${titleRange}</th>
|
|
<th>${titleSize}</th>
|
|
<th>${titleSum}</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${this.inverted ? itemFormatted.join('') : itemFormatted.reverse().join('')}
|
|
</tbody>
|
|
</table>
|
|
<span class="total-value">
|
|
${progressPercentageText}
|
|
</span>
|
|
</div>`;
|
|
}
|
|
},
|
|
dataZoom: (this.template === 'widget' && this.isMobile()) ? null : [{
|
|
type: 'inside',
|
|
realtime: true,
|
|
zoomLock: (this.template === 'widget') ? true : false,
|
|
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
|
|
moveOnMouseMove: (this.template === 'widget') ? true : false,
|
|
maxSpan: 100,
|
|
minSpan: 10,
|
|
}, {
|
|
showDetail: false,
|
|
show: (this.template === 'advanced' && this.showZoom) ? true : false,
|
|
type: 'slider',
|
|
brushSelect: false,
|
|
realtime: true,
|
|
bottom: 0,
|
|
selectedDataBackground: {
|
|
lineStyle: {
|
|
color: '#fff',
|
|
opacity: 0.45,
|
|
},
|
|
areaStyle: {
|
|
opacity: 0,
|
|
}
|
|
}
|
|
}],
|
|
animation: false,
|
|
grid: {
|
|
height: this.height,
|
|
right: this.right,
|
|
top: this.top,
|
|
left: this.left,
|
|
},
|
|
xAxis: [
|
|
{
|
|
name: formatterXAxisLabel(this.locale, this.windowPreference),
|
|
nameLocation: 'middle',
|
|
nameTextStyle: {
|
|
padding: [20, 0, 0, 0],
|
|
},
|
|
type: 'time',
|
|
boundaryGap: false,
|
|
axisLine: { onZero: true },
|
|
axisLabel: {
|
|
margin: 20,
|
|
align: 'center',
|
|
fontSize: 11,
|
|
lineHeight: 12,
|
|
hideOverlap: true,
|
|
padding: [0, 5],
|
|
},
|
|
}
|
|
],
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLine: { onZero: false },
|
|
axisLabel: {
|
|
fontSize: 11,
|
|
formatter: (value: number) => (`${this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)}`),
|
|
},
|
|
splitLine: {
|
|
lineStyle: {
|
|
type: 'dotted',
|
|
color: '#ffffff66',
|
|
opacity: 0.25,
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
getTotalValues = (values: any) => {
|
|
let totalValueTemp = 0;
|
|
const totalValueArray = [];
|
|
const valuesInverted = this.inverted ? values : [...values].reverse();
|
|
for (const item of valuesInverted) {
|
|
totalValueTemp += item.value[1];
|
|
totalValueArray.push(totalValueTemp);
|
|
}
|
|
return {
|
|
totalValue: totalValueTemp,
|
|
totalValueArray: totalValueArray.reverse(),
|
|
};
|
|
}
|
|
|
|
orderLevels() {
|
|
this.feeLevelsOrdered = [];
|
|
for (let i = 0; i < feeLevels.length; i++) {
|
|
if (feeLevels[i] === this.limitFilterFee) {
|
|
this.feeLimitIndex = i;
|
|
}
|
|
if (feeLevels[i] <= this.limitFee) {
|
|
if (this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') {
|
|
this.feeLevelsOrdered.push(`${(feeLevels[i] / 10).toFixed(1)} - ${(feeLevels[i + 1] / 10).toFixed(1)}`);
|
|
} else {
|
|
this.feeLevelsOrdered.push(`${feeLevels[i]} - ${feeLevels[i + 1]}`);
|
|
}
|
|
}
|
|
}
|
|
this.chartColorsOrdered = chartColors.slice(0, this.feeLevelsOrdered.length);
|
|
}
|
|
|
|
isMobile() {
|
|
return window.innerWidth <= 767.98;
|
|
}
|
|
}
|
|
|