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

Skip to content

Commit d403e65

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

File tree

6 files changed

+186
-3
lines changed

6 files changed

+186
-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 maiking special use of the `~.Transform` class, you probably
10+
don't need to use this verbose method, but rather 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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,84 @@ 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: The forward function for the transform
102+
103+
inverse: The inverse of the forward function.
104+
"""
105+
super().__init__()
106+
if callable(forward) and callable(inverse):
107+
self._forward = forward
108+
self._inverse = inverse
109+
else:
110+
raise ValueError('arguments to ArbitraryTransform must '
111+
'be functions')
112+
113+
def transform_non_affine(self, values):
114+
return self._forward(values)
115+
116+
def inverted(self):
117+
return ArbitraryTransform(self._inverse, self._forward)
118+
119+
120+
class ArbitraryScale(ScaleBase):
121+
"""
122+
Provide an arbitrary scale for the axis.
123+
"""
124+
125+
name = 'arbitrary'
126+
127+
def __init__(self, axis, functions):
128+
"""
129+
Parameters
130+
----------
131+
132+
axis: the axis for the scale
133+
134+
functions: (forward, inverse)
135+
two-tuple of the forward and inverse functions for the scale.
136+
"""
137+
forward, inverse = functions
138+
transform = ArbitraryTransform(forward, inverse)
139+
self._transform = transform
140+
141+
def get_transform(self):
142+
"""
143+
The transform for linear scaling is just the
144+
:class:`~matplotlib.transforms.IdentityTransform`.
145+
"""
146+
return self._transform
147+
148+
def set_default_locators_and_formatters(self, axis):
149+
"""
150+
Set the locators and formatters to reasonable defaults for
151+
linear scaling.
152+
"""
153+
axis.set_major_locator(AutoLocator())
154+
axis.set_major_formatter(ScalarFormatter())
155+
axis.set_minor_formatter(NullFormatter())
156+
# update the minor locator for x and y axis based on rcParams
157+
if rcParams['xtick.minor.visible']:
158+
axis.set_minor_locator(AutoMinorLocator())
159+
else:
160+
axis.set_minor_locator(NullLocator())
161+
162+
85163
class LogTransformBase(Transform):
86164
input_dims = 1
87165
output_dims = 1
@@ -545,6 +623,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos):
545623
'log': LogScale,
546624
'symlog': SymmetricalLogScale,
547625
'logit': LogitScale,
626+
'arbitrary': ArbitraryScale,
548627
}
549628

550629

lib/matplotlib/tests/test_scale.py

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

0 commit comments

Comments
 (0)