From 822b01fae6f5c0c315e723c54b4f0ec57fbe6f3b Mon Sep 17 00:00:00 2001 From: Joseph Fox-Rabinovitz Date: Thu, 17 Nov 2016 17:19:53 -0500 Subject: [PATCH 1/2] ENH: Added TransformFormatter to matplotlib.ticker Tests included. Example code provided with some recipes from previous attempt. --- doc/users/whats_new/new_formatters.rst | 29 +++ doc/users/whats_new/percent_formatter.rst | 6 - examples/ticks_and_spines/tick-formatters.py | 29 +-- .../tick_transform_formatter.py | 124 +++++++++++ lib/matplotlib/tests/test_ticker.py | 173 +++++++++++++++ lib/matplotlib/ticker.py | 207 +++++++++++++++++- 6 files changed, 540 insertions(+), 28 deletions(-) create mode 100644 doc/users/whats_new/new_formatters.rst delete mode 100644 doc/users/whats_new/percent_formatter.rst create mode 100644 examples/ticks_and_spines/tick_transform_formatter.py 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..027d75d84dd9 --- /dev/null +++ b/examples/ticks_and_spines/tick_transform_formatter.py @@ -0,0 +1,124 @@ +""" +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`. The following + shortcuts apply when only some of the inputs are specified: + + - none: no-op + - in_start: translation to zero + - out_start: translation from zero + - in_end: scaling to one (divide input by in_end) + - out_end: scaling from one (multiply input by in_end) + - in_start, out_start: translation + - in_end, out_end: scaling (in_start and out_start zero) + - in_start, out_end: in_end=out_end, out_start=0 + - in_end, out_start: in_start=0, out_end=in_end + + Based on the following rules: + + - start missing: set start to zero + - both ends are missing: set ranges to 1.0 + - one end is missing: set it to the other end + """ + if in_end is not None: + in_scale = in_end - in_start + elif out_end is not None: + in_scale = out_end - in_start + else: + in_scale = 1.0 + + if out_end is not None: + out_scale = out_end - out_start + elif in_end is not None: + out_scale = in_end - out_start + else: + out_scale = 1.0 + + 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 (\u00B0C)') +ax1.set_ylabel('Samples (Hex)') +ax2.set_xlabel('Temperature (\u00B0R)') + +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..44b4aca50c75 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -613,3 +613,176 @@ 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): + fmt = self.empty_fmt() + fmt.create_dummy_axis() + return fmt + + @pytest.fixture() + def loc_fmt(self): + 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: + # http://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..f29b270191b2 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 an other 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; From 64354696e1a5b6c89a817d76e418742348d7a994 Mon Sep 17 00:00:00 2001 From: Joseph Fox-Rabinovitz Date: Mon, 27 Feb 2017 10:47:35 -0500 Subject: [PATCH 2/2] Responded to review comments Some small changes and rework of example transform code. --- .../tick_transform_formatter.py | 50 +++++++------------ lib/matplotlib/tests/test_ticker.py | 18 +++---- lib/matplotlib/ticker.py | 2 +- 3 files changed, 26 insertions(+), 44 deletions(-) diff --git a/examples/ticks_and_spines/tick_transform_formatter.py b/examples/ticks_and_spines/tick_transform_formatter.py index 027d75d84dd9..7f1ffd4b93dc 100644 --- a/examples/ticks_and_spines/tick_transform_formatter.py +++ b/examples/ticks_and_spines/tick_transform_formatter.py @@ -37,38 +37,22 @@ class LinearTransform: 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`. The following - shortcuts apply when only some of the inputs are specified: - - - none: no-op - - in_start: translation to zero - - out_start: translation from zero - - in_end: scaling to one (divide input by in_end) - - out_end: scaling from one (multiply input by in_end) - - in_start, out_start: translation - - in_end, out_end: scaling (in_start and out_start zero) - - in_start, out_end: in_end=out_end, out_start=0 - - in_end, out_start: in_start=0, out_end=in_end - - Based on the following rules: - - - start missing: set start to zero - - both ends are missing: set ranges to 1.0 - - one end is missing: set it to the other end + `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. """ - if in_end is not None: - in_scale = in_end - in_start - elif out_end is not None: - in_scale = out_end - in_start - else: - in_scale = 1.0 - - if out_end is not None: - out_scale = out_end - out_start - elif in_end is not None: - out_scale = in_end - out_start - else: - out_scale = 1.0 + 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 @@ -114,9 +98,9 @@ def __call__(self, x): ax1.yaxis.set_major_formatter( TransformFormatter(int, StrMethodFormatter('{x:02X}'))) -ax1.set_xlabel('Temperature (\u00B0C)') +ax1.set_xlabel('Temperature (\N{DEGREE SIGN}C)') ax1.set_ylabel('Samples (Hex)') -ax2.set_xlabel('Temperature (\u00B0R)') +ax2.set_xlabel('Temperature (\N{DEGREE SIGN}R)') ax1.xaxis.tick_top() ax1.xaxis.set_label_position('top') diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 44b4aca50c75..e06012661e6f 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -622,25 +622,23 @@ def transform1(self, x): def transform2(self, x): return 2 * x - @pytest.fixture() + @pytest.fixture def empty_fmt(self): return mticker.TransformFormatter(self.transform1) - @pytest.fixture() - def fmt(self): - fmt = self.empty_fmt() - fmt.create_dummy_axis() - return fmt + @pytest.fixture + def fmt(self, empty_fmt): + empty_fmt.create_dummy_axis() + return empty_fmt - @pytest.fixture() - def loc_fmt(self): - fmt = self.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: - # http://stackoverflow.com/q/41900639/2988730 + # https://stackoverflow.com/q/41900639/2988730 assert empty_fmt.transform == self.transform1 assert isinstance(empty_fmt.formatter, mticker.ScalarFormatter) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index f29b270191b2..59663937725d 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1518,7 +1518,7 @@ def set_formatter(self, formatter): transformed values. The input may be an instance of - :class:`matplotlib.ticker.Formatter` or an other callable with + :class:`matplotlib.ticker.Formatter` or another callable with the same signature. .. note:: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy