I am using matplotlib to draw vertical and horizontal grid lines over an image. The resulting image will have numbered squares over it. To do this, I used this code:
def drawGridOverImage(image):
"""Draw a numbered grid with 1cm2 boxes over an image."""
import matplotlib.pyplot as plt
import matplotlib.ticker as plticker
my_dpi=300.
# Set up figure
fig=plt.figure(figsize=(float(image.size[0])/my_dpi,float(image.size[1])/my_dpi),dpi=my_dpi)
ax=fig.add_subplot(111)
# Remove whitespace from around the image
fig.subplots_adjust(left=0,right=1,bottom=0,top=1)
# Set the gridding interval
# my_dpi is dots per inch, so each square is 1 inch2
myInterval=my_dpi
loc = plticker.MultipleLocator(base=myInterval)
ax.xaxis.set_major_locator(loc)
ax.yaxis.set_major_locator(loc)
# Add the grid
ax.grid(True, which='major', axis='both', linestyle='-')
# Add the image
ax.imshow(image)
# Find number of gridsquares in x and y direction
nx=abs(int(float(ax.get_xlim()[1]-ax.get_xlim()[0])/float(myInterval)))
ny=abs(int(float(ax.get_ylim()[1]-ax.get_ylim()[0])/float(myInterval)))
# Add some labels to the gridsquares
for j in range(ny):
y=myInterval/2+j*myInterval
for i in range(nx):
x=myInterval/2.+float(i)*myInterval
ax.text(x,y,'{:d}'.format(i+j*nx),color='r',ha='center',va='center')
# Save the figure
#fig.savefig('myImageGrid.tiff',dpi=my_dpi)
newImageName = nameImageWithGrid(image.filename)
fig.savefig(newImageName,dpi=my_dpi)
return fig
However, when I run this code, the vertical grid lines stop appearing at a certain point. I have attached a screenshot of part of this image (the whole image is very large) to demonstrate the problem.
After some searching, I did find this previous issue so I am not sure if this is the same issue. It is worth noting that I am working with very large images, the one in the picture above has dimensions of 11648 × 8736.
I would appreciate some help on resolving this problem and making vertical grid lines appear on whole image.
EDIT:
I tried the code on another large image that can be found in this link here and had the same problem, here is a screenshot of part of the image where the vertical grid lines stop:
I believe that the locator is the cause of the issues that are occurring now. The grid lines are drawn for the ticks created by the interval values. For example, if you change the tick marks on the x-axis like this, the grid lines are drawn correctly.
# Set the gridding interval
# my_dpi is dots per inch, so each square is 1 inch2
myInterval=my_dpi
# loc = plticker.MultipleLocator(base=myInterval)
# ax.xaxis.set_major_locator(loc)
# ax.yaxis.set_major_locator(loc)
ax.set_xticks(np.arange(0,9900,myInterval))
Related
I was trying to put a summary of the data under its figure. However, since the length of the summary text varies, it is hard to keep the textbox and the figure vertically aligned (more specifically, the textbox and the figure should have the same width). Is there any way to do this?
You can try to do a subplot right below the figure. This guarantees that the width will be the same:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(1, 10, 50)
y = np.sin(x)
plt.subplot(2,1,1)
plt.plot(x,y)
plt.subplot(2,1,2)
description = '''
Sin Function:
In the figure avobe we see how the sin function oscillates and behaves
between 0 and 10
'''
plt.text(0.5, 0.5, description, va='top', ha='center')
plt.axis('off')
plt.show()
However, I am afraid you'll have to insert the line breaks yourself as matplotlib doesn't support text wrapping. Here there's something you can try though.
Yes. You can put a long summary of the plot under the plot. Technically the caption doesn't go into the axes object, it goes into the figure object that the axes was created in, but the reader can't tell that.
Here I estimate the length of a long caption and then add the needed vertical space to the figure in which the axes object plots. With some rough calculations of text size, this makes room for matplotlib's text wrapping to fit very long captions below a figure:
import matplotlib.pyplot as plt
t = "A point is that which has no part. A line is breadthless length. The ends of a line are points. A straight line is a line which lies evenly with the points on itself. A surface is that which has length and breadth only. The edges of a surface are lines. A plane surface is a surface which lies evenly with the straight lines on itself. A plane angle is the inclination to one another of two lines in a plane which meet one another and do not lie in a straight line. "
#t = 3*t
AestheticFigWidth = 6.4
AestheticPlotHeight = 2.4
# 12pt fonts should be 6 lines per vertical inch.
# Very rough estimate of 10 12pt characters per horizontal inch -- it varies!
# Calculate how many more inches of fig you need for your caption,
# add extra for whitespace and labels:
CaptionHeight = (len(t)/(6 * (AestheticFigWidth * 10))) + 0.5
fig = plt.figure(figsize=(AestheticFigWidth,
AestheticPlotHeight + CaptionHeight))
CaptionProportion = CaptionHeight / (AestheticPlotHeight + CaptionHeight)
ax = fig.add_axes((.1, #location proportional to figure
CaptionProportion + .03,
.85, # size proportional to figure
.85 - CaptionProportion))
fig.suptitle("Make space for a big caption")
ax.plot([1,2,3,4,5], [0,5,1,8,0], 'o-')
ax.set_ylabel('Angle')
ax.set_xlabel("Let's not overlap this")
fig.text(.05,.03, t, ha='left', rotation=0, wrap=True, fontsize=12)
plt.show()
With a very long caption:
With a middling caption:
If you need something more elegant or automatic, I recommend generating the plots in matplotlib and then generating a template in LaTeX to pick up the plots and their captions.
I would like to have a smaller spacing in the horizontal to be able to increase the size of the supplots, but I don't know how to do that
Here is my code:
# DEFINE FIGURE SIZE
fig, axarr = plt.subplots(6,3,figsize=(30,30))
for i in range(6):
for ii in range(3):
#IMAGE
axarr[i,ii].imshow([[1,2],[2,3]])
# ADDITIONAL
axarr[i,ii].axis("off")
axarr[i,ii].set_title(str(i), color = 'red')
plt.show()
This is how it looks:
I tried to change the figuresize to 150x150, but the result is almost the same (the titles are not visible anymore):
Change the figure dimensions to have a 6/3 ratio. More generally, if you have Nlines x Ncols subplots, you will want to use figure dimentions with a Nlines/Ncols ratio.
This is because imshow() uses an 'equal' aspect ratio by default to ensure that pixels end up square on the screen. That means that the subplots, which were originally created with a rectangular shape with little horizontal spacing, are being resized to have a square shape without changing the dimensions of the figure, leading to large gaps between the subplots. To avoid this problem, choose the size of the figure carefully so that the subplots are already square before the call to imshow().
width = 5
fig, axarr = plt.subplots(6,3,figsize=(width,width*6/3))
for i in range(6):
for ii in range(3):
#IMAGE
axarr[i,ii].imshow([[1,2],[2,3]])
# ADDITIONAL
axarr[i,ii].axis("off")
axarr[i,ii].set_title(str(i), color = 'red')
plt.show()
I'm trying to create imshow subplots with the same pixel size without having the figure height automatically scaled, but I haven't been able to figure out how.
Ideally, I'm looking for a plot similar to the second picture, without the extra white space (ylim going from -0.5 to 4.5) and maybe centered vertically. My pictures will always have the same width, so maybe if I could fix the subplot width instead of the height that would help. Does anyone have any ideas?
close('all')
f,ax=subplots(1,2)
ax[0].imshow(random.rand(30,4),interpolation='nearest')
ax[1].imshow(random.rand(4,4),interpolation='nearest')
tight_layout()
f,ax=subplots(1,2)
ax[0].imshow(random.rand(30,4),interpolation='nearest')
ax[1].imshow(random.rand(4,4),interpolation='nearest')
ax[1].set_ylim((29.5,-0.5))
tight_layout()
Plot without ylim adjustment:
Plot with ylim adjustment:
In principle you can just make the figure size small enough in width, such that it constrains the widths of the subplots. E.g. figsize=(2,7) would work here.
For an automated solution, you may adjust the subplot parameters, such that the left and right margin constrain the subplot width. This is shown in the code below.
It assumes that there is one row of subplots, and that all images have the same pixel number in horizontal direction.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(1,2)
im1 = ax[0].imshow(np.random.rand(30,4))
im2 = ax[1].imshow(np.random.rand(4,4))
def adjustw(images, wspace="auto"):
fig = images[0].axes.figure
if wspace=="auto":
wspace = fig.subplotpars.wspace
top = fig.subplotpars.top
bottom = fig.subplotpars.bottom
shapes = np.array([im.get_array().shape for im in images])
w,h = fig.get_size_inches()
imw = (top-bottom)*h/shapes[:,0].max()*shapes[0,1] #inch
n = len(shapes)
left = -((n+(n-1)*wspace)*imw/w - 1)/2.
right = 1.-left
fig.subplots_adjust(left=left, right=right, wspace=wspace)
adjustw([im1, im2], wspace=1)
plt.show()
If you need to use tight_layout(), do so before calling the function. Also you would then definitely need to set the only free parameter here, wspace to something other than "auto". wspace=1 means to have as much space between the plots as their width.
The result is a figure where the subplots have the same size in width.
I'm writing a pythonic script for a coastal engineering application which should output, amongst other things, a figure with two subplots.
The problem is that I would like to shade a section of both subplots using plt.axvspan() but for some reason it only shades one of them.
Please find below an excerpt of the section of the code where I set up the plots as well as the figure that it's currently outputting (link after code).
Thanks for your help, and sorry if this is a rookie question (but it just happens that I am indeed a rookie in Python... and programming in general) but I couldn't find an answer for this anywhere else.
Feel free to add any comments to the code.
# PLOTTING
# now we generate a figure with the bathymetry vs required m50 and another figure with bathy vs Hs
#1. Generate plots
fig = plt.figure() # Generate Figure
ax = fig.add_subplot(211) # add the first plot to the figure.
depth = ax.plot(results[:,0],results[:,1]*-1,label="Depth [mDMD]") #plot the first set of data onto the first set of axis.
ax2 = ax.twinx() # generate a secondary vertical axis with the same horizontal axis as the first
m50 = ax2.plot(results[:,0],results[:,6],"r",label="M50 [kg]") # plot the second set of data onto the second vertical axis
ax3 = fig.add_subplot(212) # generate the second subplot
hs = ax3.plot(results[:,0],results[:,2],"g",label="Hs(m)")
#Now we want to find where breaking starts to occur so we shade it on the plot.
xBreakingDistance = results[numpy.argmax(breakingIndex),0]
# and now we plot a box from the origin to the depth of breaking.
plt.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) # this box is called a span in matplotlib (also works for axhspan)
# and then we write BREAKING ZONE in the box we just created
yLimits = ax.get_ylim() # first we get the range of y being plotted
yMiddle = (float(yLimits[1])-float(yLimits[0])) / 2 + yLimits[0] # then we calculate the middle value in y (to center the text)
xMiddle = xBreakingDistance / 2 # and then the middle value in x (to center the text)
#now we write BREAKING ZONE in the center of the box.
ax.text(xMiddle,yMiddle,"BREAKING ZONE",fontweight="bold",rotation=90,verticalalignment="center",horizontalalignment="center")
#FIGURE FORMATTING
ax.set_xlabel("Distance [m]") # define x label
ax.set_ylabel("Depth [mDMD]") # define y label on the first vertical axis (ax)
ax2.set_ylabel("M50 [kg]") # define y label on the second vertical axis (ax2)
ax.grid() # show grid
ax3.set_xlabel("Distance[m]") #define x label
ax3.set_ylabel("Hs[m]") # define y label
ax3.grid()
plt.tight_layout() # minimize subplot labels overlapping
# generating a label on a plot with 2 vertical axis is not very intuitive. Normally we would just write ax.label(loc=0)
combined_plots = depth+m50 #first we need to combine the plots in a vector
combined_labels = [i.get_label() for i in combined_plots] # and then we combine the labels
ax.legend(combined_plots,combined_labels,loc=0) # and finally we plot the combined_labels of the combined_plots
plt.savefig("Required M50(kg) along the trench.png",dpi=1000)
plt.close(fig)
Output Figure:
By just calling plt.axvspan, you are telling matplotlib to create the axvspan on the currently active axes (i.e. in this case, the last one you created, ax3)
You need to plot the axvspan on both of the axes you would like for it to appear on. In this case, ax and ax3.
So, you could do:
ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
ax3.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
or in one line:
[this_ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) for this_ax in [ax,ax3]]
It's difficult to analyze your code and not being able to reproduce it. I advise you to build a minimal example. In any case notice that you are calling "plt.axvspan(" which is general call to the library.
You need to specifically state that you want this in both "ax" and "ax2" (i think).
Also if you need more control consider using Patches (I don't know axvspan):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig1 = plt.figure()
ax1 = fig1.add_subplot(111, aspect='equal')
ax1.add_patch(
patches.Rectangle(
(0.1, 0.1), # (x,y)
0.5, # width
0.5, # height
)
)
fig1.savefig('rect1.png', dpi=90, bbox_inches='tight')
See that call to "ax1" in the example? Just make something similar to yours. Or just add axvspan to each of your plots.
I have to plot some data and some vertical lines to delimit interesting intervals and then I would like to add some labels to that using text. I can not entirely avoid the labels overlapping with the data or the vertical lines, so I decided to put a bbox around the text to keep it readable. My problem is that I am not able to align it centrally within this box and this is clearly visible and quite annoying in my opinion.
I'm doing something like this:
import numpy
import matplotlib
import matplotlib.pyplot as plt
fig=plt.figure()
plot=fig.add_subplot(111)
x=numpy.linspace(1,10,50)
y=numpy.random.random(50)
plot.plot(x,y)
plot.text(4.5,.5,'TEST TEST',\
bbox={'facecolor':'white','alpha':1,'edgecolor':'none','pad':1})
plot.axvline(5,color='k',linestyle='solid')
plt.show()
Which creates the following plot:
It is quite apparent, that the text is not centered in its bbox. How can I change this? I've spent quite some time on Google but I could not find anything.
EDIT:
Thanks for the suggestions so far.
This suggests that what I see is actually desired behavior. Apparently the bbox in new versions of matplotlib is chosen taking into account the possible maximum descent of the text it contains (the descent of 'g').
When a 'g' appears in the text, this does indeed look good:
Unfortunately in my case there is no 'g' or anything with a similar descent. Does anyone have any further ideas?
Use the text properties ha and va:
plot.text(5.5,.5,'TEST TEST TEST TEST',
bbox={'facecolor':'white','alpha':1,'edgecolor':'none','pad':1},
ha='center', va='center')
To check, draw lines in the center of your plot:
plot.axvline(5.5,color='k',linestyle='solid')
plot.axhline(0.5,color='k',linestyle='solid')
It seems that there are now options to properly position the text in the coordinate system (in particular the new va = 'baseline'). However, as pointed out by user35915, this does not change the alignment of the box relative to the text. The misalignment is particularly obvious in single digit numbers, in particular number '1' (see also this bug). Until this is fixed, my workaround is to place the rectangle by hand, not via the bbox parameter:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define the rectangle size and the offset correction
rect_w = 0.2
rect_h = 0.2
rect_x_offset = 0.004
rect_y_offset =0.006
# text coordinates and content
x_text = 0.5
y_text = 0.5
text = '1'
# create the canvas
fig,ax = plt.subplots(figsize=(1,1),dpi=120)
ax.set_xlim((0,1))
ax.set_ylim((0,1))
# place the text
ax.text(x_text, y_text, text, ha="center", va="center", zorder=10)
# compare: vertical alignment with bbox-command: box is too low.
ax.text(x_text+0.3, y_text, text, ha="center", va="center",
bbox=dict(facecolor='wheat',boxstyle='square',edgecolor='black',pad=0.1), zorder=10)
# compare: horizontal alignment with bbox-command: box is too much to the left.
ax.text(x_text, y_text+0.3, text, ha="center", va="center",
bbox=dict(facecolor='wheat',boxstyle='square',edgecolor='black',pad=0.2), zorder=10)
# create the rectangle (below the text, hence the smaller zorder)
rect = patches.Rectangle((x_text-rect_w/2+rect_x_offset, y_text-rect_h/2+rect_y_offset),
rect_w,rect_h,linewidth=1,edgecolor='black',facecolor='white',zorder=9)
# add rectangle to plot
ax.add_patch(rect)
# show figure
fig.show()