Place xticks and yticks in the pixel center on imshow plot - python

I am working with matplotlib to plot a heat map with some information and I want to move the xticks and the yticks to the center. I have searched in stackoverflow for previous questions but I couldn't reach one suitable for the problem. I attach my code and the image that I get:
import matplotlib.pyplot as plt
from matplotlib import colors
import numpy as np
def plot():
intensity= np.random.rand(10,10)
matrix_intensity=np.matrix(intensity)
max_intensity=matrix_intensity.max()
min_intensity = matrix_intensity.min()
for e in range(len(intensity)):
for i in range(len(intensity[e])):
intensity[e][i]=float(intensity[e][i])/float(max_intensity)
np.random.seed(101)
cmap = colors.ListedColormap(['white','khaki', 'goldenrod','yellowgreen','mediumseagreen','darkcyan','tomato','indianred' ,'sienna','maroon'])
bounds = np.linspace(min_intensity/max_intensity,1,11).tolist()
norm = colors.BoundaryNorm(bounds, cmap.N)
img = plt.imshow(intensity, interpolation='none', origin='lower',extent=[0,len(intensity),0,len(intensity)],
cmap=cmap, norm=norm)
cb=plt.colorbar(img, fraction=0.1,cmap=cmap, norm=norm, boundaries=bounds,format='%.2f') #'%.2f')
cb.set_label(label='Ratio',fontsize=12,labelpad=10)
plt.ylabel('Origin',fontsize=11)
plt.xlabel('Destination',fontsize=11)
plt.title('Best route:',fontsize=10)
plt.suptitle('Best Solution:',fontsize=10)
plt.xticks(range(1,len(intensity)+1))
plt.yticks(range(1,len(intensity)+1))
plt.savefig('images/hello.png')
plt.show()
The fact is that I would like the x and the y ticks to point out the center of every square because otherwise, it doesn't make sense to plot the squares. Does somebody know how to fix this? Maybe this question is obvious but the matplotlib documentation for all the statements sometimes is difficult to understand.

The obvious solution would probably to use a different extent, namely to let the image live in the range between 0.5 and len(intensity)+0.5.
extent=[.5, len(intensity)+.5, .5, len(intensity)+.5]
img = plt.imshow(intensity, interpolation='none', origin='lower',extent=extent,
cmap=cmap, norm=norm)

You need to change the way you set your xticks and yticks loc and labels to below:
plt.xticks([x-0.5 for x in list(range(1,len(intensity)+1))], range(1,len(intensity)+1))
plt.yticks([x-0.5 for x in list(range(1,len(intensity)+1))], range(1,len(intensity)+1))
Output:

The other answers are both good, however I would like to provide a more general implementation that also doesn't alter default ticks, as I have a function that can be used to calculate the axis limits and set them as in #ImportanceOfBeingErnest answer.
import numpy as np
def span_from_pixels(p,n=None):
"""From positions of pixel centers p returns a range from side to side. Useful to adjust plot extent in imshow.
In alternative, p can be provided as range and number of pixels.
Note that np.linspace has flag retsteps to return step size."""
if n is None:
n=len(p)
dx=(np.max(p)-np.min(p))/(n-1)
return (np.min(p)-dx/2,np.max(p)+dx/2)
def test_span_from_pixels():
print (span_from_pixels([0,3],4)) #[-0.5,3.5]
print (span_from_pixels([0,2],3)) #[-0.5,2.5]
print (span_from_pixels([0,1,2])) #[-0.5,2.5]
print (span_from_pixels([0,0.5,1,1.5,2])) #[-0.25,2.25]
Please let me know if something doesn't work, these are tested in my code, but I made some change to remove dependencies. I assume I didn't break anything, but I cannot test it now.

Related

Swap axis for a linspace plot

