diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts
index 48fc1c696..284c252bc 100644
--- a/frontend/src/app/lightning/lightning.module.ts
+++ b/frontend/src/app/lightning/lightning.module.ts
@@ -14,12 +14,14 @@ import { LightningWrapperComponent } from './lightning-wrapper/lightning-wrapper
import { ChannelBoxComponent } from './channel/channel-box/channel-box.component';
import { ClosingTypeComponent } from './channel/closing-type/closing-type.component';
import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component';
+import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component';
import { NgxEchartsModule } from 'ngx-echarts';
@NgModule({
declarations: [
LightningDashboardComponent,
NodesListComponent,
NodeStatisticsComponent,
+ NodeStatisticsChartComponent,
NodeComponent,
ChannelsListComponent,
ChannelComponent,
diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html
new file mode 100644
index 000000000..c5cad52fa
--- /dev/null
+++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html
@@ -0,0 +1,8 @@
+
diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss
new file mode 100644
index 000000000..85e7c5e68
--- /dev/null
+++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss
@@ -0,0 +1,129 @@
+
+.main-title {
+ position: relative;
+ color: #ffffff91;
+ margin-top: -13px;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: 500;
+ text-align: center;
+ padding-bottom: 3px;
+}
+
+.full-container {
+ padding: 0px 15px;
+ width: 100%;
+ /* min-height: 500px; */
+ height: calc(100% - 150px);
+ @media (max-width: 992px) {
+ height: 100%;
+ padding-bottom: 100px;
+ };
+}
+/*
+.chart {
+ width: 100%;
+ height: 100%;
+ padding-bottom: 20px;
+ padding-right: 10px;
+ @media (max-width: 992px) {
+ padding-bottom: 25px;
+ }
+ @media (max-width: 829px) {
+ padding-bottom: 50px;
+ }
+ @media (max-width: 767px) {
+ padding-bottom: 25px;
+ }
+ @media (max-width: 629px) {
+ padding-bottom: 55px;
+ }
+ @media (max-width: 567px) {
+ padding-bottom: 55px;
+ }
+}
+*/
+.chart-widget {
+ width: 100%;
+ height: 100%;
+ max-height: 270px;
+}
+
+.formRadioGroup {
+ margin-top: 6px;
+ display: flex;
+ flex-direction: column;
+ @media (min-width: 991px) {
+ position: relative;
+ top: -65px;
+ }
+ @media (min-width: 830px) and (max-width: 991px) {
+ position: relative;
+ top: 0px;
+ }
+ @media (min-width: 830px) {
+ flex-direction: row;
+ float: right;
+ margin-top: 0px;
+ }
+ .btn-sm {
+ font-size: 9px;
+ @media (min-width: 830px) {
+ font-size: 14px;
+ }
+ }
+}
+
+.pool-distribution {
+ min-height: 56px;
+ display: block;
+ @media (min-width: 485px) {
+ display: flex;
+ flex-direction: row;
+ }
+ h5 {
+ margin-bottom: 10px;
+ }
+ .item {
+ width: 50%;
+ display: inline-block;
+ margin: 0px auto 20px;
+ &:nth-child(2) {
+ order: 2;
+ @media (min-width: 485px) {
+ order: 3;
+ }
+ }
+ &:nth-child(3) {
+ order: 3;
+ @media (min-width: 485px) {
+ order: 2;
+ display: block;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: block;
+ }
+ }
+ .card-title {
+ font-size: 1rem;
+ color: #4a68b9;
+ }
+ .card-text {
+ font-size: 18px;
+ span {
+ color: #ffffff66;
+ font-size: 12px;
+ }
+ }
+ }
+}
+
+.skeleton-loader {
+ width: 100%;
+ display: block;
+ max-width: 80px;
+ margin: 15px auto 3px;
+}
\ No newline at end of file
diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts
new file mode 100644
index 000000000..15997d3c3
--- /dev/null
+++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts
@@ -0,0 +1,287 @@
+import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
+import { EChartsOption } from 'echarts';
+import { Observable } from 'rxjs';
+import { switchMap, tap } from 'rxjs/operators';
+import { formatNumber } from '@angular/common';
+import { FormGroup } from '@angular/forms';
+import { StorageService } from 'src/app/services/storage.service';
+import { download } from 'src/app/shared/graphs.utils';
+import { LightningApiService } from '../lightning-api.service';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+
+@Component({
+ selector: 'app-node-statistics-chart',
+ templateUrl: './node-statistics-chart.component.html',
+ styleUrls: ['./node-statistics-chart.component.scss'],
+ styles: [`
+ .loadingGraphs {
+ position: absolute;
+ top: 50%;
+ left: calc(50% - 15px);
+ z-index: 100;
+ }
+ `],
+})
+export class NodeStatisticsChartComponent implements OnInit {
+ @Input() publicKey: string;
+ @Input() right: number | string = 65;
+ @Input() left: number | string = 55;
+ @Input() widget = false;
+
+ miningWindowPreference: string;
+ radioGroupForm: FormGroup;
+
+ chartOptions: EChartsOption = {};
+ chartInitOptions = {
+ renderer: 'svg',
+ };
+
+ @HostBinding('attr.dir') dir = 'ltr';
+
+ blockSizesWeightsObservable$: Observable;
+ isLoading = true;
+ formatNumber = formatNumber;
+ timespan = '';
+ chartInstance: any = undefined;
+
+ constructor(
+ @Inject(LOCALE_ID) public locale: string,
+ private lightningApiService: LightningApiService,
+ private storageService: StorageService,
+ private activatedRoute: ActivatedRoute,
+ ) {
+ }
+
+ ngOnInit(): void {
+
+ this.activatedRoute.paramMap
+ .pipe(
+ switchMap((params: ParamMap) => {
+ this.isLoading = true;
+ return this.lightningApiService.listNodeStats$(params.get('public_key'))
+ .pipe(
+ tap((data) => {
+ this.prepareChartOptions({
+ channels: data.map(val => [val.added * 1000, val.channels]),
+ capacity: data.map(val => [val.added * 1000, val.capacity]),
+ });
+ this.isLoading = false;
+ }),
+ );
+ }),
+ ).subscribe(() => {
+ });
+ }
+
+ prepareChartOptions(data) {
+ let title: object;
+ if (data.channels.length === 0) {
+ title = {
+ textStyle: {
+ color: 'grey',
+ fontSize: 15
+ },
+ text: `Loading`,
+ left: 'center',
+ top: 'center'
+ };
+ }
+
+ this.chartOptions = {
+ title: title,
+ animation: false,
+ color: [
+ '#FDD835',
+ '#D81B60',
+ ],
+ grid: {
+ top: 30,
+ bottom: 70,
+ right: this.right,
+ left: this.left,
+ },
+ tooltip: {
+ show: !this.isMobile(),
+ trigger: 'axis',
+ axisPointer: {
+ type: 'line'
+ },
+ backgroundColor: 'rgba(17, 19, 31, 1)',
+ borderRadius: 4,
+ shadowColor: 'rgba(0, 0, 0, 0.5)',
+ textStyle: {
+ color: '#b1b1b1',
+ align: 'left',
+ },
+ borderColor: '#000',
+ formatter: (ticks) => {
+ let sizeString = '';
+ let weightString = '';
+
+ for (const tick of ticks) {
+ if (tick.seriesIndex === 0) { // Channels
+ sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`;
+ } else if (tick.seriesIndex === 1) { // Capacity
+ weightString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100000000, this.locale, '1.0-0')} BTC`;
+ }
+ }
+
+ const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
+
+ const tooltip = `${date}
+ ${sizeString}
+ ${weightString}`;
+
+ return tooltip;
+ }
+ },
+ xAxis: data.channels.length === 0 ? undefined : {
+ type: 'time',
+ splitNumber: this.isMobile() ? 5 : 10,
+ axisLabel: {
+ hideOverlap: true,
+ }
+ },
+ legend: data.channels.length === 0 ? undefined : {
+ padding: 10,
+ data: [
+ {
+ name: 'Channels',
+ inactiveColor: 'rgb(110, 112, 121)',
+ textStyle: {
+ color: 'white',
+ },
+ icon: 'roundRect',
+ },
+ {
+ name: 'Capacity',
+ inactiveColor: 'rgb(110, 112, 121)',
+ textStyle: {
+ color: 'white',
+ },
+ icon: 'roundRect',
+ },
+ ],
+ selected: JSON.parse(this.storageService.getValue('sizes_ln_legend')) ?? {
+ 'Channels': true,
+ 'Capacity': true,
+ }
+ },
+ yAxis: data.channels.length === 0 ? undefined : [
+ {
+ min: (value) => {
+ return value.min * 0.9;
+ },
+ type: 'value',
+ axisLabel: {
+ color: 'rgb(110, 112, 121)',
+ formatter: (val) => {
+ return `${Math.round(val)}`;
+ }
+ },
+ splitLine: {
+ lineStyle: {
+ type: 'dotted',
+ color: '#ffffff66',
+ opacity: 0.25,
+ }
+ },
+ },
+ {
+ min: (value) => {
+ return value.min * 0.9;
+ },
+ type: 'value',
+ position: 'right',
+ axisLabel: {
+ color: 'rgb(110, 112, 121)',
+ formatter: (val) => {
+ return `${val / 100000000} BTC`;
+ }
+ },
+ splitLine: {
+ show: false,
+ }
+ }
+ ],
+ series: data.channels.length === 0 ? [] : [
+ {
+ zlevel: 1,
+ name: 'Channels',
+ showSymbol: false,
+ symbol: 'none',
+ data: data.channels,
+ type: 'line',
+ step: 'middle',
+ lineStyle: {
+ width: 2,
+ },
+ markLine: {
+ silent: true,
+ symbol: 'none',
+ lineStyle: {
+ type: 'solid',
+ color: '#ffffff66',
+ opacity: 1,
+ width: 1,
+ },
+ data: [{
+ yAxis: 1,
+ label: {
+ position: 'end',
+ show: true,
+ color: '#ffffff',
+ formatter: `1 MB`
+ }
+ }],
+ }
+ },
+ {
+ zlevel: 0,
+ yAxisIndex: 1,
+ name: 'Capacity',
+ showSymbol: false,
+ symbol: 'none',
+ stack: 'Total',
+ data: data.capacity,
+ areaStyle: {},
+ type: 'line',
+ step: 'middle',
+ }
+ ],
+ };
+ }
+
+ onChartInit(ec) {
+ if (this.chartInstance !== undefined) {
+ return;
+ }
+
+ this.chartInstance = ec;
+
+ this.chartInstance.on('legendselectchanged', (e) => {
+ this.storageService.setValue('sizes_ln_legend', JSON.stringify(e.selected));
+ });
+ }
+
+ isMobile() {
+ return (window.innerWidth <= 767.98);
+ }
+
+ onSaveChart() {
+ // @ts-ignore
+ const prevBottom = this.chartOptions.grid.bottom;
+ const now = new Date();
+ // @ts-ignore
+ this.chartOptions.grid.bottom = 40;
+ this.chartOptions.backgroundColor = '#11131f';
+ this.chartInstance.setOption(this.chartOptions);
+ download(this.chartInstance.getDataURL({
+ pixelRatio: 2,
+ }), `block-sizes-weights-${this.timespan}-${Math.round(now.getTime() / 1000)}.svg`);
+ // @ts-ignore
+ this.chartOptions.grid.bottom = prevBottom;
+ this.chartOptions.backgroundColor = 'none';
+ this.chartInstance.setOption(this.chartOptions);
+ }
+}
diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html
index 101250359..cf09c8868 100644
--- a/frontend/src/app/lightning/node/node.component.html
+++ b/frontend/src/app/lightning/node/node.component.html
@@ -86,6 +86,10 @@
+
+
+
+
diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts
index 3f2af52b9..1c6c5ee23 100644
--- a/frontend/src/app/lightning/node/node.component.ts
+++ b/frontend/src/app/lightning/node/node.component.ts
@@ -55,13 +55,6 @@ export class NodeComponent implements OnInit {
return node;
}),
);
-
- this.statistics$ = this.activatedRoute.paramMap
- .pipe(
- switchMap((params: ParamMap) => {
- return this.lightningApiService.listNodeStats$(params.get('public_key'));
- })
- );
}
changeSocket(index: number) {
diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts
index 3d5a81afd..0d7599ac4 100644
--- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts
+++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts
@@ -135,7 +135,7 @@ export class LightningStatisticsChartComponent implements OnInit {
for (const tick of ticks) {
if (tick.seriesIndex === 0) { // Nodes
- sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.2-2')}`;
+ sizeString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`;
} else if (tick.seriesIndex === 1) { // Capacity
weightString = `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1] / 100000000, this.locale, '1.0-0')} BTC`;
}
@@ -221,7 +221,7 @@ export class LightningStatisticsChartComponent implements OnInit {
],
series: data.nodes.length === 0 ? [] : [
{
- zlevel: 0,
+ zlevel: 1,
name: 'Nodes',
showSymbol: false,
symbol: 'none',
@@ -251,16 +251,15 @@ export class LightningStatisticsChartComponent implements OnInit {
}
},
{
- zlevel: 1,
+ zlevel: 0,
yAxisIndex: 1,
name: 'Capacity',
showSymbol: false,
symbol: 'none',
+ stack: 'Total',
data: data.capacity,
+ areaStyle: {},
type: 'line',
- lineStyle: {
- width: 2,
- }
}
],
};
diff --git a/lightning-backend/src/api/explorer/nodes.api.ts b/lightning-backend/src/api/explorer/nodes.api.ts
index 391056d0b..4c80c7f2d 100644
--- a/lightning-backend/src/api/explorer/nodes.api.ts
+++ b/lightning-backend/src/api/explorer/nodes.api.ts
@@ -15,7 +15,7 @@ class NodesApi {
public async $getNodeStats(public_key: string): Promise {
try {
- const query = `SELECT * FROM node_stats WHERE public_key = ? ORDER BY added DESC`;
+ const query = `SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels FROM node_stats WHERE public_key = ? ORDER BY added DESC`;
const [rows]: any = await DB.query(query, [public_key]);
return rows;
} catch (e) {