Understanding the subs argument in LogLocator - matplotlib

I am trying to understand the subs argument in the LogLocator class, which determines where major / minor ticks should be located on a MatPlotLib graph.
Here is my code:
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import numpy as np
x = np.arange(30)
y = np.power(x, 1.5)
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.set_yscale("log")
ax.plot(x, y)
plt.show()
And this produces the following graph:
However, what I want to do is to have labels on the y-axis for all those ticks, rather than just at 10^0, 10^1, 10^2. I believe that the way to do this is to use a LogLocator, and so I have tried inserting the following into my code:
ax.yaxis.set_major_locator(ticker.LogLocator(base=10, subs=np.arange(2, 10) * 0.1))
The idea here is that it would then show labels on 0.1, 0.2, 0.3 .... X 10^0, 10^1, 10^3 ...
However, instead, the graph appears to have removed all labels entirely:
So what should I be using in the subs argument to get my desired behaviour?

Just add the following line after plt.plot(). You will have a bit of space problem between the minor ticks due to the log spacing. But you can control that via the fontsize of the minor tick labels.
ax.yaxis.set_minor_formatter(FormatStrFormatter("%.1f"))
Output

Related

How can I place the y-axis origin at 0? [duplicate]

I want to draw a figure in matplotib where the axis are displayed within the plot itself not on the side
I have tried the following code from here:
import math
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
a = []
for item in x:
a.append(1/(1+math.exp(-item)))
return a
x = np.arange(-10., 10., 0.2)
sig = sigmoid(x)
plt.plot(x,sig)
plt.show()
The above code displays the figure like this:
What I would like to draw is something as follows (image from Wikipedia)
This question describes a similar problem, but it draws a reference line in the middle but no axis.
One way to do it is using spines:
import math
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
a = []
for item in x:
a.append(1/(1+math.exp(-item)))
return a
x = np.arange(-10., 10., 0.2)
sig = sigmoid(x)
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
# Move left y-axis and bottom x-axis to centre, passing through (0,0)
ax.spines['left'].set_position('center')
ax.spines['bottom'].set_position('center')
# Eliminate upper and right axes
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
# Show ticks in the left and lower axes only
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
plt.plot(x,sig)
plt.show()
shows:
Basically, I want to comment on the accepted answer (but my rep doesn't allow that).
The use of
ax.spines['bottom'].set_position('center')
draws the x-axes such that it intersect the y-axes in its center. In case of asymmetric ylim this means that x-axis passes NOT through y=0. Jblasco's answer has this drawback, the intersect is at y=0.5 (the center between ymin=0.0 and ymax=1.0)
However, the reference plot of the original question has axes that intersect each other at 0.0 (which is somehow conventional or at least common).
To achieve this behaviour,
ax.spines['bottom'].set_position('zero')
has to be used.
See the following example, where 'zero' makes the axes intersect at 0.0 despite asymmetrically ranges in both x and y.
import numpy as np
import matplotlib.pyplot as plt
#data generation
x = np.arange(-10,20,0.2)
y = 1.0/(1.0+np.exp(-x)) # nunpy does the calculation elementwise for you
fig, [ax0, ax1] = plt.subplots(ncols=2, figsize=(8,4))
# Eliminate upper and right axes
ax0.spines['top'].set_visible(False)
ax0.spines['right'].set_visible(False)
# Show ticks on the left and lower axes only
ax0.xaxis.set_tick_params(bottom='on', top='off')
ax0.yaxis.set_tick_params(left='on', right='off')
# Move remaining spines to the center
ax0.set_title('center')
ax0.spines['bottom'].set_position('center') # spine for xaxis
# - will pass through the center of the y-values (which is 0)
ax0.spines['left'].set_position('center') # spine for yaxis
# - will pass through the center of the x-values (which is 5)
ax0.plot(x,y)
# Eliminate upper and right axes
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
# Show ticks on the left and lower axes only (and let them protrude in both directions)
ax1.xaxis.set_tick_params(bottom='on', top='off', direction='inout')
ax1.yaxis.set_tick_params(left='on', right='off', direction='inout')
# Make spines pass through zero of the other axis
ax1.set_title('zero')
ax1.spines['bottom'].set_position('zero')
ax1.spines['left'].set_position('zero')
ax1.set_ylim(-0.4,1.0)
# No ticklabels at zero
ax1.set_xticks([-10,-5,5,10,15,20])
ax1.set_yticks([-0.4,-0.2,0.2,0.4,0.6,0.8,1.0])
ax1.plot(x,y)
plt.show()
Final remark: If ax.spines['bottom'].set_position('zero') is used but zerois not within the plotted y-range, then the axes is shown at the boundary of the plot closer to zero.
The title of this question is how to draw the spine in the middle and the accepted answer does exactly that but what you guys draw is the sigmoid function and that one passes through y=0.5. So I think what you want is the spine centered according to your data. Matplotlib offers the spine position data for that (see documentation)
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x):
return 1 / (1 + np.exp(-x))
sigmoid = np.vectorize(sigmoid) #vectorize function
values=np.linspace(-10, 10) #generate values between -10 and 10
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
#spine placement data centered
ax.spines['left'].set_position(('data', 0.0))
ax.spines['bottom'].set_position(('data', 0.0))
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
plt.plot(values, sigmoid(values))
plt.show()
Looks like this (Github):
You can simply add:
plt.axhline()
plt.axvline()
It's not fixed to the center, but it does the job very easily.
Working example:
import matplotlib.pyplot as plt
import numpy as np
def f(x):
return np.sin(x) / (x/100)
delte = 100
Xs = np.arange(-delte, +delte +1, step=0.01)
Ys = np.array([f(x) for x in Xs])
plt.axhline(color='black', lw=0.5)
plt.axvline(color='black', lw=0.5)
plt.plot(Xs, Ys)
plt.show()
If you use matplotlib >= 3.4.2, you can use Pandas syntax and do it in only one line:
plt.gca().spines[:].set_position('center')
You might find it cleaner to do it in 3 lines:
ax = plt.gca()
ax.spines[['top', 'right']].set_visible(False)
ax.spines[['left', 'bottom']].set_position('center')
See documentation here.
Check your matplotlib version with pip freeze and update it with pip install -U matplotlib.
According to latest MPL Documentation:
ax = plt.axes()
ax.spines.left.set_position('zero')
ax.spines.bottom.set_position('zero')

