124 lines
4.1 KiB
Python
124 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)
|