Adjust exponent text after setting scientific limits on matplotlib axis

后端 未结 5 967
旧时难觅i
旧时难觅i 2020-12-05 16:13

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 ad

相关标签:
5条回答
  • 2020-12-05 16:47

    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.

    0 讨论(0)
  • 2020-12-05 16:50

    Add two lines in your code

    import matplotlib.ticker as ptick
    ax.yaxis.set_major_formatter(ptick.ScalarFormatter(useMathText=True)) 
    
    0 讨论(0)
  • 2020-12-05 17:00

    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...

    0 讨论(0)
  • 2020-12-05 17:05

    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()
    

    0 讨论(0)
  • 2020-12-05 17:07

    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!

    0 讨论(0)
提交回复
热议问题