How to use mode='expand' and center a figure-legend label given only one label entry?

I would like to generate a centered figure legend for subplot(s), for which there is a single label. For my actual use case, the number of subplot(s) is greater than or equal to one; it's possible to have a 2x2 grid of subplots and I would like to use the figure-legend instead of using ax.legend(...) since the same single label entry will apply to each/every subplot.
As a brief and simplified example, consider the code just below:
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(10)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y, color='orange', label='$f(x) = sin(x)$')
fig.subplots_adjust(bottom=0.15)
fig.legend(mode='expand', loc='lower center')
plt.show()
plt.close(fig)
This code will generate the figure seen below:
I would like to use the mode='expand' kwarg to make the legend span the entire width of the subplot(s); however, doing so prevents the label from being centered. As an example, removing this kwarg from the code outputs the following figure.
Is there a way to use both mode='expand' and also have the label be centered (since there is only one label)?
EDIT:
I've tried using the bbox_to_anchor kwargs (as suggested in the docs) as an alternative to mode='expand', but this doesn't work either. One can switch out the fig.legend(...) line for the line below to test for yourself.
fig.legend(loc='lower center', bbox_to_anchor=(0, 0, 1, 0.5))
The handles and labels are flush against the left side of the legend. There is no mechanism to allow for aligning them.
A workaround could be to use 3 columns of legend handles and fill the first and third with a transparent handle.
import numpy as np
import matplotlib.pyplot as plt
x = np.arange(10)
y = np.sin(x)
fig, ax = plt.subplots()
fig.subplots_adjust(bottom=0.15)
line, = ax.plot(x, y, color='orange', label='$f(x) = sin(x)$')
proxy = plt.Rectangle((0,0),1,1, alpha=0)
fig.legend(handles=[proxy, line, proxy], mode='expand', loc='lower center', ncol=3)
plt.show()

Matplotlib log colorbar minor ticks disappear when range is less than a decade

