diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 3c578d431adc..6597d70f8ac7 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -654,6 +654,7 @@ def __init__(self, axes, pickradius=15): # Initialize here for testing; later add API self._major_tick_kw = dict() self._minor_tick_kw = dict() + self._tick_space = None self.cla() self._set_scale('linear') @@ -785,6 +786,7 @@ def set_tick_params(self, which='major', reset=False, **kw): for tick in self.minorTicks: tick._apply_params(**self._minor_tick_kw) self.stale = True + self._tick_space = None @staticmethod def _translate_tick_kw(kw, to_init_kw=True): @@ -1665,6 +1667,13 @@ def axis_date(self, tz=None): tz = pytz.timezone(tz) self.update_units(datetime.datetime(2009, 1, 1, 0, 0, 0, 0, tz)) + def get_tick_space(self): + """ + Return the estimated number of ticks that can fit on the axis. + """ + # Must be overridden in the subclass + raise NotImplementedError() + class XAxis(Axis): __name__ = 'xaxis' @@ -1988,6 +1997,18 @@ def set_default_intervals(self): self.axes.viewLim.intervalx = xmin, xmax self.stale = True + def get_tick_space(self): + if self._tick_space is None: + ends = self.axes.transAxes.transform([[0, 0], [1, 0]]) + length = ((ends[1][0] - ends[0][0]) / self.axes.figure.dpi) * 72.0 + 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 + size *= np.cos(np.deg2rad(tick.label1.get_rotation())) + self._tick_space = np.floor(length / size) + return self._tick_space + class YAxis(Axis): __name__ = 'yaxis' @@ -2318,3 +2339,14 @@ def set_default_intervals(self): if not viewMutated: self.axes.viewLim.intervaly = ymin, ymax self.stale = True + + def get_tick_space(self): + if self._tick_space is None: + ends = self.axes.transAxes.transform([[0, 0], [0, 1]]) + length = ((ends[1][1] - ends[0][1]) / self.axes.figure.dpi) * 72.0 + tick = self._get_tick(True) + # Having a spacing of at least 2 just looks good. + size = tick.label1.get_size() * 2.0 + size *= np.cos(np.deg2rad(tick.label1.get_rotation())) + self._tick_space = np.floor(length / size) + return self._tick_space diff --git a/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png b/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png index 074f1bf404d9..612eb3dd3c48 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png and b/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks.png b/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks.png new file mode 100644 index 000000000000..91d220768457 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/auto_numticks.png differ diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 3a1a25053da5..b91c6553c348 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4191,6 +4191,12 @@ def test_axes_margins(): assert ax.get_ybound() == (-0.5, 9.5) +@image_comparison(baseline_images=["auto_numticks"], style='default', + extensions=['png']) +def test_auto_numticks(): + fig, axes = plt.subplots(4, 4) + + if __name__ == '__main__': import nose import sys diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index b487671fb5e9..2c7ce1f62b1b 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -202,6 +202,10 @@ def get_data_interval(self): def set_data_interval(self, vmin, vmax): self.dataLim.intervalx = vmin, vmax + def get_tick_space(self): + # Just use the long-standing default of nbins==9 + return 9 + class TickHelper(object): axis = None @@ -1349,7 +1353,9 @@ def __init__(self, *args, **kwargs): Keyword args: *nbins* - Maximum number of intervals; one less than max number of ticks. + Maximum number of intervals; one less than max number of + ticks. If the string `'auto'`, the number of bins will be + automatically determined based on the length of the axis. *steps* Sequence of nice numbers starting with 1 and ending with 10; @@ -1387,7 +1393,9 @@ def __init__(self, *args, **kwargs): def set_params(self, **kwargs): """Set parameters within this locator.""" if 'nbins' in kwargs: - self._nbins = int(kwargs['nbins']) + self._nbins = kwargs['nbins'] + if self._nbins != 'auto': + self._nbins = int(self._nbins) if 'trim' in kwargs: self._trim = kwargs['trim'] if 'integer' in kwargs: @@ -1416,6 +1424,8 @@ def set_params(self, **kwargs): def bin_boundaries(self, vmin, vmax): nbins = self._nbins + if nbins == 'auto': + nbins = self.axis.get_tick_space() scale, offset = scale_range(vmin, vmax, nbins) if self._integer: scale = max(1, scale) @@ -1901,7 +1911,11 @@ def tick_values(self, vmin, vmax): class AutoLocator(MaxNLocator): def __init__(self): - MaxNLocator.__init__(self, nbins=9, steps=[1, 2, 5, 10]) + if rcParams['_internal.classic_mode']: + nbins = 9 + else: + nbins = 'auto' + MaxNLocator.__init__(self, nbins=nbins, steps=[1, 2, 5, 10]) class AutoMinorLocator(Locator):