Matplotlib dual x-axis logarithmic ticks - python

I've set the upper x-axis manually (the conversion is 1.218x the values on the lower x-axis) and I'd like the upper minor logarithmic ticks to move up the scale by 1.218x too. Any suggestions?
fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twiny()
X = np.linspace(0,10000000, 1000000)
ax1.semilogx(X, zero, label='$\mathbb{P} (X=0)$')
ax1.semilogx(X, one, label='$\mathbb{P} (X=1)$')
ax1.semilogx(X, two+more, label='$\mathbb{P} (X\geq2)$')
ax1.set_xlabel(r"Particle Concentration m$^{-3}$")
ax1.set_ylabel(r"Probability of occurrence")
ax1.legend(loc=6)
ax1.grid()
ax2.semilogx(X, one, label='one', alpha = 0)
ax2.set_xlim(ax1.get_xlim())
ax2.set_xticks([12.18323586744639, 121.8323586744639, 1218.323586744639, 12183.23586744639, 121832.3586744639, 1218323.586744639])
ax2.set_xticklabels(['10$^1$','10$^2$','10$^3$','10$^4$','10$^5$','10$^6$'])
ax2.set_xlabel(r"Disdrometer Particle Count (min$^{-1}$)")
plt.show()

EDIT: Rewriting after your comment below.
There will be a way to move the minor ticks but actually I think your approach here is misguided. Looking close you aren't using ax2 to plot anything: you just want it as an alternative scale. You are messing with the ticks and ticklabels when as a way of faking changing the limits. It would be much easier to just change that limits (so that matplotlib can handle the ticks etc automatically).
Replace your code above with
ax2.set_xscale("log", nonposx='clip')
ax2.set_xlim(np.array(ax1.get_xlim())/1.218)

Related

How to force and edit major and minor log plot ticks of pyplot subplot

