88
99from matplotlib .cbook import dedent
1010from matplotlib .ticker import (NullFormatter , ScalarFormatter ,
11- LogFormatterMathtext )
11+ LogFormatterMathtext , LogitFormatter )
1212from matplotlib .ticker import (NullLocator , LogLocator , AutoLocator ,
13- SymmetricalLogLocator )
13+ SymmetricalLogLocator , LogitLocator )
1414from matplotlib .transforms import Transform , IdentityTransform
1515from matplotlib import docstring
1616
@@ -86,8 +86,8 @@ def get_transform(self):
8686
8787def _mask_non_positives (a ):
8888 """
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
9191 original array is returned.
9292 """
9393 mask = a <= 0.0
@@ -97,6 +97,7 @@ def _mask_non_positives(a):
9797
9898
9999def _clip_non_positives (a ):
100+ a = np .array (a , float )
100101 a [a <= 0.0 ] = 1e-300
101102 return a
102103
@@ -120,8 +121,6 @@ class Log10Transform(LogTransformBase):
120121
121122 def transform_non_affine (self , a ):
122123 a = self ._handle_nonpos (a * 10.0 )
123- if isinstance (a , ma .MaskedArray ):
124- return ma .log10 (a )
125124 return np .log10 (a )
126125
127126 def inverted (self ):
@@ -147,8 +146,6 @@ class Log2Transform(LogTransformBase):
147146
148147 def transform_non_affine (self , a ):
149148 a = self ._handle_nonpos (a * 2.0 )
150- if isinstance (a , ma .MaskedArray ):
151- return ma .log (a ) / np .log (2 )
152149 return np .log2 (a )
153150
154151 def inverted (self ):
@@ -174,8 +171,6 @@ class NaturalLogTransform(LogTransformBase):
174171
175172 def transform_non_affine (self , a ):
176173 a = self ._handle_nonpos (a * np .e )
177- if isinstance (a , ma .MaskedArray ):
178- return ma .log (a )
179174 return np .log (a )
180175
181176 def inverted (self ):
@@ -212,8 +207,6 @@ def __init__(self, base, nonpos):
212207
213208 def transform_non_affine (self , a ):
214209 a = self ._handle_nonpos (a * self .base )
215- if isinstance (a , ma .MaskedArray ):
216- return ma .log (a ) / np .log (self .base )
217210 return np .log (a ) / np .log (self .base )
218211
219212 def inverted (self ):
@@ -478,10 +471,112 @@ def get_transform(self):
478471 return self ._transform
479472
480473
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+
481575_scale_mapping = {
482576 'linear' : LinearScale ,
483577 'log' : LogScale ,
484- 'symlog' : SymmetricalLogScale
578+ 'symlog' : SymmetricalLogScale ,
579+ 'logit' : LogitScale ,
485580 }
486581
487582
0 commit comments