"use client"; import { useEffect, useState } from "react"; type RPoint = { residue: string; chain: string; resnum: number; phi: number; psi: number; region: string }; const REGION_COLOR: Record = { core_alpha: "#4ADE80", core_beta: "#7C3AED", allowed: "#FBBF24", outlier: "#EF4444", }; export function RamachandranPlot({ pdbId, chain = "A" }: { pdbId: string | null; chain?: string }) { const [points, setPoints] = useState([]); const [loading, setLoading] = useState(true); const [hovered, setHovered] = useState(null); const [error, setError] = useState(null); useEffect(() => { if (!pdbId) { setLoading(false); return; } setLoading(true); setError(null); fetch(`/api/backend/api/structure_analysis/ramachandran/${pdbId}?chain=${chain}`) .then(r => { if (!r.ok) return r.json().then(e => Promise.reject(new Error(e.detail || `Status ${r.status}`))); return r.json(); }) .then(setPoints) .catch(e => setError(e.message)) .finally(() => setLoading(false)); }, [pdbId, chain]); if (!pdbId) return
No PDB structure available for this protein. Ramachandran analysis requires a 3D structure.
; if (loading) return
Calculating φ/ψ angles…
; if (error) return
{error}
; const W = 400, H = 400, PAD = 40; const toX = (phi: number) => PAD + ((phi + 180) / 360) * (W - PAD * 2); const toY = (psi: number) => PAD + ((180 - psi) / 360) * (H - PAD * 2); const counts = { core_alpha: points.filter(p => p.region === "core_alpha").length, core_beta: points.filter(p => p.region === "core_beta").length, allowed: points.filter(p => p.region === "allowed").length, outlier: points.filter(p => p.region === "outlier").length, }; const outlierPct = points.length ? ((counts.outlier / points.length) * 100).toFixed(1) : "0"; return (

Ramachandran Plot

{outlierPct}% outliers
φ (phi) ψ (psi) {[-180, -90, 0, 90, 180].map(v => ( {v}° {v}° ))} {points.map((p, i) => ( setHovered(p)} onMouseLeave={() => setHovered(null)} /> ))} {hovered && (

{hovered.residue}{hovered.resnum}

φ {hovered.phi.toFixed(1)}° · ψ {hovered.psi.toFixed(1)}°

{hovered.region.replace("_", " ")}

)}

n = {points.length} residues

{Object.entries(counts).map(([region, count]) => (
{region.replace("_", " ")} {points.length > 0 ? ((count / points.length) * 100).toFixed(1) : "0"}%
0 ? (count / points.length) * 100 : 0}%`, background: REGION_COLOR[region], opacity: 0.7, }} />
))}

>98% in favoured regions = high quality model

{Object.entries(REGION_COLOR).map(([region, color]) => (
{region.replace("_", " ")}
))}
); }