diff --git a/packages/phoenix-event-display/src/event-display.ts b/packages/phoenix-event-display/src/event-display.ts index 6283026b0..c1f6c7efe 100644 --- a/packages/phoenix-event-display/src/event-display.ts +++ b/packages/phoenix-event-display/src/event-display.ts @@ -51,6 +51,8 @@ export class EventDisplay { private stateManager: StateManager; /** URL manager for managing options given through URL. */ private urlOptionsManager: URLOptionsManager; + /** Event bus for external integration. */ + private eventBus: Map void>> = new Map(); /** Flag to track if EventDisplay has been initialized. */ private isInitialized: boolean = false; @@ -117,6 +119,7 @@ export class EventDisplay { // Clear accumulated callbacks this.onEventsChange = []; this.onDisplayedEventChange = []; + this.eventBus.clear(); // Reset singletons for clean view transition this.loadingManager?.reset(); this.stateManager?.resetForViewTransition(); @@ -659,6 +662,9 @@ export class EventDisplay { buildGeometryFromParameters: (parameters: { [key: string]: any }) => this.buildGeometryFromParameters(parameters), scene: this.getThreeManager().getSceneManager().getScene(), + on: (eventName: string, callback: (data: any) => void) => + this.on(eventName, callback), + emit: (eventName: string, data?: any) => this.emit(eventName, data), }; } @@ -856,4 +862,38 @@ export class EventDisplay { } } } + + /** + * Subscribe to a named event on the event bus. + * @param eventName Name of the event to listen for. + * @param callback Function to call when the event is emitted. + * @returns Unsubscribe function. + */ + public on(eventName: string, callback: (data: any) => void): () => void { + if (!this.eventBus.has(eventName)) { + this.eventBus.set(eventName, new Set()); + } + this.eventBus.get(eventName).add(callback); + return () => { + const listeners = this.eventBus.get(eventName); + if (listeners) { + listeners.delete(callback); + if (listeners.size === 0) { + this.eventBus.delete(eventName); + } + } + }; + } + + /** + * Emit a named event on the event bus. + * @param eventName Name of the event to emit. + * @param data Optional data to pass to listeners. + */ + public emit(eventName: string, data?: any): void { + const listeners = this.eventBus.get(eventName); + if (listeners) { + listeners.forEach((cb) => cb(data)); + } + } } diff --git a/packages/phoenix-event-display/src/helpers/histogram-config.ts b/packages/phoenix-event-display/src/helpers/histogram-config.ts new file mode 100644 index 000000000..249408429 --- /dev/null +++ b/packages/phoenix-event-display/src/helpers/histogram-config.ts @@ -0,0 +1,89 @@ +/** + * Configuration for a histogram panel. + * Experiment-agnostic — each experiment provides its own config. + */ +export interface HistogramConfig { + /** Display title for the histogram. */ + title: string; + /** X-axis label. */ + xlabel: string; + /** Y-axis label. */ + ylabel: string; + /** Number of bins. */ + nbins: number; + /** X-axis minimum value. */ + xmin: number; + /** X-axis maximum value. */ + xmax: number; + /** ROOT line color index. */ + lineColor: number; + /** ROOT fill color index. */ + fillColor: number; + /** ROOT fill style. */ + fillStyle: number; + /** Event bus event name to subscribe to. Defaults to 'result-recorded'. */ + eventName?: string; + /** Reference lines for known resonances. */ + hints?: HistogramHint[]; + /** Guidance text shown when histogram is empty. */ + emptyText?: string; +} + +/** A reference line marking a known physics resonance. */ +export interface HistogramHint { + /** Mass value in GeV. */ + mass: number; + /** Label to display. */ + label: string; + /** CSS color for the line. */ + color?: string; +} + +/** Default ATLAS Z-path masterclass histogram config. */ +export const ATLAS_MASS_HISTOGRAM: HistogramConfig = { + title: 'Invariant Mass', + xlabel: 'Mass (GeV)', + ylabel: 'Entries', + nbins: 50, + xmin: 20, + xmax: 120, + lineColor: 857, + fillColor: 857, + fillStyle: 1001, + hints: [{ mass: 91.2, label: 'Z', color: '#40c0f0' }], + emptyText: 'Tag particles in the masterclass panel to fill this histogram', +}; + +/** ATLAS J/psi-path masterclass histogram config. */ +export const ATLAS_JPSI_HISTOGRAM: HistogramConfig = { + title: 'Invariant Mass', + xlabel: 'Mass (GeV)', + ylabel: 'Entries', + nbins: 50, + xmin: 1, + xmax: 5, + lineColor: 857, + fillColor: 857, + fillStyle: 1001, + hints: [{ mass: 3.1, label: 'J/\u03C8', color: '#f0c040' }], + emptyText: 'Tag particles in the masterclass panel to fill this histogram', +}; + +/** ATLAS wide-range histogram config (Z + Higgs). */ +export const ATLAS_WIDE_HISTOGRAM: HistogramConfig = { + title: 'Invariant Mass', + xlabel: 'Mass (GeV)', + ylabel: 'Entries', + nbins: 60, + xmin: 0, + xmax: 200, + lineColor: 857, + fillColor: 857, + fillStyle: 1001, + hints: [ + { mass: 3.1, label: 'J/\u03C8', color: '#f0c040' }, + { mass: 91.2, label: 'Z', color: '#40c0f0' }, + { mass: 125.1, label: 'H', color: '#f07040' }, + ], + emptyText: 'Tag particles in the masterclass panel to fill this histogram', +}; diff --git a/packages/phoenix-event-display/src/index.ts b/packages/phoenix-event-display/src/index.ts index 98ddeae4a..8818dfbf8 100644 --- a/packages/phoenix-event-display/src/index.ts +++ b/packages/phoenix-event-display/src/index.ts @@ -32,6 +32,7 @@ export * from './helpers/runge-kutta'; export * from './helpers/pretty-symbols'; export * from './helpers/active-variable'; export * from './helpers/zip'; +export * from './helpers/histogram-config'; // Loaders export * from './loaders/event-data-loader'; diff --git a/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/histogram.svg b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/histogram.svg new file mode 100644 index 000000000..c2dd452fc --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-app/src/assets/icons/histogram.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/assets/icons/histogram.svg b/packages/phoenix-ng/projects/phoenix-ui-components/lib/assets/icons/histogram.svg new file mode 100644 index 000000000..c2dd452fc --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/assets/icons/histogram.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-ui.module.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-ui.module.ts index 7b96687c6..85d1bd7ad 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-ui.module.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-ui.module.ts @@ -65,6 +65,8 @@ import { EventDataExplorerComponent, EventDataExplorerDialogComponent, CycleEventsComponent, + HistogramPanelComponent, + HistogramPanelOverlayComponent, } from './ui-menu'; import { AttributePipe } from '../services/extras/attribute.pipe'; @@ -127,6 +129,8 @@ const PHOENIX_COMPONENTS: Type[] = [ FileExplorerComponent, RingLoaderComponent, CycleEventsComponent, + HistogramPanelComponent, + HistogramPanelOverlayComponent, ]; @NgModule({ diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.html b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.html new file mode 100644 index 000000000..face6089a --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.html @@ -0,0 +1,61 @@ + +
+ +
+ {{ + config.emptyText || 'Waiting for data...' + }} +
+ + +
+
+ + +
+ {{ + getHintLabel(hint) + }} +
+
+ + + +
+
diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.scss b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.scss new file mode 100644 index 000000000..a62303e9a --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.scss @@ -0,0 +1,131 @@ +.histogram-container { + padding: 0.5rem; + width: 450px; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +// Wrapper holds the canvas + hint lines in relative position +.histogram-wrapper { + position: relative; + min-height: 280px; + border-radius: 6px; + overflow: hidden; + background: #1a1a2e; +} + +.histogram-canvas { + width: 100%; + height: 280px; + border-radius: 6px; + overflow: hidden; + + // Scoped dark theme overrides for jsroot SVG + ::ng-deep { + svg { + background: #1a1a2e !important; + } + } +} + +// Reference lines for known resonances +// Aligned to jsroot's pad margins (10% each side by default) +.hint-line { + position: absolute; + top: 10%; + bottom: 10%; + border-left: 1.5px dashed; + opacity: 0.8; + pointer-events: none; + z-index: 10; +} + +.hint-label { + position: absolute; + top: -2px; + left: 4px; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + white-space: nowrap; + opacity: 0.95; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6); +} + +// Empty state +.histogram-empty { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 5; + pointer-events: none; + + .empty-hint { + font-size: 0.75rem; + color: var(--phoenix-text-color-secondary); + opacity: 0.5; + text-align: center; + max-width: 250px; + line-height: 1.4; + } +} + +// Footer — single row with stats left, actions right +.histogram-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.3rem 0.2rem 0; +} + +.histogram-stats { + display: flex; + align-items: baseline; + gap: 0.35rem; + + .stat-label { + font-size: 0.7rem; + color: var(--phoenix-text-color-secondary); + opacity: 0.6; + } + + .stat-value { + font-size: 0.8rem; + color: var(--phoenix-text-color); + font-weight: 500; + } + + .stat-sep { + font-size: 0.7rem; + color: var(--phoenix-text-color-secondary); + opacity: 0.3; + margin: 0 0.1rem; + } +} + +.histogram-actions { + display: flex; + gap: 0.3rem; +} + +.btn-action { + font-size: 0.7rem; + padding: 0.2rem 0.5rem; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 5px; + background: rgba(255, 255, 255, 0.04); + color: var(--phoenix-text-color-secondary); + cursor: pointer; + transition: all 0.15s; + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.15); + } +} diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.ts new file mode 100644 index 000000000..0b6f8eabd --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component.ts @@ -0,0 +1,349 @@ +import { + Component, + type OnInit, + type OnDestroy, + Input, + ViewChild, + ElementRef, +} from '@angular/core'; +import { + type HistogramConfig, + ATLAS_MASS_HISTOGRAM, +} from 'phoenix-event-display'; +import { EventDisplayService } from '../../../../services/event-display.service'; +import { createHistogram, gStyle } from 'jsroot'; +import { draw, redraw } from 'jsroot/draw'; + +/** localStorage key prefix for histogram persistence. */ +const STORAGE_PREFIX = 'phoenix-histogram-'; + +@Component({ + standalone: false, + selector: 'app-histogram-panel-overlay', + templateUrl: './histogram-panel-overlay.component.html', + styleUrls: ['./histogram-panel-overlay.component.scss'], +}) +export class HistogramPanelOverlayComponent implements OnInit, OnDestroy { + /** Whether the histogram panel is visible. Uses setter to trigger draw on show. */ + private _showHistogram = false; + @Input() set showHistogram(val: boolean) { + const wasHidden = !this._showHistogram; + this._showHistogram = val; + if (val && wasHidden) { + setTimeout(() => this.drawHistogram(), 0); + } else if (!val) { + this.drawn = false; + } + } + get showHistogram(): boolean { + return this._showHistogram; + } + + @Input() config: HistogramConfig = ATLAS_MASS_HISTOGRAM; + + @ViewChild('histogramDiv') histogramDiv: ElementRef; + + entries = 0; + mean = 0; + private sumValues = 0; + private rawValues: number[] = []; + private histogram: any; + private drawn = false; + private unsubscribe: () => void; + private redrawTimer: any = null; + + constructor(private eventDisplay: EventDisplayService) {} + + ngOnInit() { + const c = this.config; + this.histogram = createHistogram('TH1F', c.nbins); + this.histogram.fName = 'hmass'; + this.histogram.fTitle = ''; + this.histogram.fXaxis.fXmin = c.xmin; + this.histogram.fXaxis.fXmax = c.xmax; + this.histogram.fXaxis.fTitle = c.xlabel; + this.histogram.fYaxis.fTitle = c.ylabel; + this.histogram.fLineColor = c.lineColor; + this.histogram.fLineWidth = 0; + this.histogram.fFillColor = c.fillColor; + this.histogram.fFillStyle = c.fillStyle; + + // Disable stat box — BIT(9) = 512 + this.histogram.fBits = this.histogram.fBits | 512; + + // Restore any saved state from a previous session + this.restoreFromStorage(); + + const eventName = c.eventName ?? 'result-recorded'; + this.unsubscribe = this.eventDisplay.on(eventName, (data: any) => { + if (data?.mass != null) { + this.addValue(data.mass); + } + }); + } + + ngOnDestroy() { + this.unsubscribe?.(); + if (this.redrawTimer) { + clearTimeout(this.redrawTimer); + } + } + + /** Save gStyle, apply dark theme values, await callback, then restore gStyle. */ + private async withDarkGStyle(fn: () => Promise): Promise { + const saved = { + fCanvasColor: gStyle.fCanvasColor, + fPadColor: gStyle.fPadColor, + fFrameFillColor: gStyle.fFrameFillColor, + fFrameLineColor: gStyle.fFrameLineColor, + }; + + // Dark theme: 1=black bg, 0=white text/lines + gStyle.fCanvasColor = 1; + gStyle.fPadColor = 1; + gStyle.fFrameFillColor = 1; + gStyle.fFrameLineColor = 0; + + try { + return await fn(); + } finally { + gStyle.fCanvasColor = saved.fCanvasColor; + gStyle.fPadColor = saved.fPadColor; + gStyle.fFrameFillColor = saved.fFrameFillColor; + gStyle.fFrameLineColor = saved.fFrameLineColor; + } + } + + private async drawHistogram() { + if (this.histogramDiv?.nativeElement && this.histogram) { + // Set axis colors for dark theme directly on histogram object (not gStyle) + this.histogram.fXaxis.fAxisColor = 0; + this.histogram.fXaxis.fLabelColor = 0; + this.histogram.fXaxis.fTitleColor = 0; + this.histogram.fYaxis.fAxisColor = 0; + this.histogram.fYaxis.fLabelColor = 0; + this.histogram.fYaxis.fTitleColor = 0; + + await this.withDarkGStyle(() => + draw(this.histogramDiv.nativeElement, this.histogram, 'hist,no_stat'), + ); + this.applyDarkTheme(); + this.drawn = true; + } + } + + /** Apply dark theme to jsroot SVG via direct DOM manipulation. */ + private applyDarkTheme() { + const el = this.histogramDiv?.nativeElement; + if (!el) return; + + const svg = el.querySelector('svg'); + if (svg) { + svg.style.background = '#1a1a2e'; + } + + el.querySelectorAll('rect').forEach((rect: SVGRectElement) => { + const fill = rect.getAttribute('fill'); + if ( + fill === 'white' || + fill === 'rgb(255, 255, 255)' || + fill === '#ffffff' || + fill === 'rgb(255,255,255)' + ) { + rect.setAttribute('fill', '#1a1a2e'); + rect.setAttribute('stroke', 'rgba(255,255,255,0.15)'); + } + }); + + el.querySelectorAll('line').forEach((line: SVGLineElement) => { + const stroke = line.getAttribute('stroke'); + if ( + stroke === 'black' || + stroke === 'rgb(0, 0, 0)' || + stroke === '#000000' || + stroke === 'rgb(0,0,0)' + ) { + line.setAttribute('stroke', '#aaaaaa'); + } + }); + + el.querySelectorAll('path').forEach((path: SVGPathElement) => { + const stroke = path.getAttribute('stroke'); + if ( + stroke === 'black' || + stroke === 'rgb(0, 0, 0)' || + stroke === '#000000' || + stroke === 'rgb(0,0,0)' + ) { + path.setAttribute('stroke', '#aaaaaa'); + } + }); + + el.querySelectorAll('text').forEach((text: SVGTextElement) => { + const fill = text.getAttribute('fill'); + if ( + fill === 'black' || + fill === 'rgb(0, 0, 0)' || + fill === '#000000' || + fill === 'rgb(0,0,0)' || + !fill + ) { + text.setAttribute('fill', '#cccccc'); + } + }); + } + + /** Add a value and schedule a debounced redraw. */ + addValue(value: number) { + if (!this.histogram) return; + this.histogram.Fill(value); + this.rawValues.push(value); + this.entries++; + this.sumValues += value; + this.mean = this.sumValues / this.entries; + + this.saveToStorage(); + + if (this.drawn && this.histogramDiv?.nativeElement) { + this.scheduleRedraw(); + } + } + + /** Debounced redraw — coalesces rapid events (e.g. bulk load) into one repaint. */ + private scheduleRedraw() { + if (this.redrawTimer) return; + this.redrawTimer = setTimeout(async () => { + this.redrawTimer = null; + if (this.drawn && this.histogramDiv?.nativeElement) { + await this.withDarkGStyle(() => + redraw( + this.histogramDiv.nativeElement, + this.histogram, + 'hist,no_stat', + ), + ); + this.applyDarkTheme(); + } + }, 50); + } + + /** Reset histogram after user confirmation. */ + resetHistogram() { + if (this.entries === 0) return; + if (!confirm(`Clear all ${this.entries} entries? This cannot be undone.`)) { + return; + } + this.clearHistogram(); + } + + /** Clear all histogram data and redraw. */ + private async clearHistogram() { + if (!this.histogram) return; + for (let i = 0; i < this.histogram.fArray.length; i++) { + this.histogram.fArray[i] = 0; + } + this.histogram.fEntries = 0; + this.entries = 0; + this.mean = 0; + this.sumValues = 0; + this.rawValues = []; + + this.clearStorage(); + + if (this.drawn && this.histogramDiv?.nativeElement) { + await this.withDarkGStyle(() => + redraw(this.histogramDiv.nativeElement, this.histogram, 'hist,no_stat'), + ); + this.applyDarkTheme(); + } + } + + /** Get hint line position as percentage of the wrapper, accounting for jsroot pad margins. */ + getHintPosition(mass: number): number { + const c = this.config; + const padLeft = 0.1; // jsroot default pad left margin + const padRight = 0.1; // jsroot default pad right margin + const fraction = (mass - c.xmin) / (c.xmax - c.xmin); + return (padLeft + fraction * (1 - padLeft - padRight)) * 100; + } + + /** Format hint label with mass value and unit for readability. */ + getHintLabel(hint: { mass: number; label: string }): string { + const massStr = + hint.mass >= 10 + ? hint.mass.toFixed(1) + : hint.mass.toFixed(hint.mass < 1 ? 2 : 1); + return `${hint.label} (${massStr} GeV)`; + } + + /** Export histogram data as TSV. */ + exportCSV() { + if (!this.histogram) return; + const c = this.config; + const binWidth = (c.xmax - c.xmin) / c.nbins; + let tsv = 'bin_low\tbin_high\tentries\n'; + for (let i = 1; i <= c.nbins; i++) { + const low = c.xmin + (i - 1) * binWidth; + const high = low + binWidth; + const content = this.histogram.fArray[i] || 0; + tsv += `${low.toFixed(2)}\t${high.toFixed(2)}\t${content}\n`; + } + + const filename = c.title.toLowerCase().replace(/\s+/g, '_') + '_data.tsv'; + const blob = new Blob([tsv], { type: 'text/tab-separated-values' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + a.click(); + URL.revokeObjectURL(url); + } + + // --- localStorage persistence --- + + /** Storage key derived from config title to avoid collisions between configs. */ + private get storageKey(): string { + return ( + STORAGE_PREFIX + this.config.title.toLowerCase().replace(/\s+/g, '-') + ); + } + + /** Save raw values to localStorage for crash recovery. */ + private saveToStorage() { + try { + localStorage.setItem(this.storageKey, JSON.stringify(this.rawValues)); + } catch { + // localStorage full or unavailable — silently ignore + } + } + + /** Restore histogram state from localStorage. */ + private restoreFromStorage() { + try { + const saved = localStorage.getItem(this.storageKey); + if (!saved) return; + + const values: number[] = JSON.parse(saved); + if (!Array.isArray(values) || values.length === 0) return; + + for (const v of values) { + this.histogram.Fill(v); + this.rawValues.push(v); + this.entries++; + this.sumValues += v; + } + this.mean = this.sumValues / this.entries; + } catch { + // Corrupted data — start fresh + } + } + + /** Clear saved state from localStorage. */ + private clearStorage() { + try { + localStorage.removeItem(this.storageKey); + } catch { + // Ignore + } + } +} diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.html b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.html new file mode 100644 index 000000000..6dcffe6d7 --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.html @@ -0,0 +1,7 @@ + + diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.scss b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.ts new file mode 100644 index 000000000..cabf89058 --- /dev/null +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/histogram-panel/histogram-panel.component.ts @@ -0,0 +1,54 @@ +import { + Component, + type OnInit, + ComponentRef, + type OnDestroy, + Input, + type OnChanges, + type SimpleChanges, +} from '@angular/core'; +import { Overlay } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { HistogramPanelOverlayComponent } from './histogram-panel-overlay/histogram-panel-overlay.component'; +import type { HistogramConfig } from 'phoenix-event-display'; + +@Component({ + standalone: false, + selector: 'app-histogram-panel', + templateUrl: './histogram-panel.component.html', + styleUrls: ['./histogram-panel.component.scss'], +}) +export class HistogramPanelComponent implements OnInit, OnDestroy, OnChanges { + showHistogram = false; + overlayWindow: ComponentRef; + @Input() config?: HistogramConfig; + + constructor(private overlay: Overlay) {} + + ngOnInit() { + const overlayRef = this.overlay.create(); + const overlayPortal = new ComponentPortal(HistogramPanelOverlayComponent); + this.overlayWindow = overlayRef.attach(overlayPortal); + this.overlayWindow.instance.showHistogram = this.showHistogram; + if (this.config) { + this.overlayWindow.instance.config = this.config; + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['config'] && this.overlayWindow) { + if (this.config) { + this.overlayWindow.instance.config = this.config; + } + } + } + + ngOnDestroy(): void { + this.overlayWindow.destroy(); + } + + toggleOverlay() { + this.showHistogram = !this.showHistogram; + this.overlayWindow.instance.showHistogram = this.showHistogram; + } +} diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/index.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/index.ts index 50b4566a0..e2d7e1c89 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/index.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/index.ts @@ -37,3 +37,5 @@ export * from './event-data-explorer/event-data-explorer.component'; export * from './event-data-explorer/event-data-explorer-dialog/event-data-explorer-dialog.component'; export * from './cycle-events/cycle-events.component'; export * from './ui-menu-wrapper/ui-menu-wrapper.component'; +export * from './histogram-panel/histogram-panel.component'; +export * from './histogram-panel/histogram-panel-overlay/histogram-panel-overlay.component'; diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.html b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.html index a3126156c..5945a9f58 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.html +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.html @@ -72,6 +72,9 @@ + + + diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.ts index e11304181..ffb4fc019 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/ui-menu.component.ts @@ -4,6 +4,7 @@ import { type EventDataImportOption, } from '../../services/extras/event-data-import'; import { defaultAnimationPresets } from './animate-camera/animate-camera.component'; +import type { HistogramConfig } from 'phoenix-event-display'; export interface UIMenuConfig { showVRToggle?: boolean; @@ -50,6 +51,8 @@ export class UiMenuComponent { @Input() animationPresets = defaultAnimationPresets; + @Input() + histogramConfig?: HistogramConfig; @Input() uiConfig: UIMenuConfig = { ...defaultUIMenuConfig };