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

Skip to content

Commit 8548e47

Browse files
committed
ENH: add arbitrary-scale functionality
API: add ability to set formatter and locators from scale init
1 parent ac0525e commit 8548e47

File tree

6 files changed

+197
-3
lines changed

6 files changed

+197
-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

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.ArbitraryScale` and the ``'arbitrary'`` 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: 75 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 ``'arbitrary'`` 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,73 @@
5457
ax.yaxis.set_minor_formatter(NullFormatter())
5558

5659

60+
# Arbitrary 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('arbitrary', functions=(forward, inverse))
72+
ax.set_title('arbitrary: $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+
# Arbitrary Mercator transform
79+
def forward(a):
80+
a = np.deg2rad(a)
81+
# mask values too close to +/- 90, which are ill-defined.
82+
masked = np.ma.masked_where((a <= -np.pi/2 + 0.02) |
83+
(a >= np.pi/2 - 0.02), a)
84+
if masked.mask.any():
85+
return np.rad2deg(
86+
np.ma.log(np.abs(np.ma.tan(masked) + 1.0 / np.ma.cos(masked))))
87+
else:
88+
return np.rad2deg(np.log(np.abs(np.tan(a) + 1.0 / np.cos(a))))
89+
90+
91+
def inverse(a):
92+
a = np.deg2rad(a)
93+
return np.rad2deg(np.arctan(np.sinh(a)))
94+
95+
ax = axs[2, 1]
96+
97+
t = np.arange(-170.0, 170.0, 0.1)
98+
s = t / 2.
99+
100+
ax.plot(t, s, '-', lw=2)
101+
102+
ax.set_yscale('arbitrary', functions=(forward, inverse))
103+
ax.set_title('arbitrary: Mercator')
104+
ax.grid(True)
105+
ax.set_xlim([-180, 180])
106+
ax.yaxis.set_minor_formatter(NullFormatter())
107+
ax.yaxis.set_major_locator(FixedLocator(np.arange(-90, 90, 30)))
108+
57109
plt.show()
110+
111+
#############################################################################
112+
#
113+
# ------------
114+
#
115+
# References
116+
# """"""""""
117+
#
118+
# The use of the following functions, methods, classes and modules is shown
119+
# in this example:
120+
121+
import matplotlib
122+
matplotlib.axes.Axes.set_yscale
123+
matplotlib.axes.Axes.set_xscale
124+
matplotlib.axis.Axis.set_major_locator
125+
matplotlib.scale.LogitScale
126+
matplotlib.scale.LogScale
127+
matplotlib.scale.LinearScale
128+
matplotlib.scale.SymmetricalLogScale
129+
matplotlib.scale.ArbitraryScale

lib/matplotlib/scale.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,97 @@ def get_transform(self):
8282
return IdentityTransform()
8383

8484

85+
class ArbitraryTransform(Transform):
86+
"""
87+
A simple transform that takes and arbitrary function for the
88+
forward and inverse transform.
89+
"""
90+
91+
input_dims = 1
92+
output_dims = 1
93+
is_separable = True
94+
has_inverse = True
95+
96+
def __init__(self, forward, inverse):
97+
"""
98+
Parameters
99+
----------
100+
101+
forward: callable
102+
The forward function for the transform. This function must have
103+
an inverse and, for best behavior, be monotonic.
104+
It must have the signature::
105+
106+
def forward(values: array-like) ->
107+
array-likeThe forward function for the transform
108+
109+
inverse: callable
110+
The inverse of the forward function. Signature as ``forward``.
111+
"""
112+
super().__init__()
113+
if callable(forward) and callable(inverse):
114+
self._forward = forward
115+
self._inverse = inverse
116+
else:
117+
raise ValueError('arguments to ArbitraryTransform must '
118+
'be functions')
119+
120+
def transform_non_affine(self, values):
121+
return self._forward(values)
122+
123+
def inverted(self):
124+
return ArbitraryTransform(self._inverse, self._forward)
125+
126+
127+
class ArbitraryScale(ScaleBase):
128+
"""
129+
Provide an arbitrary scale for the axis.
130+
"""
131+
132+
name = 'arbitrary'
133+
134+
def __init__(self, axis, functions):
135+
"""
136+
Parameters
137+
----------
138+
139+
axis: the axis for the scale
140+
141+
functions: (callable, callable)
142+
two-tuple of the forward and inverse functions for the scale.
143+
The forward function must have an inverse and, for best behavior,
144+
be monotonic.
145+
146+
Both functions must have the signature::
147+
148+
def forward(values: array-like) ->
149+
array-likeThe forward function for the transform
150+
"""
151+
forward, inverse = functions
152+
transform = ArbitraryTransform(forward, inverse)
153+
self._transform = transform
154+
155+
def get_transform(self):
156+
"""
157+
The transform for arbitrary scaling
158+
"""
159+
return self._transform
160+
161+
def set_default_locators_and_formatters(self, axis):
162+
"""
163+
Set the locators and formatters to the same defaults as the
164+
linear scale.
165+
"""
166+
axis.set_major_locator(AutoLocator())
167+
axis.set_major_formatter(ScalarFormatter())
168+
axis.set_minor_formatter(NullFormatter())
169+
# update the minor locator for x and y axis based on rcParams
170+
if rcParams['xtick.minor.visible']:
171+
axis.set_minor_locator(AutoMinorLocator())
172+
else:
173+
axis.set_minor_locator(NullLocator())
174+
175+
85176
class LogTransformBase(Transform):
86177
input_dims = 1
87178
output_dims = 1
@@ -545,6 +636,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
545636
'log': LogScale,
546637
'symlog': SymmetricalLogScale,
547638
'logit': LogitScale,
639+
'arbitrary': ArbitraryScale,
548640
}
549641

550642

lib/matplotlib/tests/test_scale.py

Lines changed: 23 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,24 @@ 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=['arbitrary_scales'], remove_text=True,
156+
extensions=['png'], style='mpl20')
157+
def test_arbitrary_scale():
158+
def inverse(x):
159+
return x**2
160+
161+
def forward(x):
162+
good = x >= 0
163+
y = np.full_like(x, np.NaN)
164+
y[good] = x[good]**(1/2)
165+
return y
166+
167+
fig, ax = plt.subplots()
168+
169+
x = np.arange(1, 1000)
170+
171+
ax.plot(x, x)
172+
ax.set_xscale('arbitrary', functions=(forward, inverse))
173+
ax.set_xlim(1, 1000)

0 commit comments

Comments
 (0)