Skip to content

[Bug]: Global legend weird behaviors #26841

Closed

Description

@imxtx

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

Activity

  1. tacaswell commented on Sep 20, 2023

    @tacaswell
    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).

  2. added this to the v3.9.0 milestone on Sep 20, 2023
  3. added
    API: changesChanges to the public API, typically requiring deprecation.
    on Sep 20, 2023
  4. imxtx commented on Sep 20, 2023

    @imxtx
    Author
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    API: changesChanges to the public API, typically requiring deprecation.

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions