Merge pull request #738 from MiguelMedeiros/feature-echarts

Feature: New charts library.
This commit is contained in:
wiz 2021-09-15 11:08:11 +09:00 committed by GitHub
commit 47ae306a75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1079 additions and 1279 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

@ -23,7 +23,7 @@
"@fortawesome/fontawesome-common-types": "^0.2.35",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@mempool/chartist": "^0.11.4",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "^2.2.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "11.2.1",
@ -32,9 +32,11 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"echarts": "^5.1.2",
"express": "^4.17.1",
"lightweight-charts": "^3.3.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "^7.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "^1.4.4",
"rxjs": "^6.6.7",
@ -2252,13 +2254,10 @@
"schema-utils": "^2.7.0"
}
},
"node_modules/@mempool/chartist": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/@mempool/chartist/-/chartist-0.11.4.tgz",
"integrity": "sha512-wSemsw2NIWS7/SHxjDe9upSdUETxNRebY0ByaJzcONKUzJSUzMuSNmKEdD3kr/g02H++JvsXR2znLC6tYEAbPA==",
"engines": {
"node": ">=4.6.0"
}
"node_modules/@juggle/resize-observer": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
"integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
},
"node_modules/@mempool/mempool.js": {
"version": "2.2.4",
@ -6931,6 +6930,20 @@
"safer-buffer": "^2.1.0"
}
},
"node_modules/echarts": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.1.2.tgz",
"integrity": "sha512-okUhO4sw22vwZp+rTPNjd/bvTdpug4K4sHNHyrV8NdAncIX9/AarlolFqtJCAYKGFYhUBNjIWu1EznFrSWTFxg==",
"dependencies": {
"tslib": "2.0.3",
"zrender": "5.1.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -11916,6 +11929,18 @@
"tslib": "^2.0.0"
}
},
"node_modules/ngx-echarts": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-7.0.1.tgz",
"integrity": "sha512-DDRwEssQRjjgPElEjF1emORoUEnF6OMZ20xnQBLueSHitM7XnIUErYVe9GMmm/jCtI+iPvJPEedyxMPl62nHLw==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@juggle/resize-observer": ">=3.3.1",
"echarts": ">=5.0.0"
}
},
"node_modules/ngx-infinite-scroll": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-10.0.1.tgz",
@ -19926,6 +19951,19 @@
"dependencies": {
"tslib": "^2.0.0"
}
},
"node_modules/zrender": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.1.1.tgz",
"integrity": "sha512-oeWlmUZPQdS9f5hK4pV21tHPqA3wgQ7CkKkw7l0CCBgWlJ/FP+lRgLFtUBW6yam4JX8y9CdHJo1o587VVrbcoQ==",
"dependencies": {
"tslib": "2.0.3"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
},
"dependencies": {
@ -21914,10 +21952,10 @@
"schema-utils": "^2.7.0"
}
},
"@mempool/chartist": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/@mempool/chartist/-/chartist-0.11.4.tgz",
"integrity": "sha512-wSemsw2NIWS7/SHxjDe9upSdUETxNRebY0ByaJzcONKUzJSUzMuSNmKEdD3kr/g02H++JvsXR2znLC6tYEAbPA=="
"@juggle/resize-observer": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
"integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
},
"@mempool/mempool.js": {
"version": "2.2.4",
@ -25975,6 +26013,22 @@
"safer-buffer": "^2.1.0"
}
},
"echarts": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.1.2.tgz",
"integrity": "sha512-okUhO4sw22vwZp+rTPNjd/bvTdpug4K4sHNHyrV8NdAncIX9/AarlolFqtJCAYKGFYhUBNjIWu1EznFrSWTFxg==",
"requires": {
"tslib": "2.0.3",
"zrender": "5.1.1"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -30136,6 +30190,14 @@
"tslib": "^2.0.0"
}
},
"ngx-echarts": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/ngx-echarts/-/ngx-echarts-7.0.1.tgz",
"integrity": "sha512-DDRwEssQRjjgPElEjF1emORoUEnF6OMZ20xnQBLueSHitM7XnIUErYVe9GMmm/jCtI+iPvJPEedyxMPl62nHLw==",
"requires": {
"tslib": "^2.0.0"
}
},
"ngx-infinite-scroll": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-10.0.1.tgz",
@ -36780,6 +36842,21 @@
"requires": {
"tslib": "^2.0.0"
}
},
"zrender": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.1.1.tgz",
"integrity": "sha512-oeWlmUZPQdS9f5hK4pV21tHPqA3wgQ7CkKkw7l0CCBgWlJ/FP+lRgLFtUBW6yam4JX8y9CdHJo1o587VVrbcoQ==",
"requires": {
"tslib": "2.0.3"
},
"dependencies": {
"tslib": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
}
}
}

View file

@ -67,7 +67,7 @@
"@fortawesome/fontawesome-common-types": "^0.2.35",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@mempool/chartist": "^0.11.4",
"@juggle/resize-observer": "^3.3.1",
"@mempool/mempool.js": "^2.2.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "11.2.1",
@ -76,9 +76,11 @@
"browserify": "^17.0.0",
"clipboard": "^2.0.4",
"domino": "^2.1.6",
"echarts": "^5.1.2",
"express": "^4.17.1",
"lightweight-charts": "^3.3.0",
"ngx-bootrap-multiselect": "^2.0.0",
"ngx-echarts": "^7.0.1",
"ngx-infinite-scroll": "^10.0.1",
"qrcode": "^1.4.4",
"rxjs": "^6.6.7",

View file

@ -31,6 +31,46 @@ export const mempoolFeeColors = [
'b9254b',
];
export const chartColors = [
"#D81B60",
"#8E24AA",
"#5E35B1",
"#3949AB",
"#1E88E5",
"#039BE5",
"#00ACC1",
"#00897B",
"#43A047",
"#7CB342",
"#C0CA33",
"#FDD835",
"#FFB300",
"#FB8C00",
"#F4511E",
"#6D4C41",
"#757575",
"#546E7A",
"#b71c1c",
"#880E4F",
"#4A148C",
"#311B92",
"#1A237E",
"#0D47A1",
"#01579B",
"#006064",
"#004D40",
"#1B5E20",
"#33691E",
"#827717",
"#F57F17",
"#FF6F00",
"#E65100",
"#BF360C",
"#3E2723",
"#212121",
"#263238",
];
export const feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];

View file

