diff --git a/doc/api/next_api_changes/2018-01-28-AL.rst b/doc/api/next_api_changes/2018-01-28-AL.rst new file mode 100644 index 000000000000..aec004ee9f5b --- /dev/null +++ b/doc/api/next_api_changes/2018-01-28-AL.rst @@ -0,0 +1,11 @@ +New `Formatter.format_ticks` method +``````````````````````````````````` + +The `Formatter` class gained a new `~Formatter.format_ticks` method, which +takes the list of all tick locations as a single argument and returns the list +of all formatted values. It is called by the axis tick handling code and, by +default, repeatedly calls `~Formatter.__call__`. + +It is intended to be overridden by `Formatter` subclasses for which +the formatting of a tick value depends on other tick values, such as +`ConciseDateFormatter`. diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 64c6df9d3429..0beeeb80f078 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -929,24 +929,18 @@ def iter_ticks(self): """ Iterate through all of the major and minor ticks. """ - majorLocs = self.major.locator() - majorTicks = self.get_major_ticks(len(majorLocs)) - self.major.formatter.set_locs(majorLocs) - majorLabels = [self.major.formatter(val, i) - for i, val in enumerate(majorLocs)] - - minorLocs = self.minor.locator() - minorTicks = self.get_minor_ticks(len(minorLocs)) - self.minor.formatter.set_locs(minorLocs) - minorLabels = [self.minor.formatter(val, i) - for i, val in enumerate(minorLocs)] - - major_minor = [ - (majorTicks, majorLocs, majorLabels), - (minorTicks, minorLocs, minorLabels)] - - for group in major_minor: - yield from zip(*group) + major_locs = self.major.locator() + major_ticks = self.get_major_ticks(len(major_locs)) + self.major.formatter.set_locs(major_locs) + major_labels = self.major.formatter.format_ticks(major_locs) + + minor_locs = self.minor.locator() + minor_ticks = self.get_minor_ticks(len(minor_locs)) + self.minor.formatter.set_locs(minor_locs) + minor_labels = self.minor.formatter.format_ticks(minor_locs) + + yield from zip(major_ticks, major_locs, major_labels) + yield from zip(minor_ticks, minor_locs, minor_labels) def get_ticklabel_extents(self, renderer): """ diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 946692f62272..5e9c33ee0c5c 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -789,7 +789,7 @@ def _ticker(self, locator, formatter): self._manual_tick_data_values = b ticks = self._locate(b) formatter.set_locs(b) - ticklabels = [formatter(t, i) for i, t in enumerate(b)] + ticklabels = formatter.format_ticks(b) offset_string = formatter.get_offset() return ticks, ticklabels, offset_string diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index dd40cddcdf95..c2abd3bf08a9 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -820,8 +820,6 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None, """ self._locator = locator self._tz = tz - self._oldticks = np.array([]) - self._oldlabels = None self.defaultfmt = '%Y' # there are 6 levels with each level getting a specific format # 0: mostly years, 1: months, 2: days, @@ -869,95 +867,74 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None, '%Y-%b-%d', '%Y-%b-%d %H:%M'] self.offset_string = '' - self._formatter = DateFormatter(self.defaultfmt, self._tz) self.show_offset = show_offset def __call__(self, x, pos=None): - if hasattr(self._locator, '_get_unit'): - locator_unit_scale = float(self._locator._get_unit()) - else: - locator_unit_scale = 1.0 - ticks = self._locator() - if pos is not None: - if not np.array_equal(ticks, self._oldticks): - - offset_fmt = '' - fmt = self.defaultfmt - self._formatter = DateFormatter(fmt, self._tz) - tickdatetime = [num2date(tick) for tick in ticks] - tickdate = np.array([tdt.timetuple()[:6] - for tdt in tickdatetime]) - - # basic algorithm: - # 1) only display a part of the date if it changes over the - # ticks. - # 2) don't display the smaller part of the date if: - # it is always the same or if it is the start of the - # year, month, day etc. - # fmt for most ticks at this level - fmts = self.formats - # format beginnings of days, months, years, etc... - zerofmts = self.zero_formats - # offset fmt are for the offset in the upper left of the - # or lower right of the axis. - offsetfmts = self.offset_formats - - # determine the level we will label at: - # mostly 0: years, 1: months, 2: days, - # 3: hours, 4: minutes, 5: seconds, 6: microseconds - for level in range(5, -1, -1): - if len(np.unique(tickdate[:, level])) > 1: - break - - # level is the basic level we will label at. - # now loop through and decide the actual ticklabels - zerovals = [0, 1, 1, 0, 0, 0, 0] - ticknew = ['']*len(tickdate) - for nn in range(len(tickdate)): - if level < 5: - if tickdate[nn][level] == zerovals[level]: - fmt = zerofmts[level] - else: - fmt = fmts[level] - else: - # special handling for seconds + microseconds - if (tickdatetime[nn].second == 0 and - tickdatetime[nn].microsecond == 0): - fmt = zerofmts[level] - else: - fmt = fmts[level] - ticknew[nn] = tickdatetime[nn].strftime(fmt) - - # special handling of seconds and microseconds: - # strip extra zeros and decimal if possible... - # this is complicated by two factors. 1) we have some - # level-4 strings here (i.e. 03:00, '0.50000', '1.000') - # 2) we would like to have the same number of decimals for - # each string (i.e. 0.5 and 1.0). - if level >= 5: - trailing_zeros = min( - (len(s) - len(s.rstrip('0')) for s in ticknew - if '.' in s), - default=None) - if trailing_zeros: - for nn in range(len(ticknew)): - if '.' in ticknew[nn]: - ticknew[nn] = \ - ticknew[nn][:-trailing_zeros].rstrip('.') - - result = ticknew[pos] - self._oldticks = ticks - self._oldlabels = ticknew - - # set the offset string: - if self.show_offset: - self.offset_string = tickdatetime[-1].strftime( - offsetfmts[level]) - - result = self._oldlabels[pos] - else: - result = self._formatter(x, pos) - return result + formatter = DateFormatter(self.defaultfmt, self._tz) + return formatter(x, pos=pos) + + def format_ticks(self, values): + tickdatetime = [num2date(value) for value in values] + tickdate = np.array([tdt.timetuple()[:6] for tdt in tickdatetime]) + + # basic algorithm: + # 1) only display a part of the date if it changes over the ticks. + # 2) don't display the smaller part of the date if: + # it is always the same or if it is the start of the + # year, month, day etc. + # fmt for most ticks at this level + fmts = self.formats + # format beginnings of days, months, years, etc... + zerofmts = self.zero_formats + # offset fmt are for the offset in the upper left of the + # or lower right of the axis. + offsetfmts = self.offset_formats + + # determine the level we will label at: + # mostly 0: years, 1: months, 2: days, + # 3: hours, 4: minutes, 5: seconds, 6: microseconds + for level in range(5, -1, -1): + if len(np.unique(tickdate[:, level])) > 1: + break + + # level is the basic level we will label at. + # now loop through and decide the actual ticklabels + zerovals = [0, 1, 1, 0, 0, 0, 0] + labels = [''] * len(tickdate) + for nn in range(len(tickdate)): + if level < 5: + if tickdate[nn][level] == zerovals[level]: + fmt = zerofmts[level] + else: + fmt = fmts[level] + else: + # special handling for seconds + microseconds + if (tickdatetime[nn].second == tickdatetime[nn].microsecond + == 0): + fmt = zerofmts[level] + else: + fmt = fmts[level] + labels[nn] = tickdatetime[nn].strftime(fmt) + + # special handling of seconds and microseconds: + # strip extra zeros and decimal if possible. + # this is complicated by two factors. 1) we have some level-4 strings + # here (i.e. 03:00, '0.50000', '1.000') 2) we would like to have the + # same number of decimals for each string (i.e. 0.5 and 1.0). + if level >= 5: + trailing_zeros = min( + (len(s) - len(s.rstrip('0')) for s in labels if '.' in s), + default=None) + if trailing_zeros: + for nn in range(len(labels)): + if '.' in labels[nn]: + labels[nn] = labels[nn][:-trailing_zeros].rstrip('.') + + if self.show_offset: + # set the offset string: + self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) + + return labels def get_offset(self): return self.offset_string @@ -2064,7 +2041,7 @@ class ConciseDateConverter(DateConverter): """ def __init__(self, formats=None, zero_formats=None, offset_formats=None, - show_offset=True): + show_offset=True): self._formats = formats self._zero_formats = zero_formats self._offset_formats = offset_formats diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index c68ef258dcd9..664d90ecbebc 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -253,6 +253,10 @@ def __call__(self, x, pos=None): """ raise NotImplementedError('Derived must override') + def format_ticks(self, values): + """Return the tick labels for all the ticks at once.""" + return [self(value, i) for i, value in enumerate(values)] + def format_data(self, value): """ Returns the full string representation of the value with the @@ -291,7 +295,7 @@ def fix_minus(self, s): return s def _set_locator(self, locator): - """ Subclasses may want to override this to set a locator. """ + """Subclasses may want to override this to set a locator.""" pass diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index 3a33e98d5760..e730cd8da5d1 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -226,14 +226,12 @@ def get_tick_iterators(self, axes): major = self.axis.major majorLocs = major.locator() major.formatter.set_locs(majorLocs) - majorLabels = [major.formatter(val, i) - for i, val in enumerate(majorLocs)] + majorLabels = major.formatter.format_ticks(majorLocs) minor = self.axis.minor minorLocs = minor.locator() minor.formatter.set_locs(minorLocs) - minorLabels = [minor.formatter(val, i) - for i, val in enumerate(minorLocs)] + minorLabels = minor.formatter.format_ticks(minorLocs) trans_tick = self.get_tick_transform(axes) @@ -323,14 +321,12 @@ def get_tick_iterators(self, axes): major = self.axis.major majorLocs = major.locator() major.formatter.set_locs(majorLocs) - majorLabels = [major.formatter(val, i) - for i, val in enumerate(majorLocs)] + majorLabels = major.formatter.format_ticks(majorLocs) minor = self.axis.minor minorLocs = minor.locator() minor.formatter.set_locs(minorLocs) - minorLabels = [minor.formatter(val, i) - for i, val in enumerate(minorLocs)] + minorLabels = minor.formatter.format_ticks(minorLocs) tr2ax = axes.transData + axes.transAxes.inverted()