0

I'm trying to reverse the label and key columns in a matplotlib legend and I'm really struggling to even know where to start.

In a normal matplotlib legend the pattern is key, then label, like in the example below where it goes key (blue line), then label (First Line):

Example

To match our company plotting style we plot things the reverse, i.e., label first then key (see the legend below). So the plot above would be First line, then the key (blue line). The additional complication is that the keys should be in one column (so the align in one vertical column) regardless of the length of the label.

Company Example

mylesmoose
  • 35
  • 6

2 Answers2

2

Well, there is the keyword markerfirst for this.

from matplotlib import pyplot as plt
import numpy as np

np.random.seed(1234)
n=7

fig, ax = plt.subplots()
ax.plot(np.arange(n), np.random.random(n), label="ABCDEF")
ax.plot(np.arange(n), np.random.random(n), label="G")

ax.legend(markerfirst=False)
plt.show()

Sample output
enter image description here

Mr. T
  • 11,960
  • 10
  • 32
  • 54
  • 1
    Works perfectly! Many thanks. I looked here: https://matplotlib.org/tutorials/intermediate/legend_guide.html which I guess was a mistake. So many things to learn... – mylesmoose Nov 16 '20 at 21:12
  • Not at all a mistake. These examples and tutorials cover a wide range of typical problems. If I don't find a fitting example there, I go to the function reference. Usually, I don't know what all of this means and try to find an SO answer. :) – Mr. T Nov 16 '20 at 21:35
  • Is it possible to have `markerfirst=True` and the text aligned to the left? – Guimoute Nov 16 '20 at 22:24
  • Surprisingly, you have to go into the matplotlib basement to do this. https://stackoverflow.com/a/8078114/8881141 But there must be an easier way - matplotlib switches effortless between `markerfirst=True` and `markerfirst=False` the label alignment from left to right. Anybody up for going through the source code? I wish ImportanceOfEarnest were still here - he had such in-depth knowledge. – Mr. T Nov 17 '20 at 02:54
1

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.

enter image description here

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()
Guimoute
  • 4,407
  • 3
  • 12
  • 28