@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgxEchartsModule } from 'ngx-echarts';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
@ -26,13 +27,13 @@ import { LiquidMasterPageComponent } from './components/liquid-master-page/liqui
import { AboutComponent } from './components/about/about.component';
import { TelevisionComponent } from './components/television/television.component';
import { StatisticsComponent } from './components/statistics/statistics.component';
import { ChartistComponent } from './components/statistics/chartist.component';
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
import { BlockchainComponent } from './components/blockchain/blockchain.component';
import { FooterComponent } from './components/footer/footer.component';
import { AudioService } from './services/audio.service';
import { MempoolBlockComponent } from './components/mempool-block/mempool-block.component';
import { FeeDistributionGraphComponent } from './components/fee-distribution-graph/fee-distribution-graph.component';
import { IncomingTransactionsGraphComponent } from './components/incoming-transactions-graph/incoming-transactions-graph.component';
import { TimeSpanComponent } from './components/time-span/time-span.component';
import { SeoService } from './services/seo.service';
import { MempoolGraphComponent } from './components/mempool-graph/mempool-graph.component';
@ -78,10 +79,10 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
TimeSpanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
ChartistComponent,
FooterComponent,
MempoolBlockComponent,
FeeDistributionGraphComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
AssetComponent,
AssetsComponent,
@ -106,6 +107,9 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
NgbTypeaheadModule,
FontAwesomeModule,
SharedModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts')
})
],
providers: [
ElectrsApiService,

View file

@ -1,9 +1,5 @@
<div style="height: 225px;" *ngIf="mempoolVsizeFeesData; else loadingFees">
<app-chartist
[data]="mempoolVsizeFeesData"
[type]="'Line'"
[options]="mempoolVsizeFeesOptions">
</app-chartist>
<div class="fee-distribution-chart" *ngIf="mempoolVsizeFeesOptions; else loadingFees">
<div echarts [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
</div>
<ng-template #loadingFees>

View file

@ -1,70 +1,83 @@
import { Component, Input, OnChanges, ChangeDetectionStrategy } from '@angular/core';
import * as Chartist from '@mempool/chartist';
import { OnChanges } from '@angular/core';
import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-fee-distribution-graph',
templateUrl: './fee-distribution-graph.component.html',
styleUrls: ['./fee-distribution-graph.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FeeDistributionGraphComponent implements OnChanges {
@Input() feeRange;
export class FeeDistributionGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() height: number | string = 210;
@Input() top: number | string = 20;
@Input() right: number | string = 22;
@Input() left: number | string = 30;
mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesInitOptions = {
renderer: 'svg'
};
feeLevels = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400, 500];
constructor() { }
constructor(
) { }
ngOnChanges() {
this.mempoolVsizeFeesOptions = {
showArea: true,
showLine: true,
fullWidth: true,
showPoint: true,
low: 0,
axisY: {
showLabel: false,
offset: 0
},
axisX: {
showGrid: true,
showLabel: false,
offset: 0
},
plugins: [
Chartist.plugins.ctPointLabels({
textAnchor: 'middle',
labelInterpolationFnc: (value) => Math.round(value)
})
]
};
const fees = this.feeRange;
const series = [];
for (let i = 0; i < this.feeLevels.length; i++) {
let total = 0;
// for (let j = 0; j < fees.length; j++) {
for (const fee of fees) {
if (i === this.feeLevels.length - 1) {
if (fee >= this.feeLevels[i]) {
total += 1;
}
} else if (fee >= this.feeLevels[i] && fee < this.feeLevels[i + 1]) {
total += 1;
}
}
series.push(total);
}
this.mempoolVsizeFeesData = {
series: [fees],
labels: fees.map((d, i) => i)
};
ngOnInit() {
this.mountChart();
}
ngOnChanges() {
this.mountChart();
}
mountChart() {
this.mempoolVsizeFeesOptions = {
grid: {
height: '210',
right: '20',
top: '22',
left: '30',
},
xAxis: {
type: 'category',
boundaryGap: false,
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [{
data: this.data,
type: 'line',
label: {
show: true,
position: 'top',
color: '#ffffff',
textShadowBlur: 0,
formatter: (label: any) => {
return Math.floor(label.data);
},
},
smooth: true,
lineStyle: {
color: '#D81B60',
width: 4,
},
itemStyle: {
color: '#b71c1c',
borderWidth: 10,
borderMiterLimit: 10,
opacity: 1,
},
areaStyle: {
color: '#D81B60',
opacity: 1,
}
}]
};
}
}

View file

@ -0,0 +1 @@
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption"></div>

View file

@ -0,0 +1,180 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy } from '@angular/core';
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',
templateUrl: './incoming-transactions-graph.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IncomingTransactionsGraphComponent implements OnInit, OnChanges {
@Input() data: any;
@Input() theme: string;
@Input() height: number | string = '200';
@Input() right: number | string = '10';
@Input() top: number | string = '20';
@Input() left: number | string = '0';
@Input() template: ('widget' | 'advanced') = 'widget';
mempoolStatsChartOption: EChartsOption = {};
mempoolStatsChartInitOption = {
renderer: 'svg'
};
windowPreference: string;
constructor(
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
) { }
ngOnChanges(): void {
this.windowPreference = this.storageService.getValue('graphWindowPreference');
this.mountChart();
}
ngOnInit(): void {
this.mountChart();
}
mountChart(): void {
this.mempoolStatsChartOption = {
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
animation: false,
dataZoom: [{
type: 'inside',
realtime: true,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
show: (this.template === 'advanced') ? 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) => {
const obj = { top: -20 };
obj[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return obj;
},
extraCssText: `width: ${(['2h', '24h'].includes(this.windowPreference) || this.template === 'widget') ? '125px' : '135px'};
background: transparent;
border: none;
box-shadow: none;`,
axisPointer: {
type: 'line',
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<span class="indicator" style="background-color: ` + color + `"></span>`;
let itemFormatted = '<div class="title">' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => {
if (index < 26) {
itemFormatted += `<div class="item">
<div class="indicator-container">${colorSpan(item.color)}</div>
<div class="grow"></div>
<div class="value">${item.value} <span class="symbol">vB/s</span></div>
</div>`;
}
});
return `<div class="tx-wrapper-tooltip-chart ${(this.template === 'advanced') ? 'tx-wrapper-tooltip-chart-advanced' : ''}">${itemFormatted}</div>`;
}
},
xAxis: {
type: 'category',
data: this.data.labels.map((value: any) => `${formatDate(value, 'M/d', this.locale)}\n${formatDate(value, 'H:mm', this.locale)}`),
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [
{
data: this.data.series[0],
type: 'line',
smooth: (this.template === 'advanced') ? false : true,
showSymbol: false,
lineStyle: {
width: 3,
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: {
color: '#fff',
opacity: 0.75,
width: 2,
},
data: [{
yAxis: 1667,
label: {
show: false,
color: '#ffffff',
}
}],
}
},
],
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'
}
},
};
}
}

View file

@ -9,7 +9,7 @@
<div class="box">
<div class="row">
<div class="col-sm">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
@ -40,8 +40,8 @@
</tbody>
</table>
</div>
<div class="col-sm">
<app-fee-distribution-graph [feeRange]="mempoolBlock.feeRange"></app-fee-distribution-graph>
<div class="col-md chart-container">
<app-fee-distribution-graph [data]="mempoolBlock.feeRange" ></app-fee-distribution-graph>
</div>
</div>
</div>

View file

@ -13,11 +13,8 @@
.fiat {
font-size: 13px;
display: block;
@media (min-width: 992px) {
display: inline-block;
margin-left: 10px;
}
display: inline-block;
margin-left: 10px;
}
.table {
@ -38,4 +35,11 @@ h1 {
float: left;
margin-right: 10px;
}
}
}
.chart-container{
margin: 20px auto;
@media (min-width: 768px) {
margin: auto;
}
}

View file

@ -1,6 +1 @@
<app-chartist
*ngIf="mempoolVsizeFeesData"
[data]="mempoolVsizeFeesData"
[type]="'Line'"
[options]="mempoolVsizeFeesOptions">
</app-chartist>
<div echarts class="echarts" (chartInit)="onChartReady($event)" [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>

View file

@ -1,10 +1,11 @@
import { Component, OnInit, Input, Inject, LOCALE_ID, ChangeDetectionStrategy, OnChanges } from '@angular/core';
import { formatDate } from '@angular/common';
import { VbytesPipe } from 'src/app/shared/pipes/bytes-pipe/vbytes.pipe';
import * as Chartist from '@mempool/chartist';
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';
@Component({
selector: 'app-mempool-graph',
@ -12,134 +13,299 @@ import { StorageService } from 'src/app/services/storage.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data;
@Input() dateSpan = '2h';
@Input() showLegend = true;
@Input() offsetX = 40;
@Input() small = false;
@Input() data: any[];
@Input() limitFee = 350;
@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;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesData: any;
isMobile = window.innerWidth <= 767.98;
inverted: boolean;
mempoolVsizeFeesOptions: EChartsOption;
mempoolVsizeFeesInitOptions = {
renderer: 'svg',
};
windowPreference: string;
hoverIndexSerie = 0;
feeLimitIndex: number;
feeLevelsOrdered = [];
constructor(
private vbytesPipe: VbytesPipe,
private stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnInit(): void {
let labelHops = !this.showLegend ? 48 : 24;
if (this.small) {
labelHops = labelHops / 2;
}
if (this.isMobile) {
labelHops = 96;
}
const labelInterpolationFnc = (value: any, index: any) => {
switch (this.dateSpan) {
case '2h':
case '24h':
value = formatDate(value, 'HH:mm', this.locale);
break;
case '1w':
value = formatDate(value, 'dd/MM HH:mm', this.locale);
break;
case '1m':
case '3m':
case '6m':
case '1y':
value = formatDate(value, 'dd/MM', this.locale);
this.feeLevelsOrdered = feeLevels.map((sat, i, arr) => {
if (arr[i] === this.limitFee) { this.feeLimitIndex = i; }
if (arr[i] < this.limitFee) {
if (i === 0) { return '0 - 1'; }
return `${arr[i - 1]} - ${arr[i]}`;
} else {
return `${this.limitFee}+`;
}
return index % labelHops === 0 ? value : null;
};
this.mempoolVsizeFeesOptions = {
showArea: true,
showLine: false,
fullWidth: true,
showPoint: false,
stackedLine: !this.inverted,
low: 0,
axisX: {
labelInterpolationFnc: labelInterpolationFnc,
offset: this.offsetX,
},
axisY: {
labelInterpolationFnc: (value: number): any => this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true),
offset: this.showLegend ? 160 : 60,
},
plugins: this.inverted ? [Chartist.plugins.ctTargetLine({ value: this.stateService.blockVSize })] : []
};
if (this.showLegend) {
const legendNames: string[] = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
250, 300, 350, 400].map((sat, i, arr) => {
if (sat === 400) {
return '350+';
}
if (i === 0) {
return '0 - 1';
}
return arr[i - 1] + ' - ' + sat;
});
// Only Liquid has lower than 1 sat/vb transactions
if (this.stateService.network !== 'liquid') {
legendNames.shift();
}
this.mempoolVsizeFeesOptions.plugins.push(
Chartist.plugins.legend({ legendNames: legendNames })
);
}
});
this.mountFeeChart();
}
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);
const finalArrayVbyte = this.generateArray(mempoolStats);
const finalArrayVByte = this.generateArray(mempoolStats);
// Only Liquid has lower than 1 sat/vb transactions
if (this.stateService.network !== 'liquid') {
finalArrayVbyte.shift();
finalArrayVByte.shift();
}
return {
labels: labels,
series: finalArrayVbyte
series: finalArrayVByte
};
}
generateArray(mempoolStats: OptimizedMempoolStats[]) {
const finalArray: number[][] = [];
let feesArray: number[] = [];
for (let index = 37; index > -1; index--) {
feesArray = [];
mempoolStats.forEach((stats) => {
const theFee = stats.vsizes[index].toString();
if (theFee) {
feesArray.push(parseInt(theFee, 10));
} else {
feesArray.push(0);
}
feesArray.push(stats.vsizes[index] ? stats.vsizes[index] : 0);
});
if (this.inverted && finalArray.length) {
feesArray = feesArray.map((value, i) => value + finalArray[finalArray.length - 1][i]);
}
finalArray.push(feesArray);
}
finalArray.reverse();
return finalArray;
}
mountFeeChart() {
const { labels, series } = this.mempoolVsizeFeesData;
const seriesGraph = series.map((value: Array<number>, index: number) => {
if (index <= this.feeLimitIndex){
return {
name: this.feeLevelsOrdered[index],
type: 'line',
stack: 'total',
smooth: false,
markPoint: {
symbol: 'rect',
},
lineStyle: {
width: 0,
opacity: 0,
},
symbolSize: (this.template === 'advanced') ? 10 : 10,
showSymbol: false,
areaStyle: {
opacity: 1,
color: chartColors[index],
},
emphasis: {
focus: 'series',
areaStyle: {
opacity: 1,
},
itemStyle: {
opacity: 0.2,
},
},
itemStyle: {
opacity: 0,
},
data: this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)
};
}
});
this.mempoolVsizeFeesOptions = {
emphasis: {
areaStyle: {
opacity: 1,
}
},
color: chartColors,
tooltip: {
show: true,
trigger: 'axis',
alwaysShowContent: false,
position: (pos, params, el, elRect, size) => {
const positions = { top: (this.template === 'advanced') ? 30 : -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 legendName = (index: number) => this.feeLevelsOrdered[index];
const colorSpan = (index: number) => `<span class="indicator" style="background-color: ${chartColors[index]}"></span>
<span>
${legendName(index)}
</span>`;
const totals = (values: any) => {
let totalValueTemp = 0;
const totalValueArrayTemp = [];
const valuesInverted = values.slice(0).reverse();
for (const item of valuesInverted) {
totalValueTemp += item.value;
totalValueArrayTemp.push(totalValueTemp);
}
return {
totalValue: totalValueTemp,
totalValueArray: totalValueArrayTemp.reverse()
};
};
const { totalValue, totalValueArray } = totals(params);
const title = `<div class="title">
${params[0].axisValue}
<span class="total-value">
${this.vbytesPipe.transform(totalValue, 2, 'vB', 'MvB', false)}
</span>
</div>`;
const itemFormatted = [];
let totalParcial = 0;
let progressPercentageText = '';
params.map((item: any, index: number) => {
totalParcial += item.value;
let progressPercentage = 0;
let progressPercentageSum = 0;
if (index <= this.feeLimitIndex) {
progressPercentage = (item.value / totalValue) * 100;
progressPercentageSum = (totalValueArray[index] / totalValue) * 100;
let activeItemClass = '';
if (this.hoverIndexSerie === index) {
progressPercentageText = `<div class="total-parcial-active">
<span class="progress-percentage">
${progressPercentage.toFixed(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>
<span style="width: ${progressPercentage}%; background: ${chartColors[index]}"></span>
</span>
</div>
</div>`;
activeItemClass = 'active';
}
itemFormatted.push(`<tr class="item ${activeItemClass}">
<td class="indicator-container">
${colorSpan(index)}
</td>
<td class="value">
${this.vbytesPipe.transform(item.value, 2, 'vB', 'MvB', false)}
</td>
<td class="total-progress-sum">
<span>
${this.vbytesPipe.transform(totalValueArray[index], 2, 'vB', 'MvB', false)}
</span>
</td>
<td class="total-progress-sum-bar">
<div>
<span style="width: ${progressPercentageSum.toFixed(2)}%; background-color: ${chartColors[index]}"></span>
</div>
</td>
</tr>`);
}
});
const classActive = (this.template === 'advanced') ? 'fees-wrapper-tooltip-chart-advanced' : '';
return `<div class="fees-wrapper-tooltip-chart ${classActive}">
${title}
<table>
<thead>
<tr>
<th>Range</th>
<th>Size</th>
<th>Sum</th>
<th></th>
</tr>
</thead>
<tbody>
${itemFormatted.reverse().join('')}
</tbody>
</table>
<span class="total-value">
${progressPercentageText}
</span>
</div>`;
}
},
dataZoom: [{
type: 'inside',
realtime: true,
zoomOnMouseWheel: (this.template === 'advanced') ? true : false,
maxSpan: 100,
minSpan: 10,
}, {
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: [
{
type: 'category',
boundaryGap: false,
axisLine: { onZero: false },
data: labels.map((value: any) => `${formatDate(value, 'M/d', this.locale)}\n${formatDate(value, 'H:mm', this.locale)}`),
}
],
yAxis: {
type: 'value',
axisLine: { onZero: false },
axisLabel: {
formatter: (value: number) => (`${this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)}`),
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: seriesGraph
};
}
}

View file

@ -1,11 +0,0 @@
.ct-legend {
top: 130px;
display: flex;
flex-direction: column-reverse;
@media (min-width: 653px) {
top: 90px;
}
}
.ct-legend.inverted {
flex-direction: column !important;
}

View file

@ -1,740 +0,0 @@
import {
Component,
ElementRef,
Inject,
Input,
OnChanges,
OnDestroy,
OnInit,
PLATFORM_ID,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import * as Chartist from '@mempool/chartist';
/**
* Possible chart types
* @type {String}
*/
export type ChartType = 'Pie' | 'Bar' | 'Line';
export type ChartInterfaces =
| Chartist.IChartistPieChart
| Chartist.IChartistBarChart
| Chartist.IChartistLineChart;
export type ChartOptions =
| Chartist.IBarChartOptions
| Chartist.ILineChartOptions
| Chartist.IPieChartOptions;
export type ResponsiveOptionTuple = Chartist.IResponsiveOptionTuple<
ChartOptions
>;
export type ResponsiveOptions = ResponsiveOptionTuple[];
/**
* Represent a chart event.
* For possible values, check the Chartist docs.
*/
export interface ChartEvent {
[eventName: string]: (data: any) => void;
}
@Component({
selector: 'app-chartist',
template: '<ng-content></ng-content>',
styleUrls: ['./chartist.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ChartistComponent implements OnInit, OnChanges, OnDestroy {
@Input()
// @ts-ignore
public data: Promise<Chartist.IChartistData> | Chartist.IChartistData;
// @ts-ignore
@Input() public type: Promise<ChartType> | ChartType;
@Input()
// @ts-ignore
public options: Promise<Chartist.IChartOptions> | Chartist.IChartOptions;
@Input()
// @ts-ignore
public responsiveOptions: Promise<ResponsiveOptions> | ResponsiveOptions;
// @ts-ignore
@Input() public events: ChartEvent;
isBrowser: boolean = isPlatformBrowser(this.platformId);
// @ts-ignore
public chart: ChartInterfaces;
private element: HTMLElement;
constructor(
element: ElementRef,
@Inject(PLATFORM_ID) private platformId: any,
) {
this.element = element.nativeElement;
}
public ngOnInit(): Promise<ChartInterfaces> {
if (!this.isBrowser) {
return;
}
if (!this.type || !this.data) {
Promise.reject('Expected at least type and data.');
}
return this.renderChart().then((chart) => {
if (this.events !== undefined) {
this.bindEvents(chart);
}
return chart;
});
}
public ngOnChanges(changes: SimpleChanges): void {
if (!this.isBrowser) {
return;
}
this.update(changes);
}
public ngOnDestroy(): void {
if (this.chart) {
this.chart.detach();
}
}
public renderChart(): Promise<ChartInterfaces> {
const promises: any[] = [
this.type,
this.element,
this.data,
this.options,
this.responsiveOptions
];
return Promise.all(promises).then((values) => {
const [type, ...args]: any = values;
if (!(type in Chartist)) {
throw new Error(`${type} is not a valid chart type`);
}
this.chart = (Chartist as any)[type](...args);
return this.chart;
});
}
public update(changes: SimpleChanges): void {
if (!this.chart || 'type' in changes) {
this.renderChart();
} else {
if (changes.data) {
this.data = changes.data.currentValue;
}
if (changes.options) {
this.options = changes.options.currentValue;
}
(this.chart as any).update(this.data, this.options);
}
}
public bindEvents(chart: any): void {
for (const event of Object.keys(this.events)) {
chart.on(event, this.events[event]);
}
}
}
/**
* Chartist.js plugin to display a "target" or "goal" line across the chart.
* Only tested with bar charts. Works for horizontal and vertical bars.
*/
(function(window, document, Chartist) {
'use strict';
const defaultOptions = {
// The class name so you can style the text
className: 'ct-target-line',
// The axis to draw the line. y == vertical bars, x == horizontal
axis: 'y',
// What value the target line should be drawn at
value: null
};
Chartist.plugins = Chartist.plugins || {};
Chartist.plugins.ctTargetLine = function (options: any) {
options = Chartist.extend({}, defaultOptions, options);
return function ctTargetLine (chart: any) {
chart.on('created', function(context: any) {
const projectTarget = {
y: function (chartRect: any, bounds: any, value: any) {
const targetLineY = chartRect.y1 - (chartRect.height() / bounds.max * value);
return {
x1: chartRect.x1,
x2: chartRect.x2,
y1: targetLineY,
y2: targetLineY
};
},
x: function (chartRect: any, bounds: any, value: any) {
const targetLineX = chartRect.x1 + (chartRect.width() / bounds.max * value);
return {
x1: targetLineX,
x2: targetLineX,
y1: chartRect.y1,
y2: chartRect.y2
};
}
};
// @ts-ignore
const targetLine = projectTarget[options.axis](context.chartRect, context.bounds, options.value);
context.svg.elem('line', targetLine, options.className);
});
};
};
}(null, null, Chartist));
/**
* Chartist.js plugin to display a data label on top of the points in a line chart.
*
*/
/* global Chartist */
(function(window, document, Chartist) {
'use strict';
const defaultOptions = {
labelClass: 'ct-label',
labelOffset: {
x: 0,
y: -10
},
textAnchor: 'middle',
align: 'center',
labelInterpolationFnc: Chartist.noop
};
const labelPositionCalculation = {
point: function(data: any) {
return {
x: data.x,
y: data.y
};
},
bar: {
left: function(data: any) {
return {
x: data.x1,
y: data.y1
};
},
center: function(data: any) {
return {
x: data.x1 + (data.x2 - data.x1) / 2,
y: data.y1
};
},
right: function(data: any) {
return {
x: data.x2,
y: data.y1
};
}
}
};
Chartist.plugins = Chartist.plugins || {};
Chartist.plugins.ctPointLabels = function(options: any) {
options = Chartist.extend({}, defaultOptions, options);
function addLabel(position: any, data: any) {
// if x and y exist concat them otherwise output only the existing value
const value = data.value.x !== undefined && data.value.y ?
(data.value.x + ', ' + data.value.y) :
data.value.y || data.value.x;
data.group.elem('text', {
x: position.x + options.labelOffset.x,
y: position.y + options.labelOffset.y,
style: 'text-anchor: ' + options.textAnchor
}, options.labelClass).text(options.labelInterpolationFnc(value));
}
return function ctPointLabels(chart: any) {
if (chart instanceof Chartist.Line || chart instanceof Chartist.Bar) {
chart.on('draw', function(data: any) {
// @ts-ignore
const positonCalculator = labelPositionCalculation[data.type]
// @ts-ignore
&& labelPositionCalculation[data.type][options.align] || labelPositionCalculation[data.type];
if (positonCalculator) {
addLabel(positonCalculator(data), data);
}
});
}
};
};
}(null, null, Chartist));
const defaultOptions = {
className: '',
classNames: false,
removeAll: false,
legendNames: false,
clickable: true,
onClick: null,
position: 'top'
};
Chartist.plugins.legend = function (options: any) {
let cachedDOMPosition;
let cacheInactiveLegends: { [key:number]: boolean } = {};
// Catch invalid options
if (options && options.position) {
if (!(options.position === 'top' || options.position === 'bottom' || options.position instanceof HTMLElement)) {
throw Error('The position you entered is not a valid position');
}
if (options.position instanceof HTMLElement) {
// Detatch DOM element from options object, because Chartist.extend
// currently chokes on circular references present in HTMLElements
cachedDOMPosition = options.position;
delete options.position;
}
}
options = Chartist.extend({}, defaultOptions, options);
if (cachedDOMPosition) {
// Reattatch the DOM Element position if it was removed before
options.position = cachedDOMPosition;
}
return function legend(chart: any) {
var isSelfUpdate = false;
chart.on('created', function (data: any) {
const useLabels = chart instanceof Chartist.Pie && chart.data.labels && chart.data.labels.length;
const legendNames = getLegendNames(useLabels);
var dirtyChartData = (chart.data.series.length < legendNames.length);
if (isSelfUpdate || dirtyChartData)
return;
function removeLegendElement() {
const legendElement = chart.container.querySelector('.ct-legend');
if (legendElement) {
legendElement.parentNode.removeChild(legendElement);
}
}
// Set a unique className for each series so that when a series is removed,
// the other series still have the same color.
function setSeriesClassNames() {
chart.data.series = chart.data.series.map(function (series: any, seriesIndex: any) {
if (typeof series !== 'object') {
series = {
value: series
};
}
series.className = series.className || chart.options.classNames.series + '-' + Chartist.alphaNumerate(seriesIndex);
return series;
});
}
function createLegendElement() {
const legendElement = document.createElement('ul');
legendElement.className = 'ct-legend';
const inverted = localStorage.getItem('inverted-graph') === 'true';
if (inverted){
legendElement.classList.add('inverted');
}
if (chart instanceof Chartist.Pie) {
legendElement.classList.add('ct-legend-inside');
}
if (typeof options.className === 'string' && options.className.length > 0) {
legendElement.classList.add(options.className);
}
if (chart.options.width) {
legendElement.style.cssText = 'width: ' + chart.options.width + 'px;margin: 0 auto;';
}
return legendElement;
}
// Get the right array to use for generating the legend.
function getLegendNames(useLabels: any) {
return options.legendNames || (useLabels ? chart.data.labels : chart.data.series);
}
// Initialize the array that associates series with legends.
// -1 indicates that there is no legend associated with it.
function initSeriesMetadata(useLabels: any) {
const seriesMetadata = new Array(chart.data.series.length);
for (let i = 0; i < chart.data.series.length; i++) {
seriesMetadata[i] = {
data: chart.data.series[i],
label: useLabels ? chart.data.labels[i] : null,
legend: -1
};
}
return seriesMetadata;
}
function createNameElement(i: any, legendText: any, classNamesViable: any) {
const li = document.createElement('li');
li.classList.add('ct-series-' + i);
// Append specific class to a legend element, if viable classes are given
if (classNamesViable) {
li.classList.add(options.classNames[i]);
}
li.setAttribute('data-legend', i);
li.textContent = legendText;
return li;
}
// Append the legend element to the DOM
function appendLegendToDOM(legendElement: any) {
if (!(options.position instanceof HTMLElement)) {
switch (options.position) {
case 'top':
chart.container.insertBefore(legendElement, chart.container.childNodes[0]);
break;
case 'bottom':
chart.container.insertBefore(legendElement, null);
break;
}
} else {
// Appends the legend element as the last child of a given HTMLElement
options.position.insertBefore(legendElement, null);
}
}
function updateChart(newSeries: any, newLabels:any, useLabels: any) {
chart.data.series = newSeries;
if (useLabels) {
chart.data.labels = newLabels;
}
isSelfUpdate = true;
chart.update();
isSelfUpdate = false;
}
function addClickHandler(legendElement: any, legends: any, seriesMetadata: any, useLabels: any) {
legendElement.addEventListener('click', function(e: any) {
const li = e.target;
if (li.parentNode !== legendElement || !li.hasAttribute('data-legend'))
return;
e.preventDefault();
const legendIndex = parseInt(li.getAttribute('data-legend'));
const legend = legends[legendIndex];
const activateLegend = (_legendIndex: number): void => {
legends[_legendIndex].active = true;
legendElement.childNodes[_legendIndex].classList.remove('inactive');
cacheInactiveLegends[_legendIndex] = false;
}
const deactivateLegend = (_legendIndex: number): void => {
legends[_legendIndex].active = false;
legendElement.childNodes[_legendIndex].classList.add('inactive');
cacheInactiveLegends[_legendIndex] = true;
}
for (let i = legends.length - 1; i >= 0; i--) {
if (i >= legendIndex) {
if (!legend.active) activateLegend(i);
} else {
if (legend.active) deactivateLegend(i);
}
}
// Make sure all values are undefined (falsy) when clicking the first legend
// After clicking the first legend all indices should be falsy
if (legendIndex === 0) cacheInactiveLegends = {};
const newSeries = [];
const newLabels = [];
for (let i = 0; i < seriesMetadata.length; i++) {
if (seriesMetadata[i].legend !== -1 && legends[seriesMetadata[i].legend].active) {
newSeries.push(seriesMetadata[i].data);
newLabels.push(seriesMetadata[i].label);
}
}
updateChart(newSeries, newLabels, useLabels);
if (options.onClick) {
options.onClick(chart, e);
}
});
}
removeLegendElement();
const legendElement = createLegendElement();
const seriesMetadata = initSeriesMetadata(useLabels);
const legends: any = [];
// Check if given class names are viable to append to legends
const classNamesViable = Array.isArray(options.classNames) && options.classNames.length === legendNames.length;
var activeSeries = [];
var activeLabels = [];
// Loop through all legends to set each name in a list item.
legendNames.forEach(function (legend: any, i: any) {
const legendText = legend.name || legend;
const legendSeries = legend.series || [i];
const li = createNameElement(i, legendText, classNamesViable);
// If the value is undefined or false, isActive is true
const isActive: boolean = !cacheInactiveLegends[i];
if (isActive) {
activeSeries.push(seriesMetadata[i].data);
activeLabels.push(seriesMetadata[i].label);
} else {
li.classList.add('inactive');
}
legendElement.appendChild(li);
legendSeries.forEach(function(seriesIndex: any) {
seriesMetadata[seriesIndex].legend = i;
});
legends.push({
text: legendText,
series: legendSeries,
active: isActive
});
});
appendLegendToDOM(legendElement);
if (options.clickable) {
setSeriesClassNames();
addClickHandler(legendElement, legends, seriesMetadata, useLabels);
}
updateChart(activeSeries, activeLabels, useLabels);
});
};
};
Chartist.plugins.tooltip = function (options: any) {
options = Chartist.extend({}, defaultOptions, options);
return function tooltip(chart: any) {
let tooltipSelector = options.pointClass;
if (chart instanceof Chartist.Bar) {
tooltipSelector = 'ct-bar';
} else if (chart instanceof Chartist.Pie) {
// Added support for donut graph
if (chart.options.donut) {
tooltipSelector = 'ct-slice-donut';
} else {
tooltipSelector = 'ct-slice-pie';
}
}
const $chart = chart.container;
let $toolTip = $chart.querySelector('.chartist-tooltip');
if (!$toolTip) {
$toolTip = document.createElement('div');
$toolTip.className = (!options.class) ? 'chartist-tooltip' : 'chartist-tooltip ' + options.class;
if (!options.appendToBody) {
$chart.appendChild($toolTip);
} else {
document.body.appendChild($toolTip);
}
}
let height = $toolTip.offsetHeight;
let width = $toolTip.offsetWidth;
hide($toolTip);
function on(event: any, selector: any, callback: any) {
$chart.addEventListener(event, function (e: any) {
if (!selector || hasClass(e.target, selector)) {
callback(e);
}
});
}
on('mouseover', tooltipSelector, function (event: any) {
const $point = event.target;
let tooltipText = '';
const isPieChart = (chart instanceof Chartist.Pie) ? $point : $point.parentNode;
const seriesName = (isPieChart) ? $point.parentNode.getAttribute('ct:meta') || $point.parentNode.getAttribute('ct:series-name') : '';
let meta = $point.getAttribute('ct:meta') || seriesName || '';
const hasMeta = !!meta;
let value = $point.getAttribute('ct:value');
if (options.transformTooltipTextFnc && typeof options.transformTooltipTextFnc === 'function') {
value = options.transformTooltipTextFnc(value, $point.parentNode.getAttribute('class'));
}
if (options.tooltipFnc && typeof options.tooltipFnc === 'function') {
tooltipText = options.tooltipFnc(meta, value);
} else {
if (options.metaIsHTML) {
const txt = document.createElement('textarea');
txt.innerHTML = meta;
meta = txt.value;
}
meta = '<span class="chartist-tooltip-meta">' + meta + '</span>';
if (hasMeta) {
tooltipText += meta + '<br>';
} else {
// For Pie Charts also take the labels into account
// Could add support for more charts here as well!
if (chart instanceof Chartist.Pie) {
const label = next($point, 'ct-label');
if (label) {
tooltipText += text(label) + '<br>';
}
}
}
if (value) {
if (options.currency) {
if (options.currencyFormatCallback != undefined) {
value = options.currencyFormatCallback(value, options);
} else {
value = options.currency + value.replace(/(\d)(?=(\d{3})+(?:\.\d+)?$)/g, '$1,');
}
}
value = '<span class="chartist-tooltip-value">' + value + '</span>';
tooltipText += value;
}
}
if (tooltipText) {
$toolTip.innerHTML = tooltipText;
setPosition(event);
show($toolTip);
// Remember height and width to avoid wrong position in IE
height = $toolTip.offsetHeight;
width = $toolTip.offsetWidth;
}
});
on('mouseout', tooltipSelector, function () {
hide($toolTip);
});
on('mousemove', null, function (event: any) {
if (false === options.anchorToPoint) {
setPosition(event);
}
});
function setPosition(event: any) {
height = height || $toolTip.offsetHeight;
width = width || $toolTip.offsetWidth;
const offsetX = - width / 2 + options.tooltipOffset.x
const offsetY = - height + options.tooltipOffset.y;
let anchorX, anchorY;
if (!options.appendToBody) {
const box = $chart.getBoundingClientRect();
const left = event.pageX - box.left - window.pageXOffset ;
const top = event.pageY - box.top - window.pageYOffset ;
if (true === options.anchorToPoint && event.target.x2 && event.target.y2) {
anchorX = parseInt(event.target.x2.baseVal.value);
anchorY = parseInt(event.target.y2.baseVal.value);
}
$toolTip.style.top = (anchorY || top) + offsetY + 'px';
$toolTip.style.left = (anchorX || left) + offsetX + 'px';
} else {
$toolTip.style.top = event.pageY + offsetY + 'px';
$toolTip.style.left = event.pageX + offsetX + 'px';
}
}
}
};
Chartist.plugins.ctPointLabels = (options) => {
return function ctPointLabels(chart) {
const defaultOptions2 = {
labelClass: 'ct-point-label',
labelOffset: {
x: 0,
y: -7
},
textAnchor: 'middle'
};
options = Chartist.extend({}, defaultOptions2, options);
if (chart instanceof Chartist.Line) {
chart.on('draw', (data) => {
if (data.type === 'point') {
data.group.elem('text', {
x: data.x + options.labelOffset.x,
y: data.y + options.labelOffset.y,
style: 'text-anchor: ' + options.textAnchor
}, options.labelClass).text(options.labelInterpolationFnc(data.value.y)); // 07.11.17 added ".y"
}
});
}
};
};
function show(element: any) {
if (!hasClass(element, 'tooltip-show')) {
element.className = element.className + ' tooltip-show';
}
}
function hide(element: any) {
const regex = new RegExp('tooltip-show' + '\\s*', 'gi');
element.className = element.className.replace(regex, '').trim();
}
function hasClass(element: any, className: any) {
return (' ' + element.getAttribute('class') + ' ').indexOf(' ' + className + ' ') > -1;
}
function next(element: any, className: any) {
do {
element = element.nextSibling;
} while (element && !hasClass(element, className));
return element;
}
function text(element: any) {
return element.innerText || element.textContent;
}

View file

@ -36,29 +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 style="height: 600px;">
<app-mempool-graph dir="ltr" [data]="mempoolStats" [dateSpan]="radioGroupForm.controls.dateSpan.value"></app-mempool-graph>
<div class="incoming-transactions-graph">
<app-mempool-graph
dir="ltr"
[template]="'advanced'"
[limitFee]="500"
[height]="500"
[left]="65"
[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 style="height: 600px;">
<app-chartist
<div class="incoming-transactions-graph">
<app-incoming-transactions-graph
[height]="500"
[left]="65"
[template]="'advanced'"
[data]="mempoolTransactionsWeightPerSecondData"
[type]="'Line'"
[options]="transactionsWeightPerSecondOptions">
</app-chartist>
></app-incoming-transactions-graph>
</div>
</div>
</div>

View file

@ -56,4 +56,8 @@
text-align: center;
height: 80vh;
justify-content: center;
}
}
.incoming-transactions-graph {
height: 600px;
}

View file

@ -1,6 +1,5 @@
import { Component, OnInit, LOCALE_ID, Inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { formatDate } from '@angular/common';
import { FormGroup, FormBuilder } from '@angular/forms';
import { of, merge} from 'rxjs';
import { switchMap } from 'rxjs/operators';
@ -9,7 +8,6 @@ import { OptimizedMempoolStats } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service';
import { ApiService } from '../../services/api.service';
import * as Chartist from '@mempool/chartist';
import { StateService } from 'src/app/services/state.service';
import { SeoService } from 'src/app/services/seo.service';
import { StorageService } from 'src/app/services/storage.service';
@ -31,10 +29,7 @@ export class StatisticsComponent implements OnInit {
mempoolUnconfirmedTransactionsData: any;
mempoolTransactionsWeightPerSecondData: any;
transactionsWeightPerSecondOptions: any;
radioGroupForm: FormGroup;
inverted: boolean;
graphWindowPreference: String;
constructor(
@ -51,7 +46,6 @@ export class StatisticsComponent implements OnInit {
ngOnInit() {
this.seoService.setTitle($localize`:@@5d4f792f048fcaa6df5948575d7cb325c9393383:Graphs`);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.graphWindowPreference = this.storageService.getValue('graphWindowPreference') ? this.storageService.getValue('graphWindowPreference').trim() : '2h';
const isMobile = window.innerWidth <= 767.98;
let labelHops = isMobile ? 48 : 24;
@ -64,43 +58,6 @@ export class StatisticsComponent implements OnInit {
dateSpan: this.graphWindowPreference
});
const labelInterpolationFnc = (value: any, index: any) => {
switch (this.graphWindowPreference) {
case '2h':
case '24h':
value = formatDate(value, 'HH:mm', this.locale);
break;
case '1w':
value = formatDate(value, 'dd/MM HH:mm', this.locale);
break;
case '1m':
case '3m':
case '6m':
case '1y':
value = formatDate(value, 'dd/MM', this.locale);
}
return index % labelHops === 0 ? value : null;
};
this.transactionsWeightPerSecondOptions = {
showArea: false,
showLine: true,
showPoint: false,
low: 0,
axisY: {
offset: 40
},
axisX: {
labelInterpolationFnc: labelInterpolationFnc
},
plugins: [
Chartist.plugins.ctTargetLine({
value: 1667
}),
]
};
this.route
.fragment
.subscribe((fragment) => {
@ -164,11 +121,6 @@ export class StatisticsComponent implements OnInit {
};
}
invertGraph() {
this.storageService.setValue('inverted-graph', !this.inverted);
document.location.reload();
}
saveGraphPreference() {
this.storageService.setValue('graphWindowPreference', this.radioGroupForm.controls.dateSpan.value);
}

View file

@ -4,12 +4,17 @@
<div class="spinner-border text-light"></div>
</div>
<div class="tv-container">
<div class="chart-holder" *ngIf="mempoolStats.length">
<app-mempool-graph dir="ltr" [data]="mempoolStats"></app-mempool-graph>
<div class="tv-container" *ngIf="mempoolStats.length">
<div class="chart-holder">
<app-mempool-graph
[template]="'advanced'"
[limitFee]="500"
[height]="600"
[left]="60"
[data]="mempoolStats"
[showZoom]="false"
></app-mempool-graph>
</div>
<div class="blockchain-wrapper">
<div class="position-container">
<app-mempool-blocks></app-mempool-blocks>

View file

@ -16,40 +16,24 @@
}
.chart-holder {
height: calc(100vh - 270px);
min-height: 525px;
padding-left: 20px;
width: 98.5%;
padding-top: 20px;
@media(min-width: 992px){
padding-top: 10px;
}
@media(min-height: 800px){
padding-top: 60px !important;
}
height: 650px;
width: 100%;
margin: 30px auto 0;
}
.blockchain-wrapper {
display: flex;
display: block;
height: 100%;
min-height: 240px;
position: relative;
top: -20px;
@media(min-height: 800px) {
top: 10px;
}
top: 30px;
.position-container {
position: absolute;
left: 50%;
bottom: 170px;
}
.chart-holder {
height: calc(100% - 220px);
}
#divider {
width: 3px;
height: 175px;
@ -64,29 +48,9 @@
top: -28px;
}
}
@media (min-width: 1920px) {
.position-container {
transform: scale(1.3);
bottom: 210px;
}
.chart-holder {
height: calc(100% - 280px);
}
}
}
:host ::ng-deep .ct-legend {
top: 20px !important;
display: flex;
flex-direction: column-reverse;
@media(min-height: 800px){
padding-top: 40px !important;
}
}
.tv-container {
display: flex;
margin-top: 0px;
flex-direction: column;
}
}

View file

@ -47,8 +47,12 @@
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
<hr>
</div>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
<app-mempool-graph [data]="mempoolStats.mempool" [showLegend]="false" [offsetX]="20" [small]="true"></app-mempool-graph>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats; else loadingSpinner">
<app-mempool-graph
[template]="'widget'"
[limitFee]="150"
[data]="mempoolStats.mempool"
></app-mempool-graph>
</div>
</div>
</div>
@ -59,12 +63,11 @@
<ng-container *ngTemplateOutlet="txPerSecond; context: { $implicit: mempoolInfoData }"></ng-container>
<br>
<hr>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
<app-chartist
[data]="mempoolStats.weightPerSecond"
[type]="'Line'"
[options]="transactionsWeightPerSecondOptions">
</app-chartist>
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats; else loadingSpinner">
<app-incoming-transactions-graph
[left]="50"
[data]="mempoolStats.weightPerSecond"
></app-incoming-transactions-graph>
</div>
</div>
</div>
@ -197,13 +200,14 @@
</span>
<ng-template #inSync>
<div class="progress inc-tx-progress-bar">
<div class="progress-bar {{ mempoolInfoData.value.progressClass }}" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth}">&nbsp;</div>
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth, 'background-color': mempoolInfoData.value.progressColor}">&nbsp;</div>
<div class="progress-text">{{ mempoolInfoData.value.vBytesPerSecond | ceil | number }} <ng-container i18n="shared.vbytes-per-second|vB/s">vB/s</ng-container></div>
</div>
</ng-template>
</ng-template>
</ng-template>
<ng-template #difficultyEpoch>
<div class="main-title" i18n="dashboard.difficulty-adjustment">Difficulty Adjustment</div>
<div class="card-wrapper">
@ -228,11 +232,11 @@
<ng-template #arrowDownDifficulty >
<fa-icon class="retarget-sign" [icon]="['fas', 'caret-down']" [fixedWidth]="true"></fa-icon>
</ng-template>
{{ epochData.change | absolute | number: '1.2-2' }}
{{ epochData.change | absolute | number: '1.2-2' }}
<span class="symbol">%</span>
</div>
<div class="symbol">
<span i18n="difficulty-box.previous">Previous</span>:
<span i18n="difficulty-box.previous">Previous</span>:
<span [ngStyle]="{'color': epochData.colorPreviousAdjustments}">
<span *ngIf="epochData.previousRetarget > 0; else arrowDownPreviousDifficulty" >
<fa-icon class="previous-retarget-sign" [icon]="['fas', 'caret-up']" [fixedWidth]="true"></fa-icon>
@ -257,6 +261,12 @@
</ng-template>
<ng-template #loadingSpinner>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #loadingDifficulty>
<div class="difficulty-skeleton loading-container">
<div class="item">

View file

@ -58,11 +58,11 @@
display: block;
@media (min-width: 485px) {
display: flex;
flex-direction: row;
flex-direction: row;
}
h5 {
margin-bottom: 10px;
}
}
.item {
width: 50%;
margin: 0px auto 20px;
@ -131,7 +131,7 @@
.latest-transactions {
width: 100%;
text-align: left;
table-layout:fixed;
table-layout:fixed;
tr, td, th {
border: 0px;
}
@ -220,6 +220,11 @@
.mempool-graph {
height: 250px;
}
.loadingGraphs{
height: 250px;
display: grid;
place-items: center;
}
.inc-tx-progress-bar {
max-width: 250px;
@ -247,7 +252,7 @@
color: #ffffff66;
font-size: 12px;
}
.item {
.item {
padding: 0 5px;
width: 100%;
&:nth-child(1) {
@ -276,25 +281,25 @@
justify-content: space-between;
@media (min-width: 376px) {
flex-direction: row;
}
}
.item {
max-width: 150px;
margin: 0;
width: -webkit-fill-available;
@media (min-width: 376px) {
margin: 0 auto 0px;
}
}
&:first-child{
display: none;
@media (min-width: 485px) {
display: block;
}
}
@media (min-width: 768px) {
display: none;
}
}
@media (min-width: 992px) {
display: block;
}
}
}
&:last-child {
margin-bottom: 0;
@ -355,4 +360,4 @@
.previous-retarget-sign {
margin-right: -2px;
font-size: 10px;
}
}

View file

@ -6,11 +6,11 @@ import { OptimizedMempoolStats } from '../interfaces/node-api.interface';
import { MempoolInfo, TransactionStripped } from '../interfaces/websocket.interface';
import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service';
import * as Chartist from '@mempool/chartist';
import { formatDate } from '@angular/common';
import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service';
import { StorageService } from '../services/storage.service';
import { EChartsOption } from 'echarts';
interface MempoolBlocksData {
blocks: number;
@ -34,7 +34,7 @@ interface MempoolInfoData {
memPoolInfo: MempoolInfo;
vBytesPerSecond: number;
progressWidth: string;
progressClass: string;
progressColor: string;
}
interface MempoolStatsData {
@ -74,15 +74,15 @@ export class DashboardComponent implements OnInit {
) { }
ngOnInit(): void {
this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
this.seoService.resetTitle();
this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
this.network$ = merge(of(''), this.stateService.networkChanged$);
this.collapseLevel = this.storageService.getValue('dashboard-collapsed') || 'one';
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$.pipe(
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
);
this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
.pipe(
map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
);
this.mempoolInfoData$ = combineLatest([
this.stateService.mempoolInfo$,
@ -92,11 +92,21 @@ export class DashboardComponent implements OnInit {
map(([mempoolInfo, vbytesPerSecond]) => {
const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
let progressClass = 'bg-danger';
if (percent <= 75) {
progressClass = 'bg-success';
} else if (percent <= 99) {
progressClass = 'bg-warning';
let progressColor = '#7CB342';
if (vbytesPerSecond > 1667) {
progressColor = '#FDD835';
}
if (vbytesPerSecond > 2000) {
progressColor = '#FFB300';
}
if (vbytesPerSecond > 2500) {
progressColor = '#FB8C00';
}
if (vbytesPerSecond > 3000) {
progressColor = '#F4511E';
}
if (vbytesPerSecond > 3500) {
progressColor = '#D81B60';
}
const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
@ -111,7 +121,7 @@ export class DashboardComponent implements OnInit {
memPoolInfo: mempoolInfo,
vBytesPerSecond: vbytesPerSecond,
progressWidth: percent + '%',
progressClass: progressClass,
progressColor: progressColor,
mempoolSizeProgress: mempoolSizeProgress,
};
})
@ -164,7 +174,7 @@ export class DashboardComponent implements OnInit {
}
let colorPreviousAdjustments = '#dc3545';
if (previousRetarget){
if (previousRetarget) {
if (previousRetarget >= 0) {
colorPreviousAdjustments = '#3bcc49';
}
@ -191,7 +201,6 @@ export class DashboardComponent implements OnInit {
})
);
this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
.pipe(
map((mempoolBlocks) => {
@ -226,50 +235,32 @@ export class DashboardComponent implements OnInit {
}, []),
);
this.mempoolStats$ = this.stateService.connectionState$.pipe(
filter((state) => state === 2),
switchMap(() => this.apiService.list2HStatistics$()),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
.pipe(
scan((acc, stats) => {
acc.unshift(stats);
acc = acc.slice(0, 120);
return acc;
}, mempoolStats)
),
of(mempoolStats)
);
}),
map((mempoolStats) => {
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
}),
share(),
);
this.transactionsWeightPerSecondOptions = {
showArea: false,
showLine: true,
fullWidth: true,
showPoint: false,
low: 0,
axisY: {
offset: 40
},
axisX: {
labelInterpolationFnc: (value: any, index: any) => index % 24 === 0 ? formatDate(value, 'HH:mm', this.locale) : null,
offset: 20
},
plugins: [
Chartist.plugins.ctTargetLine({
value: 1667
}),
]
};
this.mempoolStats$ = this.stateService.connectionState$
.pipe(
filter((state) => state === 2),
switchMap(() => this.apiService.list2HStatistics$()),
switchMap((mempoolStats) => {
return merge(
this.stateService.live2Chart$
.pipe(
scan((acc, stats) => {
acc.unshift(stats);
acc = acc.slice(0, 120);
return acc;
}, mempoolStats)
),
of(mempoolStats)
);
}),
map((mempoolStats) => {
const data = this.handleNewMempoolData(mempoolStats.concat([]));
return {
mempool: mempoolStats,
weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
};
}),
share(),
);
}
handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {

View file

@ -6,7 +6,7 @@ export interface OptimizedMempoolStats {
vbytes_per_second: number;
total_fee: number;
mempool_byte_weight: number;
vsizes: number[] | string[];
vsizes: number[];
}
interface Ancestor {

View file

@ -235,7 +235,7 @@ body {
color: #dc3545;
}
.yellow-color {
.yellow-color {
color: #ffd800;
}
@ -255,168 +255,304 @@ html:lang(ru) .card-title {
font-size: 0.9rem;
}
/* Chartist */
$ct-series-names: (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z);
$ct-series-colors: (
#D81B60,
#8E24AA,
#5E35B1,
#3949AB,
#1E88E5,
#039BE5,
#00ACC1,
#00897B,
#43A047,
#7CB342,
#C0CA33,
#FDD835,
#FFB300,
#FB8C00,
#F4511E,
#6D4C41,
#757575,
#546E7A,
#b71c1c,
#880E4F,
#4A148C,
#311B92,
#1A237E,
#0D47A1,
#01579B,
#006064,
#004D40,
#1B5E20,
#33691E,
#827717,
#F57F17,
#FF6F00,
#E65100,
#BF360C,
#3E2723,
#212121,
#263238,
#a748ca,
#6188e2,
#a748ca,
#6188e2,
);
/* MEMPOOL CHARTS - start */
@import "../node_modules/@mempool/chartist/dist/scss/chartist.scss";
.ct-bar-label {
font-size: 20px;
font-weight: bold;
fill: #fff;
.mempool-wrapper-tooltip-chart {
height: 250px;
}
.ct-target-line {
stroke: #f5f5f5;
stroke-width: 3px;
stroke-dasharray: 7px;
.echarts {
height: 100%;
min-height: 180px;
}
.ct-area {
stroke: none;
fill-opacity: 0.9;
}
.ct-label {
fill: rgba(255, 255, 255, 0.4);
color: rgba(255, 255, 255, 0.4);
}
.ct-point-label {
fill: rgba(255, 255, 255, 1);
color: rgba(255, 255, 255, 1);
font-size: 14px;
}
.ct-grid {
stroke: rgba(255, 255, 255, 0.2);
}
/* LEGEND */
.ct-legend {
position: absolute;
z-index: 10;
left: 0px;
list-style: none;
font-size: 13px;
padding: 0px 0px 0px 30px;
top: 90px;
li {
position: relative;
padding-left: 23px;
margin-bottom: 0px;
}
li:before {
width: 12px;
height: 12px;
position: absolute;
left: 0;
bottom: 3px;
content: '';
border: 3px solid transparent;
border-radius: 2px;
}
li.inactive:before {
background: transparent;
}
&.ct-legend-inside {
position: absolute;
top: 0;
right: 0;
}
@for $i from 0 to length($ct-series-colors) {
.ct-series-#{$i}:before {
background-color: nth($ct-series-colors, $i + 1);
border-color: nth($ct-series-colors, $i + 1);
.tx-wrapper-tooltip-chart,
.fees-wrapper-tooltip-chart {
background: rgba(#11131f, 0.95);
border-radius: 4px;
box-shadow: 1px 1px 10px rgba(0,0,0,0.5);
color: #b1b1b1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 10px 15px;
text-align: left;
width: 200px;
thead {
th {
font-size: 9px;
color: #b1b1b1;
text-align: right;
&:first-child {
text-align: left;
left: -1px;
position: relative;
}
&:nth-child(4){
display: none;
}
}
}
.title {
font-size: 12px;
font-weight: 700;
margin-bottom: 2px;
color: #fff;
.total-value {
float: right;
}
}
.active {
color: yellow !important;
.value,
.total-partial {
color: yellow !important;
.symbol {
color: yellow !important;
}
}
}
.item {
line-height: 0.8;
.indicator-container {
.indicator {
display: inline-block;
margin-right: 5px;
border-radius: 2px;
margin-top: 5px;
width: 9px;
height: 9px;
}
}
.value {
text-align: right;
.symbol {
color: #7e7e7e;
font-size: 9px !important;
}
}
.symbol {
font-size: 9px;
}
.total-partial {
font-size: 10px;
width: 58px;
text-align: right;
}
.total-percentage-bar {
padding-left: 8px;
}
.total-progress-percentage {
width: 45px;
height: 5px;
text-align: right;
display: none;
}
.total-progress-sum {
width: 58px;
text-align: right;
}
}
.total-label {
width: 100%;
text-align: left;
color: #fff;
margin-top: 5px;
font-size: 14px;
span {
float: right;
}
.symbol {
margin-left: 3px;
font-size: 9px;
position: relative;
top: 2px;
}
}
thead {
th {
font-size: 9px;
color: #b1b1b1;
text-align: right;
&:first-child {
text-align: left;
left: -1px;
position: relative;
}
&:nth-child(4){
display: none;
}
&:nth-child(5){
display: none;
}
}
}
.total-percentage-bar {
margin: auto;
width: 35px;
position: relative;
span {
display: block;
background: #282d47;
height: 5px;
border-radius: 2px;
}
}
.total-parcial-active {
text-align: right;
margin: 5px auto 5px;
padding-left: 0px;
span {
font-size: 10px;
}
.symbol {
font-size: 9px;
}
.total-percentage-bar {
width: 100%;
span {
transition: 1000 all ease-in-out;
}
}
.progress-percentage {
float: left;
}
}
}
.tx-wrapper-tooltip-chart {
width: 115px;
.item {
display: flex;
}
.value {
margin-top: 5px;
}
.indicator-container {
border-radius: 2px;
}
}
.fee-distribution-chart {
height: 250px;
}
.fees-wrapper-tooltip-chart {
.item {
font-size: 9px;
line-height: 0.8;
margin: 0px;
}
.indicator {
margin-right: 5px !important;
border-radius: 1px !important;
margin-top: 0px !important;
}
}
.fees-wrapper-tooltip-chart-advanced,
.tx-wrapper-tooltip-chart-advanced {
background: rgba(#1d1f31, 0.98);
width: 250px;
thead {
th {
&:nth-child(4){
display: table-cell;
}
&:nth-child(5){
display: table-cell;
}
}
}
.title {
font-size: 15px;
margin-bottom: 5px;
}
.item {
line-height: 1;
.value {
width: 60px;
.symbol {
font-size: 10px !important;
}
}
.total-partial {
font-size: 10px;
width: 58px;
text-align: right;
}
.total-progress-percentage {
width: 65px;
height: 4px;
padding: 0px 5px;
display: table-cell !important;
border-radius: 4px;
}
.total-progress-sum {
width: 65px;
height: 4px;
padding: 0px 5px;
border-radius: 4px;
}
.total-progress-percentage-bar,
.total-progress-sum-bar {
width: 35px;
height: 4px;
div {
width: 100%;
border-radius: 4px;
display: block;
background: #29324c94;
}
span {
height: 4px;
border-radius: 4px;
display: block;
}
}
}
.total-label {
margin-top: 5px;
font-size: 14px;
span {
float: right;
}
}
.total-parcial-active {
text-align: right;
margin: 5px auto 5px;
span {
font-size: 10px;
}
.total-percentage-bar {
width: 100%;
left: 0;
span {
transition: 1000 all ease-in-out;
}
}
}
}
.tx-wrapper-tooltip-chart-advanced {
width: 115px;
.indicator-container {
.indicator {
margin-right: 5px;
border-radius: 0px;
margin-top: 5px;
width: 9px;
height: 9px;
}
}
}
.chartist-tooltip {
position: absolute;
display: inline-block;
opacity: 0;
min-width: 5em;
padding: .5em;
background: #F4C63D;
color: #453D3F;
font-family: Oxygen,Helvetica,Arial,sans-serif;
font-weight: 700;
text-align: center;
pointer-events: none;
z-index: 1;
-webkit-transition: opacity .2s linear;
-moz-transition: opacity .2s linear;
-o-transition: opacity .2s linear;
transition: opacity .2s linear; }
.chartist-tooltip:before {
content: "";
position: absolute;
top: 100%;
left: 50%;
width: 0;
height: 0;
margin-left: -15px;
border: 15px solid transparent;
border-top-color: #F4C63D; }
.chartist-tooltip.tooltip-show {
opacity: 1; }
/* MEMPOOL CHARTS - end */
.ct-area, .ct-line {
pointer-events: none; }
.ct-bar {
stroke-width: 1px;
.grow {
flex-grow: 1;
}
hr {
@ -639,7 +775,7 @@ th {
.card {
background-color: transparent;
padding: 0;
button {
text-align: left;
display: block;
@ -653,17 +789,17 @@ th {
box-shadow: none;
}
}
.card-header {
padding: 0;
}
.collapsed{
background-color: #2d3348;
color: #1bd8f4;
}
}
.subtitle {
font-weight: bold;
margin-bottom: 3px;
@ -675,7 +811,6 @@ th {
.pagination-container {
display: inline-block;
width: 100%;
justify-content: space-between;
@ -698,4 +833,4 @@ th {
.tooltip.show {
width: 220px;
}
}
}