Related
I am trying to display a count plot using seaborn, but the width of the bars is very high and the plot doesn't look nice. To counter it I change the width of the plot using the following code snippet:
sns.set()
fig,ax = plt.subplots(figsize=(10,4))
sns.countplot(x=imdb_data["label"],ax=ax)
for patch in ax.patches:
height = p.get_height()
width = patch.get_width
p.set_height(height*0.8)
patch.set_width(width*0.4)
x = p.get_x()
ax.text(x = x+new_width/2.,y= new_height+4,s = height,ha="center")
ax.legend(labels=("Negative","Positive"),loc='lower right')
plt.show()
But upon doing so the x-tick labels get shifted and the plot looks something like as shown in the attached image.
How should I change the width that, the x-tick location also, change automatically as per the new width of the bar ? . Also the legend is not being displayed properly. I used the below snippet to add the legend:
plt.legend(labels=['Positive','Negative'],loc='lower right')
Please help me out.
To keep the bar centered, you also need to change the x position with half the difference of the old and new width. Changing the height doesn't seem to be a good idea, as then the labels on the y-axis get mismatched. If the main reason to change the height is to make space for the text, it would be easier to change the y limits, e.g. via ax.margins(). Aligning the text vertically with 'bottom' allows leaving out the arbitrary offset for the y position.
The labels for the legend can be set via looping through the patches and setting the labels one by one. As the x-axis already has different positions for each bar, it might be better to leave out the legend and change the x tick labels?
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
sns.set()
imdb_data = pd.DataFrame({"label": np.random.randint(0, 2, 7500)})
fig, ax = plt.subplots(figsize=(10, 4))
sns.countplot(x=imdb_data["label"], ax=ax)
for patch, label in zip(ax.patches, ["Negative", "Positive"]):
height = patch.get_height()
width = patch.get_width()
new_width = width * 0.4
patch.set_width(new_width)
patch.set_label(label)
x = patch.get_x()
patch.set_x(x + (width - new_width) / 2)
ax.text(x=x + width/2, y=height, s=height, ha='center', va='bottom')
ax.legend(loc='lower right')
ax.margins(y=0.1)
plt.tight_layout()
plt.show()
PS: To change the x tick labels, so they can be used instead of the legend, add
ax.set_xticklabels(['negative', 'positive'])
and leave out the ax.legend() and patch.set_label(label) lines.
I want to to create a figure using matplotlib where I can explicitly specify the size of the axes, i.e. I want to set the width and height of the axes bbox.
I have looked around all over and I cannot find a solution for this. What I typically find is how to adjust the size of the complete Figure (including ticks and labels), for example using fig, ax = plt.subplots(figsize=(w, h))
This is very important for me as I want to have a 1:1 scale of the axes, i.e. 1 unit in paper is equal to 1 unit in reality. For example, if xrange is 0 to 10 with major tick = 1 and x axis is 10cm, then 1 major tick = 1cm. I will save this figure as pdf to import it to a latex document.
This question brought up a similar topic but the answer does not solve my problem (using plt.gca().set_aspect('equal', adjustable='box') code)
From this other question I see that it is possible to get the axes size, but not how to modify them explicitly.
Any ideas how I can set the axes box size and not just the figure size. The figure size should adapt to the axes size.
Thanks!
For those familiar with pgfplots in latex, it will like to have something similar to the scale only axis option (see here for example).
The axes size is determined by the figure size and the figure spacings, which can be set using figure.subplots_adjust(). In reverse this means that you can set the axes size by setting the figure size taking into acount the figure spacings:
import matplotlib.pyplot as plt
def set_size(w,h, ax=None):
""" w, h: width, height in inches """
if not ax: ax=plt.gca()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
figw = float(w)/(r-l)
figh = float(h)/(t-b)
ax.figure.set_size_inches(figw, figh)
fig, ax=plt.subplots()
ax.plot([1,3,2])
set_size(5,5)
plt.show()
It appears that Matplotlib has helper classes that allow you to define axes with a fixed size Demo fixed size axes
I have found that ImportanceofBeingErnests answer which modifies that figure size to adjust the axes size provides inconsistent results with the paticular matplotlib settings I use to produce publication ready plots. Slight errors were present in the final figure size, and I was unable to find a way to solve the issue with his approach. For most use cases I think this is not a problem, however the errors were noticeable when combining multiple pdf's for publication.
In lieu of developing a minimum working example to find the real issue I am having with the figure resizing approach I instead found a work around which uses the fixed axes size utilising the divider class.
from mpl_toolkits.axes_grid1 import Divider, Size
def fix_axes_size_incm(axew, axeh):
axew = axew/2.54
axeh = axeh/2.54
#lets use the tight layout function to get a good padding size for our axes labels.
fig = plt.gcf()
ax = plt.gca()
fig.tight_layout()
#obtain the current ratio values for padding and fix size
oldw, oldh = fig.get_size_inches()
l = ax.figure.subplotpars.left
r = ax.figure.subplotpars.right
t = ax.figure.subplotpars.top
b = ax.figure.subplotpars.bottom
#work out what the new ratio values for padding are, and the new fig size.
neww = axew+oldw*(1-r+l)
newh = axeh+oldh*(1-t+b)
newr = r*oldw/neww
newl = l*oldw/neww
newt = t*oldh/newh
newb = b*oldh/newh
#right(top) padding, fixed axes size, left(bottom) pading
hori = [Size.Scaled(newr), Size.Fixed(axew), Size.Scaled(newl)]
vert = [Size.Scaled(newt), Size.Fixed(axeh), Size.Scaled(newb)]
divider = Divider(fig, (0.0, 0.0, 1., 1.), hori, vert, aspect=False)
# the width and height of the rectangle is ignored.
ax.set_axes_locator(divider.new_locator(nx=1, ny=1))
#we need to resize the figure now, as we have may have made our axes bigger than in.
fig.set_size_inches(neww,newh)
Things worth noting:
Once you call set_axes_locator() on an axis instance you break the tight_layout() function.
The original figure size you choose will be irrelevent, and the final figure size is determined by the axes size you choose and the size of the labels/tick labels/outward ticks.
This approach doesn't work with colour scale bars.
This is my first ever stack overflow post.
another method using fig.add_axes was quite accurate. I have included 1 cm grid aswell
import matplotlib.pyplot as plt
import matplotlib as mpl
# This example fits a4 paper with 5mm margin printers
# figure settings
figure_width = 28.7 # cm
figure_height = 20 # cm
left_right_magrin = 1 # cm
top_bottom_margin = 1 # cm
# Don't change
left = left_right_magrin / figure_width # Percentage from height
bottom = top_bottom_margin / figure_height # Percentage from height
width = 1 - left*2
height = 1 - bottom*2
cm2inch = 1/2.54 # inch per cm
# specifying the width and the height of the box in inches
fig = plt.figure(figsize=(figure_width*cm2inch,figure_height*cm2inch))
ax = fig.add_axes((left, bottom, width, height))
# limits settings (important)
plt.xlim(0, figure_width * width)
plt.ylim(0, figure_height * height)
# Ticks settings
ax.xaxis.set_major_locator(mpl.ticker.MultipleLocator(5))
ax.xaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(mpl.ticker.MultipleLocator(5))
ax.yaxis.set_minor_locator(mpl.ticker.MultipleLocator(1))
# Grid settings
ax.grid(color="gray", which="both", linestyle=':', linewidth=0.5)
# your Plot (consider above limits)
ax.plot([1,2,3,5,6,7,8,9,10,12,13,14,15,17])
# save figure ( printing png file had better resolution, pdf was lighter and better on screen)
plt.show()
fig.savefig('A4_grid_cm.png', dpi=1000)
fig.savefig('tA4_grid_cm.pdf')
result:
Here's one example of Basemap:
fig = plt.figure(figsize=(10,6))
ax = fig.add_subplot(121)
ax.set_title('Default')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(122)
ax.set_title('Add offset')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],xoffset=100,yoffset=100)
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],xoffset=100,yoffset=100)
I want to add more space between the xlabel/ylabel and axis.
But, the space is smaller when xoffset and yoffset are added.
basemap is not being actively developed anymore, but maintenance still continues for some time. This means that things that break because of changes in other packages will still be fixed, but no new features will be added. Anyway, the fixing part may take some time and I'm guessing that the xoffset feature for the parallels and meridians is suffering from that. However, looking at the basemap documentation, the functionality of xoffset and yoffset are described as
xoffset: label offset from edge of map in x-direction (default is 0.01
times width of map in map projection coordinates).
yoffset: label
offset from edge of map in y-direction (default is 0.01 times height
of map in map projection coordinates).
This is easy enough to emulate by manipulating the Text objects that drawparallels() and drawmeridians() produce. The results of these functions are stored in a dict that contains a tuple of lists for each plotted parallel/meridian, the second of which contains the text labels. A Text object has a get_position() and a set_position() method, which, in combination with the axes limits and the definition above, can be used to compute the offset:
from matplotlib import pyplot as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
fig = plt.figure(figsize=(10,6))
ax = fig.add_subplot(121)
ax.set_title('Default')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(122)
ax.set_title('Add offset')
# miller projection
map = Basemap(projection='mill',lon_0=180)
# plot coastlines, draw label meridians and parallels.
map.drawcoastlines()
##getting axes dimensions
x0,x1 = ax.get_xlim()
w = x1-x0
y0,y1 = ax.get_ylim()
h = y1-y0
xoffset = 0.05
yoffset = 0.05
result = map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
##
for key, (lines,texts) in result.items():
for text in texts:
x,y = text.get_position()
text.set_position((x0-xoffset*w,y))
result = map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
for key, (lines,texts) in result.items():
for text in texts:
x,y = text.get_position()
text.set_position((x,y0-yoffset*h))
plt.show()
The resulting plot looks like this:
I think you are not using the correct unit. #Thomas Kühn quoted the basemap doc:
xoffset: label offset from edge of map in x-direction (default is 0.01
times width of map in map projection coordinates).
yoffset: label offset from edge of map in y-direction (default is 0.01
times height of map in map projection coordinates).
Note that it is defaulted to 1% of the domain span measured in map projection coordinates.
If you check the span in y-axis of the mill projection you used, the length has 8 digits, so no wonder yoffset=100 gives no visual offset.
So an easier way is to modify the offset using the actual domain span, like:
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.01*abs(map.ymax-map.ymin))
This gives the same offset as default, i.e. 1% of the domain span (See figure (b) below). Changing 0.01 to 0.03 will be 3x of that (figure (c)).
If you instead use cyl projection which uses degree of latitude/longitude as units, the offsets are also measured in degrees, then yoffset=100 will be an insane offset. Figure (f) uses a yoffset=30, note that is the same distance as the distance from 60S to 90S.
The script to generate the figure:
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
fig = plt.figure(figsize=(12,6))
def drawMap(proj):
map = Basemap(projection=proj,lon_0=180)
map.drawcoastlines()
return map
# miller projection
ax = fig.add_subplot(231)
ax.set_title('(a) Mill, Default')
map=drawMap('mill')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(232)
ax.set_title('(b) Mill, add same offset as default')
map=drawMap('mill')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.01*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.01*abs(map.ymax-map.ymin))
ax = fig.add_subplot(233)
ax.set_title('(c) Mill, add 3x offset as default')
map=drawMap('mill')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.03*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.03*abs(map.ymax-map.ymin))
ax = fig.add_subplot(234)
ax.set_title('(d) Cyl, Default')
map=drawMap('cyl')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0])
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1])
ax = fig.add_subplot(235)
ax.set_title('(e) Cyl, add same offset as default')
map=drawMap('cyl')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.01*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=0.01*abs(map.ymax-map.ymin))
ax = fig.add_subplot(236)
ax.set_title('(f) Cyl, add 30 degree offset')
map=drawMap('cyl')
map.drawparallels(np.arange(-90,90,30),labels=[1,0,0,0],
xoffset=0.03*abs(map.xmax-map.xmin))
map.drawmeridians(np.arange(map.lonmin,map.lonmax+30,60),labels=[0,0,0,1],
yoffset=30)
fig.subplots_adjust(hspace=0.01)
fig.show()
I have axes with each axis limited:
ul_lat, ul_long = (45.499426, 9.119963)
br_lat, br_long = (45.434210, 9.235803)
ax = fig.add_axes([0,0,1,1])
ax.set_xlim(ul_long,br_long)
ax.set_ylim(br_lat,ul_lat)
Then I try to put a satelile photo as background and set up grid:
ax.imshow(image,interpolation='none')
plt.grid()
As a result I can see no image, only grid.
Now, If I remove limits:
#ax.set_xlim(ul_long,br_long)
#ax.set_ylim(br_lat,ul_lat)
I can see the image, though the figure scale is wrong and grid is plotted within this wrong scale:
See this thin grey line in the upper part of the picture - it is a wrong-scaled grid. The size of figure equal to size of picture what I don't want to (903x708). I want to use correct latitude-longitude axes same I tried to set up with xlim/ylim.
What should I do to fix?
With imshow, you can specify the extent of your image to match your coordinates:
ul_lat, ul_long = (45.499426, 9.119963)
br_lat, br_long = (45.434210, 9.235803)
ax = fig.add_axes([0,0,1,1])
ax.set_xlim(ul_long, br_long)
ax.set_ylim(br_lat, ul_lat)
ax.imshow(image, interpolation='none', extent=[ul_long, br_long, br_lat, ul_lat])
plt.grid()
I'm dynamically generating a horizontal bar plot using MatPlotLib. It works pretty well most of the time, until people try to plot a very large numbers of data points. MatPlotLib tries to squish all of the bars into the plot and they start to disappear.
The ideal solution would be to generate the plot so that every horizontal bar is one pixel in height, with 1px separating every bar. The total height of the resulting plot image would then be dependent on the number of bars. But as everything in MatPlotLib is relative, I'm getting really stuck in how to do this. Any help would be much appreciated!
One option is to generate an image with the bars as pixels.
import matplotlib.pyplot as plt
import numpy as np
dpi = 100
N = 100 # numbner of bars (approx. half the number of pixels)
w = 200 #width of plot in pixels
sp = 3 # spacing within axes in pixels
bp = 50; lp = 70 # bottom, left pixel spacing
bottom=float(bp)/(2*N+2*sp+2*bp)
top = 1.-bottom
left=float(lp)/(w+2*lp)
right=1.-left
figheight = (2*N+2*sp)/float(dpi)/(1-(1-top)-bottom) #inch
figwidth = w/float(dpi)/(1-(1-right)-left)
# this is the input array to plot
inp = np.random.rand(N)+0.16
ar = np.zeros((2*N+2*sp,w))
ninp = np.round(inp/float(inp.max())*w).astype(np.int)
for n in range(N):
ar[2*n+sp, 0: ninp[n]] = np.ones(ninp[n])
fig, ax=plt.subplots(figsize=(figwidth, figheight), dpi=dpi)
plt.subplots_adjust(left=left, bottom=bottom, right=right, top=top)
plt.setp(ax.spines.values(), linewidth=0.5)
ext = [0,inp.max(), N-0.5+(sp+0.5)/2., -(sp+0.5)/2.]
ax.imshow(ar, extent=ext, interpolation="none", cmap="gray_r", origin="upper", aspect="auto")
ax.set_xlim((0,inp.max()*1.1))
ax.set_ylabel("item")
ax.set_xlabel("length")
plt.savefig(__file__+".png", dpi=dpi)
plt.show()
This will work for any setting of dpi.
Note that the ticklabels might appear a bit off, which is an inaccuracy from matplotlib; which I don't know how to overcome.
This example shows how you can plot lines with 1 pixel width:
yinch = 2
fig, ax = plt.subplots(figsize=(3,yinch), facecolor='w')
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
ypixels = int(yinch*fig.get_dpi())
for i in range(ypixels):
if i % 2 == 0:
c = 'k'
else:
c = 'w'
ax.plot([0,np.random.rand()], [i,i], color=c, linewidth=72./fig.get_dpi())
ax.set_ylim(0,ypixels)
ax.axis('off')
This is what the result looks like (magnified 200%):
edit:
Using a different dpi is not problem, but then using plot() becomes less useful because you cant specify the linewidth units. You can calculate the needed linewidth, but i think using barh() is more clear in that scenario.
In the example above i simply disabled the axis to focus on the 1px bars, if you remove that you can plot as normal. Spacing around it is not a problem because Matplotlib isn't bound to the 0-1 range for a Figure, but you want to add bbox_inches='tight' to your savefig to include artists outside of the normal 0-1 range. If you spend a lot of time 'precise' plotting within you axes, i think its easier to stretch the axis to span the entire figure size. You of course take a different approach but that would require you to also calculate the axes size in inches. Both angles would work, it depends or your precise case which might be more convenient.
Also be aware that old versions of Matplotlib (<2.0?) have a different default figure.dpi and savefig.dpi. You can avoid this by adding dpi=fig.get_dpi() to your savefig statement. One of many reasons to upgrade. ;)
yinch = 2
dpi = 128
fig, ax = plt.subplots(figsize=(3,yinch), facecolor='w', dpi=dpi)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
ypixels = int(yinch*fig.get_dpi())
for i in range(ypixels):
if i % 2 == 0:
c = '#aa0000'
else:
c = 'w'
ax.barh(i,np.random.rand(), height=1, color=c)
ax.set_title('DPI %i' % dpi)
ax.set_ylim(0,ypixels)
fig.savefig('mypic.png', bbox_inches='tight')