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

Skip to content

Commit c22847b

Browse files
authored
Merge pull request #12818 from jklymak/enh-arbitrary-scale
Enh arbitrary scale
2 parents a9b4fd5 + 9c74f77 commit c22847b

File tree

7 files changed

+196
-3
lines changed

7 files changed

+196
-3
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ per-file-ignores =
197197
examples/pyplots/whats_new_99_spines.py: E231, E402
198198
examples/recipes/placing_text_boxes.py: E501
199199
examples/scales/power_norm.py: E402
200+
examples/scales/scales.py: E402
200201
examples/shapes_and_collections/artist_reference.py: E402
201202
examples/shapes_and_collections/collections.py: E402
202203
examples/shapes_and_collections/compound_path.py: E402
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:orphan:
2+
3+
New `~.scale.FuncScale` added for arbitrary axes scales
4+
````````````````````````````````````````````````````````
5+
6+
A new `~.scale.FuncScale` class was added (and `~.scale.FuncTransform`)
7+
to allow the user to have arbitrary scale transformations without having to
8+
write a new subclass of `~.scale.ScaleBase`. This can be accessed by
9+
``ax.set_yscale('function', functions=(forward, inverse))``, where
10+
``forward`` and ``inverse`` are callables that return the scale transform and
11+
its inverse. See the last example in :doc:`/gallery/scales/scales`.

examples/scales/custom_scale.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
66
Create a custom scale, by implementing the scaling use for latitude data in a
77
Mercator Projection.
8+
9+
Unless you are making special use of the `~.Transform` class, you probably
10+
don't need to use this verbose method, and instead can use
11+
`~.matplotlib.scale.FuncScale` and the ``'function'`` option of
12+
`~.matplotlib.axes.Axes.set_xscale` and `~.matplotlib.axes.Axes.set_yscale`.
13+
See the last example in :doc:`/gallery/scales/scales`.
814
"""
915

1016
import numpy as np

examples/scales/scales.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
======
55
66
Illustrate the scale transformations applied to axes, e.g. log, symlog, logit.
7+
8+
The last two examples are examples of using the ``'function'`` scale by
9+
supplying forward and inverse functions for the scale transformation.
710
"""
811
import numpy as np
912
import matplotlib.pyplot as plt
10-
from matplotlib.ticker import NullFormatter
13+
from matplotlib.ticker import NullFormatter, FixedLocator
1114

1215
# Fixing random state for reproducibility
1316
np.random.seed(19680801)
@@ -19,8 +22,8 @@
1922
x = np.arange(len(y))
2023

2124
# plot with various axes scales
22-
fig, axs = plt.subplots(2, 2, sharex=True)
23-
fig.subplots_adjust(left=0.08, right=0.98, wspace=0.3)
25+
fig, axs = plt.subplots(3, 2, figsize=(6, 8),
26+
constrained_layout=True)
2427

2528
# linear
2629
ax = axs[0, 0]
@@ -54,4 +57,66 @@
5457
ax.yaxis.set_minor_formatter(NullFormatter())
5558

5659

60+
# Function x**(1/2)
61+
def forward(x):
62+
return x**(1/2)
63+
64+
65+
def inverse(x):
66+
return x**2
67+
68+
69+
ax = axs[2, 0]
70+
ax.plot(x, y)
71+
ax.set_yscale('function', functions=(forward, inverse))
72+
ax.set_title('function: $x^{1/2}$')
73+
ax.grid(True)
74+
ax.yaxis.set_major_locator(FixedLocator(np.arange(0, 1, 0.2)**2))
75+
ax.yaxis.set_major_locator(FixedLocator(np.arange(0, 1, 0.2)))
76+
77+
78+
# Function Mercator transform
79+
def forward(a):
80+
a = np.deg2rad(a)
81+
return np.rad2deg(np.log(np.abs(np.tan(a) + 1.0 / np.cos(a))))
82+
83+
84+
def inverse(a):
85+
a = np.deg2rad(a)
86+
return np.rad2deg(np.arctan(np.sinh(a)))
87+
88+
ax = axs[2, 1]
89+
90+
t = np.arange(-170.0, 170.0, 0.1)
91+
s = t / 2.
92+
93+
ax.plot(t, s, '-', lw=2)
94+
95+
ax.set_yscale('function', functions=(forward, inverse))
96+
ax.set_title('function: Mercator')
97+
ax.grid(True)
98+
ax.set_xlim([-180, 180])
99+
ax.yaxis.set_minor_formatter(NullFormatter())
100+
ax.yaxis.set_major_locator(FixedLocator(np.arange(-90, 90, 30)))
101+
57102
plt.show()
103+
104+
#############################################################################
105+
#
106+
# ------------
107+
#
108+
# References
109+
# """"""""""
110+
#
111+
# The use of the following functions, methods, classes and modules is shown
112+
# in this example:
113+
114+
import matplotlib
115+
matplotlib.axes.Axes.set_yscale
116+
matplotlib.axes.Axes.set_xscale
117+
matplotlib.axis.Axis.set_major_locator
118+
matplotlib.scale.LogitScale
119+
matplotlib.scale.LogScale
120+
matplotlib.scale.LinearScale
121+
matplotlib.scale.SymmetricalLogScale
122+
matplotlib.scale.FuncScale

