20
20
AutoMinorLocator ,
21
21
)
22
22
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
-
51
23
def _make_inset_locator (rect , trans , parent ):
52
24
"""
53
25
Helper function to locate inset axes, used in
@@ -75,14 +47,31 @@ def inset_locator(ax, renderer):
75
47
return inset_locator
76
48
77
49
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
+
78
66
class Secondary_Xaxis (_AxesBase ):
79
67
"""
80
68
General class to hold a Secondary_X/Yaxis.
81
69
"""
82
70
83
- def __init__ (self , parent , location , conversion , ** kwargs ):
71
+ def __init__ (self , parent , location , conversion , otherargs = None , ** kwargs ):
84
72
self ._conversion = conversion
85
73
self ._parent = parent
74
+ self ._otherargs = otherargs
86
75
87
76
super ().__init__ (self ._parent .figure , [0 , 1. , 1 , 0.0001 ], ** kwargs )
88
77
@@ -97,7 +86,7 @@ def __init__(self, parent, location, conversion, **kwargs):
97
86
self .set_axis_orientation ('top' )
98
87
else :
99
88
self .set_axis_orientation ('bottom' )
100
- self .set_conversion (conversion )
89
+ self .set_conversion (conversion , self . _otherargs )
101
90
102
91
def set_axis_orientation (self , orient ):
103
92
"""
@@ -179,33 +168,55 @@ def set_xticks(self, ticks, minor=False):
179
168
return ret
180
169
181
170
182
- def set_conversion (self , conversion ):
171
+ def set_conversion (self , conversion , otherargs = None ):
183
172
"""
184
173
Set how the secondary axis converts limits from the parent axes.
185
174
186
175
Parameters
187
176
----------
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
+
196
195
"""
197
196
198
197
# make the _convert function...
199
198
if isinstance (conversion , mtransforms .Transform ):
200
199
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 ())
202
204
else :
205
+ # linear conversion with offset
203
206
if isinstance (conversion , numbers .Number ):
204
207
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 :
207
213
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
+
209
220
210
221
def draw (self , renderer = None , inframe = False ):
211
222
"""
@@ -509,3 +520,182 @@ def set_aspect(self, *args, **kwargs):
509
520
"""
510
521
"""
511
522
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