Source code for mkt.databases.colors

"""Color palettes and colormap helpers for amino acids and percentile-based plotting.

Defines the :class:`AminoAcidPalette`, amino-acid-to-color mapping helpers, and
utilities for interpolating colors and building percentile colormaps with legends.
"""

from enum import Enum

import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
import numpy as np
from mkt.schema.io_utils import save_plot


[docs] def map_aa_to_single_letter_code( aa: str, ) -> str | None: """Map any amino acid input from name or 3-letter or single-letter code to validated single-letter code. Parameters ---------- aa : str Amino acid name or 3-letter or single-letter code Returns ------- str | None Single-letter amino acid code if valid; otherwise None Notes ----- 3-letter and single-letter AA converted to uppercase; AA name converted to lowercase """ # Check if 3-letter code provided if len(aa) == 3: aa_clean = aa.upper() dict_aa = {entry[2]: entry[1] for entry in AA_MAPPING} if aa_clean in dict_aa.keys(): return dict_aa[aa_clean] else: print(f"Invalid 3-letter amino acid: {aa.upper()}") return None # Check if amino acid name provided elif len(aa) > 3: aa_clean = aa.lower() dict_aa = {entry[0]: entry[1] for entry in AA_MAPPING} if aa_clean in dict_aa.keys(): return dict_aa[aa_clean] else: print(f"Invalid amino acid name: {aa.lower()}") return None # Check if single-letter code provided elif len(aa) == 1: aa_clean = aa.upper() if aa_clean in [entry[1] for entry in AA_MAPPING]: return aa.upper() else: print(f"Invalid single-letter amino acid: {aa_clean}") return None else: # Invalid amino acid input length (0 or 2 characters) print(f"Length error and invalid amino acid: {aa}") return None
[docs] def map_single_letter_aa_to_color(aa, dict_color): """Map amino acid to color using specified dictionary. Parameters ---------- aa : str Amino acid name or 3-letter or single-letter code dict_color : dict[str, str] Dictionary mapping single-letter amino acid to color Returns ------- str Color that corresponds to AA single-letter code in selected palette """ aa_clean = map_aa_to_single_letter_code(aa) if aa_clean is None: print(f"{aa} is an invalid amino acid; using '-' as default symbol.") aa_clean = "-" if aa_clean in dict_color.keys(): return dict_color[aa_clean] else: print(f"Invalid amino acid: {aa}")
[docs] class AminoAcidPalette(str, Enum): """Enumeration of supported color palettes for amino acid visualization.""" ALPHABET_PROJECT = "ALPHABET_PROJECT" ASAP = "ASAP" RASMOL = "RASMOL" SHAPELY = "SHAPELY" CLUSTALX = "CLUSTALX" ZAPPO = "ZAPPO"
AA_MAPPING = [ ("alanine", "A", "ALA"), ("arginine", "R", "ARG"), ("asparagine", "N", "ASN"), ("aspartic acid", "D", "ASP"), ("cysteine", "C", "CYS"), ("glutamic acid", "E", "GLU"), ("glutamine", "Q", "GLN"), ("glycine", "G", "GLY"), ("histidine", "H", "HIS"), ("isoleucine", "I", "ILE"), ("leucine", "L", "LEU"), ("lysine", "K", "LYS"), ("methionine", "M", "MET"), ("phenylalanine", "F", "PHE"), ("proline", "P", "PRO"), ("serine", "S", "SER"), ("threonine", "T", "THR"), ("tryptophan", "W", "TRP"), ("tyrosine", "Y", "TYR"), ("valine", "V", "VAL"), ] """list[tuple[str, str, str]]: Set of amino acid mappings (lower) to single-letter code and 3-letter code (upper)""" DICT_COLORS = { # https://en.wikipedia.org/wiki/Help:Distinguishable_colors "ALPHABET_PROJECT": { "DICT_COLORS": { "-": "#FFFFFF", "A": "#F0A3FF", "B": "#0075DC", "C": "#993F00", "D": "#4C005C", "E": "#191919", "F": "#005C31", "G": "#2BCE48", "H": "#FFCC99", "I": "#808080", "J": "#94FFB5", "K": "#8F7C00", "L": "#9DCC00", "M": "#C20088", "N": "#003380", "O": "#FFA405", "P": "#FFA8BB", "Q": "#426600", "R": "#FF0010", "S": "#5EF1F2", "T": "#00998F", "U": "#E0FF66", "V": "#740AFF", "W": "#990000", "X": "#FFFF80", "Y": "#FFE100", "Z": "#FF5005", }, "DICT_ANNOTATION": None, }, # asapdiscovery-genetics/asapdiscovery/genetics/seq_alignment.py "ASAP": { "DICT_COLORS": { "A": "red", # Alanine (ALA) "R": "blue", # Arginine (ARG) "N": "green", # Asparagine (ASN) "D": "yellow", # Aspartic acid (ASP) "C": "orange", # Cysteine (CYS) "Q": "purple", # Glutamine (GLN) "E": "cyan", # Glutamic acid (GLU) "G": "magenta", # Glycine (GLY) "H": "pink", # Histidine (HIS) "I": "brown", # Isoleucine (ILE) "L": "gray", # Leucine (LEU) "K": "lime", # Lysine (LYS) "M": "teal", # Methionine (MET) "F": "navy", # Phenylalanine (PHE) "P": "olive", # Proline (PRO) "S": "maroon", # Serine (SER) "T": "silver", # Threonine (THR) "W": "gold", # Tryptophan (TRP) "Y": "skyblue", # Tyrosine (TYR) "V": "violet", # Valine (VAL) "-": "white", }, "DICT_ANNOTATION": None, }, # http://openrasmol.org/doc/rasmol.html#aminocolours "RASMOL": { "DICT_COLORS": { "A": "#C8C8C8", # Alanine (ALA) "R": "#145AFF", # Arginine (ARG) "N": "#00DCDC", # Asparagine (ASN) "D": "#E60A0A", # Aspartic acid (ASP) "C": "#E6E600", # Cysteine (CYS) "Q": "#00DCDC", # Glutamine (GLN) "E": "#E60A0A", # Glutamic acid (GLU) "G": "#EBEBEB", # Glycine (GLY) "H": "#8282D2", # Histidine (HIS) "I": "#0F820F", # Isoleucine (ILE) "L": "#0F820F", # Leucine (LEU) "K": "#145AFF", # Lysine (LYS) "M": "#E6E600", # Methionine (MET) "F": "#3232AA", # Phenylalanine (PHE) "P": "#DC9682", # Proline (PRO) "S": "#FA9600", # Serine (SER) "T": "#FA9600", # Threonine (THR) "W": "#B45AB4", # Tryptophan (TRP) "Y": "#3232AA", # Tyrosine (TYR) "V": "#0F820F", # Valine (VAL) "-": "#BEA06E", }, "DICT_ANNOTATION": { "A": "#C8C8C8", "R, K": "#145AFF", "N, Q": "#00DCDC", "D, E": "#E60A0A", "C, M": "#E6E600", "G": "#EBEBEB", "H": "#8282D2", "I, L, V": "#0F820F", "F, Y": "#3232AA", "P": "#DC9682", "S, T": "#FA9600", "W": "#B45AB4", "-": "#BEA06E", }, }, # http://openrasmol.org/doc/rasmol.html#shapelycolours # https://www.dnastar.com/manuals/MegAlignPro/17.3.1/en/topic/change-the-analysis-view-color-scheme "SHAPELY": { "DICT_COLORS": { "A": "#8CFF8C", # Alanine (ALA) "R": "#00007C", # Arginine (ARG) "N": "#FF7C70", # Asparagine (ASN) "D": "#A00042", # Aspartic acid (ASP) "C": "#FFFF70", # Cysteine (CYS) "Q": "#FF7C70", # Glutamine (GLN) "E": "#A00042", # Glutamic acid (GLU) "G": "#FFFFFF", # Glycine (GLY) "H": "#7070FF", # Histidine (HIS) "I": "#004C00", # Isoleucine (ILE) "L": "#004C00", # Leucine (LEU) "K": "#00007C", # Lysine (LYS) "M": "#FFFF70", # Methionine (MET) "F": "#534C42", # Phenylalanine (PHE) "P": "#525252", # Proline (PRO) "S": "#FF7042", # Serine (SER) "T": "#FF7042", # Threonine (THR) "W": "#4F4600", # Tryptophan (TRP) "Y": "#534C42", # Tyrosine (TYR) "V": "#004C00", # Valine (VAL) "-": "#000000", }, "DICT_ANNOTATION": { "A": "#8CFF8C", "R, K": "#00007C", "N, Q": "#FF7C70", "D, E": "#A00042", "C, M": "#FFFF70", "G": "#FFFFFF", "H": "#7070FF", "I, L, V": "#004C00", "F, Y": "#534C42", "P": "#525252", "S, T": "#FF7042", "W": "#4F4600", "-": "#000000", }, }, # https://www.jalview.org/help/html/colourSchemes/clustal.html "CLUSTALX": { "DICT_COLORS": { "A": "blue", # Alanine (ALA) "R": "red", # Arginine (ARG) "N": "green", # Asparagine (ASN) "D": "magenta", # Aspartic acid (ASP) "C": "pink", # Cysteine (CYS) "Q": "green", # Glutamine (GLN) "E": "magenta", # Glutamic acid (GLU) "G": "orange", # Glycine (GLY) "H": "cyan", # Histidine (HIS) "I": "blue", # Isoleucine (ILE) "L": "blue", # Leucine (LEU) "K": "red", # Lysine (LYS) "M": "blue", # Methionine (MET) "F": "blue", # Phenylalanine (PHE) "P": "yellow", # Proline (PRO) "S": "green", # Serine (SER) "T": "green", # Threonine (THR) "W": "blue", # Tryptophan (TRP) "Y": "cyan", # Tyrosine (TYR) "V": "blue", # Valine (VAL) "-": "white", }, "DICT_ANNOTATION": { "hydrophobic": "blue", # A,I,L,M,F,W,V "positive": "red", # K,R "negative": "magenta", # D,E "polar": "green", # N,Q,S,T "aromatic": "cyan", # H,Y "cysteine": "pink", # C "glycine": "orange", # G "proline": "yellow", # P }, }, # https://www.jalview.org/help/html/colourSchemes/zappo.html "ZAPPO": { "DICT_COLORS": { "A": "#ffafaf", # Alanine (ALA) "R": "#6464ff", # Arginine (ARG) "N": "#02ff00", # Asparagine (ASN) "D": "#ff0000", # Aspartic acid (ASP) "C": "#ffff00", # Cysteine (CYS) "Q": "#02ff00", # Glutamine (GLN) "E": "#ff0000", # Glutamic acid (GLU) "G": "#ff00ff", # Glycine (GLY) "H": "#6464ff", # Histidine (HIS) "I": "#ffafaf", # Isoleucine (ILE) "L": "#ffafaf", # Leucine (LEU) "K": "#6464ff", # Lysine (LYS) "M": "#ffafaf", # Methionine (MET) "F": "#ffc803", # Phenylalanine (PHE) "P": "#ff00ff", # Proline (PRO) "S": "#02ff00", # Serine (SER) "T": "#02ff00", # Threonine (THR) "W": "#ffc803", # Tryptophan (TRP) "Y": "#ffc803", # Tyrosine (TYR) "V": "#ffafaf", # Valine (VAL) "-": "#000000", }, "DICT_ANNOTATION": { "aliphatic hydrophobic": "#ffafaf", # A,I,L,M,V "aromatic": "#ffc803", # F,W,Y "positive": "#6464ff", # R,H,K "negative": "#ff0000", # D,E "hydrophilic": "#02ff00", # N,Q,S,T "conformationally special": "#ff00ff", # G,P "cysteine": "#ffff00", # C }, }, } """dict[dict[str, str], dict[str, str]]: Mapping amino acid to color using specified dictionary. Dictionaries include color schemes from: - 2010 Colour Alphabet Project (ALPHABET_PROJECT) - ASAP Discovery palette (ASAP) - RasMol amino color scheme (RASMOL) - Shapely amino color scheme (SHAPELY) - Clustal X color scheme (CLUSTALX) - Zappo color scheme (ZAPPO) Dictionary keys include: - DICT_COLORS: Dictionary mapping single-letter amino acid to color - DICT_ANNOTATION: Dictionary mapping amino acid groups to color if one exists """ DEFAULT_NULL_COLOR = "darkgray" """str: Default color for null/zero values in colormaps."""
[docs] def interpolate_color( norm_value: float, start_color_hex: str, end_color_hex: str ) -> str: """Interpolate between two colors based on normalized value. Parameters: ----------- norm_value : float Normalized value between 0 and 1. start_color_hex : str Starting color in hex format (e.g., "#FFFFFF"). end_color_hex : str Ending color in hex format (e.g., "#FF0000"). Returns: -------- str Interpolated color in hex format. """ # convert hex to RGB start_r = int(start_color_hex[1:3], 16) start_g = int(start_color_hex[3:5], 16) start_b = int(start_color_hex[5:7], 16) end_r = int(end_color_hex[1:3], 16) end_g = int(end_color_hex[3:5], 16) end_b = int(end_color_hex[5:7], 16) # interpolate interp_r = int(start_r + (end_r - start_r) * norm_value) interp_g = int(start_g + (end_g - start_g) * norm_value) interp_b = int(start_b + (end_b - start_b) * norm_value) return f"#{interp_r:02x}{interp_g:02x}{interp_b:02x}"
[docs] def percentile_colormap( values: list[float], color_stops: dict[int, tuple[str, str]], zero_color: str = DEFAULT_NULL_COLOR, ) -> list[str]: """Map numeric values to colors using percentile-based interpolation. Divides non-zero values into N equal percentile bins (where N is the number of color stops) and interpolates within each bin's color range. Parameters: ----------- values : list[float] Numeric values to map to colors. color_stops : dict[int, tuple[str, str]] Dict mapping bin number (1-indexed) to (start_hex, end_hex) tuples. The number of entries determines the number of percentile bins (e.g., 4 entries = quartiles, 5 entries = quintiles). zero_color : str Color for zero values (default: "darkgray"). Returns: -------- list[str] List of color strings (hex or named). """ n_bins = len(color_stops) # calculate percentile boundaries from non-zero values non_zero_values = sorted([v for v in values if v > 0]) if non_zero_values: n = len(non_zero_values) # boundaries at each 1/n_bins fraction (e.g., 4 bins -> 0.25, 0.50, 0.75) boundaries = [ non_zero_values[max(0, int(n * (i + 1) / n_bins) - 1)] for i in range(n_bins - 1) ] else: boundaries = [(i + 1) / n_bins for i in range(n_bins - 1)] list_color = [] for value in values: if value == 0: list_color.append(zero_color) else: # determine which bin and position within it bin_idx = n_bins # default to last bin q_min = boundaries[-1] if boundaries else 0 q_max = 1.0 for i, boundary in enumerate(boundaries): if value <= boundary: bin_idx = i + 1 q_min = boundaries[i - 1] if i > 0 else 0 q_max = boundary break # interpolate position within bin (0 to 1) t = (value - q_min) / (q_max - q_min) if q_max > q_min else 0.5 start_hex, end_hex = color_stops[bin_idx] list_color.append(interpolate_color(t, start_hex, end_hex)) return list_color
DICT_QUARTILE_HEATMAP_COLORMAP = { 1: ("#228B22", "#FFD700"), # green → yellow 2: ("#FFD700", "#FF8C00"), # yellow → orange 3: ("#FF8C00", "#FF0000"), # orange → red 4: ("#FF0000", "#8B0000"), # red → dark red } """dict[int, tuple[str, str]]: Quartile heatmap colormap for mutation visualization. Maps quartile bin number (1-indexed) to (start_hex, end_hex) tuples for use with ``percentile_colormap``. """ DICT_QUARTILE_HEATMAP_COLORMAP_PLASMA = { 1: ("#0D0887", "#7E03A8"), # deep indigo → violet 2: ("#7E03A8", "#CC4778"), # violet → magenta/rose 3: ("#CC4778", "#F89441"), # magenta/rose → orange 4: ("#F89441", "#F0F921"), # orange → bright yellow } """dict[int, tuple[str, str]]: Plasma-inspired colormap for mutation visualization. Colorblind-accessible. Travels purple→magenta→orange→yellow, avoiding green entirely. Maps quartile bin number (1-indexed) to (start_hex, end_hex) tuples for use with ``percentile_colormap``. """ DICT_QUARTILE_HEATMAP_COLORMAP_VIRIDIS = { 1: ("#440154", "#31688E"), # dark purple → teal-blue 2: ("#31688E", "#35B779"), # teal-blue → green 3: ("#35B779", "#90D743"), # green → lime 4: ("#90D743", "#FDE725"), # lime → yellow } """dict[int, tuple[str, str]]: Viridis-inspired colormap for mutation visualization. Perceptually uniform with good luminance gradient. Note: passes through green tones. Maps quartile bin number (1-indexed) to (start_hex, end_hex) tuples for use with ``percentile_colormap``. """
[docs] def generate_colormap_legend( color_stops: dict[int, tuple[str, str]], output_path: str | None = None, zero_color: str = DEFAULT_NULL_COLOR, n_gradient_steps: int = 256, null_steps: int | None = None, figsize: tuple[float, float] = (0.75, 5.5), ) -> None: """Generate a vertical colormap legend image (SVG and PNG) from color stops. Creates a vertical gradient bar from bottom (null/zero color) to top (highest density), with percentile tick labels at 0, 0.25, 0.5, 0.75, and 1. Colors are generated via ``percentile_colormap`` with a synthetic uniform dataset so the legend is guaranteed to match live usage. Parameters: ----------- color_stops : dict[int, tuple[str, str]] Dict mapping bin number (1-indexed) to (start_hex, end_hex) tuples, e.g., ``DICT_QUARTILE_HEATMAP_COLORMAP``. output_path : str | None Directory path to save the plot. If None, saves to the repo root. zero_color : str Color for the null/zero band at the bottom (default: ``DEFAULT_NULL_COLOR``). n_gradient_steps : int Number of interpolation steps per bin (default: 256). null_steps : int | None Height in pixels of the null/zero color band at the bottom. Defaults to 1/10 the height of one bin (``n_gradient_steps // 10``). figsize : tuple[float, float] Figure size in inches (width, height). Default: (1, 5). """ plt.rcParams["font.family"] = "Arial" n_bins = len(color_stops) # null band height: half the height of one bin by default if null_steps is None: null_steps = n_gradient_steps // 10 # generate gradient via percentile_colormap with synthetic uniform values so # the legend is guaranteed consistent with live usage of percentile_colormap n_total = n_bins * n_gradient_steps synthetic_values = [(i + 1) / n_total for i in range(n_total)] gradient_hex = percentile_colormap( synthetic_values, color_stops, zero_color=zero_color ) # build single-column RGBA array: null band at bottom, gradient above null_rgba = mcolors.to_rgba(zero_color) colors_col = [null_rgba] * null_steps + [mcolors.to_rgba(c) for c in gradient_hex] gradient = np.array(colors_col)[:, np.newaxis, :] # shape: (total_rows, 1, 4) total_height = null_steps + n_total fig, ax = plt.subplots(figsize=figsize) ax.imshow( gradient, aspect="auto", origin="lower", extent=[0, 1, 0, total_height], ) # "0" tick at the null/gradient boundary (where green starts); then one tick # per bin boundary up to "1" at the top tick_positions = [null_steps + i * n_gradient_steps for i in range(n_bins + 1)] tick_labels = ["0", "0.25", "0.5", "0.75", "1"] ax.set_yticks(tick_positions) ax.set_yticklabels(tick_labels, fontsize=9) ax.yaxis.set_ticks_position("right") ax.set_title("Mutational\ndensity", fontsize=10, loc="center", pad=6) ax.set_xticks([]) for spine in ax.spines.values(): spine.set_visible(False) fig.tight_layout() filename = "colormap_legend" desc = "Colormap legend" if output_path is None: save_plot(fig=fig, output_filename=filename, plot_type=desc) else: save_plot( fig=fig, output_filename=filename, output_path=output_path, plot_type=desc, bool_force_local=False, )
DICT_BIOCHEM_PROP_COLORS = { "Charge": "#1f77b4", "Volume": "#ff7f0e", "Polarity": "#2ca02c", } """dict[str, str]: Dictionary mapping biochemical properties to colors. Keys are property names (e.g., "Charge", "Volume", "Polarity"), and values are hex color codes. This dictionary can be used to look up colors for biochemical properties in visualizations. """
[docs] def readable_text_color(color) -> str: """Return ``"black"`` or ``"white"`` for legible text on a background color. Parameters ---------- color : str | tuple Any matplotlib-recognized color (hex string, named color, or RGB tuple). Returns ------- str ``"black"`` for light backgrounds, ``"white"`` for dark ones, chosen by perceived luminance (0.299 R + 0.587 G + 0.114 B). """ r, g, b = mcolors.to_rgb(color) return "black" if (0.299 * r + 0.587 * g + 0.114 * b) > 0.55 else "white"
# --- KLIFS hierarchical-conservation figures / interactive explorer --- COLOR_DARK_TEXT = "#2E2E2E" """Near-black grey for axis labels, ticks, and logo text.""" COLOR_CONSERVATION_PRIMARY = "#2E5B9C" """Primary conservation accent (>=threshold line; breakpoint node fill).""" COLOR_CONSERVATION_SECONDARY = "#7B3F8D" """Secondary conservation accent.""" CMAP_CRITICAL_DEPTH = "plasma" """Colormap for the per-KLIFS-column critical-depth track.""" COLOR_DEPTH_NA = "#D9D9D9" """Grey for KLIFS columns that never survive up (no finite critical depth).""" COLOR_TREE_BREAK = COLOR_CONSERVATION_PRIMARY """Tree node fill for a split that carries >=1 fixed-difference breakpoint.""" COLOR_TREE_NO_BREAK = "#C9C9C9" """Tree node fill for a split with no fixed difference (structural only).""" COLOR_TREE_MIXED_FAMILY = "#CFCFCF" """Branch color once a clade mixes families (not monophyletic).""" COLOR_TREE_PSEUDO = "#000000" """Family color for a pseudokinase-pure clade (and inherited-conserved logo letters).""" COLOR_LOGO_BAR = "#EFEFEF" """Neutral fill for the per-column logo frequency bars.""" COLOR_LOGO_SUBTHRESHOLD = "#C9C9C9" """Logo letter color for residues present but below the conservation threshold.""" COLOR_TREE_TABLE_NO_CONSENSUS = "#EAEAEA" """Static conservation-table cell fill where no residue is >=80% conserved in a leaf row.""" COLOR_TREE_TABLE_GAP = "#BDBDBD" """Static conservation-table cell fill where a gap ('-') is the >=80% consensus -- a mid grey distinct from the blank no-consensus cell and from the amino-acid palette greys (I = #808080, E = #191919); paired with a '-' glyph to disambiguate.""" COLOR_TREE_GUIDE = "#777777" """Thin guide line linking each dendrogram leaf to its conservation-table row.""" COLOR_TREE_FALLBACK = "#999999" """Fallback fill for a tree leaf whose kinase has no group/family color.""" COLOR_TREE_SPLIT_MARKER = "#D00000" """Dashed marker on the summary dendrogram showing the top/bottom detail-panel split.""" DICT_CONSURF_GRADE_COLORS = { 1: "#10C8D1", # most variable — teal 2: "#8CFFFF", 3: "#D7FFFF", 4: "#EAFFFF", 5: "#FFFFFF", # average — white 6: "#FCEDF4", 7: "#FAC9DE", 8: "#F07DAB", 9: "#A02560", # most conserved — maroon } """Canonical ConSurf 9-grade conservation palette (grade 1 = most variable teal, grade 9 = most conserved maroon). Hex values are the exact RGB tuples from the ConSurf server's ``rasmol_gradesPE_and_pipe.pm`` (github.com/Rostlab/ConSurf), so they carry over to PyMOL/Chimera structure coloring unchanged.""" COLOR_CONSURF_UNGRADED = "#FFFF96" """ConSurf's 'insufficient data' color (RGB 255,255,150) for positions with no grade — an all-gap alignment column, or a group below the minimum size for grading. Matches the grade-0 color in the ConSurf ``rasmol_gradesPE_and_pipe.pm``."""
[docs] def get_consurf_grade_color(grade: int | None) -> str: """Return the ConSurf palette color for a conservation grade. Parameters ---------- grade : int | None ConSurf grade (1-9), or None for an ungraded position. Returns ------- str Hex color from :data:`DICT_CONSURF_GRADE_COLORS`, or :data:`COLOR_CONSURF_UNGRADED` when ``grade`` is None. """ if grade is None: return COLOR_CONSURF_UNGRADED return DICT_CONSURF_GRADE_COLORS.get(grade, COLOR_CONSURF_UNGRADED)