Standard Plot size in Python-matplotlib - matplotlib

I am generating multiple plots using matplotlib.patches.rect depending on the requirements. Some cases 2 rectangles are plotted sometimes 4 rectangles. But the visualisation size differs depending on the numbers of such rectangles although the dimensions of rectangles remains the same. Here in my case every rectangle has fixed shape (1200X230).
Below is the entire working code:
sampledata = {'Layer':[1,2,3,4,5,6], 'Type':[1,1,2,2,2,2]}
ip0 = pd.DataFrame(sampledata, columns=['Layer','Type'])
for i in range(ip0['Type'].nunique()):
fig = plt.figure()
ax = fig.add_subplot(111)
ip = ip0[ip0['Type']== i+1]
b = i+1
ax.grid(linestyle='--',linewidth = '0.3', color = 'black')
for i in range(ip['Layer'].nunique()):
y_pos = (i*300)
r = matplotlib.patches.Rectangle(xy=(0, y_pos), width=1201,height=233,
facecolor = None, edgecolor = 'red', linewidth=1.2, fill = False)
ax.text(-230, y_pos+175, 'Layer-{}'.format(i),
color='g',rotation='vertical', fontsize= (36/ip['Layer'].nunique()))
ax.add_patch(r)
plt.xlim([0, 1500])
plt.ylim([0, (ip['Layer'].nunique()*300)])
plt.savefig(f'image_bin_{b}.jpeg',bbox_inches='tight', dpi =
1600,transparent=True)
I have attached pictures of 2 cases one where there 2 rectangles and one 4. Please help me making them look similar since the actual dimensions are equal.

Related

Some matplotlib colorbars disappear when colorbar axes are moved