I have a function with an histogram, plotted like this :
import matplotlib.pyplot as plt
import numpy as np
lin = np.linspace(min(foo), max(foo), len(foo))
plt.plot(lin, bar)
plt.hist(bar, density=True, bins=100, histtype='stepfilled', alpha=0.2)
plt.show()
Where foo and bar are simple arrays.
However, I would want to have the whole thing in a vertical way... I could add orientation='horizontal' to the histogram, but it would not change the function (and from what I have seen, there is nothing similar for a plot -> obviously it wouldn't be a function then, but a curve). Otherwise, I could add plt.gca().invert_yaxis() somewhere, but the same problem resides : plot is used for functions, so the swap of it does... well, that :
So, the only way I have now is to manually turn the whole original picture by 90 degrees, but then the axis are turned too and will no longer be on the left and bottom (obviously).
So, have you another idea ? Maybe I should try something else than plt.plot ?
EDIT : In the end, I would want something like the image below, but with axes made right.
If you have a plot of y vs x, you can swap axes by swapping arrays:
plt.plot(bar, lin)
There's no special feature because it's supported out of the box. As you've discovered, plotting a transposed histogram can be accomplished by passing in
orientation='horizontal'
I couldn't find any matplotlib method dealing with the issue. You can rotate the curve in a purely mathematical way, i.e. do it through the rotation matrix. In this simple case it is sufficient to just exchange variables x and y but in general it looks like this (let's take a parabola for a clear example):
rotation = lambda angle: np.array([[ np.cos(angle), -np.sin(angle)],
[np.sin(angle), np.cos(angle)]])
x = np.linspace(-10,10,1000)
y = -x**2
matrix = np.vstack([x,y]).T
rotated_matrix = matrix # rotation(np.deg2rad(90))
fig, ax = plt.subplots(1,2)
ax[0].plot(rotated_matrix[:,0], rotated_matrix[:,1])
ax[1].plot(x,y)
rotated_matrix = matrix # rotation(np.deg2rad(-45))
fig, ax = plt.subplots(1,2)
ax[0].plot(rotated_matrix[:,0], rotated_matrix[:,1])
ax[1].plot(x,y)

Setting both axes logarithmic in bar plot matploblib

I have already binned data to plot a histogram. For this reason I'm using the plt.bar() function. I'd like to set both axes in the plot to a logarithmic scale.
If I set plt.bar(x, y, width=10, color='b', log=True) which lets me set the y-axis to log but I can't set the x-axis logarithmic.
I've tried plt.xscale('log') unfortunately this doesn't work right. The x-axis ticks vanish and the sizes of the bars don't have equal width.
I would be grateful for any help.
By default, the bars of a barplot have a width of 0.8. Therefore they appear larger for smaller x values on a logarithmic scale. If instead of specifying a constant width, one uses the distance between the bin edges and supplies this to the width argument, the bars will have the correct width. One would also need to set the align to "edge" for this to work.
import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)
x = np.logspace(0, 5, num=21)
y = (np.sin(1.e-2*(x[:-1]-20))+3)**10
fig, ax = plt.subplots()
ax.bar(x[:-1], y, width=np.diff(x), log=True,ec="k", align="edge")
ax.set_xscale("log")
plt.show()
I cannot reproduce missing ticklabels for a logarithmic scaling. This may be due to some settings in the code that are not shown in the question or due to the fact that an older matplotlib version is used. The example here works fine with matplotlib 2.0.
If the goal is to have equal width bars, assuming datapoints are not equidistant, then the most proper solution is to set width as
plt.bar(x, y, width=c*np.array(x), color='b', log=True) for a constant c appropriate for the plot. Alignment can be anything.
I know it is a very old question and you might have solved it but I've come to this post because I was with something like this but at the y axis and I manage to solve it just using ax.set_ylim(df['my data'].min()+100, df['my data'].max()+100). In y axis I have some sensible information which I thouhg the best way was to show in log scale but when I set log scale I couldn't see the numbers proper (as this post in x axis) so I just leave the idea of use log and use the min and max argment. It sets the scale of my graph much like as log. Still looking for another way for doesnt need use that -+100 at set_ylim.
While this does not actually use pyplot.bar, I think this method could be helpful in achieving what the OP is trying to do. I found this to be easier than trying to calibrate the width as a function of the log-scale, though it's more steps. Create a line collection whose width is independent of the chart scale.
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.collections as coll
#Generate data and sort into bins
a = np.random.logseries(0.5, 1000)
hist, bin_edges = np.histogram(a, bins=20, density=False)
x = bin_edges[:-1] # remove the top-end from bin_edges to match dimensions of hist
lines = []
for i in range(len(x)):
pair=[(x[i],0), (x[i], hist[i])]
lines.append(pair)
linecoll = coll.LineCollection(lines, linewidths=10, linestyles='solid')
fig, ax = plt.subplots()
ax.add_collection(linecoll)
ax.set_xscale("log")
ax.set_yscale("log")
ax.set_xlim(min(x)/10,max(x)*10)
ax.set_ylim(0.1,1.1*max(hist)) #since this is an unweighted histogram, the logy doesn't make much sense.
Resulting plot - no frills
One drawback is that the "bars" will be centered, but this could be changed by offsetting the x-values by half of the linewidth value ... I think it would be
x_new = x + (linewidth/2)*10**round(np.log10(x),0).

Aspect ratio in semi-log plot with Matplotlib

