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

Skip to content

Commit 44d5f4a

Browse files
committed
FIX: move towards preoper non-linear axis
1 parent 9509b4e commit 44d5f4a

File tree

1 file changed

+233
-43
lines changed

1 file changed

+233
-43
lines changed

lib/matplotlib/axes/_secondary_axes.py

Lines changed: 233 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,34 +20,6 @@
2020
AutoMinorLocator,
2121
)
2222

23-
class ArbitraryScale(mscale.ScaleBase):
24-
25-
name = 'arbitrary'
26-
27-
def __init__(self, axis, transform=mtransforms.IdentityTransform()):
28-
"""
29-
TODO
30-
"""
31-
self._transform = transform
32-
33-
def get_transform(self):
34-
"""
35-
The transform for linear scaling is just the
36-
:class:`~matplotlib.transforms.IdentityTransform`.
37-
"""
38-
return self._transform
39-
40-
def set_default_locators_and_formatters(self, axis):
41-
"""
42-
Set the locators and formatters to reasonable defaults for
43-
linear scaling.
44-
"""
45-
axis.set_major_locator(AutoLocator())
46-
axis.set_major_formatter(ScalarFormatter())
47-
axis.set_minor_formatter(NullFormatter())
48-
49-
mscale.register_scale(ArbitraryScale)
50-
5123
def _make_inset_locator(rect, trans, parent):
5224
"""
5325
Helper function to locate inset axes, used in
@@ -75,14 +47,31 @@ def inset_locator(ax, renderer):
7547
return inset_locator
7648

7749

50+
def _parse_conversion(name, otherargs):
51+
print(otherargs)
52+
if name == 'inverted':
53+
if otherargs is None:
54+
otherargs = [1.]
55+
otherargs = np.atleast_1d(otherargs)
56+
return _InvertTransform(otherargs[0])
57+
elif name == 'power':
58+
otherargs = np.atleast_1d(otherargs)
59+
return _PowerTransform(a=otherargs[0], b=otherargs[1])
60+
elif name == 'linear':
61+
otherargs = np.asarray(otherargs)
62+
return _LinearTransform(slope=otherargs[0], offset=otherargs[1])
63+
else:
64+
raise ValueError(f'"{name}" not a possible conversion string')
65+
7866
class Secondary_Xaxis(_AxesBase):
7967
"""
8068
General class to hold a Secondary_X/Yaxis.
8169
"""
8270

83-
def __init__(self, parent, location, conversion, **kwargs):
71+
def __init__(self, parent, location, conversion, otherargs=None, **kwargs):
8472
self._conversion = conversion
8573
self._parent = parent
74+
self._otherargs = otherargs
8675

8776
super().__init__(self._parent.figure, [0, 1., 1, 0.0001], **kwargs)
8877

@@ -97,7 +86,7 @@ def __init__(self, parent, location, conversion, **kwargs):
9786
self.set_axis_orientation('top')
9887
else:
9988
self.set_axis_orientation('bottom')
100-
self.set_conversion(conversion)
89+
self.set_conversion(conversion, self._otherargs)
10190

10291
def set_axis_orientation(self, orient):
10392
"""
@@ -179,33 +168,55 @@ def set_xticks(self, ticks, minor=False):
179168
return ret
180169

181170

182-
def set_conversion(self, conversion):
171+
def set_conversion(self, conversion, otherargs=None):
183172
"""
184173
Set how the secondary axis converts limits from the parent axes.
185174
186175
Parameters
187176
----------
188-
conversion : tuple of floats, transform, or string
189-
conversion between the parent xaxis values and the secondary xaxis
190-
values. If a tuple of floats, the floats are polynomial
191-
co-efficients, with the first entry the highest exponent's
192-
co-efficient (i.e. [2, 3, 1] is the same as
193-
``xnew = 2 x**2 + 3 * x + 1``, passed to `numpy.polyval`).
194-
If a function is specified it should accept a float as input and
195-
return a float as the result.
177+
conversion : float, two-tuple of floats, transform, or string
178+
transform between the parent xaxis values and the secondary xaxis
179+
values. If a single floats, a linear transform with the
180+
float as the slope is used. If a 2-tuple of floats, the first
181+
is the slope, and the second the offset.
182+
183+
If a transform is supplied, then the transform must have an
184+
inverse.
185+
186+
For convenience a few common transforms are provided by using
187+
a string:
188+
- 'linear': as above. ``otherargs = (slope, offset)`` must
189+
be supplied.
190+
- 'inverted': a/x where ``otherargs = a`` can be supplied
191+
(defaults to 1)
192+
- 'power': b x^a where ``otherargs = (a, b)`` must be
193+
supplied
194+
196195
"""
197196

