6

I am creating a figure (with the seaborn.objects interface) that requires a legend with both colors and markers. I am trying to move the legend that is generated, but I am having trouble moving the marker section. See the example below:

The example is intended to be run in a notebook environment. Add plt.show() to display figures when running as a Python file.

import matplotlib.pyplot as plt
import seaborn as sns
import seaborn.objects as so

mpg = sns.load_dataset("mpg")
f, axs = plt.subplots(1, 2, figsize=(8, 4))
_ = (
    so.Plot(mpg, "weight", "acceleration", color="cylinders")
    .add(so.Dot(), marker="origin")
    .scale(
        color=["#49b", "#a6a", "#5b8", "#128", "#332"],
        marker={"japan": ".", "europe": "+", "usa": "*"},
    )
    .on(axs[0])
    .plot()
)

The legends includes a color and marker section, and half of it is cut off on the right-side of the figure.

I am trying to move the legend from far-right of plot to right-side of subplot. The answer to How can I customize the legend with Seaborn 0.12 objects? is only a partial solution in this case. Only part of the legend is moved successfully. I wonder why legend_handles doesn't catch the 2nd part of legend...

l1 = f.legends.pop(0)
axs[0].legend(l1.legend_handles, [t.get_text() for t in l1.texts])

Only the color section of the legend is visible, not the marker section.

How can all parts of the legend be moved?

Joshua Shew
  • 618
  • 2
  • 19
user21559775
  • 101
  • 4

1 Answers1

0

Disclaimer: This is a hacky solution that may not work for all cases. I developed it with only this question in mind, so I cannot promise that it will generalize effectively. Please leave a comment if this does not cover your particular use-case.

There are two key elements to this solution:

  1. Accessing both figure sections with plotter._legend_contents
  2. Adapting a seaborn utility method used in the legend creation process, adjust_legend_subtitles to recreate the legend with seaborn style subtitles.

I have adapted the example in the question to be run in a notebook environment, and I have included a solution to the question. See below:

import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import seaborn.objects as so

sns.set_theme(style="white")
mpg = sns.load_dataset("mpg")
fig, axs = plt.subplots(1, 2, figsize=(8, 4))
ax = axs[0]
plotter = (
    so.Plot(mpg, "weight", "acceleration", color="cylinders")
    .add(so.Dot(), marker="origin")
    .scale(
        color=["#49b", "#a6a", "#5b8", "#128", "#332"],
        marker={"japan": ".", "europe": "+", "usa": "*"},
    )
    .on(ax)
    .plot()
)

# --- Solution begins here --- #

legend_contents = plotter._legend_contents
blank_handle = mpl.patches.Patch(alpha=0, linewidth=0, visible=False)
handles = []
labels = []
for legend_content in legend_contents:
    handles.append(blank_handle)
    handles.extend(legend_content[1])
    labels.append(legend_content[0][0])
    labels.extend(legend_content[2])
axs[0].legend(handles, labels)
def adjust_legend_subtitles(legend):
    font_size = plt.rcParams.get("legend.title_fontsize", None)
    hpackers = legend.findobj(mpl.offsetbox.VPacker)[0].get_children()
    for hpack in hpackers:
        draw_area, text_area = hpack.get_children()
        handles = draw_area.get_children()
        handle = handles[0]
        if not handles[0]._visible:
            draw_area.set_width(0)
            for text in text_area.get_children():
                if font_size is not None:
                    text.set_size(font_size)
adjust_legend_subtitles(ax.get_legend())
fig.legends.pop(0)

# --- Optional! --- #
# After this move, you can use sns.move_legend to move the legend where
# you want it. However, you will have to call adjust_legend_subtitles
# again to get the "subtitle" formatting back.

sns.move_legend(ax, "best")
adjust_legend_subtitles(axs[0].get_legend())

The result includes both aspects of the legend, and even adds the subtitles back to the legend after the move.

This solution was tested on the following versions:

  • seaborn==0.12.2
  • matplotlib==3.7.2
Joshua Shew
  • 618
  • 2
  • 19