When I plot a function in matplotlib, the plot is framed by a rectangle. I want the ratio of the length and height of this rectangle to be given by the golden mean ,i.e., dx/dy=1.618033...
If the x and y scale are linear I found this solution using google
import numpy as np
import matplotlib.pyplot as pl
golden_mean = (np.sqrt(5)-1.0)/2.0
dy=pl.gca().get_ylim()[1]-pl.gca().get_ylim()[0]
dx=pl.gca().get_xlim()[1]-pl.gca().get_xlim()[0]
pl.gca().set_aspect((dx/dy)*golden_mean,adjustable='box')
If it is a log-log plot I came up with this solution
dy=np.abs(np.log10(pl.gca().get_ylim()[1])-np.log10(pl.gca().get_ylim()[0]))
dx=np.abs(np.log10(pl.gca().get_xlim()[1])-np.log10(pl.gca().get_xlim()[0]))
pl.gca().set_aspect((dx/dy)*golden_mean,adjustable='box')
However, for a semi-log plot, when I call set_aspect, I get
UserWarning: aspect is not supported for Axes with xscale=log, yscale=linear
Can anyone think of a work-around for this?
the most simple solution would be to log your data and then use the method for lin-lin.
you can then label the axes to let it look like a normal log-plot.
ticks = np.arange(min_logx, max_logx, 1)
ticklabels = [r"$10^{}$".format(tick) for tick in ticks]
pl.yticks(ticks, ticklabels)
if you have higher values than 10e9 you will need three pairs of braces, two pairs for the LaTeX braces and one for the .format()
ticklabels = [r"$10^{{{}}}$".format(tick) for tick in ticks]
Edit:
if you want also the ticks for 0.1ex ... 0.9ex, you want to use the minor ticks as well:
they need to be located at log10(1), log10(2), log10(3) ..., log10(10), log10(20) ...
you can create and set them with:
minor_ticks = []
for i in range(min_exponent, max_exponent):
for j in range(2,10):
minor_ticks.append(i+np.log10(j))
plt.gca().set_yticks(minor_labels, minor=True)

