overlapping constrained 3d subplots - matplotlib

What knobs must I tweak to prevent these problems:
overlapping axes labels
overlapping plots with cropped axes labels
I'm using matplotlib 3.5.1 with the PGF backend. Some solutions for older versions no longer work.
fig, axes = plt.subplots\
(2, 3, constrained_layout=True, subplot_kw=dict(projection="3d"))
#it = np.nditer(axes, flags=["refs_ok","multi_index"])
#for ax in it:
# # Plot the surfaces, add row and column title annotations.
# pass
width = 150 * 0.8 * mm
height = width * 0.65
fig.set_size_inches(width, height)
fig.savefig("something.pgf", dpi=300)
plt.close(fig)

Getting rid of the constrained layout and using plt.subplots_adjust(wspace=<value>, hspace=<value>) worked for me
from matplotlib import pyplot as plt
fig, axes = plt.subplots(nrows=2, ncols=3, subplot_kw=dict(projection="3d"))
plt.subplots_adjust(wspace=0.5,hspace=0.5)
labels_x, labels_y, labels_z = [['x-axis']*3]*2, [['y-axis']*3]*2, [['z-axis']*3]*2
for i in range(len(axes)):
for j in range(len(axes[i])):
axes[i,j].set_xlabel(labels_x[i][j])
axes[i,j].set_ylabel(labels_y[i][j])
axes[i,j].set_zlabel(labels_z[i][j])
plt.show()

While both tight and constrained layouts can be used with 3d projection (mplot3d) it seems that constrained layout does not understand how to pad 3d tick labels, leading to overlapping or trimmed labels. Both layout managers adjust subplot padding and axes size given a fixed figure size. Neither can fit the figure size to the contents. To do so with tight layout, extrapolate the desired figure size from the current figure tight bbox over multiple iterations. When using the constrained layout manager, sum the axes tight bboxes and padding to determine any extraneous space. Tight layout adjusts subplotpars (axes size and figure padding) and supports "h_pad" and "w_pad" parameters. Constrained layout adjusts axes size and supports "wspace", "hspace", "w_pad", and "h_pad" parameters. The tight layout squeezes subplots into a tight group which is then centered in the available space. The constrained layout distributes subplots evenly across all available space. Regardless of the layout manager, if the ticks overlap or are clustered too tightly, switch the tick locator to "MaxNLocator" for some smaller "n".
The root problem is that the 3d projection tick labels are empty until a full canvas draw. The "_draw_disabled" draw performed by the constrained layout manager isn't sufficient to trigger the tick labels. If you trace the axes tight bbox you'll notice they don't include the labels until after a call to "fig.canvas.draw", before then the tick labels are just "Text(0, 0, '')". Be sure to include this call after sizing the figure and then the layout will be constrained as expected. Given a fixed figure width, set the height based on aesthetics or sum the axes tight bboxes with padding to determine the minimum possible height.

Related

Relationship between dpi and figure size

I have created a figure using matplotlib but I have realized the plot axis and the drawn line gets zoomed out.
Reading this earlier discussion thread, it explains how to set the figure size.
fig, ax = plt.subplots()
fig.set_size_inches(3, 1.5)
plt.savefig(file.jpeg, edgecolor='black', dpi=400, facecolor='black', transparent=True)
With the above code (other configurations removed for brevity), I do get a resulting image file with 1200 X 600 desired dimensions(should we say resolution too?) and desired file size.
The projected image is scaled out in an unusual way, annotations for example are enlarged. While I can set the size of the labels on the axis, the figure doesn't look proportional with respect to the scale since the bottom and right spines are large and so are the plotted lines.
The question, therefore, is, what configurations are going wrong?
Figure size (figsize) determines the size of the figure in inches. This gives the amount of space the axes (and other elements) have inside the figure. The default figure size is (6.4, 4.8) inches in matplotlib 2. A larger figure size will allow for longer texts, more axes or more ticklabels to be shown.
Dots per inches (dpi) determines how many pixels the figure comprises. The default dpi in matplotlib is 100. A figure of figsize=(w,h) will have
px, py = w*dpi, h*dpi # pixels
# e.g.
# 6.4 inches * 100 dpi = 640 pixels
So in order to obtain a figure with a pixel size of e.g. (1200,600) you may chose several combinations of figure size and dpi, e.g.
figsize=(15,7.5), dpi= 80
figsize=(12,6) , dpi=100
figsize=( 8,4) , dpi=150
figsize=( 6,3) , dpi=200
etc.
Now, what is the difference? This is determined by the size of the elements inside the figure. Most elements like lines, markers, texts have a size given in points.
Matplotlib figures use Points per inch (ppi) of 72. A line with thickness 1 point will be 1./72. inch wide. A text with fontsize 12 points will be 12./72. inch heigh.
Of course if you change the figure size in inches, points will not change, so a larger figure in inches still has the same size of the elements. Changing the figure size is thus like taking a piece of paper of a different size. Doing so, would of course not change the width of the line drawn with the same pen.
On the other hand, changing the dpi scales those elements. At 72 dpi, a line of 1 point size is one pixel strong. At 144 dpi, this line is 2 pixels strong. A larger dpi will therefore act like a magnifying glass. All elements are scaled by the magnifying power of the lens.
A comparisson for constant figure size and varying dpi is shown in the image below on the left. On the right you see a constant dpi and varying figure size. Figures in each row have the same pixel size.
Code to reproduce:
import matplotlib.pyplot as plt
%matplotlib inline
def plot(fs,dpi):
fig, ax=plt.subplots(figsize=fs, dpi=dpi)
ax.set_title("Figsize: {}, dpi: {}".format(fs,dpi))
ax.plot([2,4,1,5], label="Label")
ax.legend()
figsize=(2,2)
for i in range(1,4):
plot(figsize, i*72)
dpi=72
for i in [2,4,6]:
plot((i,i), dpi)

