Source code for mkt.databases.plot_config

"""Configuration dataclasses for plot_dataset_data.py.

Loads plot aesthetics and data source paths from YAML config files
via OmegaConf, following the pattern used in mkt_impact.
"""

from dataclasses import dataclass, field
from pathlib import Path

import seaborn as sns
from mkt.schema.constants import DICT_KINASE_GROUP_COLORS
from omegaconf import OmegaConf

# --- matplotlib rcParams ---


[docs] @dataclass class MatplotlibRCConfig: """Global matplotlib rcParams applied before any plot.""" svg_fonttype: str = "path" pdf_fonttype: int = 42 text_usetex: bool = False
# --- family color palette ---
[docs] @dataclass class FamilyColorConfig: """Color palette for kinase families. Two modes: 1. ``use_kinase_group_colors=True`` (default): uses ``DICT_KINASE_GROUP_COLORS`` from ``mkt.schema.constants`` — a curated, colorblind-friendly mapping. 2. ``use_kinase_group_colors=False``: builds colors from a seaborn palette (``palette_name`` / ``palette_n_colors``) with ``other_color`` for "Other". In both modes, ``families`` controls which families appear and their order. When ``families`` is None, the keys of ``DICT_KINASE_GROUP_COLORS`` are used. """ use_kinase_group_colors: bool = True palette_name: str = "tab10" palette_n_colors: int = 10 other_color: str = "#808080" families: list[str] | None = None
[docs] def get_colors(self) -> dict: """Return a dict mapping family name to color. Returns: -------- dict Mapping of family names to color values. """ if self.use_kinase_group_colors: # start from the curated constant dict base = dict(DICT_KINASE_GROUP_COLORS) if self.families is not None: # filter + reorder to only the requested families return {f: base.get(f, self.other_color) for f in self.families} return base # seaborn palette mode families = self.families if families is None: # fall back to curated dict when no explicit families given return dict(DICT_KINASE_GROUP_COLORS) colors = sns.color_palette(self.palette_name, n_colors=self.palette_n_colors) family_colors = {} non_other = [f for f in families if f != "Other"] for family in families: if family == "Other": family_colors[family] = self.other_color else: idx = non_other.index(family) family_colors[family] = colors[idx] return family_colors
# --- col_kinase / sequence-type colors (shared by venn + boxplot) ---
[docs] @dataclass class ColKinaseColorConfig: """RGB colors for sequence-type categories.""" construct_unaligned: list[float] = field(default_factory=lambda: [242, 101, 41]) klifs_region_aligned: list[float] = field(default_factory=lambda: [0, 51, 113]) klifs_residues_only: list[float] = field(default_factory=lambda: [88, 152, 255])
[docs] def as_rgb_dict(self) -> dict[str, tuple[float, float, float]]: """Return colors as 0-1 scaled RGB tuples keyed by category name.""" return { "construct_unaligned": tuple(v / 255 for v in self.construct_unaligned), "klifs_region_aligned": tuple(v / 255 for v in self.klifs_region_aligned), "klifs_residues_only": tuple(v / 255 for v in self.klifs_residues_only), }
# --- per-plot configs ---
[docs] @dataclass class DynamicRangePlotConfig: """Aesthetics for the dynamic-range histogram.""" figsize: list[float] = field(default_factory=lambda: [11, 6]) font_size: int = 14 axes_titlesize: int = 16 axes_labelsize: int = 14 figure_titlesize: int = 20 alpha: float = 0.25 bins: int = 100 color_pkis2: str = "blue" color_davis: str = "green" axvline_x: float = 99 axvline_color: str = "red" title_fontsize: int = 20 title_fontweight: str = "bold" title_y: float = 1.25 subtitle_fontsize: int = 16 subtitle_alpha: float = 0.75 subtitle_y: float = 1.16 axis_label_fontsize: int = 16 tick_labelsize: int = 14 filename: str = "dynamic_range_histogram"
[docs] @dataclass class RidgelinePlotConfig: """Aesthetics for the ridgeline plot.""" figsize: list[float] = field(default_factory=lambda: [10.5, 7.5]) overlap: float = 0.1 scale: float = 1.5 fill_alpha: float = 0.5 edgecolor: str = "black" edge_linewidth: float = 1.5 baseline_linewidth: float = 0.5 baseline_alpha: float = 0.3 ytick_fontsize: int = 20 title_fontsize: int = 22 title_fontweight: str = "bold" title_color_davis: str = "black" title_color_pkis2: str = "black" xtick_fontsize: int = 18 xlabel_fontsize: int = 20 xlabel_text: str = "% of RefSeq sequence contained in construct" grid_alpha: float = 0.3 filename: str = "ridgeline_plot"
[docs] @dataclass class StackedBarchartConfig: """Aesthetics for the stacked bar chart.""" figsize_width_per_source: float = 12 figsize_height: float = 7 layout_nrows: int = 1 stack_color_true: str = "#d3d3d3" stack_color_false: str = "#505050" bar_edgecolor: str = "black" bar_linewidth: float = 0.5 bar_alpha: float = 1.0 pct_label_fontsize: int = 20 pct_label_fontweight: str = "bold" pct_label_min_threshold: float = 5 xtick_fontsize: int = 16 xlabel_fontsize: int = 24 ylabel_fontsize: int = 24 title_fontsize: int = 26 title_fontweight: str = "bold" ytick_fontsize: int = 18 ylim_max: float = 105 legend_fontsize: int = 20 legend_title_fontsize: int = 20 legend_bbox_y: float = -0.1 bottom_adjust: float = 0.2 title_color_davis: str = "black" title_color_pkis2: str = "black" filename: str = "stacked_barchart"
[docs] @dataclass class VennDiagramConfig: """Aesthetics for the Venn diagram.""" figsize: list[float] = field(default_factory=lambda: [8, 8]) circle_alpha: float = 0.6 intersection_color: str = "lightgray" intersection_alpha: float = 0.4 set_label_fontsize: int = 16 set_label_fontweight: str = "bold" subset_label_fontsize: int = 14 title_fontsize: int = 22 title_fontweight: str = "bold" filename: str = "venn_diagram"
[docs] @dataclass class MetricsBoxplotConfig: """Aesthetics for the metrics boxplot.""" figsize: list[float] = field(default_factory=lambda: [12, 4.5]) box_widths: float = 0.6 box_alpha: float = 0.7 median_color: str = "black" median_linewidth: int = 2 whisker_color: str = "black" whisker_linewidth: float = 1.5 cap_color: str = "black" cap_linewidth: float = 1.5 jitter_std: float = 0.04 jitter_alpha: float = 0.6 jitter_size: int = 50 jitter_color: str = "black" xtick_fontsize: int = 14 ylabel_fontsize: int = 20 ylabel_text: str = "MSE (Z-Score)" title_fontsize: int = 22 title_fontweight: str = "bold" ytick_fontsize: int = 18 grid_alpha: float = 0.3 bracket_start_pct: float = 0.08 bracket_spacing_pct: float = 0.15 bracket_height_pct: float = 0.02 bracket_linewidth: float = 1.5 pvalue_fontsize: int = 14 pvalue_fontweight: str = "bold" title_color_davis: str = "black" title_color_pkis2: str = "black" filename: str = "metrics_boxplot"
[docs] @dataclass class SequenceSchematicConfig: """Aesthetics for the sequence input schematic.""" figsize: list[float] = field(default_factory=lambda: [14, 7]) rect_height: float = 0.6 gap_color: str = "white" ellipsis_color: str = "#888888" ellipsis_fontsize: int = 10 label_fontsize: int = 10 title_fontsize: int = 12 title_fontweight: str = "bold" panel_title_fontsize: int = 11 panel_title_fontweight: str = "bold" n_show_start: int = 40 n_show_end: int = 20 n_ellipsis: int = 5 filename: str = "sequence_input_schematic"
[docs] @dataclass class UpsetPlotConfig: """Aesthetics for the KinaseInfo source-coverage upset plot. Defaults reproduce the original (pre-config) figure for backwards compatibility. Use :meth:`preprint_2026` for the smaller publication size. """ figsize: list[float] = field(default_factory=lambda: [8.0, 4.0]) dict_colors: dict = field( default_factory=lambda: { "UniProt": "#00FF00", "Pfam": "#00FFFF", "KinCoRe": "#FF00FF", "KLIFS": "#FFA500", "KinHub": "#000000", } ) # upsetplot grid layout; ``element_size`` (points/cell) drives the plot-area # size when set, otherwise the figure is sized via ``figsize``. element_size: float | None = None intersection_plot_elements: int = 6 totals_plot_elements: int = 2 # close the dead whitespace between the totals bars and the category labels # (slides matrix left to fill) and trim the totals right-margin overhang tighten_totals_gap: bool = False totals_gap_margin: float = 0.01 # cap the intersections y-axis at the tallest bar so log minor ticks don't # crowd the headroom above it where the percentage labels float cap_intersection_ylim: bool = False pct_label_fontsize: int = 8 count_label_fontsize: int = 8 filename: str = "upset_plot"
[docs] @classmethod def preprint_2026(cls) -> "UpsetPlotConfig": """~5 x 3.5 in plot-area variant for the 2026 preprint figures. ``element_size`` is tuned empirically (with ``intersection_plot_elements`` / ``totals_plot_elements``) so the upset grid renders ~5 x 3.5 in rather than the near-square default; the figure is saved tight-cropped. """ cfg = cls() cfg.element_size = 30.0 cfg.intersection_plot_elements = 4 cfg.totals_plot_elements = 2 cfg.tighten_totals_gap = True cfg.cap_intersection_ylim = True return cfg
[docs] @dataclass class RegionGapViolinConfig: """Aesthetics for the combined UniProt->KLIFS map + region-gap violin figure. The figure stacks the UniProt-to-KLIFS residue map (top, spanning the full width) over two grouped violin panels (inter- and intra-region gaps) on separate log-scaled axes. All statistics are computed on the fly. """ figsize: list[float] = field(default_factory=lambda: [15.0, 10.9]) height_ratios: list[float] = field(default_factory=lambda: [1.0, 1.76]) width_ratios: list[float] = field(default_factory=lambda: [2.0, 1.0]) hspace: float = 0.12 wspace: float = 0.06 left_adjust: float = 0.05 right_adjust: float = 0.985 top_adjust: float = 0.97 bottom_adjust: float = 0.085 # --- map panel --- use_ribbon: bool = False inter_color: str = "#cfcfcf" intra_color: str = "#8c8c8c" ribbon_alpha: float = 0.30 map_name_fontsize: int = 21 map_range_fontsize: int = 17 map_track_fontsize: int = 25 map_region_fontsize: int = 12 map_ellipsis_fontsize: int = 32 map_legend_fontsize: int = 17 # --- violin panels --- violin_fontsize: int = 22 fill_alpha: float = 0.3 violin_width: float = 0.85 violin_linewidth: float = 1.0 violin_edgecolor: str = "#333333" jitter_size: float = 7.0 jitter_std: float = 0.06 jitter_alpha: float = 0.8 jitter_edgecolor: str = "black" jitter_linewidth: float = 0.25 # color for gaps whose two flanking regions differ (e.g. III–αC) jitter_mixed_color: str = "orange" grid_alpha: float = 0.3 text_color: str = "#333333" ylabel_text: str = "Number of residues" filename: str = "region_gap_violin"
[docs] @classmethod def preprint_2026(cls) -> "RegionGapViolinConfig": """Preset used by the 2026 preprint figures (current defaults).""" return cls()
# --- data sources ---
[docs] @dataclass class DataSourceConfig: """Paths to input data files (relative to repo root).""" davis_csv: str = "data/davis_data_processed.csv" pkis2_csv: str = "data/pkis2_data_processed.csv" metrics_csv: str = "data/2025_val_stable_metrics.csv"
# --- output ---
[docs] @dataclass class OutputConfig: """Output directory settings.""" subdir: str = "images" bool_svg: bool = True bool_png: bool = True bool_pdf: bool = False
# --- top-level config ---
[docs] @dataclass class PlotDatasetConfig: """Top-level config aggregating all sub-configs.""" matplotlib_rc: MatplotlibRCConfig = field(default_factory=MatplotlibRCConfig) family_colors: FamilyColorConfig = field(default_factory=FamilyColorConfig) col_kinase_colors: ColKinaseColorConfig = field( default_factory=ColKinaseColorConfig ) dynamic_range: DynamicRangePlotConfig = field( default_factory=DynamicRangePlotConfig ) ridgeline: RidgelinePlotConfig = field(default_factory=RidgelinePlotConfig) stacked_barchart: StackedBarchartConfig = field( default_factory=StackedBarchartConfig ) venn_diagram: VennDiagramConfig = field(default_factory=VennDiagramConfig) metrics_boxplot: MetricsBoxplotConfig = field(default_factory=MetricsBoxplotConfig) sequence_schematic: SequenceSchematicConfig = field( default_factory=SequenceSchematicConfig ) data_sources: DataSourceConfig = field(default_factory=DataSourceConfig) output: OutputConfig = field(default_factory=OutputConfig)
[docs] @classmethod def from_yaml(cls, config_path: str | Path) -> "PlotDatasetConfig": """Load a PlotDatasetConfig from a YAML file. Parameters: ----------- config_path : str | Path Path to the YAML configuration file. Returns: -------- PlotDatasetConfig Fully populated config instance. """ omega = OmegaConf.load(config_path) raw = OmegaConf.to_container(omega, resolve=True) cfg = cls() if "matplotlib_rc" in raw: cfg.matplotlib_rc = MatplotlibRCConfig(**raw["matplotlib_rc"]) if "family_colors" in raw: cfg.family_colors = FamilyColorConfig(**raw["family_colors"]) if "col_kinase_colors" in raw: cfg.col_kinase_colors = ColKinaseColorConfig(**raw["col_kinase_colors"]) if "dynamic_range" in raw: cfg.dynamic_range = DynamicRangePlotConfig(**raw["dynamic_range"]) if "ridgeline" in raw: cfg.ridgeline = RidgelinePlotConfig(**raw["ridgeline"]) if "stacked_barchart" in raw: cfg.stacked_barchart = StackedBarchartConfig(**raw["stacked_barchart"]) if "venn_diagram" in raw: cfg.venn_diagram = VennDiagramConfig(**raw["venn_diagram"]) if "metrics_boxplot" in raw: cfg.metrics_boxplot = MetricsBoxplotConfig(**raw["metrics_boxplot"]) if "sequence_schematic" in raw: cfg.sequence_schematic = SequenceSchematicConfig( **raw["sequence_schematic"] ) if "data_sources" in raw: cfg.data_sources = DataSourceConfig(**raw["data_sources"]) if "output" in raw: cfg.output = OutputConfig(**raw["output"]) return cfg
# --- KLIFS hierarchical conservation-tree figures --- # defaults mirror the constants in mkt.databases.conservation (kept as literals here so # plot_config stays import-light and does not trigger the conservation panel build).
[docs] @dataclass class ConservationTreeConfig: """Aesthetics for the static KLIFS conservation-tree supplemental figures. Rendered by :class:`mkt.databases.conservation.KLIFSConservationTreeFigure` (summary dendrogram + top/bottom detail panels). """ min_cluster_size: int = 12 font_size: float = 4.0 # 47 mirrors conservation.INT_TREE_SPLIT_INDEX (the CMGC/CAMK boundary of the # human-kinome KLIFS tree); set null to auto-pick via _split_index. split_index: int | None = 47 formats: list[str] = field(default_factory=lambda: ["pdf"])
[docs] @dataclass class ConservationTreeExplorerConfig: """Params for the interactive KLIFS conservation-tree Bokeh explorer. Rendered by :class:`mkt.databases.conservation.KLIFSTreeConservationApp`. """ min_cluster_size: int = 12 logo_cutoff: float = 0.10 name_trunc: int = 14 filename: str | None = None
[docs] @dataclass class ResidueDotConfig: """Aesthetics for the static per-amino-acid KLIFS dot-plot figure. Rendered by :meth:`mkt.databases.conservation.KLIFSConservationTreeFigure.plot_residue_dot`. """ amino_acid: str = "C" min_cluster_size: int = 12 highlight_targets: bool = False formats: list[str] = field(default_factory=lambda: ["pdf"])
[docs] @dataclass class ResidueDotExplorerConfig: """Params for the interactive per-amino-acid KLIFS dot-plot Bokeh explorer. Rendered by :class:`mkt.databases.conservation.KLIFSResidueDotApp`. """ min_cluster_size: int = 12 default_aa: str = "C" filename: str | None = None
[docs] @dataclass class CladeMembershipTableConfig: """Params for the LaTeX clade-membership table. Rendered by :func:`mkt.databases.plot.write_clade_membership_table`: the named conservation clades within ``str_group`` and their member kinases. """ str_group: str = "TK" filename: str = "clade_membership_table"
[docs] @dataclass class DictKinaseFiguresConfig: """Top-level config for the DICT_KINASE figures (upset + region-gap map/violin). Consumed by the ``plot_dict_kinase`` CLI. Kept separate from :class:`PlotDatasetConfig` so rendering these figures never imports the dataset-processing module (which has a network side effect on import). """ matplotlib_rc: MatplotlibRCConfig = field(default_factory=MatplotlibRCConfig) upset_plot: UpsetPlotConfig = field(default_factory=UpsetPlotConfig.preprint_2026) region_gap_violin: RegionGapViolinConfig = field( default_factory=RegionGapViolinConfig ) conservation_tree: ConservationTreeConfig = field( default_factory=ConservationTreeConfig ) conservation_tree_explorer: ConservationTreeExplorerConfig = field( default_factory=ConservationTreeExplorerConfig ) residue_dot: ResidueDotConfig = field(default_factory=ResidueDotConfig) residue_dot_explorer: ResidueDotExplorerConfig = field( default_factory=ResidueDotExplorerConfig ) clade_membership_table: CladeMembershipTableConfig = field( default_factory=CladeMembershipTableConfig ) output: OutputConfig = field(default_factory=OutputConfig)
[docs] @classmethod def from_yaml(cls, config_path: str | Path) -> "DictKinaseFiguresConfig": """Load a DictKinaseFiguresConfig from a YAML file. Parameters: ----------- config_path : str | Path Path to the YAML configuration file. Returns: -------- DictKinaseFiguresConfig Fully populated config instance. """ omega = OmegaConf.load(config_path) raw = OmegaConf.to_container(omega, resolve=True) cfg = cls() if "matplotlib_rc" in raw: cfg.matplotlib_rc = MatplotlibRCConfig(**raw["matplotlib_rc"]) if "upset_plot" in raw: cfg.upset_plot = UpsetPlotConfig(**raw["upset_plot"]) if "region_gap_violin" in raw: cfg.region_gap_violin = RegionGapViolinConfig(**raw["region_gap_violin"]) if "conservation_tree" in raw: cfg.conservation_tree = ConservationTreeConfig(**raw["conservation_tree"]) if "conservation_tree_explorer" in raw: cfg.conservation_tree_explorer = ConservationTreeExplorerConfig( **raw["conservation_tree_explorer"] ) if "residue_dot" in raw: cfg.residue_dot = ResidueDotConfig(**raw["residue_dot"]) if "residue_dot_explorer" in raw: cfg.residue_dot_explorer = ResidueDotExplorerConfig( **raw["residue_dot_explorer"] ) if "clade_membership_table" in raw: cfg.clade_membership_table = CladeMembershipTableConfig( **raw["clade_membership_table"] ) if "output" in raw: cfg.output = OutputConfig(**raw["output"]) return cfg