I would be tempted to write a standalone function that ignores ax.legend()
entirely and instead draws a white box, the labels, and the markers where you need them. All the coordinates would be expressed in ax coordinates via transform=ax.transAxes
to ensure a proper positioning and replace the locator keyword of ax.legend()
.
The following code will automatically cram all the artists found on the ax
in the legend box boundaries that you defined. You might need to adjust the "padding" a bit.
Note that for some reason it does not work with lines of width 0 that only use a marker, but it shouldn't be an issue considering your question.

import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
# Dummy data.
X = np.linspace(-5, +5, 100)
Y1 = np.sin(X)
Y2 = np.cos(X/3)
Y3 = Y2-Y1
Y4 = Y3*Y1
ax.plot(Y1, label="Y1")
ax.plot(Y2, label="Y2")
ax.plot(Y3, label="Y3", linestyle="--")
ax.plot(Y4, label="Y4", marker="d", markersize=4, linewidth=0)
fig.show()
def custom_legend(ax):
"""Adds a custom legend to the provided ax. Its labels are aligned
on the left and the markers on the right. Both are taken automatically
from the ax."""
handles, labels = ax.get_legend_handles_labels()
# Boundaries of your custom legend.
xmin, xmax = 0.7, 0.9
ymin, ymax = 0.5, 0.9
N = len(handles)
width = xmax-xmin
height = ymax-ymin
dy = height/N
r = plt.Rectangle((xmin, ymin),
width=width,
height=height,
transform=ax.transAxes,
fill=True,
facecolor="white",
edgecolor="black",
zorder=1000)
ax.add_artist(r)
# Grab the tiny lines that would be created by a call to `ax.legend()` so
# that we don't have to retrieve all the attributes ourselves.
legend = ax.legend()
handles = legend.legendHandles.copy()
legend.remove()
for n, (handle, label) in enumerate(zip(handles, labels)):
# Place the labels on the left of the legend box.
x = xmin + 0.01
y = ymax - n*dy - 0.05
ax.text(x, y, label, transform=ax.transAxes, va="center", ha="left", zorder=1001)
# Move a bit to the right and place the line artists.
x0 = (xmax - 1/2*width)
x1 = (xmax - 1/8*width)
y0, y1 = (y, y)
handle.set_data(((x0, x1), (y0, y1)))
handle.set_transform(ax.transAxes)
handle.set_zorder(1002)
ax.add_artist(handle)
custom_legend(ax)
fig.canvas.draw()