Thanks to visit codestin.com
Credit goes to github.com

Skip to content

5 minor divisions when major ticks are 2.5 units apart #13069

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 11, 2019

Conversation

hershen
Copy link
Contributor

@hershen hershen commented Dec 31, 2018

PR Summary

Currently when the major ticks are spaced 2.5 units apart there are 3 minor ticks spaced 0.625 units apart.
This PR increases the number of minor ticks to 4, spaced 0.5 units apart. To me, this is a much more natural spacing.

Before:
before

After:
after

from matplotlib import pyplot as plt
import matplotlib as mpl

fix, ax = plt.subplots()

ax.set_xlim(0, 5)
ax.set_xticks([0, 2.5, 5])
ax.minorticks_on()

ax.tick_params(which='major', length=7)

ax.yaxis.set_major_locator(mpl.ticker.NullLocator())

PR Checklist

  • [x ] Has Pytest style unit tests
  • [x ] Code is Flake 8 compliant

@hershen
Copy link
Contributor Author

hershen commented Dec 31, 2018

As a side note, I personally feel that major ticks spaced 1 unit apart are more naturally divided into 4 minor divisions (spaced 0.25 units apart) instead of the current 5 minor divisions (spaced 0.2 units apart). If others agree, I can adjust that in this PR.

@timhoffm
Copy link
Member

timhoffm commented Jan 1, 2019

I‘m also positive on on changing the subdivision of 1 unit from 5 to 4.

@hershen
Copy link
Contributor Author

hershen commented Jan 1, 2019

Thanks @timhoffm. I'll wait a bit to see if there's anyone opposed.

@jklymak
Copy link
Member

jklymak commented Jan 1, 2019

I'm not necessarily "opposed", but this seems to be special-casing a major tick interval you had to specify by hand. I don't know that its reasonable to special case all the possible manual tick intervals folks might give us in the minor tick code. If you specify the major ticks by hand, I think its reasonable to specify the minor ticks by hand. But maybe I'm missing a subtlety whereby this is more useful in general.

@hershen
Copy link
Contributor Author

hershen commented Jan 1, 2019

Ah, sorry, I see that example "masks" the fact that 2.5 is one of the default major tick intervals.
The issue can be seen without setting the major ticks:

from matplotlib import pyplot as plt

fig, ax = plt.subplots()

plt.plot([0, 10, 20], [0, 1, 2])
ax.minorticks_on()

plt.show()

4intervals_no_xlim

It's just more "crowded" so I tried to make it clearer, sacrificing the point that 2.5 major tick interval is one of the defaults.

@jklymak
Copy link
Member

jklymak commented Jan 1, 2019

Ah, OK, thats different. Thanks!

majorlocs_no_exponent = 10 ** (np.log10(majorstep) % 1)

# round to closest 0.5
majorlocs_no_exponent = round(majorlocs_no_exponent * 2) / 2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we want to round 2.4 (for instance) to 2.5 do we?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

