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
@@ -1127,61 +1127,67 @@ class DivergingNorm(TwoSlopeNorm):
1127
1127
...
1128
1128
1129
1129
1130
+ def _make_norm_from_scale (scale_cls , base_cls = None , * , init = None ):
1131
+ if base_cls is None :
1132
+ return functools .partial (_make_norm_from_scale , scale_cls , init = init )
1133
+
1134
+ if init is None :
1135
+ def init (vmin = None , vmax = None , clip = False ): pass
1136
+ init_signature = inspect .signature (init )
1137
+
1138
+ class Norm (base_cls ):
1139
+
1140
+ def __init__ (self , * args , ** kwargs ):
1141
+ ba = init_signature .bind (* args , ** kwargs )
1142
+ ba .apply_defaults ()
1143
+ super ().__init__ (
1144
+ ** {k : ba .arguments .pop (k ) for k in ["vmin" , "vmax" , "clip" ]})
1145
+ self ._scale = scale_cls (axis = None , ** ba .arguments )
1146
+ self ._trf = self ._scale .get_transform ()
1147
+ self ._inv_trf = self ._trf .inverted ()
1148
+
1149
+ def __call__ (self , value , clip = None ):
1150
+ value , is_scalar = self .process_value (value )
1151
+ self .autoscale_None (value )
1152
+ if self .vmin > self .vmax :
1153
+ raise ValueError ("vmin must be less or equal to vmax" )
1154
+ if self .vmin == self .vmax :
1155
+ return np .full_like (value , 0 )
1156
+ if clip is None :
1157
+ clip = self .clip
1158
+ if clip :
1159
+ value = np .clip (value , self .vmin , self .vmax )
1160
+ t_value = self ._trf .transform (value ).reshape (np .shape (value ))
1161
+ t_vmin , t_vmax = self ._trf .transform ([self .vmin , self .vmax ])
1162
+ if not np .isfinite ([t_vmin , t_vmax ]).all ():
1163
+ raise ValueError ("Invalid vmin or vmax" )
1164
+ t_value -= t_vmin
1165
+ t_value /= (t_vmax - t_vmin )
1166
+ t_value = np .ma .masked_invalid (t_value , copy = False )
1167
+ return t_value [0 ] if is_scalar else t_value
1168
+
1169
+ def inverse (self , value ):
1170
+ if not self .scaled ():
1171
+ raise ValueError ("Not invertible until scaled" )
1172
+ if self .vmin > self .vmax :
1173
+ raise ValueError ("vmin must be less or equal to vmax" )
1174
+ t_vmin , t_vmax = self ._trf .transform ([self .vmin , self .vmax ])
1175
+ if not np .isfinite ([t_vmin , t_vmax ]).all ():
1176
+ raise ValueError ("Invalid vmin or vmax" )
1177
+ rescaled = value * (t_vmax - t_vmin )
1178
+ rescaled += t_vmin
1179
+ return self ._inv_trf .transform (rescaled ).reshape (np .shape (value ))
1180
+
1181
+ Norm .__name__ = base_cls .__name__
1182
+ Norm .__qualname__ = base_cls .__qualname__
1183
+ Norm .__module__ = base_cls .__module__
1184
+ return Norm
1185
+
1186
+
1187
+ @_make_norm_from_scale (functools .partial (scale .LogScale , nonpositive = "mask" ))
1130
1188
class LogNorm (Normalize ):
1131
1189
"""Normalize a given value to the 0-1 range on a log scale."""
1132
1190
1133
- def _check_vmin_vmax (self ):
1134
- if self .vmin > self .vmax :
1135
- raise ValueError ("minvalue must be less than or equal to maxvalue" )
1136
- elif self .vmin <= 0 :
1137
- raise ValueError ("minvalue must be positive" )
1138
-
1139
- def __call__ (self , value , clip = None ):
1140
- if clip is None :
1141
- clip = self .clip
1142
-
1143
- result , is_scalar = self .process_value (value )
1144
-
1145
- result = np .ma .masked_less_equal (result , 0 , copy = False )
1146
-
1147
- self .autoscale_None (result )
1148
- self ._check_vmin_vmax ()
1149
- vmin , vmax = self .vmin , self .vmax
1150
- if vmin == vmax :
1151
- result .fill (0 )
1152
- else :
1153
- if clip :
1154
- mask = np .ma .getmask (result )
1155
- result = np .ma .array (np .clip (result .filled (vmax ), vmin , vmax ),
1156
- mask = mask )
1157
- # in-place equivalent of above can be much faster
1158
- resdat = result .data
1159
- mask = result .mask
1160
- if mask is np .ma .nomask :
1161
- mask = (resdat <= 0 )
1162
- else :
1163
- mask |= resdat <= 0
1164
- np .copyto (resdat , 1 , where = mask )
1165
- np .log (resdat , resdat )
1166
- resdat -= np .log (vmin )
1167
- resdat /= (np .log (vmax ) - np .log (vmin ))
1168
- result = np .ma .array (resdat , mask = mask , copy = False )
1169
- if is_scalar :
1170
- result = result [0 ]
1171
- return result
1172
-
1173
- def inverse (self , value ):
1174
- if not self .scaled ():
1175
- raise ValueError ("Not invertible until scaled" )
1176
- self ._check_vmin_vmax ()
1177
- vmin , vmax = self .vmin , self .vmax
1178
-
1179
- if np .iterable (value ):
1180
- val = np .ma .asarray (value )
1181
- return vmin * np .ma .power ((vmax / vmin ), val )
1182
- else :
1183
- return vmin * pow ((vmax / vmin ), value )
1184
-
1185
1191
def autoscale (self , A ):
1186
1192
# docstring inherited.
1187
1193
super ().autoscale (np .ma .masked_less_equal (A , 0 , copy = False ))
@@ -1191,6 +1197,10 @@ def autoscale_None(self, A):
1191
1197
super ().autoscale_None (np .ma .masked_less_equal (A , 0 , copy = False ))
1192
1198
1193
1199
1200
+ @_make_norm_from_scale (
1201
+ scale .SymmetricalLogScale ,
1202
+ init = lambda linthresh , linscale = 1. , vmin = None , vmax = None , clip = False , * ,
1203
+ base = 10 : None )
1194
1204
class SymLogNorm (Normalize ):
1195
1205
"""
1196
1206
The symmetrical logarithmic scale is logarithmic in both the
@@ -1200,115 +1210,28 @@ class SymLogNorm(Normalize):
1200
1210
need to have a range around zero that is linear. The parameter
1201
1211
*linthresh* allows the user to specify the size of this range
1202
1212
(-*linthresh*, *linthresh*).
1203
- """
1204
- def __init__ (self , linthresh , linscale = 1.0 , vmin = None , vmax = None ,
1205
- clip = False , * , base = None ):
1206
- """
1207
- Parameters
1208
- ----------
1209
- linthresh : float
1210
- The range within which the plot is linear (to avoid having the plot
1211
- go to infinity around zero).
1212
- linscale : float, default: 1
1213
- This allows the linear range (-*linthresh* to *linthresh*) to be
1214
- stretched relative to the logarithmic range. Its value is the
1215
- number of powers of *base* (decades for base 10) to use for each
1216
- half of the linear range. For example, when *linscale* == 1.0
1217
- (the default), the space used for the positive and negative halves
1218
- of the linear range will be equal to a decade in the logarithmic
1219
- range if ``base=10``.
1220
- base : float, default: None
1221
- For v3.2 the default is the old value of ``np.e``, but that is
1222
- deprecated for v3.3 when base will default to 10. During the
1223
- transition, specify the *base* kwarg to avoid a deprecation
1224
- warning.
1225
- """
1226
- Normalize .__init__ (self , vmin , vmax , clip )
1227
- if base is None :
1228
- self ._base = np .e
1229
- cbook .warn_deprecated (
1230
- "3.3" , message = "default base will change from np.e to 10. To "
1231
- "suppress this warning specify the base kwarg." )
1232
- else :
1233
- self ._base = base
1234
- self ._log_base = np .log (self ._base )
1235
-
1236
- self .linthresh = float (linthresh )
1237
- self ._linscale_adj = (linscale / (1.0 - self ._base ** - 1 ))
1238
- if vmin is not None and vmax is not None :
1239
- self ._transform_vmin_vmax ()
1240
-
1241
- def __call__ (self , value , clip = None ):
1242
- if clip is None :
1243
- clip = self .clip
1244
-
1245
- result , is_scalar = self .process_value (value )
1246
- self .autoscale_None (result )
1247
- vmin , vmax = self .vmin , self .vmax
1248
1213
1249
- if vmin > vmax :
1250
- raise ValueError ("minvalue must be less than or equal to maxvalue" )
1251
- elif vmin == vmax :
1252
- result .fill (0 )
1253
- else :
1254
- if clip :
1255
- mask = np .ma .getmask (result )
1256
- result = np .ma .array (np .clip (result .filled (vmax ), vmin , vmax ),
1257
- mask = mask )
1258
- # in-place equivalent of above can be much faster
1259
- resdat = self ._transform (result .data )
1260
- resdat -= self ._lower
1261
- resdat /= (self ._upper - self ._lower )
1262
-
1263
- if is_scalar :
1264
- result = result [0 ]
1265
- return result
1266
-
1267
- def _transform (self , a ):
1268
- """Inplace transformation."""
1269
- with np .errstate (invalid = "ignore" ):
1270
- masked = np .abs (a ) > self .linthresh
1271
- sign = np .sign (a [masked ])
1272
- log = (self ._linscale_adj +
1273
- np .log (np .abs (a [masked ]) / self .linthresh ) / self ._log_base )
1274
- log *= sign * self .linthresh
1275
- a [masked ] = log
1276
- a [~ masked ] *= self ._linscale_adj
1277
- return a
1278
-
1279
- def _inv_transform (self , a ):
1280
- """Inverse inplace Transformation."""
1281
- masked = np .abs (a ) > (self .linthresh * self ._linscale_adj )
1282
- sign = np .sign (a [masked ])
1283
- exp = np .power (self ._base ,
1284
- sign * a [masked ] / self .linthresh - self ._linscale_adj )
1285
- exp *= sign * self .linthresh
1286
- a [masked ] = exp
1287
- a [~ masked ] /= self ._linscale_adj
1288
- return a
1289
-
1290
- def _transform_vmin_vmax (self ):
1291
- """Calculates vmin and vmax in the transformed system."""
1292
- vmin , vmax = self .vmin , self .vmax
1293
- arr = np .array ([vmax , vmin ]).astype (float )
1294
- self ._upper , self ._lower = self ._transform (arr )
1295
-
1296
- def inverse (self , value ):
1297
- if not self .scaled ():
1298
- raise ValueError ("Not invertible until scaled" )
1299
- val = np .ma .asarray (value )
1300
- val = val * (self ._upper - self ._lower ) + self ._lower
1301
- return self ._inv_transform (val )
1214
+ Parameters
1215
+ ----------
1216
+ linthresh : float
1217
+ The range within which the plot is linear (to avoid having the plot
1218
+ go to infinity around zero).
1219
+ linscale : float, default: 1
1220
+ This allows the linear range (-*linthresh* to *linthresh*) to be
1221
+ stretched relative to the logarithmic range. Its value is the
1222
+ number of decades to use for each half of the linear range. For
1223
+ example, when *linscale* == 1.0 (the default), the space used for
1224
+ the positive and negative halves of the linear range will be equal
1225
+ to one decade in the logarithmic range.
1226
+ """
1302
1227
1303
- def autoscale (self , A ):
1304
- # docstring inherited.
1305
- super ().autoscale (A )
1306
- self ._transform_vmin_vmax ()
1228
+ @property
1229
+ def linthresh (self ):
1230
+ return self ._scale .linthresh
1307
1231
1308
- def autoscale_None (self , A ):
1309
- # docstring inherited.
1310
- super ().autoscale_None (A )
1311
- self ._transform_vmin_vmax ()
1232
+ @linthresh .setter
1233
+ def linthresh (self , value ):
1234
+ self ._scale .linthresh = value
1312
1235
1313
1236
1314
1237
class PowerNorm (Normalize ):
0 commit comments