Merge branch 'master' into simon/revert-info-button-color-change
This commit is contained in:
commit
16cb3de211
@ -6,7 +6,8 @@ class StatisticsApi {
|
||||
public async $getStatistics(interval: string | null = null): Promise<any> {
|
||||
interval = Common.getSqlInterval(interval);
|
||||
|
||||
let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity, tor_nodes, clearnet_nodes, unannounced_nodes
|
||||
let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity,
|
||||
tor_nodes, clearnet_nodes, unannounced_nodes, clearnet_tor_nodes
|
||||
FROM lightning_stats`;
|
||||
|
||||
if (interval) {
|
||||
|
@ -191,12 +191,20 @@
|
||||
<br>
|
||||
|
||||
<div class="title">
|
||||
<h2 i18n="transaction.diagram|Transaction diagram">Diagram</h2>
|
||||
<h2 i18n="transaction.flow|Transaction flow">Flow</h2>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="graph-container" #graphContainer>
|
||||
<tx-bowtie-graph [tx]="tx" [width]="graphWidth" [height]="graphExpanded ? (maxInOut * 15) : graphHeight" [maxStrands]="graphExpanded ? maxInOut : 24" [network]="network" [tooltip]="true"></tx-bowtie-graph>
|
||||
<tx-bowtie-graph
|
||||
[tx]="tx"
|
||||
[width]="graphWidth"
|
||||
[height]="graphExpanded ? (maxInOut * 15) : graphHeight"
|
||||
[lineLimit]="inOutLimit"
|
||||
[maxStrands]="graphExpanded ? maxInOut : 24"
|
||||
[network]="network"
|
||||
[tooltip]="true">
|
||||
</tx-bowtie-graph>
|
||||
</div>
|
||||
<div class="toggle-wrapper" *ngIf="maxInOut > 24">
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="expandGraph();" *ngIf="!graphExpanded; else collapseBtn"><span i18n="show-more">Show more</span></button>
|
||||
|
@ -50,7 +50,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
graphExpanded: boolean = false;
|
||||
graphWidth: number = 1000;
|
||||
graphHeight: number = 360;
|
||||
inOutLimit: number = 150;
|
||||
maxInOut: number = 0;
|
||||
|
||||
tooltipPosition: { x: number, y: number };
|
||||
|
||||
@ViewChild('graphContainer')
|
||||
@ -298,7 +300,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
|
||||
setupGraph() {
|
||||
this.maxInOut = Math.min(250, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
||||
this.graphHeight = Math.min(360, this.maxInOut * 80);
|
||||
}
|
||||
|
||||
|
@ -19,8 +19,6 @@ interface Xput {
|
||||
confidential?: boolean;
|
||||
}
|
||||
|
||||
const lineLimit = 250;
|
||||
|
||||
@Component({
|
||||
selector: 'tx-bowtie-graph',
|
||||
templateUrl: './tx-bowtie-graph.component.html',
|
||||
@ -31,7 +29,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
@Input() network: string;
|
||||
@Input() width = 1200;
|
||||
@Input() height = 600;
|
||||
@Input() combinedWeight = 100;
|
||||
@Input() lineLimit = 250;
|
||||
@Input() maxCombinedWeight = 100;
|
||||
@Input() minWeight = 2; //
|
||||
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
|
||||
@Input() tooltip = false;
|
||||
@ -42,6 +41,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
outputs: SvgLine[];
|
||||
middle: SvgLine;
|
||||
midWidth: number;
|
||||
combinedWeight: number;
|
||||
isLiquid: boolean = false;
|
||||
hoverLine: Xput | void = null;
|
||||
tooltipPosition = { x: 0, y: 0 };
|
||||
@ -62,20 +62,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
gradient: string[] = ['#105fb0', '#105fb0'];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.midWidth = Math.min(50, Math.ceil(this.width / 20));
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.midWidth = Math.min(50, Math.ceil(this.width / 20));
|
||||
this.initGraph();
|
||||
}
|
||||
|
||||
initGraph(): void {
|
||||
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
|
||||
this.gradient = this.gradientColors[this.network];
|
||||
this.midWidth = Math.min(10, Math.ceil(this.width / 100));
|
||||
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6));
|
||||
|
||||
const totalValue = this.calcTotalValue(this.tx);
|
||||
let voutWithFee = this.tx.vout.map(v => {
|
||||
return {
|
||||
@ -103,19 +102,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
} as Xput;
|
||||
});
|
||||
|
||||
if (truncatedInputs.length > lineLimit) {
|
||||
const valueOfRest = truncatedInputs.slice(lineLimit).reduce((r, v) => {
|
||||
if (truncatedInputs.length > this.lineLimit) {
|
||||
const valueOfRest = truncatedInputs.slice(this.lineLimit).reduce((r, v) => {
|
||||
return r + (v.value || 0);
|
||||
}, 0);
|
||||
truncatedInputs = truncatedInputs.slice(0, lineLimit);
|
||||
truncatedInputs.push({ type: 'input', value: valueOfRest, rest: this.tx.vin.length - lineLimit });
|
||||
truncatedInputs = truncatedInputs.slice(0, this.lineLimit);
|
||||
truncatedInputs.push({ type: 'input', value: valueOfRest, rest: this.tx.vin.length - this.lineLimit });
|
||||
}
|
||||
if (voutWithFee.length > lineLimit) {
|
||||
const valueOfRest = voutWithFee.slice(lineLimit).reduce((r, v) => {
|
||||
if (voutWithFee.length > this.lineLimit) {
|
||||
const valueOfRest = voutWithFee.slice(this.lineLimit).reduce((r, v) => {
|
||||
return r + (v.value || 0);
|
||||
}, 0);
|
||||
voutWithFee = voutWithFee.slice(0, lineLimit);
|
||||
voutWithFee.push({ type: 'output', value: valueOfRest, rest: outputCount - lineLimit });
|
||||
voutWithFee = voutWithFee.slice(0, this.lineLimit);
|
||||
voutWithFee.push({ type: 'output', value: valueOfRest, rest: outputCount - this.lineLimit });
|
||||
}
|
||||
|
||||
this.inputData = truncatedInputs;
|
||||
@ -126,7 +125,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
this.middle = {
|
||||
path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`,
|
||||
style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}`
|
||||
style: `stroke-width: ${this.combinedWeight + 1}; stroke: ${this.gradient[1]}`
|
||||
};
|
||||
}
|
||||
|
||||
@ -157,7 +156,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
|
||||
initLines(side: 'in' | 'out', xputs: Xput[], total: number, maxVisibleStrands: number): SvgLine[] {
|
||||
if (!total) {
|
||||
const weights = xputs.map((put): number => this.combinedWeight / xputs.length);
|
||||
const weights = xputs.map((put) => this.combinedWeight / xputs.length);
|
||||
return this.linesFromWeights(side, xputs, weights, maxVisibleStrands);
|
||||
} else {
|
||||
let unknownCount = 0;
|
||||
@ -171,19 +170,26 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
});
|
||||
const unknownShare = unknownTotal / unknownCount;
|
||||
// conceptual weights
|
||||
const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
|
||||
const weights = xputs.map((put) => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total);
|
||||
return this.linesFromWeights(side, xputs, weights, maxVisibleStrands);
|
||||
}
|
||||
}
|
||||
|
||||
linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number) {
|
||||
const lines = [];
|
||||
// actual displayed line thicknesses
|
||||
const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1);
|
||||
linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number): SvgLine[] {
|
||||
const lineParams = weights.map((w) => {
|
||||
return {
|
||||
weight: w,
|
||||
thickness: Math.max(this.minWeight - 1, w) + 1,
|
||||
offset: 0,
|
||||
innerY: 0,
|
||||
outerY: 0,
|
||||
};
|
||||
});
|
||||
const visibleStrands = Math.min(maxVisibleStrands, xputs.length);
|
||||
const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0);
|
||||
const visibleWeight = lineParams.slice(0, visibleStrands).reduce((acc, v) => v.thickness + acc, 0);
|
||||
const gaps = visibleStrands - 1;
|
||||
|
||||
// bounds of the middle segment
|
||||
const innerTop = (this.height / 2) - (this.combinedWeight / 2);
|
||||
const innerBottom = innerTop + this.combinedWeight;
|
||||
// tracks the visual bottom of the endpoints of the previous line
|
||||
@ -192,39 +198,91 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
||||
// gap between strands
|
||||
const spacing = (this.height - visibleWeight) / gaps;
|
||||
|
||||
for (let i = 0; i < xputs.length; i++) {
|
||||
const weight = weights[i];
|
||||
const minWeight = minWeights[i];
|
||||
// curve adjustments to prevent overlaps
|
||||
let offset = 0;
|
||||
let minOffset = 0;
|
||||
let maxOffset = 0;
|
||||
let lastWeight = 0;
|
||||
let pad = 0;
|
||||
lineParams.forEach((line, i) => {
|
||||
// set the vertical position of the (center of the) outer side of the line
|
||||
let outer = lastOuter + (minWeight / 2);
|
||||
const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2)));
|
||||
line.outerY = lastOuter + (line.thickness / 2);
|
||||
line.innerY = Math.min(innerBottom + (line.thickness / 2), Math.max(innerTop + (line.thickness / 2), lastInner + (line.weight / 2)));
|
||||
|
||||
// special case to center single input/outputs
|
||||
if (xputs.length === 1) {
|
||||
outer = (this.height / 2);
|
||||
line.outerY = (this.height / 2);
|
||||
}
|
||||
|
||||
lastOuter += minWeight + spacing;
|
||||
lastInner += weight;
|
||||
lines.push({
|
||||
path: this.makePath(side, outer, inner, minWeight),
|
||||
style: this.makeStyle(minWeight, xputs[i].type),
|
||||
class: xputs[i].type
|
||||
});
|
||||
}
|
||||
lastOuter += line.thickness + spacing;
|
||||
lastInner += line.weight;
|
||||
|
||||
return lines;
|
||||
// calculate conservative lower bound of the amount of horizontal offset
|
||||
// required to prevent this line overlapping its neighbor
|
||||
|
||||
if (this.tooltip || !xputs[i].rest) {
|
||||
const w = (this.width - Math.max(lastWeight, line.weight)) / 2; // approximate horizontal width of the curved section of the line
|
||||
const y1 = line.outerY;
|
||||
const y2 = line.innerY;
|
||||
const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line
|
||||
|
||||
// slope of the inflection point of the bezier curve
|
||||
const dx = 0.75 * w;
|
||||
const dy = 1.5 * (y2 - y1);
|
||||
const a = Math.atan2(dy, dx);
|
||||
|
||||
// parallel curves should be separated by >=t at the inflection point to prevent overlap
|
||||
// vertical offset is always = t, contributing tCos(a)
|
||||
// horizontal offset h will contribute hSin(a)
|
||||
// tCos(a) + hSin(a) >= t
|
||||
// h >= t(1 - cos(a)) / sin(a)
|
||||
if (Math.sin(a) !== 0) {
|
||||
// (absolute value clamped to t for sanity)
|
||||
offset += Math.max(Math.min(t * (1 - Math.cos(a)) / Math.sin(a), t), -t);
|
||||
}
|
||||
|
||||
line.offset = offset;
|
||||
minOffset = Math.min(minOffset, offset);
|
||||
maxOffset = Math.max(maxOffset, offset);
|
||||
pad = Math.max(pad, line.thickness / 2);
|
||||
lastWeight = line.weight;
|
||||
} else {
|
||||
// skip the offsets for consolidated lines in unfurls, since these *should* overlap a little
|
||||
}
|
||||
});
|
||||
|
||||
// normalize offsets
|
||||
lineParams.forEach((line) => {
|
||||
line.offset -= minOffset;
|
||||
});
|
||||
maxOffset -= minOffset;
|
||||
|
||||
return lineParams.map((line, i) => {
|
||||
return {
|
||||
path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset),
|
||||
style: this.makeStyle(line.thickness, xputs[i].type),
|
||||
class: xputs[i].type
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string {
|
||||
const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5);
|
||||
const center = this.width / 2 + (side === 'in' ? -(this.midWidth * 0.9) : (this.midWidth * 0.9) );
|
||||
const midpoint = (start + center) / 2;
|
||||
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string {
|
||||
const start = (weight * 0.5);
|
||||
const curveStart = Math.max(start + 1, pad - offset);
|
||||
const end = this.width / 2 - (this.midWidth * 0.9) + 1;
|
||||
const curveEnd = end - offset - 10;
|
||||
const midpoint = (curveStart + curveEnd) / 2;
|
||||
|
||||
// correct for svg horizontal gradient bug
|
||||
if (Math.round(outer) === Math.round(inner)) {
|
||||
outer -= 1;
|
||||
}
|
||||
return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`;
|
||||
|
||||
if (side === 'in') {
|
||||
return `M ${start} ${outer} L ${curveStart} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${curveEnd} ${inner} L ${end} ${inner}`;
|
||||
} else { // mirrored in y-axis for the right hand side
|
||||
return `M ${this.width - start} ${outer} L ${this.width - curveStart} ${outer} C ${this.width - midpoint} ${outer}, ${this.width - midpoint} ${inner}, ${this.width - curveEnd} ${inner} L ${this.width - end} ${inner}`;
|
||||
}
|
||||
}
|
||||
|
||||
makeStyle(minWeight, type): string {
|
||||
|
@ -17,50 +17,84 @@ export const wsApiDocsData = {
|
||||
codeTemplate: {
|
||||
curl: `/api/v1/ws`,
|
||||
commonJS: `
|
||||
const { %{0}: { websocket } } = mempoolJS();
|
||||
const { %{0}: { websocket } } = mempoolJS();
|
||||
|
||||
const ws = websocket.initClient({
|
||||
options: ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart'],
|
||||
});
|
||||
const ws = websocket.initClient({
|
||||
options: ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart'],
|
||||
});
|
||||
|
||||
ws.addEventListener('message', function incoming({data}) {
|
||||
const res = JSON.parse(data.toString());
|
||||
if (res.block) {
|
||||
document.getElementById("result-blocks").textContent = JSON.stringify(res.block, undefined, 2);
|
||||
}
|
||||
if (res.mempoolInfo) {
|
||||
document.getElementById("result-mempool-info").textContent = JSON.stringify(res.mempoolInfo, undefined, 2);
|
||||
}
|
||||
if (res.transactions) {
|
||||
document.getElementById("result-transactions").textContent = JSON.stringify(res.transactions, undefined, 2);
|
||||
}
|
||||
if (res["mempool-blocks"]) {
|
||||
document.getElementById("result-mempool-blocks").textContent = JSON.stringify(res["mempool-blocks"], undefined, 2);
|
||||
}
|
||||
});
|
||||
ws.addEventListener('message', function incoming({data}) {
|
||||
const res = JSON.parse(data.toString());
|
||||
if (res.block) {
|
||||
document.getElementById("result-blocks").textContent = JSON.stringify(res.block, undefined, 2);
|
||||
}
|
||||
if (res.mempoolInfo) {
|
||||
document.getElementById("result-mempool-info").textContent = JSON.stringify(res.mempoolInfo, undefined, 2);
|
||||
}
|
||||
if (res.transactions) {
|
||||
document.getElementById("result-transactions").textContent = JSON.stringify(res.transactions, undefined, 2);
|
||||
}
|
||||
if (res["mempool-blocks"]) {
|
||||
document.getElementById("result-mempool-blocks").textContent = JSON.stringify(res["mempool-blocks"], undefined, 2);
|
||||
}
|
||||
});
|
||||
`,
|
||||
esModule: `
|
||||
const { %{0}: { websocket } } = mempoolJS();
|
||||
const { %{0}: { websocket } } = mempoolJS();
|
||||
|
||||
const ws = websocket.initServer({
|
||||
options: ["blocks", "stats", "mempool-blocks", "live-2h-chart"],
|
||||
});
|
||||
const ws = websocket.initServer({
|
||||
options: ["blocks", "stats", "mempool-blocks", "live-2h-chart"],
|
||||
});
|
||||
|
||||
ws.on("message", function incoming(data) {
|
||||
const res = JSON.parse(data.toString());
|
||||
if (res.block) {
|
||||
console.log(res.block);
|
||||
}
|
||||
if (res.mempoolInfo) {
|
||||
console.log(res.mempoolInfo);
|
||||
}
|
||||
if (res.transactions) {
|
||||
console.log(res.transactions);
|
||||
}
|
||||
if (res["mempool-blocks"]) {
|
||||
console.log(res["mempool-blocks"]);
|
||||
}
|
||||
});
|
||||
ws.on("message", function incoming(data) {
|
||||
const res = JSON.parse(data.toString());
|
||||
if (res.block) {
|
||||
console.log(res.block);
|
||||
}
|
||||
if (res.mempoolInfo) {
|
||||
console.log(res.mempoolInfo);
|
||||
}
|
||||
if (res.transactions) {
|
||||
console.log(res.transactions);
|
||||
}
|
||||
if (res["mempool-blocks"]) {
|
||||
console.log(res["mempool-blocks"]);
|
||||
}
|
||||
});
|
||||
`,
|
||||
python: `import websocket
|
||||
import _thread
|
||||
import time
|
||||
import rel
|
||||
import json
|
||||
|
||||
rel.safe_read()
|
||||
|
||||
def on_message(ws, message):
|
||||
print(json.loads(message))
|
||||
|
||||
def on_error(ws, error):
|
||||
print(error)
|
||||
|
||||
def on_close(ws, close_status_code, close_msg):
|
||||
print("### closed ###")
|
||||
|
||||
def on_open(ws):
|
||||
message = { "action": "init" }
|
||||
ws.send(json.dumps(message))
|
||||
message = { "action": "want", "data": ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart', 'watch-mempool'] }
|
||||
ws.send(json.dumps(message))
|
||||
|
||||
if __name__ == "__main__":
|
||||
ws = websocket.WebSocketApp("wss://mempool.space/api/v1/ws",
|
||||
on_open=on_open,
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close=on_close)
|
||||
|
||||
ws.run_forever(dispatcher=rel) # Set dispatcher to automatic reconnection
|
||||
rel.signal(2, rel.abort) # Keyboard Interrupt
|
||||
rel.dispatch()
|
||||
`,
|
||||
},
|
||||
codeSampleMainnet: emptyCodeSample,
|
||||
|
@ -30,6 +30,13 @@
|
||||
<pre><code [innerText]="wrapEsModule(code)"></code></pre>
|
||||
</ng-template>
|
||||
</li>
|
||||
<li ngbNavItem *ngIf="showCodeExample[network] && network !== 'liquid' && network !== 'liquidtestnet'" role="presentation">
|
||||
<a ngbNavLink (click)="adjustContainerHeight( $event )" role="tab">Python</a>
|
||||
<ng-template ngbNavContent>
|
||||
<div class="subtitle"><ng-container i18n="API Docs code example">Code Example</ng-container> <app-clipboard [text]="wrapEsModule(code)"></app-clipboard></div>
|
||||
<pre><code [innerText]="wrapPythonTemplate(code)"></code></pre>
|
||||
</ng-template>
|
||||
</li>
|
||||
</ul>
|
||||
<div [ngbNavOutlet]="navCodeTemplate"></div>
|
||||
<div *ngIf="code.codeTemplate && wrapResponse(code) !== ''" class="response">
|
||||
|
@ -152,6 +152,7 @@ export class CodeTemplateComponent implements OnInit {
|
||||
const init = async () => {
|
||||
${codeText}
|
||||
};
|
||||
|
||||
init();`;
|
||||
}
|
||||
}
|
||||
@ -287,6 +288,10 @@ yarn add @mempool/liquid.js`;
|
||||
return code.codeSampleMainnet.response;
|
||||
}
|
||||
|
||||
wrapPythonTemplate(code: any) {
|
||||
return ( ( this.network === 'testnet' || this.network === 'signet' ) ? ( code.codeTemplate.python.replace( "wss://mempool.space/api/v1/ws", "wss://mempool.space/" + this.network + "/api/v1/ws" ) ) : code.codeTemplate.python );
|
||||
}
|
||||
|
||||
replaceJSPlaceholder(text: string, code: any) {
|
||||
for (let index = 0; index < code.length; index++) {
|
||||
const textReplace = code[index];
|
||||
|
@ -85,9 +85,12 @@
|
||||
{{ node.as_organization }} [ASN {{node.as_number}}]
|
||||
</a>
|
||||
</td>
|
||||
<td *ngIf="!node.as_number">
|
||||
<td *ngIf="clearnetSocketCount === 0 && torSocketCount > 0">
|
||||
<span class="badge badge-success" placement="bottom" i18n="tor">Exclusively on Tor</span>
|
||||
</td>
|
||||
<td *ngIf="node.sockets.length === 0">
|
||||
<span i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -22,6 +22,8 @@ export class NodeComponent implements OnInit {
|
||||
error: Error;
|
||||
publicKey: string;
|
||||
channelListLoading = false;
|
||||
clearnetSocketCount = 0;
|
||||
torSocketCount = 0;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
@ -47,10 +49,13 @@ export class NodeComponent implements OnInit {
|
||||
let label = '';
|
||||
if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) {
|
||||
label = 'IPv4';
|
||||
this.clearnetSocketCount++;
|
||||
} else if (socket.indexOf('[') > -1) {
|
||||
label = 'IPv6';
|
||||
this.clearnetSocketCount++;
|
||||
} else if (socket.indexOf('onion') > -1) {
|
||||
label = 'Tor';
|
||||
this.torSocketCount++;
|
||||
}
|
||||
socketsObject.push({
|
||||
label: label,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic} from 'echarts';
|
||||
import { EChartsOption, graphic, LineSeriesOption} from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { formatNumber } from '@angular/common';
|
||||
@ -89,10 +89,11 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
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]),
|
||||
clearnet_tor_nodes: data.map(val => [val.added * 1000, val.clearnet_tor_nodes]),
|
||||
};
|
||||
let maxYAxis = 0;
|
||||
for (const day of data) {
|
||||
maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes);
|
||||
maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes + day.clearnet_tor_nodes);
|
||||
}
|
||||
maxYAxis = Math.ceil(maxYAxis / 3000) * 3000;
|
||||
this.prepareChartOptions(chartData, maxYAxis);
|
||||
@ -134,6 +135,94 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
const series: LineSeriesOption[] = [
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Unknown`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.unannounced_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#D81B60' },
|
||||
{ offset: 1, color: '#D81B60AA' },
|
||||
]),
|
||||
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Reachable on Clearnet Only`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.clearnet_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#FFB300' },
|
||||
{ offset: 1, color: '#FFB300AA' },
|
||||
]),
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Reachable on Clearnet and Darknet`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.clearnet_tor_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#be7d4c' },
|
||||
{ offset: 1, color: '#be7d4cAA' },
|
||||
]),
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Reachable on Darknet Only`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.tor_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#7D4698' },
|
||||
{ offset: 1, color: '#7D4698AA' },
|
||||
]),
|
||||
smooth: false,
|
||||
},
|
||||
];
|
||||
|
||||
this.chartOptions = {
|
||||
title: title,
|
||||
animation: false,
|
||||
@ -164,12 +253,17 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
|
||||
|
||||
for (const tick of ticks.reverse()) {
|
||||
if (tick.seriesName.indexOf('ignored') !== -1) {
|
||||
continue;
|
||||
}
|
||||
if (tick.seriesIndex === 0) { // Tor
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`;
|
||||
} else if (tick.seriesIndex === 1) { // Clearnet
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`;
|
||||
} else if (tick.seriesIndex === 2) { // Unannounced
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`;
|
||||
} else if (tick.seriesIndex === 3) { // Tor + Clearnet
|
||||
tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`;
|
||||
}
|
||||
tooltip += `<br>`;
|
||||
total += tick.data[1];
|
||||
@ -190,7 +284,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
padding: 10,
|
||||
data: [
|
||||
{
|
||||
name: $localize`Total`,
|
||||
name: $localize`Reachable on Darknet Only`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@ -198,7 +292,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: $localize`Tor`,
|
||||
name: $localize`Reachable on Clearnet and Darknet`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@ -206,7 +300,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: $localize`Clearnet`,
|
||||
name: $localize`Reachable on Clearnet Only`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@ -214,7 +308,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
icon: 'roundRect',
|
||||
},
|
||||
{
|
||||
name: $localize`Unannounced`,
|
||||
name: $localize`Unknown`,
|
||||
inactiveColor: 'rgb(110, 112, 121)',
|
||||
textStyle: {
|
||||
color: 'white',
|
||||
@ -223,10 +317,10 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
},
|
||||
],
|
||||
selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? {
|
||||
'Total': true,
|
||||
'Tor': true,
|
||||
'Clearnet': true,
|
||||
'Unannounced': true,
|
||||
'$localize`Reachable on Darknet Only`': true,
|
||||
'$localize`Reachable on Clearnet Only`': true,
|
||||
'$localize`Reachable on Clearnet and Darknet`': true,
|
||||
'$localize`Unknown`': true,
|
||||
}
|
||||
},
|
||||
yAxis: data.tor_nodes.length === 0 ? undefined : [
|
||||
@ -250,7 +344,6 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
opacity: 0.25,
|
||||
},
|
||||
},
|
||||
max: maxYAxis,
|
||||
min: 0,
|
||||
interval: 3000,
|
||||
},
|
||||
@ -274,77 +367,25 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
opacity: 0.25,
|
||||
},
|
||||
},
|
||||
max: maxYAxis,
|
||||
min: 0,
|
||||
interval: 3000,
|
||||
}
|
||||
],
|
||||
series: data.tor_nodes.length === 0 ? [] : [
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Unannounced`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.unannounced_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#D81B60' },
|
||||
{ offset: 1, color: '#D81B60AA' },
|
||||
]),
|
||||
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 0,
|
||||
name: $localize`Clearnet`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.clearnet_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#FFB300' },
|
||||
{ offset: 1, color: '#FFB300AA' },
|
||||
]),
|
||||
smooth: false,
|
||||
},
|
||||
{
|
||||
zlevel: 1,
|
||||
yAxisIndex: 1,
|
||||
name: $localize`Tor`,
|
||||
showSymbol: false,
|
||||
symbol: 'none',
|
||||
data: data.tor_nodes,
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
width: 2,
|
||||
},
|
||||
areaStyle: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#7D4698' },
|
||||
{ offset: 1, color: '#7D4698AA' },
|
||||
]),
|
||||
smooth: false,
|
||||
},
|
||||
],
|
||||
series: data.tor_nodes.length === 0 ? [] : series.concat(series.map((serie) => {
|
||||
// We create dummy duplicated series so when we use the data zoom, the y axis
|
||||
// both scales properly
|
||||
const invisibleSerie = {...serie};
|
||||
invisibleSerie.name = 'ignored' + Math.random().toString();
|
||||
invisibleSerie.stack = 'ignored';
|
||||
invisibleSerie.yAxisIndex = 1;
|
||||
invisibleSerie.lineStyle = {
|
||||
opacity: 0,
|
||||
};
|
||||
invisibleSerie.areaStyle = {
|
||||
opacity: 0,
|
||||
};
|
||||
return invisibleSerie;
|
||||
})),
|
||||
dataZoom: this.widget ? null : [{
|
||||
type: 'inside',
|
||||
realtime: true,
|
||||
@ -371,6 +412,11 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
if (isMobile()) {
|
||||
// @ts-ignore
|
||||
this.chartOptions.legend.left = 50;
|
||||
}
|
||||
}
|
||||
|
||||
onChartInit(ec): void {
|
||||
|
Loading…
x
Reference in New Issue
Block a user