diff --git a/frontend/src/app/components/block-filters/block-filters.component.html b/frontend/src/app/components/block-filters/block-filters.component.html index 8c79cd438..83988d5cc 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.html +++ b/frontend/src/app/components/block-filters/block-filters.component.html @@ -14,14 +14,29 @@
-
Match
-
- - +
+
+
Match
+
+ + +
+
+
+
Gradient
+
+ + +
+
{{ group.label }}
diff --git a/frontend/src/app/components/block-filters/block-filters.component.scss b/frontend/src/app/components/block-filters/block-filters.component.scss index 1009efd72..b1c4bce17 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.scss +++ b/frontend/src/app/components/block-filters/block-filters.component.scss @@ -45,6 +45,13 @@ } .filter-menu { + .filter-row { + display: flex; + flex-direction: row; + justify-content: start; + align-items: baseline; + } + h5 { font-size: 0.8rem; color: white; @@ -118,6 +125,12 @@ background: #1a9436; } } + &.yellow { + border: solid 1px #bf7815; + &.active { + background: #bf7815; + } + } } :host-context(.block-overview-graph:hover) &, &:hover, &:active { diff --git a/frontend/src/app/components/block-filters/block-filters.component.ts b/frontend/src/app/components/block-filters/block-filters.component.ts index a16475c23..7f997617c 100644 --- a/frontend/src/app/components/block-filters/block-filters.component.ts +++ b/frontend/src/app/components/block-filters/block-filters.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; -import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils'; +import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils'; import { StateService } from '../../services/state.service'; import { Subscription } from 'rxjs'; @@ -22,6 +22,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { activeFilters: string[] = []; filterFlags: { [key: string]: boolean } = {}; filterMode: FilterMode = 'and'; + gradientMode: GradientMode = 'fee'; menuOpen: boolean = false; constructor( @@ -32,6 +33,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { ngOnInit(): void { this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => { this.filterMode = active.mode; + this.gradientMode = active.gradient; for (const key of Object.keys(this.filterFlags)) { this.filterFlags[key] = false; } @@ -39,7 +41,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { this.filterFlags[key] = !this.disabledFilters[key]; } this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])]; - this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters }); + this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters, gradient: this.gradientMode }); }); } @@ -57,8 +59,14 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { setFilterMode(mode): void { this.filterMode = mode; - this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); - this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); + this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode }); + this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode }); + } + + setGradientMode(mode): void { + this.gradientMode = mode; + this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode }); + this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode }); } toggleFilter(key): void { @@ -81,8 +89,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy { this.activeFilters = this.activeFilters.filter(f => f != key); } const booleanFlags = this.getBooleanFlags(); - this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); - this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); + this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode }); + this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode }); } getBooleanFlags(): bigint | null { 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 index 003531fce..018a0c85a 100644 --- 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 @@ -8,14 +8,11 @@ import { Color, Position } from './sprite-types'; import { Price } from '../../services/price.service'; import { StateService } from '../../services/state.service'; import { Subscription } from 'rxjs'; -import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; +import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction } from './utils'; import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; import { detectWebGL } from '../../shared/graphs.utils'; const unmatchedOpacity = 0.2; -const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity)); -const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity)); -const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity)); const unmatchedAuditColors = { censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), @@ -46,6 +43,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On @Input() excludeFilters: string[] = []; @Input() filterFlags: bigint | null = null; @Input() filterMode: FilterMode = 'and'; + @Input() gradientMode: 'fee' | 'age' = 'fee'; @Input() relativeTime: number | null; @Input() blockConversion: Price; @Input() overrideColors: ((tx: TxView) => Color) | null = null; @@ -121,21 +119,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.setHighlightingEnabled(this.auditHighlighting); } if (changes.overrideColor && this.scene) { - this.scene.setColorFunction(this.overrideColors); + this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode)); } - if ((changes.filterFlags || changes.showFilters || changes.filterMode)) { + if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) { this.setFilterFlags(); } } setFilterFlags(goggle?: ActiveFilter): void { this.filterMode = goggle?.mode || this.filterMode; + this.gradientMode = goggle?.gradient || this.gradientMode; this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags; if (this.scene) { if (this.activeFilterFlags != null && this.filtersAvailable) { - this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); + this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode)); } else { - this.scene.setColorFunction(this.overrideColors); + this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode)); } } this.start(); @@ -212,6 +211,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On remove = remove.filter(txid => this.scene.txs[txid]); change = change.filter(tx => this.scene.txs[tx.txid]); + if (this.gradientMode === 'age') { + this.scene.updateAllColors(); + } this.scene.update(add, remove, change, direction, resetLayout); this.start(); this.updateSearchHighlight(); @@ -548,25 +550,24 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On getColorFunction(): ((tx: TxView) => Color) { if (this.filterFlags) { - return this.getFilterColorFunction(this.filterFlags); + return this.getFilterColorFunction(this.filterFlags, this.gradientMode); } else if (this.activeFilterFlags) { - return this.getFilterColorFunction(this.activeFilterFlags); + return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode); } else { - return this.overrideColors; + return this.getFilterColorFunction(0n, this.gradientMode); } } - getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { + getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) { return (tx: TxView) => { if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { - return defaultColorFunction(tx); + return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)); } else { - return defaultColorFunction( + return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction( tx, - unmatchedFeeColors, - unmatchedAuditFeeColors, - unmatchedMarginalFeeColors, - unmatchedAuditColors + defaultColors.unmatchedfee, + unmatchedAuditColors, + this.relativeTime || (Date.now() / 1000) ); } }; diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index 5d2196f1e..fb45e492b 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -68,6 +68,10 @@ export default class BlockScene { setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { this.getColor = colorFunction || defaultColorFunction; + this.updateAllColors(); + } + + updateAllColors(): void { this.dirty = true; if (this.initialised && this.scene) { this.updateColors(performance.now(), 50); diff --git a/frontend/src/app/components/block-overview-graph/utils.ts b/frontend/src/app/components/block-overview-graph/utils.ts index b6c8ccf5e..568cd5ad6 100644 --- a/frontend/src/app/components/block-overview-graph/utils.ts +++ b/frontend/src/app/components/block-overview-graph/utils.ts @@ -37,10 +37,36 @@ export function setOpacity(color: Color, opacity: number): Color { }; } +interface ColorPalette { + base: Color[], + audit: Color[], + marginal: Color[], + baseLevel: (tx: TxView, rate: number, time: number) => number, +} + // precomputed colors -export const defaultFeeColors = mempoolFeeColors.map(hexToColor); -export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); -export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); +const defaultColors: { [key: string]: ColorPalette } = { + fee: { + base: mempoolFeeColors.map(hexToColor), + audit: [], + marginal: [], + baseLevel: (tx: TxView, rate: number) => feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1 + }, +} +for (const key in defaultColors) { + const base = defaultColors[key].base; + defaultColors[key].audit = base.map((color) => darken(desaturate(color, 0.3), 0.9)); + defaultColors[key].marginal = base.map((color) => darken(desaturate(color, 0.8), 1.1)); + defaultColors['unmatched' + key] = { + base: defaultColors[key].base.map(c => setOpacity(c, 0.2)), + audit: defaultColors[key].audit.map(c => setOpacity(c, 0.2)), + marginal: defaultColors[key].marginal.map(c => setOpacity(c, 0.2)), + baseLevel: defaultColors[key].baseLevel, + }; +} + +export { defaultColors as defaultColors }; + export const defaultAuditColors = { censored: hexToColor('f344df'), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), @@ -51,22 +77,21 @@ export const defaultAuditColors = { export function defaultColorFunction( tx: TxView, - feeColors: Color[] = defaultFeeColors, - auditFeeColors: Color[] = defaultAuditFeeColors, - marginalFeeColors: Color[] = defaultMarginalFeeColors, - auditColors: { [status: string]: Color } = defaultAuditColors + colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee, + auditColors: { [status: string]: Color } = defaultAuditColors, + relativeTime?: number, ): Color { const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate - const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; - const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; + const levelIndex = colors.baseLevel(tx, rate, relativeTime || (Date.now() / 1000)); + const levelColor = colors.base[levelIndex] || colors.base[mempoolFeeColors.length - 1]; // Normal mode if (!tx.scene?.highlightingEnabled) { if (tx.acc) { return auditColors.accelerated; } else { - return feeLevelColor; + return levelColor; } - return feeLevelColor; + return levelColor; } // Block audit switch(tx.status) { @@ -75,7 +100,7 @@ export function defaultColorFunction( case 'missing': case 'sigop': case 'rbf': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + return colors.marginal[levelIndex] || colors.marginal[mempoolFeeColors.length - 1]; case 'fresh': case 'freshcpfp': return auditColors.missing; @@ -84,20 +109,37 @@ export function defaultColorFunction( case 'prioritized': return auditColors.prioritized; case 'selected': - return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; + return colors.marginal[levelIndex] || colors.marginal[mempoolFeeColors.length - 1]; case 'accelerated': return auditColors.accelerated; case 'found': if (tx.context === 'projected') { - return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; + return colors.audit[levelIndex] || colors.audit[mempoolFeeColors.length - 1]; } else { - return feeLevelColor; + return levelColor; } default: if (tx.acc) { return auditColors.accelerated; } else { - return feeLevelColor; + return levelColor; } } +} + +export function ageColorFunction( + tx: TxView, + colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee, + auditColors: { [status: string]: Color } = defaultAuditColors, + relativeTime?: number, +): Color { + const color = defaultColorFunction(tx, colors, auditColors, relativeTime); + + const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60))))))); + return { + r: color.r, + g: color.g, + b: color.b, + a: color.a * (1 - ageLevel) + }; } \ No newline at end of file diff --git a/frontend/src/app/dashboard/dashboard.component.ts b/frontend/src/app/dashboard/dashboard.component.ts index f396ba6ae..5f0eea62d 100644 --- a/frontend/src/app/dashboard/dashboard.component.ts +++ b/frontend/src/app/dashboard/dashboard.component.ts @@ -7,7 +7,7 @@ import { ApiService } from '../services/api.service'; import { StateService } from '../services/state.service'; import { WebsocketService } from '../services/websocket.service'; import { SeoService } from '../services/seo.service'; -import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils'; +import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../shared/filters.utils'; import { detectWebGL } from '../shared/graphs.utils'; interface MempoolBlocksData { @@ -74,14 +74,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { private lastReservesBlockUpdate: number = 0; goggleResolution = 82; - goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [ - { index: 0, name: 'All', mode: 'and', filters: [] }, - { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] }, - { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] }, - { index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] }, + goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [ + { index: 0, name: 'All', mode: 'and', filters: [], gradient: 'fee' }, + { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'], gradient: 'fee' }, + { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'], gradient: 'fee' }, + { index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' }, ]; goggleFlags = 0n; goggleMode: FilterMode = 'and'; + gradientMode: GradientMode = 'fee'; goggleIndex = 0; private destroy$ = new Subject(); @@ -131,6 +132,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { this.goggleIndex = goggle.index; this.goggleFlags = toFlags(goggle.filters); this.goggleMode = goggle.mode; + this.gradientMode = goggle.gradient; return; } } @@ -140,6 +142,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit { name: 'Custom', mode: active.mode, filters: active.filters, + gradient: active.gradient, }); this.goggleIndex = this.goggleCycle.length - 1; this.goggleFlags = toFlags(active.filters); diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 12caf9f53..4ea574c8e 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -154,7 +154,7 @@ export class StateService { searchFocus$: Subject = new Subject(); menuOpen$: BehaviorSubject = new BehaviorSubject(false); - activeGoggles$: BehaviorSubject = new BehaviorSubject({ mode: 'and', filters: [] }); + activeGoggles$: BehaviorSubject = new BehaviorSubject({ mode: 'and', filters: [], gradient: 'fee' }); constructor( @Inject(PLATFORM_ID) private platformId: any, diff --git a/frontend/src/app/shared/filters.utils.ts b/frontend/src/app/shared/filters.utils.ts index ab99e00ce..5df6c4818 100644 --- a/frontend/src/app/shared/filters.utils.ts +++ b/frontend/src/app/shared/filters.utils.ts @@ -11,9 +11,12 @@ export interface Filter { export type FilterMode = 'and' | 'or'; +export type GradientMode = 'fee' | 'age'; + export interface ActiveFilter { mode: FilterMode, filters: string[], + gradient: GradientMode, } // binary flags for transaction classification