pandas.plot and pyplot.save_fig create different sized PNGs for same figsize - pandas

When I call the same function that uses pandas.plot with the same figsize, I get different sized PNG files. The width is same but the height in pixels changes. I suspect that the length of the x-axis labels changes the height.I have not yet tried directly calling the matplotlib functions.
I have also tried plt.rcParams['figure.figsize'] = (7,4). The problem does not appear to be in how figsize is set. My print_fig_info always produces the desire values.
# Primitive way that confirmed that the figure size does not change
def print_fig_info(label=""):
print(label,str(plt.gcf().get_size_inches()))
def my_plot(df):
global c
print_fig_info("Before plot")
df.plot(kind='bar', figsize=(7,4))
print_fig_info("After plot")
# want to make output files unique
c += 1
plt.savefig("output"+str(c), bbox_inches='tight', dpi='figure')

In your call to savefig you explicitely ask matplotlib to change the figsize to the minimal size that still fits all the elements in via bbox_inches='tight'.
Or in other words, bbox_inches='tight' is especially designed for changing the figure size to the minimum bounding box, and matplotlib is therefore doing what it's being asked for.
Solution: Don't use bbox_inches='tight'.

Related

Plotting xarray.DataArray and Geopandas together - aspect ratio errors

I am trying to create two images side by side: one satellite image alone, and next to it, the same satellite image with outlines of agricultural fields. My raster data "raster_clip" is loaded into rioxarray (original satellite image from NAIP, converted from .sid to .tif), and my vector data "ag_clip" is in geopandas. My code is as follows:
fig, (ax1, ax2) = plt.subplots(ncols = 2, figsize=(14,8))
raster_clip.plot.imshow(ax=ax1)
raster_clip.plot.imshow(ax=ax2)
ag_clip.boundary.plot(ax=ax1, color="yellow")
I can't seem to figure out how to get the y axes in each plot to be the same. When the vector data is excluded, then the two plots end up the same shape and size.
I have tried the following:
Setting sharey=True in the subplots method. Doesn't affect shape of resulting images, just removes the tic labels on the second image.
Setting "aspect='equal'" in the imshow method, leads to an error, which doesn't make sense because the 'aspect' kwarg is listed in the documentation for xarray.plot.imshow.
plt.imshow's 'aspect' kwarg is not available in xarray
Removing the "figsize" variable, doesn't affect the ratio of the two plots.
not entirely related to your question but i've used cartopy before for overlaying a GeoDataFrame to a DataArray
plt.figure(figsize=(16, 8))
ax = plt.subplot(projection=ccrs.PlateCarree())
ds.plot(ax=ax)
gdf.plot(ax=ax)

Making plot with subfigure created elsewhere (matplotlib)

I have a function "makeGrid(tensor)" that takes as an argument a pytorch tensor of shape (B,3,H,W), and uses ImageGrid to make a figure that displays the batch of figures in a grid.
Now, the model that outputs "tensor", depends on one parameter, "alpha". I would like to include a slider on the figure such that I can modify alpha "live". I am using the "Slider" widget from matplotlib roughly as such :
result = model(tensor)
images,grid = makeGrid(result)
ifig = plt.figure()# Figure with slider
axalpha = ifig.add_axes([0.25, 0.1, 0.65, 0.03])
# How to add the "images" to ifig ???
alpha_slider = Slider(
ax=axsmol,
valmin=-2,
valmax=2,
valinit=1,
)
def update(val):
model.alpha = alpha_slider.val
result= model(img_batch)
images,grid = makeGrid(result)
# Same problem, need to update ifig with new images
alpha_slider.on_changed(update)
plt.show()
So, my main problem is I have no idea how to use the already created figure (images) and/or grid (which is an ImageGrid object, roughly a list of axes afaik) as a subplot of "ifig", the interactive figure which contains slider and images.
Very sorry as this seems to be a basic question, but searching for "how to add already created figure as subplot of figure" or other things didn't yield solutions to my problem (or at least, in my limited point of view).

Reduce the size of pyplot subplot by scaling

