Search code examples
pythonmatplotlibaxis-labels

Log scale with a different factor and base


I see that set_xscale accepts a base parameter, but I also want to scale with a factor; i.e. if the base is 4 and the factor is 10, then:

40, 160, 640, ...

Also, the documentation says that the sub-grid values represented by subsx should be integers, but I will want floating-point values.

What is the cleanest way to do this?


Solution

  • I'm not aware of any built-in method to apply a scaling factor after the exponent, but you could create a custom tick locator and formatter by subclassing matplotlib.ticker.LogLocator and matplotlib.ticker.LogFormatter.

    Here's a fairly quick-and-dirty hack that does what you're looking for:

    from matplotlib import pyplot as plt
    from matplotlib.ticker import LogLocator, LogFormatter, ScalarFormatter, \
                                  is_close_to_int, nearest_long
    import numpy as np
    import math
    
    class ScaledLogLocator(LogLocator):
        def __init__(self, *args, scale=10.0, **kwargs):
            self._scale = scale
            LogLocator.__init__(self, *args, **kwargs)
    
        def view_limits(self, vmin, vmax):
            s = self._scale
            vmin, vmax = LogLocator.view_limits(self, vmin / s, vmax / s)
            return s * vmin, s * vmax
    
        def tick_values(self, vmin, vmax):
            s = self._scale
            locs = LogLocator.tick_values(self, vmin / s, vmax / s)
            return s * locs
    
    class ScaledLogFormatter(LogFormatter):
        def __init__(self, *args, scale=10.0, **kwargs):
            self._scale = scale
            LogFormatter.__init__(self, *args, **kwargs)
    
        def __call__(self, x, pos=None):
            b = self._base
            s = self._scale
    
            # only label the decades
            if x == 0:
                return '$\mathdefault{0}$'
    
            fx = math.log(abs(x / s)) / math.log(b)
            is_decade = is_close_to_int(fx)
            sign_string = '-' if x < 0 else ''
    
            # use string formatting of the base if it is not an integer
            if b % 1 == 0.0:
                base = '%d' % b
            else:
                base = '%s' % b
            scale = '%d' % s
    
            if not is_decade and self.labelOnlyBase:
                return ''
            elif not is_decade:
                return ('$\mathdefault{%s%s\times%s^{%.2f}}$'
                         % (sign_string, scale, base, fx))
            else:
                return (r'$%s%s\times%s^{%d}$'
                        % (sign_string, scale, base, nearest_long(fx)))
    

    For example:

    fig, ax = plt.subplots(1, 1)
    x = np.arange(1000)
    y = np.random.randn(1000)
    ax.plot(x, y)
    ax.set_xscale('log')
    subs = np.linspace(0, 1, 10)
    
    majloc = ScaledLogLocator(scale=10, base=4)
    minloc = ScaledLogLocator(scale=10, base=4, subs=subs)
    fmt = ScaledLogFormatter(scale=10, base=4)
    ax.xaxis.set_major_locator(majloc)
    ax.xaxis.set_minor_locator(minloc)
    ax.xaxis.set_major_formatter(fmt)
    ax.grid(True)
    
    # show the same tick locations with non-exponential labels
    ax2 = ax.twiny()
    ax2.set_xscale('log')
    ax2.set_xlim(*ax.get_xlim())
    fmt2 = ScalarFormatter()
    ax2.xaxis.set_major_locator(majloc)
    ax2.xaxis.set_minor_locator(minloc)
    ax2.xaxis.set_major_formatter(fmt2)
    

    enter image description here