Add total sum to mempool chart.

Add zoom tools.
Add different theme for charts `big` and `small` (default).
Fix date format on mouseover.
Fix animations on graphs page.
Fix overflow tv page.
Remove `crosshair` on mouseover, changed to `line`.
Fix custom tooltip styles.
Remove inverted button (will add in a future PR).
Remove fee range labels (will add in a future PR).
Fix e2e testing.
This commit is contained in:
Miguel Medeiros 2021-08-25 01:01:35 -03:00
parent 9b956ff88d
commit 3574f8639e
No known key found for this signature in database
GPG Key ID: 819EDEE4673F3EBB
7 changed files with 233 additions and 111 deletions

View File

@ -281,12 +281,12 @@ describe('Mainnet', () => {
}); });
}); });
it('loads the tv screen - mobile', () => { it.only('loads the tv screen - mobile', () => {
cy.viewport('iphone-6'); cy.viewport('iphone-6');
cy.visit('/tv'); cy.visit('/tv');
cy.waitForSkeletonGone(); cy.waitForSkeletonGone();
cy.get('.chart-holder'); cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('be.visible'); cy.get('.blockchain-wrapper').should('not.visible');
}); });
it('loads the api screen', () => { it('loads the api screen', () => {

View File

@ -2,6 +2,7 @@ import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy }
import { formatDate } from '@angular/common'; import { formatDate } from '@angular/common';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
import { OnChanges } from '@angular/core'; import { OnChanges } from '@angular/core';
import { StorageService } from 'src/app/services/storage.service';
@Component({ @Component({
selector: 'app-incoming-transactions-graph', selector: 'app-incoming-transactions-graph',
@ -15,14 +16,18 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
@Input() right: number | string = '10'; @Input() right: number | string = '10';
@Input() top: number | string = '20'; @Input() top: number | string = '20';
@Input() left: number | string = '50'; @Input() left: number | string = '50';
@Input() size: ('small' | 'big') = 'small';
mempoolStatsChartOption: EChartsOption = {}; mempoolStatsChartOption: EChartsOption = {};
windowPreference: string;
constructor( constructor(
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
) { } ) { }
ngOnChanges(): void { ngOnChanges(): void {
this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mountChart(); this.mountChart();
} }
@ -38,6 +43,24 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
top: this.top, top: this.top,
left: this.left, left: this.left,
}, },
dataZoom: [{
type: 'inside',
realtime: true,
}, {
show: (this.size === 'big') ? true : false,
type: 'slider',
brushSelect: false,
realtime: true,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
}
}],
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
position: (pos, params, el, elRect, size) => { position: (pos, params, el, elRect, size) => {
@ -45,25 +68,16 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80; obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return obj; return obj;
}, },
extraCssText: `background: transparent; extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.size === 'small') ? '105px' : '135px'};
background: transparent;
border: none; border: none;
box-shadow: none;`, box-shadow: none;`,
axisPointer: { axisPointer: {
type: 'cross', type: 'line',
label: {
formatter: (axis: any) => {
if (axis.axisDimension === 'y') {
return `${Math.floor(axis.value)}`;
}
if (axis.axisDimension === 'x') {
return axis.value;
}
},
}
}, },
formatter: (params: any) => { formatter: (params: any) => {
const colorSpan = (color: string) => `<div class="indicator" style="background-color: ` + color + `"></div>`; const colorSpan = (color: string) => `<div class="indicator" style="background-color: ` + color + `"></div>`;
let itemFormatted = '<div>' + params[0].axisValue + '</div>'; let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => { params.map((item: any, index: number) => {
if (index < 26) { if (index < 26) {
itemFormatted += `<div class="item"> itemFormatted += `<div class="item">
@ -73,15 +87,18 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
</div>`; </div>`;
} }
}); });
if (this.theme !== '') { return `<div class="tx-wrapper-tooltip-chart ${(this.size === 'big') ? 'tx-wrapper-tooltip-chart-big' : ''}">${itemFormatted}</div>`;
return `<div class="tx-wrapper-tooltip-chart ${this.theme}">${itemFormatted}</div>`;
}
return `<div class="tx-wrapper-tooltip-chart">${itemFormatted}</div>`;
} }
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
data: this.data.labels.map((value: any) => formatDate(value, 'HH:mm', this.locale)), data: this.data.labels.map((value: any) => {
if (['2h', '24h'].includes(this.windowPreference) || this.size === 'small') {
return formatDate(value, 'HH:mm', this.locale);
} else {
return formatDate(value, 'MM/dd - HH:mm', this.locale);
}
}),
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',

View File

@ -1 +1 @@
<div class="echarts" echarts [options]="mempoolVsizeFeesOptions"></div> <div echarts class="echarts" (chartInit)="onChartReady($event)" [options]="mempoolVsizeFeesOptions"></div>

View File

@ -7,13 +7,6 @@ import { StorageService } from 'src/app/services/storage.service';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
import { feeLevels, chartColors } from 'src/app/app.constants'; import { feeLevels, chartColors } from 'src/app/app.constants';
interface AxisObject {
axisDimension: string;
axisIndex: number;
seriesData: any;
value: string;
}
@Component({ @Component({
selector: 'app-mempool-graph', selector: 'app-mempool-graph',
templateUrl: './mempool-graph.component.html', templateUrl: './mempool-graph.component.html',
@ -26,18 +19,18 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() top: number | string = 20; @Input() top: number | string = 20;
@Input() right: number | string = 10; @Input() right: number | string = 10;
@Input() left: number | string = 75; @Input() left: number | string = 75;
@Input() dateSpan = '2h';
@Input() showLegend = true;
@Input() small = false; @Input() small = false;
@Input() size: ('small' | 'big') = 'small';
mempoolVsizeFeesData: any; mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: EChartsOption; mempoolVsizeFeesOptions: EChartsOption;
windowPreference: string;
inverted: boolean; hoverIndexSerie: -1;
constructor( constructor(
private vbytesPipe: VbytesPipe, private vbytesPipe: VbytesPipe,
private stateService: StateService, private stateService: StateService,
private storageService: StorageService,
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
) { } ) { }
@ -46,11 +39,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
} }
ngOnChanges() { ngOnChanges() {
// this.inverted = this.storageService.getValue('inverted-graph') === 'true'; this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([])); this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart(); this.mountFeeChart();
} }
onChartReady(myChart: any) {
myChart.on('mouseover', 'series', (serie: any) => {
this.hoverIndexSerie = serie.seriesIndex;
});
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) { handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
mempoolStats.reverse(); mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added); const labels = mempoolStats.map(stats => stats.added);
@ -81,9 +80,9 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
feesArray.push(0); feesArray.push(0);
} }
}); });
// if (this.inverted && finalArray.length) { if (finalArray.length) {
// feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]); feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
// } }
finalArray.push(feesArray); finalArray.push(feesArray);
} }
finalArray.reverse(); finalArray.reverse();
@ -93,45 +92,45 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
mountFeeChart(){ mountFeeChart(){
const { labels, series } = this.mempoolVsizeFeesData; const { labels, series } = this.mempoolVsizeFeesData;
const legendNames: string[] = feeLevels.map((sat, i, arr) => { const feeLevelsOrdered = feeLevels.map((sat, i, arr) => {
if (sat > this.limitFee) { return `${this.limitFee}+`; } if (i <= 26) {
if (i === 0) { return '0 - 1'; } if (i === 0) { return '0 - 1'; }
return arr[i - 1] + ' - ' + sat; if (i === 26) { return '350+'; }
return arr[i - 1] + ' - ' + sat;
}
}); });
const yAxisSeries = series.map((value: Array<number>, index: number) => { const yAxisSeries = series.map((value: Array<number>, index: number) => {
return { if (index <= 26){
name: labels[index].name, return {
type: 'line', name: feeLevelsOrdered[index],
stack: 'total', type: 'line',
smooth: false, stack: 'total',
lineStyle: { smooth: false,
width: 0, markPoint: {
opacity: 0, symbol: 'rect',
},
showSymbol: false,
areaStyle: {
opacity: 1,
color: chartColors[index],
},
emphasis: {
focus: 'series'
},
markLine: {
symbol: 'none',
itemStyle: {
borderWidth: 0,
borderColor: 'none',
color: '#fff',
}, },
lineStyle: { lineStyle: {
color: '#fff', width: 0,
opacity: 0.75, opacity: 0,
width: 2,
}, },
}, symbolSize: (this.size === 'big') ? 15 : 10,
data: this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true) showSymbol: false,
}; areaStyle: {
opacity: 1,
color: chartColors[index],
},
emphasis: {
focus: 'series',
},
itemStyle: {
borderWidth: 30,
color: chartColors[index],
borderColor: chartColors[index],
},
data: this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)
};
}
}); });
this.mempoolVsizeFeesOptions = { this.mempoolVsizeFeesOptions = {
@ -143,39 +142,57 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80; positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return positions; return positions;
}, },
extraCssText: `width: 150px; extraCssText: `width: ${(this.size === 'big') ? '200px' : '170px'};
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none;`, box-shadow: none;`,
axisPointer: { axisPointer: {
type: 'cross', type: 'line',
label: {
formatter: (axis: AxisObject) => {
if (axis.axisDimension === 'y') {
return `${this.vbytesPipe.transform(axis.value, 2, 'vB', 'MvB', true)}`;
}
if (axis.axisDimension === 'x') {
return axis.value;
}
},
}
}, },
formatter: (params: any) => { formatter: (params: any) => {
const colorSpan = (index: number) => `<div class="indicator" style="background-color: ` + chartColors[index] + `"></div>`; const colorSpan = (index: number) => `<div class="indicator" style="background-color: ` + chartColors[index] + `"></div>`;
const legendName = (index: number) => legendNames[index]; const legendName = (index: number) => feeLevelsOrdered[index];
let itemFormatted = '<div>' + params[0].axisValue + '</div>'; let itemFormatted = `<div class="title">${params[0].axisValue}</div>`;
let total = 0;
params.map((item: any, index: number) => { params.map((item: any, index: number) => {
if (feeLevels[index - 1] < this.limitFee) { total += item.value;
itemFormatted += `<div class="item"> if (index <= 26) {
${colorSpan(index - 1)} ${legendName(index)} let activeItemClass = '';
if (this.hoverIndexSerie === index){
activeItemClass = 'active';
}
itemFormatted += `<div class="item ${activeItemClass}">
${colorSpan(index)} ${legendName(index)}
<div class="grow"></div> <div class="grow"></div>
<div class="value">${this.vbytesPipe.transform(item.value, 2, 'vB', 'MvB', true)}</div> <div class="value">${this.vbytesPipe.transform(item.value, 2, 'vB', 'MvB', false)}</div>
</div>`; </div>`;
} }
}); });
return `<div class="fees-wrapper-tooltip-chart">${itemFormatted}</div>`; const totalDiv = `<div class="total-label">Total
<span class="total-value">${this.vbytesPipe.transform(total, 2, 'vB', 'MvB', true)}</span>
</div>`;
const bigClass = (this.size === 'big') ? 'fees-wrapper-tooltip-chart-big' : '';
return `<div class="fees-wrapper-tooltip-chart ${bigClass}">${itemFormatted} ${totalDiv}</div>`;
} }
}, },
dataZoom: [{
type: 'inside',
realtime: true,
}, {
show: (this.size === 'big') ? true : false,
type: 'slider',
brushSelect: false,
realtime: true,
selectedDataBackground: {
lineStyle: {
color: '#fff',
opacity: 0.45,
},
areaStyle: {
opacity: 0,
}
}
}],
grid: { grid: {
height: this.height, height: this.height,
right: this.right, right: this.right,
@ -186,11 +203,19 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
{ {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: labels.map((value: any) => formatDate(value, 'HH:mm', this.locale)), axisLine: { onZero: false },
data: labels.map((value: any) => {
if (['2h', '24h'].includes(this.windowPreference) || this.size === 'small') {
return formatDate(value, 'HH:mm', this.locale);
} else {
return formatDate(value, 'MM/dd - HH:mm', this.locale);
}
}),
} }
], ],
yAxis: { yAxis: {
type: 'value', type: 'value',
axisLine: { onZero: false },
axisLabel: { axisLabel: {
formatter: (value: number) => (`${this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)}`), formatter: (value: number) => (`${this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)}`),
}, },

View File

@ -36,25 +36,36 @@
<input ngbButton type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y"> 1Y <input ngbButton type="radio" [value]="'1y'" [routerLink]="['/graphs' | relativeUrl]" fragment="1y"> 1Y
</label> </label>
</div> </div>
<!-- <button (click)="invertGraph()" class="btn btn-primary btn-sm ml-2 d-none d-md-inline"><fa-icon [icon]="['fas', 'exchange-alt']" [rotate]="90" [fixedWidth]="true" i18n-title="statistics.component-invert.title" title="Invert"></fa-icon></button> -->
</form> </form>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="incoming-transactions-graph"> <div class="incoming-transactions-graph">
<app-mempool-graph dir="ltr" [limitFee]="1200" [height]="550" [left]="60" [data]="mempoolStats"></app-mempool-graph> <app-mempool-graph
dir="ltr"
[size]="'big'"
[limitFee]="1200"
[height]="500"
[left]="60"
[data]="mempoolStats"
></app-mempool-graph>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-12"> <div>
<div class="card mb-3" *ngIf="mempoolTransactionsWeightPerSecondData"> <div class="card mb-3" *ngIf="mempoolTransactionsWeightPerSecondData">
<div class="card-header"> <div class="card-header">
<i class="fa fa-area-chart"></i> <span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span> <i class="fa fa-area-chart"></i> <span i18n="statistics.transaction-vbytes-per-second">Transaction vBytes per second (vB/s)</span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="incoming-transactions-graph"> <div class="incoming-transactions-graph">
<app-incoming-transactions-graph [height]="500" [data]="mempoolTransactionsWeightPerSecondData"></app-incoming-transactions-graph> <app-incoming-transactions-graph
[height]="500"
[left]="60"
[size]="'big'"
[data]="mempoolTransactionsWeightPerSecondData"
></app-incoming-transactions-graph>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,8 +5,15 @@
</div> </div>
<div class="tv-container" *ngIf="mempoolStats.length"> <div class="tv-container" *ngIf="mempoolStats.length">
<div class="chart-holder" > <div class="chart-holder">
<app-mempool-graph dir="ltr" [data]="mempoolStats" [limitFee]="1200" [height]="600"></app-mempool-graph> <app-mempool-graph
dir="ltr"
[size]="'big'"
[limitFee]="1200"
[height]="500"
[left]="60"
[data]="mempoolStats"
></app-mempool-graph>
</div> </div>
<div class="blockchain-wrapper"> <div class="blockchain-wrapper">
<div class="position-container"> <div class="position-container">

View File

@ -255,7 +255,7 @@ html:lang(ru) .card-title {
font-size: 0.9rem; font-size: 0.9rem;
} }
/* MEMPOOL CHARTS */ /* MEMPOOL CHARTS - start */
.mempool-wrapper-tooltip-chart { .mempool-wrapper-tooltip-chart {
height: 250px; height: 250px;
@ -267,54 +267,116 @@ html:lang(ru) .card-title {
} }
.tx-wrapper-tooltip-chart, .fees-wrapper-tooltip-chart { .tx-wrapper-tooltip-chart, .fees-wrapper-tooltip-chart {
display: flex;
justify-content: space-between;
flex-direction: column;
background: rgba(#11131f, 0.85); background: rgba(#11131f, 0.85);
color: #fff;
padding: 10px 15px;
border-radius: 4px; border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.2); box-shadow: 1px 1px 10px rgba(0,0,0,0.2);
color: #b1b1b1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 15px;
text-align: left;
.title {
font-size: 12px;
font-weight: 700;
margin-bottom: 2px;
color: #fff;
}
.active {
color: yellow !important;
font-weight: 900;
.value {
.symbol {
color: yellow !important;
}
}
}
.item { .item {
text-align: left;
display: flex; display: flex;
.indicator { .indicator {
display: block;
margin-right: 5px; margin-right: 5px;
border-radius: 10px; border-radius: 0px;
margin-top: 5px; margin-top: 5px;
width: 9px; width: 9px;
height: 9px; height: 9px;
} }
.value { .value {
text-align: right; text-align: right;
span { .symbol {
color: #212121 !important; color: #7e7e7e;
font-size: 9px !important;
} }
} }
} }
} .total-label {
width: 100%;
.grow { margin-top: 0px;
flex-grow: 1; font-size: 10px;
text-align: left;
color: #fff;
span {
font-weight: 700;
float: right !important;
}
}
} }
.fees-wrapper-tooltip-chart { .fees-wrapper-tooltip-chart {
.item { .item {
font-size: 9px; font-size: 9px;
line-height: 1; line-height: 1;
margin: 0px;
} }
.indicator { .indicator {
margin-right: 5px !important; margin-right: 5px !important;
border-radius: 10px !important; border-radius: 1px !important;
margin-top: 0px !important; margin-top: 0px !important;
} }
} }
.fees-wrapper-tooltip-chart-big, .tx-wrapper-tooltip-chart-big {
background: rgba(#1d1f31, 0.85);
.title {
font-size: 15px;
margin-bottom: 5px;
}
.item {
font-size: 12px;
line-height: 1;
margin: 2px 0px;
.value {
.symbol {
font-size: 12px !important;
}
}
}
.total-label {
width: 100%;
margin-top: 5px;
font-size: 14px;
span {
font-weight: 700;
float: right !important;
}
}
}
.tx-wrapper-tooltip-chart-big {
.indicator {
margin: 0px !important;;
}
}
.fee-distribution-chart { .fee-distribution-chart {
height: 250px; height: 250px;
} }
/* MEMPOOL CHARTS - end */
.grow {
flex-grow: 1;
}
hr { hr {
border-top: 1px solid rgba(255, 255, 255, 0.1); border-top: 1px solid rgba(255, 255, 255, 0.1);
} }