diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index b3a7ffcc3ca3..adb6dbc45a58 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1760,6 +1760,11 @@ def get_minpos(self): raise NotImplementedError() +def tick_space_heuristic(axis_length, label_fontsize, label_fontsize_factor): + size = label_fontsize * label_fontsize_factor + return int(np.floor(axis_length / size)) + + class XAxis(Axis): __name__ = 'xaxis' axis_name = 'x' @@ -2117,15 +2122,16 @@ def set_default_intervals(self): self.axes.viewLim.intervalx = xmin, xmax self.stale = True - def get_tick_space(self): + def get_tick_space(self, + heuristic=lambda x, y: tick_space_heuristic(x, y, 3)): ends = self.axes.transAxes.transform([[0, 0], [1, 0]]) length = ((ends[1][0] - ends[0][0]) / self.axes.figure.dpi) * 72 tick = self._get_tick(True) # There is a heuristic here that the aspect ratio of tick text # is no more than 3:1 - size = tick.label1.get_size() * 3 - if size > 0: - return int(np.floor(length / size)) + label_fontsize = tick.label1.get_size() + if label_fontsize > 0: + return heuristic(length, label_fontsize) else: return 2**31 - 1 @@ -2496,13 +2502,14 @@ def set_default_intervals(self): self.axes.viewLim.intervaly = ymin, ymax self.stale = True - def get_tick_space(self): + def get_tick_space(self, + heuristic=lambda x, y: tick_space_heuristic(x, y, 2.0)): ends = self.axes.transAxes.transform([[0, 0], [0, 1]]) length = ((ends[1][1] - ends[0][1]) / self.axes.figure.dpi) * 72 tick = self._get_tick(True) # Having a spacing of at least 2 just looks good. - size = tick.label1.get_size() * 2.0 - if size > 0: - return int(np.floor(length / size)) + label_fontsize = tick.label1.get_size() + if label_fontsize > 0: + return heuristic(length, label_fontsize) else: return 2**31 - 1 diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 201d81b0f9d7..905f5abc3b7f 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -7,6 +7,7 @@ import matplotlib import matplotlib.pyplot as plt import matplotlib.ticker as mticker +import matplotlib.axis as maxis class TestMaxNLocator(object): @@ -22,6 +23,13 @@ class TestMaxNLocator(object): (1, 55, [1, 1.5, 5, 6, 10], np.array([0, 15, 30, 45, 60])), ] + heuristic_data = [ + (-0.5, 9.5, None, np.array([-5., 0., 5., 10.])), + (-0.5, 9.5, + lambda x, y: maxis.tick_space_heuristic(x, y, 2.), + np.array([-4., 0., 4., 8., 12.])) + ] + @pytest.mark.parametrize('vmin, vmax, expected', basic_data) def test_basic(self, vmin, vmax, expected): loc = mticker.MaxNLocator(nbins=5) @@ -32,6 +40,13 @@ def test_integer(self, vmin, vmax, steps, expected): loc = mticker.MaxNLocator(nbins=5, integer=True, steps=steps) assert_almost_equal(loc.tick_values(vmin, vmax), expected) + @pytest.mark.parametrize('vmin, vmax, heuristic, expected', heuristic_data) + def test_tick_space(self, vmin, vmax, heuristic, expected): + fig, ax = plt.subplots(figsize=(2, 2)) + loc = mticker.MaxNLocator(nbins='auto', tick_space_heuristic=heuristic) + loc.set_axis(ax.xaxis) + assert_almost_equal(loc.tick_values(vmin, vmax), expected) + class TestLinearLocator(object): def test_basic(self): diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 47de3e105ebe..2815a1414b2c 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1779,7 +1779,8 @@ class MaxNLocator(Locator): integer=False, symmetric=False, prune=None, - min_n_ticks=2) + min_n_ticks=2, + tick_space_heuristic=None) def __init__(self, *args, **kwargs): """ @@ -1821,6 +1822,12 @@ def __init__(self, *args, **kwargs): Relax `nbins` and `integer` constraints if necessary to obtain this minimum number of ticks. + *tick_space_heuristic* + Controls spacing of ticks through `axis.get_tick_space`. This must + be a callable object that takes two parameters, the axis length and + the label font size, both in pt units. Default value `None` will + use the default x- or y-axis heuristic. + """ if args: kwargs['nbins'] = args[0] @@ -1882,11 +1889,17 @@ def set_params(self, **kwargs): self._extended_steps = self._staircase(self._steps) if 'integer' in kwargs: self._integer = kwargs['integer'] + if 'tick_space_heuristic' in kwargs: + tick_space_heuristic = kwargs['tick_space_heuristic'] + if tick_space_heuristic is not None: + self._tickspace_kw = {'heuristic': tick_space_heuristic} + else: + self._tickspace_kw = {} def _raw_ticks(self, vmin, vmax): if self._nbins == 'auto': if self.axis is not None: - nbins = np.clip(self.axis.get_tick_space(), + nbins = np.clip(self.axis.get_tick_space(**self._tickspace_kw), max(1, self._min_n_ticks - 1), 9) else: nbins = 9