99

My python script uses matplotlib to plot a 2D "heat map" of an x, y, z dataset. My x- and y-values represent amino acid residues in a protein and can therefore only be integers. When I zoom into the plot, it looks like this:

2D heat map with float tick marks

As I said, float values on the x-y axes do not make sense with my data and I therefore want it to look like this: enter image description here

Any ideas how to achieve this? This is the code that generates the plot:

def plotDistanceMap(self):
    # Read on x,y,z
    x = self.currentGraph['xData']
    y = self.currentGraph['yData']
    X, Y = numpy.meshgrid(x, y)
    Z = self.currentGraph['zData']
    # Define colormap
    cmap = colors.ListedColormap(['blue', 'green', 'orange', 'red'])
    cmap.set_under('white')
    cmap.set_over('white')
    bounds = [1,15,50,80,100]
    norm = colors.BoundaryNorm(bounds, cmap.N)
    # Draw surface plot
    img = self.axes.pcolor(X, Y, Z, cmap=cmap, norm=norm)
    self.axes.set_xlim(x.min(), x.max())
    self.axes.set_ylim(y.min(), y.max())
    self.axes.set_xlabel(self.currentGraph['xTitle'])
    self.axes.set_ylabel(self.currentGraph['yTitle'])
    # Cosmetics
    #matplotlib.rcParams.update({'font.size': 12})
    xminorLocator = MultipleLocator(10)
    yminorLocator = MultipleLocator(10)
    self.axes.xaxis.set_minor_locator(xminorLocator)
    self.axes.yaxis.set_minor_locator(yminorLocator)
    self.axes.tick_params(direction='out', length=6, width=1)
    self.axes.tick_params(which='minor', direction='out', length=3, width=1)
    self.axes.xaxis.labelpad = 15
    self.axes.yaxis.labelpad = 15
    # Draw colorbar
    colorbar = self.figure.colorbar(img, boundaries = [0,1,15,50,80,100], 
                                    spacing = 'proportional',
                                    ticks = [15,50,80,100], 
                                    extend = 'both')
    colorbar.ax.set_xlabel('Angstrom')
    colorbar.ax.xaxis.set_label_position('top')
    colorbar.ax.xaxis.labelpad = 20
    self.figure.tight_layout()      
    self.canvas.draw()
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
gha
  • 3,459
  • 3
  • 18
  • 13

5 Answers5

178

This should be simpler:

