Skip to content

Let Formatters format all ticks at once. #12909

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions doc/api/next_api_changes/2018-01-28-AL.rst
Original file line number Diff line number Diff line change
@@ -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`.
30 changes: 12 additions & 18 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
155 changes: 66 additions & 89 deletions lib/matplotlib/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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


Expand Down
12 changes: 4 additions & 8 deletions lib/mpl_toolkits/axisartist/axislines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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()

Expand Down