简体   繁体   中英

Prevent Matplotlib Legend from smushing h-padding on vertical subplots?

I have a script where I am plotting several variables in a grid of 5x1. I have noticed that when I have data that makes my legend shorter, the subplots themselves have an acceptable height and horizontal padding. When I have data that makes my legend bigger (vertically) the subplots are squashed leaving extra horizontal padding between the plots.

Is there a way to prevent this? To separate the legend assignment from the axes object and draw each plot independent of legend spacing?

Below is a minimal reproducible example to show what I mean:

#!/usr/bin/env python3
from pathlib import Path
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


def plotter(df, var_cols):
    dfa = df.query("accepted == 'accepted'")
    dfr = df.query("accepted != 'accepted'")
    colors = {v: c for v, c in zip(['accepted', 'rejected', 'rerun'],
                                   ['darkgreen', 'firebrick', 'steelblue'])}
    fig, axes = plt.subplots(nrows=len(var_cols), sharex=True)

    for var, ax in zip(var_cols, axes):
        for k, d in dfr.groupby('accepted'):
            ax.scatter(d.iteration, d[var], label=k, alpha=0.8, c=d.accepted.map(colors))
        ax.plot(dfa.iteration, dfa[var], '-o', label='accepted', color=colors['accepted'])

    # Grab 3rd axes because I want the legend to be towards the center
    handles, labels = axes[2].get_legend_handles_labels()
    # Sort legend labels to put 'accepted' on top
    labels, handles = zip(*sorted(zip(labels, handles), key=lambda t: t[0]))
    axes[2].legend(handles, labels, markerscale=1.2, bbox_to_anchor=(1, 0.5))
    fig.tight_layout()


def main():
    states = {1: 'accepted', 2: 'rejected', 3: 'rerun'}
    np.random.seed(666)
    dat1 = pd.DataFrame({
        'iteration': [0, 1, 2],
        'accepted': ['accepted']*3,
        'h_cap': [10.1, 6.5, 12.2],
        'h_stor': [500, 410, 0],
        'h_mark': [10, 6, 1],
        'bid': [500, 100, 50],
        'npv': [2.278, 2.6, 2.85]
    })

    dat2 = pd.DataFrame({
        'iteration': range(10),
        'accepted': [states[num] for num in np.random.randint(1, 4, size=10)],
        'h_cap': np.random.rand(10),
        'h_stor': np.random.rand(10),
        'h_mark': np.random.rand(10),
        'bid': np.random.rand(10),
        'npv': np.random.rand(10)
    })

    var_cols = ['h_cap', 'h_stor', 'h_mark', 'bid', 'npv']
    plotter(dat1, var_cols)
    plt.savefig(Path('~/Desktop/nonsmushed.png').expanduser())

    plotter(dat2, var_cols)
    plt.savefig(Path('~/Desktop/smushed.png').expanduser())


if __name__ == '__main__':
    main()

nonsmushed.png

在此处输入图片说明

smushed.png

在此处输入图片说明

Because your legend "belongs" to axes[2] , tight_layout() adjusts the spacing so that the adjacent axes don't cover the legend.

I think the simplest solution would be to create a "figure-level" legend ( fig.legend() ), but the problem with that is that tight_layout() doesn't account for that legend, and you will have to adjust the right margin by hand (there might be a way to calculate it automatically if needed, but that might get messy)

(...)
labels, handles = zip(*sorted(zip(labels, handles), key=lambda t: t[0]))
fig.legend(handles, labels, markerscale=1.2, bbox_to_anchor=(1, 0.5))
fig.tight_layout()
fig.subplots_adjust(right=0.75)  # adjust value as needed

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM