hackathon-ENGIN/engin/charts.py
Jakub Famulski 2 8981ae0a00
All checks were successful
ENGIN CI / Build, test and smoke (push) Successful in 31s
ci: enforce Ruff lint checks
2026-08-25 12:37:41 +02:00

123 lines
4.1 KiB
Python

"""Pure Plotly chart factories, independently testable from Streamlit."""
from __future__ import annotations
import numpy as np
import plotly.graph_objects as go
from .config import FREQ_COLS, LABEL_COLORS
from .explainability import EngineAnalysis
PLOT_BG = "#101820"
GRID = "rgba(148, 163, 184, 0.14)"
TEXT = "#dce8ee"
MUTED = "#8fa6b2"
def _base_layout(fig: go.Figure, *, height: int) -> go.Figure:
fig.update_layout(
height=height,
margin=dict(l=24, r=24, t=42, b=28),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor=PLOT_BG,
font=dict(color=TEXT, family="Inter, system-ui, sans-serif"),
hoverlabel=dict(bgcolor="#16232c", font_color="#ffffff"),
)
fig.update_xaxes(gridcolor=GRID, zeroline=False)
fig.update_yaxes(gridcolor=GRID, zeroline=False)
return fig
def engine_heatmap(analysis: EngineAnalysis) -> go.Figure:
cylinders = analysis.measurements["cylinder"].astype(int).tolist()
max_abs = max(float(np.percentile(analysis.absolute_deviation, 95)), 1.0)
fig = go.Figure(
go.Heatmap(
z=analysis.deviation,
x=list(range(len(FREQ_COLS))),
y=[f"C{value:02d}" for value in cylinders],
zmin=-max_abs,
zmax=max_abs,
zmid=0,
colorscale=[
[0.0, "#28b7d9"],
[0.5, "#13222b"],
[1.0, "#ff6b57"],
],
colorbar=dict(title="Δ mV", thickness=12),
hovertemplate="Cylinder %{y}<br>%{x} kHz<br>Odchylenie %{z:.1f} mV<extra></extra>",
)
)
fig.update_layout(title="Engine Diagnostic Fingerprint")
fig.update_yaxes(autorange="reversed", title="Cylinder")
fig.update_xaxes(title="Częstotliwość [kHz]", dtick=2)
return _base_layout(fig, height=420)
def cylinder_spectrum(analysis: EngineAnalysis, cylinder: int) -> go.Figure:
positions = np.flatnonzero(analysis.measurements["cylinder"].to_numpy() == cylinder)
if len(positions) != 1:
raise KeyError(f"Unknown cylinder={cylinder}")
position = int(positions[0])
row = analysis.diagnostics.iloc[position]
color = LABEL_COLORS[str(row["label"])]
frequency = np.arange(len(FREQ_COLS))
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=frequency,
y=analysis.reference[position],
mode="lines",
name="Mediana pozostałych cylindrów",
line=dict(color="#7f95a1", width=2, dash="dash"),
)
)
fig.add_trace(
go.Scatter(
x=frequency,
y=analysis.spectra[position],
mode="lines+markers",
name=f"Cylinder {cylinder}",
line=dict(color=color, width=3),
marker=dict(size=6),
)
)
top = {
int(value)
for value in str(row["top_anomalous_frequencies_khz"]).split("|")
if value
}
for value in top:
fig.add_vrect(
x0=value - 0.35,
x1=value + 0.35,
fillcolor=color,
opacity=0.10,
line_width=0,
)
fig.update_layout(title="Widmo cylindra vs referencja", legend=dict(orientation="h", y=1.14))
fig.update_xaxes(title="Częstotliwość [kHz]", dtick=1)
fig.update_yaxes(title="Amplituda [mV]")
return _base_layout(fig, height=410)
def deviation_chart(analysis: EngineAnalysis, cylinder: int) -> go.Figure:
position = int(
np.flatnonzero(analysis.measurements["cylinder"].to_numpy() == cylinder)[0]
)
values = analysis.deviation[position]
colors = ["#ff6b57" if value >= 0 else "#28b7d9" for value in values]
fig = go.Figure(
go.Bar(
x=np.arange(len(FREQ_COLS)),
y=values,
marker_color=colors,
hovertemplate="%{x} kHz<br>Δ %{y:.1f} mV<extra></extra>",
)
)
fig.add_hline(y=0, line_color="#6e8794", line_width=1)
fig.update_layout(title="Odchylenie od referencji")
fig.update_xaxes(title="Częstotliwość [kHz]", dtick=2)
fig.update_yaxes(title="Różnica [mV]")
return _base_layout(fig, height=320)