diff --git a/doc/users/next_whats_new/auto_minor_tick.rst b/doc/users/next_whats_new/auto_minor_tick.rst new file mode 100644 index 000000000000..02db8f6beb38 --- /dev/null +++ b/doc/users/next_whats_new/auto_minor_tick.rst @@ -0,0 +1,5 @@ +rcParams for ``AutoMinorLocator`` divisions +------------------------------------------- +The rcParams :rc:`xtick.minor.ndivs` and :rc:`ytick.minor.ndivs` have been +added to enable setting the default number of divisions; if set to ``auto``, +the number of divisions will be chosen by the distance between major ticks. diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index 578777167d6c..86f70a23dacd 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -489,6 +489,7 @@ #xtick.major.bottom: True # draw x axis bottom major ticks #xtick.minor.top: True # draw x axis top minor ticks #xtick.minor.bottom: True # draw x axis bottom minor ticks +#xtick.minor.ndivs: auto # number of minor ticks between the major ticks on x-axis #xtick.alignment: center # alignment of xticks #ytick.left: True # draw ticks on the left side @@ -510,6 +511,7 @@ #ytick.major.right: True # draw y axis right major ticks #ytick.minor.left: True # draw y axis left minor ticks #ytick.minor.right: True # draw y axis right minor ticks +#ytick.minor.ndivs: auto # number of minor ticks between the major ticks on y-axis #ytick.alignment: center_baseline # alignment of yticks diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b27ac35bcb4d..663ff4b70536 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -568,6 +568,14 @@ def _validate_greaterequal0_lessequal1(s): raise RuntimeError(f'Value must be >=0 and <=1; got {s}') +def _validate_int_greaterequal0(s): + s = validate_int(s) + if s >= 0: + return s + else: + raise RuntimeError(f'Value must be >=0; got {s}') + + def validate_hatch(s): r""" Validate a hatch pattern. @@ -587,6 +595,24 @@ def validate_hatch(s): validate_dashlist = _listify_validator(validate_floatlist) +def _validate_minor_tick_ndivs(n): + """ + Validate ndiv parameter related to the minor ticks. + It controls the number of minor ticks to be placed between + two major ticks. + """ + + if isinstance(n, str) and n.lower() == 'auto': + return n + try: + n = _validate_int_greaterequal0(n) + return n + except (RuntimeError, ValueError): + pass + + raise ValueError("'tick.minor.ndivs' must be 'auto' or non-negative int") + + _prop_validators = { 'color': _listify_validator(validate_color_for_prop_cycle, allow_stringlist=True), @@ -1093,6 +1119,8 @@ def _convert_validator_spec(key, conv): "xtick.minor.bottom": validate_bool, # draw bottom minor xticks "xtick.major.top": validate_bool, # draw top major xticks "xtick.major.bottom": validate_bool, # draw bottom major xticks + # number of minor xticks + "xtick.minor.ndivs": _validate_minor_tick_ndivs, "xtick.labelsize": validate_fontsize, # fontsize of xtick labels "xtick.direction": ["out", "in", "inout"], # direction of xticks "xtick.alignment": ["center", "right", "left"], @@ -1114,6 +1142,8 @@ def _convert_validator_spec(key, conv): "ytick.minor.right": validate_bool, # draw right minor yticks "ytick.major.left": validate_bool, # draw left major yticks "ytick.major.right": validate_bool, # draw right major yticks + # number of minor yticks + "ytick.minor.ndivs": _validate_minor_tick_ndivs, "ytick.labelsize": validate_fontsize, # fontsize of ytick labels "ytick.direction": ["out", "in", "inout"], # direction of yticks "ytick.alignment": [ diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 15981c4c9047..3d38df575f09 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -211,6 +211,60 @@ def test_additional(self, lim, ref): assert_almost_equal(ax.yaxis.get_ticklocs(minor=True), ref) + @pytest.mark.parametrize('use_rcparam', [False, True]) + @pytest.mark.parametrize( + 'lim, ref', [ + ((0, 1.39), + [0.05, 0.1, 0.15, 0.25, 0.3, 0.35, 0.45, 0.5, 0.55, 0.65, 0.7, + 0.75, 0.85, 0.9, 0.95, 1.05, 1.1, 1.15, 1.25, 1.3, 1.35]), + ((0, 0.139), + [0.005, 0.01, 0.015, 0.025, 0.03, 0.035, 0.045, 0.05, 0.055, + 0.065, 0.07, 0.075, 0.085, 0.09, 0.095, 0.105, 0.11, 0.115, + 0.125, 0.13, 0.135]), + ]) + def test_number_of_minor_ticks_auto(self, lim, ref, use_rcparam): + if use_rcparam: + context = {'xtick.minor.ndivs': 'auto', 'ytick.minor.ndivs': 'auto'} + kwargs = {} + else: + context = {} + kwargs = {'n': 'auto'} + + with mpl.rc_context(context): + fig, ax = plt.subplots() + ax.set_xlim(*lim) + ax.set_ylim(*lim) + ax.xaxis.set_minor_locator(mticker.AutoMinorLocator(**kwargs)) + ax.yaxis.set_minor_locator(mticker.AutoMinorLocator(**kwargs)) + assert_almost_equal(ax.xaxis.get_ticklocs(minor=True), ref) + assert_almost_equal(ax.yaxis.get_ticklocs(minor=True), ref) + + @pytest.mark.parametrize('use_rcparam', [False, True]) + @pytest.mark.parametrize( + 'n, lim, ref', [ + (2, (0, 4), [0.5, 1.5, 2.5, 3.5]), + (4, (0, 2), [0.25, 0.5, 0.75, 1.25, 1.5, 1.75]), + (10, (0, 1), [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]), + ]) + def test_number_of_minor_ticks_int(self, n, lim, ref, use_rcparam): + if use_rcparam: + context = {'xtick.minor.ndivs': n, 'ytick.minor.ndivs': n} + kwargs = {} + else: + context = {} + kwargs = {'n': n} + + with mpl.rc_context(context): + fig, ax = plt.subplots() + ax.set_xlim(*lim) + ax.set_ylim(*lim) + ax.xaxis.set_major_locator(mticker.MultipleLocator(1)) + ax.xaxis.set_minor_locator(mticker.AutoMinorLocator(**kwargs)) + ax.yaxis.set_major_locator(mticker.MultipleLocator(1)) + ax.yaxis.set_minor_locator(mticker.AutoMinorLocator(**kwargs)) + assert_almost_equal(ax.xaxis.get_ticklocs(minor=True), ref) + assert_almost_equal(ax.yaxis.get_ticklocs(minor=True), ref) + class TestLogLocator: def test_basic(self): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index a29343029f60..bf059567f5fe 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -2869,7 +2869,11 @@ def __init__(self, n=None): major ticks; e.g., n=2 will place a single minor tick midway between major ticks. - If *n* is omitted or None, it will be set to 5 or 4. + If *n* is omitted or None, the value stored in rcParams will be used. + In case *n* is set to 'auto', it will be set to 4 or 5. If the distance + between the major ticks equals 1, 2.5, 5 or 10 it can be perfectly + divided in 5 equidistant sub-intervals with a length multiple of + 0.05. Otherwise it is divided in 4 sub-intervals. """ self.ndivs = n @@ -2892,6 +2896,14 @@ def __call__(self): if self.ndivs is None: + if self.axis.axis_name == 'y': + self.ndivs = mpl.rcParams['ytick.minor.ndivs'] + else: + # for x and z axis + self.ndivs = mpl.rcParams['xtick.minor.ndivs'] + + if self.ndivs == 'auto': + majorstep_no_exponent = 10 ** (np.log10(majorstep) % 1) if np.isclose(majorstep_no_exponent, [1.0, 2.5, 5.0, 10.0]).any():