Move axes relative to figure in matplotlib - python

I am trying to leave some whitespace on the left side of my figure in matplotlib and cannot figure out how to do this.
From the docs, I understand that using the add_axes() method on a figure, I can place an axes at an arbitrary location.
For example, the code below should create the axes on the right half of the figure:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6,4))
fig.add_axes([0.5, 0, 0.5, 1])
However, if you run this, the axes will appear on the left half of the figure instead. Is there something I am missing here?

You can use the add_axes method, to place the axes on an arbitrary position. The position list must contain the positions of the origin (measured from bottom left), and the height and width as follows:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6,4))
# [x0, y0, width, height]
fig.add_axes([0.3, 0.1, 0.6, 0.8])
All relative in the figure (values between zero and 1)
However, if you want to use the default margins, and maintain the position of the other axes, you can first obtaining the original position, update it, and then set it again as follows.
fig = plt.figure(figsize=(6,4))
ax = plt.axes()
pos = ax.get_position()
pos.x0 = 0.2 # for example 0.2, choose your value
ax.set_position(pos)
pos contains x0, x1, y0 and y1, which are the positions of the Bbox of the axes.

Related

How do you get the size of a matplotlib plot area in inches?

I am making figures for a paper, and I want to put a label some fixed distance from the top-left corner of the plot. ax.text(x, y, label, transform=ax.transAxes) nearly does this, but specifies the position as a fraction of the size of the plot. If I could get the absolute size of the plot area I could use that to convert. For example, in the following script how could I get the height and width?
Edit: I want the height and width just of the plot (not of the whole figure), and excluding the labels, ticks, etc.
from matplotlib import pyplot as plt
import numpy as np
data = np.random.rand(10,10)
fig, ax = plt.subplots()
ax.pcolormesh(data)
ax.set_aspect("equal")
# get width and height of plot here?
plt.show()
Your response above is good, but perhaps better:
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
fig, ax = plt.subplots()
trans = (fig.dpi_scale_trans +
mtransforms.ScaledTranslation(0, 1, ax.transAxes))
ax.set_aspect(1)
ax.text(0.1, -0.2, 'Boo', transform=trans)
plt.show()
You can read more at: https://matplotlib.org/tutorials/advanced/transforms_tutorial.html#plotting-in-physical-coordinates
I did manage to find a way to do this. It was made more complicated because I used set_aspect() - set_aspect() modifies the bounding box of ax, but by default doesn't apply the modification until ax is drawn (e.g. by plt.show()), which messes up attempts to get the bounding box size.
The solution is:
To avoid problems from set_aspect(), call apply_aspect() before trying to get the bounding box size. This makes the updated aspect ratio actually modify the bounding box size so we can find out what it is.
Get the size of the plot area with ax.get_window_extent() - this gets the size of just the plot area, excluding axis labels, ticks, etc.
The result of ax.get_window_extent() is in 'display units', which we can convert to inches using fig.dpi.
So adding the kind of label I wanted as an example:
from matplotlib import pyplot as plt
import numpy as np
data = np.random.rand(10,10)
fig, ax = plt.subplots()
ax.pcolormesh(data)
ax.set_aspect("equal")
ax.apply_aspect()
bbox = ax.get_window_extent()
# dpi used to convert from display units to inches
dpi = fig.dpi
height = bbox.height / dpi # in inches
width = bbox.width / dpi # in inches
x = 0.2 # in inches
y = 0.1 # in inches
ax.text(x / width, 1.0 - y / height, "(a)", verticalalignment="top", color="w", transform=ax.transAxes)
plt.show()
which gives:
Edit: I tested this solution with matplotlib-3.3.2.

How to prevent colorbar from moving up/down as heatmap height changes? Matplotlib/seaborn

