Aspect ratio in subplots with various y-axes - python

I would like the following code to produce 4 subplots of the same size with a common aspect ratio between the size of x-axis and y-axis set by me. Referring to the below example, I would like all of the subplots look exactly like the first one (upper left). What is wrong right now is that the size of the y-axis is correlated with its largest value. That is the behaviour I want to avoid.
import matplotlib.pyplot as plt
import numpy as np
def main():
fig = plt.figure(1, [5.5, 3])
for i in range(1,5):
fig.add_subplot(221+i-1, adjustable='box', aspect=1)
plt.plot(np.arange(0,(i)*4,i))
plt.show()
if __name__ == "__main__":
main()
Surprisingly, matplotlib produces the right thing by default (picture below):
import matplotlib.pyplot as plt
import numpy as np
def main():
fig = plt.figure(1, [5.5, 3])
for i in range(1,5):
fig.add_subplot(221+i-1)
plt.plot(np.arange(0,(i)*4,i))
plt.show()
I just want to add to this an ability to control the aspect ratio between lengths of x and y-axes.

I can't quite tell what you want from your question.
Do you want all of the plots to have the same data limits?
If so, use shared axes (I'm using subplots here, but you can avoid it if you want to stick to matlab-style code):
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=2, ncols=2, sharey=True, sharex=True)
for i, ax in enumerate(axes.flat, start=1):
ax.set(aspect=1)
ax.plot(np.arange(0, i * 4, i))
plt.show()
If you want them all to share their axes limits, but to have adjustable='box' (i.e. non-square axes boundaries), use adjustable='box-forced':
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=2, ncols=2, sharey=True, sharex=True)
for i, ax in enumerate(axes.flat, start=1):
ax.set(aspect=1, adjustable='box-forced', xticks=range(i))
ax.plot(np.arange(0, i * 4, i))
plt.show()
Edit: Sorry, I'm still a bit confused. Do you want something like this?
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=2, ncols=2)
for i, ax in enumerate(axes.flat, start=1):
ax.set(adjustable='datalim', aspect=1)
ax.plot(np.arange(0, i * 4, i))
plt.show()
Okay, I think I finally understand your question. We both meant entirely different things by "aspect ratio".
In matplotlib, the aspect ratio of the plot refers to the relative scales of the data limits. In other words, if the aspect ratio of the plot is 1, a line with a slope of one will appear at 45 degrees. You were assuming that the aspect ratio applied to the outline of the axes and not the data plotted on the axes.
You just want the outline of the subplots to be square. (In which case, they all have different aspect ratios, as defined by matplotlib.)
In that case, you need a square figure. (There are other ways, but just making a square figure is far simpler. Matplotlib axes fill up a space that is proportional to the size of the figure they're in.)
import matplotlib.pyplot as plt
import numpy as np
# The key here is the figsize (it needs to be square). The position and size of
# axes in matplotlib are defined relative to the size of the figure.
fig, axes = plt.subplots(nrows=2, ncols=2, figsize=(8,8))
for i, ax in enumerate(axes.flat, start=1):
ax.plot(np.arange(0, i * 4, i))
# By default, subplots leave a bit of room for tick labels on the left.
# We'll remove it so that the axes are perfectly square.
fig.subplots_adjust(left=0.1)
plt.show()

Combing the answer of Joe Kington with new pythonic style for shared axes square subplots in matplotlib?
and another post that I am afraid I cannot find it again, I made a code for precisely setting the ratio of the box to a given value.
Let desired_box_ratioN indicate the desired ratio between y and x sides of the box.
temp_inverse_axis_ratioN is the ratio between x and y sides of the current plot; since 'aspect' is the ratio between y and x scale (and not axes), we need to set aspect to desired_box_ratioN * temp_inverse_axis_ratioN.
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=2, ncols=2)
desired_box_ratioN = 1
for i, ax in enumerate(axes.flat, start=1):
ax.plot(np.arange(0, i * 4, i))
temp_inverse_axis_ratioN = abs( (ax.get_xlim()[1] - ax.get_xlim()[0])/(ax.get_ylim()[1] - ax.get_ylim()[0]) )
ax.set(aspect = desired_box_ratioN * temp_inverse_axis_ratioN, adjustable='box-forced')
plt.show()

