From f46543b264ecc66e954f51ac143ca5ea429a9ec8 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 2 Jul 2022 16:46:57 +0200 Subject: [PATCH] Node graphs --- .../src/app/lightning/lightning.module.ts | 2 + .../node-statistics-chart.component.html | 8 + .../node-statistics-chart.component.scss | 129 ++++++++ .../node-statistics-chart.component.ts | 287 ++++++++++++++++++ .../app/lightning/node/node.component.html | 4 + .../src/app/lightning/node/node.component.ts | 7 - .../lightning-statistics-chart.component.ts | 11 +- .../src/api/explorer/nodes.api.ts | 2 +- 8 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.html create mode 100644 frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.scss create mode 100644 frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts 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) {