lib/matplotlib/scale.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,96 @@ def get_transform(self):
103103
return IdentityTransform()
104104

105105

106+
class FuncTransform(Transform):
107+
"""
108+
A simple transform that takes and arbitrary function for the
109+
forward and inverse transform.
110+
"""
111+
112+
input_dims = 1
113+
output_dims = 1
114+
is_separable = True
115+
has_inverse = True
116+
117+
def __init__(self, forward, inverse):
118+
"""
119+
Parameters
120+
----------
121+
122+
forward : callable
123+
The forward function for the transform. This function must have
124+
an inverse and, for best behavior, be monotonic.
125+
It must have the signature::
126+
127+
def forward(values: array-like) -> array-like
128+
129+
inverse : callable
130+
The inverse of the forward function. Signature as ``forward``.
131+
"""
132+
super().__init__()
133+
if callable(forward) and callable(inverse):
134+
self._forward = forward
135+
self._inverse = inverse
136+
else:
137+
raise ValueError('arguments to FuncTransform must '
138+
'be functions')
139+
140+
def transform_non_affine(self, values):
141+
return self._forward(values)
142+
143+
def inverted(self):
144+
return FuncTransform(self._inverse, self._forward)
145+
146+
147+
class FuncScale(ScaleBase):
148+
"""
149+
Provide an arbitrary scale with user-supplied function for the axis.
150+
"""
151+
152+
name = 'function'
153+
154+
def __init__(self, axis, functions):
155+
"""
156+
Parameters
157+
----------
158+
159+
axis: the axis for the scale
160+
161+
functions : (callable, callable)
162+
two-tuple of the forward and inverse functions for the scale.
163+
The forward function must have an inverse and, for best behavior,
164+
be monotonic.
165+
166+
Both functions must have the signature::
167+
168+
def forward(values: array-like) -> array-like
169+
"""
170+
forward, inverse = functions
171+
transform = FuncTransform(forward, inverse)
172+
self._transform = transform
173+
174+
def get_transform(self):
175+
"""
176+
The transform for arbitrary scaling
177+
"""
178+
return self._transform
179+
180+
def set_default_locators_and_formatters(self, axis):
181+
"""
182+
Set the locators and formatters to the same defaults as the
183+
linear scale.
184+
"""
185+
axis.set_major_locator(AutoLocator())
186+
axis.set_major_formatter(ScalarFormatter())
187+
axis.set_minor_formatter(NullFormatter())
188+
# update the minor locator for x and y axis based on rcParams
189+
if (axis.axis_name == 'x' and rcParams['xtick.minor.visible']
190+
or axis.axis_name == 'y' and rcParams['ytick.minor.visible']):
191+
axis.set_minor_locator(AutoMinorLocator())
192+
else:
193+
axis.set_minor_locator(NullLocator())
194+
195+
106196
class LogTransformBase(Transform):
107197
input_dims = 1
108198
output_dims = 1
@@ -566,6 +656,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
566656
'log': LogScale,
567657
'symlog': SymmetricalLogScale,
568658
'logit': LogitScale,
659+
'function': FuncScale,
569660
}
570661

571662

lib/matplotlib/tests/test_scale.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from matplotlib.testing.decorators import image_comparison
22
import matplotlib.pyplot as plt
33
from matplotlib.scale import Log10Transform, InvertedLog10Transform
4+
45
import numpy as np
56
import io
67
import platform
@@ -148,3 +149,21 @@ def test_invalid_log_lims():
148149
with pytest.warns(UserWarning):
149150
ax.set_ylim(top=-1)
150151
assert ax.get_ylim() == original_ylim
152+
153+
154+
@image_comparison(baseline_images=['function_scales'], remove_text=True,
155+
extensions=['png'], style='mpl20')
156+
def test_function_scale():
157+
def inverse(x):
158+
return x**2
159+
160+
def forward(x):
161+
return x**(1/2)
162+
163+
fig, ax = plt.subplots()
164+
165+
x = np.arange(1, 1000)
166+
167+
ax.plot(x, x)
168+
ax.set_xscale('function', functions=(forward, inverse))
169+
ax.set_xlim(1, 1000)

0 commit comments

Comments
 (0)