# helixmind/heatmap.py """ HelixMind - Bioinformatics Toolkit Author: Aryan Dutt (https://github.com/biostackaryan) License: GNU GPL v3 Copyright (C) 2025 Aryan Dutt """ from typing import Optional, Sequence, Union import numpy as np import pandas as pd import plotly.graph_objects as go ArrayLike2D = Union[np.ndarray, pd.DataFrame, Sequence[Sequence[float]]] def _to_matrix_and_labels( data: ArrayLike2D, xlabels: Optional[Sequence[str]] = None, ylabels: Optional[Sequence[str]] = None ): """ Normalize input to (z, x, y) for Heatmap. - If DataFrame: rows -> y, columns -> x, values -> z; use index/columns as labels. - Else: convert to ndarray; generate default labels if not provided. """ if isinstance(data, pd.DataFrame): z = data.values x = list(data.columns) if xlabels is None else list(xlabels) y = list(data.index) if ylabels is None else list(ylabels) return z, x, y arr = np.asarray(data) if arr.ndim != 2: raise ValueError("Heatmap data must be 2D.") n_rows, n_cols = arr.shape x = list(xlabels) if xlabels is not None else [f"C{j}" for j in range(n_cols)] y = list(ylabels) if ylabels is not None else [f"R{i}" for i in range(n_rows)] return arr, x, y def make_heatmap_figure( data: ArrayLike2D, xlabels: Optional[Sequence[str]] = None, ylabels: Optional[Sequence[str]] = None, colorscale: str = "Viridis", zmin: Optional[float] = None, zmax: Optional[float] = None, show_annotations: bool = False, annotation_format: str = ".2f", # d3-format for texttemplate when using z values custom_text: Optional[Sequence[Sequence[str]]] = None, # same shape as z if provided xgap: int = 0, # px gap between cells horizontally ygap: int = 0, # px gap between cells vertically showscale: bool = True, title: Optional[str] = None, hovertemplate: Optional[str] = None, # e.g., "x=%{x}
y=%{y}
z=%{z:.3f}" ): """ Build a Plotly Heatmap figure for Dash. - data: 2D array-like or DataFrame. - xlabels/ylabels: axis labels if data not a DataFrame (or to override DF labels). - colorscale: Plotly colorscale name or custom list. - zmin/zmax: lock color scale range across multiple heatmaps. - show_annotations: overlay text on each cell. - annotation_format: d3 format for z text (e.g., '.2f', '.0%', '.3f'). - custom_text: 2D strings to display instead of z (must match z shape). - xgap/ygap: gaps in px to visually separate cells. - showscale: show colorbar. - title: optional title. - hovertemplate: custom hover text template. """ z, x, y = _to_matrix_and_labels(data, xlabels, ylabels) # Prepare text + template for annotations if requested text = None texttemplate = None textfont = None if show_annotations: if custom_text is not None: # Use custom text text = custom_text texttemplate = "%{text}" else: # Use z values with formatting # Note: Heatmap texttemplate supports d3-format via %{z:.2f}, %{z:.0%}, etc. texttemplate = f"%{{z:{annotation_format}}}" textfont = {"color": "black"} # ensures visibility on most colorscales hm = go.Heatmap( z=z, x=x, y=y, colorscale=colorscale, zmin=zmin, zmax=zmax, xgap=xgap, ygap=ygap, showscale=showscale, text=text, texttemplate=texttemplate, textfont=textfont, hovertemplate=hovertemplate, # if None, default hover is used hoverongaps=False, colorbar=dict(title="Value") ) fig = go.Figure(data=hm) fig.update_layout( title=title or "", xaxis=dict(title="", tickangle=0, automargin=True), yaxis=dict(title="", automargin=True, autorange="reversed"), # heatmaps often show first row at top margin=dict(l=60, r=20, t=60, b=60), template="plotly_white" ) return fig