I am using the following lines of python code to create a figure with multiple subplots in a Jupiter notebook and attempting to add colorbars to some of the plots. The following lines are 1 of 7 sections copied and pasted with adjustments to GridSpec, variables, labels and axes handles made for each:
fig = plt.figure(figsize=(20,20))
gs = gridspec.GridSpec(21, 13)
...
if i >= 1:
ax3 = plt.subplot(gs[6:9, 3*i+1:3*i+4],projection=ccrs.Robinson())
else:
ax3 = plt.subplot(gs[6:9, 3*i:3*i+3],projection=ccrs.Robinson())
if i == 0:
cs3 = ax3.contourf(Lon,lat,cldhgh.squeeze(),12,transform=ccrs.PlateCarree(),cmap='gist_gray',vmin=0,vmax=1)
ax3.coastlines()
Cticks=np.around(np.linspace(0,1,6),decimals=1)
Cbar_ax3 = fig.add_axes([0.3,0.58,0.01,0.10])
cb3 = fig.colorbar(cs3, spacing='proportional',orientation='vertical',cax=Cbar_ax3,ticks=Cticks)
#cb2.set_ticklabels(Cticks.astype(int).astype(str),fontsize=7)
cb3.set_ticklabels(Cticks.astype(str),fontsize=12)
cb3.set_label('High Cloud Fraction',fontsize=10)
else:
cs3 = ax3.contourf(Lon,lat,delta_cldhgh,61,transform=ccrs.PlateCarree(),cmap='BrBG',vmin=-0.2,vmax=0.2)
c3 = ax3.contour(Lon,lat,cldhgh.squeeze(),12,vmin=0,vmax=1,colors='black',linewidths=0.5)
ax3.coastlines()
if i == 1:
cticks=np.around(np.linspace(-0.2,0.2,5),decimals=1)
cbar_ax = fig.add_axes([1.02,0.58,0.01,0.10])
ax3.set_ylabel('Hybrid Sigma-Pressure level (mb)',fontsize=12)
#cb = fig.colorbar(cs, spacing='proportional',orientation='vertical',cax=cbar_ax,ticks=cticks)
cb3 = fig.colorbar(mappable=None, norm=Normalize(vmin=-0.2,vmax=0.2), cmap='BrBG',spacing='proportional',orientation='vertical',cax=cbar_ax,ticks=cticks)
cb3.set_ticklabels(cticks.astype(str),fontsize=12)
#cb2.set_ticklabels(cticks.astype(int).astype(str),fontsize=10)
cb3.set_label('Cloud Fraction Difference',fontsize=10)
...
plt.suptitle('Comparison of mappables of Background Climate States',fontsize=24,y=1.01)
#fig.text(-0.04, 0.5, 'Sigma Pressure Level (mb)', va='center', rotation='vertical')
fig.tight_layout(pad=0.2)
plt.show()
fig.savefig(figure_path+'Reference_Climate_Comparison_of_Mappables.pdf',bbox_inches='tight')
I am able to almost do this successfully, except the original guess I made for the x displacement of my colorbars on the left side of the figure was too large:
To fix this I simply adjusted the first index of each subplot's "Cbar_ax" variable to be slightly smaller (e.g. from 0.3 to 0.25):
Cbar_ax3 = fig.add_axes([0.25,0.58,0.01,0.10])
The adjustment works for some subplots, but for others the colorbars all but vanish:
I have no idea how to solve this problem. I can make the colorbars appear using plt.colorbar() instead of fig.colorbar() without an colorbar axes designation, but the subplots themselves are not a consistent size with the rest of the figure (since plt.colorbar steals axes space from it's parent axes by default). What am I not seeing here? Why do some of these colorbars disappear when I move them?

How to generate several legends for single plot matplotlib

I was making a plot of f(x,y,z) and wanted this to be displayed in a 2D-plane. To avoid cluttering my legend i decided to have different linestyles for y, different colors for z and place the two in two separate legends. I couldn't find out how to do this even after a lot of digging, so I'm posting the solution i came up with here :) If anyone has more elegant solutions I'm all ears :)
Basically the solution was to make three plots, set two of them to have size (0,0) and place those two where i wanted the legends. It feels like an ugly way to do it, but it gave a nice plot and i didn't find any other way :) The resulting plot looks like this:
def plot_alt(style = 'log'):
cmap = cm.get_cmap('inferno')
color_scale = 1.2 #Variable to get colors from a certain part of the colormap
#Making grids for delta T and average concentration
D_T_axis = -np.logspace(np.log10(400), np.log10(1), 7)
C_bar_list = np.linspace(5,10,4)
ST_list = np.logspace(-3,-1,100)
# f(x,y,z)
DC_func = lambda C_bar, ST, DT: 2*C_bar * (1 - np.exp(ST*DT))/(1 + np.exp(ST*DT))
#Some different linestyles
styles = ['-', '--', '-.', ':']
fig, ax = plt.subplots(1,3, figsize = (10,5))
plt.sca(ax[0])
for i, C_bar in enumerate(C_bar_list): #See plot_c_rel_av_DT() for 'enumerate'
for j, DT in enumerate(D_T_axis):
plt.plot(ST_list, DC_func(C_bar, ST_list, DT), color = cmap(np.log10(-DT)/(color_scale*np.log10(-D_T_axis[0]))),
linestyle = styles[i])
# Generating separate legends by plotting lines in the two other subplots
# Basically: to get two separate legends i make two plots, place them where i want the legends
# and set their size to zero, then display their legends.
plt.sca(ax[1]) #Set current axes to ax[1]
for i, C_bar in enumerate(C_bar_list):
# Plotting the different linestyles
plt.plot(C_bar_list, linestyle = styles[i], color = 'black', label = str(round(C_bar, 2)))
plt.sca(ax[2])
for DT in D_T_axis:
#plotting the different colors
plt.plot(D_T_axis, color = cmap(np.log10(-DT)/(color_scale*np.log10(-D_T_axis[0]))), label = str(int(-DT)))
#Placing legend
#This is where i move and scale the three plots to make one plot and two legends
box0 = ax[0].get_position() #box0 is an object that contains the position and dimentions of the ax[0] subplot
box2 = ax[2].get_position()
ax[0].set_position([box0.x0, box0.y0, box2.x0 + 0.4*box2.width, box0.height])
box0 = ax[0].get_position()
ax[1].set_position([box0.x0 + box0.width, box0.y0 + box0.height + 0.015, 0,0])
ax[1].set_axis_off()
ax[2].set_position([box0.x0 + box0.width ,box0.y0 + box0.height - 0.25, 0,0])
ax[2].set_axis_off()
#Displaying plot
plt.sca(ax[0])
plt.xscale('log')
plt.xlim(0.001, 0.1)
plt.ylim(0, 5)
plt.xlabel(r'$S_T$')
plt.ylabel(r'$\Delta C$')
ax[1].legend(title = r'$\langle c \rangle$ [mol/L]',
bbox_to_anchor = (1,1), loc = 'upper left')
ax[2].legend(title = r'$-\Delta T$ [K]', bbox_to_anchor = (1,1), loc = 'upper left')
#Suptitle is the title of the figure. You can also have titles for the individual subplots
plt.suptitle('Steady state concentration gradient as a function of Soret-coefficient\n'
'for different temperature gradients and total concentrations')

