From 9c74f7793f067048bab2f6256daa8bd1e56938ce Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Thu, 15 Nov 2018 12:08:22 -0800 Subject: [PATCH] ENH: add arbitrary-scale functionality API: add ability to set formatter and locators from scale init FIX: rename to FuncScale DOC: add whats new FIX: simplify Mercator transform TST: simplify test --- .flake8 | 1 + doc/users/next_whats_new/2018-11-25-JMK.rst | 11 +++ examples/scales/custom_scale.py | 6 ++ examples/scales/scales.py | 71 +++++++++++++- lib/matplotlib/scale.py | 91 ++++++++++++++++++ .../test_scale/function_scales.png | Bin 0 -> 12706 bytes lib/matplotlib/tests/test_scale.py | 19 ++++ 7 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 doc/users/next_whats_new/2018-11-25-JMK.rst create mode 100644 lib/matplotlib/tests/baseline_images/test_scale/function_scales.png diff --git a/.flake8 b/.flake8 index b3c38d015490..3719d82131e5 100644 --- a/.flake8 +++ b/.flake8 @@ -197,6 +197,7 @@ per-file-ignores = examples/pyplots/whats_new_99_spines.py: E231, E402 examples/recipes/placing_text_boxes.py: E501 examples/scales/power_norm.py: E402 + examples/scales/scales.py: E402 examples/shapes_and_collections/artist_reference.py: E402 examples/shapes_and_collections/collections.py: E402 examples/shapes_and_collections/compound_path.py: E402 diff --git a/doc/users/next_whats_new/2018-11-25-JMK.rst b/doc/users/next_whats_new/2018-11-25-JMK.rst new file mode 100644 index 000000000000..6915de01700a --- /dev/null +++ b/doc/users/next_whats_new/2018-11-25-JMK.rst @@ -0,0 +1,11 @@ +:orphan: + +New `~.scale.FuncScale` added for arbitrary axes scales +```````````````````````````````````````````````````````` + +A new `~.scale.FuncScale` class was added (and `~.scale.FuncTransform`) +to allow the user to have arbitrary scale transformations without having to +write a new subclass of `~.scale.ScaleBase`. This can be accessed by +``ax.set_yscale('function', functions=(forward, inverse))``, where +``forward`` and ``inverse`` are callables that return the scale transform and +its inverse. See the last example in :doc:`/gallery/scales/scales`. diff --git a/examples/scales/custom_scale.py b/examples/scales/custom_scale.py index ea73b9d45e27..b4a4ea243527 100644 --- a/examples/scales/custom_scale.py +++ b/examples/scales/custom_scale.py @@ -5,6 +5,12 @@ Create a custom scale, by implementing the scaling use for latitude data in a Mercator Projection. + +Unless you are making special use of the `~.Transform` class, you probably +don't need to use this verbose method, and instead can use +`~.matplotlib.scale.FuncScale` and the ``'function'`` option of +`~.matplotlib.axes.Axes.set_xscale` and `~.matplotlib.axes.Axes.set_yscale`. +See the last example in :doc:`/gallery/scales/scales`. """ import numpy as np diff --git a/examples/scales/scales.py b/examples/scales/scales.py index 37a783ae2d30..89352c4351a5 100644 --- a/examples/scales/scales.py +++ b/examples/scales/scales.py @@ -4,10 +4,13 @@ ====== Illustrate the scale transformations applied to axes, e.g. log, symlog, logit. + +The last two examples are examples of using the ``'function'`` scale by +supplying forward and inverse functions for the scale transformation. """ import numpy as np import matplotlib.pyplot as plt -from matplotlib.ticker import NullFormatter +from matplotlib.ticker import NullFormatter, FixedLocator # Fixing random state for reproducibility np.random.seed(19680801) @@ -19,8 +22,8 @@ x = np.arange(len(y)) # plot with various axes scales -fig, axs = plt.subplots(2, 2, sharex=True) -fig.subplots_adjust(left=0.08, right=0.98, wspace=0.3) +fig, axs = plt.subplots(3, 2, figsize=(6, 8), + constrained_layout=True) # linear ax = axs[0, 0] @@ -54,4 +57,66 @@ ax.yaxis.set_minor_formatter(NullFormatter()) +# Function x**(1/2) +def forward(x): + return x**(1/2) + + +def inverse(x): + return x**2 + + +ax = axs[2, 0] +ax.plot(x, y) +ax.set_yscale('function', functions=(forward, inverse)) +ax.set_title('function: $x^{1/2}$') +ax.grid(True) +ax.yaxis.set_major_locator(FixedLocator(np.arange(0, 1, 0.2)**2)) +ax.yaxis.set_major_locator(FixedLocator(np.arange(0, 1, 0.2))) + + +# Function Mercator transform +def forward(a): + a = np.deg2rad(a) + return np.rad2deg(np.log(np.abs(np.tan(a) + 1.0 / np.cos(a)))) + + +def inverse(a): + a = np.deg2rad(a) + return np.rad2deg(np.arctan(np.sinh(a))) + +ax = axs[2, 1] + +t = np.arange(-170.0, 170.0, 0.1) +s = t / 2. + +ax.plot(t, s, '-', lw=2) + +ax.set_yscale('function', functions=(forward, inverse)) +ax.set_title('function: Mercator') +ax.grid(True) +ax.set_xlim([-180, 180]) +ax.yaxis.set_minor_formatter(NullFormatter()) +ax.yaxis.set_major_locator(FixedLocator(np.arange(-90, 90, 30))) + plt.show() + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The use of the following functions, methods, classes and modules is shown +# in this example: + +import matplotlib +matplotlib.axes.Axes.set_yscale +matplotlib.axes.Axes.set_xscale +matplotlib.axis.Axis.set_major_locator +matplotlib.scale.LogitScale +matplotlib.scale.LogScale +matplotlib.scale.LinearScale +matplotlib.scale.SymmetricalLogScale +matplotlib.scale.FuncScale diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index c9f0fea1d791..9a6bef33d13d 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -94,6 +94,96 @@ def get_transform(self): return IdentityTransform() +class FuncTransform(Transform): + """ + A simple transform that takes and arbitrary function for the + forward and inverse transform. + """ + + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self, forward, inverse): + """ + Parameters + ---------- + + forward : callable + The forward function for the transform. This function must have + an inverse and, for best behavior, be monotonic. + It must have the signature:: + + def forward(values: array-like) -> array-like + + inverse : callable + The inverse of the forward function. Signature as ``forward``. + """ + super().__init__() + if callable(forward) and callable(inverse): + self._forward = forward + self._inverse = inverse + else: + raise ValueError('arguments to FuncTransform must ' + 'be functions') + + def transform_non_affine(self, values): + return self._forward(values) + + def inverted(self): + return FuncTransform(self._inverse, self._forward) + + +class FuncScale(ScaleBase): + """ + Provide an arbitrary scale with user-supplied function for the axis. + """ + + name = 'function' + + def __init__(self, axis, functions): + """ + Parameters + ---------- + + axis: the axis for the scale + + functions : (callable, callable) + two-tuple of the forward and inverse functions for the scale. + The forward function must have an inverse and, for best behavior, + be monotonic. + + Both functions must have the signature:: + + def forward(values: array-like) -> array-like + """ + forward, inverse = functions + transform = FuncTransform(forward, inverse) + self._transform = transform + + def get_transform(self): + """ + The transform for arbitrary scaling + """ + return self._transform + + def set_default_locators_and_formatters(self, axis): + """ + Set the locators and formatters to the same defaults as the + linear scale. + """ + axis.set_major_locator(AutoLocator()) + axis.set_major_formatter(ScalarFormatter()) + axis.set_minor_formatter(NullFormatter()) + # update the minor locator for x and y axis based on rcParams + if (axis.axis_name == 'x' and rcParams['xtick.minor.visible'] + or axis.axis_name == 'y' and rcParams['ytick.minor.visible']): + axis.set_minor_locator(AutoMinorLocator()) + else: + axis.set_minor_locator(NullLocator()) + + class LogTransformBase(Transform): input_dims = 1 output_dims = 1 @@ -557,6 +647,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'log': LogScale, 'symlog': SymmetricalLogScale, 'logit': LogitScale, + 'function': FuncScale, } diff --git a/lib/matplotlib/tests/baseline_images/test_scale/function_scales.png b/lib/matplotlib/tests/baseline_images/test_scale/function_scales.png new file mode 100644 index 0000000000000000000000000000000000000000..8789aea213fd5811cc1d70e4850a3c861c2d77ef GIT binary patch literal 12706 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 literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index ebe5c4de9ed7..26822a7adc69 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -1,6 +1,7 @@ from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt from matplotlib.scale import Log10Transform, InvertedLog10Transform + import numpy as np import io import platform @@ -148,3 +149,21 @@ def test_invalid_log_lims(): with pytest.warns(UserWarning): ax.set_ylim(top=-1) assert ax.get_ylim() == original_ylim + + +@image_comparison(baseline_images=['function_scales'], remove_text=True, + extensions=['png'], style='mpl20') +def test_function_scale(): + def inverse(x): + return x**2 + + def forward(x): + return x**(1/2) + + fig, ax = plt.subplots() + + x = np.arange(1, 1000) + + ax.plot(x, x) + ax.set_xscale('function', functions=(forward, inverse)) + ax.set_xlim(1, 1000)