Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Enh arbitrary scale #12818

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

Merged
merged 1 commit into from
Jan 21, 2019
Merged
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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions doc/users/next_whats_new/2018-11-25-JMK.rst
Original file line number Diff line number Diff line change
@@ -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`.
6 changes: 6 additions & 0 deletions examples/scales/custom_scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 68 additions & 3 deletions examples/scales/scales.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]
Expand Down Expand Up @@ -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
91 changes: 91 additions & 0 deletions lib/matplotlib/scale.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Contributor

@anntzer anntzer Nov 26, 2018

Choose a reason for hiding this comment

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

Is there a reason why FuncTransform takes the two functions as separate args but FuncScale takes the pair as single arg? (Perhaps there is, didn't think more than that about it.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Just because the kwarg gets passed in as a tuple, whereas to me it makes more sense for the transform to take them separately. But I don't feel very strongly about it.

"""
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())

Choose a reason for hiding this comment

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

Can you incoorporate the fix from #12938 here as well?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry for the delay - Done!

else:
axis.set_minor_locator(NullLocator())


class LogTransformBase(Transform):
input_dims = 1
output_dims = 1
Expand Down Expand Up @@ -557,6 +647,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
'log': LogScale,
'symlog': SymmetricalLogScale,
'logit': LogitScale,
'function': FuncScale,
}


Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions lib/matplotlib/tests/test_scale.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)