Matplotlib Gridspsec spacing between rows

I have images with three different dimensions (WxH): 4 images with (174x145), 4 images with (145x145) and 4 images with (145x174). I could remove space between columns, but I cannot remove space between rows. Any suggestions?
This is my code:
fig = plt.figure(figsize=(10, 10))
gs = fig.add_gridspec(3, 4, hspace=0, wspace=0)
for r in range(3):
for c in range(4):
ax = fig.add_subplot(gs[r, c])
ax.imshow(slices[r][c].T, origin="lower", cmap="gray")
ax.axis("off")
As suggested in the comments, you need to set the height_ratios for your GridSpec, but that's not enough. You also need to adjust the size of your figure so that the width/height ratio of the figure matches the total width/height ratios of your images. But herein lies another problem, in that the axes will be scaled when plotting the images (because of aspect='equal') and because they do not all have the same width/height ratios.
The solution that I'm proposing is first to calculate what the dimensions of figures would be, once stretched to a common width size, then use that correct information to adjust the figure size and the height_ratios of the GridSpec.
# this is just for visualization purposes
cmaps = iter([ 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern',
'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg',
'gist_rainbow', 'rainbow', 'jet', 'nipy_spectral', 'gist_ncar'])
sizes = [(174,145), (145,145), (145,174)]
# create random images
p = []
for s in sizes:
p.append([np.random.random(size=s) for _ in range(4)])
p = np.array(p)
max_w = max([w for w,h in sizes])
new_sizes = np.array([(w*max_w/w, h*max_w/w) for w,h in sizes])
print(new_sizes)
total_w = 4*new_sizes[:,0].sum()
total_h = 3*new_sizes[:,1].sum()
eps=10/total_w
fig = plt.figure(figsize=(eps*total_w,eps*total_h))
gs0 = matplotlib.gridspec.GridSpec(3,4, height_ratios=[h for w,h in new_sizes], hspace=0, wspace=0)
for i in range(3):
for j in range(4):
ax = fig.add_subplot(gs0[i,j])
ax.imshow(p[i,j].T, origin="lower", cmap=next(cmaps))
ax.set_axis_off()
Unfortunately, this solution gets you almost to the desired output, but not quite, probably due to some rounding effect. But it close enough that I think you could use aspect='auto' if you can live with pixels that are ever so slightly not square.
(...)
ax.imshow(p[i,j].T, aspect='auto', origin="lower", cmap=next(cmaps))
(...)

Marker size/alpha scaling with window size/zoom in plot/scatter

