Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/phoenix-event-display/src/event-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Set<(data: any) => void>> = new Map();
/** Flag to track if EventDisplay has been initialized. */
private isInitialized: boolean = false;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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),
};
}

Expand Down Expand Up @@ -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));
}
}
}
89 changes: 89 additions & 0 deletions packages/phoenix-event-display/src/helpers/histogram-config.ts
Original file line number Diff line number Diff line change
@@ -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',
};
1 change: 1 addition & 0 deletions packages/phoenix-event-display/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
EventDataExplorerComponent,
EventDataExplorerDialogComponent,
CycleEventsComponent,
HistogramPanelComponent,
HistogramPanelOverlayComponent,
} from './ui-menu';

import { AttributePipe } from '../services/extras/attribute.pipe';
Expand Down Expand Up @@ -127,6 +129,8 @@ const PHOENIX_COMPONENTS: Type<any>[] = [
FileExplorerComponent,
RingLoaderComponent,
CycleEventsComponent,
HistogramPanelComponent,
HistogramPanelOverlayComponent,
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<app-overlay
[overlayTitle]="config.title"
icon="histogram"
[active]="showHistogram"
[resizable]="false"
>
<div class="histogram-container" *ngIf="showHistogram">
<!-- Empty state guidance -->
<div class="histogram-empty" *ngIf="entries === 0">
<span class="empty-hint">{{
config.emptyText || 'Waiting for data...'
}}</span>
</div>

<!-- Histogram canvas with hint overlays -->
<div class="histogram-wrapper">
<div #histogramDiv class="histogram-canvas"></div>

<!-- Reference lines for known resonances -->
<div
class="hint-line"
*ngFor="let hint of config.hints"
[style.left.%]="getHintPosition(hint.mass)"
[style.borderColor]="hint.color || '#ffffff55'"
>
<span class="hint-label" [style.color]="hint.color || '#ffffff88'">{{
getHintLabel(hint)
}}</span>
</div>
</div>

<!-- Footer: stats + actions -->
<div class="histogram-footer">
<div class="histogram-stats">
<span class="stat-label">Entries:</span>
<span class="stat-value">{{ entries }}</span>
<span class="stat-sep">|</span>
<span class="stat-label">Mean:</span>
<span class="stat-value">{{
entries > 0 ? (mean | number: '1.1-1') + ' GeV' : '—'
}}</span>
</div>
<div class="histogram-actions">
<button
class="btn-action"
(click)="resetHistogram()"
matTooltip="Clear all entries"
>
Reset
</button>
<button
class="btn-action"
(click)="exportCSV()"
matTooltip="Download bin data as TSV"
>
Export
</button>
</div>
</div>
</div>
</app-overlay>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading