"""Utilities for integrating external content and adding panel labels.
This module provides tools for:
- Adding external plots (from R, MATLAB, etc.) as images to panels
- Adding panel labels (A, B, C, ...) with precise positioning
"""
import matplotlib.pyplot as plt
from matplotlib.image import imread
from typing import Literal, Optional, Tuple, Dict
from pathlib import Path
[docs]
class ExternalPanel:
"""Utilities for adding external plot images to panels.
This class provides static methods for displaying images from external
plotting tools (R, MATLAB, etc.) in matplotlib axes.
"""
[docs]
@staticmethod
def add_image_panel(
ax: plt.Axes, image_path: str, aspect: str = "equal", hide_axes: bool = True
) -> None:
"""Add an external image to a matplotlib axes.
Parameters
----------
ax : matplotlib.axes.Axes
The axes to add the image to
image_path : str
Path to the image file
aspect : str, default 'equal'
Aspect ratio for the image display
hide_axes : bool, default True
Whether to hide axis ticks and labels
Examples
--------
>>> fig, ax = plt.subplots()
>>> ExternalPanel.add_image_panel(ax, 'external_plot.png')
>>> plt.show()
"""
# Read and display image
img = imread(image_path)
ax.imshow(img, aspect=aspect)
# Hide axes if requested
if hide_axes:
ax.set_xticks([])
ax.set_yticks([])
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["left"].set_visible(False)
[docs]
@staticmethod
def create_placeholder(
ax: plt.Axes, text: str = "External Plot", fontsize: int = 14, color: str = "gray"
) -> None:
"""Create a placeholder for external content.
Useful during figure development when external plots are not yet ready.
Parameters
----------
ax : matplotlib.axes.Axes
The axes to add the placeholder to
text : str, default 'External Plot'
Placeholder text to display
fontsize : int, default 14
Font size for placeholder text
color : str, default 'gray'
Color for placeholder elements
Examples
--------
>>> fig, ax = plt.subplots()
>>> ExternalPanel.create_placeholder(ax, 'R plot goes here')
>>> plt.show()
"""
# Draw placeholder box
ax.add_patch(
plt.Rectangle(
(0.1, 0.1),
0.8,
0.8,
fill=False,
edgecolor=color,
linestyle="--",
linewidth=2,
transform=ax.transAxes,
)
)
# Add placeholder text
ax.text(
0.5,
0.5,
text,
horizontalalignment="center",
verticalalignment="center",
fontsize=fontsize,
color=color,
transform=ax.transAxes,
)
# Hide axes
ax.set_xticks([])
ax.set_yticks([])
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)
ax.spines["bottom"].set_visible(False)
ax.spines["left"].set_visible(False)
[docs]
class PanelLabeler:
"""Utilities for adding panel labels (A, B, C, ...) to multi-panel figures.
Parameters
----------
fontsize_pt : float, default 12
Font size for labels in points
location : {'top_left', 'top_right', 'bottom_left', 'bottom_right'}, default 'top_left'
Location for panel labels
offset : tuple of float, default (-0.1, 1.05)
Offset for label position in axes coordinates (x, y)
fontweight : str, default 'bold'
Font weight for labels
fontfamily : str, default 'sans-serif'
Font family for labels
Examples
--------
>>> labeler = PanelLabeler(fontsize_pt=10, location='top_left')
>>> fig, axes = plt.subplots(2, 2)
>>> for idx, (ax, label) in enumerate(zip(axes.flat, ['A', 'B', 'C', 'D'])):
... labeler.add_label(ax, label, dpi=300)
>>> plt.show()
"""
[docs]
def __init__(
self,
fontsize_pt: float = 12,
location: Literal["top_left", "top_right", "bottom_left", "bottom_right"] = "top_left",
offset: Optional[Tuple[float, float]] = None,
fontweight: str = "bold",
fontfamily: str = "sans-serif",
):
"""Initialize PanelLabeler.
Parameters
----------
fontsize_pt : float, default 12
Font size for labels in points.
location : {'top_left', 'top_right', 'bottom_left', 'bottom_right'}, default 'top_left'
Location for panel labels.
offset : tuple of float, optional
Offset for label position in axes coordinates (x, y). If None, uses
location-specific defaults.
fontweight : str, default 'bold'
Font weight for labels.
fontfamily : str, default 'sans-serif'
Font family for labels.
"""
self.fontsize_pt = fontsize_pt
self.location = location
self.fontweight = fontweight
self.fontfamily = fontfamily
# Set default offset based on location if not provided
if offset is None:
offset_map = {
"top_left": (-0.1, 1.05),
"top_right": (1.05, 1.05),
"bottom_left": (-0.1, -0.1),
"bottom_right": (1.05, -0.1),
}
self.offset = offset_map[location]
else:
self.offset = offset
# Set alignment based on location
alignment_map = {
"top_left": ("right", "bottom"),
"top_right": ("left", "bottom"),
"bottom_left": ("right", "top"),
"bottom_right": ("left", "top"),
}
self.ha, self.va = alignment_map[location]
[docs]
def add_label(self, ax: plt.Axes, label: str, dpi: int = 300, **text_kwargs) -> None:
"""Add a panel label to an axes.
Parameters
----------
ax : matplotlib.axes.Axes
The axes to label
label : str
Label text (e.g., 'A', 'B', 'C')
dpi : int, default 300
DPI of the figure (used for size calculations)
**text_kwargs
Additional keyword arguments passed to ax.text()
Examples
--------
>>> fig, ax = plt.subplots()
>>> labeler = PanelLabeler()
>>> labeler.add_label(ax, 'A', dpi=300)
"""
# Merge user kwargs with defaults
text_params = {
"transform": ax.transAxes,
"fontsize": self.fontsize_pt,
"fontweight": self.fontweight,
"fontfamily": self.fontfamily,
"horizontalalignment": self.ha,
"verticalalignment": self.va,
}
text_params.update(text_kwargs)
# Add label
ax.text(self.offset[0], self.offset[1], label, **text_params)
[docs]
def add_labels_to_dict(
self, axes_dict: Dict[str, plt.Axes], dpi: int = 300, **text_kwargs
) -> None:
"""Add labels to all axes in a dictionary.
Convenience method for labeling all panels in a PanelLayout figure.
Parameters
----------
axes_dict : dict of matplotlib.axes.Axes
Dictionary mapping panel names to axes
dpi : int, default 300
DPI of the figure
**text_kwargs
Additional keyword arguments passed to ax.text()
Examples
--------
>>> layout = PanelLayout(units='cm', dpi=300)
>>> layout.add_panel('A', size=(8, 6))
>>> layout.add_panel('B', size=(8, 6))
>>> layout.set_grid(rows=1, cols=2)
>>> fig, axes = layout.create_figure()
>>> labeler = PanelLabeler()
>>> labeler.add_labels_to_dict(axes, dpi=layout.dpi)
"""
for name, ax in axes_dict.items():
self.add_label(ax, name, dpi=dpi, **text_kwargs)