11

At the moment if I set matplotlib y axis ticklabels to scientific mode it gives me an exponent at the top of the y axis of the form 1e-5

I'd like to adjust this to read r'$\mathregular{10^{-5}}$' so that it prints out nicely.

Here's my example code:

# Create a figure and axis
fig, ax = plt.subplots()

# Plot 100 random points 
# the y values of which are very small
ax.scatter(np.random.rand(100), np.random.rand(100)/100000.0)

# Set the y limits appropriately
ax.set_ylim(0, 1/100000.0)

# Change the y ticklabel format to scientific format
ax.ticklabel_format(axis='y', style='sci', scilimits=(-2, 2))

# Get the offset value
offset = ax.yaxis.get_offset_text()

# Print it out
print '1st offset printout: {}'.format(offset)

# Run plt.tight_layout()
plt.tight_layout()

# Print out offset again - you can see the value now!
print '2nd offset printout: {}'.format(offset)

# Change it to latex format
offset.set_text(r'$\mathregular{10^{-5}}$')

# Print it out
print '3rd offset printout: {}'.format(offset)

# Add some text to the middle of the figure just to 
# check that it isn't the latex format that's the problem
ax.text(0.5, 0.5/100000.0, r'$\mathregular{10^{-2}}$')

# And show the figure
plt.show()

My output looks like this:

1st offset printout: Text(0,0.5,u'')
2nd offset printout: Text(0,636.933,u'1e\u22125')
3rd offset printout: Text(0,636.933,u'$\\mathregular{10^{-5}}$')

enter image description here

You can find the code and output figure here.

There are two oddities: One is that I can't overwrite the 1e-5 at the top of the y axis (which is the goal), and the other is that I have to run plt.tight_layout() in order to even see that unicode value as the offset.

Can anyone tell me where I'm going wrong?

Thank you

EDIT: The original question didn't make clear that I'd like to automatically detect the exponent as is currently calculated by ticklabel_format. So instead of passing a set string to the offset text it should automatically detect that value and adjust the latex string accordingly.

KirstieJane
  • 598
  • 1
  • 4
  • 12

5 Answers5

7

Building on @edsmith's answer one possible work around which does what I'd like is to get the offset text, convert it to a latex string, turn off the offset and add in that string at the top of the axis.

def format_exponent(ax, axis='y'):

    # Change the ticklabel format to scientific format
    ax.ticklabel_format(axis=axis, style='sci', scilimits=(-2, 2))

    # Get the appropriate axis
    if axis == 'y':
        ax_axis = ax.yaxis
        x_pos = 0.0
        y_pos = 1.0
        horizontalalignment='left'
        verticalalignment='bottom'
    else:
        ax_axis = ax.xaxis
        x_pos = 1.0
        y_pos = -0.05
        horizontalalignment='right'
        verticalalignment='top'

    # Run plt.tight_layout() because otherwise the offset text doesn't update
    plt.tight_layout()
    ##### THIS IS A BUG 
    ##### Well, at least it's sub-optimal because you might not
    ##### want to use tight_layout(). If anyone has a better way of 
    ##### ensuring the offset text is updated appropriately
    ##### please comment!

    # Get the offset value
    offset = ax_axis.get_offset_text().get_text()

    if len(offset) > 0:
        # Get that exponent value and change it into latex format
        minus_sign = u'\u2212'
        expo = np.float(offset.replace(minus_sign, '-').split('e')[-1])
        offset_text = r'x$\mathregular{10^{%d}}$' %expo

        # Turn off the offset text that's calculated automatically
        ax_axis.offsetText.set_visible(False)

        # Add in a text box at the top of the y axis
        ax.text(x_pos, y_pos, offset_text, transform=ax.transAxes,
               horizontalalignment=horizontalalignment,
               verticalalignment=verticalalignment)
    return ax