The theory
Different coordinate systems exists in matplotlib. The differences between different coordinate systems can really confuse a lot of people. What the OP want is aspect ratio in display coordinate but ax.set_aspect() is setting the aspect ratio in data coordinate. Their relationship can be formulated as:
aspect = 1.0/dataRatio*dispRatio
where, aspect is the argument to use in set_aspect method, dataRatio is aspect ratio in data coordinate and dispRatio is your desired aspect ratio in display coordinate.
The practice
There is a get_data_ratio method which we can use to make our code more concise. A work code snippet is shown below:
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=2, ncols=2)
dispRatio = 0.5
for i, ax in enumerate(axes.flat, start=1):
ax.plot(np.arange(0, i * 4, i))
ax.set(aspect=1.0/ax.get_data_ratio()*dispRatio, adjustable='box-forced')
plt.show()
I have also written a detailed post about all this stuff here.

Related

How to reduce horizontal spacing between subplots in matplotlib python? [duplicate]

The code below produces gaps between the subplots. How do I remove the gaps between the subplots and make the image a tight grid?
import matplotlib.pyplot as plt
for i in range(16):
i = i + 1
ax1 = plt.subplot(4, 4, i)
plt.axis('on')
ax1.set_xticklabels([])
ax1.set_yticklabels([])
ax1.set_aspect('equal')
plt.subplots_adjust(wspace=None, hspace=None)
plt.show()
The problem is the use of aspect='equal', which prevents the subplots from stretching to an arbitrary aspect ratio and filling up all the empty space.
Normally, this would work:
import matplotlib.pyplot as plt
ax = [plt.subplot(2,2,i+1) for i in range(4)]
for a in ax:
a.set_xticklabels([])
a.set_yticklabels([])
plt.subplots_adjust(wspace=0, hspace=0)
The result is this:
However, with aspect='equal', as in the following code:
import matplotlib.pyplot as plt
ax = [plt.subplot(2,2,i+1) for i in range(4)]
for a in ax:
a.set_xticklabels([])
a.set_yticklabels([])
a.set_aspect('equal')
plt.subplots_adjust(wspace=0, hspace=0)
This is what we get:
The difference in this second case is that you've forced the x- and y-axes to have the same number of units/pixel. Since the axes go from 0 to 1 by default (i.e., before you plot anything), using aspect='equal' forces each axis to be a square. Since the figure is not a square, pyplot adds in extra spacing between the axes horizontally.
To get around this problem, you can set your figure to have the correct aspect ratio. We're going to use the object-oriented pyplot interface here, which I consider to be superior in general:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,8)) # Notice the equal aspect ratio
ax = [fig.add_subplot(2,2,i+1) for i in range(4)]
for a in ax:
a.set_xticklabels([])
a.set_yticklabels([])
a.set_aspect('equal')
fig.subplots_adjust(wspace=0, hspace=0)
Here's the result:
You can use gridspec to control the spacing between axes. There's more information here.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
plt.figure(figsize = (4,4))
gs1 = gridspec.GridSpec(4, 4)
gs1.update(wspace=0.025, hspace=0.05) # set the spacing between axes.
for i in range(16):
# i = i + 1 # grid spec indexes from 0
ax1 = plt.subplot(gs1[i])
plt.axis('on')
ax1.set_xticklabels([])
ax1.set_yticklabels([])
ax1.set_aspect('equal')
plt.show()
Without resorting gridspec entirely, the following might also be used to remove the gaps by setting wspace and hspace to zero:
import matplotlib.pyplot as plt
plt.clf()
f, axarr = plt.subplots(4, 4, gridspec_kw = {'wspace':0, 'hspace':0})
for i, ax in enumerate(f.axes):
ax.grid('on', linestyle='--')
ax.set_xticklabels([])
ax.set_yticklabels([])
plt.show()
plt.close()
Resulting in:
With recent matplotlib versions you might want to try Constrained Layout. This does (or at least did) not work with plt.subplot() however, so you need to use plt.subplots() instead:
fig, axs = plt.subplots(4, 4, constrained_layout=True)
Have you tried plt.tight_layout()?
with plt.tight_layout()
without it:
Or: something like this (use add_axes)
left=[0.1,0.3,0.5,0.7]
width=[0.2,0.2, 0.2, 0.2]
rectLS=[]
for x in left:
for y in left:
rectLS.append([x, y, 0.2, 0.2])
axLS=[]
fig=plt.figure()
axLS.append(fig.add_axes(rectLS[0]))
for i in [1,2,3]:
axLS.append(fig.add_axes(rectLS[i],sharey=axLS[-1]))
axLS.append(fig.add_axes(rectLS[4]))
for i in [1,2,3]:
axLS.append(fig.add_axes(rectLS[i+4],sharex=axLS[i],sharey=axLS[-1]))
axLS.append(fig.add_axes(rectLS[8]))
for i in [5,6,7]:
axLS.append(fig.add_axes(rectLS[i+4],sharex=axLS[i],sharey=axLS[-1]))
axLS.append(fig.add_axes(rectLS[12]))
for i in [9,10,11]:
axLS.append(fig.add_axes(rectLS[i+4],sharex=axLS[i],sharey=axLS[-1]))
If you don't need to share axes, then simply axLS=map(fig.add_axes, rectLS)
Another method is to use the pad keyword from plt.subplots_adjust(), which also accepts negative values:
import matplotlib.pyplot as plt
ax = [plt.subplot(2,2,i+1) for i in range(4)]
for a in ax:
a.set_xticklabels([])
a.set_yticklabels([])
plt.subplots_adjust(pad=-5.0)
Additionally, to remove the white at the outer fringe of all subplots (i.e. the canvas), always save with plt.savefig(fname, bbox_inches="tight").

Why is the figure size (y-axis) fluctuating in this example?

I've got a map of the world on which I am iteratively plotting drought areas in a for-loop.
For reproducibility, data is here: https://data.humdata.org/dataset/global-droughts-events-1980-2001
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import clear_output
sns.set_theme(style='whitegrid')
dr_geometry = gpd.read_file('data/dr_events.shp')
world_geometry = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
for y in dr_geometry.year.unique():
clear_output(wait=True)
fig, ax = plt.subplots(1, 1, figsize=(15, 15))
world_geometry.plot(ax=ax)
dr_geometry[dr_geometry.year == y].plot(ax=ax, color='red', edgecolor='black', linewidth=0.1)
plt.show();
This is working fine, except that the y-axis shrinks or expands on each iteration by a small but very noticeable amount, resulting in a choppy animation. How can I eliminate this behavior?
Note: Setting the ylim explicitly does not change this. Also I have tried moving the subplots instantiation outside of the for-loop, but this results in empty outputs.
An iteration output:
ax.set_aspect('equal') prevents the shifting on my end:
for y in dr_geometry.year.unique():
clear_output(wait=True)
fig, ax = plt.subplots(1, 1, figsize=(15, 15))
world_geometry.plot(ax=ax)
dr_geometry[dr_geometry.year == y].plot(ax=ax, color='red', edgecolor='black', linewidth=0.1)
# set aspect ratio explicitly
ax.set_aspect('equal')
plt.show();
Thanks to #martinfleis for pointing out the reason for the shifting:
geopandas#1121 - Consider scaling y-axis for unprojected map plots
geopandas#1290 - ENH: scaling y-axis for plots in geographic CRS
.plot() now automatically determines whether GeoSeries (or GeoDataFrame) is in geographic or projected CRS and calculates aspect for geographic using 1/cos(s_y * pi/180) with s_y as the y coordinate of the mean of y-bounds of GeoSeries. This leads to better representation of the actual shapes than current hard-coded 'equal' aspect.

Share scaling of differntly sized subplots' axes (not sharing axes)

With matplotlib, I want to plot two graphs with the same x-axis scale, but I want to show different sized sections. How can I accomplish that?
So far I can plot differently sized subplots with GridSpec or same sized ones who share the x-axis. When I try both at once, the smaller subplot has the same axis but smaller scaled, while I want the same scaling and a different axis, so sharing the axis might be a wrong idea.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
x=np.linspace(0,10,100)
y=np.sin(x)
x2=np.linspace(0,5,60)
y2=np.cos(x2)
fig=plt.figure()
gs=GridSpec(2,3)
ax1 = fig.add_subplot(gs[0, :])
ax1.plot(x,y)
ax2 = fig.add_subplot(gs[1,:-1])
#using sharex=ax1 here decreases the scaling of ax2 too much
ax2.plot(x2,y2)
plt.show()
I want the x.axes to have the same scaling, i.e. the same x values are always exactly on top of each other, this should give you an idea. The smaller plot's frame could be expanded or fit the plot, that doesn't matter. As it is now, the scales don't match.
Thanks in advance.
This is still a bit rough. I'm sure there's a slightly more elegant way to do this, but you can create a custom transformation (see Transformations Tutorial) between the Axes coordinates of ax2 and the data coordinates of ax1. In other word, your calculating what is the data-value (according to ax1) at the position corresponding to the left and right edges of ax2, and then adjust the xlim of ax2 accordingly.
Here is a demonstration showing that it works even if the second subplot is not aligned in any particular way with the first.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
x=np.linspace(0,25,100)
y=np.sin(x)
x2=np.linspace(10,30,60)
y2=np.cos(x2)
fig=plt.figure()
gs=GridSpec(2,6)
ax1 = fig.add_subplot(gs[0, :])
ax1.plot(x,y)
ax2 = fig.add_subplot(gs[1,3:-1])
ax2.plot(x2,y2)
# here is where the magic happens
trans = ax2.transAxes + ax1.transData.inverted()
((xmin,_),(xmax,_)) = trans.transform([[0,1],[1,1]])
ax2.set_xlim(xmin,xmax)
# for demonstration, show that the vertical lines end up aligned
for ax in [ax1,ax2]:
for pos in [15,20]:
ax.axvline(pos)
plt.show()
EDIT: One possible refinement would be to do the transform in the xlim_changed event callback. That way, the axes stay in sync even when zooming/panning in the first axes.
There is also a slight issue with tight_layout() as you noted, but that is easily fixed by calling the callback function directly.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
def on_xlim_changed(event):
# here is where the magic happens
trans = ax2.transAxes + ax1.transData.inverted()
((xmin, _), (xmax, _)) = trans.transform([[0, 1], [1, 1]])
ax2.set_xlim(xmin, xmax)
x = np.linspace(0, 25, 100)
y = np.sin(x)
x2 = np.linspace(10, 30, 60)
y2 = np.cos(x2)
fig = plt.figure()
gs = GridSpec(2, 6)
ax1 = fig.add_subplot(gs[0, :])
ax1.plot(x, y)
ax2 = fig.add_subplot(gs[1, 3:-1])
ax2.plot(x2, y2)
# for demonstration, show that the vertical lines end up aligned
for ax in [ax1, ax2]:
for pos in [15, 20]:
ax.axvline(pos)
# tight_layout() messes up the axes xlim
# but can be fixed by calling on_xlim_changed()
fig.tight_layout()
on_xlim_changed(None)
ax1.callbacks.connect('xlim_changed', on_xlim_changed)
plt.show()
I suggest setting limits of the second axis based on the limits of ax1.
Try this!
ax2 = fig.add_subplot(gs[1,:-1])
ax2.plot(x2,y2)
lb, ub = ax1.get_xlim()
# Default margin is 0.05, which would be used for auto-scaling, hence reduce that here
# Set lower bound and upper bound based on the grid size, which you choose for second plot
ax2.set_xlim(lb, ub *(2/3) -0.5)
plt.show()