How to plot images of different size at the same resolution?

A collection of images are plotted as follow:
figure(num=None, figsize=(16, 14), dpi=300)
k=1
for i in range(1,10):
for j in range(1,6):
subplot(9,5,k,xticks=[],yticks=[])
imshow(rgb_chromosomes[k-1],interpolation='nearest')
k=k+1
It is visible that from a image to an other, pixels are not the same size.
How to fix that issue?
Use interpolation= 'bilinear' and subsample the result with regular spacing (say take every other four pixel, this depends on the final pixel size you want) and form a tiny image. Then magnify this tiny image with 'nearest' interpolation.
You can also keep the 'nearest' setting for the first interpolation, but the result will look ugly.
so, from image to image are the pixels different sizes? From context, I am guessing that these are all snippets from the same image/imaging conditions and you want the scale to be the same in all of them.
Something like:
fig, ax_lst = plt.subplots(9, 6) # better way to set up your axes
for k, ax in enumerate(ax_lst.ravel()):
ax.imshow(rgb_chromosomes[k], interpolation='none')
ax.set_xlim([0, max_image_width])
ax.set_ylim([0, max_image_height])
ax.set_frame_on(False)

Matplotlib difference between two images

I have images (4000x2000 pixels) that are derived from the same image, but with subtle differences in less than 1% of the pixels. I'd like to plot the two images side-by-side and highlight the regions of the array's that are different (by highlight I mean I want the pixels that differ to jump out, but still display the color that matches their value. I've been using rectangles that are unfilled to outline the edges of such pixels so far. I can do this very nicely in small images (~50x50) with:
fig=figure(figsize=(20,15))
ax1=fig.add_subplot(1,2,1)
imshow(image1,interpolation='nearest',origin='lower left')
colorbar()
ax2=fig.add_subplot(122,sharex=ax1, sharey=ax1)
imshow(image2,interpolation='nearest',origin='lower left')
colorbar()
#now show differences
Xspots=im1!=im2
Xx,Xy=nonzero(Xspots)
for x,y in zip(Xx,Xy):
rect=Rectangle((y-.5,x-.5),1,1,color='w',fill=False,ec='w')
ax1.add_patch(rect)
ax2.add_patch(rect)
However this doesn't work so well when the image is very large. Strange things happen, for example when I zoom in the patch disappears. Also, this way sucks because it takes forever to load things when I zoom in/out.
I feel like there must be a better way to do this, maybe one where there is only one patch that determines where all of the things are, rather than a whole bunch of patches. I could do a scatter plot on top of the imshow image, but I don't know how to fix it so that the points will stay exactly the size of the pixel when I zoom in/out.
Any ideas?
I would try something with the alpha channel:
import copy
N, M = 20, 40
test_data = np.random.rand(N, M)
mark_mask = np.random.rand(N, M) < .01 # mask 1%
# this is redundant in this case, but in general you need it
my_norm = matplotlib.colors.Normalize(vmin=0, vmax=1)
# grab a copy of the color map
my_cmap = copy.copy(cm.get_cmap('cubehelix'))
c_data= my_cmap(my_norm(test_data))
c_data[:, :, 3] = .5 # make everything half alpha
c_data[mark_mask, 3] = 1 # reset the marked pixels as full opacity
# plot it
figure()
imshow(c_data, interpolation='none')
No idea if this will work with your data or not.

matplotlib one centered axis label for two diagrams

I'd like to have one axis label centered over two axes in matplotlib.
For example, I set up the axes as follows:
figure = pyplot.figure(figsize=10,10))
diagram1 = figure.add_axes([0.01,0.62,0.90,0.30])
diagram2 = figure.add_axes([0.01,0.32,0.90,0.30])
This will generate two diagrams on top of each other. How can i now define one axis label for the y axis centered on both diagrams.
I think this can be done with subplots, but I prefer to set every diagram individually, as shown above.
You can manually change the y-coordinate of the label.
diagram1.set_ylabel('y label').set_y(0)
# Alternatively you can use
# diagram2.set_ylabel('y label').set_y(1)
The coordinate is in the axes coordinate space, meaning 0 is the bottom and 1 is the top of the Axes.
Seeing how your Axes are placed at x=0.01, you can make the label appear at the right side of the plots as well
diagram1.yaxis.set_label_position("right")