I am generating a heatmap dynamically and the number of categories on the y and x axes may be different each time. How can I position the colorbar next to the heatmap so that it is always anchored at the very top (basically first row of the heatmap) regardless of the height of the figure?
Here's what's happening:
I have so far managed to set the colorbar height and width using add_axes so that these remain constant whatever the figure size. However I am struggling to set its y-axis position dynamically. Minimal example below:
import seaborn as sns
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable, axes_size
data = np.random.rand(5,5)
# Dynamic figure parameters.
topmargin = 0.1
bottommargin = 0.1
# Square height
cat_height = 0.4
# Number of y-axis points.
n=data.shape[0]
leftmargin = 0.1
rightmargin = 0.1
# Square width.
cat_width = 0.5
# Number of x-axis points.
m=data.shape[1]
# Dynamic figure height.
figheight = topmargin + bottommargin + (n+1)*cat_height
# Dynamic figure width.
figwidth = leftmargin + rightmargin + (m+1)*cat_width
fig, ax = plt.subplots(figsize=(figwidth, figheight))
# [x, y, width, height]
cbar_ax = fig.add_axes([0.93, 0.33, 0.13/m, 2.75/n])
# Plot the heatmap.
ax = sns.heatmap(data, ax=ax, cmap='coolwarm', cbar_ax=cbar_ax, cbar=True)
plt.show()
Basically colorbar is moving up/down when the figure height changes but I would like it anchored at the top of the figure every time.
You could simply calculate the bottom coordinates based on the height of your cbar and the top of the heatmap axes
cbar_ax = fig.add_axes([0.93, 0.88-2.75/n, 0.13/m, 2.75/n])
0.88 is the top of the top subplot with the default margins (see plt.rcParams['figure.subplot.top']).
However, for this kind of things, I would use a GridSpec to define a grid of axes with configurable size ratios (adjust the height_ratios to suit your needs):
gs = matplotlib.gridspec.GridSpec(2,2, height_ratios=[3,n-3], width_ratios=[20,1])
fig = plt.figure(figsize=(figwidth, figheight))
ax = fig.add_subplot(gs[:,0])
cbar_ax = fig.add_subplot(gs[0,1])

matplotlib polar plot tick/axis label position

I have been looking for a way to reliably position the tick and axis labels in a plot in polar coordinates. Please take a look at the following example:
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=[10, 5])
ax0 = fig.add_axes([0.05, 0.05, 0.4, 0.9], projection="polar")
ax1 = fig.add_axes([0.55, 0.05, 0.4, 0.9], projection="polar")
r0 = np.linspace(10, 12, 10)
theta0 = np.linspace(0, 0.1, 10)
ax0.quiver(theta0, r0, -0.1, 0.1)
ax1.quiver(theta0 + np.pi, r0, -0.1, 0.1)
ax0.set_thetamin(-2)
ax0.set_thetamax(10)
ax1.set_thetamin(178)
ax1.set_thetamax(190)
for ax in [ax0, ax1]:
# Labels
ax.set_xlabel("r")
ax.set_ylabel(r"$\theta$", labelpad=10)
# R range
ax.set_rorigin(0)
ax.set_rmin(9)
ax.set_rmax(13)
plt.show()
which results in this figure:
You can clearly see that
(a) the tick label position on the radial axis switches from bottom to top between the plots and the tick labels for theta switch from right to left.
(b) the axis label positions are fixed. I'd want the axis labels to also move with the tick labels. i.e. in the left plot, "theta" should be on the right, and in the right plot "r" should be on top.
How do I control the axis/tick labels in a way, so that they are positioned correctly? This even gets worse for e.g. a 90 degree shift, because then the theta axis is actually vertical and the tick labels are then totally off.
I think the most important bit is to become clear about how the usual notions of left, right, bottom, top translate into the polar axes in matplotlib.
The angular axis is the "x"-axis. The radial axis is the "y"-axis. The "bottom" is the outer ring. The "top" is the inner ring. "Left" is the radial axis at the start of the angular axis, "right" is the end of it.
This then allows to set the tick locations as usual, e.g.
ax.tick_params(labelleft=True, labelright=False,
labeltop=False, labelbottom=True)
for the case shown above.
The x and y labels (set_xlabel / set_ylabel) are not translated. Here left, right, top, bottom refer to the cartesian definition, just as with normal linear axes. This means that for certain positions, they cannot be used to label the axis, because they are just too far away. An alternative is to create a text at the desired position.
A complete example code:
import numpy as np
import matplotlib.pyplot as plt
fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10,5),
subplot_kw=dict(projection="polar"))
ax0.set(thetamin=180, thetamax=230)
ax1.set(thetamin= 0, thetamax= 50)
plt.setp([ax0, ax1], rorigin=0, rmin=5, rmax=10)
ax0.tick_params(labelleft=False, labelright=True,
labeltop=True, labelbottom=False)
trans, _ , _ = ax1.get_xaxis_text1_transform(-10)
ax1.text(np.deg2rad(22.5), -0.18, "Theta Label", transform=trans,
rotation=22.5-90, ha="center", va="center")
plt.show()
To answer question (b):
ax0.yaxis.set_label_position('right')
ax1.xaxis.set_label_position('top')
In addition, I modified the ax.set_ylabel(r"$\theta$", labelpad=15)

python matplotlib overlapping rectangles at certain size