majorlocs_no_exponent is only used to decide if there should be 4 or 5 minor intervals. The rounding is there to take care of floating point inaccuracies so the major tick step interval can be searched for in the following line (number 2627).
There should be no effect on the major ticks (if that's your concern).

P.S. I just realized that the name of that variable is wrong - it should be majorstep_no_exponent. I'll fix that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it’s that you are rounding 2.4 to 2.5 so if that was the major tick the minor ticks will now be different. I think you want to use np.isclose or some such to check and allow some float inaccuracies.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. Thanks. Replaced the round with np.isclose.


# make sure we're checking all major steps in ticker.AutoLocator
majorsteps = [x[0] for x in majorstep_minordivisions]
assert np.allclose(majorsteps, mticker.AutoLocator()._steps)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to make sure that the major steps in majorstep_minordivisions are the ones in AutoLocator()._steps with the goal that test_number_of_minor_ticks will fail if that's not true. So I added this assert outside any test function.
But if this assert fails pytest emits an error in the collection phase and doesn't run any tests.
Any suggestions how to improve this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just move it to its own test with the comment that it's really intended to test that the next test is correctly parametrized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - thanks.
I created a staticmethod, because it's the only way I found for the parameters to be accessible both in a test and by the @pytest.mark.parametrize decorator.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make it a class variable like params just above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - done.

@hershen hershen force-pushed the five_minor_ticks_2.5_major_ticks branch from 55135d6 to d079d67 Compare January 19, 2019 02:15

majorstep_no_exponent = 10 ** (np.log10(majorstep) % 1)

# The part after the 'or' is for #13069 to make minimal changes to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the comment understandable without refering to github (perhaps just describe the desired behavior instead of refering to the behavior change).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed comment as it's no longer relevant.

# behaviour
if np.isclose(majorstep_no_exponent, 2.5) or (
(not np.isclose(majorstep_no_exponent, [1, 10]).any() and
int(np.round(majorstep_no_exponent)) in [1, 5, 10])):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two conditions (not close to 1 or 10, but possibly in [1 or 10]) seem contradictory.
Given that you are already going to use round(), I would just use its ability to round to one decimal point:

majorstep_no_exponent = round(10**(np.log10(majorstep) % 1), 1)
# (in fact the following looks more natural to me, but heh)
majorstep_no_exponent = round(majorstep / 10**int(np.log(majorstep)), 1)
if majorstep_no_exponent in [2.5, 5]: ...
else: ...

Copy link
Contributor Author

@hershen hershen Jan 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @anntzer for the comments.

  1. The conditions are not close to 1 or 10 and when rounded are in [1 or 10], so not always contradictory (for example 1.1 satisfies both).
    I was trying to make as minimal a change to the number of minor ticks calculation.
    Rounding to 1 decimal place:
if round(majorstep_no_exponent, 1) in [2.5, 5]:
    ndivs = 5
else:
    ndivs = 4

will change the number of minor divisions from 5 to 4 for major ticks spaced 1.1 units apart (for example), compared to the situation before this PR. @jklymak indicated this is undesirable. The code is a bit ugly because of this, though.

  1. I think the calculation used % in order to guarantee 1 <= majorstep_no_exponent <= 10 because that's the range of the steps in AutoLocator. majorstep / 10**int(np.log10(majorstep)) can return values outside this range (for example if marjostep is 0.5).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re 1: I personally think if we're already changing the minorstep mapping for the common case of 1/5/10-spaced ticks (which appear by default), it's really not a big leap to also change the case of 1.1-spaced ticks (which don't even appear by default). This just needs to be documented... (plus, I'm pretty sure there's a lot of other places in the code which probably don't work so well with "weird" tick spacings like 1.1)
Re 2: Sorry, that should have been m/10**np.floor(np.log10(m)) (int rounds towards zero, but I want to round down), which works for m<1 too. Anyways, the formulas are equivalent, no big deal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified condition according to @anntzer's suggestion.

@hershen hershen changed the title 5 minor divisions when major ticks are 2.5 units apart 5 minor divisions when major ticks are 2.5 units apart and 4 minor divisions when major ticks are 1 or 10 units apart Jan 22, 2019
@hershen
Copy link
Contributor Author

hershen commented Jan 22, 2019

I went ahead and adjusted the number of minor ticks from 5 to 4 when the major ticks are spaced 1 or 10 units apart.

@anntzer
Copy link
Contributor

anntzer commented Jan 23, 2019

Needs a changelog entry too.

@hershen hershen force-pushed the five_minor_ticks_2.5_major_ticks branch from a769bef to cfa5393 Compare January 25, 2019 23:06
@hershen
Copy link
Contributor Author

hershen commented Jan 25, 2019

I added an entry in doc/users/next_whats_new/. I hope that's the right place for it.

I think that addresses all current comments.
Additional comments welcome :)

Copy link
Contributor

@anntzer anntzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this open for comments for a bit longer as this is a behavior change, but looks reasonable to me.

@tacaswell tacaswell added this to the v3.1 milestone Jan 28, 2019

majorstep_no_exponent = 10 ** (np.log10(majorstep) % 1)

if np.isclose(majorstep_no_exponent, [2.5, 5.0]).any():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that removing 1, 10 from this list is a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to the original 5 divisions for 1, 10.

Copy link
Member

@efiring efiring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please put 1, 10 back in the list for 5 minor ticks.

@anntzer
Copy link
Contributor

anntzer commented Jan 28, 2019

Agreed during the call that this could be merged after restoring the old behavior for 1 & 10.

@hershen
Copy link
Contributor Author

hershen commented Jan 28, 2019

My preference for 4 divisions for 1 and 10 is not strong (and might be just because that's what I'm used to). But I'd be interested to know why the consensus is that 5 divisions is better for 1 and 10.

@efiring
Copy link
Member

efiring commented Jan 28, 2019

  1. One advantage of 5 minors for intervals of 1 or 10 is that it minimizes the numbers of digits of precision when labeling. It's avoiding using fractions when not needed. It's also a bit like the difference between a metric ruler and an English ruler: the former uses millimeters between centimeters, while the latter uses eighths or sixteenths. Which is more useful, and which is expected, depends on context.
  2. The other reason for not wanting to make this change is that we try to be conservative about style changes, making them only when the rationale is clear and minimally controversial.

@hershen hershen force-pushed the five_minor_ticks_2.5_major_ticks branch from cfa5393 to 67b22e4 Compare January 28, 2019 21:25
@hershen hershen changed the title 5 minor divisions when major ticks are 2.5 units apart and 4 minor divisions when major ticks are 1 or 10 units apart 5 minor divisions when major ticks are 2.5 units apart Jan 28, 2019
@hershen
Copy link
Contributor Author

hershen commented Jan 28, 2019

Thanks for the explanation @efiring.
I see the advantages.

@hershen hershen force-pushed the five_minor_ticks_2.5_major_ticks branch from 67b22e4 to 4e227bd Compare February 4, 2019 21:33
@hershen
Copy link
Contributor Author

hershen commented Feb 11, 2019

Anything else I can do to make this PR more mergeable?

@efiring efiring merged commit d01b3d1 into matplotlib:master Feb 11, 2019
@efiring
Copy link
Member

efiring commented Feb 11, 2019

The test failure was unrelated, so I went ahead with the merge. Thank you for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants