2

For example I need to set a tick only if dividing by 9 gives a "nice" value (nice here has the same meaning as in https://matplotlib.org/3.2.1/api/ticker_api.html).

Accordingly any set of following ticks should be allowed.

  • [9, 18, 27] allowed b/c dividing by 9 gives [1, 2, 3]
  • [27, 54, 81, 108] allowed b/c dividing by 9 gives [3, 6, 9, 12]
  • [0.45, 0.9, 1.35] allowed b/c dividing by 9 gives [0.05, 0.1, 0.15]

Note that I am not looking to set ticks at every multiple of 9, so MultipleLocator doesn't work here.

I also want the locator to automatically figure out "how many ticks" and "best tick interval" etc. Basically I need the functionality of MaxNLocator with my additional divisibility requirement.

Current best solution is MaxNLocator with the arguments MaxNLocator('auto', steps=[9]). But MaxNLocator explicitly adds 10 to the steps argument (see https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/ticker.py#L2058) so sometimes I get tick values like [1000, 2000, 3000] which aren't acceptable. (1000 is not a multiple of 9, see the example 1,2,3 below)

Any suggestions?

UPDATE: See below for some nice vs. not nice examples.

import matplotlib.pyplot as plt
import numpy as np

def draw(name, xmax, steps, verdict):
    x = np.linspace(0, xmax)
    plt.plot(x, np.sin(x))
    plt.gca().set_xlim([0, xmax])
    plt.gca().xaxis.set_major_locator(plt.MaxNLocator('auto', steps=steps))

    vd = 'NICE!!!\n' if verdict else 'NOT nice!\n'
    print(name, plt.gca().get_xticks()/9, '>>', vd)


draw('example 1', 64, [9], True)
draw('example 2', 7.2, [9], True)
draw('example 3', 90000, [9], False)
draw('example 4', 9, [1, 3, 9], False)
draw('example 5', 90, [1.8, 4.5, 9], False)

Output:

example 1 [0. 1. 2. 3. 4. 5. 6. 7. 8.] >> NICE!!!

example 2 [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8] >> NICE!!!

example 3 [0. 1111.11111111  2222.22222222  3333.33333333
  4444.44444444  5555.55555556  6666.66666667  7777.77777778
  8888.88888889 10000.] >> NOT nice!

example 4 [0. 0.11111111 0.22222222 0.33333333 0.44444444 0.55555556
 0.66666667 0.77777778 0.88888889 1.] >> NOT nice!

example 5 [ 0.  1.11111111  2.22222222  3.33333333  4.44444444  5.55555556
  6.66666667  7.77777778  8.88888889 10.] >> NOT nice!
Batta
  • 455
  • 5
  • 13

1 Answers1

2

You can provide a steps= argument to MaxNLocator. These steps need to be between 1 and 10. In your case you could put there the values you like to be used as multiplicator. For example [1.8, 4.5, 9].

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

x = np.linspace(0, 999)
y = np.sin(x/100)
plt.plot(x, y)
plt.gca().xaxis.set_major_locator(MaxNLocator(steps=[1.8, 4.5, 9]))
plt.gca().yaxis.set_major_locator(MaxNLocator(steps=[1.8, 4.5, 9]))
plt.show()

example plot

JohanC
  • 71,591
  • 8
  • 33
  • 66
  • Thanks! But as per my requirement 3, 6, 12 shouldn't appear b/c they don't give nice ticks. See example 4 in my updated question. – Batta Jun 02 '20 at 18:43
  • I see you have updated the answer. Again I can find some x limit that breaks the solution. See newly added example 5. I think MaxNLocator can't deliver as long as it manually adds 10 to the steps argument (see the second link in my question). Do you know why MaxNLocator explicitly adds 10? – Batta Jun 04 '20 at 00:44
  • Good point. Just tested this by removing 'auto'. It works sometimes but doesn't work with for example xmax=99, steps=[1.8, 4.5, 9]. – Batta Jun 05 '20 at 21:20
  • `draw('example 6', 999, [1.8, 4.5, 9], True)` gives `[ 0. 20. 40. 60. 80. 100. 120.]` to me. What wrong with that? – JohanC Jun 05 '20 at 21:49
  • Strange, here's what I got for `for i in range(1,100): draw(str(i), i, [1.8, 4.5, 9], True)` with `MaxNLocator(steps=steps)`. Ignore the NICE and NOT NICE in the output: https://pastebin.com/1aqRxdEs. So still doesn't work for `i` = 1, 10, 91-99. Can you run a range and see what happens? – Batta Jun 06 '20 at 00:07
  • Indeed, for some values, the output is `[0,1,2,...]` or `[0,10,20,..]` instead of the multiples of 9. I think this is a bug in the implementation of the `steps` option of `MaxNLocator`. It seems to happen every time the numbers `[0,1,2,3,4,5,6,7,8,9,10]` (or a power of 10 multiple) are a perfect fit. – JohanC Jun 06 '20 at 00:52
  • I think the reason for this is mpl manually adds `10` to `steps` (ticker.py#L2058). So no matter what `steps` you pass into the locator `10` is always there and sometimes we get multiples of `10`, not `9`. The question is why does it do that? I don't know if there is a fundamental reason for this. Currently my hack is to manually set the `_extended_steps` variable in `MaxNLocator`. – Batta Jun 06 '20 at 03:14
  • You could rise an issue in matplotlib's github to at least get an option to leave 10 out of the `steps`. With *usual use case* I meant most people do want 10 inside the steps, and seemingly nobody has raised the question before. It could also be interesting to have `'auto'` work nicely together with these steps. There seems to be an old discussion [here](https://github.com/matplotlib/matplotlib/issues/7578) insisting on 1 and 10 being part of steps, and that older versions could fall in an infinite loop. – JohanC Jun 06 '20 at 21:20
  • Also note that matplotlib jumps to an [offset notation](https://stackoverflow.com/questions/28371674/prevent-scientific-notation-in-matplotlib-pyplot), for example if the x-range would go from `1000000` to `1000010`. This behavior could be complicated to combine with your desire of leaving out 1 and 10. – JohanC Jun 06 '20 at 22:16
  • Okay, great. Thanks for links. I am gong to keep this SO post as an open question for now. – Batta Jun 08 '20 at 15:41