Note that you should be able to use the position of the offset text by calling pos = ax_axis.get_offset_text().get_position() but these values are not in axis units (they're likely pixel units - thanks @EdSmith - and thus not very helpful). Therefore I've just set the x_pos and y_pos values according to whichever axis we're looking at.

I also wrote a little function to automatically detect appropriate x and y limits (even though I know that matplotlib has lots of fancy ways of doing this).

def get_min_max(x, pad=0.05):
    '''
    Find min and max values such that
    all the data lies within 90% of
    of the axis range
    '''
    r = np.max(x) - np.min(x)
    x_min = np.min(x) - pad * r
    x_max = np.max(x) + pad * r
    return x_min, x_max

So, to update my example from the question (with a slight change to make both axes need the exponent):

import matplotlib.pylab as plt
import numpy as np

# Create a figure and axis
fig, ax = plt.subplots()

# Plot 100 random points that are very small
x = np.random.rand(100)/100000.0
y = np.random.rand(100)/100000.0
ax.scatter(x, y)

# Set the x and y limits
x_min, x_max = get_min_max(x)
ax.set_xlim(x_min, x_max)
y_min, y_max = get_min_max(y)    
ax.set_ylim(y_min, y_max)

# Format the exponents nicely
ax = format_exponent(ax, axis='x')
ax = format_exponent(ax, axis='y')

# And show the figure
plt.show()

enter image description here

A gist with an ipython notebook showing the output of the code is available here.

I hope that helps!

KirstieJane
  • 598
  • 1
  • 4
  • 12
  • 1
    Very neat. I assume this is a bug in matplotlib as you should be able to replace the `offsettext` string directly. Your description is a little confusing -- you don't add in that string but turn off `offsettext` and put the latex label in the same place. Ideally the replacement text would be based on the location of `ax_axis.get_offset_text().get_position()`. I couldn't find a way to do this as `pos` is in pixel units (I think), did you? – Ed Smith Jul 22 '15 at 09:40
  • You're right - I'll edit my reference to `pos` in the answer now. I couldn't figure out what to do with those numbers (in fact - it didn't occur to me that they were pixel coordinates - good guess!). Do you have any suggestions on how to get the values to update without doing plt.tight_layout()? – KirstieJane Jul 23 '15 at 10:46
  • @KirstieJane I would like to point out that I had a similar question, and just now found a [slightly better solution](https://stackoverflow.com/a/62468524/5472354). – mapf Jun 19 '20 at 10:49
7

It seems that plt.ticklabel_format does not work correctly. However if you define the ScalarFormatter yourself and set the limits for scientific notation to the formatter, you can get the offset automatically in the mathtext format like so:

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

x = np.linspace(3,5)
y = np.sin(np.linspace(0,6*np.pi))*1e5

plt.plot(x,y)

mf = matplotlib.ticker.ScalarFormatter(useMathText=True)
mf.set_powerlimits((-2,2))
plt.gca().yaxis.set_major_formatter(mf)

plt.show()

enter image description here

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712
3

You get offset and set the text value but there doesn't seem to be a way to actually apply this to the axis... Even calling ax.yaxis.offsetText.set_text(offset) doesn't update the offset displayed. A work around it to remove the offset text and replace with brackets on the axis label,

ax.yaxis.offsetText.set_visible(False)
ax.set_ylabel("datalabel " +  r'$\left(\mathregular{10^{-5}}\right)$')

Or replace it with a manual text box, as a minimal example,

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np

# Create a figure and axis
fig, ax = plt.subplots()
mpl.rc('text', usetex = True)

# Plot 100 random points 
# the y values of which are very small
large = 100000.0
x = np.random.rand(100)
y = np.random.rand(100)/large

ax.scatter(x,y)

# Set the y limits appropriately
ax.set_ylim(0, 1/large)

# Change the y ticklabel format to scientific format
ax.ticklabel_format(axis='y', style='sci', scilimits=(-2, 2))

#print(ax.yaxis.offsetText.get_position())
ax.yaxis.offsetText.set_visible(False)
ax.text(-0.21, 1.01/large, r'$\mathregular{10^{-2}}$')

# And show the figure
plt.show()

I know this isn't ideal but it may be that offset text cannot be changed manually or can only be the consistent with the numerical values...

Ed Smith
  • 12,716
  • 2
  • 43
  • 55
  • Thanks for your answer - there's something very similar at this [blog post](https://alexpearce.me/2014/04/exponent-label-in-matplotlib). It is a work around but it misses out the fact that the `ticklabel_format` method figures out the exponent for you and I feel like that should be something I can get from the `offsetText`. I'm going to adjust my question to try to make that clearer. – KirstieJane Jul 20 '15 at 16:17
0

Add two lines in your code

import matplotlib.ticker as ptick
ax.yaxis.set_major_formatter(ptick.ScalarFormatter(useMathText=True)) 
0

Building on @KirstieJane's solution, I found a sightly better way, wich also works without having to call tight_layout, and in particular, also works for figures using constrained_layout. Calling ax.get_tightbbox(renderer) is all that is necessary for setting the text. Here is an MWE:

import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.tight_layout import get_renderer
from matplotlib.backends.backend_qt5agg import \
    FigureCanvasQTAgg as FigureCanvas
# from matplotlib.transforms import Bbox
# from mpl_toolkits.axes_grid1 import make_axes_locatable

from PyQt5.QtWidgets import QDialog, QApplication, QGridLayout


class MainWindow(QDialog):
    def __init__(self):
        super().__init__()
        fig, ax = plt.subplots(constrained_layout=True)
        canvas = FigureCanvas(fig)
        lay = QGridLayout(self)
        lay.addWidget(canvas)
        self.setLayout(lay)

        image = np.random.uniform(10000000, 100000000, (100, 100))
        image_artist = ax.imshow(image)
        colorbar = fig.colorbar(image_artist)
        colorbar.ax.ticklabel_format()
        renderer = get_renderer(fig)
        colorbar.ax.get_tightbbox(renderer)
        colorbar.ax.yaxis.offsetText.set_visible(False)
        offset_text = colorbar.ax.yaxis.get_offset_text()
        exp = offset_text.get_text().split('e')[1].replace('+', '')
        colorbar.ax.set_ylabel(rf'Parameter [U${{\times}}10^{{{exp}}}$]')

        canvas.draw_idle()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    GUI = MainWindow()
    GUI.show()
    sys.exit(app.exec_())

For a thorough discussion of the solution, see my answer here.

mapf
  • 1,906
  • 1
  • 14
  • 40