8
8
9
9
from matplotlib .cbook import dedent
10
10
from matplotlib .ticker import (NullFormatter , ScalarFormatter ,
11
- LogFormatterMathtext )
11
+ LogFormatterMathtext , LogitFormatter )
12
12
from matplotlib .ticker import (NullLocator , LogLocator , AutoLocator ,
13
- SymmetricalLogLocator )
13
+ SymmetricalLogLocator , LogitLocator )
14
14
from matplotlib .transforms import Transform , IdentityTransform
15
15
from matplotlib import docstring
16
16
@@ -86,8 +86,8 @@ def get_transform(self):
86
86
87
87
def _mask_non_positives (a ):
88
88
"""
89
- Return a Numpy masked array where all non-positive values are
90
- replaced with NaNs. If there are no non-positive values, the
89
+ Return a Numpy array where all non-positive values are
90
+ replaced with NaNs. If there are no non-positive values, the
91
91
original array is returned.
92
92
"""
93
93
mask = a <= 0.0
@@ -97,6 +97,7 @@ def _mask_non_positives(a):
97
97
98
98
99
99
def _clip_non_positives (a ):
100
+ a = np .array (a , float )
100
101
a [a <= 0.0 ] = 1e-300
101
102
return a
102
103
@@ -120,8 +121,6 @@ class Log10Transform(LogTransformBase):
120
121
121
122
def transform_non_affine (self , a ):
122
123
a = self ._handle_nonpos (a * 10.0 )
123
- if isinstance (a , ma .MaskedArray ):
124
- return ma .log10 (a )
125
124
return np .log10 (a )
126
125
127
126
def inverted (self ):
@@ -147,8 +146,6 @@ class Log2Transform(LogTransformBase):
147
146
148
147
def transform_non_affine (self , a ):
149
148
a = self ._handle_nonpos (a * 2.0 )
150
- if isinstance (a , ma .MaskedArray ):
151
- return ma .log (a ) / np .log (2 )
152
149
return np .log2 (a )
153
150
154
151
def inverted (self ):
@@ -174,8 +171,6 @@ class NaturalLogTransform(LogTransformBase):
174
171
175
172
def transform_non_affine (self , a ):
176
173
a = self ._handle_nonpos (a * np .e )
177
- if isinstance (a , ma .MaskedArray ):
178
- return ma .log (a )
179
174
return np .log (a )
180
175
181
176
def inverted (self ):
@@ -212,8 +207,6 @@ def __init__(self, base, nonpos):
212
207
213
208
def transform_non_affine (self , a ):
214
209
a = self ._handle_nonpos (a * self .base )
215
- if isinstance (a , ma .MaskedArray ):
216
- return ma .log (a ) / np .log (self .base )
217
210
return np .log (a ) / np .log (self .base )
218
211
219
212
def inverted (self ):
@@ -478,10 +471,112 @@ def get_transform(self):
478
471
return self ._transform
479
472
480
473
474
+ def _mask_non_logit (a ):
475
+ """
476
+ Return a Numpy array where all values outside ]0, 1[ are
477
+ replaced with NaNs. If all values are inside ]0, 1[, the original
478
+ array is returned.
479
+ """
480
+ mask = (a <= 0.0 ) | (a >= 1.0 )
481
+ if mask .any ():
482
+ return np .where (mask , np .nan , a )
483
+ return a
484
+
485
+
486
+ def _clip_non_logit (a ):
487
+ a = np .array (a , float )
488
+ a [a <= 0.0 ] = 1e-300
489
+ a [a >= 1.0 ] = 1 - 1e-300
490
+ return a
491
+
492
+
493
+ class LogitTransform (Transform ):
494
+ input_dims = 1
495
+ output_dims = 1
496
+ is_separable = True
497
+ has_inverse = True
498
+
499
+ def __init__ (self , nonpos ):
500
+ Transform .__init__ (self )
501
+ if nonpos == 'mask' :
502
+ self ._handle_nonpos = _mask_non_logit
503
+ else :
504
+ self ._handle_nonpos = _clip_non_logit
505
+ self ._nonpos = nonpos
506
+
507
+ def transform_non_affine (self , a ):
508
+ """logit transform (base 10), masked or clipped"""
509
+ a = self ._handle_nonpos (a )
510
+ return np .log10 (1.0 * a / (1.0 - a ))
511
+
512
+ def inverted (self ):
513
+ return LogisticTransform (self ._nonpos )
514
+
515
+
516
+ class LogisticTransform (Transform ):
517
+ input_dims = 1
518
+ output_dims = 1
519
+ is_separable = True
520
+ has_inverse = True
521
+
522
+ def __init__ (self , nonpos = 'mask' ):
523
+ Transform .__init__ (self )
524
+ self ._nonpos = nonpos
525
+
526
+ def transform_non_affine (self , a ):
527
+ """logistic transform (base 10)"""
528
+ return 1.0 / (1 + 10 ** (- a ))
529
+
530
+ def inverted (self ):
531
+ return LogitTransform (self ._nonpos )
532
+
533
+
534
+ class LogitScale (ScaleBase ):
535
+ """
536
+ Logit scale for data between zero and one, both excluded.
537
+
538
+ This scale is similar to a log scale close to zero and to one, and almost
539
+ linear around 0.5. It maps the interval ]0, 1[ onto ]-infty, +infty[.
540
+ """
541
+ name = 'logit'
542
+
543
+ def __init__ (self , axis , nonpos = 'mask' ):
544
+ """
545
+ *nonpos*: ['mask' | 'clip' ]
546
+ values beyond ]0, 1[ can be masked as invalid, or clipped to a number
547
+ very close to 0 or 1
548
+ """
549
+ if nonpos not in ['mask' , 'clip' ]:
550
+ raise ValueError ("nonposx, nonposy kwarg must be 'mask' or 'clip'" )
551
+
552
+ self ._transform = LogitTransform (nonpos )
553
+
554
+ def get_transform (self ):
555
+ """
556
+ Return a :class:`LogitTransform` instance.
557
+ """
558
+ return self ._transform
559
+
560
+ def set_default_locators_and_formatters (self , axis ):
561
+ # ..., 0.01, 0.1, 0.5, 0.9, 0.99, ...
562
+ axis .set_major_locator (LogitLocator ())
563
+ axis .set_major_formatter (LogitFormatter ())
564
+ axis .set_minor_locator (LogitLocator (minor = True ))
565
+ axis .set_minor_formatter (LogitFormatter ())
566
+
567
+ def limit_range_for_scale (self , vmin , vmax , minpos ):
568
+ """
569
+ Limit the domain to values between 0 and 1 (excluded).
570
+ """
571
+ return (vmin <= 0 and minpos or vmin ,
572
+ vmax >= 1 and (1 - minpos ) or vmax )
573
+
574
+
481
575
_scale_mapping = {
482
576
'linear' : LinearScale ,
483
577
'log' : LogScale ,
484
- 'symlog' : SymmetricalLogScale
578
+ 'symlog' : SymmetricalLogScale ,
579
+ 'logit' : LogitScale ,
485
580
}
486
581
487
582
0 commit comments