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

Skip to content

Commit bb16ae4

Browse files
committed
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
1 parent 6b3c015 commit bb16ae4

File tree

7 files changed

+197
-3
lines changed

7 files changed

+197
-3
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ per-file-ignores =
198198
examples/pyplots/whats_new_99_spines.py: E231, E402
199199
examples/recipes/placing_text_boxes.py: E501
200200
examples/scales/power_norm.py: E402
201+
examples/scales/scales.py: E402
201202
examples/shapes_and_collections/artist_reference.py: E402
202203
examples/shapes_and_collections/collections.py: E402
203204
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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,97 @@ def get_transform(self):
9494
return IdentityTransform()
9595

9696

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

562654

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)