My users sometimes wish to see log scaling of the values of a 2-d plot, even though the data spans less than one decade. I'm able to make plots using 'pcolormesh' or 'imshow' using the
norm=LogNorm(vmin=minimum,vmax=maximum)
parameter and accurately show log scaled 'intensity' values. I would like the 'colorbar' to show some minor ticks and tick labels, but when minimum and maximum span less than a decade, no matter what I do there is only one tick value displayed. I tried the suggestion in this SO posting:
Minor ticks in matplotlib's colorbar
As adapted in the following snippet:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
# fill grid
x = np.linspace(1,10,10)
y = np.linspace(1,10,10)
X, Y = np.meshgrid(x,y)
Z = np.abs(X/10 + Y/10)
# plot
f, ax = plt.subplots()
p = plt.pcolormesh(X, Y, Z, norm=LogNorm(), vmin=2e-1, vmax=1)
cb = plt.colorbar(p, ax=ax)
cb.ax.minorticks_on()
plt.show()
But there are no minor ticks, labeled or otherwise:
I have also tried the following:
from matplotlib.ticker import LogFormatterMathtext
from matplotlib.ticker import LogLocator
from matplotlib.ticker import LogFormatter
import numpy as nmp
import matplotlib.pyplot as pyp
'''
<snip>
'''
ccbb=pyp.colorbar(label='ohms')
ccbb.ax.yaxis.set_minor_locator(LogLocator(subs=nmp.arange(2,10)))
# AND/OR
# ccbb.ax.yaxis.set_minor_locator(LogLocator(subs=[0.2,0.5,1.0]))
ccbb.ax.yaxis.set_minor_formatter(LogFormatterMathtext())
ccbb.update_ticks()
'''
<snip>
'''
And several other things, which I haven't saved. All of which yield the same result with the colorbar missing any but the single decade tick / label. The documentation for the ticker class is pretty impenetrable:
http://matplotlib.org/api/ticker_api.html
Especially the following statement about LogFormatter parameter labelOnlyBase:
"base is used to locate the decade tick, which will be the only one to be labeled if labelOnlyBase is False" Neither False nor True cause more than the base to be ticked, I suppose that's because this refers to the Major ticks, But why in the world can't I get the minor ticks or labels??
Any advice would be appreciated.
Matplotlib colorbars don't seem to do minor ticks in log scale. Using the method in this answer works, though it's a bit inconvenient - one day this will be automatic, but for now you have to organise the minor tick values by hand (np.arange(2, 10)/10. in this case, but you'd have to append np.arange(2, 10) if your values went up to 10)
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
# fill grid
x = np.linspace(1,10,10)
y = np.linspace(1,10,10)
X, Y = np.meshgrid(x,y)
# Z = np.abs(X + Y)
Z = np.abs(X/10 + Y/10)
# plot
f, ax = plt.subplots()
p = plt.pcolormesh(X, Y, Z, norm=LogNorm(), vmin=2e-1, vmax=1)
cb = plt.colorbar(p, ax=ax)
# cb.ax.minorticks_on()
# We need to nomalize the tick locations so that they're in the range from 0-1...
minorticks = p.norm(np.arange(2, 10)/10.)
cb.ax.yaxis.set_ticks(minorticks, minor=True)
plt.show()
The minorticks_on() method wasn't doing anything, so I commented it out.

Minor ticks on logarithmic axis in Matplotlib

I have a scatter plot with logarithmic x- and y axis (because I'm mainly interested in the lower values along both).
However, I want the tick labels to be in decimal format, not as 10^x.
I'm using this:
# axis limits:
ax.set_xlim(xmin=0, xmax = 1.2)
ax.set_ylim(ymin=0,ymax=1000)
# log scales:
ax.set_yscale('log')
ax.set_xscale('log')
# set y-ticks:
ax.set_yticks((1,10,100,1000))
ax.set_yticklabels(("1","10","100","1000"))
This works (though introducing ax.set_yscale('log') or ax.set_xscale('log') brings up the following warning (any idea what's up with that?):
Warning (from warnings module):
File "C:\Python27\lib\site-packages\numpy\ma\core.py", line 3785
warnings.warn("Warning: converting a masked element to nan.")
UserWarning: Warning: converting a masked element to nan.
But when I try the same for the x-axis, I get a MaskError:
# set x-ticks:
ax.set_xticks((0, 0.2, 0.4, 0.8, 1))
ax.set_xticklabels(("0", "0.2", "0.4", "0.8", "1"))
[snip long long traceback]
File "C:\Python27\lib\site-packages\numpy\ma\core.py", line 3795, in __int__
raise MaskError, 'Cannot convert masked element to a Python int.'
MaskError: Cannot convert masked element to a Python int.
I think it has something to do with minor vs major ticks. I have tried to play around with ticker, but always run into the same error in the end.
I'd be immensely grateful for any help!
Edit after answer:
Problem solved by replacing
ax.set_yticks((1,10,100,1000))
ax.set_yticklabels(("1","10","100","1000"))
ax.set_xticks((0, 0.2, 0.4, 0.8, 1))
ax.set_xticklabels(("0", "0.2", "0.4", "0.8", "1"))
with
ax.yaxis.set_major_formatter(FormatStrFormatter('%1.0f'))
ax.xaxis.set_major_formatter(FormatStrFormatter('%.1f'))
ax.xaxis.set_minor_formatter(FormatStrFormatter('%.1f'))
You can use ticker as:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import FormatStrFormatter
fig = plt.figure(1, [5,4])
ax = fig.add_subplot(111)
ax.plot( range(1,100) , range(1,100) , color='#aaaaff')
ax.set_xscale('log')
ax.xaxis.set_major_formatter(FormatStrFormatter('%.03f'))
plt.show()

matplotlib: Stretch image to cover the whole figure

I am quite used to working with matlab and now trying to make the shift matplotlib and numpy. Is there a way in matplotlib that an image you are plotting occupies the whole figure window.
import numpy as np
import matplotlib.pyplot as plt
# get image im as nparray
# ........
plt.figure()
plt.imshow(im)
plt.set_cmap('hot')
plt.savefig("frame.png")
I want the image to maintain its aspect ratio and scale to the size of the figure ... so when I do savefig it exactly the same size as the input figure, and it is completely covered by the image.
Thanks.
I did this using the following snippet.
#!/usr/bin/env python
import numpy as np
import matplotlib.cm as cm
import matplotlib.mlab as mlab
import matplotlib.pyplot as plt
from pylab import *
delta = 0.025
x = y = np.arange(-3.0, 3.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = mlab.bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
Z2 = mlab.bivariate_normal(X, Y, 1.5, 0.5, 1, 1)
Z = Z2-Z1 # difference of Gaussians
ax = Axes(plt.gcf(),[0,0,1,1],yticks=[],xticks=[],frame_on=False)
plt.gcf().delaxes(plt.gca())
plt.gcf().add_axes(ax)
im = plt.imshow(Z, cmap=cm.gray)
plt.show()
Note the grey border on the sides is related to the aspect rario of the Axes which is altered by setting aspect='equal', or aspect='auto' or your ratio.
Also as mentioned by Zhenya in the comments Similar StackOverflow Question
mentions the parameters to savefig of bbox_inches='tight' and pad_inches=-1 or pad_inches=0
You can use a function like the one below.
It calculates the needed size for the figure (in inches) according to the resolution in dpi you want.
import numpy as np
import matplotlib.pyplot as plt
def plot_im(image, dpi=80):
px,py = im.shape # depending of your matplotlib.rc you may
have to use py,px instead
#px,py = im[:,:,0].shape # if image has a (x,y,z) shape
size = (py/np.float(dpi), px/np.float(dpi)) # note the np.float()
fig = plt.figure(figsize=size, dpi=dpi)
ax = fig.add_axes([0, 0, 1, 1])
# Customize the axis
# remove top and right spines
ax.spines['right'].set_color('none')
ax.spines['left'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
# turn off ticks
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('none')
ax.xaxis.set_ticklabels([])
ax.yaxis.set_ticklabels([])
ax.imshow(im)
plt.show()
Here's a minimal object-oriented solution:
fig = plt.figure(figsize=(8, 8))
ax = fig.add_axes([0, 0, 1, 1], frameon=False, xticks=[], yticks=[])
Testing it out with
ax.imshow([[0]])
fig.savefig('test.png')
saves out a uniform purple block.
edit: As #duhaime points out below, this requires the figure to have the same aspect as the axes.
If you'd like the axes to resize to the figure, add aspect='auto' to imshow.
If you'd like the figure to resize to be resized to the axes, add
from matplotlib import tight_bbox
bbox = fig.get_tightbbox(fig.canvas.get_renderer())
tight_bbox.adjust_bbox(fig, bbox, fig.canvas.fixed_dpi)
after the imshow call. This is the important bit of matplotlib's tight_layout functionality which is implicitly called by things like Jupyter's renderer.