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 ):