I would like to have a .mplstyle that by default produces square plots, per discussion started in #15001 and I still think "square" plots would be a reasonable thing to be able to set as default.
tl;dr Want square plots of the same size with or without colorbar. What should change is the xdim of the figure.
Anyway, for a single plot this is easy setting figsize either live or in the style. This doesn't let me specify the box size other than manually, but I can live with that
x = np.linspace(0,1,51)
y = np.random.uniform(0,5,51)
z = np.random.normal(0,1,51)
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot()
ax.scatter(x, y, c = z);
It gets trickier if I want to add a colorbar as well, but I would like the main box size to not change. The simplest method as far as I can tell is with fig.colorbar()
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot()
sc = ax.scatter(x, y, c = z);
fig.colorbar(sc, ax=ax);
However, this modifies the main box to fit within figure and changes the aspect.
The other option is to use make_axes_locatable Which if I understand correctly better splits the current axes and some reason this results in a less screwed up aspect than the above, but still changes it.
from mpl_toolkits.axes_grid1 import make_axes_locatable
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot()
divider = make_axes_locatable(ax)
cax = divider.append_axes('right', size="7%", pad=0.2,)
sc = ax.scatter(x, y, c = z);
cbar = fig.colorbar(sc, cax=cax);
Current best solution I found is resizing figure afterwards based on a set axes size with @ImportanceOfBeingErnest 's answer https://stackoverflow.com/a/44971177. However, this is sub optimal because based on ticks/labels etc it doesn't always produce the same size output figure, which can be a problem when formatting a tex document later for example.
One solutions would be to have custom function(s) presetting the sizes manually like make_rgb_axes in https://matplotlib.org/3.1.1/gallery/axes_grid1/demo_axes_rgb.html which would get rid of the trial and error of setting the fig/ax size until it's just right. The obvious drawback of that is someone then wants a different size cbar or different padding it will screw up the layout and the fact that it needs to be packages somewhere else, which makes it more complicated for user to look up syntax/problems.
If you want an automatic solution either the figure size stays constant and you resize the elements inside to fit, or the elements stay the same and you resize the figure. In general, Matplotlib assumes you want the figure to be a certain size and changes the size of elements to fit.
It’s not possible for a given axes to know that other axes will have a colorbar. Constrained layout makes the axes have the same size regardless of whether they have a colorbar or not for a single figure. But of course it knows nothing of what you may have done in another figure.
Making the plots square is something proposed in the previously noted PR. That’s pretty orthogonal to the rest of this discussion.
Overall it seems you need a custom solution and as you noted before it’d probably be best as a fig, axs = mysubplotarrange(params) type method.
Making the plots square is something proposed in the previously noted PR. That’s pretty orthogonal to the rest of this discussion.
Yes and no. If I could have a default figsize(7,5) and a default option of square axes aspect ratio, resulting in a (5,5) axes. Appending colorbar would not have to affect the main plot, it would just eat the extra x whitespace. Then if I want them side by side in a pdf. I can size them by height to get a consistent result.
That's it for functionality, but I really think having an rcParam to be able to get square plots by default is reasonable, though I understand the concern of not having an infinitely sized config.
OK, if you want to advocate for the functionality in #14917 to have an rcParam, we can have that discussion there or after that PR is merged. I'm -0.5 on this as something that belongs in rcParams, but others have more expansive ideas of how many things should be automated vs explicitly stated in user code.
It all depends on the constrains, so solutions may become arbitrarily complicated. IIUC, the constrains here are:
One or more square subplots
Arbitrary data ranges
A colorbar next to each subplot
The colorbar should have the same height as the axes it belongs to
There is currently no inbuilt way to achieve this. Even the proposed #14917 would not automatically change the colorbar height. Also, inbuilt options in axes_grid1 do not allow for arbitrary data ranges. So the only solution for all those constrains I can think of is the following. It looks a bit complicated, but is copy-pastable without need to understand each detail.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.transforms import Bbox
from mpl_toolkits.axes_grid1 import make_axes_locatable, axes_size
class RemainderFixed(axes_size.Scaled):
def __init__(self, xsizes, ysizes, divider):
self.xsizes =xsizes
self.ysizes =ysizes
self.div = divider
def get_size(self, renderer):
xrel, xabs = axes_size.AddList(self.xsizes).get_size(renderer)
yrel, yabs = axes_size.AddList(self.ysizes).get_size(renderer)
bb = Bbox.from_bounds(*self.div.get_position()).transformed(self.div._fig.transFigure)
w = bb.width/self.div._fig.dpi - xabs
h = bb.height/self.div._fig.dpi - yabs
return 0, min([w,h])
def make_square_axes_with_colorbar(ax, size=0.1, pad=0.1):
""" Make an axes square, add a colorbar axes next to it,
Parameters: size: Size of colorbar axes in inches
pad : Padding between axes and cbar in inches
Returns: colorbar axes
"""
divider = make_axes_locatable(ax)
margin_size = axes_size.Fixed(size)
pad_size = axes_size.Fixed(pad)
xsizes = [pad_size, margin_size]
yhax = divider.append_axes("right", size=margin_size, pad=pad_size)
divider.set_horizontal([RemainderFixed(xsizes, [], divider)] + xsizes)
divider.set_vertical([RemainderFixed(xsizes, [], divider)])
return yhax
x = np.random.normal(512, 112, 240)
y = np.random.normal(0.5, 0.1, 240)
fig, ax = plt.subplots()
sc = ax.scatter(x,y, c=y)
cax = make_square_axes_with_colorbar(ax, size=0.15, pad=0.1)
fig.colorbar(sc, cax=cax)
plt.show()
The power of this solution becomes clear when interactively resizing the figure - all constraints are kept.
@ImportanceOfBeingErnest This is pretty great, though I get some strange behaviour. Setting plt.subplots(figsize=(10,5))
I get this with the function
And this if I don't call the make_square_axes_with_colorbar()
For some reason the y-size is visibly different, even though fig.get_size_inches() gives the initial numbers back in both cases.
If it can be factored out into make_axes_square() and append_axes_not_changing_current_layout() or just specifically append_cbar_not_changing_current_layout() it would fit all the constraints. Either way would be fine if it required extra white space in the figure from the beginning or if it extend the figsize in the append direction.
The approach above does not change the figure size. I'm not sure I understand "axes_not_changing_current_layout" or "cbar_not_changing_current_layout", because adding a colorbar necessarily needs to change the layout (either by taking space from the axes, or adding the colorbar outside).
In any case, the result you get seems to be a consequence of using bbox_inches="tight" when saving the figure (or using the %matplotlib inline backend, which does so automatically). So this would need to be turned off in case the same image size is desired.
Indeed seems, that was due to using jupyter. If I run the same code in a script, setting matplotlib.use('Agg') to be sure, the result is that the plot is outside of the plottable area.
Indeed what changed was the main axes y dimensions, which is not what I want. Maybe I didn't describe it so well.
Based on the discussed constraint on not being able to have a square fig by default I start with regular output of fig, ax = plt.subplots()
After calling make_axes_square() this would be
and after appending the cbar this would be
Or maybe it would just be simpler to start with a square figsize like this:
and for the cbar to extend the figure so that it fits in whithout modifying the main axes
I cannot reproduce the cut figure, it works fine, also when using agg backend.
In case you don't want to show the colorbar you could just set it invisible (cax.set_visible(False)).
I'm a bit confused about the rest here, because this all started off with the desire not to have the figure size changed, now the last proposal turns this around again and changes the figure size?
That's why I put up the illustrations. I realize now that "plotting area" in the title might have been interpreted as the figure size as opposed to the main axes size.
It should be possible to get this two ways 1) think ahead and have a figsize that will accommodate the cbar if added (i.e square main axes with extra white space on the sides and then what i thought your function above would do or 2) extending the figsize in the direction where the cax is added.
Does that make any sense?
From the example, I took your function above and just tested different figsize, pad, and size
I had it build from master, but just ran pip install --upgrade --force-reinstall matplotlib to x-check and I still see the same result. Version: 3.1.1. I am using conda and python3.7
That doesn't take the padding between axes and colorbar into account, right? I am a bit lost on what the ultimate purpose is here, I always thought it to be about square axes. But if you found something that suits your needs, I'm glad I could help.
Good point, but I think I can add that. Square axes, was my use case, but really the idea is to be generally able to append a cbar or ratio on the bottom and not changing the sizing of the main plot. If I take the two plots above and size them vertically in a document, the main plot will be the same size for both.
Reacted by Rodrigo L.C., Minkyu Park and Jamie Fitz
2 years later, this is still a problem. Why is it that this still requires hacky solutions to work?
Reacted by Benjamin Spiegel, Habtamu Wubie Tesfaw, Markus Dirnberger, MrGibus, MichalLebeda, Matthias Zech, akern40, Charles Dufour, pasteller, Kundan Panta and 7 more
If you really want to be clever and use the code base in an unintended way, you can just add the same axis to all your subplots and then hide all but the one you want to actually show the color bar. Obviously this is not desirable, but it works nonetheless.
Using the LayoutEngine API that we added in 3.6 (https://matplotlib.org/stable/api/layout_engine_api.html) it should now be possible to write a layout engine that can implement what @ImportanceOfBeingErnest did in a more generic way, however I expect there to be some surprises while implementing it (I suspect you will have to replace the transforms on the Axes which could get interesting).
The core of the problem is that currently the absolute figure size (fig.get_size_inches()) is fixed and everything in the figure is positioned in a fractional coordinate system of the Figure (and things like fixed aspect are implemented as a second step of the layout that will adjust very late in the draw process). Additionally the elements in the figure do not actually know about each other in a meaningful way (the LayoutEngines know about all of the elements and can make global choices to re-position each of the elements).
In contrast, what is being requested here is that the Axes have absolute sizes and positions and then the figure resizes to accommodate. This is reasonable, but goes against a lot of the assumptions in the mpl code base (and you can sort of fake this behavior with fig.savefig(..., bbox_inches='tight') (which is used by the matplotlib-inline backend)). As I said above, I think this can be done using LayoutEngine, but am not sure what other changes would be needed to make it "nice".
Activity
jklymak commented on Aug 8, 2019
If you want an automatic solution either the figure size stays constant and you resize the elements inside to fit, or the elements stay the same and you resize the figure. In general, Matplotlib assumes you want the figure to be a certain size and changes the size of elements to fit.
It’s not possible for a given axes to know that other axes will have a colorbar. Constrained layout makes the axes have the same size regardless of whether they have a colorbar or not for a single figure. But of course it knows nothing of what you may have done in another figure.
Making the plots square is something proposed in the previously noted PR. That’s pretty orthogonal to the rest of this discussion.
Overall it seems you need a custom solution and as you noted before it’d probably be best as a fig, axs = mysubplotarrange(params) type method.
andrzejnovak commented on Aug 8, 2019
Yes and no. If I could have a default figsize(7,5) and a default option of square axes aspect ratio, resulting in a (5,5) axes. Appending colorbar would not have to affect the main plot, it would just eat the extra x whitespace. Then if I want them side by side in a pdf. I can size them by height to get a consistent result.
jklymak commented on Aug 8, 2019
OK, so what more than #14917 is needed?
andrzejnovak commented on Aug 8, 2019
That's it for functionality, but I really think having an rcParam to be able to get square plots by default is reasonable, though I understand the concern of not having an infinitely sized config.
jklymak commented on Aug 8, 2019
OK, if you want to advocate for the functionality in #14917 to have an rcParam, we can have that discussion there or after that PR is merged. I'm -0.5 on this as something that belongs in rcParams, but others have more expansive ideas of how many things should be automated vs explicitly stated in user code.
I'll close this as covered in #14917
ImportanceOfBeingErnest commented on Aug 8, 2019
It all depends on the constrains, so solutions may become arbitrarily complicated. IIUC, the constrains here are:
There is currently no inbuilt way to achieve this. Even the proposed #14917 would not automatically change the colorbar height. Also, inbuilt options in axes_grid1 do not allow for arbitrary data ranges. So the only solution for all those constrains I can think of is the following. It looks a bit complicated, but is copy-pastable without need to understand each detail.
import numpy as np import matplotlib.pyplot as plt from matplotlib.transforms import Bbox from mpl_toolkits.axes_grid1 import make_axes_locatable, axes_size class RemainderFixed(axes_size.Scaled): def __init__(self, xsizes, ysizes, divider): self.xsizes =xsizes self.ysizes =ysizes self.div = divider def get_size(self, renderer): xrel, xabs = axes_size.AddList(self.xsizes).get_size(renderer) yrel, yabs = axes_size.AddList(self.ysizes).get_size(renderer) bb = Bbox.from_bounds(*self.div.get_position()).transformed(self.div._fig.transFigure) w = bb.width/self.div._fig.dpi - xabs h = bb.height/self.div._fig.dpi - yabs return 0, min([w,h]) def make_square_axes_with_colorbar(ax, size=0.1, pad=0.1): """ Make an axes square, add a colorbar axes next to it, Parameters: size: Size of colorbar axes in inches pad : Padding between axes and cbar in inches Returns: colorbar axes """ divider = make_axes_locatable(ax) margin_size = axes_size.Fixed(size) pad_size = axes_size.Fixed(pad) xsizes = [pad_size, margin_size] yhax = divider.append_axes("right", size=margin_size, pad=pad_size) divider.set_horizontal([RemainderFixed(xsizes, [], divider)] + xsizes) divider.set_vertical([RemainderFixed(xsizes, [], divider)]) return yhax x = np.random.normal(512, 112, 240) y = np.random.normal(0.5, 0.1, 240) fig, ax = plt.subplots() sc = ax.scatter(x,y, c=y) cax = make_square_axes_with_colorbar(ax, size=0.15, pad=0.1) fig.colorbar(sc, cax=cax) plt.show()The power of this solution becomes clear when interactively resizing the figure - all constraints are kept.
andrzejnovak commented on Aug 9, 2019
@ImportanceOfBeingErnest This is pretty great, though I get some strange behaviour. Setting plt.subplots(figsize=(10,5))


I get this with the function
And this if I don't call the make_square_axes_with_colorbar()
For some reason the y-size is visibly different, even though fig.get_size_inches() gives the initial numbers back in both cases.
If it can be factored out into make_axes_square() and append_axes_not_changing_current_layout() or just specifically append_cbar_not_changing_current_layout() it would fit all the constraints. Either way would be fine if it required extra white space in the figure from the beginning or if it extend the figsize in the append direction.
ImportanceOfBeingErnest commented on Aug 9, 2019
The approach above does not change the figure size. I'm not sure I understand "axes_not_changing_current_layout" or "cbar_not_changing_current_layout", because adding a colorbar necessarily needs to change the layout (either by taking space from the axes, or adding the colorbar outside).
In any case, the result you get seems to be a consequence of using bbox_inches="tight" when saving the figure (or using the %matplotlib inline backend, which does so automatically). So this would need to be turned off in case the same image size is desired.
andrzejnovak commented on Aug 9, 2019
Indeed seems, that was due to using jupyter. If I run the same code in a script, setting matplotlib.use('Agg') to be sure, the result is that the plot is outside of the plottable area.

Indeed what changed was the main axes y dimensions, which is not what I want. Maybe I didn't describe it so well.
Based on the discussed constraint on not being able to have a square fig by default I start with regular output of fig, ax = plt.subplots()
After calling make_axes_square() this would be

and after appending the cbar this would be
Or maybe it would just be simpler to start with a square figsize like this:

and for the cbar to extend the figure so that it fits in whithout modifying the main axes

ImportanceOfBeingErnest commented on Aug 9, 2019
I cannot reproduce the cut figure, it works fine, also when using agg backend.
In case you don't want to show the colorbar you could just set it invisible (cax.set_visible(False)).
I'm a bit confused about the rest here, because this all started off with the desire not to have the figure size changed, now the last proposal turns this around again and changes the figure size?
andrzejnovak commented on Aug 9, 2019
That's why I put up the illustrations. I realize now that "plotting area" in the title might have been interpreted as the figure size as opposed to the main axes size.
It should be possible to get this two ways 1) think ahead and have a figsize that will accommodate the cbar if added (i.e square main axes with extra white space on the sides and then what i thought your function above would do or 2) extending the figsize in the direction where the cax is added.
Does that make any sense?
From the example, I took your function above and just tested different figsize, pad, and size
x = np.random.normal(512, 112, 240) y = np.random.normal(0.5, 0.1, 240) fig, ax = plt.subplots(figsize=(10,5)) sc = ax.scatter(x,y, c=y) cax = make_square_axes_with_colorbar(ax, size=1, pad=1) fig.colorbar(sc, cax=cax); fig.savefig('figtest.png') fig.savefig('figtest.pdf')I see the same effect regardless if png/pdf.
ImportanceOfBeingErnest commented on Aug 9, 2019
Are you by chance running this in PyCharm or some other environment? Which matplotlib version do you use?
andrzejnovak commented on Aug 9, 2019
I had it build from master, but just ran pip install --upgrade --force-reinstall matplotlib to x-check and I still see the same result. Version: 3.1.1. I am using conda and python3.7
andrzejnovak commented on Aug 23, 2019
Just for the record, if someone stumbles upon this, here's what I meant, implemented in the second approach. @ImportanceOfBeingErnest
def append_axes(ax, size=0.1, pad=0.1, position="right", extend=False): fig = ax.figure bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) width, height = bbox.width, bbox.height divider = make_axes_locatable(ax) margin_size = axes_size.Fixed(size) pad_size = axes_size.Fixed(pad) xsizes = [pad_size, margin_size] if position in ['top', 'bottom']: xsizes = xsizes[::-1] yhax = divider.append_axes(position, size=margin_size, pad=pad_size) def extend_ratio(ax): ax.figure.canvas.draw() orig_size = ax.get_position().size new_size = 0 for itax in ax.figure.axes: new_size += itax.get_position().size return new_size/orig_size if position in ["right"]: divider.set_horizontal([axes_size.Fixed(width)] + xsizes) fig.set_size_inches(fig.get_size_inches()[0] * extend_ratio(ax)[0], fig.get_size_inches()[1]) elif position in ["left"]: divider.set_horizontal(xsizes[::-1] + [axes_size.Fixed(width)]) fig.set_size_inches(fig.get_size_inches()[0] * extend_ratio(ax)[0], fig.get_size_inches()[1]) elif position in ['top']: divider.set_vertical([axes_size.Fixed(height)] + xsizes[::-1]) fig.set_size_inches(fig.get_size_inches()[0], fig.get_size_inches()[1] * extend_ratio(ax)[1]) elif position in ['bottom']: divider.set_vertical(xsizes + [axes_size.Fixed(height)]) fig.set_size_inches(fig.get_size_inches()[0], fig.get_size_inches()[1] * extend_ratio(ax)[1]) return yhaxWhich will take you from


