0

I'm working on a program that allows users to put a different number of plots onto an axis. I need to show the legend, and the plot labels are long so I think it is best to show this under the plot. When I've done this before, I've always just shrunk the plot a bit and put the legend under the plot. Now that there are a varying number of plots this isn't as simple since I cannot find a nice formula for determining how much to shrink the plot and how far down to put the legend so it is not being cut off or overlapping the axis.

I've written a an example code to demonstrate what I currently am doing, which is ugly. I currently am checking how many items are in the plot and tried to manually optimize the axis shrink and legend offset parameters then did a big if loop to use the manually optimized values. They are not optimized for this example code, but I think it demonstrates what I am doing.

import matplotlib.pyplot as plt
import numpy as np

def find_scales(legendData):
        leg_len = len(legendData)
        if leg_len == 0:
            height_scale = 1
            legend_offset = 0
        elif leg_len == 1:
            height_scale = 0.96
            legend_offset = -0.18
        elif leg_len == 2:
            height_scale = 0.95
            legend_offset = -0.25
        elif leg_len == 3:
            height_scale = 0.94
            legend_offset = -0.35
        elif leg_len == 4:
            height_scale = 0.93
            legend_offset = -0.45
        elif leg_len == 5:
            height_scale = 0.93
            legend_offset = -0.57
        elif leg_len == 6:
            height_scale = 0.93
            legend_offset = -0.68
        elif leg_len == 7:
            height_scale = 0.93
            legend_offset = -0.82
        elif leg_len == 8:
            height_scale = 0.93
            legend_offset = -0.98
        elif leg_len == 9:
            height_scale = 0.92
            legend_offset = -1.3
        elif leg_len == 10:
            height_scale = 0.92
            legend_offset = -1.53
        else:
            height_scale = 0.92
            legend_offset = -1.8
        return height_scale, legend_offset

num_plots = 3
x_range = np.arange(10)

fig,ax = plt.subplots()

for i in range(num_plots):
    ax.plot(x_range, np.random.rand(10))

legend_labels = ['a','b','c','d','e','f','g','h','i','j'][:num_plots]

box = ax.get_position()

height_scale, legend_offset = find_scales(legend_labels)

ax.set_position([box.x0, box.y0 + box.height * (1-height_scale), #left, bottom, width, height
             box.width, box.height * height_scale])

ax.legend(legend_labels, loc=3, bbox_to_anchor=(0,legend_offset), borderaxespad=0.)

plt.show()

I'm hoping there's a better way to do this. I want the legend to be under the axis. I cannot have the legend be overlapping the axis or x-label. I cannot have the legend being cut off by being too low and out of the figure. Is there a way to do this so the axis and legend will automatically size themselves to fit in the figure?

TheStrangeQuark
  • 2,257
  • 5
  • 31
  • 58
  • you're (pretty much) linearly scaling `height_scale` and `legend_offset` as a function of `leg_len`). So just write a function that computes them for you and ditch all the `if/elif` blocks. – Paul H Nov 28 '17 at 19:43
  • I would guess that [○this question](https://stackoverflow.com/questions/42994338/creating-figure-with-exact-size-and-no-padding-and-legend-outside-the-axes)'s answer does what you want (it's just putting the legend above instead of below the plot). – ImportanceOfBeingErnest Nov 28 '17 at 19:46
  • Possible duplicate of [How to put the legend out of the plot](https://stackoverflow.com/questions/4700614/how-to-put-the-legend-out-of-the-plot) – wwii Nov 28 '17 at 19:48
  • @PaulH I spent time trying to find a scaling function but it seems to be more complex than a linearly scaling function, which is why I needed to calculate each case specifically. – TheStrangeQuark Nov 28 '17 at 19:52
  • @wwii This is the post I was working from. It does not address having a variable number of items in the legend. – TheStrangeQuark Nov 28 '17 at 19:54
  • @ImportanceOfBeingErnest I will look into this post and see if I can adapt it to my problem. – TheStrangeQuark Nov 28 '17 at 19:54
  • From the question it is clear that PO knows how to put the legend out of the plot. So the question obviously is not about putting the legend out of the plot but about automizing how much space there should be once the legend is created. @greenthumbtack It would be better to work with the extent of the legend box instead of the number of items in it. When putting the legend below the plot you would in addition to the linked answer want to use the xlabel's bottom as anchor point. – ImportanceOfBeingErnest Nov 28 '17 at 20:00
  • @ImportanceOfBeingErnest How can I use the xlabel's bottom as an ancho? I was looking exactly for that earlier but couldn't find a way to. – TheStrangeQuark Nov 28 '17 at 20:02

1 Answers1

1

A way to correct the axes position, such that the legend has enough space is shown in this question's answer: Creating figure with exact size and no padding (and legend outside the axes)

For the legend to sit on the bottom the solution is much simpler. Essentially you only need to subtract the legend height from the axes height and move the axes by the amount of the legend height towards the top.

import matplotlib.pyplot as plt 

fig = plt.figure(figsize = [3.5,2]) 
ax = fig.add_subplot(111)
ax.set_title('title')
ax.set_ylabel('y label')
ax.set_xlabel('x label')
ax.plot([1,2,3], marker="o", label="quantity 1")
ax.plot([2,1.7,1.2], marker="s", label="quantity 2")

def legend(ax, x0=0.5,y0=0, pad=0.5,**kwargs):
    otrans = ax.figure.transFigure
    t = ax.legend(bbox_to_anchor=(x0,y0), loc=8, bbox_transform=otrans,**kwargs)
    ax.figure.tight_layout(pad=pad)
    ax.figure.canvas.draw()
    tbox = t.get_window_extent().transformed( ax.figure.transFigure.inverted() )
    bbox = ax.get_position()
    ax.set_position([bbox.x0, bbox.y0+tbox.height,bbox.width, bbox.height-tbox.height]) 

legend(ax,y0=0, borderaxespad=0.2)

plt.savefig(__file__+'.pdf')
plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712