I'm trying to plot different rectangles with matplotlib which should have a little gap in between them like in following example:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
Nmax = 200
xvalues = np.arange(Nmax)
fig = plt.figure()
ax = plt.subplot(111)
for xvalue in xvalues:
rect = Rectangle(
xy=(xvalue - 0.25, xvalue),
width = 0.5,
height = 1.5,
facecolor = 'r',
edgecolor = 'r',
)
ax.add_patch(rect)
ax.autoscale_view()
plt.show()
It's working as I would like to work for Nmax = 20 rectangles. Plot of Nmax=20 below:
As the rectangle positions are always 1 'unit' apart with a width of 0.5 there is always a spacing of 0.5 between two neighbouring rectangles.
However when I try it for example with 200 rectangles the rectangles get thicker and start overlapping. Upon zooming into the graph the rectangles are separated again. But saving the original figure as pdf still yields overlapping rectangles. Zoom of pdf with Nmax=200 below:
I don't know why this is happening, as I'm specifying still their widths to 0.5. I would be glad if someone could give me a hint on this.
I'm not sure but in a vector format it should be possible to determine the rectangle position exactly, so maybe saving it as svg and converting it to pdf would do the trick?
Final solution:
alright, thanks to zephyr the solution is to turn off the rectangle edge:
edgecolor = 'none',
Changing the edgecolor to 'none' in matplotlib.finance would also solve overlapping candlestick bars which seems to be the same problem here
Assuming you do want to use an edgecolor (that is, setting edgecolor='none' is not an option), you could produce a PDF which shows the space between boxes by increasing the figsize and dpi when creating the figure:
fig = plt.figure(figsize=(12,4), dpi=600)
If the figsize and dpi are big enough, the pdf-generating backend will display the whitespace between the rectangles:
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np
Nmax = 200
xvalues = np.arange(Nmax)
fig = plt.figure(figsize=(12,4), dpi=600)
ax = plt.subplot(111)
for xvalue in xvalues:
rect = Rectangle(
xy=(xvalue - 0.25, xvalue),
width = 0.5,
height = 1.5,
facecolor = 'r',
edgecolor = 'r',
)
ax.add_patch(rect)
ax.autoscale_view()
# plt.show()
plt.savefig('/tmp/test.pdf')
Detail:
Another option is to reduce the linewidth when creating the Rectangle:
Rectangle(..., edgecolor='b', linewidth=0.01)
Thanks to zephyr the solution is to turn off the rectangle edge:
edgecolor = 'none',

Add second axis to polar plot

