"""Some utility functions related to creating good looking figures."""
from matplotlib.figure import Figure
from matplotlib.axes import Axes
import os
import typing
[docs]def set_title(fig: Figure, ax: Axes, title: str, digital: bool):
"""Sets the title for the given axes to the given value. This is more
particular than the default matplotlib Axes.set_title.
:param Figure fig: the figure the axes is in
:param Axes ax: the axes which you want to have a title
:param str title: the desired title text
:param bool digital: if True, a large font size is selected. Otherwise,
a smaller font size is selected
:returns: TextCollection that was added
"""
figw = fig.get_figwidth()
figh = fig.get_figheight()
figw_px = figw * fig.get_dpi()
pad = max(int(0.3125 * figh), 1)
font_size = int((8 / 1.92) * figw) if digital else int((4 / 1.92) * figw)
font_size = max(5, font_size)
axtitle = ax.set_title(title, pad=pad)
axtitle.set_fontsize(font_size)
renderer = fig.canvas.get_renderer()
bb = axtitle.get_window_extent(renderer=renderer)
while bb.width >= (figw_px - 26) * 0.9 and font_size > 9:
font_size = max(5, font_size - 5)
axtitle.set_fontsize(font_size)
bb = axtitle.get_window_extent(renderer=renderer)
return axtitle
[docs]def set_ticklabel_sizes(fig: Figure, ax: Axes, digital: bool):
"""Updates the sizes of the tick labels for the given figure based on its
canvas size and canvas dpi.
:param Figure fig: The figure to update
:param Axes ax: The specific axes within the figure to update
:param bool digital: True if this is for digital display,
False for physical display
"""
figw = fig.get_figwidth()
font_size = int((30 / 19.2) * figw) if digital else int((20 / 19.2) * figw)
font_size = max(font_size, 5)
for tick in ax.xaxis.get_major_ticks():
tick.label.set_fontsize(font_size)
for tick in ax.yaxis.get_major_ticks():
tick.label.set_fontsize(font_size)
def _filtered_savefig(filter_, fig, path, *args, **kwargs):
if filter_(path):
fig.savefig(path, *args, **kwargs)
[docs]def save_fig(fig: Figure, ax: Axes, title: str, outfile_wo_ext: str,
filter_: typing.Optional[typing.Callable] = None):
r"""Saves the given figure with many different commonly used figure sizes,
resizing labels and titles as appropriate. This technique works only for
a single axis plot, since for other styles different font sizes would be
appropriate.
:param Figure fig: The figure to save
:param Axes ax: The axis on the figure
:param str title: The title of the plot
:param str outfile_wo_ext: where to save the figure, omitting the file
extension (as many files may be saved)
:param optional[callable] filter_: if not None, a callable object which
accepts a string which is a (possibly relative) path to a file which
will only be saved if the result of filter\_ is True.
"""
filter_ = filter_ if filter_ is not None else lambda x: True
set_title(fig, ax, title, True)
ax.xaxis.label.set_size(48)
ax.yaxis.label.set_size(48)
set_ticklabel_sizes(fig, ax, True)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_1920x1080.png', dpi=100)
set_title(fig, ax, title, False)
ax.xaxis.label.set_size(24)
ax.yaxis.label.set_size(24)
set_ticklabel_sizes(fig, ax, False)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_19.2x10.8.pdf', dpi=300, transparent=True)
fig.set_figwidth(7.25) # paper width
fig.set_figheight(4.08) # 56.25% width
fig.subplots_adjust(left=0.15, right=0.925)
set_title(fig, ax, title, True)
ax.xaxis.label.set_size(24)
ax.yaxis.label.set_size(24)
set_ticklabel_sizes(fig, ax, True)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_725x408.png', dpi=100)
set_title(fig, ax, title, False)
ax.xaxis.label.set_size(9)
ax.yaxis.label.set_size(9)
set_ticklabel_sizes(fig, ax, False)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_7.25x4.08.pdf', dpi=300, transparent=True)
fig.set_figwidth(3.54) # column width
fig.set_figheight(1.99) # 56.25% width
fig.subplots_adjust(left=0.2, right=0.975)
set_title(fig, ax, title, True)
ax.xaxis.label.set_size(12)
ax.yaxis.label.set_size(12)
set_ticklabel_sizes(fig, ax, True)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_354x199.png', dpi=100)
set_title(fig, ax, title, False)
ax.xaxis.label.set_size(8)
ax.yaxis.label.set_size(8)
set_ticklabel_sizes(fig, ax, False)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_3.54x1.99.pdf', dpi=300, transparent=True)
fig.set_figwidth(1.73) # half column width
fig.set_figheight(1.73) # square
fig.subplots_adjust(left=0.35, right=0.925, bottom=0.25, top=0.825)
set_title(fig, ax, title, True)
set_ticklabel_sizes(fig, ax, True)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_173x173.png', dpi=100)
set_title(fig, ax, title, False)
set_ticklabel_sizes(fig, ax, False)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_1.73x1.73.pdf', dpi=300, transparent=True)
fig.set_figwidth(1.73) # half column width
fig.set_figheight(0.972) # 56.25% width
fig.subplots_adjust(left=0.35, right=0.925, bottom=0.25, top=0.825)
ax.xaxis.label.set_size(5)
ax.xaxis.labelpad = -5
ax.yaxis.label.set_size(5)
set_title(fig, ax, title, True)
set_ticklabel_sizes(fig, ax, True)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_173x97.png', dpi=100)
set_title(fig, ax, title, False)
set_ticklabel_sizes(fig, ax, False)
_filtered_savefig(filter_, fig, outfile_wo_ext + '_1.73x0.97.pdf', dpi=300, transparent=True)
[docs]def fig_exists(outfile_wo_ext: str, filter_: typing.Optional[typing.Callable] = None) -> bool:
"""Returns true if the files from save_fig with the given outfile already
exist.
:param outfile_wo_ext: where we expect the file to have been saved
:type outfile_wo_ext: str
:param filter_: an optional callable which accepts images and returns True if they
are relevant and False if they can be ignored
:type filter_: Optional[callable]
:return: True if the file(s) exists, False otherwise
:rtype: bool
"""
filter_ = filter_ if filter_ is not None else lambda x: True
for size in ((19.2, 10.8), (7.25, 4.08), (3.54, 1.99), (1.73, 0.97), (1.73, 1.73)):
img = outfile_wo_ext + f'_{size[0]}x{size[1]}.pdf'
if filter_(img) and not os.path.exists(img):
return False
img = outfile_wo_ext + f'_{int(size[0]*100)}x{int(size[1]*100)}.png'
if filter_(img) and not os.path.exists(img):
return False
return True
[docs]def make_vs_title(x: str, y: str):
"""Creates the axes title for a plot with the given x-axis variable and
y-axis variable
:param x: the name for the x-variable
:type x: str
:param y: the name for the y-variable
:type y: str
:return: The correct axes title
:rtype: str
"""
return f'{y} vs {x}'