Open mwaskom opened 2 years ago
Hi @mwaskom. Thank you for developing the power Plot object. Any method for adjusting legend position in the latest version?
For a lot of use cases it would just be enough to at least be able to silence the legend of a specific property. So, I want the dots of a scatterplot to have different hue based on property "a", but I don't want to see the legend, however I still want to see the legend for style, which is mapped to property "b". At the moment is all in, so you need to gut the matplotlib legend, or nothing, so you need to gut the matplotlib axes content to iterate over the styles and construct the legend by hand. Both are really painful.
Instead, the legend parameter of scatterplot (to keep the example) could also accept a dictionary, so instead of "auto”, “brief”, “full”, or False you could also pass, dict(hue=False, style="full", size="brief") and selectively change the behavior for each of the properties.
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
I have run into this limitation as well. @thuiop has a work-around written in https://github.com/mwaskom/seaborn/issues/3247#issuecomment-1420731692 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
@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.
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!
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.
Would be great to get this closed. Any update?
There's not much ability to customize how the
Plot
legend appears, beyond what's available throughPlot.theme
. Desiderata include:C0
even if that doesn't appear in the plot. (This probably needs both better defaults and more customizability).