diff --git a/doc/users/whats_new/new_formatters.rst b/doc/users/whats_new/new_formatters.rst new file mode 100644 index 000000000000..82c4afd7b5e2 --- /dev/null +++ b/doc/users/whats_new/new_formatters.rst @@ -0,0 +1,29 @@ +Two new Formatters added to `matplotlib.ticker` +----------------------------------------------- + +Two new formatters have been added for displaying some specialized +tick labels: + + - :class:`matplotlib.ticker.PercentFormatter` + - :class:`matplotlib.ticker.TransformFormatter` + + +:class:`matplotlib.ticker.PercentFormatter` +``````````````````````````````````````````` + +This new formatter has some nice features like being able to convert +from arbitrary data scales to percents, a customizable percent symbol +and either automatic or manual control over the decimal points. + + +:class:`matplotlib.ticker.TransformFormatter` +``````````````````````````````````````````````` + +A more generic version of :class:`matplotlib.ticker.FuncFormatter` that +allows the tick values to be transformed before being passed to an +underlying formatter. The transformation can yield results of arbitrary +type, so for example, using `int` as the transformation will allow +:class:`matplotlib.ticker.StrMethodFormatter` to use integer format +strings. If the underlying formatter is an instance of +:class:`matplotlib.ticker.Formatter`, it will be configured correctly +through this class. diff --git a/doc/users/whats_new/percent_formatter.rst b/doc/users/whats_new/percent_formatter.rst deleted file mode 100644 index 5948d588ca90..000000000000 --- a/doc/users/whats_new/percent_formatter.rst +++ /dev/null @@ -1,6 +0,0 @@ -Added `matplotlib.ticker.PercentFormatter` ------------------------------------------- - -The new formatter has some nice features like being able to convert from -arbitrary data scales to percents, a customizable percent symbol and -either automatic or manual control over the decimal points. diff --git a/examples/ticks_and_spines/tick-formatters.py b/examples/ticks_and_spines/tick-formatters.py index 13f17ab1ac02..4261dec876ac 100644 --- a/examples/ticks_and_spines/tick-formatters.py +++ b/examples/ticks_and_spines/tick-formatters.py @@ -19,16 +19,16 @@ def setup(ax): ax.set_xlim(0, 5) ax.set_ylim(0, 1) ax.patch.set_alpha(0.0) + ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) + ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) plt.figure(figsize=(8, 6)) -n = 7 +n = 8 # Null formatter ax = plt.subplot(n, 1, 1) setup(ax) -ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) -ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) ax.xaxis.set_major_formatter(ticker.NullFormatter()) ax.xaxis.set_minor_formatter(ticker.NullFormatter()) ax.text(0.0, 0.1, "NullFormatter()", fontsize=16, transform=ax.transAxes) @@ -36,8 +36,6 @@ def setup(ax): # Fixed formatter ax = plt.subplot(n, 1, 2) setup(ax) -ax.xaxis.set_major_locator(ticker.MultipleLocator(1.0)) -ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) majors = ["", "0", "1", "2", "3", "4", "5"] ax.xaxis.set_major_formatter(ticker.FixedFormatter(majors)) minors = [""] + ["%.2f" % (x-int(x)) if (x-int(x)) @@ -54,8 +52,6 @@ def major_formatter(x, pos): ax = plt.subplot(n, 1, 3) setup(ax) -ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) -ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) ax.xaxis.set_major_formatter(ticker.FuncFormatter(major_formatter)) ax.text(0.0, 0.1, 'FuncFormatter(lambda x, pos: "[%.2f]" % x)', fontsize=15, transform=ax.transAxes) @@ -64,8 +60,6 @@ def major_formatter(x, pos): # FormatStr formatter ax = plt.subplot(n, 1, 4) setup(ax) -ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) -ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) ax.xaxis.set_major_formatter(ticker.FormatStrFormatter(">%d<")) ax.text(0.0, 0.1, "FormatStrFormatter('>%d<')", fontsize=15, transform=ax.transAxes) @@ -73,16 +67,12 @@ def major_formatter(x, pos): # Scalar formatter ax = plt.subplot(n, 1, 5) setup(ax) -ax.xaxis.set_major_locator(ticker.AutoLocator()) -ax.xaxis.set_minor_locator(ticker.AutoMinorLocator()) ax.xaxis.set_major_formatter(ticker.ScalarFormatter(useMathText=True)) ax.text(0.0, 0.1, "ScalarFormatter()", fontsize=15, transform=ax.transAxes) # StrMethod formatter ax = plt.subplot(n, 1, 6) setup(ax) -ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) -ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) ax.xaxis.set_major_formatter(ticker.StrMethodFormatter("{x}")) ax.text(0.0, 0.1, "StrMethodFormatter('{x}')", fontsize=15, transform=ax.transAxes) @@ -90,14 +80,19 @@ def major_formatter(x, pos): # Percent formatter ax = plt.subplot(n, 1, 7) setup(ax) -ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) -ax.xaxis.set_minor_locator(ticker.MultipleLocator(0.25)) ax.xaxis.set_major_formatter(ticker.PercentFormatter(xmax=5)) ax.text(0.0, 0.1, "PercentFormatter(xmax=5)", fontsize=15, transform=ax.transAxes) -# Push the top of the top axes outside the figure because we only show the -# bottom spine. +# TransformFormatter +ax = plt.subplot(n, 1, 8) +setup(ax) +ax.xaxis.set_major_formatter(ticker.TransformFormatter(lambda x: 7 - 2 * x)) +ax.text(0.0, 0.1, "TransformFormatter(lambda x: 7 - 2 * x)", + fontsize=15, transform=ax.transAxes) + +# Push the top of the top axes outside the figure because we only show +# the bottom spine. plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=1.05) plt.show() diff --git a/examples/ticks_and_spines/tick_transform_formatter.py b/examples/ticks_and_spines/tick_transform_formatter.py new file mode 100644 index 000000000000..7f1ffd4b93dc --- /dev/null +++ b/examples/ticks_and_spines/tick_transform_formatter.py @@ -0,0 +1,108 @@ +""" +Demo of the `matplotlib.ticker.TransformFormatter` class. + +This code demonstrates two features: + + 1. A linear transformation of the input values. A callable class for + doing the transformation is presented as a recipe here. The data + type of the inputs does not change. + 2. A transformation of the input type. The example here allows + `matplotlib.ticker.StrMethodFormatter` to handle integer formats + ('b', 'o', 'd', 'n', 'x', 'X'), which will normally raise an error + if used directly. This transformation is associated with a + `matplotlib.ticker.MaxNLocator` which has `integer` set to True to + ensure that the inputs are indeed integers. + +The same histogram is plotted in two sub-plots with a shared x-axis. +Each axis shows a different temperature scale: one in degrees Celsius, +one in degrees Rankine (the Fahrenheit analogue of Kelvins). This is one +of the few examples of recognized scientific units that have both a +scale and an offset relative to each other. +""" + +import numpy as np +from matplotlib import pyplot as plt +from matplotlib.axis import Ticker +from matplotlib.ticker import ( + TransformFormatter, StrMethodFormatter, MaxNLocator +) + + +class LinearTransform: + """ + A callable class that transforms input values to output according to + a linear transformation. + """ + + def __init__(self, in_start=0.0, in_end=None, out_start=0.0, out_end=None): + """ + Sets up the transformation such that `in_start` gets mapped to + `out_start` and `in_end` gets mapped to `out_end`. + + Configuration arguments set up the mapping and do not impose + any restriction on the subsequent arguments to `__call__`. None + of the configuration arguments are required. + + A missing `in_start` or `out_start` defaults to zero. A missing + `in_end` and `out_end` default to `in_start + 1.0` and + `out_start + 1.0`, respectively. + + A simple scaling transformation can be created by only + supplying the end arguments. A translation can be obtained by + only supplying the start arguments. + """ + in_scale = 1.0 if in_end is None else in_end - in_start + out_scale = 1.0 if out_end is None else out_end - out_start + + self._scale = out_scale / in_scale + self._offset = out_start - self._scale * in_start + + def __call__(self, x): + """ + Transforms the input value `x` according to the rule set up in + `__init__`. + """ + return x * self._scale + self._offset + +# X-data +temp_C = np.arange(-5.0, 5.1, 0.25) +# Y-data +counts = 15.0 * np.exp(-temp_C**2 / 25) +# Add some noise +counts += np.random.normal(scale=4.0, size=counts.shape) +if counts.min() < 0: + counts += counts.min() + +fig, ax1 = plt.subplots() +ax2 = fig.add_subplot(111, sharex=ax1, sharey=ax1, frameon=False) + +ax1.plot(temp_C, counts, drawstyle='steps-mid') + +ax1.xaxis.set_major_formatter(StrMethodFormatter('{x:0.2f}')) + +# This step is necessary to allow the shared x-axes to have different +# Formatter and Locator objects. +ax2.xaxis.major = Ticker() +# 0C -> 491.67R (definition), -273.15C (0K)->0R (-491.67F)(definition) +ax2.xaxis.set_major_locator(ax1.xaxis.get_major_locator()) +ax2.xaxis.set_major_formatter( + TransformFormatter(LinearTransform(in_start=-273.15, in_end=0, + out_end=491.67), + StrMethodFormatter('{x:0.2f}'))) + +# The y-axes share their locators and formatters, so only one needs to +# be set +ax1.yaxis.set_major_locator(MaxNLocator(integer=True)) +# Setting the transfrom to `int` will only alter the type, not the +# actual value of the ticks +ax1.yaxis.set_major_formatter( + TransformFormatter(int, StrMethodFormatter('{x:02X}'))) + +ax1.set_xlabel('Temperature (\N{DEGREE SIGN}C)') +ax1.set_ylabel('Samples (Hex)') +ax2.set_xlabel('Temperature (\N{DEGREE SIGN}R)') + +ax1.xaxis.tick_top() +ax1.xaxis.set_label_position('top') + +plt.show() diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index d6eee0d6d7bd..e06012661e6f 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -613,3 +613,174 @@ def test_latex(self, is_latex, usetex, expected): fmt = mticker.PercentFormatter(symbol='\\{t}%', is_latex=is_latex) with matplotlib.rc_context(rc={'text.usetex': usetex}): assert fmt.format_pct(50, 100) == expected + + +class TestTransformFormatter(object): + def transform1(self, x): + return -x + + def transform2(self, x): + return 2 * x + + @pytest.fixture + def empty_fmt(self): + return mticker.TransformFormatter(self.transform1) + + @pytest.fixture + def fmt(self, empty_fmt): + empty_fmt.create_dummy_axis() + return empty_fmt + + @pytest.fixture + def loc_fmt(self, fmt): + fmt.set_locs([1, 2, 3]) + return fmt + + def test_attributes(self, empty_fmt): + # Using == is the right way to compare bound methods: + # https://stackoverflow.com/q/41900639/2988730 + assert empty_fmt.transform == self.transform1 + assert isinstance(empty_fmt.formatter, mticker.ScalarFormatter) + + def test_create_dummy_axis(self, empty_fmt): + assert empty_fmt.axis is None + assert empty_fmt.formatter.axis is None + + empty_fmt.create_dummy_axis() + + assert isinstance(empty_fmt.axis, mticker._DummyAxis) + assert isinstance(empty_fmt.formatter.axis, mticker._DummyAxis) + assert empty_fmt.axis is not empty_fmt.formatter.axis + + def test_set_axis(self, fmt): + prev_axis = fmt.axis + prev_inner_axis = fmt.formatter.axis + + fmt.set_axis(mticker._DummyAxis()) + + assert fmt.axis is not None + assert fmt.axis is not prev_axis + assert fmt.formatter.axis is prev_inner_axis + + fmt.set_axis(None) + assert fmt.axis is None + assert fmt.formatter.axis is None + + def test_set_view_interval(self, fmt): + bounds = [100, 200] + xbounds = [self.transform1(x) for x in bounds] + + fmt.set_view_interval(*bounds) + + assert np.array_equal(fmt.axis.get_view_interval(), bounds) + assert np.array_equal(fmt.formatter.axis.get_view_interval(), + xbounds) + + def test_set_data_interval(self, fmt): + bounds = [50, 60] + xbounds = [self.transform1(x) for x in bounds] + + fmt.set_data_interval(*bounds) + + assert np.array_equal(fmt.axis.get_data_interval(), bounds) + assert np.array_equal(fmt.formatter.axis.get_data_interval(), xbounds) + + def test_set_bounds(self, fmt): + bounds = [-7, 7] + xbounds = [self.transform1(x) for x in bounds] + + fmt.set_bounds(*bounds) + + assert np.array_equal(fmt.axis.get_view_interval(), bounds) + assert np.array_equal(fmt.axis.get_data_interval(), bounds) + assert np.array_equal(fmt.formatter.axis.get_view_interval(), xbounds) + assert np.array_equal(fmt.formatter.axis.get_data_interval(), xbounds) + + def test_format_data(self, fmt): + with matplotlib.rc_context({'text.usetex': False}): + assert fmt.format_data(100.0) == '\N{MINUS SIGN}1e2' + + def test_format_data_short(self, fmt): + assert fmt.format_data_short(-200.0) == '{:<12g}'.format(200) + + def test_get_offset(self, fmt): + assert fmt.get_offset() == fmt.formatter.get_offset() + + def test_set_locs(self, fmt): + locs = [1.0, 2.0, 3.0] + xlocs = [self.transform1(x) for x in locs] + + fmt.set_locs(locs) + + # Currently, `fmt.locs is locs` works, but should not be + # tested for since it is not contractually guaranteed. + assert fmt.locs == locs + assert fmt.formatter.locs == xlocs + + def test_clear_locs(self, loc_fmt): + loc_fmt.set_locs(None) + assert loc_fmt.locs is None + assert loc_fmt.formatter.locs is None + + def test_fix_minus(self, fmt): + val = '-19.0' + with matplotlib.rc_context(rc={'text.usetex': False}): + assert fmt.fix_minus(val) == '\N{MINUS SIGN}19.0' + assert fmt.fix_minus(val) == fmt.formatter.fix_minus(val) + + def test_call(self, loc_fmt): + # .__call__ can only be tested after `set_locs` has been called + # at least once because of the default underlying formatter. + with matplotlib.rc_context(rc={'text.usetex': False}): + assert loc_fmt(5.0) == '\N{MINUS SIGN}5' + + def test_set_formatter(self, loc_fmt): + prev_axis, prev_inner_axis = loc_fmt.axis, loc_fmt.formatter.axis + prev_locs, prev_inner_locs = loc_fmt.locs, loc_fmt.formatter.locs + + bounds = [123, 456] + xbounds = [self.transform1(x) for x in bounds] + inner = mticker.PercentFormatter() + + # Set the bounds first to verify that they remain constant while + # inner's axis object changes + loc_fmt.set_bounds(*bounds) + loc_fmt.set_formatter(inner) + + assert loc_fmt.formatter is inner + assert loc_fmt.axis is prev_axis + # Inner axis is NOT preserved when the formatter changes, + # but the bounds are the same + assert loc_fmt.formatter.axis is not prev_inner_axis + assert isinstance(loc_fmt.formatter.axis, mticker._DummyAxis) + assert np.array_equal(loc_fmt.formatter.axis.get_view_interval(), + xbounds) + assert np.array_equal(loc_fmt.formatter.axis.get_data_interval(), + xbounds) + assert loc_fmt.locs is prev_locs + # `is` won't work here because a new copy is made for the new + # formatter. + assert loc_fmt.formatter.locs == prev_inner_locs + + def test_set_transform(self, fmt): + prev_axis, prev_inner_axis = fmt.axis, fmt.formatter.axis + prev_locs, prev_inner_locs = fmt.locs, fmt.formatter.locs + + bounds = [-1, 4] + xbounds = [self.transform2(x) for x in bounds] + xlocs = [self.transform2(x) for x in prev_locs] + + fmt.set_bounds(*bounds) + fmt.set_transform(self.transform2) + + assert fmt.axis is prev_axis + assert np.array_equal(fmt.axis.get_view_interval(), bounds) + assert np.array_equal(fmt.axis.get_data_interval(), bounds) + assert fmt.locs is prev_locs + # Inner axis IS preserved when the formatter changes, but the + # bounds change + assert fmt.formatter.axis is prev_inner_axis + assert np.array_equal(fmt.formatter.axis.get_view_interval(), xbounds) + assert np.array_equal(fmt.formatter.axis.get_data_interval(), xbounds) + assert fmt.formatter.locs is not prev_inner_locs + assert fmt.formatter.locs == xlocs diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index ddfae88619a5..59663937725d 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -151,6 +151,9 @@ :class:`PercentFormatter` Format labels as a percentage +:class:`TransformFormatter` + Generic form of :class:`FuncFormatter` that transforms input values. + You can derive your own formatter from the Formatter base class by simply overriding the ``__call__`` method. The formatter class has access to the axis view and data limits. @@ -192,10 +195,10 @@ 'LogFormatterExponent', 'LogFormatterMathtext', 'IndexFormatter', 'LogFormatterSciNotation', 'LogitFormatter', 'EngFormatter', 'PercentFormatter', - 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator', - 'LinearLocator', 'LogLocator', 'AutoLocator', - 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator', - 'SymmetricalLogLocator', 'LogitLocator') + 'TransformFormatter', 'Locator', 'IndexLocator', + 'FixedLocator', 'NullLocator', 'LinearLocator', 'LogLocator', + 'AutoLocator', 'MultipleLocator', 'MaxNLocator', + 'AutoMinorLocator', 'SymmetricalLogLocator', 'LogitLocator') if six.PY3: @@ -239,6 +242,9 @@ def set_view_interval(self, vmin, vmax): def get_minpos(self): return self._minpos + def set_minpos(self, minpos=0): + self._minpos = minpos + def get_data_interval(self): return self.dataLim.intervalx @@ -251,6 +257,13 @@ def get_tick_space(self): class TickHelper(object): + """ + A base class for objects that interact with an axis and its bounds. + Specifically, this is the base class for `Formatter` and `Locator`. + """ + # Note to the developer: Please make the appropriate changes to + # `TransformFormatter` if methods or other attributes are added or + # removed from this class. axis = None def set_axis(self, axis): @@ -275,6 +288,10 @@ class Formatter(TickHelper): """ Create a string based on a tick value and location. """ + # Note to the developer: Please make the appropriate changes to + # `TransformFormatter` if methods or other attributes are added or + # removed from this class. + # some classes want to see all the locs to help format # individual ones locs = [] @@ -672,7 +689,7 @@ def set_locs(self, locs): Set the locations of the ticks. """ self.locs = locs - if len(self.locs) > 0: + if self.locs is not None and len(self.locs) > 0: vmin, vmax = self.axis.get_view_interval() d = abs(vmax - vmin) if self._useOffset: @@ -1383,6 +1400,186 @@ def symbol(self): self._symbol = symbol +class TransformFormatter(Formatter): + """ + A generalized alternative to `FuncFormatter` that allows the tick + values to be transformed arbitrarily before being passed off to + the formatting function. + + This class accepts a callable transform and a callable formatter. + The transform function may return any type that is acceptable to the + formatter, not necessarily just a `float`. For example, using + ``int`` as the transform allows integer format strings such as + `'{x:d}'` to be used with :class:`StrMethodFormatter`. + + The formatter can be either a :class:`matplotlib.ticker.Formatter` + or any other callable with the signature ``formatter(x, pos=None)``. + If the formatter is a :class:`matplotlib.ticker.Formatter` instance, + most methods of this class will delegate directly to it. Notable + exceptions are `set_locs`, `set_bounds`, `set_view_interval`, and + `set_data_interval`, which will apply the transformation to each + of the passed in values before delegating. In the case of a generic + callable, this class will handle all of the ``Formatter`` + functionality directly. + + If the underlying formatter is a simple callable, setting + ``transform=lambda x: x`` makes this class exactly equivalent to + :class:`matplotlib.ticker.FuncFormatter`. + """ + def __init__(self, transform, formatter=ScalarFormatter()): + # Since we only need to call _update_locs and _transform_axis + # once, only one of set_transform/set_formatter needs to be + # called. set_formatter is chosen because it creates the + # self._need_redirect attribute while set_transform does not. + self.transform = transform + self.set_formatter(formatter) + + def __call__(self, x, pos=None): + return self.formatter(self.transform(x), pos) + + def _redirect(self, name, *args, **kwargs): + """ + Invokes the specified method on the underlying formatter if + possible, or on `self` if not. This method allows the actual + formatter to be a generic callable rather than a Formatter. + """ + if self._need_redirect: + return getattr(self.formatter, name)(*args, **kwargs) + # only evaluate this if necessary + default = getattr(super(TransformFormatter, self), name) + return default(*args, **kwargs) + + def _itransform(self, arg): + """ + Transforms an iterable element-by-element, returns a list. + """ + return [self.transform(x) for x in arg] + + def _invoke_both(self, name, *args): + """ + Invokes the specified method on both the underlying formatter + and on `self`. All arguments are transformed before being passed + to the formatter. If the underlying formatter is not a + `Formatter` instance, it is ignored. The return value from the + underlying formatter is returned when possible. + """ + default = getattr(super(TransformFormatter, self), name) + ret = default(*args) + if self._need_redirect: + xargs = self._itransform(args) + ret = getattr(self.formatter, name)(*xargs) + return ret + + def _update_locs(self): + if self._need_redirect: + if self.locs is None: + self.formatter.set_locs(None) + else: + self.formatter.set_locs(self._itransform(self.locs)) + + def _transform_axis(self): + """ + Ensure that the underlying formatter has a Dummy axis with a + transformed version of the bounds. + """ + if not self._need_redirect: + return + if self.axis is None: + self.formatter.set_axis(None) + else: + minpos = self.transform(self.axis.get_minpos()) + if isinstance(self.formatter.axis, _DummyAxis): + ax_t = self.formatter.axis + ax_t.set_minpos(minpos) + else: + ax_t = _DummyAxis(minpos=minpos) + self.formatter.set_axis(ax_t) + ax_t.set_view_interval( + *self._itransform(self.axis.get_view_interval())) + ax_t.set_data_interval( + *self._itransform(self.axis.get_data_interval())) + + def set_transform(self, transform): + """ + Changes the transform used to convert the values. + + The input is a callable that takes a single argument and returns + a single value. This method will update all the locs and bounds + for the formatter if it is an instance of + :class:`matplotlib.ticker.Formatter`. + """ + self.transform = transform + self._update_locs() + self._transform_axis() + + def set_formatter(self, formatter): + """ + Changes the underlying formatter used to actually format the + transformed values. + + The input may be an instance of + :class:`matplotlib.ticker.Formatter` or another callable with + the same signature. + + .. note:: + + The `axis` attribute of a `Formatter` instance will *always* + be replaced by a dummy axis that contains the transformed + bounds of the outer formatter. + """ + self.formatter = formatter + self._need_redirect = isinstance(formatter, Formatter) + self._update_locs() + self._transform_axis() + + def set_axis(self, ax): + """ + Sets the axis for this formatter. + + If the underlying formatter is a `Formatter` instance, it will + get a dummy axis with bounds adjusted to the transformed version + of the bounds of the new axis. + + Setting the axis to None will also set the underlying + formatter's axis to None. + """ + super(TransformFormatter, self).set_axis(ax) + self._transform_axis() + + def create_dummy_axis(self, **kwargs): + super(TransformFormatter, self).create_dummy_axis(**kwargs) + self._transform_axis() + + def set_view_interval(self, vmin, vmax): + self._invoke_both('set_view_interval', vmin, vmax) + + def set_data_interval(self, vmin, vmax): + self._invoke_both('set_data_interval', vmin, vmax) + + def set_bounds(self, vmin, vmax): + self._invoke_both('set_bounds', vmin, vmax) + + def format_data(self, value): + return self._redirect('format_data', self.transform(value)) + + def format_data_short(self, value): + return self._redirect('format_data_short', self.transform(value)) + + def get_offset(self): + return self._redirect('get_offset') + + def set_locs(self, locs): + """ + Sets the transformed locs to the underlying formatter, if + possible, and the untransformed version to `self`. + """ + super(TransformFormatter, self).set_locs(locs) + self._update_locs() + + def fix_minus(self, s): + return self._redirect('fix_minus', s) + + class Locator(TickHelper): """ Determine the tick locations;