""" Analysis dashboard. Runs SequenceAnalyzer against the active sequence and displays: - GC content sliding window (Plotly) - CAI score + codon usage heatmap - Homopolymer summary - Restriction site hits - Kozak context evaluation - ViennaRNA secondary structure (MFE) """ from __future__ import annotations from typing import TYPE_CHECKING, Optional import panel as pn import param import plotly.graph_objects as go from core.analysis.analyzer import SequenceAnalyzer, AnalysisReport if TYPE_CHECKING: from ui.state import AppState _analyzer = SequenceAnalyzer() def _gc_plot(report: AnalysisReport) -> pn.pane.Plotly: """Interactive GC% sliding window Plotly chart.""" pos = report.gc_sliding_positions vals = report.gc_sliding_values if pos is None or len(pos) == 0: return pn.pane.HTML('
Insufficient sequence length for GC plot.
') fig = go.Figure() fig.add_trace(go.Scatter( x=pos, y=vals, mode="lines", line={"color": "#0F766E", "width": 1.5}, name="GC%", hovertemplate="Position: %{x}
GC: %{y:.1f}%", )) fig.add_hline(y=50, line_dash="dash", line_color="#DC2626", opacity=0.5, annotation_text="50%", annotation_position="bottom right") fig.update_layout( title=dict(text="GC Content (sliding window)", font=dict(size=13)), xaxis_title="Position (nt)", yaxis_title="GC (%)", yaxis=dict(range=[0, 100]), height=260, margin=dict(l=50, r=20, t=40, b=40), plot_bgcolor="#F8FAFC", paper_bgcolor="white", ) return pn.pane.Plotly(fig, sizing_mode="stretch_width") def _cai_card(report: AnalysisReport) -> pn.pane.HTML: cai = report.cai if cai is None: return pn.pane.HTML('
No CDS available for CAI.
') pct = int(cai * 100) color = "#059669" if cai >= 0.7 else "#D97706" if cai >= 0.4 else "#DC2626" bar = f'
' \ f'
' org = report.cai_organism or "unknown" return pn.pane.HTML(f"""
{cai:.3f} CAI ({org})
{bar}
0.7+ = high adaptation · 0.4–0.7 = moderate · <0.4 = poor
""") def _validation_card(report: AnalysisReport) -> pn.pane.HTML: checks = [ ("Start codon (ATG)", report.has_start_codon), ("Stop codon present", report.has_stop_codon), ("In-frame CDS (÷3)", report.in_frame), ] rows = "" for label, ok in checks: if ok is None: icon, color = "—", "#94A3B8" elif ok: icon, color = "Pass", "#059669" else: icon, color = "Fail", "#DC2626" rows += ( f'
' f'{icon}' f'{label}
' ) return pn.pane.HTML(rows) def _homopolymer_card(report: AnalysisReport) -> pn.pane.HTML: if report.homopolymer_count == 0: return pn.pane.HTML( '
No homopolymers detected ' f'(min run ≥ 5 nt)
' ) runs = report.homopolymer_runs rows = "".join( f'' f'{r.nucleotide}×{r.length}' f'pos {r.start}' for r in sorted(runs, key=lambda r: -r.length)[:10] ) return pn.pane.HTML( f'
' f'{report.homopolymer_count} homopolymer run(s) — ' f'longest: {report.longest_homopolymer} nt
' f'{rows}
' ) def _restriction_card(report: AnalysisReport) -> pn.pane.HTML: present = report.restriction_enzymes_present if not present: return pn.pane.HTML( '
No common restriction sites found.
' ) chips = " ".join( f'{e}' for e in sorted(present) ) return pn.pane.HTML(f'
Sites present: {chips}
') def _kozak_card(report: AnalysisReport) -> pn.pane.HTML: kz = report.kozak if kz is None: return pn.pane.HTML('
No Kozak context available.
') color = {"strong": "#059669", "adequate": "#D97706", "weak": "#DC2626"}.get(kz.strength, "#94A3B8") r3_ok = "Yes" if kz.has_optimal_r3 else "No" return pn.pane.HTML(f"""
{kz.strength.upper()} score {kz.score:.2f}
{kz.context}
-3 purine (R): {r3_ok} · ATG pos: {kz.atg_position}
""") def _structure_card(report: AnalysisReport) -> pn.pane.HTML: s = report.structure if s is None or s.is_stub: return pn.pane.HTML( '
' 'ViennaRNA not installed. Install with: conda install -c bioconda viennarna
' ) color = "#059669" if s.mfe > -50 else "#D97706" if s.mfe > -200 else "#DC2626" return pn.pane.HTML(f"""
{s.mfe:.1f} kcal/mol MFE
{s.structure[:300]}{'…' if len(s.structure) > 300 else ''}
""") def _uridine_card(report: AnalysisReport) -> pn.pane.HTML: u = report.uridine if u is None: return pn.pane.HTML('
No uridine data.
') color = "#059669" if u.u_percent < 35 else "#D97706" if u.u_percent < 45 else "#DC2626" stretches = len(u.high_u_stretches) return pn.pane.HTML(f"""
{u.u_percent:.1f}% uridine
{stretches} high-U stretch(es) · U/A ratio {u.ua_ratio:.2f}
High U is immunostimulatory — modified nucleotides mitigate it.
""") def _motif_card(report: AnalysisReport) -> pn.pane.HTML: hits = report.motif_hits if not hits: return pn.pane.HTML( '
No liability motifs detected.
' ) sev_color = {"critical": "#DC2626", "warning": "#D97706", "info": "#64748B"} rows = "" for h in hits[:12]: c = sev_color.get(h.severity, "#64748B") rows += ( f'
' f'{h.severity}' f'{h.label}' f'' f'{h.region}:{h.start}
' ) more = f'
+{len(hits)-12} more
' if len(hits) > 12 else "" return pn.pane.HTML(rows + more) _VERDICT_STYLE = { "pass": ("#059669", "PASS"), "review": ("#D97706", "REVIEW"), "fail": ("#DC2626", "FAIL"), } _SEV_STYLE = { "critical": ("#DC2626", "Critical"), "warning": ("#D97706", "Warning"), "info": ("#64748B", "Info"), } def render_liability_panel(report: AnalysisReport) -> pn.pane.HTML: """Reusable liability / QC scorecard + ranked flag list (pure HTML).""" lia = getattr(report, "liability", None) if lia is None: return pn.pane.HTML('
No liability assessment.
') vcolor, vlabel = _VERDICT_STYLE.get(lia.verdict, ("#64748B", lia.verdict.upper())) score_color = "#059669" if lia.score >= 85 else "#D97706" if lia.score >= 60 else "#DC2626" counts = ( f'{lia.n_critical} critical · ' f'{lia.n_warning} warning · ' f'{lia.n_info} info' ) if not lia.flags: flag_html = ( '
' f'No liabilities flagged across {lia.checks_run} checks.
' ) else: items = "" for f in lia.sorted_flags(): sc, sl = _SEV_STYLE.get(f.severity, ("#64748B", f.severity.title())) loc = f'{f.location}' if f.location else "" rec = f'
↳ {f.recommendation}
' if f.recommendation else "" items += f"""
{sl} {f.title} {loc}
{f.detail}
{rec}
""" flag_html = items return pn.pane.HTML(f"""
QC SCORE
{lia.score}
{vlabel}
{counts}
{lia.checks_run} checks
{flag_html}
""") def _metric_panel(title: str, content: pn.viewable.Viewable) -> pn.Column: return pn.Column( pn.pane.HTML( f'
{title}
' ), content, styles={ "background": "white", "border": "1px solid #CBD5E1", "border-radius": "6px", "padding": "12px 14px", }, sizing_mode="stretch_width", margin=(0, 0, 10, 0), ) class AnalysisDashboard(param.Parameterized): """Analysis dashboard panel.""" def __init__(self, state: "AppState", **params: object) -> None: super().__init__(**params) self._state = state self._report: Optional[AnalysisReport] = None @param.depends("_state.active_sequence", "_state.active_tab") def panel(self) -> pn.Column: seq = self._state.active_sequence if seq is None: return pn.Column( pn.pane.HTML( '
Select a sequence first.
' ) ) # Run analysis (cached on sequence object) try: report = _analyzer.run_full_analysis(seq) self._report = report except Exception as e: return pn.Column( pn.pane.HTML( f'
Analysis error: {e}
' ) ) warnings_html = "" if report.warnings: warn_items = "".join(f"
  • {w}
  • " for w in report.warnings) warnings_html = ( f'
    ' f'Warnings:
    ' ) # Summary bar gc_str = f"{report.gc_percent_global:.1f}%" cai_str = f"{report.cai:.3f}" if report.cai is not None else "N/A" mfe_str = ( f"{report.structure.mfe:.1f} kcal/mol" if report.structure and not report.structure.is_stub else "N/A" ) summary_html = f"""
    LENGTH
    {report.sequence_length} nt
    GC%
    {gc_str}
    CAI
    {cai_str}
    MFE
    {mfe_str}
    """ return pn.Column( pn.pane.HTML( f'
    ' f'Analysis: {seq.name}
    ' ), pn.pane.HTML(warnings_html) if warnings_html else pn.pane.HTML(""), pn.pane.HTML(summary_html), pn.pane.HTML( '
    Liability / QC assessment
    ' ), render_liability_panel(report), pn.layout.Divider(), _gc_plot(report), pn.GridBox( _metric_panel("Codon Adaptation Index", _cai_card(report)), _metric_panel("CDS Validation", _validation_card(report)), _metric_panel("Kozak Context", _kozak_card(report)), _metric_panel("Homopolymers", _homopolymer_card(report)), _metric_panel("Restriction Sites", _restriction_card(report)), _metric_panel("Uridine Content", _uridine_card(report)), _metric_panel("Liability Motifs", _motif_card(report)), _metric_panel("Secondary Structure (MFE)", _structure_card(report)), ncols=2, sizing_mode="stretch_width", ), sizing_mode="stretch_width", styles={"padding": "8px 16px"}, )