set matplotlib 3d plot aspect ratio

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
Setting the aspect ratio works for 2d plots:
ax = plt.axes()
ax.plot([0,1],[0,10])
ax.set_aspect('equal','box')
But does not for 3d:
ax = plt.axes(projection='3d')
ax.plot([0,1],[0,1],[0,10])
ax.set_aspect('equal','box')
Is there a different syntax for the 3d case, or it's not implemented?
As of matplotlib 3.3.0, Axes3D.set_box_aspect seems to be the recommended approach.
import numpy as np
import matplotlib.pyplot as plt
xs, ys, zs = ...
ax = plt.axes(projection='3d')
ax.set_box_aspect((np.ptp(xs), np.ptp(ys), np.ptp(zs))) # aspect ratio is 1:1:1 in data space
ax.plot(xs, ys, zs)
I didn't try all of these answers, but this kludge did it for me:
def axisEqual3D(ax):
extents = np.array([getattr(ax, 'get_{}lim'.format(dim))() for dim in 'xyz'])
sz = extents[:,1] - extents[:,0]
centers = np.mean(extents, axis=1)
maxsize = max(abs(sz))
r = maxsize/2
for ctr, dim in zip(centers, 'xyz'):
getattr(ax, 'set_{}lim'.format(dim))(ctr - r, ctr + r)
Looks like this feature has since been added so thought I'd add an answer for people who come by this thread in the future like I did:
fig = plt.figure(figsize=plt.figaspect(0.5)*1.5) #Adjusts the aspect ratio and enlarges the figure (text does not enlarge)
ax = fig.add_subplot(projection='3d')
figaspect(0.5) makes the figure twice as wide as it is tall. Then the *1.5 increases the size of the figure. The labels etc won't increase so this is a way to make the graph look less cluttered by the labels.
I think setting the correct "box aspect" is a good solution:
ax.set_box_aspect(aspect = (1,1,1))
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_box_aspect(aspect = (1,1,1))
ax.plot(dataX,dataY,dataZ)
https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.mplot3d.axes3d.Axes3D.html?highlight=3d%20set_box_aspect#mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
If you know the bounds, eg. +-3 centered around (0,0,0), you can add invisible points like this:
import numpy as np
import pylab as pl
from mpl_toolkits.mplot3d import Axes3D
fig = pl.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')
MAX = 3
for direction in (-1, 1):
for point in np.diag(direction * MAX * np.array([1,1,1])):
ax.plot([point[0]], [point[1]], [point[2]], 'w')
If you know the bounds you can also set the aspect ratio this way:
ax.auto_scale_xyz([minbound, maxbound], [minbound, maxbound], [minbound, maxbound])
Another helpful (hopefully) solution when, for example, it is necessary to update an already existing figure:
world_limits = ax.get_w_lims()
ax.set_box_aspect((world_limits[1]-world_limits[0],world_limits[3]-world_limits[2],world_limits[5]-world_limits[4]))
get_w_lims()
set_box_aspect()
My understanding is basically that this isn't implemented yet (see this bug in GitHub). I'm also hoping that it is implemented soon. See This link for a possible solution (I haven't tested it myself).
A follow-up to Matt Panzer's answer. (This was originally a comment on said answer.)
limits = np.array([getattr(ax, f'get_{axis}lim')() for axis in 'xyz'])
ax.set_box_aspect(np.ptp(limits, axis=1))
Now that this pull request has been merged, when the next release of Matplotlib drops, you should be able to just use ax.set_aspect('equal'). I will try to remember and update this answer when that happens.
Update: Matplotlib 3.6 has been released; ax.set_aspect('equal') will now work as expected.
As of matplotlib 3.6.0, this feature has been added with the shortcut
ax.set_aspect('equal'). Other options are 'equalxy', 'equalxz', and 'equalyz', to set only two directions to equal aspect ratios. This changes the data limits, example below.
In the upcoming 3.7.0, you will be able to change the plot box aspect ratios rather than the data limits via the command ax.set_aspect('equal', adjustable='box'). (Thanks to #tfpf on another answer here for implementing that!) To get the original behavior, use adjustable='datalim'.
Matt Panzer's answer worked for me, but it took me a while to figure out an issue I had.
If you're plotting multiple datasets into the same graph, you have to calculate the peak-to-peak values for the entire range of datapoints.
I used the following code to solve it for my case:
x1, y1, z1 = ..., ..., ...
x2, y2, z2 = ..., ..., ...
ax.set_box_aspect((
max(np.ptp(x1), np.ptp(x2)),
max(np.ptp(y1), np.ptp(y2)),
max(np.ptp(z1), np.ptp(y2))
))
ax.plot(x1, y1, z1)
ax.scatter(x2, y2, z2)
Note that this solution is not perfect. It will not work if x1 contains the most negative number and x2 contains the most positive one. Only if either x1 or x2 contains the greatest peak-to-peak range.
If you know numpy better than I do, feel free to edit this answer so it works in a more general case.
I tried several methods, such as ax.set_box_aspect(aspect = (1,1,1)) and it does not work. I want a sphere to show up as a sphere -- not ellipsoid. I wrote this function and tried it on a variety of data. It is a hack and it is not perfect, but pretty close.
def set_aspect_equal(ax):
"""
Fix the 3D graph to have similar scale on all the axes.
Call this after you do all the plot3D, but before show
"""
X = ax.get_xlim3d()
Y = ax.get_ylim3d()
Z = ax.get_zlim3d()
a = [X[1]-X[0],Y[1]-Y[0],Z[1]-Z[0]]
b = np.amax(a)
ax.set_xlim3d(X[0]-(b-a[0])/2,X[1]+(b-a[0])/2)
ax.set_ylim3d(Y[0]-(b-a[1])/2,Y[1]+(b-a[1])/2)
ax.set_zlim3d(Z[0]-(b-a[2])/2,Z[1]+(b-a[2])/2)
ax.set_box_aspect(aspect = (1,1,1))

matplotlib.pyplot, preserve aspect ratio of the plot

Assuming we have a polygon coordinates as polygon = [(x1, y1), (x2, y2), ...], the following code displays the polygon:
import matplotlib.pyplot as plt
plt.fill(*zip(*polygon))
plt.show()
By default it is trying to adjust the aspect ratio so that the polygon (or whatever other diagram) fits inside the window, and automatically changing it so that it fits even after resizing. Which is great in many cases, except when you are trying to estimate visually if the image is distorted. How to fix the aspect ratio to be strictly 1:1?
(Not sure if "aspect ratio" is the right term here, so in case it is not - I need both X and Y axes to have 1:1 scale, so that (0, 1) on both X and Y takes an exact same amount of screen space. And I need to keep it 1:1 no matter how I resize the window.)
Does it help to use:
plt.axis('equal')
'scaled' using plt
The best thing is to use:
plt.axis('scaled')
As Saullo Castro said. Because with equal you can't change one axis limit without changing the other so if you want to fit all non-squared figures you will have a lot of white space.
Equal
Scaled
'equal' using ax
Alternatively, you can use the axes class.
fig = plt.figure()
ax = figure.add_subplot(111)
ax.imshow(image)
ax.axes.set_aspect('equal')
There is, I'm sure, a way to set this directly as part of your plot command, but I don't remember the trick. To do it after the fact you can use the current axis and set it's aspect ratio with "set_aspect('equal')". In your example:
import matplotlib.pyplot as plt
plt.fill(*zip(*polygon))
plt.axes().set_aspect('equal', 'datalim')
plt.show()
I use this all the time and it's from the examples on the matplotlib website.
Better plt.axis('scaling'), it works better if you want to modify the axes with xlim() and ylim().

Categories