Source code for astronomix.plotting_helpers.inset_box
"""
Add a zoomed-in inset axes to an existing matplotlib axes.
Replicates the line and scatter artists of a parent axes inside a magnified
inset box (preserving styling such as colors, markers, sizes and colormaps)
and draws connector lines between the inset and the highlighted region. Useful
for emphasizing a small feature of a plot without a second figure.
"""
# numerics
import numpy as np
# plotting (optional dependency — this helper only runs when matplotlib is
# installed, so guard the import to keep the module importable without it)
try:
from matplotlib.collections import PathCollection
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset
except ModuleNotFoundError: # pragma: no cover - exercised only without matplotlib
PathCollection = None
inset_axes = mark_inset = None
[docs]
def add_inset_box(
ax,
x1,
x2,
y1,
y2,
loc="lower left",
connect_loc1=2,
connect_loc2=4,
width="40%",
height="40%",
):
"""Add a magnified inset axes mirroring the artists of ``ax``.
Args:
ax: The parent axes whose line and scatter artists are replicated.
x1: The left edge of the zoomed-in x-range.
x2: The right edge of the zoomed-in x-range.
y1: The lower edge of the zoomed-in y-range.
y2: The upper edge of the zoomed-in y-range.
loc: The location of the inset within the parent axes.
connect_loc1: The first corner used for the inset connector lines.
connect_loc2: The second corner used for the inset connector lines.
width: The inset width (as a fraction string or absolute size).
height: The inset height (as a fraction string or absolute size).
Returns:
The created inset axes.
"""
if inset_axes is None:
raise ModuleNotFoundError(
"matplotlib is required for add_inset_box but is not installed. "
"Install it with `pip install matplotlib`."
)
axins = inset_axes(ax, width=width, height=height, loc=loc)
for line in ax.get_lines():
axins.plot(
line.get_xdata(),
line.get_ydata(),
linestyle=line.get_linestyle(),
linewidth=line.get_linewidth(),
color=line.get_color(),
marker=line.get_marker(),
markersize=line.get_markersize(),
alpha=line.get_alpha(),
)
for collection in ax.collections:
if not isinstance(collection, PathCollection):
continue
offsets = collection.get_offsets()
if offsets is None or len(offsets) == 0:
continue
offsets = np.asarray(offsets)
if np.ma.isMaskedArray(offsets):
offsets = offsets.filled(np.nan)
facecolors = collection.get_facecolors()
edgecolors = collection.get_edgecolors()
has_no_facecolor = facecolors is None or len(facecolors) == 0
scatter_kwargs = {
"alpha": collection.get_alpha(),
"linewidths": collection.get_linewidths(),
}
paths = collection.get_paths()
if len(paths) > 0:
scatter_kwargs["marker"] = paths[0]
sizes = collection.get_sizes()
if sizes is not None and len(sizes) > 0:
scatter_kwargs["s"] = sizes
if has_no_facecolor:
scatter_kwargs["facecolors"] = "none"
if edgecolors is not None and len(edgecolors) > 0:
scatter_kwargs["edgecolors"] = edgecolors
else:
scatter_kwargs["edgecolors"] = collection.get_edgecolor()
else:
array = collection.get_array()
if array is not None and len(array) == len(offsets):
scatter_kwargs["c"] = np.asarray(array)
scatter_kwargs["cmap"] = collection.cmap
scatter_kwargs["norm"] = collection.norm
else:
scatter_kwargs["facecolors"] = facecolors
if edgecolors is not None and len(edgecolors) > 0:
scatter_kwargs["edgecolors"] = edgecolors
axins.scatter(
offsets[:, 0],
offsets[:, 1],
**scatter_kwargs,
)
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
axins.tick_params(labelleft=False, labelbottom=False)
axins.set_xticks([])
axins.set_yticks([])
mark_inset(
ax,
axins,
loc1=connect_loc1,
loc2=connect_loc2,
fc="none",
ec="0.5",
)
return axins