Source code for visual.ReportGenerator

from __future__ import annotations

import json
import html
from pathlib import Path
from typing import Dict, List
from typing_extensions import Self
import matplotlib as mpl
import matplotlib.colors as mcolors
from graph.PPINetwork import PPINetwork

ADD_SECTION_STRING = "__ADD_MORE_SECTIONS__"


[docs] class ReportGenerator: """ Generate interactive HTML reports for protein-protein interaction (PPI) networks. This class creates standalone HTML reports with embedded Cytoscape.js visualizations and analysis plots. Reports include interactive controls for selecting which node/edge attributes determine visual properties (color, size, width). Attributes: html_template (str): The base HTML template with placeholders for dynamic content. """ def __init__(self, base_file_html_path: str = "./visual/web/base.html"): """ Initialize the ReportGenerator with a base HTML template. Args: base_file_html_path: Path to the base HTML template file. Defaults to "./visual/web/base.html". Raises: FileNotFoundError: If the base HTML template file does not exist. IOError: If the template file cannot be read. """ with open(base_file_html_path, 'r') as file: self.html_template = file.read() @staticmethod def _make_colormap_palette(cmap_name: str = "viridis", n_colors: int = 256) -> List[str]: """ Generate a list of hex color codes from a matplotlib colormap. Args: cmap_name: Name of a matplotlib colormap. Defaults to "viridis". Falls back to "viridis" if the name is not found. n_colors: Number of colors to generate. Defaults to 256. Returns: List[str]: List of hex color codes (e.g., ["#440154", "#482878", ...]). Note: If n_colors is <= 1, only one color is returned. """ try: cmap = mpl.colormaps[cmap_name] except KeyError: cmap = mpl.colormaps["viridis"] if n_colors <= 1: return [mcolors.to_hex(cmap(0.0))] return [ mcolors.to_hex(cmap(i / (n_colors - 1))) for i in range(n_colors) ]
[docs] def add_cytoscape_html_report( self, network: PPINetwork, title: str = "Protein Network Report", node_color_attr: str = "pagerank", node_size_attr: str = "degree", edge_width_attr: str = "combined_score", cmap_name: str = "viridis", min_node_size: float = 35, max_node_size: float = 100, min_edge_width: float = 1, max_edge_width: float = 10, ) -> "ReportGenerator": """ Add an interactive Cytoscape.js network visualization to the report. The visualization includes dropdown menus for dynamically selecting which node/edge attributes control color, size, and width, allowing interactive exploration of different network properties. Args: network: The PPINetwork object to visualize. title: Title for the network report. Defaults to "Protein Network Report". node_color_attr: Node attribute to control node color. Defaults to "pagerank". node_size_attr: Node attribute to control node size. Defaults to "degree". edge_width_attr: Edge attribute to control edge width. Defaults to "combined_score". cmap_name: Name of the matplotlib colormap. Defaults to "viridis". min_node_size: Minimum node size in pixels. Defaults to 35. max_node_size: Maximum node size in pixels. Defaults to 100. min_edge_width: Minimum edge width in pixels. Defaults to 1. max_edge_width: Maximum edge width in pixels. Defaults to 10. Returns: ReportGenerator: Self for method chaining. Note: The generated HTML tries to load cytoscape.min.js locally first. If not found, it loads Cytoscape.js from the online CDN. """ elements = network.networkx_to_cytoscape_elements( node_color_attr=node_color_attr, node_size_attr=node_size_attr, edge_width_attr=edge_width_attr, cmap_name=cmap_name, min_node_size=min_node_size, max_node_size=max_node_size, min_edge_width=min_edge_width, max_edge_width=max_edge_width, ) node_numeric_attrs = network.get_numeric_node_attrs() edge_numeric_attrs = network.get_numeric_edge_attrs() color_palette = self._make_colormap_palette(cmap_name=cmap_name, n_colors=256) elements_json = json.dumps(elements, ensure_ascii=False) node_numeric_attrs_json = json.dumps(node_numeric_attrs, ensure_ascii=False) edge_numeric_attrs_json = json.dumps(edge_numeric_attrs, ensure_ascii=False) color_palette_json = json.dumps(color_palette, ensure_ascii=False) safe_title = html.escape(title) self.html_template = ( self.html_template .replace("__TITLE__", safe_title) .replace("__NODE_COLOR_ATTR__", html.escape(node_color_attr)) .replace("__NODE_SIZE_ATTR__", html.escape(node_size_attr)) .replace("__EDGE_WIDTH_ATTR__", html.escape(edge_width_attr)) .replace("__NODE_COLOR_ATTR_JS__", json.dumps(node_color_attr)[1:-1]) .replace("__NODE_SIZE_ATTR_JS__", json.dumps(node_size_attr)[1:-1]) .replace("__EDGE_WIDTH_ATTR_JS__", json.dumps(edge_width_attr)[1:-1]) .replace("__N_NODES__", str(network.G.number_of_nodes())) .replace("__N_EDGES__", str(network.G.number_of_edges())) .replace("__MIN_NODE_SIZE__", str(float(min_node_size))) .replace("__MAX_NODE_SIZE__", str(float(max_node_size))) .replace("__MIN_EDGE_WIDTH__", str(float(min_edge_width))) .replace("__MAX_EDGE_WIDTH__", str(float(max_edge_width))) .replace("__ELEMENTS_JSON__", elements_json) .replace("__NODE_NUMERIC_ATTRS_JSON__", node_numeric_attrs_json) .replace("__EDGE_NUMERIC_ATTRS_JSON__", edge_numeric_attrs_json) .replace("__COLOR_PALETTE_JSON__", color_palette_json) ) return self
[docs] def add_section(self, html_to_add: str) -> "ReportGenerator": """ Add a new section to the HTML report. Adds the provided HTML content by replacing the placeholder string. Args: html_to_add: HTML string to insert into the report. Returns: ReportGenerator: Self for method chaining. Note: The placeholder string is defined as `__ADD_MORE_SECTIONS__` and should be present in the base HTML template. """ self.html_template = self.html_template.replace( ADD_SECTION_STRING, f"{html_to_add} \n {ADD_SECTION_STRING}" ) return self
[docs] def add_section_from_file( self, file: str, replacements: Dict[str, str] | None = None, ) -> "ReportGenerator": """ Add a new section to the HTML report by reading from an HTML file. Optionally performs string replacements before inserting the content. Args: file: Path to an HTML file to read and insert. replacements: Optional dictionary mapping strings to replace in the file content. Defaults to None. Returns: ReportGenerator: Self for method chaining. Raises: FileNotFoundError: If the specified file does not exist. IOError: If the file cannot be read. Example: >>> gen = ReportGenerator() >>> gen.add_section_from_file( ... file="./plots.html", ... replacements={"__PLOT_1__": encoded_image_1, "__PLOT_2__": encoded_image_2} ... ) """ with open(file, 'r') as f: html_file = f.read() if replacements is not None: for to_replace, new_str in replacements.items(): html_file = html_file.replace( to_replace, new_str ) return self.add_section(html_to_add=html_file)
[docs] def generate_report_file(self, output_file: str = "protein_network_report.html") -> str: """ Generate and save the final HTML report file. Args: output_file: Output file path for the HTML report. Defaults to "protein_network_report.html". Returns: str: The absolute path to the generated report file. Raises: IOError: If the report file cannot be written. """ html_report = self.html_template.replace(ADD_SECTION_STRING, "") output_path = Path(output_file) output_path.write_text(html_report, encoding="utf-8") return str(output_path)