mwaskom / seaborn

Statistical data visualization in Python
https://seaborn.pydata.org
BSD 3-Clause "New" or "Revised" License
12.53k stars 1.92k forks source link

Improve multi-axes layout options #2051

Open mwaskom opened 4 years ago

mwaskom commented 4 years ago

Here are some notes on a plan for improving the layout options in the classes defined in seaborn.axisgrid. These objects try to produce plots with "nice layouts" by default, including by placing a joint legend "outside" and then resizing the figure to include it. They also use a different approach to specifying figure size such that the user gives the height and aspect ratio of each facet rather than the total figure size.

Some issues are that

Matplotlib has a new constrained layout manager which could give better performance than the current approach of periodically calling tight_layout. It is currently described as "experimental" so it can't be the default but it could be made an option.

I'd also like to replace the bespoke approach to modifying the figure size to account for the legend by using the existing bbox_inches="tight" machinery, and also extend it to expand the figure to include the axis ticks and labels in a way that doesn't distort the requested size and aspect ratio.

I've figured out some of this. Here's a relevant example:

f, ax = plt.subplots(figsize=(4, 4))

f.subplots_adjust(0, 0, 1, 1)
ax.plot([0, 1], [0, 1], label="the plot")
ax.legend(loc="center left", bbox_to_anchor=(1, .5))

renderer = f.canvas.get_renderer()
f.draw(renderer)
bbox = f.get_tightbbox(renderer)
tight_bbox.adjust_bbox(f, bbox.padded(.05))

image

The basic approach then is going to be to set up the subplot array, set the subplot params to minimize exterior padding, delegate arrangement of the interior of the plot to tight_layout or constrained_layout, and then use an "expand figure" operation with the above logic when the legend/labels change.

Other related ideas:

MaozGelbart commented 4 years ago

These objects try to produce plots with "nice layouts" by default, including by placing a joint legend "outside" and then resizing the figure to include it.

I think https://github.com/matplotlib/matplotlib/pull/13072 may be useful to overcome some of the described difficulties with the current legend placement of FacetGrid (in fact seaborn facet plots are mentioned as a use case in the linked issue).

mwaskom commented 4 years ago

Thanks for the pointer. Unfortunately I don't think matplotlib has any machinery to place the legend outside the subplot grid and expand the figure to fully match what seaborn wants to do. This is because I want to preserve the height/aspect parameterization of facetgrid figure sizes.

It's possible that it would give seaborn a different route: put the legend outside, let constrained layout shrink the subplot grid, then calculate how much it shrank by and resize the whole figure (similar to the current approach, maybe more robust).

jklymak commented 4 years ago

Right now CL doesn't shrink the axes yet if the axes have a fixed aspect. https://github.com/matplotlib/matplotlib/pull/17246 does this with a new kwarg, because its not super flexible.

mwaskom commented 4 years ago

@jklymak aspect in seaborn (in this context) means something a little bit different than in matplotlib. I want to fix the aspect ratio of the subplot in figure coordinates, not in data coordinates.

jklymak commented 4 years ago

Fair enough, but the same problem applies. The space between the subplots is, by default, spread out. I still think matplotlib/matplotlib#17246 could apply to your case - it just cares about the difference between the axes "original" position and the actual position.

mwaskom commented 4 years ago

It might apply in some cases, but what I'm trying to deal with is situations where the subplots get squashed inwards. e.g.:

f, axs = plt.subplots(1, 2, figsize=(6, 3), constrained_layout=True, sharey=True)
axs[0].set(yticks=[0, 1], yticklabels=["This is a really long tick label", "This one is too"])

image

jklymak commented 4 years ago

I see, so you want the equivalent of bbox_inches='tight' but at draw time instead of save time? i.e. 6, 3 is the "natural" size, but it will expand if that is too small?

mwaskom commented 4 years ago

Yep, that's the basic idea. The tricky part is going to be combining that with either constrained_layout or tight_layout to get nice automatic spacing on the interior of a subplot grid without changing the aspect ratio.

I guess, formally, I want the automatic transformations to translate the axes but not scale them.