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.visit('/tv');
cy.waitForSkeletonGone();
cy.get('.chart-holder');
cy.get('.blockchain-wrapper').should('be.visible');
cy.get('.blockchain-wrapper').should('not.visible');
});
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 { EChartsOption } from 'echarts';
import { OnChanges } from '@angular/core';
import { StorageService } from 'src/app/services/storage.service';
@Component({
selector: 'app-incoming-transactions-graph',
@ -15,14 +16,18 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
@Input() right: number | string = '10';
@Input() top: number | string = '20';
@Input() left: number | string = '50';
@Input() size: ('small' | 'big') = 'small';
mempoolStatsChartOption: EChartsOption = {};
windowPreference: string;
constructor(
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
) { }
ngOnChanges(): void {
this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mountChart();
}
@ -38,6 +43,24 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
top: this.top,
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: {
trigger: 'axis',
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;
return obj;
},
extraCssText: `background: transparent;
extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.size === 'small') ? '105px' : '135px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'cross',
label: {
formatter: (axis: any) => {
if (axis.axisDimension === 'y') {
return `${Math.floor(axis.value)}`;
}
if (axis.axisDimension === 'x') {
return axis.value;
}
},
}
type: 'line',
},
formatter: (params: any) => {
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) => {
if (index < 26) {
itemFormatted += `<div class="item">
@ -73,15 +87,18 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
</div>`;
}
});
if (this.theme !== '') {
return `<div class="tx-wrapper-tooltip-chart ${this.theme}">${itemFormatted}</div>`;
}
return `<div class="tx-wrapper-tooltip-chart">${itemFormatted}</div>`;
return `<div class="tx-wrapper-tooltip-chart ${(this.size === 'big') ? 'tx-wrapper-tooltip-chart-big' : ''}">${itemFormatted}</div>`;
}
},
xAxis: {
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: {
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 { feeLevels, chartColors } from 'src/app/app.constants';
interface AxisObject {
axisDimension: string;
axisIndex: number;
seriesData: any;
value: string;
}
@Component({
selector: 'app-mempool-graph',
templateUrl: './mempool-graph.component.html',
@ -26,18 +19,18 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@Input() left: number | string = 75;
@Input() dateSpan = '2h';
@Input() showLegend = true;
@Input() small = false;
@Input() size: ('small' | 'big') = 'small';
mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: EChartsOption;
inverted: boolean;
windowPreference: string;
hoverIndexSerie: -1;
constructor(
private vbytesPipe: VbytesPipe,
private stateService: StateService,
private storageService: StorageService,
@Inject(LOCALE_ID) private locale: string,
) { }
@ -46,11 +39,17 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
}
ngOnChanges() {
// this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart();
}
onChartReady(myChart: any) {
myChart.on('mouseover', 'series', (serie: any) => {
this.hoverIndexSerie = serie.seriesIndex;
});
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
mempoolStats.reverse();
const labels = mempoolStats.map(stats => stats.added);
@ -81,9 +80,9 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
feesArray.push(0);
}
});
// if (this.inverted && finalArray.length) {
// feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
// }
if (finalArray.length) {
feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
}
finalArray.push(feesArray);
}
finalArray.reverse();
@ -93,45 +92,45 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
mountFeeChart(){
const { labels, series } = this.mempoolVsizeFeesData;
const legendNames: string[] = feeLevels.map((sat, i, arr) => {
if (sat > this.limitFee) { return `${this.limitFee}+`; }
if (i === 0) { return '0 - 1'; }
return arr[i - 1] + ' - ' + sat;
const feeLevelsOrdered = feeLevels.map((sat, i, arr) => {
if (i <= 26) {
if (i === 0) { return '0 - 1'; }
if (i === 26) { return '350+'; }
return arr[i - 1] + ' - ' + sat;
}
});
const yAxisSeries = series.map((value: Array<number>, index: number) => {
return {
name: labels[index].name,
type: 'line',
stack: 'total',
smooth: false,
lineStyle: {
width: 0,
opacity: 0,
},
showSymbol: false,
areaStyle: {
opacity: 1,
color: chartColors[index],
},
emphasis: {
focus: 'series'
},
markLine: {
symbol: 'none',
itemStyle: {
borderWidth: 0,
borderColor: 'none',
color: '#fff',
if (index <= 26){
return {
name: feeLevelsOrdered[index],
type: 'line',
stack: 'total',
smooth: false,
markPoint: {
symbol: 'rect',
},
lineStyle: {
color: '#fff',
opacity: 0.75,
width: 2,
width: 0,
opacity: 0,
},
},
data: this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)
};
symbolSize: (this.size === 'big') ? 15 : 10,
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 = {
@ -143,39 +142,57 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return positions;
},
extraCssText: `width: 150px;
extraCssText: `width: ${(this.size === 'big') ? '200px' : '170px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'cross',
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;
}
},
}
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (index: number) => `<div class="indicator" style="background-color: ` + chartColors[index] + `"></div>`;
const legendName = (index: number) => legendNames[index];
let itemFormatted = '<div>' + params[0].axisValue + '</div>';
const legendName = (index: number) => feeLevelsOrdered[index];
let itemFormatted = `<div class="title">${params[0].axisValue}</div>`;
let total = 0;
params.map((item: any, index: number) => {
if (feeLevels[index - 1] < this.limitFee) {
itemFormatted += `<div class="item">
${colorSpan(index - 1)} ${legendName(index)}
total += item.value;
if (index <= 26) {
let activeItemClass = '';
if (this.hoverIndexSerie === index){
activeItemClass = 'active';
}
itemFormatted += `<div class="item ${activeItemClass}">
${colorSpan(index)} ${legendName(index)}
<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>`;
}
});
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: {
height: this.height,
right: this.right,
@ -186,11 +203,19 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
{
type: 'category',
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: {
type: 'value',
axisLine: { onZero: false },
axisLabel: {
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
</label>
</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>
</div>
<div class="card-body">
<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 class="col-lg-12">
<div>
<div class="card mb-3" *ngIf="mempoolTransactionsWeightPerSecondData">
<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>
</div>
<div class="card-body">
<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>

View File

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

View File

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