Add new component incoming-transactions-graph;

Refactor component mempool-graph;
Refactor component fee-distribution-graph;
Add incoming-transactions-graph to dashboard;
Add incoming-transactions-graph to statistics;
Add incoming-transactions-graph to television;
Add mempool-graph to dashboard;
Add mempool-graph to statistics;
Add mempool-graph to television;
Remove chartist.component;
This commit is contained in:
Miguel Medeiros 2021-08-21 01:46:28 -03:00
parent 1a98a14541
commit 9b956ff88d
No known key found for this signature in database
GPG Key ID: 819EDEE4673F3EBB
24 changed files with 578 additions and 1230 deletions

View File

@ -24,7 +24,6 @@
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@juggle/resize-observer": "^3.3.1",
"@mempool/chartist": "^0.11.4",
"@mempool/mempool.js": "^2.2.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "11.2.1",
@ -2260,14 +2259,6 @@
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
"integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
},
"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/@mempool/mempool.js": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@mempool/mempool.js/-/mempool.js-2.2.4.tgz",
@ -21966,11 +21957,6 @@
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz",
"integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw=="
},
"@mempool/chartist": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/@mempool/chartist/-/chartist-0.11.4.tgz",
"integrity": "sha512-wSemsw2NIWS7/SHxjDe9upSdUETxNRebY0ByaJzcONKUzJSUzMuSNmKEdD3kr/g02H++JvsXR2znLC6tYEAbPA=="
},
"@mempool/mempool.js": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@mempool/mempool.js/-/mempool.js-2.2.4.tgz",

View File

@ -68,7 +68,6 @@
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@juggle/resize-observer": "^3.3.1",
"@mempool/chartist": "^0.11.4",
"@mempool/mempool.js": "^2.2.4",
"@ng-bootstrap/ng-bootstrap": "^7.0.0",
"@nguniversal/express-engine": "11.2.1",

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

@ -27,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';
@ -79,10 +79,10 @@ import { SponsorComponent } from './components/sponsor/sponsor.component';
TimeSpanComponent,
AddressLabelsComponent,
MempoolBlocksComponent,
ChartistComponent,
FooterComponent,
MempoolBlockComponent,
FeeDistributionGraphComponent,
IncomingTransactionsGraphComponent,
MempoolGraphComponent,
AssetComponent,
AssetsComponent,

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 [options]="mempoolVsizeFeesOptions"></div>
</div>
<ng-template #loadingFees>

View File

@ -1,70 +1,80 @@
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;
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 [options]="mempoolStatsChartOption"></div>

View File

@ -0,0 +1,162 @@
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';
@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 = '50';
mempoolStatsChartOption: EChartsOption = {};
constructor(
@Inject(LOCALE_ID) private locale: string,
) { }
ngOnChanges(): void {
this.mountChart();
}
ngOnInit(): void {
this.mountChart();
}
mountChart(): void {
this.mempoolStatsChartOption = {
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
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: `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;
}
},
}
},
formatter: (params: any) => {
const colorSpan = (color: string) => `<div class="indicator" style="background-color: ` + color + `"></div>`;
let itemFormatted = '<div>' + params[0].axisValue + '</div>';
params.map((item: any, index: number) => {
if (index < 26) {
itemFormatted += `<div class="item">
${colorSpan(item.color)}
<div class="grow"></div>
<div class="value">${item.value}</div>
</div>`;
}
});
if (this.theme !== '') {
return `<div class="tx-wrapper-tooltip-chart ${this.theme}">${itemFormatted}</div>`;
}
return `<div class="tx-wrapper-tooltip-chart">${itemFormatted}</div>`;
}
},
xAxis: {
type: 'category',
data: this.data.labels.map((value: any) => formatDate(value, 'HH:mm', this.locale)),
},
yAxis: {
type: 'value',
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: [
{
data: this.data.series[0],
type: 'line',
smooth: 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 class="echarts" echarts [options]="mempoolVsizeFeesOptions"></div>

View File

@ -1,10 +1,18 @@
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';
interface AxisObject {
axisDimension: string;
axisIndex: number;
seriesData: any;
value: string;
}
@Component({
selector: 'app-mempool-graph',
@ -12,111 +20,50 @@ import { StorageService } from 'src/app/services/storage.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MempoolGraphComponent implements OnInit, OnChanges {
@Input() data;
@Input() data: any[];
@Input() limitFee = 300;
@Input() height: number | string = 200;
@Input() top: number | string = 20;
@Input() right: number | string = 10;
@Input() left: number | string = 75;
@Input() dateSpan = '2h';
@Input() showLegend = true;
@Input() offsetX = 40;
@Input() small = false;
mempoolVsizeFeesOptions: any;
mempoolVsizeFeesData: any;
mempoolVsizeFeesOptions: EChartsOption;
isMobile = window.innerWidth <= 767.98;
inverted: boolean;
constructor(
private vbytesPipe: VbytesPipe,
private stateService: StateService,
@Inject(LOCALE_ID) private locale: string,
private storageService: StorageService,
) { }
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);
}
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.inverted = this.storageService.getValue('inverted-graph') === 'true';
this.mempoolVsizeFeesData = this.handleNewMempoolData(this.data.concat([]));
this.mountFeeChart();
}
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
};
}
@ -134,12 +81,128 @@ 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 (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 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 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',
},
lineStyle: {
color: '#fff',
opacity: 0.75,
width: 2,
},
},
data: this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)
};
});
this.mempoolVsizeFeesOptions = {
color: chartColors,
tooltip: {
trigger: 'axis',
position: (pos, params, el, elRect, size) => {
const positions = { top: -20 };
positions[['left', 'right'][+(pos[0] < size.viewSize[0] / 2)]] = 80;
return positions;
},
extraCssText: `width: 150px;
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;
}
},
}
},
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>';
params.map((item: any, index: number) => {
if (feeLevels[index - 1] < this.limitFee) {
itemFormatted += `<div class="item">
${colorSpan(index - 1)} ${legendName(index)}
<div class="grow"></div>
<div class="value">${this.vbytesPipe.transform(item.value, 2, 'vB', 'MvB', true)}</div>
</div>`;
}
});
return `<div class="fees-wrapper-tooltip-chart">${itemFormatted}</div>`;
}
},
grid: {
height: this.height,
right: this.right,
top: this.top,
left: this.left,
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: labels.map((value: any) => formatDate(value, 'HH:mm', this.locale)),
}
],
yAxis: {
type: 'value',
axisLabel: {
formatter: (value: number) => (`${this.vbytesPipe.transform(value, 2, 'vB', 'MvB', true)}`),
},
splitLine: {
lineStyle: {
type: 'dotted',
color: '#ffffff66',
opacity: 0.25,
}
}
},
series: yAxisSeries
};
}
}

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,12 +36,12 @@
<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>
<!-- <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" [limitFee]="1200" [height]="550" [left]="60" [data]="mempoolStats"></app-mempool-graph>
</div>
</div>
</div>
@ -53,12 +53,8 @@
<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
[data]="mempoolTransactionsWeightPerSecondData"
[type]="'Line'"
[options]="transactionsWeightPerSecondOptions">
</app-chartist>
<div class="incoming-transactions-graph">
<app-incoming-transactions-graph [height]="500" [data]="mempoolTransactionsWeightPerSecondData"></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

