diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html
new file mode 100644
index 000000000..017eeab99
--- /dev/null
+++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.html
@@ -0,0 +1,6 @@
+
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.scss b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss
similarity index 82%
rename from frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.scss
rename to frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss
index 8c3c271d1..cbb95ec0e 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.scss
+++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss
@@ -1,4 +1,4 @@
-.mempool-block-overview {
+.block-overview-graph {
position: relative;
width: 100%;
padding-bottom: 100%;
@@ -8,12 +8,15 @@
align-items: center;
}
-.block-overview {
+
+.block-overview-canvas {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
+ width: 100%;
+ height: 100%;
overflow: hidden;
}
@@ -27,6 +30,7 @@
justify-content: center;
align-items: center;
transition: opacity 500ms 500ms;
+ pointer-events: none;
&.hidden {
opacity: 0;
diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
new file mode 100644
index 000000000..cbb0225ff
--- /dev/null
+++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts
@@ -0,0 +1,360 @@
+import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit } from '@angular/core';
+import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
+import { WebsocketService } from 'src/app/services/websocket.service';
+import { FastVertexArray } from './fast-vertex-array';
+import BlockScene from './block-scene';
+import TxSprite from './tx-sprite';
+import TxView from './tx-view';
+
+@Component({
+ selector: 'app-block-overview-graph',
+ templateUrl: './block-overview-graph.component.html',
+ styleUrls: ['./block-overview-graph.component.scss'],
+})
+export class BlockOverviewGraphComponent implements AfterViewInit {
+ @Input() isLoading: boolean;
+ @Input() resolution: number;
+ @Input() blockLimit: number;
+ @Output() txPreviewEvent = new EventEmitter();
+
+ @ViewChild('blockCanvas')
+ canvas: ElementRef;
+
+ gl: WebGLRenderingContext;
+ animationFrameRequest: number;
+ displayWidth: number;
+ displayHeight: number;
+ cssWidth: number;
+ cssHeight: number;
+ shaderProgram: WebGLProgram;
+ vertexArray: FastVertexArray;
+ running: boolean;
+ scene: BlockScene;
+ hoverTx: TxView | void;
+ selectedTx: TxView | void;
+
+ constructor(
+ readonly ngZone: NgZone,
+ ) {
+ this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
+ }
+
+ ngAfterViewInit(): void {
+ this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
+ this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
+ this.gl = this.canvas.nativeElement.getContext('webgl');
+ this.initCanvas();
+
+ this.resizeCanvas();
+ }
+
+ clear(direction): void {
+ this.exit(direction);
+ this.hoverTx = null;
+ this.selectedTx = null;
+ this.txPreviewEvent.emit(null);
+ }
+
+ enter(transactions: TransactionStripped[], direction: string): void {
+ if (this.scene) {
+ this.scene.enter(transactions, direction);
+ }
+ }
+
+ exit(direction: string): void {
+ if (this.scene) {
+ this.scene.exit(direction);
+ }
+ }
+
+ replace(transactions: TransactionStripped[], direction: string): void {
+ if (this.scene) {
+ this.scene.replace(transactions, direction);
+ }
+ }
+
+ update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
+ if (this.scene) {
+ this.scene.update(add, remove, direction, resetLayout);
+ }
+ }
+
+ initCanvas(): void {
+ this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
+ this.gl.clear(this.gl.COLOR_BUFFER_BIT);
+
+ const shaderSet = [
+ {
+ type: this.gl.VERTEX_SHADER,
+ src: vertShaderSrc
+ },
+ {
+ type: this.gl.FRAGMENT_SHADER,
+ src: fragShaderSrc
+ }
+ ];
+
+ this.shaderProgram = this.buildShaderProgram(shaderSet);
+
+ this.gl.useProgram(this.shaderProgram);
+
+ // Set up alpha blending
+ this.gl.enable(this.gl.BLEND);
+ this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
+
+ const glBuffer = this.gl.createBuffer();
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
+
+ /* SET UP SHADER ATTRIBUTES */
+ Object.keys(attribs).forEach((key, i) => {
+ attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
+ this.gl.enableVertexAttribArray(attribs[key].pointer);
+ });
+
+ this.start();
+ }
+
+ handleContextLost(event): void {
+ event.preventDefault();
+ cancelAnimationFrame(this.animationFrameRequest);
+ this.animationFrameRequest = null;
+ this.running = false;
+ }
+
+ handleContextRestored(event): void {
+ this.initCanvas();
+ }
+
+ @HostListener('window:resize', ['$event'])
+ resizeCanvas(): void {
+ this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
+ this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
+ this.displayWidth = window.devicePixelRatio * this.cssWidth;
+ this.displayHeight = window.devicePixelRatio * this.cssHeight;
+ this.canvas.nativeElement.width = this.displayWidth;
+ this.canvas.nativeElement.height = this.displayHeight;
+ if (this.gl) {
+ this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
+ }
+ if (this.scene) {
+ this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
+ } else {
+ this.scene = this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
+ blockLimit: this.blockLimit, vertexArray: this.vertexArray });
+ }
+ }
+
+ compileShader(src, type): WebGLShader {
+ const shader = this.gl.createShader(type);
+
+ this.gl.shaderSource(shader, src);
+ this.gl.compileShader(shader);
+
+ if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
+ console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
+ console.log(this.gl.getShaderInfoLog(shader));
+ }
+ return shader;
+ }
+
+ buildShaderProgram(shaderInfo): WebGLProgram {
+ const program = this.gl.createProgram();
+
+ shaderInfo.forEach((desc) => {
+ const shader = this.compileShader(desc.src, desc.type);
+ if (shader) {
+ this.gl.attachShader(program, shader);
+ }
+ });
+
+ this.gl.linkProgram(program);
+
+ if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
+ console.log('Error linking shader program:');
+ console.log(this.gl.getProgramInfoLog(program));
+ }
+
+ return program;
+ }
+
+ start(): void {
+ this.running = true;
+ this.ngZone.runOutsideAngular(() => this.run());
+ }
+
+ run(now?: DOMHighResTimeStamp): void {
+ if (!now) {
+ now = performance.now();
+ }
+
+ /* SET UP SHADER UNIFORMS */
+ // screen dimensions
+ this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
+ // frame timestamp
+ this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
+
+ /* SET UP SHADER ATTRIBUTES */
+ Object.keys(attribs).forEach((key, i) => {
+ this.gl.vertexAttribPointer(attribs[key].pointer,
+ attribs[key].count, // number of primitives in this attribute
+ this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
+ false, // never normalised
+ stride, // distance between values of the same attribute
+ attribs[key].offset); // offset of the first value
+ });
+
+ const pointArray = this.vertexArray.getVertexData();
+
+ if (pointArray.length) {
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
+ this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
+ }
+
+ /* LOOP */
+ if (this.running) {
+ if (this.animationFrameRequest) {
+ cancelAnimationFrame(this.animationFrameRequest);
+ this.animationFrameRequest = null;
+ }
+ this.animationFrameRequest = requestAnimationFrame(() => this.run());
+ }
+ }
+
+ @HostListener('click', ['$event'])
+ onClick(event) {
+ this.setPreviewTx(event.offsetX, event.offsetY, true);
+ }
+
+ @HostListener('pointermove', ['$event'])
+ onPointerMove(event) {
+ this.setPreviewTx(event.offsetX, event.offsetY, false);
+ }
+
+ @HostListener('pointerleave', ['$event'])
+ onPointerLeave(event) {
+ this.setPreviewTx(-1, -1, false);
+ }
+
+ setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) {
+ const x = cssX * window.devicePixelRatio;
+ const y = cssY * window.devicePixelRatio;
+ if (this.scene && (!this.selectedTx || clicked)) {
+ const selected = this.scene.getTxAt({ x, y });
+ const currentPreview = this.selectedTx || this.hoverTx;
+
+ if (selected !== currentPreview) {
+ if (currentPreview) {
+ currentPreview.setHover(false);
+ }
+ if (selected) {
+ selected.setHover(true);
+ this.txPreviewEvent.emit({
+ txid: selected.txid,
+ fee: selected.fee,
+ vsize: selected.vsize,
+ value: selected.value
+ });
+ if (clicked) {
+ this.selectedTx = selected;
+ } else {
+ this.hoverTx = selected;
+ }
+ } else {
+ if (clicked) {
+ this.selectedTx = null;
+ }
+ this.hoverTx = null;
+ this.txPreviewEvent.emit(null);
+ }
+ } else if (clicked) {
+ if (selected === this.selectedTx) {
+ this.hoverTx = this.selectedTx;
+ this.selectedTx = null;
+ } else {
+ this.selectedTx = selected;
+ }
+ }
+ }
+ }
+}
+
+// WebGL shader attributes
+const attribs = {
+ offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
+ posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
+ posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
+ posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
+ colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
+ colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
+ colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
+ colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
+};
+// Calculate the number of bytes per vertex based on specified attributes
+const stride = Object.values(attribs).reduce((total, attrib) => {
+ return total + (attrib.count * 4);
+}, 0);
+// Calculate vertex attribute offsets
+for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
+ const attrib = Object.values(attribs)[i];
+ attrib.offset = offset;
+ offset += (attrib.count * 4);
+}
+
+const vertShaderSrc = `
+varying lowp vec4 vColor;
+
+// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
+// shader interpolates between start and end values at the given rate, from the given time
+
+attribute vec2 offset;
+attribute vec4 posX;
+attribute vec4 posY;
+attribute vec4 posR;
+attribute vec4 colR;
+attribute vec4 colG;
+attribute vec4 colB;
+attribute vec4 colA;
+
+uniform vec2 screenSize;
+uniform float now;
+
+float smootherstep(float x) {
+ x = clamp(x, 0.0, 1.0);
+ float ix = 1.0 - x;
+ x = x * x;
+ return x / (x + ix * ix);
+}
+
+float interpolateAttribute(vec4 attr) {
+ float d = (now - attr.z) * attr.w;
+ float delta = smootherstep(d);
+ return mix(attr.x, attr.y, delta);
+}
+
+void main() {
+ vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
+ // vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
+
+ float radius = interpolateAttribute(posR);
+ vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
+
+ gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
+
+ float red = interpolateAttribute(colR);
+ float green = interpolateAttribute(colG);
+ float blue = interpolateAttribute(colB);
+ float alpha = interpolateAttribute(colA);
+
+ vColor = vec4(red, green, blue, alpha);
+}
+`;
+
+const fragShaderSrc = `
+varying lowp vec4 vColor;
+
+void main() {
+ gl_FragColor = vColor;
+ // premultiply alpha
+ gl_FragColor.rgb *= gl_FragColor.a;
+}
+`;
diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts
new file mode 100644
index 000000000..b71c1ab6c
--- /dev/null
+++ b/frontend/src/app/components/block-overview-graph/block-scene.ts
@@ -0,0 +1,724 @@
+import { FastVertexArray } from './fast-vertex-array';
+import TxView from './tx-view';
+import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
+import { Position, Square } from './sprite-types';
+
+export default class BlockScene {
+ scene: { count: number, offset: { x: number, y: number}};
+ vertexArray: FastVertexArray;
+ txs: { [key: string]: TxView };
+ width: number;
+ height: number;
+ gridWidth: number;
+ gridHeight: number;
+ gridSize: number;
+ vbytesPerUnit: number;
+ unitPadding: number;
+ unitWidth: number;
+ initialised: boolean;
+ layout: BlockLayout;
+ dirty: boolean;
+
+ constructor({ width, height, resolution, blockLimit, vertexArray }:
+ { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }
+ ) {
+ this.init({ width, height, resolution, blockLimit, vertexArray });
+ }
+
+ destroy(): void {
+ Object.values(this.txs).forEach(tx => tx.destroy());
+ }
+
+ resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
+ this.width = width;
+ this.height = height;
+ this.gridSize = this.width / this.gridWidth;
+ this.unitPadding = width / 500;
+ this.unitWidth = this.gridSize - (this.unitPadding * 2);
+
+ this.dirty = true;
+ if (this.initialised && this.scene) {
+ this.updateAll(performance.now());
+ }
+ }
+
+ // Animate new block entering scene
+ enter(txs: TransactionStripped[], direction) {
+ this.replace(txs, direction);
+ }
+
+ // Animate block leaving scene
+ exit(direction: string): void {
+ const startTime = performance.now();
+ const removed = this.removeBatch(Object.keys(this.txs), startTime, direction);
+
+ // clean up sprites
+ setTimeout(() => {
+ removed.forEach(tx => {
+ tx.destroy();
+ });
+ }, 2000);
+ }
+
+ // Reset layout and replace with new set of transactions
+ replace(txs: TransactionStripped[], direction: string = 'left'): void {
+ const startTime = performance.now();
+ const nextIds = {};
+ const remove = [];
+ txs.forEach(tx => {
+ nextIds[tx.txid] = true;
+ });
+ Object.keys(this.txs).forEach(txid => {
+ if (!nextIds[txid]) {
+ remove.push(txid);
+ }
+ });
+ txs.forEach(tx => {
+ if (!this.txs[tx.txid]) {
+ this.txs[tx.txid] = new TxView(tx, this.vertexArray);
+ }
+ });
+
+ const removed = this.removeBatch(remove, startTime, direction);
+
+ // clean up sprites
+ setTimeout(() => {
+ removed.forEach(tx => {
+ tx.destroy();
+ });
+ }, 1000);
+
+ this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
+
+ Object.values(this.txs).sort(feeRateDescending).forEach(tx => {
+ this.place(tx);
+ });
+
+ this.updateAll(startTime, direction);
+ }
+
+ update(add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
+ const startTime = performance.now();
+ const removed = this.removeBatch(remove, startTime, direction);
+
+ // clean up sprites
+ setTimeout(() => {
+ removed.forEach(tx => {
+ tx.destroy();
+ });
+ }, 1000);
+
+ if (resetLayout) {
+ add.forEach(tx => {
+ if (!this.txs[tx.txid]) {
+ this.txs[tx.txid] = new TxView(tx, this.vertexArray);
+ }
+ });
+ this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
+ Object.values(this.txs).sort(feeRateDescending).forEach(tx => {
+ this.place(tx);
+ });
+ } else {
+ // try to insert new txs directly
+ const remaining = [];
+ add.map(tx => new TxView(tx, this.vertexArray)).sort(feeRateDescending).forEach(tx => {
+ if (!this.tryInsertByFee(tx)) {
+ remaining.push(tx);
+ }
+ });
+ this.placeBatch(remaining);
+ this.layout.applyGravity();
+ }
+
+ this.updateAll(startTime, direction);
+ }
+
+ // return the tx at this screen position, if any
+ getTxAt(position: Position): TxView | void {
+ if (this.layout) {
+ const gridPosition = this.screenToGrid(position);
+ return this.layout.getTx(gridPosition);
+ } else {
+ return null;
+ }
+ }
+
+ private init({ width, height, resolution, blockLimit, vertexArray }:
+ { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }
+ ): void {
+ this.vertexArray = vertexArray;
+
+ this.scene = {
+ count: 0,
+ offset: {
+ x: 0,
+ y: 0
+ }
+ };
+
+ // Set the scale of the visualization (with a 5% margin)
+ this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2);
+ this.gridWidth = resolution;
+ this.gridHeight = resolution;
+ this.resize({ width, height });
+ this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight });
+
+ this.txs = {};
+
+ this.initialised = true;
+ this.dirty = true;
+ }
+
+ private insert(tx: TxView, startTime: number, direction: string = 'left'): void {
+ this.txs[tx.txid] = tx;
+ this.place(tx);
+ this.updateTx(tx, startTime, direction);
+ }
+
+ private updateTx(tx: TxView, startTime: number, direction: string = 'left'): void {
+ if (tx.dirty || this.dirty) {
+ this.saveGridToScreenPosition(tx);
+ this.setTxOnScreen(tx, startTime, direction);
+ }
+ }
+
+ private setTxOnScreen(tx: TxView, startTime: number, direction: string = 'left'): void {
+ if (!tx.initialised) {
+ const txColor = tx.getColor();
+ tx.update({
+ display: {
+ position: {
+ x: tx.screenPosition.x + (direction === 'right' ? -this.width : this.width) * 1.4,
+ y: tx.screenPosition.y,
+ s: tx.screenPosition.s
+ },
+ color: txColor,
+ },
+ start: startTime,
+ delay: 0,
+ });
+ tx.update({
+ display: {
+ position: tx.screenPosition,
+ color: txColor
+ },
+ duration: 1000,
+ start: startTime,
+ delay: 50,
+ });
+ } else {
+ tx.update({
+ display: {
+ position: tx.screenPosition
+ },
+ duration: 1000,
+ minDuration: 500,
+ start: startTime,
+ delay: 50,
+ adjust: true
+ });
+ }
+ }
+
+ private updateAll(startTime: number, direction: string = 'left'): void {
+ this.scene.count = 0;
+ const ids = this.getTxList();
+ startTime = startTime || performance.now();
+ for (const id of ids) {
+ this.updateTx(this.txs[id], startTime, direction);
+ }
+ this.dirty = false;
+ }
+
+ private remove(id: string, startTime: number, direction: string = 'left'): TxView | void {
+ const tx = this.txs[id];
+ if (tx) {
+ this.layout.remove(tx);
+ tx.update({
+ display: {
+ position: {
+ x: tx.screenPosition.x + (direction === 'right' ? this.width : -this.width) * 1.4,
+ y: this.txs[id].screenPosition.y,
+ }
+ },
+ duration: 1000,
+ start: startTime,
+ delay: 50
+ });
+ }
+ delete this.txs[id];
+ return tx;
+ }
+
+ private getTxList(): string[] {
+ return Object.keys(this.txs);
+ }
+
+ private saveGridToScreenPosition(tx: TxView): void {
+ tx.screenPosition = this.gridToScreen(tx.gridPosition);
+ }
+
+ // convert grid coordinates to screen coordinates
+ private gridToScreen(position: Square | void): Square {
+ if (position) {
+ const slotSize = (position.s * this.gridSize);
+ const squareSize = slotSize - (this.unitPadding * 2);
+
+ // The grid is laid out notionally left-to-right, bottom-to-top
+ // So we rotate 90deg counterclockwise then flip the y axis
+ //
+ // grid screen
+ // ________ ________ ________
+ // | | | b| | a|
+ // | | rotate | | flip | c |
+ // | c | --> | c | --> | |
+ // |a______b| |_______a| |_______b|
+ return {
+ x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
+ y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
+ s: squareSize
+ };
+ } else {
+ return { x: 0, y: 0, s: 0 };
+ }
+ }
+
+ screenToGrid(position: Position): Position {
+ const grid = {
+ x: Math.floor((position.y - this.unitPadding) / this.gridSize),
+ y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
+ };
+ return grid;
+ }
+
+ // calculates and returns the size of the tx in multiples of the grid size
+ private txSize(tx: TxView): number {
+ const scale = Math.max(1, Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)));
+ return Math.min(this.gridWidth, Math.max(1, scale)); // bound between 1 and the max displayable size (just in case!)
+ }
+
+ private place(tx: TxView): void {
+ const size = this.txSize(tx);
+ this.layout.insert(tx, size);
+ }
+
+ private tryInsertByFee(tx: TxView): boolean {
+ const size = this.txSize(tx);
+ const position = this.layout.tryInsertByFee(tx, size);
+ if (position) {
+ this.txs[tx.txid] = tx;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Add a list of transactions to the layout,
+ // keeping everything approximately sorted by feerate.
+ private placeBatch(txs: TxView[]): void {
+ if (txs.length) {
+ // grab the new tx with the highest fee rate
+ txs = txs.sort(feeRateDescending);
+ const maxSize = 2 * txs.reduce((max, tx) => {
+ return Math.max(this.txSize(tx), max);
+ }, 1);
+
+ // find a reasonable place for it in the layout
+ const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize);
+
+ // extract a sub tree of transactions from the layout, rooted at that point
+ const popped = this.layout.popTree(root.x, root.y, maxSize);
+ // combine those with the new transactions and sort
+ txs = txs.concat(popped);
+ txs = txs.sort(feeRateDescending);
+
+ // insert everything back into the layout
+ txs.forEach(tx => {
+ this.txs[tx.txid] = tx;
+ this.place(tx);
+ });
+ }
+ }
+
+ private removeBatch(ids: string[], startTime: number, direction: string = 'left'): TxView[] {
+ if (!startTime) {
+ startTime = performance.now();
+ }
+ return ids.map(id => {
+ return this.remove(id, startTime, direction);
+ }).filter(tx => tx != null) as TxView[];
+ }
+}
+
+
+class Slot {
+ l: number;
+ r: number;
+ w: number;
+
+ constructor(l: number, r: number) {
+ this.l = l;
+ this.r = r;
+ this.w = r - l;
+ }
+
+ intersects(slot: Slot): boolean {
+ return !((slot.r <= this.l) || (slot.l >= this.r));
+ }
+
+ subtract(slot: Slot): Slot[] | void {
+ if (this.intersects(slot)) {
+ // from middle
+ if (slot.l > this.l && slot.r < this.r) {
+ return [
+ new Slot(this.l, slot.l),
+ new Slot(slot.r, this.r)
+ ];
+ } // totally covered
+ else if (slot.l <= this.l && slot.r >= this.r) {
+ return [];
+ } // from left side
+ else if (slot.l <= this.l) {
+ if (slot.r === this.r) {
+ return [];
+ } else {
+ return [new Slot(slot.r, this.r)];
+ }
+ } // from right side
+ else if (slot.r >= this.r) {
+ if (slot.l === this.l) {
+ return [];
+ } else {
+ return [new Slot(this.l, slot.l)];
+ }
+ }
+ } else {
+ return [this];
+ }
+ }
+}
+
+class TxSlot extends Slot {
+ tx: TxView;
+
+ constructor(l: number, r: number, tx: TxView) {
+ super(l, r);
+ this.tx = tx;
+ }
+}
+
+class Row {
+ y: number;
+ w: number;
+ filled: TxSlot[];
+ slots: Slot[];
+
+
+ constructor(y: number, width: number) {
+ this.y = y;
+ this.w = width;
+ this.filled = [];
+ this.slots = [new Slot(0, this.w)];
+ }
+
+ // insert a transaction w/ given width into row starting at position x
+ insert(x: number, w: number, tx: TxView): void {
+ const newSlot = new TxSlot(x, x + w, tx);
+ // insert into filled list
+ let index = this.filled.findIndex((slot) => (slot.l >= newSlot.r));
+ if (index < 0) {
+ index = this.filled.length;
+ }
+ this.filled.splice(index || 0, 0, newSlot);
+ // subtract from overlapping slots
+ for (let i = 0; i < this.slots.length; i++) {
+ if (newSlot.intersects(this.slots[i])) {
+ const diff = this.slots[i].subtract(newSlot);
+ if (diff) {
+ this.slots.splice(i, 1, ...diff);
+ i += diff.length - 1;
+ }
+ }
+ }
+ }
+
+ remove(x: number, w: number): void {
+ const txIndex = this.filled.findIndex((slot) => (slot.l === x) );
+ this.filled.splice(txIndex, 1);
+
+ const newSlot = new Slot(x, x + w);
+ let slotIndex = this.slots.findIndex((slot) => (slot.l >= newSlot.r) );
+ if (slotIndex < 0) {
+ slotIndex = this.slots.length;
+ }
+ this.slots.splice(slotIndex || 0, 0, newSlot);
+ this.normalize();
+ }
+
+ // merge any contiguous empty slots
+ private normalize(): void {
+ for (let i = 0; i < this.slots.length - 1; i++) {
+ if (this.slots[i].r === this.slots[i + 1].l) {
+ this.slots[i].r = this.slots[i + 1].r;
+ this.slots[i].w += this.slots[i + 1].w;
+ this.slots.splice(i + 1, 1);
+ i--;
+ }
+ }
+ }
+
+ txAt(x: number): TxView | void {
+ let i = 0;
+ while (i < this.filled.length && this.filled[i].l <= x) {
+ if (this.filled[i].l <= x && this.filled[i].r > x) {
+ return this.filled[i].tx;
+ }
+ i++;
+ }
+ }
+
+ getSlotsBetween(left: number, right: number): TxSlot[] {
+ const range = new Slot(left, right);
+ return this.filled.filter(slot => {
+ return slot.intersects(range);
+ });
+ }
+
+ slotAt(x: number): Slot | void {
+ let i = 0;
+ while (i < this.slots.length && this.slots[i].l <= x) {
+ if (this.slots[i].l <= x && this.slots[i].r > x) {
+ return this.slots[i];
+ }
+ i++;
+ }
+ }
+
+ getAvgFeerate(): number {
+ let count = 0;
+ let total = 0;
+ this.filled.forEach(slot => {
+ if (slot.tx) {
+ count += slot.w;
+ total += (slot.tx.feerate * slot.w);
+ }
+ });
+ return total / count;
+ }
+}
+
+class BlockLayout {
+ width: number;
+ height: number;
+ rows: Row[];
+ txPositions: { [key: string]: Square };
+ txs: { [key: string]: TxView };
+
+ constructor({ width, height }: { width: number, height: number }) {
+ this.width = width;
+ this.height = height;
+ this.rows = [new Row(0, this.width)];
+ this.txPositions = {};
+ this.txs = {};
+ }
+
+ getRow(position: Square): Row {
+ return this.rows[position.y];
+ }
+
+ getTx(position: Square): TxView | void {
+ if (this.getRow(position)) {
+ return this.getRow(position).txAt(position.x);
+ }
+ }
+
+ addRow(): void {
+ this.rows.push(new Row(this.rows.length, this.width));
+ }
+
+ remove(tx: TxView) {
+ const position = this.txPositions[tx.txid];
+ if (position) {
+ for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
+ this.rows[y].remove(position.x, position.s);
+ }
+ }
+ delete this.txPositions[tx.txid];
+ delete this.txs[tx.txid];
+ }
+
+ insert(tx: TxView, width: number): Square {
+ const fit = this.fit(tx, width);
+
+ // insert the tx into rows at that position
+ for (let y = fit.y; y < fit.y + width; y++) {
+ if (y >= this.rows.length) {
+ this.addRow();
+ }
+ this.rows[y].insert(fit.x, width, tx);
+ }
+ const position = { x: fit.x, y: fit.y, s: width };
+ this.txPositions[tx.txid] = position;
+ this.txs[tx.txid] = tx;
+ tx.applyGridPosition(position);
+ return position;
+ }
+
+ // Find the first slot large enough to hold a transaction of this size
+ fit(tx: TxView, width: number): Square {
+ let fit;
+ for (let y = 0; y < this.rows.length && !fit; y++) {
+ fit = this.findFit(0, this.width, y, y, width);
+ }
+ // fall back to placing tx in a new row at the top of the layout
+ if (!fit) {
+ fit = { x: 0, y: this.rows.length };
+ }
+ return fit;
+ }
+
+ // recursively check rows to see if there's space for a tx (depth-first)
+ // left/right: initial column boundaries to check
+ // row: current row to check
+ // start: starting row
+ // size: size of space needed
+ findFit(left: number, right: number, row: number, start: number, size: number): Square {
+ if ((row - start) >= size || row >= this.rows.length) {
+ return { x: left, y: start };
+ }
+ for (const slot of this.rows[row].slots) {
+ const l = Math.max(left, slot.l);
+ const r = Math.min(right, slot.r);
+ if (r - l >= size) {
+ const fit = this.findFit(l, r, row + 1, start, size);
+ if (fit) {
+ return fit;
+ }
+ }
+ }
+ }
+
+ // insert only if the tx fits into a fee-appropriate position
+ tryInsertByFee(tx: TxView, size: number): Square | void {
+ const fit = this.fit(tx, size);
+
+ if (this.checkRowFees(fit.y, tx.feerate)) {
+ // insert the tx into rows at that position
+ for (let y = fit.y; y < fit.y + size; y++) {
+ if (y >= this.rows.length) {
+ this.addRow();
+ }
+ this.rows[y].insert(fit.x, size, tx);
+ }
+ const position = { x: fit.x, y: fit.y, s: size };
+ this.txPositions[tx.txid] = position;
+ this.txs[tx.txid] = tx;
+ tx.applyGridPosition(position);
+ return position;
+ }
+ }
+
+ // Return the first slot with a lower feerate
+ getReplacementRoot(feerate: number, width: number): Square {
+ let slot;
+ for (let row = 0; row <= this.rows.length; row++) {
+ if (this.rows[row].slots.length > 0) {
+ return { x: this.rows[row].slots[0].l, y: row };
+ } else {
+ slot = this.rows[row].filled.find(x => {
+ return x.tx.feerate < feerate;
+ });
+ if (slot) {
+ return { x: Math.min(slot.l, this.width - width), y: row };
+ }
+ }
+ }
+ return { x: 0, y: this.rows.length };
+ }
+
+ // remove and return all transactions in a subtree of the layout
+ popTree(x: number, y: number, width: number) {
+ const selected: { [key: string]: TxView } = {};
+ let left = x;
+ let right = x + width;
+ let prevWidth = right - left;
+ let prevFee = Infinity;
+ // scan rows upwards within a channel bounded by 'left' and 'right'
+ for (let row = y; row < this.rows.length; row++) {
+ let rowMax = 0;
+ const slots = this.rows[row].getSlotsBetween(left, right);
+ // check each slot in this row overlapping the search channel
+ slots.forEach(slot => {
+ // select the associated transaction
+ selected[slot.tx.txid] = slot.tx;
+ rowMax = Math.max(rowMax, slot.tx.feerate);
+ // widen the search channel to accommodate this slot if necessary
+ if (slot.w > prevWidth) {
+ left = slot.l;
+ right = slot.r;
+ // if this slot's tx has a higher feerate than the max in the previous row
+ // (i.e. it's out of position)
+ // select all txs overlapping the slot's full width in some rows *below*
+ // to free up space for this tx to sink down to its proper position
+ if (slot.tx.feerate > prevFee) {
+ let count = 0;
+ // keep scanning back down until we find a full row of higher-feerate txs
+ for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
+ const echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r);
+ count = 0;
+ echoSlots.forEach(echoSlot => {
+ selected[echoSlot.tx.txid] = echoSlot.tx;
+ if (echoSlot.tx.feerate >= slot.tx.feerate) {
+ count += echoSlot.w;
+ }
+ });
+ }
+ }
+ }
+ });
+ prevWidth = right - left;
+ prevFee = rowMax;
+ }
+
+ const txList = Object.values(selected);
+
+ txList.forEach(tx => {
+ this.remove(tx);
+ });
+ return txList;
+ }
+
+ // Check if this row has high enough avg fees
+ // for a tx with this feerate to make sense here
+ checkRowFees(row: number, targetFee: number): boolean {
+ // first row is always fine
+ if (row === 0 || !this.rows[row]) {
+ return true;
+ }
+ return (this.rows[row].getAvgFeerate() > (targetFee * 0.9));
+ }
+
+ // drop any free-floating transactions down into empty spaces
+ applyGravity(): void {
+ Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
+ return posA.y - posB.y || posA.x - posB.x;
+ }).forEach(([txid, position]) => {
+ // see how far this transaction can fall
+ let dropTo = position.y;
+ while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
+ dropTo--;
+ }
+ // if it can fall at all
+ if (dropTo < position.y) {
+ // remove and reinsert in the row we found
+ const tx = this.txs[txid];
+ this.remove(tx);
+ this.insert(tx, position.s);
+ }
+ });
+ }
+}
+
+function feeRateDescending(a: TxView, b: TxView) {
+ return b.feerate - a.feerate;
+}
diff --git a/frontend/src/app/components/mempool-block-overview/fast-vertex-array.ts b/frontend/src/app/components/block-overview-graph/fast-vertex-array.ts
similarity index 100%
rename from frontend/src/app/components/mempool-block-overview/fast-vertex-array.ts
rename to frontend/src/app/components/block-overview-graph/fast-vertex-array.ts
diff --git a/frontend/src/app/components/mempool-block-overview/sprite-types.ts b/frontend/src/app/components/block-overview-graph/sprite-types.ts
similarity index 100%
rename from frontend/src/app/components/mempool-block-overview/sprite-types.ts
rename to frontend/src/app/components/block-overview-graph/sprite-types.ts
diff --git a/frontend/src/app/components/mempool-block-overview/tx-sprite.ts b/frontend/src/app/components/block-overview-graph/tx-sprite.ts
similarity index 100%
rename from frontend/src/app/components/mempool-block-overview/tx-sprite.ts
rename to frontend/src/app/components/block-overview-graph/tx-sprite.ts
diff --git a/frontend/src/app/components/mempool-block-overview/tx-view.ts b/frontend/src/app/components/block-overview-graph/tx-view.ts
similarity index 100%
rename from frontend/src/app/components/mempool-block-overview/tx-view.ts
rename to frontend/src/app/components/block-overview-graph/tx-view.ts
diff --git a/frontend/src/app/components/mempool-block-overview/block-scene.ts b/frontend/src/app/components/mempool-block-overview/block-scene.ts
deleted file mode 100644
index 38ca35eda..000000000
--- a/frontend/src/app/components/mempool-block-overview/block-scene.ts
+++ /dev/null
@@ -1,684 +0,0 @@
-import { FastVertexArray } from './fast-vertex-array'
-import TxView from './tx-view'
-import { TransactionStripped } from 'src/app/interfaces/websocket.interface';
-import { Position, Square } from './sprite-types'
-
-export default class BlockScene {
- scene: { count: number, offset: { x: number, y: number}};
- vertexArray: FastVertexArray;
- txs: { [key: string]: TxView };
- width: number;
- height: number;
- gridWidth: number;
- gridHeight: number;
- gridSize: number;
- vbytesPerUnit: number;
- unitPadding: number;
- unitWidth: number;
- initialised: boolean;
- layout: BlockLayout;
- dirty: boolean;
-
- constructor ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }) {
- this.init({ width, height, resolution, blockLimit, vertexArray })
- }
-
- destroy (): void {
- Object.values(this.txs).forEach(tx => tx.destroy())
- }
-
- resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void {
- this.width = width;
- this.height = height;
- this.gridSize = this.width / this.gridWidth;
- this.unitPadding = width / 500;
- this.unitWidth = this.gridSize - (this.unitPadding * 2);
-
- this.dirty = true;
- if (this.initialised && this.scene) {
- this.updateAll(performance.now());
- }
- }
-
- // Animate new block entering scene
- enter (txs: TransactionStripped[], direction) {
- this.replace(txs, direction)
- }
-
- // Animate block leaving scene
- exit (direction: string): void {
- const startTime = performance.now()
- const removed = this.removeBatch(Object.keys(this.txs), startTime, direction)
-
- // clean up sprites
- setTimeout(() => {
- removed.forEach(tx => {
- tx.destroy()
- })
- }, 2000)
- }
-
- // Reset layout and replace with new set of transactions
- replace (txs: TransactionStripped[], direction: string = 'left'): void {
- const startTime = performance.now()
- const nextIds = {}
- const remove = []
- txs.forEach(tx => {
- nextIds[tx.txid] = true
- })
- Object.keys(this.txs).forEach(txid => {
- if (!nextIds[txid]) remove.push(txid)
- })
- txs.forEach(tx => {
- if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
- })
-
- const removed = this.removeBatch(remove, startTime, direction)
-
- // clean up sprites
- setTimeout(() => {
- removed.forEach(tx => {
- tx.destroy()
- })
- }, 1000)
-
- this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
-
- Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
- this.place(tx)
- })
-
- this.updateAll(startTime, direction)
- }
-
- update (add: TransactionStripped[], remove: string[], direction: string = 'left', resetLayout: boolean = false): void {
- const startTime = performance.now()
- const removed = this.removeBatch(remove, startTime, direction)
-
- // clean up sprites
- setTimeout(() => {
- removed.forEach(tx => {
- tx.destroy()
- })
- }, 1000)
-
- if (resetLayout) {
- add.forEach(tx => {
- if (!this.txs[tx.txid]) this.txs[tx.txid] = new TxView(tx, this.vertexArray)
- })
- this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
- Object.values(this.txs).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
- this.place(tx)
- })
- } else {
- // try to insert new txs directly
- const remaining = []
- add.map(tx => new TxView(tx, this.vertexArray)).sort((a,b) => { return b.feerate - a.feerate }).forEach(tx => {
- if (!this.tryInsertByFee(tx)) {
- remaining.push(tx)
- }
- })
- this.placeBatch(remaining)
- this.layout.applyGravity()
- }
-
- this.updateAll(startTime, direction)
- }
-
- //return the tx at this screen position, if any
- getTxAt (position: Position): TxView | void {
- if (this.layout) {
- const gridPosition = this.screenToGrid(position)
- return this.layout.getTx(gridPosition)
- } else return null
- }
-
- private init ({ width, height, resolution, blockLimit, vertexArray }: { width: number, height: number, resolution: number, blockLimit: number, vertexArray: FastVertexArray }): void {
- this.vertexArray = vertexArray
-
- this.scene = {
- count: 0,
- offset: {
- x: 0,
- y: 0
- }
- }
-
- // Set the scale of the visualization (with a 5% margin)
- this.vbytesPerUnit = blockLimit / Math.pow(resolution / 1.05, 2)
- this.gridWidth = resolution
- this.gridHeight = resolution
- this.resize({ width, height })
- this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight })
-
- this.txs = {}
-
- this.initialised = true
- this.dirty = true
- }
-
- private insert (tx: TxView, startTime: number, direction: string = 'left'): void {
- this.txs[tx.txid] = tx
- this.place(tx)
- this.updateTx(tx, startTime, direction)
- }
-
- private updateTx (tx: TxView, startTime: number, direction: string = 'left'): void {
- if (tx.dirty || this.dirty) {
- this.saveGridToScreenPosition(tx)
- this.setTxOnScreen(tx, startTime, direction)
- }
- }
-
- private setTxOnScreen (tx: TxView, startTime: number, direction: string = 'left'): void {
- if (!tx.initialised) {
- const txColor = tx.getColor()
- tx.update({
- display: {
- position: {
- x: tx.screenPosition.x + (direction == 'right' ? -this.width : this.width) * 1.4,
- y: tx.screenPosition.y,
- s: tx.screenPosition.s
- },
- color: txColor,
- },
- start: startTime,
- delay: 0,
- })
- tx.update({
- display: {
- position: tx.screenPosition,
- color: txColor
- },
- duration: 1000,
- start: startTime,
- delay: 50,
- })
- } else {
- tx.update({
- display: {
- position: tx.screenPosition
- },
- duration: 1000,
- minDuration: 500,
- start: startTime,
- delay: 50,
- adjust: true
- })
- }
- }
-
- private updateAll (startTime: number, direction: string = 'left'): void {
- this.scene.count = 0
- const ids = this.getTxList()
- startTime = startTime || performance.now()
- for (let i = 0; i < ids.length; i++) {
- this.updateTx(this.txs[ids[i]], startTime, direction)
- }
- this.dirty = false
- }
-
- private remove (id: string, startTime: number, direction: string = 'left'): TxView | void {
- const tx = this.txs[id]
- if (tx) {
- this.layout.remove(tx)
- tx.update({
- display: {
- position: {
- x: tx.screenPosition.x + (direction == 'right' ? this.width : -this.width) * 1.4,
- y: this.txs[id].screenPosition.y,
- }
- },
- duration: 1000,
- start: startTime,
- delay: 50
- })
- }
- delete this.txs[id]
- return tx
- }
-
- private getTxList (): string[] {
- return Object.keys(this.txs)
- }
-
- private saveGridToScreenPosition (tx: TxView): void {
- tx.screenPosition = this.gridToScreen(tx.gridPosition)
- }
-
- // convert grid coordinates to screen coordinates
- private gridToScreen (position: Square | void): Square {
- if (position) {
- const slotSize = (position.s * this.gridSize)
- const squareSize = slotSize - (this.unitPadding * 2)
-
- // The grid is laid out notionally left-to-right, bottom-to-top
- // So we rotate 90deg counterclockwise then flip the y axis
- //
- // grid screen
- // ________ ________ ________
- // | | | b| | a|
- // | | rotate | | flip | c |
- // | c | --> | c | --> | |
- // |a______b| |_______a| |_______b|
- return {
- x: this.width + (this.unitPadding * 2) - (this.gridSize * position.y) - slotSize,
- y: this.height - ((this.gridSize * position.x) + (slotSize - this.unitPadding)),
- s: squareSize
- }
- } else {
- return { x: 0, y: 0, s: 0 }
- }
- }
-
- screenToGrid (position: Position): Position {
- const grid = {
- x: Math.floor((position.y - this.unitPadding) / this.gridSize),
- y: Math.floor((this.width + (this.unitPadding * 2) - position.x) / this.gridSize)
- }
- return grid
- }
-
- // calculates and returns the size of the tx in multiples of the grid size
- private txSize (tx: TxView): number {
- let scale = Math.max(1,Math.round(Math.sqrt(tx.vsize / this.vbytesPerUnit)))
- return Math.min(this.gridWidth, Math.max(1, scale)) // bound between 1 and the max displayable size (just in case!)
- }
-
- private place (tx: TxView): void {
- const size = this.txSize(tx)
- this.layout.insert(tx, size)
- }
-
- private tryInsertByFee (tx: TxView): boolean {
- const size = this.txSize(tx)
- const position = this.layout.tryInsertByFee(tx, size)
- if (position) {
- this.txs[tx.txid] = tx
- return true
- } else {
- return false
- }
- }
-
- // Add a list of transactions to the layout,
- // keeping everything approximately sorted by feerate.
- private placeBatch (txs: TxView[]): void {
- if (txs.length) {
- // grab the new tx with the highest fee rate
- txs = txs.sort((a,b) => { return b.feerate - a.feerate })
- let i = 0
- let maxSize = txs.reduce((max, tx) => {
- return Math.max(this.txSize(tx), max)
- }, 1) * 2
-
- // find a reasonable place for it in the layout
- const root = this.layout.getReplacementRoot(txs[0].feerate, maxSize)
-
- // extract a sub tree of transactions from the layout, rooted at that point
- const popped = this.layout.popTree(root.x, root.y, maxSize)
- // combine those with the new transactions and sort
- txs = txs.concat(popped)
- txs = txs.sort((a,b) => { return b.feerate - a.feerate })
-
- // insert everything back into the layout
- txs.forEach(tx => {
- this.txs[tx.txid] = tx
- this.place(tx)
- })
- }
- }
-
- private removeBatch (ids: string[], startTime: number, direction: string = 'left'): TxView[] {
- if (!startTime) startTime = performance.now()
- return ids.map(id => {
- return this.remove(id, startTime, direction)
- }).filter(tx => tx != null) as TxView[]
- }
-}
-
-
-class Slot {
- l: number
- r: number
- w: number
-
- constructor (l: number, r: number) {
- this.l = l
- this.r = r
- this.w = r - l
- }
-
- intersects (slot: Slot): boolean {
- return !((slot.r <= this.l) || (slot.l >= this.r))
- }
-
- subtract (slot: Slot): Slot[] | void {
- if (this.intersects(slot)) {
- // from middle
- if (slot.l > this.l && slot.r < this.r) {
- return [
- new Slot(this.l, slot.l),
- new Slot(slot.r, this.r)
- ]
- } // totally covered
- else if (slot.l <= this.l && slot.r >= this.r) {
- return []
- } // from left side
- else if (slot.l <= this.l) {
- if (slot.r == this.r) return []
- else return [new Slot(slot.r, this.r)]
- } // from right side
- else if (slot.r >= this.r) {
- if (slot.l == this.l) return []
- else return [new Slot(this.l, slot.l)]
- }
- } else return [this]
- }
-}
-
-class TxSlot extends Slot {
- tx: TxView
-
- constructor (l: number, r: number, tx: TxView) {
- super(l, r)
- this.tx = tx
- }
-}
-
-class Row {
- y: number
- w: number
- filled: TxSlot[]
- slots: Slot[]
-
-
- constructor (y: number, width: number) {
- this.y = y
- this.w = width
- this.filled = []
- this.slots = [new Slot(0, this.w)]
- }
-
- // insert a transaction w/ given width into row starting at position x
- insert (x: number, w: number, tx: TxView): void {
- const newSlot = new TxSlot(x, x + w, tx)
- // insert into filled list
- let index = this.filled.findIndex((slot) => { return slot.l >= newSlot.r })
- if (index < 0) index = this.filled.length
- this.filled.splice(index || 0, 0, newSlot)
- // subtract from overlapping slots
- for (let i = 0; i < this.slots.length; i++) {
- if (newSlot.intersects(this.slots[i])) {
- const diff = this.slots[i].subtract(newSlot)
- if (diff) {
- this.slots.splice(i, 1, ...diff)
- i += diff.length - 1
- }
- }
- }
- }
-
- remove (x: number, w: number): void {
- const txIndex = this.filled.findIndex((slot) => { return slot.l == x })
- this.filled.splice(txIndex, 1)
-
- const newSlot = new Slot(x, x + w)
- let slotIndex = this.slots.findIndex((slot) => { return slot.l >= newSlot.r })
- if (slotIndex < 0) slotIndex = this.slots.length
- this.slots.splice(slotIndex || 0, 0, newSlot)
- this.normalize()
- }
-
- // merge any contiguous empty slots
- private normalize (): void {
- for (let i = 0; i < this.slots.length - 1; i++) {
- if (this.slots[i].r == this.slots[i+1].l) {
- this.slots[i].r = this.slots[i+1].r
- this.slots[i].w += this.slots[i+1].w
- this.slots.splice(i+1, 1)
- i--
- }
- }
- }
-
- txAt (x: number): TxView | void {
- let i = 0
- while (i < this.filled.length && this.filled[i].l <= x) {
- if (this.filled[i].l <= x && this.filled[i].r > x) return this.filled[i].tx
- i++
- }
- }
-
- getSlotsBetween (left: number, right: number): TxSlot[] {
- const range = new Slot(left, right)
- return this.filled.filter(slot => {
- return slot.intersects(range)
- })
- }
-
- slotAt (x: number): Slot | void {
- let i = 0
- while (i < this.slots.length && this.slots[i].l <= x) {
- if (this.slots[i].l <= x && this.slots[i].r > x) return this.slots[i]
- i++
- }
- }
-
- getAvgFeerate (): number {
- let count = 0
- let total = 0
- this.filled.forEach(slot => {
- if (slot.tx) {
- count += slot.w
- total += (slot.tx.feerate * slot.w)
- }
- })
- return total / count
- }
-}
-
-class BlockLayout {
- width: number;
- height: number;
- rows: Row[];
- txPositions: { [key: string]: Square }
- txs: { [key: string]: TxView }
-
- constructor ({ width, height } : { width: number, height: number }) {
- this.width = width
- this.height = height
- this.rows = [new Row(0, this.width)]
- this.txPositions = {}
- this.txs = {}
- }
-
- getRow (position: Square): Row {
- return this.rows[position.y]
- }
-
- getTx (position: Square): TxView | void {
- if (this.getRow(position)) {
- return this.getRow(position).txAt(position.x)
- }
- }
-
- addRow (): void {
- this.rows.push(new Row(this.rows.length, this.width))
- }
-
- remove (tx: TxView) {
- const position = this.txPositions[tx.txid]
- if (position) {
- for (let y = position.y; y < position.y + position.s && y < this.rows.length; y++) {
- this.rows[y].remove(position.x, position.s)
- }
- }
- delete this.txPositions[tx.txid]
- delete this.txs[tx.txid]
- }
-
- insert (tx: TxView, width: number): Square {
- const fit = this.fit(tx, width)
-
- // insert the tx into rows at that position
- for (let y = fit.y; y < fit.y + width; y++) {
- if (y >= this.rows.length) this.addRow()
- this.rows[y].insert(fit.x, width, tx)
- }
- const position = { x: fit.x, y: fit.y, s: width }
- this.txPositions[tx.txid] = position
- this.txs[tx.txid] = tx
- tx.applyGridPosition(position)
- return position
- }
-
- // Find the first slot large enough to hold a transaction of this size
- fit (tx: TxView, width: number): Square {
- let fit
- for (let y = 0; y < this.rows.length && !fit; y++) {
- fit = this.findFit(0, this.width, y, y, width)
- }
- // fall back to placing tx in a new row at the top of the layout
- if (!fit) {
- fit = { x: 0, y: this.rows.length }
- }
- return fit
- }
-
- // recursively check rows to see if there's space for a tx (depth-first)
- // left/right: initial column boundaries to check
- // row: current row to check
- // start: starting row
- // size: size of space needed
- findFit (left: number, right: number, row: number, start: number, size: number) : Square {
- if ((row - start) >= size || row >= this.rows.length) {
- return { x: left, y: start }
- }
- for (let i = 0; i < this.rows[row].slots.length; i++) {
- const slot = this.rows[row].slots[i]
- const l = Math.max(left, slot.l)
- const r = Math.min(right, slot.r)
- if (r - l >= size) {
- const fit = this.findFit(l, r, row + 1, start, size)
- if (fit) return fit
- }
- }
- }
-
- // insert only if the tx fits into a fee-appropriate position
- tryInsertByFee (tx: TxView, size: number): Square | void {
- const fit = this.fit(tx, size)
-
- if (this.checkRowFees(fit.y, tx.feerate)) {
- // insert the tx into rows at that position
- for (let y = fit.y; y < fit.y + size; y++) {
- if (y >= this.rows.length) this.addRow()
- this.rows[y].insert(fit.x, size, tx)
- }
- const position = { x: fit.x, y: fit.y, s: size }
- this.txPositions[tx.txid] = position
- this.txs[tx.txid] = tx
- tx.applyGridPosition(position)
- return position
- }
- }
-
- // Return the first slot with a lower feerate
- getReplacementRoot (feerate: number, width: number): Square {
- let slot
- for (let row = 0; row <= this.rows.length; row++) {
- if (this.rows[row].slots.length > 0) {
- return { x: this.rows[row].slots[0].l, y: row }
- } else {
- slot = this.rows[row].filled.find(x => {
- return x.tx.feerate < feerate
- })
- if (slot) {
- return { x: Math.min(slot.l, this.width - width), y: row }
- }
- }
- }
- return { x: 0, y: this.rows.length }
- }
-
- // remove and return all transactions in a subtree of the layout
- popTree (x: number, y: number, width: number) {
- const selected: { [key: string]: TxView } = {}
- let left = x
- let right = x + width
- let prevWidth = right - left
- let prevFee = Infinity
- // scan rows upwards within a channel bounded by 'left' and 'right'
- for (let row = y; row < this.rows.length; row++) {
- let rowMax = 0
- let slots = this.rows[row].getSlotsBetween(left, right)
- // check each slot in this row overlapping the search channel
- slots.forEach(slot => {
- // select the associated transaction
- selected[slot.tx.txid] = slot.tx
- rowMax = Math.max(rowMax, slot.tx.feerate)
- // widen the search channel to accommodate this slot if necessary
- if (slot.w > prevWidth) {
- left = slot.l
- right = slot.r
- // if this slot's tx has a higher feerate than the max in the previous row
- // (i.e. it's out of position)
- // select all txs overlapping the slot's full width in some rows *below*
- // to free up space for this tx to sink down to its proper position
- if (slot.tx.feerate > prevFee) {
- let count = 0
- // keep scanning back down until we find a full row of higher-feerate txs
- for (let echo = row - 1; echo >= 0 && count < slot.w; echo--) {
- let echoSlots = this.rows[echo].getSlotsBetween(slot.l, slot.r)
- count = 0
- echoSlots.forEach(echoSlot => {
- selected[echoSlot.tx.txid] = echoSlot.tx
- if (echoSlot.tx.feerate >= slot.tx.feerate) {
- count += echoSlot.w
- }
- })
- }
- }
- }
- })
- prevWidth = right - left
- prevFee = rowMax
- }
-
- const txList = Object.values(selected)
-
- txList.forEach(tx => {
- this.remove(tx)
- })
- return txList
- }
-
- // Check if this row has high enough avg fees
- // for a tx with this feerate to make sense here
- checkRowFees (row: number, targetFee: number): boolean {
- // first row is always fine
- if (row == 0 || !this.rows[row]) return true
- return (this.rows[row].getAvgFeerate() > (targetFee * 0.9))
- }
-
- // drop any free-floating transactions down into empty spaces
- applyGravity (): void {
- Object.entries(this.txPositions).sort(([keyA, posA], [keyB, posB]) => {
- return posA.y - posB.y || posA.x - posB.x
- }).forEach(([txid, position]) => {
- // see how far this transaction can fall
- let dropTo = position.y
- while (dropTo > 0 && !this.rows[dropTo - 1].getSlotsBetween(position.x, position.x + position.s).length) {
- dropTo--;
- }
- // if it can fall at all
- if (dropTo < position.y) {
- // remove and reinsert in the row we found
- const tx = this.txs[txid]
- this.remove(tx)
- this.insert(tx, position.s)
- }
- })
- }
-}
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
index 58f09548f..c9696f222 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.html
@@ -1,6 +1,7 @@
-
+
+
diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
index ab9ffb3b5..aed949af3 100644
--- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
+++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts
@@ -1,40 +1,23 @@
-import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, OnInit,
- OnDestroy, OnChanges, ChangeDetectionStrategy, NgZone, AfterViewInit } from '@angular/core';
+import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter,
+ OnDestroy, OnChanges, ChangeDetectionStrategy, AfterViewInit } from '@angular/core';
import { StateService } from 'src/app/services/state.service';
import { MempoolBlockDelta, TransactionStripped } from 'src/app/interfaces/websocket.interface';
+import { BlockOverviewGraphComponent } from 'src/app/components/block-overview-graph/block-overview-graph.component';
import { Subscription, BehaviorSubject, merge, of } from 'rxjs';
import { switchMap, filter } from 'rxjs/operators';
import { WebsocketService } from 'src/app/services/websocket.service';
-import { FastVertexArray } from './fast-vertex-array';
-import BlockScene from './block-scene';
-import TxSprite from './tx-sprite';
-import TxView from './tx-view';
@Component({
selector: 'app-mempool-block-overview',
templateUrl: './mempool-block-overview.component.html',
- styleUrls: ['./mempool-block-overview.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChanges, AfterViewInit {
+export class MempoolBlockOverviewComponent implements OnDestroy, OnChanges, AfterViewInit {
@Input() index: number;
@Output() txPreviewEvent = new EventEmitter();
- @ViewChild('blockCanvas')
- canvas: ElementRef;
+ @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
- gl: WebGLRenderingContext;
- animationFrameRequest: number;
- displayWidth: number;
- displayHeight: number;
- cssWidth: number;
- cssHeight: number;
- shaderProgram: WebGLProgram;
- vertexArray: FastVertexArray;
- running: boolean;
- scene: BlockScene;
- hoverTx: TxView | void;
- selectedTx: TxView | void;
lastBlockHeight: number;
blockIndex: number;
isLoading$ = new BehaviorSubject(true);
@@ -44,13 +27,10 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
constructor(
public stateService: StateService,
- private websocketService: WebsocketService,
- readonly ngZone: NgZone,
- ) {
- this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
- }
+ private websocketService: WebsocketService
+ ) { }
- ngOnInit(): void {
+ ngAfterViewInit(): void {
this.blockSub = merge(
of(true),
this.stateService.connectionState$.pipe(filter((state) => state === 2))
@@ -64,18 +44,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
});
}
- ngAfterViewInit(): void {
- this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
- this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
- this.gl = this.canvas.nativeElement.getContext('webgl');
- this.initCanvas();
-
- this.resizeCanvas();
- }
-
ngOnChanges(changes): void {
if (changes.index) {
- this.clearBlock(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
+ if (this.blockGraph) {
+ this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? 'right' : 'left');
+ }
this.isLoading$.next(true);
this.websocketService.startTrackMempoolBlock(changes.index.currentValue);
}
@@ -87,26 +60,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
this.websocketService.stopTrackMempoolBlock();
}
- clearBlock(direction): void {
- if (this.scene) {
- this.scene.exit(direction);
- }
- this.hoverTx = null;
- this.selectedTx = null;
- this.txPreviewEvent.emit(null);
- }
-
replaceBlock(transactionsStripped: TransactionStripped[]): void {
- if (!this.scene) {
- this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
- blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
- }
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
- this.scene.enter(transactionsStripped, direction);
+ this.blockGraph.enter(transactionsStripped, direction);
} else {
- this.scene.replace(transactionsStripped, blockMined ? 'right' : 'left');
+ this.blockGraph.replace(transactionsStripped, blockMined ? 'right' : 'left');
}
this.lastBlockHeight = this.stateService.latestBlockHeight;
@@ -115,20 +75,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
}
updateBlock(delta: MempoolBlockDelta): void {
- if (!this.scene) {
- this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
- blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
- }
const blockMined = (this.stateService.latestBlockHeight > this.lastBlockHeight);
if (this.blockIndex !== this.index) {
const direction = (this.blockIndex == null || this.index < this.blockIndex) ? 'left' : 'right';
- this.scene.exit(direction);
- this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: 75,
- blockLimit: this.stateService.blockVSize, vertexArray: this.vertexArray });
- this.scene.enter(delta.added, direction);
+ this.blockGraph.replace(delta.added, direction);
} else {
- this.scene.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
+ this.blockGraph.update(delta.added, delta.removed, blockMined ? 'right' : 'left', blockMined);
}
this.lastBlockHeight = this.stateService.latestBlockHeight;
@@ -136,279 +89,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang
this.isLoading$.next(false);
}
- initCanvas(): void {
- this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
- this.gl.clear(this.gl.COLOR_BUFFER_BIT);
-
- const shaderSet = [
- {
- type: this.gl.VERTEX_SHADER,
- src: vertShaderSrc
- },
- {
- type: this.gl.FRAGMENT_SHADER,
- src: fragShaderSrc
- }
- ];
-
- this.shaderProgram = this.buildShaderProgram(shaderSet);
-
- this.gl.useProgram(this.shaderProgram);
-
- // Set up alpha blending
- this.gl.enable(this.gl.BLEND);
- this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
-
- const glBuffer = this.gl.createBuffer();
- this.gl.bindBuffer(this.gl.ARRAY_BUFFER, glBuffer);
-
- /* SET UP SHADER ATTRIBUTES */
- Object.keys(attribs).forEach((key, i) => {
- attribs[key].pointer = this.gl.getAttribLocation(this.shaderProgram, key);
- this.gl.enableVertexAttribArray(attribs[key].pointer);
- });
-
- this.start();
- }
-
- handleContextLost(event): void {
- event.preventDefault();
- cancelAnimationFrame(this.animationFrameRequest);
- this.animationFrameRequest = null;
- this.running = false;
- }
-
- handleContextRestored(event): void {
- this.initCanvas();
- }
-
- @HostListener('window:resize', ['$event'])
- resizeCanvas(): void {
- this.cssWidth = this.canvas.nativeElement.parentElement.clientWidth;
- this.cssHeight = this.canvas.nativeElement.parentElement.clientHeight;
- this.displayWidth = window.devicePixelRatio * this.cssWidth;
- this.displayHeight = window.devicePixelRatio * this.cssHeight;
- this.canvas.nativeElement.width = this.displayWidth;
- this.canvas.nativeElement.height = this.displayHeight;
- if (this.gl) {
- this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
- }
- if (this.scene) {
- this.scene.resize({ width: this.displayWidth, height: this.displayHeight });
- }
- }
-
- compileShader(src, type): WebGLShader {
- const shader = this.gl.createShader(type);
-
- this.gl.shaderSource(shader, src);
- this.gl.compileShader(shader);
-
- if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
- console.log(`Error compiling ${type === this.gl.VERTEX_SHADER ? 'vertex' : 'fragment'} shader:`);
- console.log(this.gl.getShaderInfoLog(shader));
- }
- return shader;
- }
-
- buildShaderProgram(shaderInfo): WebGLProgram {
- const program = this.gl.createProgram();
-
- shaderInfo.forEach((desc) => {
- const shader = this.compileShader(desc.src, desc.type);
- if (shader) {
- this.gl.attachShader(program, shader);
- }
- });
-
- this.gl.linkProgram(program);
-
- if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
- console.log('Error linking shader program:');
- console.log(this.gl.getProgramInfoLog(program));
- }
-
- return program;
- }
-
- start(): void {
- this.running = true;
- this.ngZone.runOutsideAngular(() => this.run());
- }
-
- run(now?: DOMHighResTimeStamp): void {
- if (!now) {
- now = performance.now();
- }
-
- /* SET UP SHADER UNIFORMS */
- // screen dimensions
- this.gl.uniform2f(this.gl.getUniformLocation(this.shaderProgram, 'screenSize'), this.displayWidth, this.displayHeight);
- // frame timestamp
- this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, 'now'), now);
-
- /* SET UP SHADER ATTRIBUTES */
- Object.keys(attribs).forEach((key, i) => {
- this.gl.vertexAttribPointer(attribs[key].pointer,
- attribs[key].count, // number of primitives in this attribute
- this.gl[attribs[key].type], // type of primitive in this attribute (e.g. gl.FLOAT)
- false, // never normalised
- stride, // distance between values of the same attribute
- attribs[key].offset); // offset of the first value
- });
-
- const pointArray = this.vertexArray.getVertexData();
-
- if (pointArray.length) {
- this.gl.bufferData(this.gl.ARRAY_BUFFER, pointArray, this.gl.DYNAMIC_DRAW);
- this.gl.drawArrays(this.gl.TRIANGLES, 0, pointArray.length / TxSprite.vertexSize);
- }
-
- /* LOOP */
- if (this.running) {
- if (this.animationFrameRequest) {
- cancelAnimationFrame(this.animationFrameRequest);
- this.animationFrameRequest = null;
- }
- this.animationFrameRequest = requestAnimationFrame(() => this.run());
- }
- }
-
- @HostListener('click', ['$event'])
- onClick(event) {
- this.setPreviewTx(event.offsetX, event.offsetY, true);
- }
-
- @HostListener('pointermove', ['$event'])
- onPointerMove(event) {
- this.setPreviewTx(event.offsetX, event.offsetY, false);
- }
-
- @HostListener('pointerleave', ['$event'])
- onPointerLeave(event) {
- this.setPreviewTx(-1, -1, false);
- }
-
- setPreviewTx(cssX: number, cssY: number, clicked: boolean = false) {
- const x = cssX * window.devicePixelRatio;
- const y = cssY * window.devicePixelRatio;
- if (this.scene && (!this.selectedTx || clicked)) {
- const selected = this.scene.getTxAt({ x, y });
- const currentPreview = this.selectedTx || this.hoverTx;
-
- if (selected !== currentPreview) {
- if (currentPreview) {
- currentPreview.setHover(false);
- }
- if (selected) {
- selected.setHover(true);
- this.txPreviewEvent.emit({
- txid: selected.txid,
- fee: selected.fee,
- vsize: selected.vsize,
- value: selected.value
- });
- if (clicked) {
- this.selectedTx = selected;
- } else {
- this.hoverTx = selected;
- }
- } else {
- if (clicked) {
- this.selectedTx = null;
- }
- this.hoverTx = null;
- this.txPreviewEvent.emit(null);
- }
- } else if (clicked) {
- if (selected === this.selectedTx) {
- this.hoverTx = this.selectedTx;
- this.selectedTx = null;
- } else {
- this.selectedTx = selected;
- }
- }
- }
+ onTxPreview(event: TransactionStripped | void): void {
+ this.txPreviewEvent.emit(event);
}
}
-
-// WebGL shader attributes
-const attribs = {
- offset: { type: 'FLOAT', count: 2, pointer: null, offset: 0 },
- posX: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
- posY: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
- posR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
- colR: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
- colG: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
- colB: { type: 'FLOAT', count: 4, pointer: null, offset: 0 },
- colA: { type: 'FLOAT', count: 4, pointer: null, offset: 0 }
-};
-// Calculate the number of bytes per vertex based on specified attributes
-const stride = Object.values(attribs).reduce((total, attrib) => {
- return total + (attrib.count * 4);
-}, 0);
-// Calculate vertex attribute offsets
-for (let i = 0, offset = 0; i < Object.keys(attribs).length; i++) {
- const attrib = Object.values(attribs)[i];
- attrib.offset = offset;
- offset += (attrib.count * 4);
-}
-
-const vertShaderSrc = `
-varying lowp vec4 vColor;
-
-// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
-// shader interpolates between start and end values at the given rate, from the given time
-
-attribute vec2 offset;
-attribute vec4 posX;
-attribute vec4 posY;
-attribute vec4 posR;
-attribute vec4 colR;
-attribute vec4 colG;
-attribute vec4 colB;
-attribute vec4 colA;
-
-uniform vec2 screenSize;
-uniform float now;
-
-float smootherstep(float x) {
- x = clamp(x, 0.0, 1.0);
- float ix = 1.0 - x;
- x = x * x;
- return x / (x + ix * ix);
-}
-
-float interpolateAttribute(vec4 attr) {
- float d = (now - attr.z) * attr.w;
- float delta = smootherstep(d);
- return mix(attr.x, attr.y, delta);
-}
-
-void main() {
- vec4 screenTransform = vec4(2.0 / screenSize.x, 2.0 / screenSize.y, -1.0, -1.0);
- // vec4 screenTransform = vec4(1.0 / screenSize.x, 1.0 / screenSize.y, -0.5, -0.5);
-
- float radius = interpolateAttribute(posR);
- vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY)) + (radius * offset);
-
- gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
-
- float red = interpolateAttribute(colR);
- float green = interpolateAttribute(colG);
- float blue = interpolateAttribute(colB);
- float alpha = interpolateAttribute(colA);
-
- vColor = vec4(red, green, blue, alpha);
-}
-`;
-
-const fragShaderSrc = `
-varying lowp vec4 vColor;
-
-void main() {
- gl_FragColor = vColor;
- // premultiply alpha
- gl_FragColor.rgb *= gl_FragColor.a;
-}
-`;
diff --git a/frontend/src/app/components/mempool-block/mempool-block.component.ts b/frontend/src/app/components/mempool-block/mempool-block.component.ts
index e20c0b67d..75e171f2b 100644
--- a/frontend/src/app/components/mempool-block/mempool-block.component.ts
+++ b/frontend/src/app/components/mempool-block/mempool-block.component.ts
@@ -81,12 +81,12 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
}
setTxPreview(event: TransactionStripped | void): void {
- this.previewTx = event
+ this.previewTx = event;
}
}
-function detectWebGL () {
- const canvas = document.createElement("canvas");
- const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
- return (gl && gl instanceof WebGLRenderingContext)
+function detectWebGL() {
+ const canvas = document.createElement('canvas');
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+ return (gl && gl instanceof WebGLRenderingContext);
}
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 8b280e9c8..5886be5d3 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -45,6 +45,7 @@ import { StartComponent } from '../components/start/start.component';
import { TransactionComponent } from '../components/transaction/transaction.component';
import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component';
import { BlockComponent } from '../components/block/block.component';
+import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
import { AddressComponent } from '../components/address/address.component';
import { SearchFormComponent } from '../components/search-form/search-form.component';
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
@@ -110,6 +111,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
StartComponent,
TransactionComponent,
BlockComponent,
+ BlockOverviewGraphComponent,
TransactionsListComponent,
AddressComponent,
SearchFormComponent,
@@ -203,6 +205,7 @@ import { SvgImagesComponent } from '../components/svg-images/svg-images.componen
StartComponent,
TransactionComponent,
BlockComponent,
+ BlockOverviewGraphComponent,
TransactionsListComponent,
AddressComponent,
SearchFormComponent,