1

This question addresses a problem I'm currently facing. I have some data, say [12.301, 12.318, 12.302] which, when plotted, results in an offset displayed as +1.23e1.

I don't mind the offset itself, but that exponential form instead of just writing 12.3 is not very nice. Can I force somehow the exponent to only appear for multiple-of-three powers of ten? 1e3 instead of 1000 makes sense, 1e1 instead of 10 not at all.

I found this other question somewhat related, but that is more related to only having an integer form, whereas I don't mind decimals.

Enzo
  • 338
  • 1
  • 2
  • 15
  • So you want matplotlib to automatically determine an offset for you, but you would like to specify the format of this offset yourself, did I get this right? – ImportanceOfBeingErnest Dec 13 '18 at 20:17
  • Have you seen [this answer](https://stackoverflow.com/a/3679918/2454357) to the second question you linked? I think adapting that solution to your needs would be the a good solution to your problem. – Thomas Kühn Dec 14 '18 at 09:53
  • @ImportanceOfBeingErnest nearly, iiuc. They additionally want to constrain the automatism to create offsets with 10-exponents of exclusively multiples of 3. – SpghttCd Dec 14 '18 at 10:20
  • @SpghttCd got it right, sorry if I was unclear. But yes, that's what I'm trying to do. I also read the answer pointed out by Thomas, and indeed it gets very close to what I want to achieve, but I couldn't find in the documentation anything that would help me adapt it to what I need. – Enzo Dec 14 '18 at 11:16

1 Answers1

1

With a little bit of searching in the web and adapting the answers found here, here, and here plus a print(dir(ScalarFormatter)), I was able to adapt the first linked post:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter, FormatStrFormatter

#from https://stackoverflow.com/a/45332730/2454357
def fexp(f):
    return int(np.floor(np.log10(abs(f)))) if f != 0 else 0

#from https://stackoverflow.com/a/45332730/2454357
def fman(f):
    return f/10**fexp(f)

#adapted from https://stackoverflow.com/a/3679918/2454357
class PowerMultipleOfThreeFormatter(ScalarFormatter):
    """Formats axis ticks using scientific notation with a constant order of 
    magnitude"""
    def __init__(self, useOffset=True, useMathText=False):
        ScalarFormatter.__init__(self, useOffset=useOffset, 
                                 useMathText=useMathText)

    def _set_orderOfMagnitude(self, range):
        """Over-riding this to avoid having orderOfMagnitude reset elsewhere"""
        exponent = fexp(range)
        if -3 < exponent < 3:
            self.orderOfMagnitude = 0
        else:
            new_exp = (exponent//3)*3
            self.orderOfMagnitude = new_exp


    def format_data(self, *args, **kwargs):

        ##make sure that format_data does everyting it shoud:
        super(PowerMultipleOfThreeFormatter, self).format_data(
            *args, **kwargs
        )

        ##compute the offset in the right format
        exponent = fexp(self.offset)
        mantissa = fman(self.offset)
        if -3 < exponent < 3:
            return '{:g}'.format(self.offset)

        new_exp = (exponent//3)*3
        factor = 10**new_exp

        ##from https://stackoverflow.com/a/2440786/2454357
        man_string = '{}'.format(self.offset/factor).rstrip('0').rstrip('.')
        return man_string+'e{}'.format(new_exp)

# Generate some random data...
x = np.linspace(55478, 55486, 100) 
y = np.random.random(100) - 0.5
y = np.cumsum(y)
y *= 1e-8

# Plot the data...
fig,axes = plt.subplots(nrows=2, ncols=2)
for ax, y0 in zip(axes.ravel(), [-1e4, 1.15e-4, 12, -0.1]):
    ax.plot(x, y+y0, 'b-')
    ax.yaxis.set_major_formatter(PowerMultipleOfThreeFormatter())

fig.tight_layout()
plt.show()

In general, the idea is to compute the exponent and mantissa of a number and manipulate the two such that the exponent is a multiple of 3 (with (exponent//3)*3 and exponent%3). For the multiplier, it was already demonstrated here, where and how these calculations should be added (i.e. in _set_orderOfMagnitude). The offset value is stored in ScalarFormatter.offset and the string representation is computed in the function format_data(). Overloading that function we can thus change how the offset is displayed. The code also contains an example how to use the new formatter (the way how to generate the data is again shamelessly copied from here). The result of the code looks like this:

result of above code

Thomas Kühn
  • 9,412
  • 3
  • 47
  • 63
  • Hi. This works to format the shift well, but for some reason I now have `1e-3+12.3`. Where did that first term come from? My data does not have that `1e-3` multiplier. – Enzo Dec 17 '18 at 14:14
  • @Enzo Sorry, that first term is a factor. In the Formatter I wrote, I assumed that you want the same number formatting also for the factor. If that is not the case, comment out the function `_set_orderOfMagnitude`. – Thomas Kühn Dec 17 '18 at 14:17
  • Hi Thomas, yes, I got that, the problem is that I don't have that factor in my data. The format is correct, yes, but the actual value is not. To be clear, my data is formatted like `12.301, 12.302, 12.307` and so on. That `1e-3` transforms them to `0.012...` – Enzo Dec 17 '18 at 14:19
  • @Enzo even if you comment out that function? This command: `new_exp = (exponent//3)*3` should be the cause. I now realise that I forgot the `if` statement there ... – Thomas Kühn Dec 17 '18 at 14:22
  • no, if I comment it out it works. Thank you very much for your help! – Enzo Dec 17 '18 at 14:22
  • @Enzo no problem. I changed the code slightly -- should be fixed now. – Thomas Kühn Dec 17 '18 at 14:30