@ -9,7 +9,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,8 +30,6 @@ export class StatisticsComponent implements OnInit {
mempoolUnconfirmedTransactionsData: any;
mempoolTransactionsWeightPerSecondData: any;
transactionsWeightPerSecondOptions: any;
radioGroupForm: FormGroup;
inverted: boolean;
graphWindowPreference: String;
@ -64,43 +61,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) => {

View File

@ -4,12 +4,10 @@
<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 dir="ltr" [data]="mempoolStats" [limitFee]="1200" [height]="600"></app-mempool-graph>
</div>
<div class="blockchain-wrapper">
<div class="position-container">
<app-mempool-blocks></app-mempool-blocks>

View File

@ -16,30 +16,21 @@
}
.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;
}
.blockchain-wrapper {
display: flex;
height: 100%;
min-height: 240px;
position: relative;
top: -20px;
@media(min-height: 800px) {
top: 10px;
top: 30px;
}
.position-container {
position: absolute;
left: 50%;
@ -89,4 +80,4 @@
display: flex;
margin-top: 0px;
flex-direction: column;
}
}

View File

@ -47,8 +47,8 @@
<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 [data]="mempoolStats.mempool"></app-mempool-graph>
</div>
</div>
</div>
@ -59,12 +59,8 @@
<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 [data]="mempoolStats.weightPerSecond"></app-incoming-transactions-graph>
</div>
</div>
</div>
@ -197,13 +193,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 +225,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 +254,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

@ -235,7 +235,7 @@ body {
color: #dc3545;
}
.yellow-color {
.yellow-color {
color: #ffd800;
}
@ -255,168 +255,64 @@ 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 */
@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;
.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);
.item {
text-align: left;
display: flex;
.indicator {
display: block;
margin-right: 5px;
border-radius: 10px;
margin-top: 5px;
width: 9px;
height: 9px;
}
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);
.value {
text-align: right;
span {
color: #212121 !important;
}
}
}
}
.grow {
flex-grow: 1;
}
.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; }
.fees-wrapper-tooltip-chart {
.item {
font-size: 9px;
line-height: 1;
}
.indicator {
margin-right: 5px !important;
border-radius: 10px !important;
margin-top: 0px !important;
}
}
.ct-area, .ct-line {
pointer-events: none; }
.ct-bar {
stroke-width: 1px;
.fee-distribution-chart {
height: 250px;
}
hr {
@ -639,7 +535,7 @@ th {
.card {
background-color: transparent;
padding: 0;
button {
text-align: left;
display: block;
@ -653,17 +549,17 @@ th {
box-shadow: none;
}
}
.card-header {
padding: 0;
}
.collapsed{
background-color: #2d3348;
color: #1bd8f4;
}
}
.subtitle {
font-weight: bold;
margin-bottom: 3px;
@ -675,7 +571,6 @@ th {
.pagination-container {
display: inline-block;
width: 100%;
justify-content: space-between;
@ -698,4 +593,4 @@ th {
.tooltip.show {
width: 220px;
}
}
}