"""Plotly renderer for remote-admin TX-power optimization sweeps. Produces a self-contained, interactive HTML chart: median SNR measured *at the target* and trace success rate versus the admin node's TX power, with the chosen optimum marked. """ from __future__ import annotations from datetime import datetime from pathlib import Path import plotly.graph_objects as go from ..core.models import TxOptResult _TEMPLATE = "plotly_dark" _BRAND = "#5eead4" _MUTED = "#94a3b8" def render_tx_optimization(result: TxOptResult, output_dir: Path) -> Path: """Render a TX-power optimization sweep to an interactive HTML file. Args: result: The optimization result to visualize. output_dir: Directory the HTML file is written to (created if needed). Returns: Path to the written HTML file. """ levels = result.sorted_by_tx() tx = [lv.tx_power for lv in levels] snr = [lv.target_snr for lv in levels] success = [lv.success_rate * 100 for lv in levels] fig = go.Figure() fig.add_trace( go.Scatter( x=tx, y=snr, name="target SNR (dB)", mode="lines+markers", line=dict(color=_BRAND, width=3), marker=dict(size=8), connectgaps=False, hovertemplate="TX %{x}
SNR %{y:+.1f} dB", ) ) fig.add_trace( go.Bar( x=tx, y=success, name="success rate (%)", yaxis="y2", marker=dict(color=_MUTED), opacity=0.25, hovertemplate="TX %{x}
%{y:.0f}% success", ) ) if result.best_snr is not None: fig.add_vline(x=result.best_tx, line=dict(color="#4ade80", width=2, dash="dash")) fig.add_trace( go.Scatter( x=[result.best_tx], y=[result.best_snr], name=f"optimum (TX {result.best_tx})", mode="markers", marker=dict(color="#4ade80", size=16, symbol="star"), hovertemplate=f"optimum
TX {result.best_tx}" "
%{y:+.1f} dB", ) ) fig.update_layout( template=_TEMPLATE, title=dict( text=f"TX-power optimization — {result.admin_node} → {result.target}" f" · optimum TX {result.best_tx}", font=dict(color=_BRAND), ), xaxis=dict(title="admin-node TX power", dtick=1), yaxis=dict(title="target SNR (dB)"), yaxis2=dict( title="success rate (%)", overlaying="y", side="right", range=[0, 105], showgrid=False, ), legend=dict(orientation="h", yanchor="bottom", y=1.02), bargap=0.4, ) output_dir.mkdir(parents=True, exist_ok=True) stamp = datetime.now().strftime("%Y%m%d-%H%M%S") path = output_dir / f"tx-optimize_{_slug(result.target)}_{stamp}.html" fig.write_html(path, include_plotlyjs=True) # fully self-contained / offline return path def _slug(value: str) -> str: """Make a filesystem-safe slug from a node name. Args: value: Arbitrary node name or key prefix. Returns: A lowercase slug containing only alphanumerics, dashes, and underscores. """ return "".join(c if c.isalnum() or c in "-_" else "-" for c in value).strip("-").lower()