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