When I'm plotting my data, I want to be able to control the major and minor ticks of my subplots. However, whatever I try I seem to be unable modify the ticks of my second subplot. I've tried applying the advice from other stackoverflow questions, unfortunately to no avail. I think I'm doing something fundamentally wrong when constructing my boxplots.
As none of my colleagues have much experience with matplotlib, I'm turning to you stackoverflow! Any help would be greatly appreciated!
Currently, my figures look like this:
On the second boxplot, I also want to force a major tick on every 10^xth and show the default log minor ticks.
Currently, I generate my boxplots as follows:
def generateLogBoxPlot(title, plot_id, xlabels, data, initializion_time, fig):
# Create an axes instance
ax = fig.add_subplot(plot_id)
# Set Log Scale
ax.set_yscale('log')
# Create the boxplot
bp = ax.boxplot(data_of_plot)
# Show Initialization Time (as a Line)
line = plt.plot(...)
# Custom rotated x-axis labels
ax.set_xticklabels(xlabels)
plt.xticks(rotation=15)
#Set Labels
plt.ylabel('Time (ms)')
ax.set_title(title)
#Show Grid
ax.get_yaxis().grid()
And I call this function like this:
# Create a figure instance
fig = plt.figure(1, figsize=(9, 3))
# Generate first subplot
generateLogBoxPlot("No Context-Dependent A.C.\nBusiness Case", #title
121, #plot_id
["Add User", "Add Role", "Add Demarcation", "Add Permission"], #labels
results["AddEntities"], #data
40000, #initializion_time
fig) #figure
line = generateLogBoxPlot("Context-Dependent A.C.\nBusiness Case",
122, #plot_id
["Add User", "Add Role", "Add Demarcation", "Add Permission"], #labels
results["AddEntities2"], #data
153000, #initialization_time
fig) #figure
#Show Legend
plt.legend(plt.plot([], [],linestyle="--", color="#A9A9A9", label="Initialization
Time"),["Initialization Time"], loc='center left', bbox_to_anchor=(1, 0.5))
#Show
plt.tight_layout()
plt.show()
Whatever I try, I only seem to be able to modify the ticks of the fist subplot. How could I force/edit them on the second subbplot?
Matplotlib automatically shows or hides minor ticks of log scales depending on the range of values and to some extent the figure size as well. With regards to a y-axis base 10 log scale, here is what I have noticed from testing variations of the example shown further below (using matplotlib 3.3.2 with default settings):
For a figure height of 4 inches (default) or more: when the range of the y-axis covers 9 integer powers or more, the log scale switches from showing major ticks with labels at every power integer as well as all minor tick marks to showing major ticks every two (or more) power integers with no minor ticks (like in your plot on the right).
For a figure height of less than 4 inches (which seems to be your case): there is a more flexible adjustment of the ticks based on the range of the y-axis and the space available.
For your particular example, I would in any case start off by sharing the y-axis to make the plots more comparable. That then leaves two options: either leave the default tick formatting as it is and make do with no minor ticks or else force minor ticks for both plots.
Examples of matplotlib default log tick behavior and how to change it
First, here is an illustration of the matplotlib default behavior with log ticks:
import numpy as np # v 1.19.2
import matplotlib.pyplot as plt # v 3.3.2
import matplotlib.ticker as ticker
# Create sample data with exponentially increasing values for x and
# the y functions and where the y functions are nearly identical
x = 10**np.linspace(0, 3, 30)
y1 = x**2
y2 = x**2.5
# Create figure and subplots containing semilogy plots
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 3))
fig.suptitle('Matplotlib defaults with figure height of 3 inches', y=1.25)
ax1.set_title('Minor ticks are shown\n\n', pad=10)
ax1.semilogy(x, y1, label='y1')
ax1.legend(loc='lower right')
ax2.set_title(f'Minor ticks are not shown:\nthe y range covers less than 9 \
integer\npowers, but the figure height is <4 inches', pad=10)
ax2.semilogy(x, y2, label='y2')
ax2.legend(loc='lower right')
plt.show()
Now, what if the figure height is increased to make more space for the ticks?
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
fig.suptitle('Figure height of 4 inches', y=1.15)
ax1.set_title('Minor ticks are not shown:\nthe custom y-axis limits causes the\
\ny range to cover 9 integer powers', pad=10)
ax1.semilogy(x, y1, label='y1')
ax1.legend(loc='lower right')
ax1.set_ylim(1, 10**8) # the minor ticks disappear by changing the axis limits
ax2.set_title('Minor ticks are shown by default:\nthe y range covers less \
than 9 integer\npowers and the figure height is 4 inches', pad=10)
ax2.semilogy(x, y2, label='y2')
ax2.legend(loc='lower right')
plt.show()
This particular example shows that increasing the figure size can solve the problem of minor ticks not showing, but this may often not be the case.
Here is how to force minor ticks to be shown whatever the range of y and the figure size, by using the LogLocator from the matplotlib.ticker module (this example also includes a shared y-axis):
# Add sharey=True
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 3), sharey=True)
fig.suptitle('Figure height of 3 inches with\ncustomized tick locator and shared \
y-axis', y=1.25)
ax1.set_title('Ticks shared by ax2\n', pad=10)
ax1.semilogy(x, y1, label='y1')
ax1.legend(loc='lower right')
ax2.set_title('Customized LogLocator:\nminor ticks are forced to be shown', pad=10)
ax2.semilogy(x, y2, label='y2')
ax2.legend(loc='lower right')
# Set ax2 major and minor tick locators with custom parameters to show
# all major and minor ticks despite the small figure height and the large
# range of y: the numticks argument must be an arbitrary number at least
# one unit above the number of integer powers covering the range of y
nticks = 9
maj_loc = ticker.LogLocator(numticks=nticks)
min_loc = ticker.LogLocator(subs='all', numticks=nticks)
ax2.yaxis.set_major_locator(maj_loc)
ax2.yaxis.set_minor_locator(min_loc)
# The tick labels are formatted as one would expect, so no need to use
# log tick formatters for this example.
plt.show()
If you want to create a plotting function that always shows minor ticks for any range of values, you need to set numticks at a high value.
References: answer by ImportanceOfBeingErnest, matplotlib Log Demo, matplotlib Tick Locators

Is there a way to make the legend in matplotlib fit better within the plot?

I need to plot a line plot with multiple lines.
Due to a large number of lines, the legend gets so large that it hides some of the lines, is there a way to automatically set the y ticks so that there will be enough room in the plot for the legend to fit properly?
Example plot:
Thank you.
You can adjust the xlim and the dict prop, for example
x = np.linspace(1,10,10)
y = x + np.random.rand(10,10)
labels = list('abcdefghij')
fig,ax = plt.subplots(figsize=(12,6))
ax.plot(x,y,'-o')
ax.set_xlim(1,11)
ax.legend(labels,loc='upper right', prop={'size': 10})
Tuning ax.set_xlim to leave enough space for the legend, and the size of the legend is controlled by prop={'size':10}

