diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 85a10b34b..9aa96a25e 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 25; + private static currentVersion = 26; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -257,6 +257,14 @@ class DatabaseMigration { await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats')); } + if (databaseSchemaVersion < 26 && isBitcoin === true) { + this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated. Will re-generate historical data from scratch.`); + await this.$executeQuery(`TRUNCATE lightning_stats`); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD unannounced_nodes int(11) NOT NULL DEFAULT "0"'); + } + } catch (e) { throw e; } diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index 5dd4609b5..3a9451b4c 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -4,7 +4,7 @@ import DB from '../../database'; class StatisticsApi { public async $getStatistics(): Promise { try { - const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM lightning_stats ORDER BY id DESC`; + const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity, tor_nodes, clearnet_nodes, unannounced_nodes FROM lightning_stats ORDER BY id DESC`; const [rows]: any = await DB.query(query); return rows; } catch (e) { diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 0047eae77..85c705936 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -1,6 +1,8 @@ -import logger from "../../logger"; -import DB from "../../database"; -import lightningApi from "../../api/lightning/lightning-api-factory"; + +import DB from '../../database'; +import logger from '../../logger'; +import lightningApi from '../../api/lightning/lightning-api-factory'; +import * as net from 'net'; class LightningStatsUpdater { constructor() {} @@ -115,37 +117,67 @@ class LightningStatsUpdater { added, channel_count, node_count, - total_capacity + total_capacity, + tor_nodes, + clearnet_nodes, + unannounced_nodes ) - VALUES (FROM_UNIXTIME(?), ?, ?, ?)`; + VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?)`; await DB.query(query, [ date.getTime() / 1000, channelsCount, 0, totalCapacity, + 0, + 0, + 0 ]); // Add one day and continue date.setDate(date.getDate() + 1); } - const [nodes]: any = await DB.query(`SELECT first_seen FROM nodes ORDER BY first_seen ASC`); + const [nodes]: any = await DB.query(`SELECT first_seen, sockets FROM nodes ORDER BY first_seen ASC`); date = new Date(startTime); while (date < currentDate) { let nodeCount = 0; + let clearnetNodes = 0; + let torNodes = 0; + let unannouncedNodes = 0; for (const node of nodes) { if (new Date(node.first_seen) > date) { break; } nodeCount++; + + const sockets = node.sockets.split(','); + let isUnnanounced = true; + for (const socket of sockets) { + const hasOnion = socket.indexOf('.onion') !== -1; + if (hasOnion) { + torNodes++; + isUnnanounced = false; + } + const hasClearnet = [4, 6].includes(net.isIP(socket.split(':')[0])); + if (hasClearnet) { + clearnetNodes++; + isUnnanounced = false; + } + } + if (isUnnanounced) { + unannouncedNodes++; + } } - const query = `UPDATE lightning_stats SET node_count = ? WHERE added = FROM_UNIXTIME(?)`; + const query = `UPDATE lightning_stats SET node_count = ?, tor_nodes = ?, clearnet_nodes = ?, unannounced_nodes = ? WHERE added = FROM_UNIXTIME(?)`; await DB.query(query, [ nodeCount, + torNodes, + clearnetNodes, + unannouncedNodes, date.getTime() / 1000, ]); @@ -178,18 +210,46 @@ class LightningStatsUpdater { } } + let clearnetNodes = 0; + let torNodes = 0; + let unannouncedNodes = 0; + for (const node of networkGraph.nodes) { + let isUnnanounced = true; + for (const socket of node.sockets) { + const hasOnion = socket.indexOf('.onion') !== -1; + if (hasOnion) { + torNodes++; + isUnnanounced = false; + } + const hasClearnet = [4, 6].includes(net.isIP(socket.split(':')[0])); + if (hasClearnet) { + clearnetNodes++; + isUnnanounced = false; + } + } + if (isUnnanounced) { + unannouncedNodes++; + } + } + const query = `INSERT INTO lightning_stats( added, channel_count, node_count, - total_capacity + total_capacity, + tor_nodes, + clearnet_nodes, + unannounced_nodes ) - VALUES (NOW(), ?, ?, ?)`; + VALUES (NOW(), ?, ?, ?, ?, ?, ?)`; await DB.query(query, [ networkGraph.channels.length, networkGraph.nodes.length, total_capacity, + torNodes, + clearnetNodes, + unannouncedNodes ]); logger.info(`Lightning daily stats done.`); } catch (e) { diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index 23c2c80ae..eb840efe6 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -33,8 +33,10 @@
-
- +
+
+ +
diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index fe04f507c..0549fe45c 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -16,6 +16,7 @@ import { ClosingTypeComponent } from './channel/closing-type/closing-type.compon import { LightningStatisticsChartComponent } from './statistics-chart/lightning-statistics-chart.component'; import { NodeStatisticsChartComponent } from './node-statistics-chart/node-statistics-chart.component'; import { GraphsModule } from '../graphs/graphs.module'; +import { NodesNetworksChartComponent } from './nodes-networks-chart/nodes-networks-chart.component'; @NgModule({ declarations: [ LightningDashboardComponent, @@ -29,6 +30,7 @@ import { GraphsModule } from '../graphs/graphs.module'; ChannelBoxComponent, ClosingTypeComponent, LightningStatisticsChartComponent, + NodesNetworksChartComponent, ], imports: [ CommonModule, diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.html b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.html new file mode 100644 index 000000000..30c3bf66e --- /dev/null +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.html @@ -0,0 +1,50 @@ +
+ +
+ Nodes count by network + + +
+
+ + + + + + + + + + +
+
+
+ +
+
+
+
+ +
diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss new file mode 100644 index 000000000..fa044a4d6 --- /dev/null +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.scss @@ -0,0 +1,135 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } +} + +.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/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts new file mode 100644 index 000000000..faba3f104 --- /dev/null +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -0,0 +1,364 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; +import { EChartsOption} from 'echarts'; +import { Observable } from 'rxjs'; +import { startWith, switchMap, tap } from 'rxjs/operators'; +import { formatNumber } from '@angular/common'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { StorageService } from 'src/app/services/storage.service'; +import { MiningService } from 'src/app/services/mining.service'; +import { download, formatterXAxis } from 'src/app/shared/graphs.utils'; +import { SeoService } from 'src/app/services/seo.service'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-nodes-networks-chart', + templateUrl: './nodes-networks-chart.component.html', + styleUrls: ['./nodes-networks-chart.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodesNetworksChartComponent implements OnInit { + @Input() right: number | string = 45; + @Input() left: number | string = 55; + @Input() widget = false; + + miningWindowPreference: string; + radioGroupForm: FormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + @HostBinding('attr.dir') dir = 'ltr'; + + nodesNetworkObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + timespan = ''; + chartInstance: any = undefined; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private lightningApiService: LightningApiService, + private formBuilder: FormBuilder, + private storageService: StorageService, + private miningService: MiningService + ) { + } + + ngOnInit(): void { + let firstRun = true; + + if (this.widget) { + this.miningWindowPreference = '1y'; + } else { + this.seoService.setTitle($localize`Nodes per network`); + this.miningWindowPreference = this.miningService.getDefaultTimespan('1m'); + } + this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference }); + this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference); + + this.nodesNetworkObservable$ = this.radioGroupForm.get('dateSpan').valueChanges + .pipe( + startWith(this.miningWindowPreference), + switchMap((timespan) => { + this.timespan = timespan; + if (!this.widget && !firstRun) { + this.storageService.setValue('miningWindowPreference', timespan); + } + firstRun = false; + this.miningWindowPreference = timespan; + this.isLoading = true; + return this.lightningApiService.listStatistics$() + .pipe( + tap((data) => { + this.prepareChartOptions({ + node_count: data.map(val => [val.added * 1000, val.node_count]), + tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]), + clearnet_nodes: data.map(val => [val.added * 1000, val.clearnet_nodes]), + unannounced_nodes: data.map(val => [val.added * 1000, val.unannounced_nodes]), + }); + this.isLoading = false; + }), + ); + }), + ) + } + + prepareChartOptions(data) { + let title: object; + if (data.node_count.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + animation: false, + color: [ + '#D81B60', + '#039BE5', + '#7CB342', + '#FFB300', + ], + grid: { + top: 40, + bottom: this.widget ? 30 : 70, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile() || !this.widget, + 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) => { + const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); + let tooltip = `${date}
`; + + for (const tick of ticks) { + if (tick.seriesIndex === 0) { // Total + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } else if (tick.seriesIndex === 1) { // Tor + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } else if (tick.seriesIndex === 2) { // Clearnet + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } else if (tick.seriesIndex === 3) { // Unannounced + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } + tooltip += `
`; + } + + return tooltip; + } + }, + xAxis: data.node_count.length === 0 ? undefined : { + type: 'time', + splitNumber: (this.isMobile() || this.widget) ? 5 : 10, + axisLabel: { + hideOverlap: true, + } + }, + legend: data.node_count.length === 0 ? undefined : { + padding: 10, + data: [ + { + name: $localize`Total`, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: $localize`Tor`, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: $localize`Clearnet`, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: $localize`Unannounced`, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + ], + selected: JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { + 'Total': true, + 'Tor': true, + 'Clearnet': true, + 'Unannounced': true, + } + }, + yAxis: data.node_count.length === 0 ? undefined : [ + { + type: 'value', + position: 'left', + min: (value) => { + return value.min * 0.9; + }, + axisLabel: { + color: 'rgb(110, 112, 121)', + formatter: (val) => { + return `${formatNumber(Math.round(val * 100) / 100, this.locale, '1.0-0')}`; + } + }, + splitLine: { + lineStyle: { + type: 'dotted', + color: '#ffffff66', + opacity: 0.25, + } + }, + } + ], + series: data.node_count.length === 0 ? [] : [ + { + zlevel: 1, + name: $localize`Total`, + showSymbol: false, + symbol: 'none', + data: data.node_count, + type: 'line', + lineStyle: { + width: 2, + }, + markLine: { + silent: true, + symbol: 'none', + lineStyle: { + type: 'solid', + color: '#ffffff66', + opacity: 1, + width: 1, + }, + }, + areaStyle: { + opacity: 0.25, + }, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Tor`, + showSymbol: false, + symbol: 'none', + data: data.tor_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.25, + }, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Clearnet`, + showSymbol: false, + symbol: 'none', + data: data.clearnet_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.25, + }, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Unannounced`, + showSymbol: false, + symbol: 'none', + data: data.unannounced_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.25, + }, + } + ], + dataZoom: this.widget ? null : [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 5, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + areaStyle: { + opacity: 0, + } + }, + }], + }; + } + + onChartInit(ec) { + if (this.chartInstance !== undefined) { + return; + } + + this.chartInstance = ec; + + this.chartInstance.on('legendselectchanged', (e) => { + this.storageService.setValue('nodes_networks_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, + excludeComponents: ['dataZoom'], + }), `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/statistics-chart/lightning-statistics-chart.component.html b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html index 252947352..566bf918e 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.html @@ -12,21 +12,4 @@
- - - -
-
-
Hashrate
-

- -

-
-
-
Difficulty
-

- -

-
-
-
+ \ No newline at end of file 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 345cdd604..2cf94c8ef 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 @@ -24,7 +24,7 @@ import { LightningApiService } from '../lightning-api.service'; `], }) export class LightningStatisticsChartComponent implements OnInit { - @Input() right: number | string = 65; + @Input() right: number | string = 45; @Input() left: number | string = 55; @Input() widget = false; @@ -76,7 +76,7 @@ export class LightningStatisticsChartComponent implements OnInit { .pipe( tap((data) => { this.prepareChartOptions({ - nodes: data.map(val => [val.added * 1000, val.node_count]), + channel_count: data.map(val => [val.added * 1000, val.channel_count]), capacity: data.map(val => [val.added * 1000, val.total_capacity]), }); this.isLoading = false; @@ -89,7 +89,7 @@ export class LightningStatisticsChartComponent implements OnInit { prepareChartOptions(data) { let title: object; - if (data.nodes.length === 0) { + if (data.channel_count.length === 0) { title = { textStyle: { color: 'grey', @@ -109,8 +109,8 @@ export class LightningStatisticsChartComponent implements OnInit { '#D81B60', ], grid: { - top: 30, - bottom: 70, + top: 40, + bottom: this.widget ? 30 : 70, right: this.right, left: this.left, }, @@ -133,7 +133,7 @@ export class LightningStatisticsChartComponent implements OnInit { let weightString = ''; for (const tick of ticks) { - if (tick.seriesIndex === 0) { // Nodes + 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`; @@ -149,18 +149,18 @@ export class LightningStatisticsChartComponent implements OnInit { return tooltip; } }, - xAxis: data.nodes.length === 0 ? undefined : { + xAxis: data.channel_count.length === 0 ? undefined : { type: 'time', - splitNumber: this.isMobile() ? 5 : 10, + splitNumber: (this.isMobile() || this.widget) ? 5 : 10, axisLabel: { hideOverlap: true, } }, - legend: data.nodes.length === 0 ? undefined : { + legend: data.channel_count.length === 0 ? undefined : { padding: 10, data: [ { - name: 'Nodes', + name: 'Channels', inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -168,7 +168,7 @@ export class LightningStatisticsChartComponent implements OnInit { icon: 'roundRect', }, { - name: 'Capacity', + name: 'Capacity (BTC)', inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -177,20 +177,18 @@ export class LightningStatisticsChartComponent implements OnInit { }, ], selected: JSON.parse(this.storageService.getValue('sizes_ln_legend')) ?? { - 'Nodes': true, - 'Capacity': true, + 'Channels': true, + 'Capacity (BTC)': true, } }, - yAxis: data.nodes.length === 0 ? undefined : [ + yAxis: data.channel_count.length === 0 ? undefined : [ { - min: (value) => { - return value.min * 0.9; - }, + min: 0, type: 'value', axisLabel: { color: 'rgb(110, 112, 121)', formatter: (val) => { - return `${Math.round(val)}`; + return `${formatNumber(Math.round(val), this.locale, '1.0-0')}`; } }, splitLine: { @@ -202,15 +200,13 @@ export class LightningStatisticsChartComponent implements OnInit { }, }, { - min: (value) => { - return value.min * 0.9; - }, + min: 0, type: 'value', position: 'right', axisLabel: { color: 'rgb(110, 112, 121)', formatter: (val) => { - return `${val / 100000000} BTC`; + return `${formatNumber(Math.round(val / 100000000), this.locale, '1.0-0')}`; } }, splitLine: { @@ -218,13 +214,13 @@ export class LightningStatisticsChartComponent implements OnInit { } } ], - series: data.nodes.length === 0 ? [] : [ + series: data.channel_count.length === 0 ? [] : [ { zlevel: 1, - name: 'Nodes', + name: 'Channels', showSymbol: false, symbol: 'none', - data: data.nodes, + data: data.channel_count, type: 'line', lineStyle: { width: 2, @@ -238,21 +234,12 @@ export class LightningStatisticsChartComponent implements OnInit { opacity: 1, width: 1, }, - data: [{ - yAxis: 1, - label: { - position: 'end', - show: true, - color: '#ffffff', - formatter: `1 MB` - } - }], } }, { zlevel: 0, yAxisIndex: 1, - name: 'Capacity', + name: 'Capacity (BTC)', showSymbol: false, symbol: 'none', stack: 'Total',