I am using Julia v.0.6.0, with Juno+Atom IDE, and I am trying to make subplots with PyPlot package, v2.3.2. (Pretty new to this)
Consider the following MWE:
using PyPlot
fig = figure("Test subplots",figsize=(9,9))
subplot(2,2,1)
title("Plot 221")
fig[:add_subplot](2,2,2,polar="true")
title("Plot 222")
fig[:canvas][:draw]() # Update the figure
suptitle("2x2 Subplot",fontsize=15)
tight_layout(pad=2)
which yields me this:
Note how the second subplot is too big such that its title is too close to the polar plot.
What I want to achieve is to have the subplot 222 to still take up the same amount of space in the grid, but to have the polar plot scaled down in size, perhaps to 0.9 of its current size.
Note that this should not affect the size of rectangular grid in subplot 221 as well.
Is there an argument that I am missing from the matplotlib documentation?
The main thing here is to capture any subplot axes, title objects etc into 'handles', so that you can manipulate their properties individually in an easy manner. So change your initial code like so:
using PyPlot
fig = figure("Test subplots",figsize=(9,9))
subplot(2,2,1)
title("Plot 221")
S = subplot(2,2,2,polar="true") ## captured
T = title("Plot 222") ## captured
fig[:canvas][:draw]() # Update the figure
ST = suptitle("2x2 Subplot",fontsize=15) ## captured
tight_layout(pad=2)
Now you can use properties such as T[:get_verticalalignment] to inspect, and T[:set_verticalalignment] to set it to one of "center", "bottom", "top" or "baseline" (as per the matplotlib documentation). E.g.
T[:set_verticalalignment]("bottom")
ST[:set_verticalalignment]("center")
seems to get the amount of separation you'd probably expect.
Alternatively, for even finer control, you can inspect or change the absolute positioning (and implicitly the size) for S, T, or ST via their [:get_position] and [:set_position] methods respectively.
These methods accept either via a normal array denoting [start_x, start_y, width_x, width_y], or a Bbox, which is the form returned by the get methods above, therefore you might want to get that object from matplotlib:
Bbox = PyPlot.matplotlib[:transforms][:Bbox]
Now you can do stuff like this:
# inspect the object's position
S[:get_position]()
#> PyObject Bbox([[0.544201388889, 0.517261679293],
#> [0.943952721661, 0.917013012065]])
# absolute positioning using 'width' notation
S[:set_position]([0.51, 0.51, 0.4, 0.4])
# absolute positioning using a 'Bbox' (note 2D array input, not a vector!)
S[:set_position](Bbox([0.51 0.51; 0.89 0.89]))
# 'relative' by adjusting current position, and wrapping back in a Bbox
S[:set_position](Bbox( S[:get_position]()[:get_points]() + [0.1 0.1; -0.1 -0.1]))
# inspect title position
T[:get_position]()
#> (0.5, 1.05)
# raise title a bit higher (manually)
T[:set_position]([0.5, 1.10])
etc.

matplotlib: Get resulting bounding box of `bbox_inches=tight`

I often produce individual figures with matplotlib which are supposed to be aligned vertically or horizontally e.g. in a LaTeX document. My goals are:
Avoid excessive margins or clipping in all figures. For stand-alone figures this is easily solved by using bbox_inches='tight' in savefig.
Use the exact same bounding box for all figures, since I want my axes elements to line up nicely in the resulting document.
My current solution is a very inconvenient trial and error approach: I manually try to guess reasonable margins and set them via plt.subplots_adjust(). This often takes a lot of time until I have a satisfactory result.
I'm wondering if it is possible to combine the power of bbox_inches='tight' with plt.subplots_adjust()? Is it possible to programmatically get the resulting bounding box of bbox_inches='tight'? This would allow my to determining the bounding box for the figure with the largest axis labels/titles, and use this bounding box for all other figures.
Thanks to #Schorsch and #tcaswell for pointing out that ax.get_tightbbox almost solves this problem. The only tricky part is to convert the renderer dependent bbox into an "inches" bbox. The general solution is:
tight_bbox_raw = ax.get_tightbbox(fig.canvas.get_renderer())
tight_bbox = TransformedBbox(tight_bbox_raw, Affine2D().scale(1./fig.dpi))
I can now reuse the tight_bbox for my other plots as well, resulting in nice, identically boxed figures.
I have also written a small helper class -- not elegant, but useful: I call max_box_tracker.add_figure for every figure I create, and it will internally update a maximum bounding box. After generating all plots, I can print out the resulting bounding box (directly in the form of code to add to my script).
class MaxBoxTracker:
def __init__(self):
self.box = None
def add_figure(self, fig, ax):
from matplotlib.transforms import TransformedBbox, Affine2D
box_raw = ax.get_tightbbox(fig.canvas.get_renderer())
box = TransformedBbox(box_raw, Affine2D().scale(1./fig.dpi)).get_points()
if self.box is None:
self.box = box
else:
self.box[0][0] = min(self.box[0][0], box[0][0])
self.box[0][1] = min(self.box[0][1], box[0][1])
self.box[1][0] = max(self.box[1][0], box[1][0])
self.box[1][1] = max(self.box[1][1], box[1][1])
def print_max(self):
print 'Code to set maximum bbox:\nfrom matplotlib.transforms import Bbox\ntight_bbox = Bbox([[%f, %f], [%f, %f]])' % (
self.box[0][0], self.box[0][1], self.box[1][0], self.box[1][1]
)

Multiplot with matplotlib without knowing the number of plots before running

I have a problem with Matplotlib's subplots. I do not know the number of subplots I want to plot beforehand, but I know that I want them in two rows. so I cannot use
plt.subplot(212)
because I don't know the number that I should provide.
It should look like this:
Right now, I plot all the plots into a folder and put them together with illustrator, but there has to be a better way with Matplotlib. I can provide my code if I was unclear somewhere.
My understanding is that you only know the number of plots at runtime and hence are struggling with the shorthand syntax, e.g.:
plt.subplot(121)
Thankfully, to save you having to do some awkward math to figure out this number programatically, there is another interface which allows you to use the form:
plt.subplot(n_cols, n_rows, plot_num)
So in your case, given you want n plots, you can do:
n_plots = 5 # (or however many you programatically figure out you need)
n_cols = 2
n_rows = (n_plots + 1) // n_cols
for plot_num in range(n_plots):
ax = plt.subplot(n_cols, n_rows, plot_num)
# ... do some plotting
Alternatively, there is also a slightly more pythonic interface which you may wish to be aware of:
fig, subplots = plt.subplots(n_cols, n_rows)
for ax in subplots:
# ... do some plotting
(Notice that this was subplots() not the plain subplot()). Although I must admit, I have never used this latter interface.
HTH