I try to plot two polar plots in one figure. See code below:
fig = super(PlotWindPowerDensity, self).get_figure()
rect = [0.1, 0.1, 0.8, 0.8]
ax = WindSpeedDirectionAxes(fig, rect)
self.values_dict = collections.OrderedDict(sorted(self.values_dict.items()))
values = self.values_dict.items()
di, wpd = zip(*values)
wpd = np.array(wpd).astype(np.double)
wpdmask = np.isfinite(wpd)
theta = self.radar_factory(int(len(wpd)))
# spider plot
ax.plot(theta[wpdmask], wpd[wpdmask], color = 'b', alpha = 0.5)
ax.fill(theta[wpdmask], wpd[wpdmask], facecolor = 'b', alpha = 0.5)
# bar plot
ax.plot_bar(table=self.table, sectors=self.sectors, speedbins=self.wpdbins, option='wind_power_density', colorfn=get_sequential_colors)
fig.add_axes(ax)
return fig
The length of the bar is the data base (how many sampling points for this sector). The colors of the bars show the frequency of certain value bins (eg. 2.5-5 m/s) in the correspondent sector (blue: low, red: high). The blue spider plot shows the mean value for each sector.
In the shown figure, the values of each plot are similar, but this is rare. I need to assign the second plot to another axis and show this axis in another direction.
EDIT:
After the nice answer of Joe, i get the result of the figure.
That's almost everything i wanted to achieve. But there are some points i wasn't able to figure out.
The plot is made for dynamicly changing data bases. Therefore i need a dynamic way to get the same location of the circles. Till now I solve it with:
start, end = ax2.get_ylim()
ax2.yaxis.set_ticks(np.arange(0, end, end / len(ax.yaxis.get_ticklocs())))
means: for second axis i alter the ticks in order to fit the ticklocs to the one's of first axis.
In most cases i get some decimal places, but i don't want that, because it corrupts the clearness of the plot. Is there a way to solve this problem more smartly?
The ytics (the radial one's) range from 0 to the next-to-last circle. How can i achieve that the values range from the first circle to the very last (the border)? The same like for the first axis.
So, as I understand it, you want to display data with very different magnitudes on the same polar plot. Basically you're asking how to do something similar to twinx for polar axes.
As an example to illustrate the problem, it would be nice to display the green series on the plot below at a different scale than the blue series, while keeping them on the same polar axes for easy comparison.:
import numpy as np
import matplotlib.pyplot as plt
numpoints = 30
theta = np.linspace(0, 2*np.pi, numpoints)
r1 = np.random.random(numpoints)
r2 = 5 * np.random.random(numpoints)
params = dict(projection='polar', theta_direction=-1, theta_offset=np.pi/2)
fig, ax = plt.subplots(subplot_kw=params)
ax.fill_between(theta, r2, color='blue', alpha=0.5)
ax.fill_between(theta, r1, color='green', alpha=0.5)
plt.show()
However, ax.twinx() doesn't work for polar plots.
It is possible to work around this, but it's not very straight-forward. Here's an example:
import numpy as np
import matplotlib.pyplot as plt
def main():
numpoints = 30
theta = np.linspace(0, 2*np.pi, numpoints)
r1 = np.random.random(numpoints)
r2 = 5 * np.random.random(numpoints)
params = dict(projection='polar', theta_direction=-1, theta_offset=np.pi/2)
fig, ax = plt.subplots(subplot_kw=params)
ax2 = polar_twin(ax)
ax.fill_between(theta, r2, color='blue', alpha=0.5)
ax2.fill_between(theta, r1, color='green', alpha=0.5)
plt.show()
def polar_twin(ax):
ax2 = ax.figure.add_axes(ax.get_position(), projection='polar',
label='twin', frameon=False,
theta_direction=ax.get_theta_direction(),
theta_offset=ax.get_theta_offset())
ax2.xaxis.set_visible(False)
# There should be a method for this, but there isn't... Pull request?
ax2._r_label_position._t = (22.5 + 180, 0.0)
ax2._r_label_position.invalidate()
# Ensure that original axes tick labels are on top of plots in twinned axes
for label in ax.get_yticklabels():
ax.figure.texts.append(label)
return ax2
main()
That does what we want, but it looks fairly bad at first. One improvement would be to the tick labels to correspond to what we're plotting:
plt.setp(ax2.get_yticklabels(), color='darkgreen')
plt.setp(ax.get_yticklabels(), color='darkblue')
However, we still have the double-grids, which are rather confusing. One easy way around this is to manually set the r-limits (and/or r-ticks) such that the grids will fall on top of each other. Alternately, you could write a custom locator to do this automatically. Let's stick with the simple approach here:
ax.set_rlim([0, 5])
ax2.set_rlim([0, 1])
Caveat: Because shared axes don't work for polar plots, the implmentation I have above will have problems with anything that changes the position of the original axes. For example, adding a colorbar to the figure will cause all sorts of problems. It's possible to work around this, but I've left that part out. If you need it, let me know, and I'll add an example.
At any rate, here's the full, stand-alone code to generate the final figure:
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(1977)
def main():
numpoints = 30
theta = np.linspace(0, 2*np.pi, numpoints)
r1 = np.random.random(numpoints)
r2 = 5 * np.random.random(numpoints)
params = dict(projection='polar', theta_direction=-1, theta_offset=np.pi/2)
fig, ax = plt.subplots(subplot_kw=params)
ax2 = polar_twin(ax)
ax.fill_between(theta, r2, color='blue', alpha=0.5)
ax2.fill_between(theta, r1, color='green', alpha=0.5)
plt.setp(ax2.get_yticklabels(), color='darkgreen')
plt.setp(ax.get_yticklabels(), color='darkblue')
ax.set_ylim([0, 5])
ax2.set_ylim([0, 1])
plt.show()
def polar_twin(ax):
ax2 = ax.figure.add_axes(ax.get_position(), projection='polar',
label='twin', frameon=False,
theta_direction=ax.get_theta_direction(),
theta_offset=ax.get_theta_offset())
ax2.xaxis.set_visible(False)
# There should be a method for this, but there isn't... Pull request?
ax2._r_label_position._t = (22.5 + 180, 0.0)
ax2._r_label_position.invalidate()
# Bit of a hack to ensure that the original axes tick labels are on top of
# whatever is plotted in the twinned axes. Tick labels will be drawn twice.
for label in ax.get_yticklabels():
ax.figure.texts.append(label)
return ax2
if __name__ == '__main__':
main()
Just to add onto #JoeKington 's (great) answer, I found that the "hack to ensure that the original axes tick labels are on top of whatever is plotted in the twinned axes" didn't work for me so as an alternative I've used:
from matplotlib.ticker import MaxNLocator
#Match the tick point locations by setting the same number of ticks in the
# 2nd axis as the first
ax2.yaxis.set_major_locator(MaxNLocator(nbins=len(ax1.get_yticks())))
#Set the last tick as the plot limit
ax2.set_ylim(0, ax2.get_yticks()[-1])
#Remove the tick label at zero
ax2.yaxis.get_major_ticks()[0].label1.set_visible(False)

Categories