When exploring data sets with many points on an xy chart, I can adjust the alpha and/or marker size to give a good quick visual impression of where the points are most densely clustered. However when I zoom in or make the window bigger, the a different alpha and/or marker size is needed to give the same visual impression.
How can I have the alpha value and/or the marker size increase when I make the window bigger or zoom in on the data? I am thinking that if I double the window area I could double the marker size, and/or take the square root of the alpha; and the opposite for zooming.
Note that all points have the same size and alpha. Ideally the solution would work with plot(), but if it can only be done with scatter() that would be helpful also.
You can achieve what you want with matplotlib event handling. You have to catch zoom and resize events separately. It's a bit tricky to account for both at the same time, but not impossible. Below is an example with two subplots, a line plot on the left and a scatter plot on the right. Both zooming (factor) and resizing of the figure (fig_factor) re-scale the points according to the scaling factors in figure size and x- and y- limits. As there are two limits defined -- one for the x and one for the y direction, I used here the respective minima for the two factors. If you'd rather want to scale with the larger factors, change the min to max in both event functions.
from matplotlib import pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=1, ncols = 2)
ax1,ax2 = axes
fig_width = fig.get_figwidth()
fig_height = fig.get_figheight()
fig_factor = 1.0
##saving some values
xlim = dict()
ylim = dict()
lines = dict()
line_sizes = dict()
paths = dict()
point_sizes = dict()
## a line plot
x1 = np.linspace(0,np.pi,30)
y1 = np.sin(x1)
lines[ax1] = ax1.plot(x1, y1, 'ro', markersize = 3, alpha = 0.8)
xlim[ax1] = ax1.get_xlim()
ylim[ax1] = ax1.get_ylim()
line_sizes[ax1] = [line.get_markersize() for line in lines[ax1]]
## a scatter plot
x2 = np.random.normal(1,1,30)
y2 = np.random.normal(1,1,30)
paths[ax2] = ax2.scatter(x2,y2, c = 'b', s = 20, alpha = 0.6)
point_sizes[ax2] = paths[ax2].get_sizes()
xlim[ax2] = ax2.get_xlim()
ylim[ax2] = ax2.get_ylim()
def on_resize(event):
global fig_factor
w = fig.get_figwidth()
h = fig.get_figheight()
fig_factor = min(w/fig_width,h/fig_height)
for ax in axes:
lim_change(ax)
def lim_change(ax):
lx = ax.get_xlim()
ly = ax.get_ylim()
factor = min(
(xlim[ax][1]-xlim[ax][0])/(lx[1]-lx[0]),
(ylim[ax][1]-ylim[ax][0])/(ly[1]-ly[0])
)
try:
for line,size in zip(lines[ax],line_sizes[ax]):
line.set_markersize(size*factor*fig_factor)
except KeyError:
pass
try:
paths[ax].set_sizes([s*factor*fig_factor for s in point_sizes[ax]])
except KeyError:
pass
fig.canvas.mpl_connect('resize_event', on_resize)
for ax in axes:
ax.callbacks.connect('xlim_changed', lim_change)
ax.callbacks.connect('ylim_changed', lim_change)
plt.show()
The code has been tested in Pyton 2.7 and 3.6 with matplotlib 2.1.1.
EDIT
Motivated by the comments below and this answer, I created another solution. The main idea here is to only use one type of event, namely draw_event. At first the plots did not update correctly upon zooming. Also ax.draw_artist() followed by a fig.canvas.draw_idle() like in the linked answer did not really solve the problem (however, this might be platform/backend specific). Instead I added an extra call to fig.canvas.draw() whenever the scaling changes (the if statement prevents infinite loops).
In addition, do avoid all the global variables, I wrapped everything into a class called MarkerUpdater. Each Axes instance can be registered separately to the MarkerUpdater instance, so you could also have several subplots in one figure, of which some are updated and some not. I also fixed another bug, where the points in the scatter plot scaled wrongly -- they should scale quadratic, not linear (see here).
Finally, as it was missing from the previous solution, I also added updating for the alpha value of the markers. This is not quite as straight forward as the marker size, because alpha values must not be larger than 1.0. For this reason, in my implementation the alpha value can only be decreased from the original value. Here I implemented it such that the alpha decreases when the figure size is decreased. Note that if no alpha value is provided to the plot command, the artist stores None as alpha value. In this case the automatic alpha tuning is off.
What should be updated in which Axes can be defined with the features keyword -- see below if __name__ == '__main__': for an example how to use MarkerUpdater.
EDIT 2
As pointed out by #ImportanceOfBeingErnest, there was a problem with infinite recursion with my answer when using the TkAgg backend, and apparently problems with the figure not refreshing properly upon zooming (which I couldn't verify, so probably that was implementation dependent). Removing the fig.canvas.draw() and adding ax.draw_artist(ax) within the loop over the Axes instances instead fixed this issue.
EDIT 3
I updated the code to fix an ongoing issue where figure is not updated properly upon a draw_event. The fix was taken from this answer, but modified to also work for several figures.
In terms of an explanation of how the factors are obtained, the MarkerUpdater instance contains a dict that stores for each Axes instance the figure dimensions and the limits of the axes at the time it is added with add_ax. Upon a draw_event, which is for instance triggered when the figure is resized or the user zooms in on the data, the new (current) values for figure size and axes limits are retrieved and a scaling factor is calculated (and stored) such that zooming in and increasing the figure size makes the markers bigger. Because x- and y-dimensions may change at different rates, I use min to pick one of the two calculated factors and always scale against the original size of the figure.
If you want your alpha to scale with a different function, you can easily change the lines that adjust the alpha value. For instance, if you want a power law instead of a linear decrease, you can write path.set_alpha(alpha*facA**n), where n is the power.
from matplotlib import pyplot as plt
import numpy as np
##plt.switch_backend('TkAgg')
class MarkerUpdater:
def __init__(self):
##for storing information about Figures and Axes
self.figs = {}
##for storing timers
self.timer_dict = {}
def add_ax(self, ax, features=[]):
ax_dict = self.figs.setdefault(ax.figure,dict())
ax_dict[ax] = {
'xlim' : ax.get_xlim(),
'ylim' : ax.get_ylim(),
'figw' : ax.figure.get_figwidth(),
'figh' : ax.figure.get_figheight(),
'scale_s' : 1.0,
'scale_a' : 1.0,
'features' : [features] if isinstance(features,str) else features,
}
ax.figure.canvas.mpl_connect('draw_event', self.update_axes)
def update_axes(self, event):
for fig,axes in self.figs.items():
if fig is event.canvas.figure:
for ax, args in axes.items():
##make sure the figure is re-drawn
update = True
fw = fig.get_figwidth()
fh = fig.get_figheight()
fac1 = min(fw/args['figw'], fh/args['figh'])
xl = ax.get_xlim()
yl = ax.get_ylim()
fac2 = min(
abs(args['xlim'][1]-args['xlim'][0])/abs(xl[1]-xl[0]),
abs(args['ylim'][1]-args['ylim'][0])/abs(yl[1]-yl[0])
)
##factor for marker size
facS = (fac1*fac2)/args['scale_s']
##factor for alpha -- limited to values smaller 1.0
facA = min(1.0,fac1*fac2)/args['scale_a']
##updating the artists
if facS != 1.0:
for line in ax.lines:
if 'size' in args['features']:
line.set_markersize(line.get_markersize()*facS)
if 'alpha' in args['features']:
alpha = line.get_alpha()
if alpha is not None:
line.set_alpha(alpha*facA)
for path in ax.collections:
if 'size' in args['features']:
path.set_sizes([s*facS**2 for s in path.get_sizes()])
if 'alpha' in args['features']:
alpha = path.get_alpha()
if alpha is not None:
path.set_alpha(alpha*facA)
args['scale_s'] *= facS
args['scale_a'] *= facA
self._redraw_later(fig)
def _redraw_later(self, fig):
timer = fig.canvas.new_timer(interval=10)
timer.single_shot = True
timer.add_callback(lambda : fig.canvas.draw_idle())
timer.start()
##stopping previous timer
if fig in self.timer_dict:
self.timer_dict[fig].stop()
##storing a reference to prevent garbage collection
self.timer_dict[fig] = timer
if __name__ == '__main__':
my_updater = MarkerUpdater()
##setting up the figure
fig, axes = plt.subplots(nrows = 2, ncols =2)#, figsize=(1,1))
ax1,ax2,ax3,ax4 = axes.flatten()
## a line plot
x1 = np.linspace(0,np.pi,30)
y1 = np.sin(x1)
ax1.plot(x1, y1, 'ro', markersize = 10, alpha = 0.8)
ax3.plot(x1, y1, 'ro', markersize = 10, alpha = 1)
## a scatter plot
x2 = np.random.normal(1,1,30)
y2 = np.random.normal(1,1,30)
ax2.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
## scatter and line plot
ax4.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
ax4.plot([0,0.5,1],[0,0.5,1],'ro', markersize = 10) ##note: no alpha value!
##setting up the updater
my_updater.add_ax(ax1, ['size']) ##line plot, only marker size
my_updater.add_ax(ax2, ['size']) ##scatter plot, only marker size
my_updater.add_ax(ax3, ['alpha']) ##line plot, only alpha
my_updater.add_ax(ax4, ['size', 'alpha']) ##scatter plot, marker size and alpha
plt.show()