(from https://scivision.co/matplotlib-force-integer-labeling-of-axis/)

import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
#...
ax = plt.figure().gca()
#...
ax.xaxis.set_major_locator(MaxNLocator(integer=True))

Read the official docs: https://matplotlib.org/stable/api/ticker_api.html#matplotlib.ticker.MaxNLocator

Jesse H.
  • 465
  • 4
  • 12
Cédric Van Rompay
  • 2,489
  • 1
  • 18
  • 24
  • 2
    It does not put the tick label to the middle point of the given region for me (as in the question's sample image), and every second (unlabeled) tick is missing. Is there a simple trick to achieve those? The '2' should be in the position of 2.5, not at 2.0. – Andris Jul 07 '16 at 11:57
  • 1
    @Andris wow about this I have strictly no idea. But it seems weird anyway to tweak the plotting library for this task, you should probably modify your data instead. The sample image you are referring to seems to have been edited with paint or something like that so it does not mean such output is possible with matplotlib. – Cédric Van Rompay Jul 09 '16 at 08:28
  • 5
    This gives very weird results for me. It turns ticks [0, 2, 4, 8, 10] into [0.0, 1.5, 3.0, 4.5, 7.5, 9.0, 10.5]. Given what the [docs](http://matplotlib.org/api/ticker_api.html#matplotlib.ticker.MaxNLocator) say this should not happen. – AnnanFay Mar 15 '17 at 06:40
  • 7
    +1 for your answer, but -1 for the horrible API! `plt.axes().xaxis.set_major_locator(MaxNLocator(integer=True))` worked for me. – PatrickT Jul 23 '21 at 00:37
  • 1
    If `plt.subplot` is used you can also do the following: `fig, ax = plt.subplots(figsize =(12, 4))` then you can use ax again: `ax.yaxis.set_major_locator(MaxNLocator(integer=True))` – Johannes Jun 29 '22 at 12:20
  • A more direct link to the docs may benefit some: https://matplotlib.org/stable/api/ticker_api.html#matplotlib.ticker.MaxNLocator – Jesse H. Dec 14 '22 at 19:52
  • You might need to add bins=10 or the like to force all integers to be ticked – pas-calc Feb 14 '23 at 14:48
5

The following solution by simply casting the index i to string worked for me:

    import matplotlib.pyplot as plt
    import time

    datay = [1,6,8,4] # Just an example
    datax = []
    
    # In the following for loop datax in the end will have the same size of datay, 
    # can be changed by replacing the range with wathever you need
    for i in range(len(datay)):
        # In the following assignment statement every value in the datax 
        # list will be set as a string, this solves the floating point issue
        datax += [str(1 + i)]

    a = plt

    # The plot function sets the datax content as the x ticks, the datay values
    # are used as the actual values to plot
    a.plot(datax, datay)

    a.show()
Mister Noob
  • 51
  • 1
  • 2
  • Hello and welcome to SO! What does `datax` correspond to? You have not shared code to set the tick labels so it is not clear if `datax` is a list of labels for the x ticks or for the y ticks (corresponding to the `datay` variable). I do not see how this would work if, for example, OP's x variable ranges from 1 to 15. I suggest you make your answer workable for any range of values like is the case of the currently accepted answer. I refrain from downvoting this answer for now seeing as you are a new user, but I encourage you to improve it as soon as you can. – Patrick FitzGerald Feb 15 '21 at 17:01
  • Thanks for the advice! My bad, i answered hastly, let me know if the answer is ok now! – Mister Noob Feb 19 '21 at 16:13
  • 2
    to convert list of integers to list of str => `list(map(str, list_int))` – ibilgen Mar 08 '21 at 20:24
  • To construct the `list` of `str`: ``datax = [str(i + 1) for i in range(len(datay))]`` – jolvi May 10 '22 at 17:34
5
ax.set_xticks([2,3])
ax.set_yticks([2,3])
LBoss
  • 496
  • 6
  • 15
  • 2
    @Andris’ [answer from six years ago](https://stackoverflow.com/a/30916505/3025856) offers the same basic recommendation, but with a lot of additional code for context. – Jeremy Caney Feb 27 '21 at 17:51
2

Based on an answer for modifying tick labels I came up with a solution, don't know whether it will work in your case as your code snippet can't be executed in itself.

The idea is to force the tick labels to a .5 spacing, then replace every .5 tick with its integer counterpart, and others with an empty string.

import numpy
import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(1,2)

x1, x2 = 1, 5
y1, y2 = 3, 7

# first axis: ticks spaced at 0.5
ax1.plot([x1, x2], [y1, y2])
ax1.set_xticks(numpy.arange(x1-1, x2+1, 0.5))
ax1.set_yticks(numpy.arange(y1-1, y2+1, 0.5))

# second axis: tick labels will be replaced
ax2.plot([x1, x2], [y1, y2])
ax2.set_xticks(numpy.arange(x1-1, x2+1, 0.5))
ax2.set_yticks(numpy.arange(y1-1, y2+1, 0.5))

# We need to draw the canvas, otherwise the labels won't be positioned and 
# won't have values yet.
fig.canvas.draw()

# new x ticks  '1'->'', '1.5'->'1', '2'->'', '2.5'->'2' etc.
labels = [item.get_text() for item in ax2.get_xticklabels()]
new_labels = [ "%d" % int(float(l)) if '.5' in l else '' for l in labels]
ax2.set_xticklabels(new_labels)

# new y ticks
labels = [item.get_text() for item in ax2.get_yticklabels()]
new_labels = [ "%d" % int(float(l)) if '.5' in l else '' for l in labels]
ax2.set_yticklabels(new_labels)

fig.canvas.draw()
plt.show()

If you want to zoom out a lot, that will need some extra care, as this one produces a very dense set of tick labels then.

Community
  • 1
  • 1
Andris
  • 921
  • 7
  • 14
2

An alternative to MaxNLocator is matplotlib.ticker.MultipleLocator. By default it outputs only integer values.

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as tck

x = np.linspace(-5, 5, 90)
y = np.sinc(x)

fig, ax = plt.subplots(figsize=(6, 2), layout='constrained')
ax.xaxis.set_major_locator(tck.MultipleLocator())
ax.set_xlabel('x')
ax.plot(x, y)

enter image description here

If you need only one out of n ticks, then just provide this number to MultipleLocator, e.g. MultipleLocator(3):

enter image description here

Actually you can set the base to any real number, not exclusively integers.


Comparing to MaxNLocator, MultipleLocator ensures having integer ticks, while using integer=True in MaxNLocator has a limitation:

integer: bool, default False. If True, ticks will take only integer values, provided at least min_n_ticks integers are found within the view limits.

Thus MaxNLocator can produce this as well:

enter image description here

mins
  • 6,478
  • 12
  • 56
  • 75
  • 2
    Though this answer is similar to another one that uses `MaxNLocator`, I find the extra context in this answer to be a nice enhancement. Thanks. – Mepix Apr 25 '23 at 19:24