24">
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 948eecdb1..3c29f210d 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -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);
}
diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts
index 78a865b89..16e2736f7 100644
--- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts
+++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts
@@ -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 {
diff --git a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html
index 8b486eff5..a61273d4d 100644
--- a/frontend/src/app/lightning/channel/channel-box/channel-box.component.html
+++ b/frontend/src/app/lightning/channel/channel-box/channel-box.component.html
@@ -11,44 +11,64 @@