"""
Results display panel for HBAT analysis.
This module provides GUI components for displaying analysis results
including hydrogen bonds, halogen bonds, and π interactions.
"""
import math
import tkinter as tk
from tkinter import messagebox, ttk
from typing import Optional
try:
from .chain_visualization import ChainVisualizationWindow
VISUALIZATION_AVAILABLE = True
except ImportError:
VISUALIZATION_AVAILABLE = False
from ..core.analysis import MolecularInteractionAnalyzer
[docs]
class ResultsPanel:
"""Panel for displaying analysis results.
This class provides a tabbed interface for viewing different types
of molecular interaction results including summaries, detailed lists,
and statistical analysis.
:param parent: Parent widget to contain this panel
:type parent: tkinter widget
"""
[docs]
def __init__(self, parent) -> None:
"""Initialize the results panel.
Creates a complete results display interface with multiple tabs
for different views of analysis results.
:param parent: Parent widget
:type parent: tkinter widget
:returns: None
:rtype: None
"""
self.parent = parent
self.analyzer: Optional[MolecularInteractionAnalyzer] = None
self._create_widgets()
def _create_widgets(self):
"""Create result display widgets."""
# Create main notebook for different result types
self.notebook = ttk.Notebook(self.parent)
self.notebook.pack(fill=tk.BOTH, expand=True)
# Summary tab
self._create_summary_tab()
# Hydrogen bonds tab
self._create_hydrogen_bonds_tab()
# Halogen bonds tab
self._create_halogen_bonds_tab()
# Pi interactions tab
self._create_pi_interactions_tab()
# Cooperativity chains tab
self._create_cooperativity_chains_tab()
def _create_summary_tab(self):
"""Create summary results tab."""
summary_frame = ttk.Frame(self.notebook)
self.notebook.add(summary_frame, text="Summary")
# Create text widget with scrollbars
text_frame = ttk.Frame(summary_frame)
text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.summary_text = tk.Text(text_frame, wrap=tk.NONE, font=("Courier", 12))
summary_v_scrollbar = ttk.Scrollbar(
text_frame, orient=tk.VERTICAL, command=self.summary_text.yview
)
summary_h_scrollbar = ttk.Scrollbar(
text_frame, orient=tk.HORIZONTAL, command=self.summary_text.xview
)
self.summary_text.configure(
yscrollcommand=summary_v_scrollbar.set,
xscrollcommand=summary_h_scrollbar.set,
)
# Use grid layout for proper scrollbar positioning
self.summary_text.grid(row=0, column=0, sticky="nsew")
summary_v_scrollbar.grid(row=0, column=1, sticky="ns")
summary_h_scrollbar.grid(row=1, column=0, sticky="ew")
text_frame.grid_rowconfigure(0, weight=1)
text_frame.grid_columnconfigure(0, weight=1)
# Configure text tags for formatting
self.summary_text.tag_configure(
"header", font=("Courier", 12, "bold"), foreground="blue"
)
self.summary_text.tag_configure("subheader", font=("Courier", 12, "bold"))
self.summary_text.tag_configure("highlight", background="cyan")
def _create_hydrogen_bonds_tab(self):
"""Create hydrogen bonds results tab."""
hb_frame = ttk.Frame(self.notebook)
self.notebook.add(hb_frame, text="Hydrogen Bonds")
# Create treeview for hydrogen bonds
tree_frame = ttk.Frame(hb_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
columns = (
"donor_res",
"donor_atom",
"acceptor_res",
"acceptor_atom",
"distance",
"angle",
"da_distance",
"type",
"da_props",
"bs_int",
)
self.hb_tree = ttk.Treeview(
tree_frame, columns=columns, show="headings", height=15
)
# Configure columns
self.hb_tree.heading("donor_res", text="Donor Residue")
self.hb_tree.heading("donor_atom", text="Donor Atom")
self.hb_tree.heading("acceptor_res", text="Acceptor Residue")
self.hb_tree.heading("acceptor_atom", text="Acceptor Atom")
self.hb_tree.heading("distance", text="H...A (Å)")
self.hb_tree.heading("angle", text="Angle (°)")
self.hb_tree.heading("da_distance", text="D...A (Å)")
self.hb_tree.heading("type", text="Type")
self.hb_tree.heading("da_props", text="D-A Props")
self.hb_tree.heading("bs_int", text="B/S")
# Configure column widths
self.hb_tree.column("donor_res", width=120)
self.hb_tree.column("donor_atom", width=100)
self.hb_tree.column("acceptor_res", width=120)
self.hb_tree.column("acceptor_atom", width=100)
self.hb_tree.column("distance", width=90)
self.hb_tree.column("angle", width=90)
self.hb_tree.column("da_distance", width=90)
self.hb_tree.column("type", width=120)
self.hb_tree.column("da_props", width=100)
self.hb_tree.column("bs_int", width=70)
# Add scrollbars
hb_v_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.VERTICAL, command=self.hb_tree.yview
)
hb_h_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.HORIZONTAL, command=self.hb_tree.xview
)
self.hb_tree.configure(
yscrollcommand=hb_v_scrollbar.set, xscrollcommand=hb_h_scrollbar.set
)
self.hb_tree.grid(row=0, column=0, sticky="nsew")
hb_v_scrollbar.grid(row=0, column=1, sticky="ns")
hb_h_scrollbar.grid(row=1, column=0, sticky="ew")
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# Add search functionality
search_frame = ttk.Frame(hb_frame)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT)
self.hb_search_var = tk.StringVar()
search_entry = ttk.Entry(
search_frame, textvariable=self.hb_search_var, width=30
)
search_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Filter",
command=lambda: self._filter_results(
self.hb_tree, self.hb_search_var.get()
),
).pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Clear",
command=lambda: self._clear_filter(self.hb_tree, self.hb_search_var),
).pack(side=tk.LEFT, padx=5)
def _create_halogen_bonds_tab(self):
"""Create halogen bonds results tab."""
xb_frame = ttk.Frame(self.notebook)
self.notebook.add(xb_frame, text="Halogen Bonds")
# Create treeview for halogen bonds
tree_frame = ttk.Frame(xb_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
columns = (
"halogen_res",
"halogen_atom",
"acceptor_res",
"acceptor_atom",
"distance",
"angle",
"type",
"bs_interaction",
"da_properties",
)
self.xb_tree = ttk.Treeview(
tree_frame, columns=columns, show="headings", height=15
)
# Configure columns
self.xb_tree.heading("halogen_res", text="Halogen Residue")
self.xb_tree.heading("halogen_atom", text="Halogen Atom")
self.xb_tree.heading("acceptor_res", text="Acceptor Residue")
self.xb_tree.heading("acceptor_atom", text="Acceptor Atom")
self.xb_tree.heading("distance", text="X...A (Å)")
self.xb_tree.heading("angle", text="Angle (°)")
self.xb_tree.heading("type", text="Type")
self.xb_tree.heading("bs_interaction", text="B/S Interaction")
self.xb_tree.heading("da_properties", text="D-A Properties")
# Configure column widths
self.xb_tree.column("halogen_res", width=140)
self.xb_tree.column("halogen_atom", width=120)
self.xb_tree.column("acceptor_res", width=140)
self.xb_tree.column("acceptor_atom", width=120)
self.xb_tree.column("distance", width=90)
self.xb_tree.column("angle", width=90)
self.xb_tree.column("type", width=120)
self.xb_tree.column("bs_interaction", width=100)
self.xb_tree.column("da_properties", width=120)
# Add scrollbars
xb_v_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.VERTICAL, command=self.xb_tree.yview
)
xb_h_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.HORIZONTAL, command=self.xb_tree.xview
)
self.xb_tree.configure(
yscrollcommand=xb_v_scrollbar.set, xscrollcommand=xb_h_scrollbar.set
)
self.xb_tree.grid(row=0, column=0, sticky="nsew")
xb_v_scrollbar.grid(row=0, column=1, sticky="ns")
xb_h_scrollbar.grid(row=1, column=0, sticky="ew")
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# Add search functionality
search_frame = ttk.Frame(xb_frame)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT)
self.xb_search_var = tk.StringVar()
search_entry = ttk.Entry(
search_frame, textvariable=self.xb_search_var, width=30
)
search_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Filter",
command=lambda: self._filter_results(
self.xb_tree, self.xb_search_var.get()
),
).pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Clear",
command=lambda: self._clear_filter(self.xb_tree, self.xb_search_var),
).pack(side=tk.LEFT, padx=5)
def _create_pi_interactions_tab(self):
"""Create π interactions results tab."""
pi_frame = ttk.Frame(self.notebook)
self.notebook.add(pi_frame, text="π Interactions")
# Create treeview for π interactions
tree_frame = ttk.Frame(pi_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
columns = (
"donor_res",
"donor_atom",
"pi_res",
"distance",
"angle",
"type",
"da_props",
"bs_int",
)
self.pi_tree = ttk.Treeview(
tree_frame, columns=columns, show="headings", height=15
)
# Configure columns
self.pi_tree.heading("donor_res", text="Donor Residue")
self.pi_tree.heading("donor_atom", text="Donor Atom")
self.pi_tree.heading("pi_res", text="π Residue")
self.pi_tree.heading("distance", text="H...π (Å)")
self.pi_tree.heading("angle", text="Angle (°)")
self.pi_tree.heading("type", text="Type")
self.pi_tree.heading("da_props", text="D-A Props")
self.pi_tree.heading("bs_int", text="B/S")
# Configure column widths
self.pi_tree.column("donor_res", width=140)
self.pi_tree.column("donor_atom", width=120)
self.pi_tree.column("pi_res", width=140)
self.pi_tree.column("distance", width=110)
self.pi_tree.column("angle", width=110)
self.pi_tree.column("type", width=90)
self.pi_tree.column("da_props", width=100)
self.pi_tree.column("bs_int", width=70)
# Add scrollbars
pi_v_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.VERTICAL, command=self.pi_tree.yview
)
pi_h_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.HORIZONTAL, command=self.pi_tree.xview
)
self.pi_tree.configure(
yscrollcommand=pi_v_scrollbar.set, xscrollcommand=pi_h_scrollbar.set
)
self.pi_tree.grid(row=0, column=0, sticky="nsew")
pi_v_scrollbar.grid(row=0, column=1, sticky="ns")
pi_h_scrollbar.grid(row=1, column=0, sticky="ew")
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# Add search functionality
search_frame = ttk.Frame(pi_frame)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT)
self.pi_search_var = tk.StringVar()
search_entry = ttk.Entry(
search_frame, textvariable=self.pi_search_var, width=30
)
search_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Filter",
command=lambda: self._filter_results(
self.pi_tree, self.pi_search_var.get()
),
).pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Clear",
command=lambda: self._clear_filter(self.pi_tree, self.pi_search_var),
).pack(side=tk.LEFT, padx=5)
def _create_cooperativity_chains_tab(self):
"""Create cooperativity chains results tab."""
coop_frame = ttk.Frame(self.notebook)
self.notebook.add(coop_frame, text="Cooperativity Chains")
# Create treeview for cooperativity chains
tree_frame = ttk.Frame(coop_frame)
tree_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
columns = ("chain_id", "chain_length", "chain_description")
self.coop_tree = ttk.Treeview(
tree_frame, columns=columns, show="headings", height=15
)
# Configure columns
self.coop_tree.heading("chain_id", text="Chain ID")
self.coop_tree.heading("chain_length", text="Length")
self.coop_tree.heading("chain_description", text="Chain Description")
# Configure column widths
self.coop_tree.column("chain_id", width=100)
self.coop_tree.column("chain_length", width=100)
self.coop_tree.column("chain_description", width=1000)
# Add scrollbars
coop_v_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.VERTICAL, command=self.coop_tree.yview
)
coop_h_scrollbar = ttk.Scrollbar(
tree_frame, orient=tk.HORIZONTAL, command=self.coop_tree.xview
)
self.coop_tree.configure(
yscrollcommand=coop_v_scrollbar.set, xscrollcommand=coop_h_scrollbar.set
)
self.coop_tree.grid(row=0, column=0, sticky="nsew")
coop_v_scrollbar.grid(row=0, column=1, sticky="ns")
coop_h_scrollbar.grid(row=1, column=0, sticky="ew")
tree_frame.grid_rowconfigure(0, weight=1)
tree_frame.grid_columnconfigure(0, weight=1)
# Bind double-click event to visualize chain
self.coop_tree.bind("<Double-1>", self._on_chain_double_click)
# Add info label
info_frame = ttk.Frame(coop_frame)
info_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(
info_frame,
text="Potential Cooperative Chains: Sequences where acceptors also act as donors",
).pack(side=tk.LEFT)
# Add search functionality
search_frame = ttk.Frame(coop_frame)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="Search:").pack(side=tk.LEFT)
self.coop_search_var = tk.StringVar()
search_entry = ttk.Entry(
search_frame, textvariable=self.coop_search_var, width=30
)
search_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Filter",
command=lambda: self._filter_results(
self.coop_tree, self.coop_search_var.get()
),
).pack(side=tk.LEFT, padx=5)
ttk.Button(
search_frame,
text="Clear",
command=lambda: self._clear_filter(self.coop_tree, self.coop_search_var),
).pack(side=tk.LEFT, padx=5)
# Add visualization button
if VISUALIZATION_AVAILABLE:
ttk.Button(
search_frame,
text="Visualize Selected Chain",
command=self._visualize_selected_chain,
).pack(side=tk.RIGHT, padx=5)
[docs]
def update_results(self, analyzer: MolecularInteractionAnalyzer) -> None:
"""Update the results panel with new analysis results.
Refreshes all result displays with data from the provided
analyzer instance.
:param analyzer: MolecularInteractionAnalyzer instance with results
:type analyzer: MolecularInteractionAnalyzer
:returns: None
:rtype: None
"""
self.analyzer = analyzer
# Update all tabs
self._update_summary()
self._update_hydrogen_bonds()
self._update_halogen_bonds()
self._update_pi_interactions()
self._update_cooperativity_chains()
def _update_summary(self):
"""Update the summary tab."""
if not self.analyzer:
return
self.summary_text.delete(1.0, tk.END)
# Insert header
self.summary_text.insert(tk.END, "HBAT Analysis Summary\n", "header")
self.summary_text.insert(tk.END, "=" * 50 + "\n\n")
# Get summary
summary = self.analyzer.get_summary()
# Timing information
if "timing" in summary:
self.summary_text.insert(tk.END, "Analysis Performance:\n", "subheader")
timing = summary["timing"]
self.summary_text.insert(
tk.END,
f" Analysis Duration: {timing['analysis_duration_seconds']:.3f} seconds\n\n",
)
# PDB fixing information
if "pdb_fixing" in summary:
pdb_info = summary["pdb_fixing"]
self.summary_text.insert(tk.END, "PDB Structure Processing:\n", "subheader")
if pdb_info.get("applied", False):
self.summary_text.insert(
tk.END, f" PDB Fixing: Applied using {pdb_info['method']}\n"
)
self.summary_text.insert(
tk.END, f" Original Atoms: {pdb_info['original_atoms']}\n"
)
self.summary_text.insert(
tk.END, f" Fixed Atoms: {pdb_info['fixed_atoms']}\n"
)
if pdb_info.get("added_hydrogens", 0) > 0:
self.summary_text.insert(
tk.END,
f" Added Hydrogens: {pdb_info['added_hydrogens']} "
f"({pdb_info['original_hydrogens']} → {pdb_info['fixed_hydrogens']})\n",
)
self.summary_text.insert(
tk.END, f" Re-detected Bonds: {pdb_info['redetected_bonds']}\n"
)
elif "error" in pdb_info:
self.summary_text.insert(
tk.END, f" PDB Fixing: Failed ({pdb_info['error']})\n"
)
else:
self.summary_text.insert(tk.END, " PDB Fixing: Not applied\n")
self.summary_text.insert(tk.END, "\n")
# Insert summary statistics
self.summary_text.insert(tk.END, "Interaction Counts:\n", "subheader")
self.summary_text.insert(
tk.END, f" Hydrogen Bonds: {summary['hydrogen_bonds']['count']}\n"
)
self.summary_text.insert(
tk.END, f" Halogen Bonds: {summary['halogen_bonds']['count']}\n"
)
self.summary_text.insert(
tk.END, f" π Interactions: {summary['pi_interactions']['count']}\n"
)
self.summary_text.insert(
tk.END,
f" Cooperativity Chains: {summary['cooperativity_chains']['count']}\n",
)
self.summary_text.insert(
tk.END, f" Total Interactions: {summary['total_interactions']}\n\n"
)
# Bond detection statistics
if "bond_detection" in summary:
bond_stats = summary["bond_detection"]
self.summary_text.insert(tk.END, "Bond Detection:\n", "subheader")
self.summary_text.insert(
tk.END, f" Total Bonds Detected: {bond_stats['total_bonds']}\n"
)
if bond_stats["breakdown"]:
self.summary_text.insert(tk.END, " Detection Methods:\n")
for method, stats in bond_stats["breakdown"].items():
method_name = method.replace("_", " ").title()
self.summary_text.insert(
tk.END,
f" {method_name}: {stats['count']} ({stats['percentage']}%)\n",
)
self.summary_text.insert(tk.END, "\n")
# Detailed interaction statistics
if summary["hydrogen_bonds"]["count"] > 0:
self.summary_text.insert(tk.END, "Hydrogen Bond Statistics:\n", "subheader")
hb_data = summary["hydrogen_bonds"]
self.summary_text.insert(
tk.END,
f" Average H...A Distance: {hb_data['average_distance']:.2f} Å\n",
)
self.summary_text.insert(
tk.END, f" Average Angle: {hb_data['average_angle']:.1f}°\n"
)
# Bond type distribution
if "bond_types" in hb_data:
self.summary_text.insert(tk.END, f" Bond Types:\n")
for bond_type, count in sorted(hb_data["bond_types"].items()):
self.summary_text.insert(tk.END, f" {bond_type}: {count}\n")
self.summary_text.insert(tk.END, "\n")
if summary["halogen_bonds"]["count"] > 0:
self.summary_text.insert(tk.END, "Halogen Bond Statistics:\n", "subheader")
xb_data = summary["halogen_bonds"]
self.summary_text.insert(
tk.END,
f" Average X...A Distance: {xb_data['average_distance']:.2f} Å\n",
)
self.summary_text.insert(
tk.END, f" Average Angle: {xb_data['average_angle']:.1f}°\n"
)
# Bond type distribution
if "bond_types" in xb_data:
self.summary_text.insert(tk.END, f" Bond Types:\n")
for bond_type, count in sorted(xb_data["bond_types"].items()):
self.summary_text.insert(tk.END, f" {bond_type}: {count}\n")
self.summary_text.insert(tk.END, "\n")
if summary["pi_interactions"]["count"] > 0:
self.summary_text.insert(tk.END, "π Interaction Statistics:\n", "subheader")
pi_data = summary["pi_interactions"]
self.summary_text.insert(
tk.END,
f" Average H...π Distance: {pi_data['average_distance']:.2f} Å\n",
)
self.summary_text.insert(
tk.END, f" Average Angle: {pi_data['average_angle']:.1f}°\n\n"
)
# Cooperativity chain statistics
if summary["cooperativity_chains"]["count"] > 0:
self.summary_text.insert(
tk.END, "Cooperativity Chain Statistics:\n", "subheader"
)
coop_data = summary["cooperativity_chains"]
self.summary_text.insert(tk.END, f" Total Chains: {coop_data['count']}\n")
# Chain types
if "types" in coop_data and coop_data["types"]:
type_counts = {}
for chain_type in coop_data["types"]:
type_counts[chain_type] = type_counts.get(chain_type, 0) + 1
self.summary_text.insert(tk.END, f" Chain Types:\n")
for chain_type, count in sorted(type_counts.items()):
self.summary_text.insert(tk.END, f" {chain_type}: {count}\n")
# Chain length distribution
if "chain_lengths" in coop_data:
self.summary_text.insert(tk.END, f" Chain Length Distribution:\n")
for length, count in sorted(coop_data["chain_lengths"].items()):
self.summary_text.insert(
tk.END, f" Length {length}: {count} chains\n"
)
self.summary_text.insert(tk.END, "\n")
# Add some example interactions
if self.analyzer.hydrogen_bonds:
self.summary_text.insert(tk.END, "Sample Hydrogen Bonds:\n", "subheader")
for i, hb in enumerate(self.analyzer.hydrogen_bonds[:5]):
self.summary_text.insert(tk.END, f" {i+1}. {hb}\n")
if len(self.analyzer.hydrogen_bonds) > 5:
self.summary_text.insert(
tk.END,
f" ... and {len(self.analyzer.hydrogen_bonds) - 5} more\n\n",
)
if self.analyzer.halogen_bonds:
self.summary_text.insert(tk.END, "Sample Halogen Bonds:\n", "subheader")
for i, xb in enumerate(self.analyzer.halogen_bonds[:3]):
self.summary_text.insert(tk.END, f" {i+1}. {xb}\n")
if len(self.analyzer.halogen_bonds) > 3:
self.summary_text.insert(
tk.END, f" ... and {len(self.analyzer.halogen_bonds) - 3} more\n\n"
)
if self.analyzer.pi_interactions:
self.summary_text.insert(tk.END, "Sample π Interactions:\n", "subheader")
for i, pi in enumerate(self.analyzer.pi_interactions[:3]):
self.summary_text.insert(tk.END, f" {i+1}. {pi}\n")
if len(self.analyzer.pi_interactions) > 3:
self.summary_text.insert(
tk.END, f" ... and {len(self.analyzer.pi_interactions) - 3} more\n"
)
def _update_hydrogen_bonds(self):
"""Update the hydrogen bonds tab."""
if not self.analyzer:
return
# Clear existing items
for item in self.hb_tree.get_children():
self.hb_tree.delete(item)
# Add hydrogen bonds
for hb in self.analyzer.hydrogen_bonds:
self.hb_tree.insert(
"",
tk.END,
values=(
hb.donor_residue,
hb.donor.name,
hb.acceptor_residue,
hb.acceptor.name,
f"{hb.distance:.2f}",
f"{math.degrees(hb.angle):.1f}",
f"{hb.donor_acceptor_distance:.2f}",
hb.bond_type,
hb.donor_acceptor_properties,
hb.get_backbone_sidechain_interaction(),
),
)
def _update_halogen_bonds(self):
"""Update the halogen bonds tab."""
if not self.analyzer:
return
# Clear existing items
for item in self.xb_tree.get_children():
self.xb_tree.delete(item)
# Add halogen bonds
for xb in self.analyzer.halogen_bonds:
self.xb_tree.insert(
"",
tk.END,
values=(
xb.halogen_residue,
xb.halogen.name,
xb.acceptor_residue,
xb.acceptor.name,
f"{xb.distance:.2f}",
f"{math.degrees(xb.angle):.1f}",
xb.bond_type,
xb.get_backbone_sidechain_interaction(),
xb.donor_acceptor_properties,
),
)
def _update_pi_interactions(self):
"""Update the π interactions tab."""
if not self.analyzer:
return
# Clear existing items
for item in self.pi_tree.get_children():
self.pi_tree.delete(item)
# Add π interactions
for pi in self.analyzer.pi_interactions:
self.pi_tree.insert(
"",
tk.END,
values=(
pi.donor_residue,
pi.donor.name,
pi.pi_residue,
f"{pi.distance:.2f}",
f"{math.degrees(pi.angle):.1f}",
pi.get_interaction_type_display(),
pi.donor_acceptor_properties,
pi.get_backbone_sidechain_interaction(),
),
)
def _update_cooperativity_chains(self):
"""Update the cooperativity chains tab."""
if not self.analyzer:
return
# Clear existing items
for item in self.coop_tree.get_children():
self.coop_tree.delete(item)
# Add cooperativity chains
for i, chain in enumerate(self.analyzer.cooperativity_chains, 1):
# Create chain description
chain_desc = self._format_chain_description(chain)
self.coop_tree.insert(
"", tk.END, values=(f"Chain-{i}", chain.chain_length, chain_desc)
)
def _format_chain_description(self, chain) -> str:
"""Format a chain description for display."""
if not chain.interactions:
return "Empty chain"
parts = []
for i, interaction in enumerate(chain.interactions):
if i == 0:
# First interaction: show donor
donor_res = interaction.get_donor_residue()
donor_atom = interaction.get_donor_atom()
donor_name = donor_atom.name if donor_atom else "?"
parts.append(f"{donor_res}({donor_name})")
# Add interaction symbol and acceptor
acceptor_res = interaction.get_acceptor_residue()
if interaction.get_acceptor_atom():
acceptor_name = interaction.get_acceptor_atom().name
acceptor_str = f"{acceptor_res}({acceptor_name})"
else:
acceptor_str = acceptor_res # For π interactions
# Get interaction symbol
if interaction.interaction_type == "H-Bond":
symbol = " -> "
elif interaction.interaction_type == "X-Bond":
symbol = " =X=> "
elif interaction.interaction_type == "π–Inter":
symbol = " ~π~> "
else:
symbol = " -> "
angle_str = f"[{math.degrees(interaction.angle):.1f}°]"
parts.append(f"{symbol}{acceptor_str} {angle_str}")
return "".join(parts)
def _filter_results(self, tree, search_term):
"""Filter tree results based on search term."""
if not search_term:
return
# Hide items that don't match the search term
for item in tree.get_children():
values = tree.item(item)["values"]
match = any(search_term.lower() in str(value).lower() for value in values)
if not match:
tree.detach(item)
def _clear_filter(self, tree, search_var):
"""Clear filter and show all results."""
search_var.set("")
# Refresh the tree by updating the corresponding data
if self.analyzer:
if tree == self.hb_tree:
self._update_hydrogen_bonds()
elif tree == self.xb_tree:
self._update_halogen_bonds()
elif tree == self.pi_tree:
self._update_pi_interactions()
elif tree == self.coop_tree:
self._update_cooperativity_chains()
[docs]
def clear_results(self) -> None:
"""Clear all results from the panel.
Removes all displayed results and resets the panel to
its initial empty state.
:returns: None
:rtype: None
"""
self.analyzer = None
# Clear text widgets
self.summary_text.delete(1.0, tk.END)
# Clear treeviews
for item in self.hb_tree.get_children():
self.hb_tree.delete(item)
for item in self.xb_tree.get_children():
self.xb_tree.delete(item)
for item in self.pi_tree.get_children():
self.pi_tree.delete(item)
for item in self.coop_tree.get_children():
self.coop_tree.delete(item)
# Clear all search filters
self.hb_search_var.set("")
self.xb_search_var.set("")
self.pi_search_var.set("")
self.coop_search_var.set("")
# Add placeholder text
self.summary_text.insert(tk.END, "No analysis results available.\n\n")
self.summary_text.insert(
tk.END, "Please load a PDB file and run analysis to see results."
)
def _visualize_selected_chain(self):
"""Visualize the selected cooperativity chain in a new window."""
if not VISUALIZATION_AVAILABLE:
messagebox.showerror(
"Error",
"Visualization libraries (networkx, matplotlib) are not available.",
)
return
selection = self.coop_tree.selection()
if not selection:
messagebox.showwarning(
"Warning", "Please select a cooperativity chain to visualize."
)
return
item = selection[0]
values = self.coop_tree.item(item)["values"]
chain_id = values[0] # Chain-1, Chain-2, etc.
# Get the chain index from the ID
try:
chain_index = int(chain_id.split("-")[1]) - 1
if chain_index < 0 or chain_index >= len(
self.analyzer.cooperativity_chains
):
raise IndexError
chain = self.analyzer.cooperativity_chains[chain_index]
except (ValueError, IndexError):
messagebox.showerror("Error", "Invalid chain selection.")
return
# Create the visualization window using the new module
ChainVisualizationWindow(self.parent, chain, chain_id)
def _on_chain_double_click(self, event):
"""Handle double-click on cooperativity chain to open visualization."""
# Get the item that was double-clicked
item = self.coop_tree.identify_row(event.y)
if item:
# Select the item first
self.coop_tree.selection_set(item)
# Then visualize it
self._visualize_selected_chain()