67
67
68
68
from collections .abc import Sized
69
69
import functools
70
+ import inspect
70
71
import itertools
71
72
from numbers import Number
72
73
import re
73
74
74
75
import numpy as np
75
- import matplotlib .cbook as cbook
76
- from matplotlib import docstring
76
+ from matplotlib import cbook , docstring , scale
77
77
from ._color_data import BASE_COLORS , TABLEAU_COLORS , CSS4_COLORS , XKCD_COLORS
78
78
79
79
@@ -1158,61 +1158,67 @@ class DivergingNorm(TwoSlopeNorm):
1158
1158
...
1159
1159
1160
1160
1161
+ def _make_norm_from_scale (scale_cls , base_cls = None , * , init = None ):
1162
+ if base_cls is None :
1163
+ return functools .partial (_make_norm_from_scale , scale_cls , init = init )
1164
+
1165
+ if init is None :
1166
+ def init (vmin = None , vmax = None , clip = False ): pass
1167
+ init_signature = inspect .signature (init )
1168
+
1169
+ class Norm (base_cls ):
1170
+
1171
+ def __init__ (self , * args , ** kwargs ):
1172
+ ba = init_signature .bind (* args , ** kwargs )
1173
+ ba .apply_defaults ()
1174
+ super ().__init__ (
1175
+ ** {k : ba .arguments .pop (k ) for k in ["vmin" , "vmax" , "clip" ]})
1176
+ self ._scale = scale_cls (axis = None , ** ba .arguments )
1177
+ self ._trf = self ._scale .get_transform ()
1178
+ self ._inv_trf = self ._trf .inverted ()
1179
+
1180
+ def __call__ (self , value , clip = None ):
1181
+ value , is_scalar = self .process_value (value )
1182
+ self .autoscale_None (value )
1183
+ if self .vmin > self .vmax :
1184
+ raise ValueError ("vmin must be less or equal to vmax" )
1185
+ if self .vmin == self .vmax :
1186
+ return np .full_like (value , 0 )
1187
+ if clip is None :
1188
+ clip = self .clip
1189
+ if clip :
1190
+ value = np .clip (value , self .vmin , self .vmax )
1191
+ t_value = self ._trf .transform (value ).reshape (np .shape (value ))
1192
+ t_vmin , t_vmax = self ._trf .transform ([self .vmin , self .vmax ])
1193
+ if not np .isfinite ([t_vmin , t_vmax ]).all ():
1194
+ raise ValueError ("Invalid vmin or vmax" )
1195
+ t_value -= t_vmin
1196
+ t_value /= (t_vmax - t_vmin )
1197
+ t_value = np .ma .masked_invalid (t_value , copy = False )
1198
+ return t_value [0 ] if is_scalar else t_value
1199
+
1200
+ def inverse (self , value ):
1201
+ if not self .scaled ():
1202
+ raise ValueError ("Not invertible until scaled" )
1203
+ if self .vmin > self .vmax :
1204
+ raise ValueError ("vmin must be less or equal to vmax" )
1205
+ t_vmin , t_vmax = self ._trf .transform ([self .vmin , self .vmax ])
1206
+ if not np .isfinite ([t_vmin , t_vmax ]).all ():
1207
+ raise ValueError ("Invalid vmin or vmax" )
1208
+ rescaled = value * (t_vmax - t_vmin )
1209
+ rescaled += t_vmin
1210
+ return self ._inv_trf .transform (rescaled ).reshape (np .shape (value ))
1211
+
1212
+ Norm .__name__ = base_cls .__name__
1213
+ Norm .__qualname__ = base_cls .__qualname__
1214
+ Norm .__module__ = base_cls .__module__
1215
+ return Norm
1216
+
1217
+
1218
+ @_make_norm_from_scale (functools .partial (scale .LogScale , nonpositive = "mask" ))
1161
1219
class LogNorm (Normalize ):
1162
1220
"""Normalize a given value to the 0-1 range on a log scale."""
1163
1221
1164
- def _check_vmin_vmax (self ):
1165
- if self .vmin > self .vmax :
1166
- raise ValueError ("minvalue must be less than or equal to maxvalue" )
1167
- elif self .vmin <= 0 :
1168
- raise ValueError ("minvalue must be positive" )
1169
-
1170
- def __call__ (self , value , clip = None ):
1171
- if clip is None :
1172
- clip = self .clip
1173
-
1174
- result , is_scalar = self .process_value (value )
1175
-
1176
- result = np .ma .masked_less_equal (result , 0 , copy = False )
1177
-
1178
- self .autoscale_None (result )
1179
- self ._check_vmin_vmax ()
1180
- vmin , vmax = self .vmin , self .vmax
1181
- if vmin == vmax :
1182
- result .fill (0 )
1183
- else :
1184
- if clip :
1185
- mask = np .ma .getmask (result )
1186
- result = np .ma .array (np .clip (result .filled (vmax ), vmin , vmax ),
1187
- mask = mask )
1188
- # in-place equivalent of above can be much faster
1189
- resdat = result .data
1190
- mask = result .mask
1191
- if mask is np .ma .nomask :
1192
- mask = (resdat <= 0 )
1193
- else :
1194
- mask |= resdat <= 0
1195
- np .copyto (resdat , 1 , where = mask )
1196
- np .log (resdat , resdat )
1197
- resdat -= np .log (vmin )
1198
- resdat /= (np .log (vmax ) - np .log (vmin ))
1199
- result = np .ma .array (resdat , mask = mask , copy = False )
1200
- if is_scalar :
1201
- result = result [0 ]
1202
- return result
1203
-
1204
- def inverse (self , value ):
1205
- if not self .scaled ():
1206
- raise ValueError ("Not invertible until scaled" )
1207
- self ._check_vmin_vmax ()
1208
- vmin , vmax = self .vmin , self .vmax
1209
-
1210
- if np .iterable (value ):
1211
- val = np .ma .asarray (value )
1212
- return vmin * np .ma .power ((vmax / vmin ), val )
1213
- else :
1214
- return vmin * pow ((vmax / vmin ), value )
1215
-
1216
1222
def autoscale (self , A ):
1217
1223
# docstring inherited.
1218
1224
super ().autoscale (np .ma .masked_less_equal (A , 0 , copy = False ))
@@ -1222,6 +1228,10 @@ def autoscale_None(self, A):
1222
1228
super ().autoscale_None (np .ma .masked_less_equal (A , 0 , copy = False ))
1223
1229
1224
1230
1231
+ @_make_norm_from_scale (
1232
+ scale .SymmetricalLogScale ,
1233
+ init = lambda linthresh , linscale = 1. , vmin = None , vmax = None , clip = False , * ,
1234
+ base = 10 : None )
1225
1235
class SymLogNorm (Normalize ):
1226
1236
"""
1227
1237
The symmetrical logarithmic scale is logarithmic in both the
@@ -1231,124 +1241,29 @@ class SymLogNorm(Normalize):
1231
1241
need to have a range around zero that is linear. The parameter
1232
1242
*linthresh* allows the user to specify the size of this range
1233
1243
(-*linthresh*, *linthresh*).
1234
- """
1235
- def __init__ (self , linthresh , linscale = 1.0 , vmin = None , vmax = None ,
1236
- clip = False , * , base = None ):
1237
- """
1238
- Parameters
1239
- ----------
1240
- linthresh : float
1241
- The range within which the plot is linear (to avoid having the plot
1242
- go to infinity around zero).
1243
-
1244
- linscale : float, default: 1
1245
- This allows the linear range (-*linthresh* to *linthresh*)
1246
- to be stretched relative to the logarithmic range. Its
1247
- value is the number of powers of *base* to use for each
1248
- half of the linear range.
1249
-
1250
- For example, when *linscale* == 1.0 (the default) and
1251
- ``base=10``, then space used for the positive and negative
1252
- halves of the linear range will be equal to a decade in
1253
- the logarithmic.
1254
-
1255
- base : float, default: None
1256
- If not given, defaults to ``np.e`` (consistent with prior
1257
- behavior) and warns.
1258
-
1259
- In v3.3 the default value will change to 10 to be consistent with
1260
- `.SymLogNorm`.
1261
-
1262
- To suppress the warning pass *base* as a keyword argument.
1263
1244
1264
- """
1265
- Normalize .__init__ (self , vmin , vmax , clip )
1266
- if base is None :
1267
- self ._base = np .e
1268
- cbook .warn_deprecated (
1269
- "3.2" , removal = "3.4" , message = "default base will change from "
1270
- "np.e to 10 %(removal)s. To suppress this warning specify "
1271
- "the base keyword argument." )
1272
- else :
1273
- self ._base = base
1274
- self ._log_base = np .log (self ._base )
1275
-
1276
- self .linthresh = float (linthresh )
1277
- self ._linscale_adj = (linscale / (1.0 - self ._base ** - 1 ))
1278
- if vmin is not None and vmax is not None :
1279
- self ._transform_vmin_vmax ()
1280
-
1281
- def __call__ (self , value , clip = None ):
1282
- if clip is None :
1283
- clip = self .clip
1284
-
1285
- result , is_scalar = self .process_value (value )
1286
- self .autoscale_None (result )
1287
- vmin , vmax = self .vmin , self .vmax
1288
-
1289
- if vmin > vmax :
1290
- raise ValueError ("minvalue must be less than or equal to maxvalue" )
1291
- elif vmin == vmax :
1292
- result .fill (0 )
1293
- else :
1294
- if clip :
1295
- mask = np .ma .getmask (result )
1296
- result = np .ma .array (np .clip (result .filled (vmax ), vmin , vmax ),
1297
- mask = mask )
1298
- # in-place equivalent of above can be much faster
1299
- resdat = self ._transform (result .data )
1300
- resdat -= self ._lower
1301
- resdat /= (self ._upper - self ._lower )
1302
-
1303
- if is_scalar :
1304
- result = result [0 ]
1305
- return result
1306
-
1307
- def _transform (self , a ):
1308
- """Inplace transformation."""
1309
- with np .errstate (invalid = "ignore" ):
1310
- masked = np .abs (a ) > self .linthresh
1311
- sign = np .sign (a [masked ])
1312
- log = (self ._linscale_adj +
1313
- np .log (np .abs (a [masked ]) / self .linthresh ) / self ._log_base )
1314
- log *= sign * self .linthresh
1315
- a [masked ] = log
1316
- a [~ masked ] *= self ._linscale_adj
1317
- return a
1318
-
1319
- def _inv_transform (self , a ):
1320
- """Inverse inplace Transformation."""
1321
- masked = np .abs (a ) > (self .linthresh * self ._linscale_adj )
1322
- sign = np .sign (a [masked ])
1323
- exp = np .power (self ._base ,
1324
- sign * a [masked ] / self .linthresh - self ._linscale_adj )
1325
- exp *= sign * self .linthresh
1326
- a [masked ] = exp
1327
- a [~ masked ] /= self ._linscale_adj
1328
- return a
1329
-
1330
- def _transform_vmin_vmax (self ):
1331
- """Calculate vmin and vmax in the transformed system."""
1332
- vmin , vmax = self .vmin , self .vmax
1333
- arr = np .array ([vmax , vmin ]).astype (float )
1334
- self ._upper , self ._lower = self ._transform (arr )
1335
-
1336
- def inverse (self , value ):
1337
- if not self .scaled ():
1338
- raise ValueError ("Not invertible until scaled" )
1339
- val = np .ma .asarray (value )
1340
- val = val * (self ._upper - self ._lower ) + self ._lower
1341
- return self ._inv_transform (val )
1245
+ Parameters
1246
+ ----------
1247
+ linthresh : float
1248
+ The range within which the plot is linear (to avoid having the plot
1249
+ go to infinity around zero).
1250
+ linscale : float, default: 1
1251
+ This allows the linear range (-*linthresh* to *linthresh*) to be
1252
+ stretched relative to the logarithmic range. Its value is the
1253
+ number of decades to use for each half of the linear range. For
1254
+ example, when *linscale* == 1.0 (the default), the space used for
1255
+ the positive and negative halves of the linear range will be equal
1256
+ to one decade in the logarithmic range.
1257
+ base : float, default: 10
1258
+ """
1342
1259
1343
- def autoscale (self , A ):
1344
- # docstring inherited.
1345
- super ().autoscale (A )
1346
- self ._transform_vmin_vmax ()
1260
+ @property
1261
+ def linthresh (self ):
1262
+ return self ._scale .linthresh
1347
1263
1348
- def autoscale_None (self , A ):
1349
- # docstring inherited.
1350
- super ().autoscale_None (A )
1351
- self ._transform_vmin_vmax ()
1264
+ @linthresh .setter
1265
+ def linthresh (self , value ):
1266
+ self ._scale .linthresh = value
1352
1267
1353
1268
1354
1269
class PowerNorm (Normalize ):
0 commit comments