Source code for so101_mvbench.evaluation.plot_style
"""Shared matplotlib conventions for the plots this package writes.
One rule so far, but it applies to every figure: the legend belongs BELOW the
x-axis. An outside-right legend eats roughly a quarter of the width for a few
short entries, and an in-plot one covers data -- in the episode timeline it
literally hid a bar. Vertical space is the cheap direction.
"""
from __future__ import annotations
__version__ = "1.0.0"
[docs]
def legend_below(ax, handles=None, fontsize=9, max_ncol=4, y=None) -> None:
"""Place ``ax``'s legend as a horizontal row underneath the x-axis.
``handles`` overrides the artists collected from the axes, for figures that
build proxy handles. Does nothing when there is nothing to label.
``y`` is an axes-fraction offset. Left at None it is derived from a constant
PHYSICAL gap, because the same fraction means different distances on a tall
square plot and on a short wide one: at a fixed -0.09 the legend landed on
top of the x-axis label of the wide rotation and timing figures.
"""
if y is None:
# Whatever already sits under the axes has to be cleared: the x-axis
# label, and rotated tick labels, which are what the aggregate charts
# use and which are taller than upright ones.
rotated = any(t.get_rotation() % 180 != 0 for t in ax.get_xticklabels())
gap_in = 0.30 + (0.32 if ax.get_xlabel() else 0.0) + (0.32 if rotated else 0.0)
height_in = ax.get_position().height * ax.figure.get_figheight()
y = -gap_in / max(height_in, 0.5)
if handles is None:
handles, labels = ax.get_legend_handles_labels()
else:
labels = [h.get_label() for h in handles]
if not handles:
return
ax.legend(handles=handles, labels=labels, loc="upper center",
bbox_to_anchor=(0.5, y), ncol=min(len(handles), max_ncol),
columnspacing=1.4, handletextpad=0.5, fontsize=fontsize,
frameon=True)