adesomics.plotting.style

Shared plotting style for adesomics.

All plots pull colours and labels from here, so a condition looks the same in a volcano plot and in a growth curve.

  1"""Shared plotting style for adesomics.
  2
  3All plots pull colours and labels from here, so a condition looks the same
  4in a volcano plot and in a growth curve.
  5"""
  6
  7import matplotlib as mpl
  8import matplotlib.pyplot as plt
  9
 10# Adesonica corporate palette
 11PINK = "#e61f68"
 12YELLOW = "#ffe11d"
 13BLUE = "#5acdff"
 14INK = "#1a1a2e"
 15GREY = "#b0b0b8"
 16
 17PALETTE = [PINK, BLUE, YELLOW, INK, GREY]
 18
 19# distinguishable line styles, used for a second factor
 20LINESTYLES = ["-", "--", ":", "-."]
 21
 22
 23def use_style(base_size: int = 13) -> None:
 24    """Readable defaults: larger type, no top/right spines, soft grid."""
 25    mpl.rcParams.update({
 26        "font.size": base_size,
 27        "axes.titlesize": base_size + 4,
 28        "axes.labelsize": base_size + 1,
 29        "xtick.labelsize": base_size - 1,
 30        "ytick.labelsize": base_size - 1,
 31        "legend.fontsize": base_size - 1,
 32        "axes.titleweight": "bold",
 33        "axes.spines.top": False,
 34        "axes.spines.right": False,
 35        "axes.grid": True,
 36        "grid.alpha": 0.25,
 37        "grid.linewidth": 0.6,
 38        "lines.linewidth": 2.2,
 39        "figure.dpi": 110,
 40        "savefig.bbox": "tight",
 41        "axes.prop_cycle": mpl.cycler(color=PALETTE),
 42    })
 43
 44
 45def group_colors(metadata, palette=None) -> dict:
 46    """One colour per condition, stable across plots."""
 47    palette = palette or PALETTE
 48    groups = sorted(metadata.groups.unique())
 49    return {g: palette[i % len(palette)] for i, g in enumerate(groups)}
 50
 51
 52def factor_style(metadata, color_by="genotype", style_by="supplement") -> dict:
 53    """Encode a two-factor design as colour x line style.
 54
 55    Returns per-sample styling plus the two lookup tables, so the legend can
 56    be built per factor instead of per group. Falls back to plain per-group
 57    colours when the structured factors are absent.
 58    """
 59    tab = metadata.table
 60
 61    if color_by not in tab.columns:
 62        lut = group_colors(metadata)
 63        return {
 64            "per_sample": {s: {"color": lut[g], "linestyle": "-"}
 65                           for s, g in metadata.groups.items()},
 66            "color_map": lut,
 67            "style_map": {},
 68            "color_by": metadata.group_col,
 69            "style_by": None,
 70        }
 71
 72    c_levels = sorted(tab[color_by].unique())
 73    color_map = {lv: PALETTE[i % len(PALETTE)] for i, lv in enumerate(c_levels)}
 74
 75    if style_by in tab.columns:
 76        s_levels = sorted(tab[style_by].unique())
 77        style_map = {lv: LINESTYLES[i % len(LINESTYLES)]
 78                     for i, lv in enumerate(s_levels)}
 79    else:
 80        style_by, style_map = None, {}
 81
 82    per_sample = {
 83        s: {
 84            "color": color_map[tab.loc[s, color_by]],
 85            "linestyle": (style_map[tab.loc[s, style_by]] if style_by else "-"),
 86        }
 87        for s in metadata.samples
 88    }
 89    return {"per_sample": per_sample, "color_map": color_map,
 90            "style_map": style_map, "color_by": color_by, "style_by": style_by}
 91
 92
 93def factor_legend(ax, style: dict, loc="best") -> None:
 94    """Two-part legend: colour = factor 1, line style = factor 2."""
 95    from matplotlib.lines import Line2D
 96
 97    handles = [Line2D([], [], color="none", label=f"{style['color_by']}:")]
 98    handles += [Line2D([], [], color=c, lw=3, label=f"  {lv}")
 99                for lv, c in style["color_map"].items()]
