mirror of
https://github.com/mempool/mempool.git
synced 2025-03-01 09:10:02 +01:00
Merge pull request #738 from MiguelMedeiros/feature-echarts
Feature: New charts library.
This commit is contained in:
commit
47ae306a75
26 changed files with 1079 additions and 1279 deletions
|
@ -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', () => {
|
||||
|
|
101
frontend/package-lock.json
generated
101
frontend/package-lock.json
generated
|
@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption"></div>
|
|
@ -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'
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
@ -39,3 +36,10 @@ h1 {
|
|||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container{
|
||||
margin: 20px auto;
|
||||
@media (min-width: 768px) {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -57,3 +57,7 @@
|
|||
height: 80vh;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.incoming-transactions-graph {
|
||||
height: 600px;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -16,29 +16,17 @@
|
|||
}
|
||||
|
||||
.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;
|
||||
|
@ -46,10 +34,6 @@
|
|||
bottom: 170px;
|
||||
}
|
||||
|
||||
.chart-holder {
|
||||
height: calc(100% - 220px);
|
||||
}
|
||||
|
||||
#divider {
|
||||
width: 3px;
|
||||
height: 175px;
|
||||
|
@ -64,27 +48,7 @@
|
|||
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;
|
||||
|
|
|
@ -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}"> </div>
|
||||
<div class="progress-bar" role="progressbar" [ngStyle]="{'width': mempoolInfoData.value.progressWidth, 'background-color': mempoolInfoData.value.progressColor}"> </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">
|
||||
|
@ -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">
|
||||
|
|
|
@ -220,6 +220,11 @@
|
|||
.mempool-graph {
|
||||
height: 250px;
|
||||
}
|
||||
.loadingGraphs{
|
||||
height: 250px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.inc-tx-progress-bar {
|
||||
max-width: 250px;
|
||||
|
|
|
@ -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[]) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
@ -675,7 +811,6 @@ th {
|
|||
|
||||
|
||||
.pagination-container {
|
||||
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
|
Loading…
Add table
Reference in a new issue