pyplot scatter plot marker size

In the pyplot document for scatter plot:
matplotlib.pyplot.scatter(x, y, s=20, c='b', marker='o', cmap=None, norm=None,
vmin=None, vmax=None, alpha=None, linewidths=None,
faceted=True, verts=None, hold=None, **kwargs)
The marker size
s:
size in points^2. It is a scalar or an array of the same length as x and y.
What kind of unit is points^2? What does it mean? Does s=100 mean 10 pixel x 10 pixel?
Basically I'm trying to make scatter plots with different marker sizes, and I want to figure out what does the s number mean.
This can be a somewhat confusing way of defining the size but you are basically specifying the area of the marker. This means, to double the width (or height) of the marker you need to increase s by a factor of 4. [because A = WH => (2W)(2H)=4A]
There is a reason, however, that the size of markers is defined in this way. Because of the scaling of area as the square of width, doubling the width actually appears to increase the size by more than a factor 2 (in fact it increases it by a factor of 4). To see this consider the following two examples and the output they produce.
# doubling the width of markers
x = [0,2,4,6,8,10]
y = [0]*len(x)
s = [20*4**n for n in range(len(x))]
plt.scatter(x,y,s=s)
plt.show()
gives
Notice how the size increases very quickly. If instead we have
# doubling the area of markers
x = [0,2,4,6,8,10]
y = [0]*len(x)
s = [20*2**n for n in range(len(x))]
plt.scatter(x,y,s=s)
plt.show()
gives
Now the apparent size of the markers increases roughly linearly in an intuitive fashion.
As for the exact meaning of what a 'point' is, it is fairly arbitrary for plotting purposes, you can just scale all of your sizes by a constant until they look reasonable.
Edit: (In response to comment from #Emma)
It's probably confusing wording on my part. The question asked about doubling the width of a circle so in the first picture for each circle (as we move from left to right) it's width is double the previous one so for the area this is an exponential with base 4. Similarly the second example each circle has area double the last one which gives an exponential with base 2.
However it is the second example (where we are scaling area) that doubling area appears to make the circle twice as big to the eye. Thus if we want a circle to appear a factor of n bigger we would increase the area by a factor n not the radius so the apparent size scales linearly with the area.
Edit to visualize the comment by #TomaszGandor:
This is what it looks like for different functions of the marker size:
x = [0,2,4,6,8,10,12,14,16,18]
s_exp = [20*2**n for n in range(len(x))]
s_square = [20*n**2 for n in range(len(x))]
s_linear = [20*n for n in range(len(x))]
plt.scatter(x,[1]*len(x),s=s_exp, label='$s=2^n$', lw=1)
plt.scatter(x,[0]*len(x),s=s_square, label='$s=n^2$')
plt.scatter(x,[-1]*len(x),s=s_linear, label='$s=n$')
plt.ylim(-1.5,1.5)
plt.legend(loc='center left', bbox_to_anchor=(1.1, 0.5), labelspacing=3)
plt.show()
Because other answers here claim that s denotes the area of the marker, I'm adding this answer to clearify that this is not necessarily the case.
Size in points^2
The argument s in plt.scatter denotes the markersize**2. As the documentation says
s : scalar or array_like, shape (n, ), optional
size in points^2. Default is rcParams['lines.markersize'] ** 2.
This can be taken literally. In order to obtain a marker which is x points large, you need to square that number and give it to the s argument.
So the relationship between the markersize of a line plot and the scatter size argument is the square. In order to produce a scatter marker of the same size as a plot marker of size 10 points you would hence call scatter( .., s=100).
import matplotlib.pyplot as plt
fig,ax = plt.subplots()
ax.plot([0],[0], marker="o", markersize=10)
ax.plot([0.07,0.93],[0,0], linewidth=10)
ax.scatter([1],[0], s=100)
ax.plot([0],[1], marker="o", markersize=22)
ax.plot([0.14,0.86],[1,1], linewidth=22)
ax.scatter([1],[1], s=22**2)
plt.show()
Connection to "area"
So why do other answers and even the documentation speak about "area" when it comes to the s parameter?
Of course the units of points**2 are area units.
For the special case of a square marker, marker="s", the area of the marker is indeed directly the value of the s parameter.
For a circle, the area of the circle is area = pi/4*s.
For other markers there may not even be any obvious relation to the area of the marker.
In all cases however the area of the marker is proportional to the s parameter. This is the motivation to call it "area" even though in most cases it isn't really.
Specifying the size of the scatter markers in terms of some quantity which is proportional to the area of the marker makes in thus far sense as it is the area of the marker that is perceived when comparing different patches rather than its side length or diameter. I.e. doubling the underlying quantity should double the area of the marker.
What are points?
So far the answer to what the size of a scatter marker means is given in units of points. Points are often used in typography, where fonts are specified in points. Also linewidths is often specified in points. The standard size of points in matplotlib is 72 points per inch (ppi) - 1 point is hence 1/72 inches.
It might be useful to be able to specify sizes in pixels instead of points. If the figure dpi is 72 as well, one point is one pixel. If the figure dpi is different (matplotlib default is fig.dpi=100),
1 point == fig.dpi/72. pixels
While the scatter marker's size in points would hence look different for different figure dpi, one could produce a 10 by 10 pixels^2 marker, which would always have the same number of pixels covered:
import matplotlib.pyplot as plt
for dpi in [72,100,144]:
fig,ax = plt.subplots(figsize=(1.5,2), dpi=dpi)
ax.set_title("fig.dpi={}".format(dpi))
ax.set_ylim(-3,3)
ax.set_xlim(-2,2)
ax.scatter([0],[1], s=10**2,
marker="s", linewidth=0, label="100 points^2")
ax.scatter([1],[1], s=(10*72./fig.dpi)**2,
marker="s", linewidth=0, label="100 pixels^2")
ax.legend(loc=8,framealpha=1, fontsize=8)
fig.savefig("fig{}.png".format(dpi), bbox_inches="tight")
plt.show()
If you are interested in a scatter in data units, check this answer.
You can use markersize to specify the size of the circle in plot method
import numpy as np
import matplotlib.pyplot as plt
x1 = np.random.randn(20)
x2 = np.random.randn(20)
plt.figure(1)
# you can specify the marker size two ways directly:
plt.plot(x1, 'bo', markersize=20) # blue circle with size 10
plt.plot(x2, 'ro', ms=10,) # ms is just an alias for markersize
plt.show()
From here
It is the area of the marker. I mean if you have s1 = 1000 and then s2 = 4000, the relation between the radius of each circle is: r_s2 = 2 * r_s1. See the following plot:
plt.scatter(2, 1, s=4000, c='r')
plt.scatter(2, 1, s=1000 ,c='b')
plt.scatter(2, 1, s=10, c='g')
I had the same doubt when I saw the post, so I did this example then I used a ruler on the screen to measure the radii.
I also attempted to use 'scatter' initially for this purpose. After quite a bit of wasted time - I settled on the following solution.
import matplotlib.pyplot as plt
input_list = [{'x':100,'y':200,'radius':50, 'color':(0.1,0.2,0.3)}]
output_list = []
for point in input_list:
output_list.append(plt.Circle((point['x'], point['y']), point['radius'], color=point['color'], fill=False))
ax = plt.gca(aspect='equal')
ax.cla()
ax.set_xlim((0, 1000))
ax.set_ylim((0, 1000))
for circle in output_list:
ax.add_artist(circle)
This is based on an answer to this question
If the size of the circles corresponds to the square of the parameter in s=parameter, then assign a square root to each element you append to your size array, like this: s=[1, 1.414, 1.73, 2.0, 2.24] such that when it takes these values and returns them, their relative size increase will be the square root of the squared progression, which returns a linear progression.
If I were to square each one as it gets output to the plot: output=[1, 2, 3, 4, 5]. Try list interpretation: s=[numpy.sqrt(i) for i in s]