Retrieving handles from legend returns line, and not tuple

See the example

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Patch

print(mpl.__version__)

fig, ax = plt.subplots()

ax.plot([1, 2], [1, 2])
ax.legend(
    [(Line2D([], [], color="C0", label="0"), Patch(color="C0", alpha=0.5, label="0"))],
    ["Label"]
)
legend = ax.get_legend()
handles = legend.legend_handles
print(handles)
[<matplotlib.lines.Line2D object at 0x7fa78a933310>]

I would expect it to be a list with a tuple of length 2: [(Line2D, Patch)] but this only shows [Line2D]. I looked into the documentation but I didn’t find this explained. I’m not sure if this is how it is supposed to work, or if it is a bug.

print(mpl.__version__)
# 3.7.0.dev1147+ge9d1f9c066

Note It looks like it keep only the first object. See the modification

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Patch

fig, ax = plt.subplots()

ax.plot([1, 2], [1, 2])
ax.legend(
    [(Patch(color="C0", alpha=0.5, label="0"), Line2D([], [], color="C0", label="0"))],
    ["Label"]
)
legend = ax.get_legend()
handles = legend.legend_handles
print(handles)
[<matplotlib.patches.Rectangle object at 0x7fa78a8aac40>]
1 Like

I found that this is intended (at least for what I see in the comment)

I would like to ask what’s the reason to do so

To add more to the confusion, I could dig into the legend handle box and its children to finally “find” the Recangle and Line2D artists. But now the question is why do we have them twice.

import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import Patch

fig, ax = plt.subplots()
ax.plot([1, 2], [1, 2])
legend = ax.legend([(Patch(color="C0", alpha=0.5), Line2D([], [], color="C0"))], ["Label"])
legend._legend_handle_box.get_children()[0].get_children()[0].get_children()[0].get_children()
[<matplotlib.patches.Rectangle at 0x7ff16711fd30>,
 <matplotlib.lines.Line2D at 0x7ff16711fcd0>,
 <matplotlib.patches.Rectangle at 0x7ff16712c100>,
 <matplotlib.lines.Line2D at 0x7ff16711ff10>]

Plus one to this. This is the context of why it would be great to resolve/fix this. python - Is it possible to iteratively build up legend entries through multiple plot function calls? - Stack Overflow

I think you are asking a bit much here - legend doesn’t say it will accept a list of tuples of artists, it says it will accept a list of artists. That a list of tuples works is surprising to me, but fair enough. But after that you are going to have to build the handle/label lists yourself, which seems easy enough…

import matplotlib.pyplot as plt
import numpy as np

handles = []
labels = []


def plot(x, y, ax, col, group, **kwargs):
    hp = ax.plot(x, y, '--', color=col, label=f"group {group}: Mean")
    hf = ax.fill_between(x, y + 10, y - 10, color=col, alpha=0.5, label='interval')
    return hp[0], hf

fig, ax = plt.subplots()
x = np.linspace(1, 100, 100)
hp, hf = plot(x, x, ax, "C0", 1)
handles += [(hp, hf)]
labels += ['group 1']
hp, hf = plot(x, x + 30, ax, "C1", 2)
handles += [(hp, hf)]
labels += ['group 2']
hp, hf = plot(x, x + 60, ax, "C2", 3)
handles += [(hp, hf)]
labels += ['group 3']

print(handles)
ax.legend(handles=handles,
              labels=labels)

plt.tight_layout()
plt.show()

At some point if you are going to try and do complicated things, you are going to need to manipulate the lists manually.

1 Like

Thanks a lot for the idea for the solution!

I’m not sure what’s the state about using tuples. I agree the behavior is a little surprising, but it’s also documented here Legend guide — Matplotlib 3.6.2 documentation and there’s a dedicated legend handler matplotlib.legend_handler — Matplotlib 3.6.2 documentation

Again, thanks a lot. I see we want to do something complicated and thus we need to pay some price for that.

How about matplotlib.axes.Axes.get_legend_handles_labels — Matplotlib 3.6.2 documentation ?