Source code for mplotter.annotating
# Copyright (c) 2021 Marco Rigobello, MIT License
"""
Annotating figures.
Collection of function and classes controlling text elements in figures;
e.g. inserting, formatting and positioning axes and tick labels.
"""
import math
from itertools import count
from string import ascii_lowercase
import numpy as np
import matplotlib as mpl
__all__ = [
"enum_axes",
"SignedScalarFormatter",
"SSDecimalFormatter",
"SSFractionFormatter",
]
[docs]
def enum_axes(axs, loc, fmt="({})", enum="letters", **kw):
"""
Labels subfigures (axes).
Parameters
----------
axs : iterable[matplotlib.axes.Axes]
Axes to be labelled.
loc : str
Label location, 'title' or a valid loc for matplotlib.offsetbox.AnchoredText.
fmt : str, default '({})'
Label format string.
enum : str or iterable, default 'letters'
Provides the labels via iteration. Special values:
('letters' | 'numbers') for (alphabetic | numeric) enumeration.
**kw :
Keyword arguments for matplotlib.offsetbox.AnchoredText or matplotlib.axes.Axes.set_title.
Returns
-------
list[matplotlib.offsetbox.AnchoredText]
The added labels.
"""
axs = np.asanyarray(axs)
if enum == "letters":
enum = ascii_lowercase
elif enum == "numbers":
enum = count(start=1)
if loc == "title":
artist = lambda ax, lbl: ax.set_title(lbl, **kw)
else:
kw.setdefault("frameon", False)
kw.setdefault("borderpad", 0)
artist = lambda ax, lbl: ax.add_artist(
mpl.offsetbox.AnchoredText(lbl, loc, **kw),
)
return [artist(ax, fmt.format(e)) for ax, e in zip(axs.flat, enum)]
[docs]
class ScaledFormatter(mpl.ticker.Formatter):
def __init__(self, unit=1, squeeze=True):
super().__init__()
try:
self.base, self.mark = unit
except TypeError:
self.base = unit
self.mark = ""
self.squeeze = squeeze
def __call__(self, val, pos=None):
val /= self.base
mark = self.mark
if self.squeeze:
if val == 0:
mark = ""
elif val == 1 and self.mark:
val = ""
return val, mark
[docs]
class SignedFormatter(mpl.ticker.Formatter):
def __init__(self, sign=None, sign_zero=True):
super().__init__()
self._init_sign = sign
self.sign_zero = sign_zero
[docs]
def set_locs(self, locs):
super().set_locs(locs)
sign = self._init_sign
if sign is None:
sign = np.any(np.asanyarray(self.locs) < 0)
self.sign = sign
def __call__(self, val, pos=None):
if val < 0:
return "-"
elif self.sign and (val > 0 or self.sign_zero):
return "+"
else:
return ""
[docs]
class SgnScalarFormatter(SignedFormatter, mpl.ticker.ScalarFormatter):
def __init__(self, sign=None, **kwargs):
SignedFormatter.__init__(self, sign)
mpl.ticker.ScalarFormatter.__init__(self, **kwargs)
[docs]
def set_locs(self, locs):
SignedFormatter.set_locs(self, locs)
mpl.ticker.ScalarFormatter.set_locs(self, locs)
if self.sign:
# first replace is likely pointless
self.format = self.format.replace("%+", "%").replace("%", "%+")
[docs]
class SSDecimalFormatter(SignedFormatter, ScaledFormatter):
def __init__(self, digits, sign=None, sign_zero=True, unit=1):
SignedFormatter.__init__(self, sign, sign_zero)
ScaledFormatter.__init__(self, unit, squeeze=False)
self.digits = digits
def __call__(self, val, pos=None):
sgn = SignedFormatter.__call__(self, val, pos)
val, mark = ScaledFormatter.__call__(self, abs(val), pos)
fmt = "${sgn}{val:" + f".{self.digits}f" + "}{mark}$"
return fmt.format(sgn=sgn, val=val, mark=mark)
[docs]
class SSFractionFormatter(SignedFormatter, ScaledFormatter):
def __init__(
self,
D,
frac=True,
sign=None,
sign_zero=False,
unit=1,
squeeze=True,
):
SignedFormatter.__init__(self, sign, sign_zero)
ScaledFormatter.__init__(self, unit, squeeze)
self.base /= D
self.D = D
self.frac = frac
self.format = {
"int": r"${sgn}{N}{mark}$",
"frac": (
r"${sgn}\frac{{{N}{mark}}}{{{D}}}$" if frac else r"${sgn}{N}{mark}/{D}$"
),
}
if mpl.rcParams["text.usetex"]:
vspace = self.format["frac"]
vspace = vspace.format(sgn="", N=1, D=1, mark=self.mark)
self.format["int"] += r"\vphantom{{{}}}".format(vspace)
def set_axis(self, axis):
super().set_axis(axis)
axis.set_major_locator(mpl.ticker.MultipleLocator(self.base))
def __call__(self, val, pos=None):
D = self.D
if self.squeeze:
_N = round(val / self.base)
gcd = math.gcd(_N, D)
val /= gcd
D //= gcd
sgn = SignedFormatter.__call__(self, val, pos)
N, mark = ScaledFormatter.__call__(self, abs(val), pos)
try:
N = round(N)
except TypeError:
pass
if D == 1:
fmt = self.format["int"]
else:
fmt = self.format["frac"]
return fmt.format(sgn=sgn, N=N, D=D, mark=mark)