|
1 | 1 | from contextlib import AbstractContextManager |
2 | 2 | from enum import Enum |
3 | | -from typing import List |
| 3 | +from pathlib import Path |
| 4 | +from typing import List, TYPE_CHECKING |
| 5 | + |
| 6 | +if TYPE_CHECKING: |
| 7 | + import matplotlib.pyplot as plt |
| 8 | + |
| 9 | +ASSETS_DIR = Path(__file__).parent / "assets" |
4 | 10 |
|
5 | 11 | # Primary |
6 | 12 | BLACK = "#000000" |
@@ -39,6 +45,15 @@ class MXM_STYLE(Enum): |
39 | 45 | DARK = 2 |
40 | 46 |
|
41 | 47 |
|
| 48 | +LOGO_MAP = { |
| 49 | + (MXM_STYLE.LIGHT, 0): ASSETS_DIR / "logo_black.png", |
| 50 | + (MXM_STYLE.LIGHT, 1): ASSETS_DIR / "logo_black.png", |
| 51 | + (MXM_STYLE.DARK, 0): ASSETS_DIR / "logo_offwhite.png", |
| 52 | + (MXM_STYLE.DARK, 1): ASSETS_DIR / "logo_offwhite.png", |
| 53 | + (MXM_STYLE.DARK, 2): ASSETS_DIR / "logo_offwhite.png", |
| 54 | +} |
| 55 | + |
| 56 | + |
42 | 57 | def get_color_cycle(style: MXM_STYLE = MXM_STYLE.LIGHT) -> List[str]: |
43 | 58 | """Get color cycle for the chosen style. |
44 | 59 |
|
@@ -152,3 +167,54 @@ def get_mpl_context(style: MXM_STYLE = MXM_STYLE.LIGHT, variant: int = 0) -> Abs |
152 | 167 | rc = get_rc_params(style, variant) |
153 | 168 |
|
154 | 169 | return plt.rc_context(rc=rc) |
| 170 | + |
| 171 | + |
| 172 | +def add_logo( |
| 173 | + fig: plt.Figure, |
| 174 | + style: MXM_STYLE = MXM_STYLE.LIGHT, |
| 175 | + variant: int = 0, |
| 176 | + logo_path: str | Path | None = None, |
| 177 | + position: str = "upper right", |
| 178 | + size: float = 0.08, |
| 179 | + alpha: float = 0.8, |
| 180 | +) -> None: |
| 181 | + """Add a small logo to the corner of a matplotlib figure. |
| 182 | +
|
| 183 | + Args: |
| 184 | + fig: The matplotlib figure to add the logo to. |
| 185 | + style: The style, used to pick an appropriate default logo. Defaults to MXM_STYLE.LIGHT. |
| 186 | + variant: The variant of the style. Used to select a contrasting logo. Defaults to 0. |
| 187 | + logo_path: Path or URI to a custom logo image. If None, uses the default logo for the style/variant. |
| 188 | + position: Corner placement. One of "lower right", "lower left", "upper right", "upper left". |
| 189 | + size: Size of the logo as a fraction of figure height. Defaults to 0.08. |
| 190 | + alpha: Opacity of the logo. Defaults to 0.8. |
| 191 | + """ |
| 192 | + import io |
| 193 | + |
| 194 | + import fsspec |
| 195 | + import matplotlib.image as mpimg |
| 196 | + from matplotlib.offsetbox import AnnotationBbox, OffsetImage |
| 197 | + |
| 198 | + if logo_path is None: |
| 199 | + logo_path = str(LOGO_MAP[(style, variant)]) |
| 200 | + |
| 201 | + with fsspec.open(str(logo_path), "rb") as f: |
| 202 | + data = f.read() |
| 203 | + |
| 204 | + img = mpimg.imread(io.BytesIO(data), format=Path(str(logo_path)).suffix.lstrip(".")) |
| 205 | + |
| 206 | + positions = { |
| 207 | + "lower right": (0.95, 0.05), |
| 208 | + "lower left": (0.05, 0.05), |
| 209 | + "upper right": (0.95, 0.95), |
| 210 | + "upper left": (0.05, 0.95), |
| 211 | + } |
| 212 | + xy = positions[position] |
| 213 | + |
| 214 | + fig_height_px = fig.get_size_inches()[1] * fig.dpi |
| 215 | + logo_height_px = size * fig_height_px |
| 216 | + zoom = logo_height_px / img.shape[0] |
| 217 | + |
| 218 | + imagebox = OffsetImage(img, zoom=zoom, alpha=alpha) |
| 219 | + ab = AnnotationBbox(imagebox, xy, xycoords="figure fraction", frameon=False) |
| 220 | + fig.add_artist(ab) |
0 commit comments