6868import base64
6969from collections .abc import Sized
7070import functools
71+ import inspect
7172import io
7273import itertools
7374from numbers import Number
7778
7879import matplotlib as mpl
7980import numpy as np
80- import matplotlib .cbook as cbook
81- from matplotlib import docstring
81+ from matplotlib import cbook , docstring , scale
8282from ._color_data import BASE_COLORS , TABLEAU_COLORS , CSS4_COLORS , XKCD_COLORS
8383
8484
@@ -1203,61 +1203,67 @@ class DivergingNorm(TwoSlopeNorm):
12031203 ...
12041204
12051205
1206+ def _make_norm_from_scale (scale_cls , base_cls = None , * , init = None ):
1207+ if base_cls is None :
1208+ return functools .partial (_make_norm_from_scale , scale_cls , init = init )
1209+
1210+ if init is None :
1211+ def init (vmin = None , vmax = None , clip = False ): pass
1212+ init_signature = inspect .signature (init )
1213+
1214+ class Norm (base_cls ):
1215+
1216+ def __init__ (self , * args , ** kwargs ):
1217+ ba = init_signature .bind (* args , ** kwargs )
1218+ ba .apply_defaults ()
1219+ super ().__init__ (
1220+ ** {k : ba .arguments .pop (k ) for k in ["vmin" , "vmax" , "clip" ]})
1221+ self ._scale = scale_cls (axis = None , ** ba .arguments )
1222+ self ._trf = self ._scale .get_transform ()
1223+ self ._inv_trf = self ._trf .inverted ()
1224+
1225+ def __call__ (self , value , clip = None ):
1226+ value , is_scalar = self .process_value (value )
1227+ self .autoscale_None (value )
1228+ if self .vmin > self .vmax :
1229+ raise ValueError ("vmin must be less or equal to vmax" )
1230+ if self .vmin == self .vmax :
1231+ return np .full_like (value , 0 )
1232+ if clip is None :
1233+ clip = self .clip
1234+ if clip :
1235+ value = np .clip (value , self .vmin , self .vmax )
1236+ t_value = self ._trf .transform (value ).reshape (np .shape (value ))
1237+ t_vmin , t_vmax = self ._trf .transform ([self .vmin , self .vmax ])
1238+ if not np .isfinite ([t_vmin , t_vmax ]).all ():
1239+ raise ValueError ("Invalid vmin or vmax" )
1240+ t_value -= t_vmin
1241+ t_value /= (t_vmax - t_vmin )
1242+ t_value = np .ma .masked_invalid (t_value , copy = False )
1243+ return t_value [0 ] if is_scalar else t_value
1244+
1245+ def inverse (self , value ):
1246+ if not self .scaled ():
1247+ raise ValueError ("Not invertible until scaled" )
1248+ if self .vmin > self .vmax :
1249+ raise ValueError ("vmin must be less or equal to vmax" )
1250+ t_vmin , t_vmax = self ._trf .transform ([self .vmin , self .vmax ])
1251+ if not np .isfinite ([t_vmin , t_vmax ]).all ():
1252+ raise ValueError ("Invalid vmin or vmax" )
1253+ rescaled = value * (t_vmax - t_vmin )
1254+ rescaled += t_vmin
1255+ return self ._inv_trf .transform (rescaled ).reshape (np .shape (value ))
1256+
1257+ Norm .__name__ = base_cls .__name__
1258+ Norm .__qualname__ = base_cls .__qualname__
1259+ Norm .__module__ = base_cls .__module__
1260+ return Norm
1261+
1262+
1263+ @_make_norm_from_scale (functools .partial (scale .LogScale , nonpositive = "mask" ))
12061264class LogNorm (Normalize ):
12071265 """Normalize a given value to the 0-1 range on a log scale."""
12081266
1209- def _check_vmin_vmax (self ):
1210- if self .vmin > self .vmax :
1211- raise ValueError ("minvalue must be less than or equal to maxvalue" )
1212- elif self .vmin <= 0 :
1213- raise ValueError ("minvalue must be positive" )
1214-
1215- def __call__ (self , value , clip = None ):
1216- if clip is None :
1217- clip = self .clip
1218-
1219- result , is_scalar = self .process_value (value )
1220-
1221- result = np .ma .masked_less_equal (result , 0 , copy = False )
1222-
1223- self .autoscale_None (result )
1224- self ._check_vmin_vmax ()
1225- vmin , vmax = self .vmin , self .vmax
1226- if vmin == vmax :
1227- result .fill (0 )
1228- else :
1229- if clip :
1230- mask = np .ma .getmask (result )
1231- result = np .ma .array (np .clip (result .filled (vmax ), vmin , vmax ),
1232- mask = mask )
1233- # in-place equivalent of above can be much faster
1234- resdat = result .data
1235- mask = result .mask
1236- if mask is np .ma .nomask :
1237- mask = (resdat <= 0 )
1238- else :
1239- mask |= resdat <= 0
1240- np .copyto (resdat , 1 , where = mask )
1241- np .log (resdat , resdat )
1242- resdat -= np .log (vmin )
1243- resdat /= (np .log (vmax ) - np .log (vmin ))
1244- result = np .ma .array (resdat , mask = mask , copy = False )
1245- if is_scalar :
1246- result = result [0 ]
1247- return result
1248-
1249- def inverse (self , value ):
1250- if not self .scaled ():
1251- raise ValueError ("Not invertible until scaled" )
1252- self ._check_vmin_vmax ()
1253- vmin , vmax = self .vmin , self .vmax
1254-
1255- if np .iterable (value ):
1256- val = np .ma .asarray (value )
1257- return vmin * np .ma .power ((vmax / vmin ), val )
1258- else :
1259- return vmin * pow ((vmax / vmin ), value )
1260-
12611267 def autoscale (self , A ):
12621268 # docstring inherited.
12631269 super ().autoscale (np .ma .masked_less_equal (A , 0 , copy = False ))
@@ -1267,6 +1273,10 @@ def autoscale_None(self, A):
12671273 super ().autoscale_None (np .ma .masked_less_equal (A , 0 , copy = False ))
12681274
12691275
1276+ @_make_norm_from_scale (
1277+ scale .SymmetricalLogScale ,
1278+ init = lambda linthresh , linscale = 1. , vmin = None , vmax = None , clip = False , * ,
1279+ base = 10 : None )
12701280class SymLogNorm (Normalize ):
12711281 """
12721282 The symmetrical logarithmic scale is logarithmic in both the
@@ -1276,124 +1286,29 @@ class SymLogNorm(Normalize):
12761286 need to have a range around zero that is linear. The parameter
12771287 *linthresh* allows the user to specify the size of this range
12781288 (-*linthresh*, *linthresh*).
1279- """
1280- def __init__ (self , linthresh , linscale = 1.0 , vmin = None , vmax = None ,
1281- clip = False , * , base = None ):
1282- """
1283- Parameters
1284- ----------
1285- linthresh : float
1286- The range within which the plot is linear (to avoid having the plot
1287- go to infinity around zero).
1288-
1289- linscale : float, default: 1
1290- This allows the linear range (-*linthresh* to *linthresh*)
1291- to be stretched relative to the logarithmic range. Its
1292- value is the number of powers of *base* to use for each
1293- half of the linear range.
1294-
1295- For example, when *linscale* == 1.0 (the default) and
1296- ``base=10``, then space used for the positive and negative
1297- halves of the linear range will be equal to a decade in
1298- the logarithmic.
1299-
1300- base : float, default: None
1301- If not given, defaults to ``np.e`` (consistent with prior
1302- behavior) and warns.
1303-
1304- In v3.3 the default value will change to 10 to be consistent with
1305- `.SymLogNorm`.
1306-
1307- To suppress the warning pass *base* as a keyword argument.
13081289
1309- """
1310- Normalize .__init__ (self , vmin , vmax , clip )
1311- if base is None :
1312- self ._base = np .e
1313- cbook .warn_deprecated (
1314- "3.2" , removal = "3.4" , message = "default base will change from "
1315- "np.e to 10 %(removal)s. To suppress this warning specify "
1316- "the base keyword argument." )
1317- else :
1318- self ._base = base
1319- self ._log_base = np .log (self ._base )
1320-
1321- self .linthresh = float (linthresh )
1322- self ._linscale_adj = (linscale / (1.0 - self ._base ** - 1 ))
1323- if vmin is not None and vmax is not None :
1324- self ._transform_vmin_vmax ()
1325-
1326- def __call__ (self , value , clip = None ):
1327- if clip is None :
1328- clip = self .clip
1329-
1330- result , is_scalar = self .process_value (value )
1331- self .autoscale_None (result )
1332- vmin , vmax = self .vmin , self .vmax
1333-
1334- if vmin > vmax :
1335- raise ValueError ("minvalue must be less than or equal to maxvalue" )
1336- elif vmin == vmax :
1337- result .fill (0 )
1338- else :
1339- if clip :
1340- mask = np .ma .getmask (result )
1341- result = np .ma .array (np .clip (result .filled (vmax ), vmin , vmax ),
1342- mask = mask )
1343- # in-place equivalent of above can be much faster
1344- resdat = self ._transform (result .data )
1345- resdat -= self ._lower
1346- resdat /= (self ._upper - self ._lower )
1347-
1348- if is_scalar :
1349- result = result [0 ]
1350- return result
1351-
1352- def _transform (self , a ):
1353- """Inplace transformation."""
1354- with np .errstate (invalid = "ignore" ):
1355- masked = np .abs (a ) > self .linthresh
1356- sign = np .sign (a [masked ])
1357- log = (self ._linscale_adj +
1358- np .log (np .abs (a [masked ]) / self .linthresh ) / self ._log_base )
1359- log *= sign * self .linthresh
1360- a [masked ] = log
1361- a [~ masked ] *= self ._linscale_adj
1362- return a
1363-
1364- def _inv_transform (self , a ):
1365- """Inverse inplace Transformation."""
1366- masked = np .abs (a ) > (self .linthresh * self ._linscale_adj )
1367- sign = np .sign (a [masked ])
1368- exp = np .power (self ._base ,
1369- sign * a [masked ] / self .linthresh - self ._linscale_adj )
1370- exp *= sign * self .linthresh
1371- a [masked ] = exp
1372- a [~ masked ] /= self ._linscale_adj
1373- return a
1374-
1375- def _transform_vmin_vmax (self ):
1376- """Calculate vmin and vmax in the transformed system."""
1377- vmin , vmax = self .vmin , self .vmax
1378- arr = np .array ([vmax , vmin ]).astype (float )
1379- self ._upper , self ._lower = self ._transform (arr )
1380-
1381- def inverse (self , value ):
1382- if not self .scaled ():
1383- raise ValueError ("Not invertible until scaled" )
1384- val = np .ma .asarray (value )
1385- val = val * (self ._upper - self ._lower ) + self ._lower
1386- return self ._inv_transform (val )
1290+ Parameters
1291+ ----------
1292+ linthresh : float
1293+ The range within which the plot is linear (to avoid having the plot
1294+ go to infinity around zero).
1295+ linscale : float, default: 1
1296+ This allows the linear range (-*linthresh* to *linthresh*) to be
1297+ stretched relative to the logarithmic range. Its value is the
1298+ number of decades to use for each half of the linear range. For
1299+ example, when *linscale* == 1.0 (the default), the space used for
1300+ the positive and negative halves of the linear range will be equal
1301+ to one decade in the logarithmic range.
1302+ base : float, default: 10
1303+ """
13871304
1388- def autoscale (self , A ):
1389- # docstring inherited.
1390- super ().autoscale (A )
1391- self ._transform_vmin_vmax ()
1305+ @property
1306+ def linthresh (self ):
1307+ return self ._scale .linthresh
13921308
1393- def autoscale_None (self , A ):
1394- # docstring inherited.
1395- super ().autoscale_None (A )
1396- self ._transform_vmin_vmax ()
1309+ @linthresh .setter
1310+ def linthresh (self , value ):
1311+ self ._scale .linthresh = value
13971312
13981313
13991314class PowerNorm (Normalize ):
0 commit comments