Legend alignment

Hello,

Below is a MWE of a figure that includes two legends, one in two-column format and the other in one-column format. As written, the horizontal alignment between the two legends depends on details such as whether I generate the figure in a Jupyter notebook or with a script. I’ve experimented with transformations and have been unable to find a solution that robustly centers the legend keys of the single-column legend at the horizontal center of the two legend keys in the two-column legend. Any suggestions?

fig, ax = plt.subplots()

x = np.linspace(0, 1)
m = np.arange(1, 4)
lgnd_loc = (0.1, 0.1)
lbl = []
ln_pos = []
ln_neg = []
for k in m:
    ln_pos += ax.plot(x, k * x)
    ln_neg += ax.plot(x, -k * x)
    lbl.append(f"m = {k:d}")
ln_zero = ax.plot(x, np.zeros_like(x), label=f"m = 0")

# Create first part of the legend
lgnd_2col = ax.legend(
    title="Pos.   Neg.",
    alignment="left",
    handles=[*zip(ln_pos, ln_neg)],
    labels=lbl,
    frameon=False,
    fontsize="small",
    handler_map={tuple: HandlerTuple(ndivide=None, pad=1.0)},
    handlelength=6.0,
    loc="lower left",
    bbox_to_anchor=lgnd_loc,
    borderpad=0,
)

# Add first part of legend as an artist to create second part
lgnd_2col_art = ax.add_artist(lgnd_2col)

# Create second part of the legend
lgnd_loc_new = (lgnd_loc[0] - ax.transAxes.inverted().transform((60, 0))[0], lgnd_loc[1])
lgnd_1col = ax.legend(
    alignment="center",
    handles=ln_zero,
    ncols=1,
    frameon=False,
    fontsize="medium",
    loc="upper left",
    bbox_to_anchor=lgnd_loc_new,
    borderpad=0,
)

plt.show()

I’m not 100% certain which alignment you mean, but is there a reason why using the line itself is not okay?

x = np.linspace(0, 1)
m = np.arange(1, 4)
lgnd_loc = (0.1, 0.1)
lbl = []
ln_pos = []
ln_neg = []
for k in m:
    ln_pos += ax.plot(x, k * x)
    ln_neg += ax.plot(x, -k * x)
    lbl.append(f"m = {k:d}")
ln_zero, = ax.plot(x, np.zeros_like(x), label="m = 0")

ax.legend(
    title="Pos.   Neg.",
    alignment="left",
    handles=[*zip(ln_pos, ln_neg), ln_zero],
    labels=[*lbl, ln_zero.get_label()],
    frameon=False,
    fontsize="small",
    handler_map={tuple: HandlerTuple(ndivide=None, pad=1.0)},
    handlelength=6.0,
    loc="lower left",
    bbox_to_anchor=lgnd_loc,
    borderpad=0,
)

Thanks for the suggestion—I would prefer to have all of the keys the same size, though. I discovered I could do this by creating an empty handle with ln_empty = ax.plot([], []) and replacing the ln_zero in your example with (ln_empty, ln_zero). I was then able to center it by adjusting xdata for ln_zero.

That said, in my actual application I wanted the extra space between the last legend entry and the rest, and I also wanted better control over the column headings than the title parameter allowed. In the end, I used two calls to legend, one for the positive and negative values and one for the zero value, one call to fig.canvas.draw() to get intermediate positioning information, a call to lgnd_loc_new.set_bbox_to_anchor(...) to reposition the second legend with respect to the first, and finally two calls to text to place the column labels for the first legend. The call to fig.canvas.draw() feels a bit clunky, but I couldn’t find a better way to get the necessary legend positioning information.

The legend guide and the transformation tutorial were both helpful, but neither provided enough guidance for the issues I encountered with this problem. If anyone knows of a good reference for this kind of thing, please let me know.