100
101    if style["style_map"]:
102        handles += [Line2D([], [], color="none", label=" "),
103                    Line2D([], [], color="none", label=f"{style['style_by']}:")]
104        handles += [Line2D([], [], color=INK, ls=ls, lw=2, label=f"  {lv}")
105                    for lv, ls in style["style_map"].items()]
106
107    ax.legend(handles=handles, loc=loc, frameon=False,
108              handlelength=2.4, labelspacing=0.35)
109
110
111def short_labels(metadata, factors=("genotype", "supplement")) -> list:
112    """Compact tick labels, e.g. 'del_atoC / LiCl_10mM'."""
113    cols = [f for f in factors if f in metadata.table.columns]
114    if not cols:
115        return list(metadata.groups)
116    return metadata.table[cols].astype(str).agg("\n".join, axis=1).tolist()
117
118
119def annotate_study(ax, metadata) -> None:
120    """Footer with organism and project."""
121    bits = [metadata.study.get(k) for k in ("Organism", "BioProject")]
122    text = " · ".join(b for b in bits if b)
123    if text:
124        ax.figure.text(0.99, 0.005, text, ha="right", fontsize=8, color=GREY)
PINK = '#e61f68'
YELLOW = '#ffe11d'
BLUE = '#5acdff'
INK = '#1a1a2e'
GREY = '#b0b0b8'
PALETTE = ['#e61f68', '#5acdff', '#ffe11d', '#1a1a2e', '#b0b0b8']
LINESTYLES = ['-', '--', ':', '-.']
def use_style(base_size: int = 13) -> None:
24def use_style(base_size: int = 13) -> None:
25    """Readable defaults: larger type, no top/right spines, soft grid."""
26    mpl.rcParams.update({
27        "font.size": base_size,
28        "axes.titlesize": base_size + 4,
29        "axes.labelsize": base_size + 1,
30        "xtick.labelsize": base_size - 1,
31        "ytick.labelsize": base_size - 1,
32        "legend.fontsize": base_size - 1,
33        "axes.titleweight": "bold",
34        "axes.spines.top": False,
35        "axes.spines.right": False,
36        "axes.grid": True,
37        "grid.alpha": 0.25,
38        "grid.linewidth": 0.6,
39        "lines.linewidth": 2.2,
40        "figure.dpi": 110,
41        "savefig.bbox": "tight",
42        "axes.prop_cycle": mpl.cycler(color=PALETTE),
43    })

Readable defaults: larger type, no top/right spines, soft grid.

def group_colors(metadata, palette=None) -> dict:
46def group_colors(metadata, palette=None) -> dict:
47    """One colour per condition, stable across plots."""
48    palette = palette or PALETTE
49    groups = sorted(metadata.groups.unique())
50    return {g: palette[i % len(palette)] for i, g in enumerate(groups)}

One colour per condition, stable across plots.

def factor_style(metadata, color_by='genotype', style_by='supplement') -> dict:
53def factor_style(metadata, color_by="genotype", style_by="supplement") -> dict:
54    """Encode a two-factor design as colour x line style.
55
56    Returns per-sample styling plus the two lookup tables, so the legend can
57    be built per factor instead of per group. Falls back to plain per-group
58    colours when the structured factors are absent.
59    """
60    tab = metadata.table
61
62    if color_by not in tab.columns:
63        lut = group_colors(metadata)
64        return {
65            "per_sample": {s: {"color": lut[g], "linestyle": "-"}
66                           for s, g in metadata.groups.items()},
67            "color_map": lut,
68            "style_map": {},
69            "color_by": metadata.group_col,
70            "style_by": None,
71        }
72
73    c_levels = sorted(tab[color_by].unique())
74    color_map = {lv: PALETTE[i % len(PALETTE)] for i, lv in enumerate(c_levels)}
75
76    if style_by in tab.columns:
77        s_levels = sorted(tab[style_by].unique())
78        style_map = {lv: LINESTYLES[i % len(LINESTYLES)]
79                     for i, lv in enumerate(s_levels)}
80    else:
81        style_by, style_map = None, {}
82
83    per_sample = {
84        s: {
85            "color": color_map[tab.loc[s, color_by]],
86            "linestyle": (style_map[tab.loc[s, style_by]] if style_by else "-"),
87        }
88        for s in metadata.samples
89    }
90    return {"per_sample": per_sample, "color_map": color_map,
91            "style_map": style_map, "color_by": color_by, "style_by": style_by}

Encode a two-factor design as colour x line style.

Returns per-sample styling plus the two lookup tables, so the legend can be built per factor instead of per group. Falls back to plain per-group colours when the structured factors are absent.

def factor_legend(ax, style: dict, loc='best') -> None:
 94def factor_legend(ax, style: dict, loc="best") -> None:
 95    """Two-part legend: colour = factor 1, line style = factor 2."""
 96    from matplotlib.lines import Line2D
 97
 98    handles = [Line2D([], [], color="none", label=f"{style['color_by']}:")]
 99    handles += [Line2D([], [], color=c, lw=3, label=f"  {lv}")
100                for lv, c in style["color_map"].items()]
101
102    if style["style_map"]:
103        handles += [Line2D([], [], color="none", label=" "),
104                    Line2D([], [], color="none", label=f"{style['style_by']}:")]
105        handles += [Line2D([], [], color=INK, ls=ls, lw=2, label=f"  {lv}")
106                    for lv, ls in style["style_map"].items()]
107
108    ax.legend(handles=handles, loc=loc, frameon=False,
109              handlelength=2.4, labelspacing=0.35)

Two-part legend: colour = factor 1, line style = factor 2.

def short_labels(metadata, factors=('genotype', 'supplement')) -> list:
112def short_labels(metadata, factors=("genotype", "supplement")) -> list:
113    """Compact tick labels, e.g. 'del_atoC / LiCl_10mM'."""
114    cols = [f for f in factors if f in metadata.table.columns]
115    if not cols:
116        return list(metadata.groups)
117    return metadata.table[cols].astype(str).agg("\n".join, axis=1).tolist()

Compact tick labels, e.g. 'del_atoC / LiCl_10mM'.

def annotate_study(ax, metadata) -> None:
120def annotate_study(ax, metadata) -> None:
121    """Footer with organism and project."""
122    bits = [metadata.study.get(k) for k in ("Organism", "BioProject")]
123    text = " · ".join(b for b in bits if b)
124    if text:
125        ax.figure.text(0.99, 0.005, text, ha="right", fontsize=8, color=GREY)

Footer with organism and project.