Skip to content
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

Plot legend needs more customizability #2994

Open
mwaskom opened this issue Sep 3, 2022 · 7 comments
Open

Plot legend needs more customizability #2994

mwaskom opened this issue Sep 3, 2022 · 7 comments

Comments

@mwaskom
Copy link
Owner

mwaskom commented Sep 3, 2022

There's not much ability to customize how the Plot legend appears, beyond what's available through Plot.theme. Desiderata include:

  • Control over position
  • Control over number of columns and location of title
  • Potential for independent drawing / localization of legends for distinct properties
  • Appearance of mapped properties on legend artists for other properties. In other words, If you map color and pointsize to different variables, the pointsize artists will appear in C0 even if that doesn't appear in the plot. (This probably needs both better defaults and more customizability).
@y9c
Copy link

y9c commented Jan 5, 2023

Hi @mwaskom. Thank you for developing the power Plot object. Any method for adjusting legend position in the latest version?

@albertogomcas

This comment was marked as off-topic.

@mwaskom
Copy link
Owner Author

mwaskom commented Feb 22, 2023

This issue is about the objects interface (where what you want actually is possible), so that comment is off topic. You may want to read through #2231

@JeppeKlitgaard
Copy link

I have run into this limitation as well. @thuiop has a work-around written in #3247 (comment) that I will copy over since I think people might end up finding this issue looking for a temporary solution:

# Credit: @thuiop 
def move_legend_fig_to_ax(fig, ax, loc, bbox_to_anchor=None, **kwargs):
    if fig.legends:
        old_legend = fig.legends[-1]
    else:
        raise ValueError("Figure has no legend attached.")

    old_boxes = old_legend.get_children()[0].get_children()

    legend_kws = inspect.signature(mpl.legend.Legend).parameters
    props = {
        k: v for k, v in old_legend.properties().items() if k in legend_kws
    }

    props.pop("bbox_to_anchor")
    title = props.pop("title")
    if "title" in kwargs:
        title.set_text(kwargs.pop("title"))
    title_kwargs = {k: v for k, v in kwargs.items() if k.startswith("title_")}
    for key, val in title_kwargs.items():
        title.set(**{key[6:]: val})
        kwargs.pop(key)
    kwargs.setdefault("frameon", old_legend.legendPatch.get_visible())

    # Remove the old legend and create the new one
    props.update(kwargs)
    fig.legends = []
    new_legend = ax.legend(
        [], [], loc=loc, bbox_to_anchor=bbox_to_anchor, **props
    )
    new_legend.get_children()[0].get_children().extend(old_boxes)

@thuiop could you elaborate on how to use this function? I can't get it to work with a

fig, ax = plt.subplots()
plot = so.Plot(...).on(ax)
plot.show()  # Needed otherwise `ValueError: Figure has no legend attached.`
move_legend_fig_to_ax(fig, ax, loc="center right")  # Doesn't do anything, but doesn't error

@FischyM
Copy link

FischyM commented May 18, 2023

@JeppeKlitgaard I was running into the same problem, except I was getting a plot with two legends where one was improperly placed. Here's a working example I got running that has two changes to the code you posted. The first was using plot.plot() instead of plot.show(), and the second was setting the first (and original?) legend visibility to False with fig.legends[0].set(visible=False).

import matplotlib as mpl
import inspect
import seaborn as sns
import seaborn.objects as so

def move_legend_fig_to_ax(fig, ax, loc, bbox_to_anchor=None, **kwargs):
    if fig.legends:
        fig.legends[0].set(visible=False)
        old_legend = fig.legends[-1]
    else:
        raise ValueError("Figure has no legend attached.")

    old_boxes = old_legend.get_children()[0].get_children()

    legend_kws = inspect.signature(mpl.legend.Legend).parameters
    props = {
        k: v for k, v in old_legend.properties().items() if k in legend_kws
    }

    props.pop("bbox_to_anchor")
    title = props.pop("title")
    if "title" in kwargs:
        title.set_text(kwargs.pop("title"))
    title_kwargs = {k: v for k, v in kwargs.items() if k.startswith("title_")}
    for key, val in title_kwargs.items():
        title.set(**{key[6:]: val})
        kwargs.pop(key)
    kwargs.setdefault("frameon", old_legend.legendPatch.get_visible())

    # Remove the old legend and create the new one
    props.update(kwargs)
    fig.legends = []
    new_legend = ax.legend(
        [], [], loc=loc, bbox_to_anchor=bbox_to_anchor, **props
    )
    new_legend.get_children()[0].get_children().extend(old_boxes)

penguins = sns.load_dataset("penguins")
fig, ax = plt.subplots()
plot = (
    so.Plot(penguins, x="bill_length_mm", y="bill_depth_mm",
            color="species", pointsize="body_mass_g",
            )
    .add(so.Dot())
    ).on(ax)
# plot.show()  # Needed otherwise `ValueError: Figure has no legend attached.`
plot.plot()
move_legend_fig_to_ax(fig, ax, loc="center", bbox_to_anchor=(0.7, 0.0, 0.9, 1))

Here are 3 figures showing the differences before and after my update to move_legend_fig_to_ax(). The first is the original code before move_legend_fig_to_ax(), the second is after running move_legend_fig_to_ax(), and the third is after my changes.

seaborn-legend-1
seaborn-legend-2
seaborn-legends-3

Full disclosure, I don't fully understand how or why this works, but in the end I got the plot legend looking how I wanted. It seems like calling plot.show() uses pyplot as the backend renderer but calling plot.show() uses whatever you have set. In my case, I'm using a Jupyter notebook in vscode, and after a little testing, my legends show up better using plot.plot() then plot.show(). Hope this helps!

@thuiop
Copy link
Contributor

thuiop commented Jun 20, 2023

My bad for only seeing this now @JeppeKlitgaard. This is intended to be used after plotting using p.on(ax).plot() ; .show() actually calls plt.show() so the trick will not work (don't forget to call plt.show() manually later though). I am unsure of why @FischyM had to manually set thevisibility of the original to false though as it should not exist anymore.

@jlec
Copy link

jlec commented Jul 24, 2024

Would be great to get this closed. Any update?

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

No branches or pull requests

7 participants