2023-06-15 18:56:34 -04:00
|
|
|
import { Component, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
2024-10-22 21:05:01 +09:00
|
|
|
import { EChartsOption } from '@app/graphs/echarts';
|
2021-08-21 01:46:28 -03:00
|
|
|
import { OnChanges } from '@angular/core';
|
2024-10-22 21:05:01 +09:00
|
|
|
import { StorageService } from '@app/services/storage.service';
|
|
|
|
import { download, formatterXAxis, formatterXAxisLabel } from '@app/shared/graphs.utils';
|
2022-03-22 16:03:54 +09:00
|
|
|
import { formatNumber } from '@angular/common';
|
2024-10-22 21:05:01 +09:00
|
|
|
import { StateService } from '@app/services/state.service';
|
2023-06-15 18:56:34 -04:00
|
|
|
import { Subscription } from 'rxjs';
|
2021-08-21 01:46:28 -03:00
|
|
|
|
2023-11-15 14:08:44 +09:00
|
|
|
const OUTLIERS_MEDIAN_MULTIPLIER = 4;
|
|
|
|
|
2021-08-21 01:46:28 -03:00
|
|
|
@Component({
|
|
|
|
selector: 'app-incoming-transactions-graph',
|
|
|
|
templateUrl: './incoming-transactions-graph.component.html',
|
2021-11-19 00:10:12 +04:00
|
|
|
styles: [`
|
|
|
|
.loadingGraphs {
|
|
|
|
position: absolute;
|
|
|
|
top: 50%;
|
|
|
|
left: calc(50% - 16px);
|
2024-07-26 00:00:14 +02:00
|
|
|
z-index: 99;
|
2021-11-19 00:10:12 +04:00
|
|
|
}
|
|
|
|
`],
|
2021-08-21 01:46:28 -03:00
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
|
})
|
2023-06-15 18:56:34 -04:00
|
|
|
export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, OnDestroy {
|
2021-08-21 01:46:28 -03:00
|
|
|
@Input() data: any;
|
|
|
|
@Input() theme: string;
|
|
|
|
@Input() height: number | string = '200';
|
|
|
|
@Input() right: number | string = '10';
|
|
|
|
@Input() top: number | string = '20';
|
2021-08-26 23:30:57 -03:00
|
|
|
@Input() left: number | string = '0';
|
|
|
|
@Input() template: ('widget' | 'advanced') = 'widget';
|
2021-12-11 15:26:59 +09:00
|
|
|
@Input() windowPreferenceOverride: string;
|
2023-11-15 14:08:44 +09:00
|
|
|
@Input() outlierCappingEnabled: boolean = false;
|
2024-05-23 18:40:48 +02:00
|
|
|
@Input() isLoading: boolean;
|
2021-08-21 01:46:28 -03:00
|
|
|
|
|
|
|
mempoolStatsChartOption: EChartsOption = {};
|
2021-09-07 17:54:27 -03:00
|
|
|
mempoolStatsChartInitOption = {
|
|
|
|
renderer: 'svg'
|
|
|
|
};
|
2021-08-25 01:01:35 -03:00
|
|
|
windowPreference: string;
|
2022-05-05 16:38:16 +09:00
|
|
|
chartInstance: any = undefined;
|
2023-10-11 08:09:06 -04:00
|
|
|
MA: number[][] = [];
|
2023-06-15 18:56:34 -04:00
|
|
|
weightMode: boolean = false;
|
|
|
|
rateUnitSub: Subscription;
|
2023-11-15 14:08:44 +09:00
|
|
|
medianVbytesPerSecond: number | undefined;
|
2021-08-21 01:46:28 -03:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
@Inject(LOCALE_ID) private locale: string,
|
2021-08-25 01:01:35 -03:00
|
|
|
private storageService: StorageService,
|
2023-11-02 01:29:55 +00:00
|
|
|
public stateService: StateService,
|
2021-08-21 01:46:28 -03:00
|
|
|
) { }
|
|
|
|
|
2021-11-19 00:10:12 +04:00
|
|
|
ngOnInit() {
|
2023-06-15 18:56:34 -04:00
|
|
|
this.rateUnitSub = this.stateService.rateUnits$.subscribe(rateUnits => {
|
|
|
|
this.weightMode = rateUnits === 'wu';
|
|
|
|
if (this.data) {
|
|
|
|
this.mountChart();
|
|
|
|
}
|
|
|
|
});
|
2021-11-19 00:10:12 +04:00
|
|
|
}
|
|
|
|
|
2021-08-21 01:46:28 -03:00
|
|
|
ngOnChanges(): void {
|
2021-11-19 00:10:12 +04:00
|
|
|
if (!this.data) {
|
|
|
|
return;
|
|
|
|
}
|
2024-05-09 18:00:09 +00:00
|
|
|
this.windowPreference = (this.windowPreferenceOverride ? this.windowPreferenceOverride : this.storageService.getValue('graphWindowPreference')) || '2h';
|
2023-11-12 07:59:06 +00:00
|
|
|
const windowSize = Math.max(10, Math.floor(this.data.series[0].length / 8));
|
|
|
|
this.MA = this.calculateMA(this.data.series[0], windowSize);
|
2023-11-15 14:08:44 +09:00
|
|
|
if (this.outlierCappingEnabled === true) {
|
|
|
|
this.computeMedianVbytesPerSecond(this.data.series[0]);
|
|
|
|
}
|
2021-08-21 01:46:28 -03:00
|
|
|
this.mountChart();
|
|
|
|
}
|
|
|
|
|
2021-11-19 00:10:12 +04:00
|
|
|
rendered() {
|
|
|
|
if (!this.data) {
|
2023-11-15 14:08:44 +09:00
|
|
|
return;
|
2021-11-19 00:10:12 +04:00
|
|
|
}
|
2021-08-21 01:46:28 -03:00
|
|
|
}
|
|
|
|
|
2023-11-15 14:08:44 +09:00
|
|
|
/**
|
|
|
|
* Calculate the median value of the vbytes per second chart to hide outliers
|
|
|
|
*/
|
|
|
|
computeMedianVbytesPerSecond(data: number[][]): void {
|
|
|
|
const vBytes: number[] = [];
|
|
|
|
for (const value of data) {
|
|
|
|
vBytes.push(value[1]);
|
|
|
|
}
|
|
|
|
const sorted = vBytes.slice().sort((a, b) => a - b);
|
|
|
|
const middle = Math.floor(sorted.length / 2);
|
|
|
|
this.medianVbytesPerSecond = sorted[middle];
|
|
|
|
if (sorted.length % 2 === 0) {
|
|
|
|
this.medianVbytesPerSecond = (sorted[middle - 1] + sorted[middle]) / 2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-12 07:59:06 +00:00
|
|
|
/// calculate the moving average of the provided data based on windowSize
|
|
|
|
calculateMA(data: number[][], windowSize: number = 100): number[][] {
|
2023-10-11 08:09:06 -04:00
|
|
|
//update const variables that are not changed
|
|
|
|
const ma: number[][] = [];
|
|
|
|
let sum = 0;
|
|
|
|
let i = 0;
|
|
|
|
|
|
|
|
//calculate the centered moving average
|
2023-11-12 07:59:06 +00:00
|
|
|
for (i = 0; i < data.length; i++) {
|
|
|
|
sum += data[i][1];
|
|
|
|
if (i >= windowSize) {
|
|
|
|
sum -= data[i - windowSize][1];
|
|
|
|
const midpoint = i - Math.floor(windowSize / 2);
|
|
|
|
const avg = sum / windowSize;
|
|
|
|
ma.push([data[midpoint][0], avg]);
|
2023-10-11 08:09:06 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//return the moving average array
|
|
|
|
return ma;
|
|
|
|
}
|
|
|
|
|
2021-08-21 01:46:28 -03:00
|
|
|
mountChart(): void {
|
2023-10-11 08:09:06 -04:00
|
|
|
//create an array for the echart series
|
|
|
|
//similar to how it is done in mempool-graph.component.ts
|
|
|
|
const seriesGraph = [];
|
|
|
|
seriesGraph.push({
|
|
|
|
zlevel: 0,
|
|
|
|
name: 'data',
|
|
|
|
data: this.data.series[0],
|
|
|
|
type: 'line',
|
|
|
|
smooth: false,
|
|
|
|
showSymbol: false,
|
|
|
|
symbol: 'none',
|
|
|
|
lineStyle: {
|
|
|
|
width: 3,
|
|
|
|
},
|
|
|
|
markLine: {
|
|
|
|
silent: true,
|
|
|
|
symbol: 'none',
|
|
|
|
lineStyle: {
|
|
|
|
color: '#fff',
|
|
|
|
opacity: 1,
|
|
|
|
width: 2,
|
|
|
|
},
|
|
|
|
data: [{
|
|
|
|
yAxis: 1667,
|
|
|
|
label: {
|
|
|
|
show: false,
|
|
|
|
color: '#ffffff',
|
|
|
|
}
|
|
|
|
}],
|
|
|
|
}
|
2023-11-12 07:59:06 +00:00
|
|
|
});
|
|
|
|
if (this.template !== 'widget') {
|
|
|
|
seriesGraph.push({
|
|
|
|
zlevel: 0,
|
|
|
|
name: 'MA',
|
|
|
|
data: this.MA,
|
|
|
|
type: 'line',
|
|
|
|
smooth: false,
|
|
|
|
showSymbol: false,
|
2023-10-11 08:09:06 -04:00
|
|
|
symbol: 'none',
|
|
|
|
lineStyle: {
|
|
|
|
width: 2,
|
2023-11-12 07:59:06 +00:00
|
|
|
color: "white",
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-10-11 08:09:06 -04:00
|
|
|
|
2021-08-21 01:46:28 -03:00
|
|
|
this.mempoolStatsChartOption = {
|
|
|
|
grid: {
|
|
|
|
height: this.height,
|
|
|
|
right: this.right,
|
|
|
|
top: this.top,
|
|
|
|
left: this.left,
|
|
|
|
},
|
2021-08-26 23:30:57 -03:00
|
|
|
animation: false,
|
2022-01-13 12:00:49 +09:00
|
|
|
dataZoom: (this.template === 'widget' && this.isMobile()) ? null : [{
|
2021-08-25 01:01:35 -03:00
|
|
|
type: 'inside',
|
|
|
|
realtime: true,
|
2022-01-11 12:16:09 +09:00
|
|
|
zoomLock: (this.template === 'widget') ? true : false,
|
2021-08-26 23:30:57 -03:00
|
|
|
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
|
2022-01-11 12:16:09 +09:00
|
|
|
moveOnMouseMove: (this.template === 'widget') ? true : false,
|
2021-08-26 23:30:57 -03:00
|
|
|
maxSpan: 100,
|
|
|
|
minSpan: 10,
|
2021-08-25 01:01:35 -03:00
|
|
|
}, {
|
2021-12-14 16:33:17 +09:00
|
|
|
showDetail: false,
|
2021-08-26 23:30:57 -03:00
|
|
|
show: (this.template === 'advanced') ? true : false,
|
2021-08-25 01:01:35 -03:00
|
|
|
type: 'slider',
|
|
|
|
brushSelect: false,
|
|
|
|
realtime: true,
|
2021-12-13 14:27:05 +09:00
|
|
|
bottom: 0,
|
2021-08-25 01:01:35 -03:00
|
|
|
selectedDataBackground: {
|
|
|
|
lineStyle: {
|
|
|
|
color: '#fff',
|
|
|
|
opacity: 0.45,
|
|
|
|
},
|
|
|
|
areaStyle: {
|
|
|
|
opacity: 0,
|
|
|
|
}
|
2021-12-13 14:27:05 +09:00
|
|
|
},
|
2021-08-25 01:01:35 -03:00
|
|
|
}],
|
2021-08-21 01:46:28 -03:00
|
|
|
tooltip: {
|
2022-01-13 12:00:49 +09:00
|
|
|
show: !this.isMobile(),
|
2021-08-21 01:46:28 -03:00
|
|
|
trigger: 'axis',
|
|
|
|
position: (pos, params, el, elRect, size) => {
|
|
|
|
const obj = { top: -20 };
|
|
|
|
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
|
|
|
|
return obj;
|
|
|
|
},
|
2023-11-19 16:42:09 +01:00
|
|
|
extraCssText: `background: transparent;
|
2021-08-21 01:46:28 -03:00
|
|
|
border: none;
|
|
|
|
box-shadow: none;`,
|
|
|
|
axisPointer: {
|
2021-08-25 01:01:35 -03:00
|
|
|
type: 'line',
|
2021-08-21 01:46:28 -03:00
|
|
|
},
|
|
|
|
formatter: (params: any) => {
|
2024-05-09 18:00:09 +00:00
|
|
|
const bestItem = params.reduce((best, item) => {
|
|
|
|
return (item.seriesName === 'data' && (!best || best.value[1] < item.value[1])) ? item : best;
|
|
|
|
}, null);
|
|
|
|
const axisValueLabel: string = formatterXAxis(this.locale, this.windowPreference, bestItem.axisValue);
|
2021-08-26 23:30:57 -03:00
|
|
|
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
|
2021-12-11 15:26:59 +09:00
|
|
|
let itemFormatted = '<div class="title">' + axisValueLabel + '</div>';
|
2024-05-09 18:00:09 +00:00
|
|
|
if (bestItem) {
|
|
|
|
itemFormatted += `<div class="item">
|
|
|
|
<div class="indicator-container">${colorSpan(bestItem.color)}</div>
|
2023-10-11 08:09:06 -04:00
|
|
|
<div class="grow"></div>
|
2024-05-24 17:08:36 +02:00
|
|
|
<div class="value">${formatNumber(bestItem.value[1], this.locale, '1.0-0')} <span class="symbol">vB/s</span></div>
|
2023-10-11 08:09:06 -04:00
|
|
|
</div>`;
|
2024-05-09 18:00:09 +00:00
|
|
|
}
|
2023-11-19 16:42:09 +01:00
|
|
|
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}"
|
|
|
|
style="width: ${(this.windowPreference === '2h' || this.template === 'widget') ? '125px' : '215px'}">${itemFormatted}</div>`;
|
2021-08-21 01:46:28 -03:00
|
|
|
}
|
|
|
|
},
|
2021-12-13 14:27:05 +09:00
|
|
|
xAxis: [
|
|
|
|
{
|
2022-06-14 09:58:32 +02:00
|
|
|
name: this.template === 'widget' ? '' : formatterXAxisLabel(this.locale, this.windowPreference),
|
2021-12-13 14:27:05 +09:00
|
|
|
nameLocation: 'middle',
|
|
|
|
nameTextStyle: {
|
|
|
|
padding: [20, 0, 0, 0],
|
|
|
|
},
|
|
|
|
type: 'time',
|
|
|
|
axisLabel: {
|
|
|
|
margin: 20,
|
|
|
|
align: 'center',
|
|
|
|
fontSize: 11,
|
|
|
|
lineHeight: 12,
|
|
|
|
hideOverlap: true,
|
|
|
|
padding: [0, 5],
|
|
|
|
},
|
|
|
|
}
|
|
|
|
],
|
2021-08-21 01:46:28 -03:00
|
|
|
yAxis: {
|
2024-06-22 01:58:46 +00:00
|
|
|
max: (value): number => {
|
|
|
|
let cappedMax = value.max;
|
|
|
|
if (this.outlierCappingEnabled && value.max >= (this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER)) {
|
|
|
|
cappedMax = Math.round(this.medianVbytesPerSecond * OUTLIERS_MEDIAN_MULTIPLIER);
|
2023-11-15 18:46:33 +09:00
|
|
|
}
|
2024-06-22 01:58:46 +00:00
|
|
|
// always show the clearing rate line, plus a small margin
|
|
|
|
return Math.max(1800, cappedMax);
|
2023-11-15 18:46:33 +09:00
|
|
|
},
|
2021-08-21 01:46:28 -03:00
|
|
|
type: 'value',
|
2021-09-17 09:40:51 -03:00
|
|
|
axisLabel: {
|
|
|
|
fontSize: 11,
|
2024-03-24 16:22:05 +09:00
|
|
|
formatter: (value): string => {
|
|
|
|
return this.weightMode ? (value * 4).toString() : value.toString();
|
2023-06-15 18:56:34 -04:00
|
|
|
}
|
2021-09-17 09:40:51 -03:00
|
|
|
},
|
2021-08-21 01:46:28 -03:00
|
|
|
splitLine: {
|
|
|
|
lineStyle: {
|
|
|
|
type: 'dotted',
|
2024-04-04 15:36:24 +09:00
|
|
|
color: 'var(--transparent-fg)',
|
2021-08-21 01:46:28 -03:00
|
|
|
opacity: 0.25,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2023-10-11 08:09:06 -04:00
|
|
|
series: seriesGraph,
|
2021-08-21 01:46:28 -03:00
|
|
|
visualMap: {
|
|
|
|
show: false,
|
|
|
|
top: 50,
|
|
|
|
right: 10,
|
|
|
|
pieces: [{
|
|
|
|
gt: 0,
|
|
|
|
lte: 1667,
|
|
|
|
color: '#7CB342'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
gt: 1667,
|
|
|
|
lte: 2000,
|
|
|
|
color: '#FDD835'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
gt: 2000,
|
|
|
|
lte: 2500,
|
|
|
|
color: '#FFB300'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
gt: 2500,
|
|
|
|
lte: 3000,
|
|
|
|
color: '#FB8C00'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
gt: 3000,
|
|
|
|
lte: 3500,
|
|
|
|
color: '#F4511E'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
gt: 3500,
|
|
|
|
color: '#D81B60'
|
|
|
|
}],
|
|
|
|
outOfRange: {
|
|
|
|
color: '#999'
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
2022-01-13 12:00:49 +09:00
|
|
|
|
2022-05-05 16:38:16 +09:00
|
|
|
onChartInit(ec) {
|
|
|
|
this.chartInstance = ec;
|
|
|
|
}
|
|
|
|
|
2022-01-13 12:00:49 +09:00
|
|
|
isMobile() {
|
|
|
|
return window.innerWidth <= 767.98;
|
|
|
|
}
|
2022-05-05 16:38:16 +09:00
|
|
|
|
|
|
|
onSaveChart(timespan) {
|
|
|
|
// @ts-ignore
|
|
|
|
const prevHeight = this.mempoolStatsChartOption.grid.height;
|
|
|
|
const now = new Date();
|
|
|
|
// @ts-ignore
|
|
|
|
this.mempoolStatsChartOption.grid.height = prevHeight + 20;
|
2024-04-04 15:36:24 +09:00
|
|
|
this.mempoolStatsChartOption.backgroundColor = 'var(--active-bg)';
|
2022-05-05 16:38:16 +09:00
|
|
|
this.chartInstance.setOption(this.mempoolStatsChartOption);
|
|
|
|
download(this.chartInstance.getDataURL({
|
|
|
|
pixelRatio: 2,
|
|
|
|
excludeComponents: ['dataZoom'],
|
2022-05-09 11:01:51 +02:00
|
|
|
}), `incoming-vbytes-${timespan}-${Math.round(now.getTime() / 1000)}.svg`);
|
2022-05-05 16:38:16 +09:00
|
|
|
// @ts-ignore
|
|
|
|
this.mempoolStatsChartOption.grid.height = prevHeight;
|
2022-05-09 11:01:51 +02:00
|
|
|
this.mempoolStatsChartOption.backgroundColor = 'none';
|
2022-05-05 16:38:16 +09:00
|
|
|
this.chartInstance.setOption(this.mempoolStatsChartOption);
|
|
|
|
}
|
2023-06-15 18:56:34 -04:00
|
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
|
|
this.rateUnitSub.unsubscribe();
|
|
|
|
}
|
2021-08-21 01:46:28 -03:00
|
|
|
}
|