small scatter plot markers in matplotlib are always black

I'm trying to use matplotlib to make a scatter plot with very small gray points. Because of the point density, the points need to be small. The problem is that the scatter() function's markers seem to have both a line and a fill. When the markers are small, only the line is visible, not the fill, and the line isn't the right colour (it's always black).
I can get exactly what I want using gnuplot: plot 'nodes' with points pt 0 lc rgb 'gray'
How can I make very small gray points using matplotlib scatterplot()?
scatter([1,2,3], [2,4,5], s=1, facecolor='0.5', lw = 0)
This sets the markersize to 1 (s=1), the facecolor to gray (facecolor='0.5'), and the linewidth to 0 (lw=0).
If the marker has no face (cannot be filled, e.g. '+','x'), then the edgecolor has to be set instead of c, and lw should not be 0:
scatter([1,2,3], [2,4,5], marker='+', edgecolor='r')
The following will no work
scatter([1,2,3], [2,4,5], s=1, marker='+', facecolor='0.5', lw = 0)
because the edge/line will not be displayed, so nothing will be displayed.
The absolute simplest answer to your question is: use the color parameter instead of the c parameter to set the color of the whole marker.
It's easy to see the difference when you compare the results:
from matplotlib import pyplot as plt
plt.scatter([1,2,3], [3,1,2], c='0.8') # marker not all gray
plt.scatter([1,2,3], [3,1,2], color='0.8') # marker all gray
Details:
For your simple use case where you just want to make your whole marker be the same shade of gray color, you really shouldn't have to worry about things like face color vs edge color, and whether your marker is defined as all edges or some edges and some fill. Instead, just use the color parameter and know that your whole marker will be set to the single color that you specify!
In response to zwol's question in comment - my reputation is not high enough to leave comments, so this will have to do: In the event that your colors come from a colormap (i.e., are from a "sequence of values to be mapped") you can use color = as demonstrated in the following:
from matplotlib import pyplot
x = [1,5,8,9,5]
y = [4,2,4,7,9]
numSides = [2,3,1,1,5]
cmap = pyplot.cm.get_cmap("copper_r")
min, max = min(numSides), max(numSides)
for i in range(len(x)):
if numSides[i] >= 2:
cax = pyplot.scatter(x[i], y[i], marker = '+', s = 100, c = numSides[i], cmap = cmap)
cax.set_clim(min, max)
elif numSides[i] == 1:
pyplot.scatter(x[i], y[i], marker = '.', s = 40, color = cmap(numSides[i]))
fig = pyplot.gcf()
fig.set_size_inches(8.4, 6)
fig.savefig('figure_test.png', dpi = 200)
pyplot.show()