10000 [Bug]: Global legend weird behaviors · Issue #26841 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

[Bug]: Global legend weird behaviors #26841

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
imxtx opened this issue Sep 20, 2023 · 2 comments · Fixed by #27175
Closed

[Bug]: Global legend weird behaviors #26841

imxtx opened this issue Sep 20, 2023 · 2 comments · Fixed by #27175
Milestone

Comments

@imxtx
Copy link
imxtx commented Sep 20, 2023

Bug summary

The color of the global legend for all subplots does not match the label colors.

Code for reproduction

esbs = np.array(list(range(0,41)))
lines = [e for e in asrs]

colors = ['dodgerblue', 'tomato', 'yellowgreen', 'orange', 'dodgerblue', 'tomato', 'yellowgreen', 'orange']
labels = ['A', 'C', 'M','F','A+', 'C+', 'M+','F+',]
linestyles = ['-', '-','-', '-','--','--','--', '--']

fig, axs = plt.subplots(2, figsize=(6, 6), sharex=False)

handles = []
for i in range(8):
    print(i, colors[i], labels[i])
    if i == 3 or i == 7:
        h = axs[1].plot(esbs, lines[i], color=colors[i], linewidth=1.2, linestyle=linestyles[i], label=labels[i])
    else:
        h = axs[0].plot(esbs, lines[i], color=colors[i], linewidth=1.2, linestyle=linestyles[i], label=labels[i])
    handles.append(h)

fig.legend(handles, labels=labels, ncol=2, bbox_to_anchor=(0.15, 1.06), loc='upper left')

plt.show()

Actual outcome

image

Expected outcome

The colors for legend F and legend F+ should be orange. But they are different.

Additional information

No response

Operating system

No response

Matplotlib Version

3.7.1

Matplotlib Backend

No response

Python version

No response

Jupyter version

No response

Installation

None

@tacaswell
Copy link
Member

This looks like it is due to the signature on legend being too forgiving.

https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.legend

It looks like we try to infer what the positional argument means (it can be a list of strings to go with the auto-discovered artists if it is the only positional argument or a list of handles if there are two positional arguments). If you do not pass the labels as a keyword argument it will raise.

What is going on here is we are going through a branch of

if (handles is not None or labels is not None) and args:
_api.warn_external("You have mixed positional and keyword arguments, "
"some input may be discarded.")
# if got both handles and labels as kwargs, make same length
if handles and labels:
handles, labels = zip(*zip(handles, labels))
elif handles is not None and labels is None:
labels = [handle.get_label() for handle in handles]
elif labels is not None and handles is None:
# Get as many handles as there are labels.
handles = [handle for handle, label
in zip(_get_legend_handles(axs, handlers), labels)]
elif len(args) == 0: # 0 args: automatically detect labels and handles.
handles, labels = _get_legend_handles_labels(axs, handlers)
if not handles:
log.warning(
"No artists with labels found to put in legend. Note that "
"artists whose label start with an underscore are ignored "
"when legend() is called with no argument.")
elif len(args) == 1: # 1 arg: user defined labels, automatic handle detection.
labels, = args
if any(isinstance(l, Artist) for l in labels):
raise TypeError("A single argument passed to legend() must be a "
"list of labels, but found an Artist in there.")
# Get as many handles as there are labels.
handles = [handle for handle, label
in zip(_get_legend_handles(axs, handlers), labels)]
elif len(args) == 2: # 2 args: user defined handles and labels.
handles, labels = args[:2]
else:
raise _api.nargs_error('legend', '0-2', len(args))
that never looks at *args so the list of handles is being ignored (!!) so are zipping the labels passed in with the list of discovered handles which will be by axes hence the two lines on the second axes will be together at the end.

You should be getting a warning that looks like

<ipython-input-12-f89da0bd97bd>:19: UserWarning: You have mixed positional and keyword arguments, some input may be discarded.
  fig.legend(handles, labels=labels, ncol=2, bbox_to_anchor=(0.15, 1.06), loc='upper left')

I think we should escalate that warning to an exception because we have been warning for at least 6 years (but without saying it would break in the future), it is really bad to drop user input on the floor, and this behavior is deeply miss leading.


In the mean time

fig.legend(handles, labels, ncol=2, bbox_to_anchor=(0.15, 1.06), loc='upper left')
fig.legend(handles=handles, labels=labels, ncol=2, bbox_to_anchor=(0.15, 1.06), loc='upper left')

both work correctly but he best invocation is probably

fig.legend(handles=handles, ncol=2, bbox_to_anchor=(0.15, 1.06), loc='upper lef

which lets the legend code extract the label from the Artist (which you set when you create the Artist so it can never gets out of sync).

@tacaswell tacaswell added this to the v3.9.0 milestone Sep 20, 2023
@imxtx
Copy link
Author
imxtx commented Sep 20, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants
0