Axes.invert_axis() does not work with sharey=True for matplotlib subplots

I am trying to make 4 subplots (2x2) with an inverted y axis while also sharing the y axis between subplots. Here is what I get:
import matplotlib.pyplot as plt
import numpy as np
fig,AX = plt.subplots(2, 2, sharex=True, sharey=True)
for ax in AX.flatten():
ax.invert_yaxis()
ax.plot(range(10), np.random.random(10))
It appears that ax.invert_axis() is being ignored when sharey=True. If I set sharey=False I get an inverted y axis in all subplots but obviously the y axis is no longer shared among subplots. Am I doing something wrong here, is this a bug, or does it not make sense to do something like this?
Since you set sharey=True, all three axes now behave as if their were one. For instance, when you invert one of them, you affect all four. The problem resides in that you are inverting the axes in a for loop which runs over an iterable of length four, you are thus inverting ALL axes for an even number of times... By inverting an already inverted ax, you simply restore its original orientation. Try with an odd number of subplots instead, and you will see that the axes are successfully inverted.
To solve your problem, you should invert the y-axis of one single subplot (and only once). Following code works for me:
import matplotlib.pyplot as plt
import numpy as np
fig,AX = plt.subplots(2, 2, sharex=True, sharey=True)
## access upper left subplot and invert it
AX[0,0].invert_yaxis()
for ax in AX.flatten():
ax.plot(range(10), np.random.random(10))
plt.show()

how to get matplotlib physical (not data) scale

I have this simple code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.patches import Ellipse
PlotFileName="test.pdf"
pdf = PdfPages(PlotFileName)
fig=plt.figure(1)
ax1=fig.add_subplot(111)
plt.xlim([0,10])
plt.ylim([0,10])
ax1.plot([0,10],[0,10])
e=0.0
theta=0
maj_ax=2
min_ax=maj_ax*np.sqrt(1-e**2)
const=1
ax1.add_artist(Ellipse((5, 5), maj_ax, const*min_ax, angle=theta, facecolor="green", edgecolor="black",zorder=2, alpha=0.5))
plt.grid()
pdf.savefig(fig)
pdf.close()
plt.close()
Here is how it looks:
As you see from the code, it should be a circle, but it isn't! I have narrowed the problem down to the const term in line 16. I don't want to use ax1.axis("equal") because my data don't have the same scales on the vertical and horizontal. Could any one tell me how I can ask matplotlib to tell me what aspect ratio it is using so I can set the const term correctly so I have a circle in the end?
In other words I want to know the ratio of the horizontal to the vertical axis "physical" length (for example, what is printed out).
I would really appreciate any suggestions, thanks in advance
One option is to explicitly define the figure size... you may also need to specify the subplot parameters if you are using non-default settings. Adjust figsize and subplot parameters as needed for non-equal horizontal and vertical scales. For example:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
fig = plt.figure(figsize=(6,4))
fig.subplots_adjust(left=0.1, right=0.9, bottom=0.1, top=0.9)
ax1 = fig.add_subplot(111, xlim=(-2.5,12.5), ylim=(0,10))
ax1.plot((0,10), (0,10))
maj_ax, e, theta = 2, 0, 0
min_ax = maj_ax * np.sqrt(1 - e**2)
ax1.add_artist(Ellipse((5, 5), maj_ax, min_ax, angle=theta,
fc="green", ec="black", zorder=2, alpha=0.5))
plt.grid()
plt.show()

Categories

Resources