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

Skip to content

Commit 6220a18

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 6220a18

File tree

7 files changed

+199
-3
lines changed

7 files changed

+199
-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: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,98 @@ 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) ->
161+
array-likeThe forward function for the transform
162+
"""
163+
forward, inverse = functions
164+
transform = FuncTransform(forward, inverse)
165+
self._transform = transform
166+
167+
def get_transform(self):
168+
"""
169+
The transform for arbitrary scaling
170+
"""
171+
return self._transform
172+
173+
def set_default_locators_and_formatters(self, axis):
174+
"""
175+
Set the locators and formatters to the same defaults as the
176+
linear scale.
177+
"""
178+
axis.set_major_locator(AutoLocator())
179+
axis.set_major_formatter(ScalarFormatter())
180+
axis.set_minor_formatter(NullFormatter())
181+
# update the minor locator for x and y axis based on rcParams
182+
if (axis.axis_name == 'x' and rcParams['xtick.minor.visible']
183+
or axis.axis_name == 'y' and rcParams['ytick.minor.visible']):
184+
axis.set_minor_locator(AutoMinorLocator())
185+
else:
186+
axis.set_minor_locator(NullLocator())
187+
188+
97189
class LogTransformBase(Transform):
98190
input_dims = 1
99191
output_dims = 1
@@ -557,6 +649,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
557649
'log': LogScale,
558650
'symlog': SymmetricalLogScale,
559651
'logit': LogitScale,
652+
'function': FuncScale,
560653
}
561654

562655

lib/matplotlib/tests/test_scale.py

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

0 commit comments

Comments
 (0)