ax.locator_params(nbins=k) does not work in matplotlib

I have this simple piece of code where I try to plot simple graph while limiting number of x ticks. There are hundreds of items in iters variable and if they get plotted it would just create one fat black line.
However, ax.locator_params does not work and the number of ticks aren't reduced.
I have tried setting it on plt object, but no help.
I also tried specifying x and y axes in locator_params, but no help as well.
Finally, I have tried moving ax.locator_params before and after ax.plot, but nothing seemed to help. I am completely out of ideas.
fig, ax = plt.subplots(1, 1, figsize=(20,10))
ax.locator_params(tight=True, nbins=4)
ax.plot(iters, vals)
plt.xticks(rotation=30)
plt.show()
locator_params() with nbins= is only supported for numerical axes where the tick positions are set via MaxNLocator.
To get the same effect with text ticks, the current ticks can be stored in a list (get_xticks) and then be replaced by a subset. Note that changes to ticks (and to limits) should be called after the main plot functions.
xticks = ax.get_xticks()
ax.set_xticks(xticks[::len(xticks) // 4]) # set new tick positions
ax.tick_params(axis='x', rotation=30) # set tick rotation
ax.margins(x=0) # set tight margins

Python Matplotlib: Dual y-axis with same tick spacing and different scale [duplicate]

I created a matplotlib plot that has 2 y-axes. The y-axes have different scales, but I want the ticks and grid to be aligned. I am pulling the data from excel files, so there is no way to know the max limits beforehand. I have tried the following code.
# creates double-y axis
ax2 = ax1.twinx()
locs = ax1.yaxis.get_ticklocs()
ax2.set_yticks(locs)
The problem now is that the ticks on ax2 do not have labels anymore. Can anyone give me a good way to align ticks with different scales?
Aligning the tick locations of two different scales would mean to give up on the nice automatic tick locator and set the ticks to the same positions on the secondary axes as on the original one.
The idea is to establish a relation between the two axes scales using a function and set the ticks of the second axes at the positions of those of the first.
import matplotlib.pyplot as plt
import matplotlib.ticker
fig, ax = plt.subplots()
# creates double-y axis
ax2 = ax.twinx()
ax.plot(range(5), [1,2,3,4,5])
ax2.plot(range(6), [13,17,14,13,16,12])
ax.grid()
l = ax.get_ylim()
l2 = ax2.get_ylim()
f = lambda x : l2[0]+(x-l[0])/(l[1]-l[0])*(l2[1]-l2[0])
ticks = f(ax.get_yticks())
ax2.yaxis.set_major_locator(matplotlib.ticker.FixedLocator(ticks))
plt.show()
Note that this is a solution for the general case and it might result in totally unreadable labels depeding on the use case. If you happen to have more a priori information on the axes range, better solutions may be possible.
Also see this question for a case where automatic tick locations of the first axes is sacrificed for an easier setting of the secondary axes tick locations.
To anyone who's wondering (and for my future reference), the lambda function f in ImportanceofBeingErnest's answer maps the input left tick to a corresponding right tick through:
RHS tick = Bottom RHS tick + (% of LHS range traversed * RHS range)
Refer to this question on tick formatting to truncate decimal places:
from matplotlib.ticker import FormatStrFormatter
ax2.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) # ax2 is the RHS y-axis

How to set number of ticks in plt.colorbar?

When I plot a matrix with a colorbar, then the colorbar has 10 ticks. Since the colorbar has to be pretty small, the ticklabels overlap. Therefore I want to reduce the number of ticks from 10 to 5. I do not want to reduce the font size!
Is there an easy way to do this? I do not want to set the ticks manually...
The MaxNLocator ticker might suit your purposes?
class matplotlib.ticker.MaxNLocator
Select no more than N intervals at nice locations
For example:
from matplotlib import ticker
# (generate plot here)
cb = plt.colorbar()
tick_locator = ticker.MaxNLocator(nbins=5)
cb.locator = tick_locator
cb.update_ticks()
plt.show()
For the record, this is now possible also via:
cbar = plt.colorbar()
cbar.ax.locator_params(nbins=5)
which talks to ticker.MaxNLocator.

Categories