to
ImportanceOfBeingErnest commented on Aug 23, 2019
That doesn't take the padding between axes and colorbar into account, right? I am a bit lost on what the ultimate purpose is here, I always thought it to be about square axes. But if you found something that suits your needs, I'm glad I could help.
andrzejnovak commented on Aug 25, 2019
Good point, but I think I can add that. Square axes, was my use case, but really the idea is to be generally able to append a cbar or ratio on the bottom and not changing the sizing of the main plot. If I take the two plots above and size them vertically in a document, the main plot will be the same size for both.
392781 commented on Nov 10, 2021
2 years later, this is still a problem. Why is it that this still requires hacky solutions to work?
souvikita commented on May 28, 2022
I agree.. It is a problem even now!!! :(
nmiles2718 commented on Oct 18, 2022
If you really want to be clever and use the code base in an unintended way, you can just add the same axis to all your subplots and then hide all but the one you want to actually show the color bar. Obviously this is not desirable, but it works nonetheless.
MWE
Laser-Cho commented on Oct 20, 2022
yeah. it is still a problem.
tacaswell commented on Oct 20, 2022
Using the LayoutEngine API that we added in 3.6 (https://matplotlib.org/stable/api/layout_engine_api.html) it should now be possible to write a layout engine that can implement what @ImportanceOfBeingErnest did in a more generic way, however I expect there to be some surprises while implementing it (I suspect you will have to replace the transforms on the Axes which could get interesting).
The core of the problem is that currently the absolute figure size (fig.get_size_inches()) is fixed and everything in the figure is positioned in a fractional coordinate system of the Figure (and things like fixed aspect are implemented as a second step of the layout that will adjust very late in the draw process). Additionally the elements in the figure do not actually know about each other in a meaningful way (the LayoutEngines know about all of the elements and can make global choices to re-position each of the elements).
In contrast, what is being requested here is that the Axes have absolute sizes and positions and then the figure resizes to accommodate. This is reasonable, but goes against a lot of the assumptions in the mpl code base (and you can sort of fake this behavior with fig.savefig(..., bbox_inches='tight') (which is used by the matplotlib-inline backend)). As I said above, I think this can be done using LayoutEngine, but am not sure what other changes would be needed to make it "nice".