Skip to content

New Formatter API #5804

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

Closed
wants to merge 3 commits into from
Closed
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
4 changes: 2 additions & 2 deletions doc/users/recipes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ the x locations are formatted the same way the tick labels are, e.g.,
a higher degree of precision, e.g., giving us the exact date out mouse is
hovering over. To fix the first problem, we can use
:func:`matplotlib.figure.Figure.autofmt_xdate` and to fix the second
problem we can use the ``ax.fmt_xdata`` attribute which can be set to
problem we can use the ``ax.format_xdata`` attribute which can be set to
any function that takes a scalar and returns a string. matplotlib has
a number of date formatters built in, so we'll use one of those.

Expand All @@ -157,7 +157,7 @@ a number of date formatters built in, so we'll use one of those.
# use a more precise date string for the x axis locations in the
# toolbar
import matplotlib.dates as mdates
ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
ax.format_xdata = mdates.DateFormatter('%Y-%m-%d').format_for_cursor
plt.title('fig.autofmt_xdate fixes the labels')

Now when you hover your mouse over the plotted data, you'll see date
Expand Down
6 changes: 3 additions & 3 deletions examples/api/custom_projection_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,12 @@ class DegreeFormatter(Formatter):
"""

def __init__(self, round_to=1.0):
super(HammerAxes.DegreeFormatter, self).__init__()
self._round_to = round_to

def __call__(self, x, pos=None):
def format_for_tick(self, x, pos=None):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again a much needed improvement IMHO. If we really want __call__() functionality I would just call format_for_tick() without any additional code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropping __call__ is a pretty big API change.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with a call with non-annotated code is that it is hard to interpret.

The obvious solution is to document carefully what the call method does. The only downside is that this documentation is not directly obvious for a derived class if that class is not documented properly.

To keep Ticker etc a callable object, either format_for_tick() method could be invoked in the call() although that might be a bit artificial thinking of it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not quite follow this. The __call__ method is no different of a documentation burden than any other method and IPython's help prints it out nicely:


In [9]: ax = plt.gca()

In [10]: fmt = ax.xaxis.get_major_formatter()

In [11]: ? fmt
Type:           ScalarFormatter
String form:    <matplotlib.ticker.ScalarFormatter object at 0x7f836fcd22b0>
File:           ~/src/p/matplotlib/lib/matplotlib/ticker.py
Signature:      fmt(x, pos=None)
Docstring:
Tick location is a plain old number.  If useOffset==True and the data range
is much smaller than the data average, then an offset will be determined
such that the tick labels are meaningful. Scientific notation is used for
data < 10^-n or data >= 10^m, where n and m are the power limits set using
set_powerlimits((n,m)). The defaults for these are controlled by the
axes.formatter.limits rc parameter.
Call docstring: Return the format for tick val *x* at position *pos*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The burden is the same. I am just trying to say that if documentation is omitted - which might be the case for a quickly written and very simple derived class - the name of the method (i.e. __call__) cannot help you to understand what it does.

degrees = np.round(np.degrees(x) / self._round_to) * self._round_to
# \u00b0 : degree symbol
return "%d\u00b0" % degrees
return "%d\N{DEGREE SIGN}" % degrees

def set_longitude_grid(self, degrees):
"""
Expand Down
5 changes: 2 additions & 3 deletions examples/api/custom_scale_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ def set_default_locators_and_formatters(self, axis):
value::
"""
class DegreeFormatter(Formatter):
def __call__(self, x, pos=None):
# \u00b0 : degree symbol
return "%d\u00b0" % (np.degrees(x))
def format_for_tick(self, x, pos=None):
return "%d\N{DEGREE SIGN}" % (np.degrees(x))

axis.set_major_locator(FixedLocator(
np.radians(np.arange(-90, 90, 10))))
Expand Down
2 changes: 1 addition & 1 deletion examples/api/date_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
# format the coords message box
def price(x):
return '$%1.2f' % x
ax.format_xdata = mdates.DateFormatter('%Y-%m-%d')
ax.format_xdata = mdates.DateFormatter('%Y-%m-%d').format_for_cursor
ax.format_ydata = price
ax.grid(True)

Expand Down
3 changes: 1 addition & 2 deletions examples/pylab_examples/contour_label_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ def __repr__(self):

CS = plt.contour(X, Y, 100**Z, locator=plt.LogLocator())
fmt = ticker.LogFormatterMathtext()
fmt.create_dummy_axis()
plt.clabel(CS, CS.levels, fmt=fmt)
plt.clabel(CS, CS.levels, fmt=fmt.format_for_tick)
plt.title("$100^Z$")

plt.show()
4 changes: 2 additions & 2 deletions examples/pylab_examples/date_demo1.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
# format the coords message box
def price(x):
return '$%1.2f' % x
ax.fmt_xdata = DateFormatter('%Y-%m-%d')
ax.fmt_ydata = price
ax.format_xdata = DateFormatter('%Y-%m-%d').format_for_cursor
ax.format_ydata = price
ax.grid(True)

fig.autofmt_xdate()
Expand Down
2 changes: 1 addition & 1 deletion examples/pylab_examples/date_demo_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
ax.xaxis.set_minor_locator(HourLocator(arange(0, 25, 6)))
ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d'))

ax.fmt_xdata = DateFormatter('%Y-%m-%d %H:%M:%S')
ax.format_xdata = DateFormatter('%Y-%m-%d %H:%M:%S').format_for_cursor
fig.autofmt_xdate()

plt.show()
3 changes: 2 additions & 1 deletion examples/pylab_examples/date_index_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@

class MyFormatter(Formatter):
def __init__(self, dates, fmt='%Y-%m-%d'):
super(MyFormatter, self).__init__()
self.dates = dates
self.fmt = fmt

def __call__(self, x, pos=0):
def format_for_tick(self, x, pos=0):
'Return the label for time x at position pos'
ind = int(round(x))
if ind >= len(self.dates) or ind < 0:
Expand Down
2 changes: 1 addition & 1 deletion examples/pylab_examples/finance_work2.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def moving_average_convergence(x, nslow=26, nfast=12):
label.set_rotation(30)
label.set_horizontalalignment('right')

ax.fmt_xdata = mdates.DateFormatter('%Y-%m-%d')
ax.format_xdata = mdates.DateFormatter('%Y-%m-%d').format_for_cursor


class MyLocator(mticker.MaxNLocator):
Expand Down
23 changes: 1 addition & 22 deletions examples/pylab_examples/newscalarformatter_demo.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,6 @@
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import OldScalarFormatter, ScalarFormatter

# Example 1
x = np.arange(0, 1, .01)
fig, [[ax1, ax2], [ax3, ax4]] = plt.subplots(2, 2, figsize=(6, 6))
fig.text(0.5, 0.975, 'The old formatter',
horizontalalignment='center', verticalalignment='top')
ax1.plot(x * 1e5 + 1e10, x * 1e-10 + 1e-5)
ax1.xaxis.set_major_formatter(OldScalarFormatter())
ax1.yaxis.set_major_formatter(OldScalarFormatter())

ax2.plot(x * 1e5, x * 1e-4)
ax2.xaxis.set_major_formatter(OldScalarFormatter())
ax2.yaxis.set_major_formatter(OldScalarFormatter())

ax3.plot(-x * 1e5 - 1e10, -x * 1e-5 - 1e-10)
ax3.xaxis.set_major_formatter(OldScalarFormatter())
ax3.yaxis.set_major_formatter(OldScalarFormatter())

ax4.plot(-x * 1e5, -x * 1e-4)
ax4.xaxis.set_major_formatter(OldScalarFormatter())
ax4.yaxis.set_major_formatter(OldScalarFormatter())
from matplotlib.ticker import ScalarFormatter

# Example 2
x = np.arange(0, 1, .01)
Expand Down
38 changes: 10 additions & 28 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2566,19 +2566,19 @@ def ticklabel_format(self, **kwargs):
self.yaxis.major.formatter.set_scientific(sb)
if scilimits is not None:
if axis == 'both' or axis == 'x':
self.xaxis.major.formatter.set_powerlimits(scilimits)
self.xaxis.major.formatter.powerlimits = scilimits
if axis == 'both' or axis == 'y':
self.yaxis.major.formatter.set_powerlimits(scilimits)
self.yaxis.major.formatter.powerlimits = scilimits
if useOffset is not None:
if axis == 'both' or axis == 'x':
self.xaxis.major.formatter.set_useOffset(useOffset)
self.xaxis.major.formatter.use_offset = useOffset
if axis == 'both' or axis == 'y':
self.yaxis.major.formatter.set_useOffset(useOffset)
self.yaxis.major.formatter.use_offset = useOffset
if useLocale is not None:
if axis == 'both' or axis == 'x':
self.xaxis.major.formatter.set_useLocale(useLocale)
self.xaxis.major.formatter.use_locale = useLocale
if axis == 'both' or axis == 'y':
self.yaxis.major.formatter.set_useLocale(useLocale)
self.yaxis.major.formatter.use_locale = useLocale
except AttributeError:
raise AttributeError(
"This method only works with the ScalarFormatter.")
Expand Down Expand Up @@ -3268,30 +3268,12 @@ def yaxis_date(self, tz=None):
self.yaxis.axis_date(tz)

def format_xdata(self, x):
"""
Return *x* string formatted. This function will use the attribute
self.fmt_xdata if it is callable, else will fall back on the xaxis
major formatter
"""
try:
return self.fmt_xdata(x)
except TypeError:
func = self.xaxis.get_major_formatter().format_data_short
val = func(x)
return val
return (self.fmt_xdata(x) if self.fmt_xdata is not None
else self.xaxis.get_major_formatter().format_for_cursor(x))

def format_ydata(self, y):
"""
Return y string formatted. This function will use the
:attr:`fmt_ydata` attribute if it is callable, else will fall
back on the yaxis major formatter
"""
try:
return self.fmt_ydata(y)
except TypeError:
func = self.yaxis.get_major_formatter().format_data_short
val = func(y)
return val
return (self.fmt_ydata(y) if self.fmt_ydata is not None
else self.yaxis.get_major_formatter().format_for_cursor(y))

def format_coord(self, x, y):
"""Return a format string formatting the *x*, *y* coord"""
Expand Down
12 changes: 6 additions & 6 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,14 +901,14 @@ def iter_ticks(self):
"""
majorLocs = self.major.locator()
majorTicks = self.get_major_ticks(len(majorLocs))
self.major.formatter.set_locs(majorLocs)
majorLabels = [self.major.formatter(val, i)
self.major.formatter.locs = majorLocs
majorLabels = [self.major.formatter.format_for_tick(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)
self.minor.formatter.locs = minorLocs
minorLabels = [self.minor.formatter.format_for_tick(val, i)
for i, val in enumerate(minorLocs)]

major_minor = [
Expand Down Expand Up @@ -1087,7 +1087,7 @@ def get_tightbbox(self, renderer):
self._update_label_position(ticklabelBoxes, ticklabelBoxes2)

self._update_offset_text_position(ticklabelBoxes, ticklabelBoxes2)
self.offsetText.set_text(self.major.formatter.get_offset())
self.offsetText.set_text(self.major.formatter.offset_text)

bb = []

Expand Down Expand Up @@ -1140,7 +1140,7 @@ def draw(self, renderer, *args, **kwargs):
self.label.draw(renderer)

self._update_offset_text_position(ticklabelBoxes, ticklabelBoxes2)
self.offsetText.set_text(self.major.formatter.get_offset())
self.offsetText.set_text(self.major.formatter.offset_text)
self.offsetText.draw(renderer)

if 0: # draw the bounding boxes around the text for debug
Expand Down
25 changes: 12 additions & 13 deletions lib/matplotlib/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,16 +370,16 @@ def update_ticks(self):
called whenever the tick locator and/or tick formatter changes.
"""
ax = self.ax
ticks, ticklabels, offset_string = self._ticker()
ticks, ticklabels, offset_text = self._ticker()
if self.orientation == 'vertical':
ax.yaxis.set_ticks(ticks)
ax.set_yticklabels(ticklabels)
ax.yaxis.get_major_formatter().set_offset_string(offset_string)
ax.yaxis.get_major_formatter().offset_text = offset_text

else:
ax.xaxis.set_ticks(ticks)
ax.set_xticklabels(ticklabels)
ax.xaxis.get_major_formatter().set_offset_string(offset_string)
ax.xaxis.get_major_formatter().offset_text = offset_text

def set_ticks(self, ticks, update_ticks=True):
"""
Expand Down Expand Up @@ -584,12 +584,12 @@ def _ticker(self):
intv = self._values[0], self._values[-1]
else:
intv = self.vmin, self.vmax
locator.create_dummy_axis(minpos=intv[0])
formatter.create_dummy_axis(minpos=intv[0])
locator.set_view_interval(*intv)
locator.set_data_interval(*intv)
formatter.set_view_interval(*intv)
formatter.set_data_interval(*intv)
locator.set_axis({"minpos": intv[0]})
formatter.set_axis({"minpos": intv[0]})
locator.axis.set_view_interval(*intv)
locator.axis.set_data_interval(*intv)
formatter.axis.set_view_interval(*intv)
formatter.axis.set_data_interval(*intv)

b = np.array(locator())
if isinstance(locator, ticker.LogLocator):
Expand All @@ -599,10 +599,9 @@ def _ticker(self):
eps = (intv[1] - intv[0]) * 1e-10
b = b[(b <= intv[1] + eps) & (b >= intv[0] - eps)]
ticks = self._locate(b)
formatter.set_locs(b)
ticklabels = [formatter(t, i) for i, t in enumerate(b)]
offset_string = formatter.get_offset()
return ticks, ticklabels, offset_string
formatter.locs = b
ticklabels = [formatter.format_for_tick(t, i) for i, t in enumerate(b)]
return ticks, ticklabels, formatter.offset_text

def _process_values(self, b=None):
'''
Expand Down
22 changes: 7 additions & 15 deletions lib/matplotlib/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,12 +470,13 @@ def __init__(self, fmt, tz=None):
*fmt* is a :func:`strftime` format string; *tz* is the
:class:`tzinfo` instance.
"""
super(DateFormatter, self).__init__()
if tz is None:
tz = _get_rc_timezone()
self.fmt = fmt
self.tz = tz

def __call__(self, x, pos=0):
def format_for_tick(self, x, pos=0):
if x == 0:
raise ValueError('DateFormatter found a value of x=0, which is '
'an illegal date. This usually occurs because '
Expand Down Expand Up @@ -601,13 +602,14 @@ def __init__(self, t, fmt, tz=None):
*t* is a sequence of dates (floating point days). *fmt* is a
:func:`strftime` format string.
"""
super(IndexDateFormatter, self).__init__()
if tz is None:
tz = _get_rc_timezone()
self.t = t
self.fmt = fmt
self.tz = tz

def __call__(self, x, pos=0):
def format_for_tick(self, x, pos=0):
'Return the label for time *x* at position *pos*'
ind = int(np.round(x))
if ind >= len(self.t) or ind <= 0:
Expand Down Expand Up @@ -681,6 +683,7 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'):
if none of the values in ``self.scaled`` are greater than the unit
returned by ``locator._get_unit()``.
"""
super(AutoDateFormatter, self).__init__()
self._locator = locator
self._tz = tz
self.defaultfmt = defaultfmt
Expand All @@ -694,7 +697,7 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d'):
1. / (SEC_PER_DAY):
rcParams['date.autoformatter.second']}

def __call__(self, x, pos=None):
def format_for_tick(self, x, pos=None):
locator_unit_scale = float(self._locator._get_unit())
fmt = self.defaultfmt

Expand All @@ -706,7 +709,7 @@ def __call__(self, x, pos=None):

if isinstance(fmt, six.string_types):
self._formatter = DateFormatter(fmt, self._tz)
result = self._formatter(x, pos)
result = self._formatter.format_for_tick(x, pos)
elif six.callable(fmt):
result = fmt(x, pos)
else:
Expand Down Expand Up @@ -1130,9 +1133,6 @@ def get_locator(self, dmin, dmax):
locator = MicrosecondLocator(interval, tz=self.tz)

locator.set_axis(self.axis)

locator.set_view_interval(*self.axis.get_view_interval())
locator.set_data_interval(*self.axis.get_data_interval())
return locator


Expand Down Expand Up @@ -1357,14 +1357,6 @@ def set_axis(self, axis):
self._wrapped_locator.set_axis(axis)
return DateLocator.set_axis(self, axis)

def set_view_interval(self, vmin, vmax):
self._wrapped_locator.set_view_interval(vmin, vmax)
return DateLocator.set_view_interval(self, vmin, vmax)

def set_data_interval(self, vmin, vmax):
self._wrapped_locator.set_data_interval(vmin, vmax)
return DateLocator.set_data_interval(self, vmin, vmax)

def __call__(self):
# if no data have been set, this will tank with a ValueError
try:
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/projections/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ class ThetaFormatter(Formatter):
unit of radians into degrees and adds a degree symbol.
"""
def __init__(self, round_to=1.0):
super(GeoAxes.ThetaFormatter, self).__init__()
self._round_to = round_to

def __call__(self, x, pos=None):
def format_for_tick(self, x, pos=None):
degrees = (x / np.pi) * 180.0
degrees = np.round(degrees / self._round_to) * self._round_to
if rcParams['text.usetex'] and not rcParams['text.latex.unicode']:
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/projections/polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class ThetaFormatter(Formatter):
Used to format the *theta* tick labels. Converts the native
unit of radians into degrees and adds a degree symbol.
"""
def __call__(self, x, pos=None):
def format_for_tick(self, x, pos=None):
# \u00b0 : degree symbol
if rcParams['text.usetex'] and not rcParams['text.latex.unicode']:
return r"$%0.0f^\circ$" % ((x / np.pi) * 180.0)
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
from matplotlib.patches import Polygon, Rectangle, Circle, Arrow
from matplotlib.widgets import SubplotTool, Button, Slider, Widget

from .ticker import TickHelper, Formatter, FixedFormatter, NullFormatter,\
from .ticker import Formatter, FixedFormatter, NullFormatter,\
FuncFormatter, FormatStrFormatter, ScalarFormatter,\
LogFormatter, LogFormatterExponent, LogFormatterMathtext,\
Locator, IndexLocator, FixedLocator, NullLocator,\
Expand Down
Loading