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 000000000000..8789aea213fd Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_scale/function_scales.png differ 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) 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