198197
# make the _convert function...
199198
if isinstance(conversion, mtransforms.Transform):
200199
self._convert = conversion
201-
self.set_xscale('arbitrary', transform=conversion)
200+
self.set_xscale('arbitrary', transform=conversion.inverted())
201+
elif isinstance(conversion, str):
202+
self._convert = _parse_conversion(conversion, otherargs)
203+
self.set_xscale('arbitrary', transform=self._convert.inverted())
202204
else:
205+
# linear conversion with offset
203206
if isinstance(conversion, numbers.Number):
204207
conversion = np.asanyarray([conversion])
205-
shp = len(conversion)
206-
if shp < 2:
208+
if len(conversion) > 2:
209+
raise ValueError('secondary_axes conversion can be a '
210+
'float, two-tuple of float, a transform '
211+
'with an inverse, or a string.')
212+
elif len(conversion) < 2:
207213
conversion = np.array([conversion, 0.])
208-
self._convert = lambda x: np.polyval(conversion, x)
214+
conversion = _LinearTransform(slope=conversion[0],
215+
offset=conversion[1])
216+
self._convert = conversion
217+
# this will track log/non log so long as the user sets...
218+
self.set_xscale(self._parent.get_xscale())
219+
209220

210221
def draw(self, renderer=None, inframe=False):
211222
"""
@@ -509,3 +520,182 @@ def set_aspect(self, *args, **kwargs):
509520
"""
510521
"""
511522
warnings.warn("Secondary axes can't set the aspect ratio")
523+
524+
525+
class _LinearTransform(mtransforms.AffineBase):
526+
"""
527+
Linear transform 1d
528+
"""
529+
input_dims = 1
530+
output_dims = 1
531+
is_separable = True
532+
has_inverse = True
533+
534+
def __init__(self, slope, offset):
535+
mtransforms.AffineBase.__init__(self)
536+
self._slope = slope
537+
self._offset = offset
538+
539+
def transform_affine(self, values):
540+
return np.asarray(values) * self._slope + self._offset
541+
542+
def inverted(self):
543+
return _InvertedLinearTransform(self._slope, self._offset)
544+
545+
546+
class _InverseLinearTransform(mtransforms.AffineBase):
547+
"""
548+
Inverse linear transform 1d
549+
"""
550+
input_dims = 1
551+
output_dims = 1
552+
is_separable = True
553+
has_inverse = True
554+
555+
def __init__(self, slope, offset):
556+
mtransforms.AffineBase.__init__(self)
557+
self._slope = slope
558+
self._offset = offset
559+
560+
def transform_affine(self, values):
561+
return (np.asarray(values) - self._offset) / self._slope
562+
563+
def inverted(self):
564+
return _LinearTransform(self._slope, self._offset)
565+
566+
def _mask_out_of_bounds(a):
567+
"""
568+
Return a Numpy array where all values outside ]0, 1[ are
569+
replaced with NaNs. If all values are inside ]0, 1[, the original
570+
array is returned.
571+
"""
572+
a = numpy.array(a, float)
573+
mask = (a <= 0.0) | (a >= 1.0)
574+
if mask.any():
575+
return numpy.where(mask, numpy.nan, a)
576+
return a
577+
578+
class _InvertTransform(mtransforms.Transform):
579+
"""
580+
Return a/x
581+
"""
582+
583+
input_dims = 1
584+
output_dims = 1
585+
is_separable = True
586+
has_inverse = True
587+
588+
def __init__(self, fac, out_of_bounds='mask'):
589+
mtransforms.Transform.__init__(self)
590+
self._fac = fac
591+
self.out_of_bounds = out_of_bounds
592+
if self.out_of_bounds == 'mask':
593+
self._handle_out_of_bounds = _mask_out_of_bounds
594+
elif self.out_of_bounds == 'clip':
595+
self._handle_out_of_bounds = _clip_out_of_bounds
596+
else:
597+
raise ValueError("`out_of_bounds` muse be either 'mask' or 'clip'")
598+
599+
def transform_non_affine(self, values):
600+
with np.errstate(divide="ignore", invalid="ignore"):
601+
q = self._fac / values
602+
return q
603+
604+
def inverted(self):
605+
""" we are just our own inverse """
606+
return _InvertTransform(1 / self._fac)
607+
608+
609+
class _PowerTransform(mtransforms.Transform):
610+
"""
611+
Return b * x^a
612+
"""
613+
614+
input_dims = 1
615+
output_dims = 1
616+
is_separable = True
617+
has_inverse = True
618+
619+
def __init__(self, a, b, out_of_bounds='mask'):
620+
mtransforms.Transform.__init__(self)
621+
self._a = a
622+
self._b = b
623+
self.out_of_bounds = out_of_bounds
624+
if self.out_of_bounds == 'mask':
625+
self._handle_out_of_bounds = _mask_out_of_bounds
626+
elif self.out_of_bounds == 'clip':
627+
self._handle_out_of_bounds = _clip_out_of_bounds
628+
else:
629+
raise ValueError("`out_of_bounds` muse be either 'mask' or 'clip'")
630+
631+
def transform_non_affine(self, values):
632+
with np.errstate(divide="ignore", invalid="ignore"):
633+
q = self._b * (values ** self._a)
634+
print('forward', values, q)
635+
return q
636+
637+
def inverted(self):
638+
""" we are just our own inverse """
639+
return _InversePowerTransform(self._a, self._b)
640+
641+
642+
class _InversePowerTransform(mtransforms.Transform):
643+
"""
644+
Return b * x^a
645+
"""
646+
647+
input_dims = 1
648+
output_dims = 1
649+
is_separable = True
650+
has_inverse = True
651+
652+
def __init__(self, a, b, out_of_bounds='mask'):
653+
mtransforms.Transform.__init__(self)
654+
self._a = a
655+
self._b = b
656+
self.out_of_bounds = out_of_bounds
657+
if self.out_of_bounds == 'mask':
658+
self._handle_out_of_bounds = _mask_out_of_bounds
659+
elif self.out_of_bounds == 'clip':
660+
self._handle_out_of_bounds = _clip_out_of_bounds
661+
else:
662+
raise ValueError("`out_of_bounds` must be either 'mask' or 'clip'")
663+
664+
def transform_non_affine(self, values):
665+
with np.errstate(divide="ignore", invalid="ignore"):
666+
q = (values / self._b) ** (1 / self._a)
667+
print(values, q)
668+
return q
669+
670+
def inverted(self):
671+
""" we are just our own inverse """
672+
return _PowerTransform(self._a, self._b)
673+
674+
675+
class ArbitraryScale(mscale.ScaleBase):
676+
677+
name = 'arbitrary'
678+
679+
def __init__(self, axis, transform=mtransforms.IdentityTransform()):
680+
"""
681+
TODO
682+
"""
683+
self._transform = transform
684+
685+
def get_transform(self):
686+
"""
687+
The transform for linear scaling is just the
688+
:class:`~matplotlib.transforms.IdentityTransform`.
689+
"""
690+
return self._transform
691+
692+
def set_default_locators_and_formatters(self, axis):
693+
"""
694+
Set the locators and formatters to reasonable defaults for
695+
linear scaling.
696+
"""
697+
axis.set_major_locator(AutoLocator())
698+
axis.set_major_formatter(ScalarFormatter())
699+
axis.set_minor_formatter(NullFormatter())
700+
701+
mscale.register_scale(ArbitraryScale)

0 commit comments

Comments
 (0)