From bab3658cfdfdcfeb60f79d890be221c91b5659bd Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 16 Nov 2015 18:48:02 -0500 Subject: [PATCH 1/4] Fix #5488: Adjust nticks based on length of axis --- lib/matplotlib/axis.py | 28 ++++++++++++++++++++++++ lib/matplotlib/ticker.py | 46 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 3c578d431adc..084b4fbb2030 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1665,6 +1665,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 +1995,17 @@ def set_default_intervals(self): self.axes.viewLim.intervalx = xmin, xmax self.stale = True + def get_tick_space(self): + # TODO: cache this computation + 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 4:1 + size = tick.label1.get_size() * 4 + size *= np.cos(np.deg2rad(tick.label1.get_rotation())) + return np.floor(length / size) + class YAxis(Axis): __name__ = 'yaxis' @@ -2318,3 +2336,13 @@ def set_default_intervals(self): if not viewMutated: self.axes.viewLim.intervaly = ymin, ymax self.stale = True + + def get_tick_space(self): + # TODO: cache this computation + 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())) + return np.floor(length / size) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 450869878c14..e647c8018ce1 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1449,6 +1449,48 @@ def view_limits(self, dmin, dmax): return np.take(self.bin_boundaries(dmin, dmax), [0, -1]) +class AutoSpacedLocator(MaxNLocator): + """ + Behaves like a MaxNLocator, except N is automatically determined + from the length of the axis. + """ + def __init__(self, *args, **kwargs): + """ + Keyword args: + + *steps* + Sequence of nice numbers starting with 1 and ending with 10; + e.g., [1, 2, 4, 5, 10] + + *integer* + If True, ticks will take only integer values. + + *symmetric* + If True, autoscaling will result in a range symmetric + about zero. + + *prune* + ['lower' | 'upper' | 'both' | None] + Remove edge ticks -- useful for stacked or ganged plots + where the upper tick of one axes overlaps with the lower + tick of the axes above it. + If prune=='lower', the smallest tick will + be removed. If prune=='upper', the largest tick will be + removed. If prune=='both', the largest and smallest ticks + will be removed. If prune==None, no ticks will be removed. + + """ + if 'nbins' in kwargs: + raise ValueError( + 'AutoSpacedLocator does not take nbins as an argument') + self.set_params(**self.default_params) + self.set_params(**kwargs) + + def __call__(self): + self._nbins = self.axis.get_tick_space() + return super(AutoSpacedLocator, self).__call__() + + def decade_down(x, base=10): 'floor x to the nearest lower decade' if x == 0.0: @@ -1872,9 +1914,9 @@ def tick_values(self, vmin, vmax): return self.raise_if_exceeds(np.array(ticklocs)) -class AutoLocator(MaxNLocator): +class AutoLocator(AutoSpacedLocator): def __init__(self): - MaxNLocator.__init__(self, nbins=9, steps=[1, 2, 5, 10]) + AutoSpacedLocator.__init__(self, steps=[1, 2, 5, 10]) class AutoMinorLocator(Locator): From 7bf93704049ba9fe1d3d5438fafafef66a2e92b7 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 16 Nov 2015 20:45:56 -0500 Subject: [PATCH 2/4] Cache tick space calculation --- lib/matplotlib/axis.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 084b4fbb2030..760b48299637 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): @@ -1996,15 +1998,16 @@ def set_default_intervals(self): self.stale = True def get_tick_space(self): - # TODO: cache this computation - 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 4:1 - size = tick.label1.get_size() * 4 - size *= np.cos(np.deg2rad(tick.label1.get_rotation())) - return np.floor(length / size) + 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 4:1 + size = tick.label1.get_size() * 4 + size *= np.cos(np.deg2rad(tick.label1.get_rotation())) + self._tick_space = np.floor(length / size) + return self._tick_space class YAxis(Axis): @@ -2338,11 +2341,12 @@ def set_default_intervals(self): self.stale = True def get_tick_space(self): - # TODO: cache this computation - 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())) - return np.floor(length / size) + 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 From bdc95184c6e6144be63f356a864fad0fadd4a2b7 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Tue, 17 Nov 2015 18:33:28 -0500 Subject: [PATCH 3/4] Always use at least 3 bins --- lib/matplotlib/ticker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index e647c8018ce1..3032faea021e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1487,7 +1487,7 @@ def __init__(self, *args, **kwargs): self.set_params(**kwargs) def __call__(self): - self._nbins = self.axis.get_tick_space() + self._nbins = max(self.axis.get_tick_space(), 3) return super(AutoSpacedLocator, self).__call__() From e529f0727640440e1db89e5b0df07c53fed77d03 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 18 Nov 2015 09:05:36 -0500 Subject: [PATCH 4/4] Make behavior backward compatible --- examples/units/basic_units.py | 6 +++++- lib/matplotlib/axis.py | 5 ++++- lib/matplotlib/ticker.py | 6 ++++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/units/basic_units.py b/examples/units/basic_units.py index f8425f1ec45c..fa83f7643b1a 100644 --- a/examples/units/basic_units.py +++ b/examples/units/basic_units.py @@ -328,8 +328,12 @@ def axisinfo(unit, axis): label=unit.fullname, ) elif unit == degrees: + if rcParams['_internal.classic_mode']: + locator = ticker.ClassicAutoLocator() + else: + locator = ticker.AutoLocator() return units.AxisInfo( - majloc=ticker.AutoLocator(), + majloc=locator, majfmt=ticker.FormatStrFormatter(r'$%i^\circ$'), label=unit.fullname, ) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 760b48299637..333140f8c293 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -711,7 +711,10 @@ def get_children(self): def cla(self): 'clear the current axis' - self.set_major_locator(mticker.AutoLocator()) + if rcParams['_internal.classic_mode']: + self.set_major_locator(mticker.ClassicAutoLocator()) + else: + self.set_major_locator(mticker.AutoLocator()) self.set_major_formatter(mticker.ScalarFormatter()) self.set_minor_locator(mticker.NullLocator()) self.set_minor_formatter(mticker.NullFormatter()) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 3032faea021e..d9aeb0b15ae9 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1919,6 +1919,12 @@ def __init__(self): AutoSpacedLocator.__init__(self, steps=[1, 2, 5, 10]) +class ClassicAutoLocator(MaxNLocator): + # Used only for classic style + def __init__(self): + MaxNLocator.__init__(self, nbins=9, steps=[1, 2, 5, 10]) + + class AutoMinorLocator(Locator): """ Dynamically find minor tick positions based on the positions of