Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bb3d7fc

Browse files
committed
Build lognorm/symlognorm from corresponding scales.
This fails test_colors.py::test_SymLogNorm because the old symlog norm and scale were not consistent. test_contour::test_contourf_log_extension looks better with the patch?
1 parent d311b9b commit bb3d7fc

File tree

1 file changed

+82
-143
lines changed

1 file changed

+82
-143
lines changed

lib/matplotlib/colors.py

Lines changed: 82 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,13 @@
6767

6868
from collections.abc import Sized
6969
import functools
70+
import inspect
7071
import itertools
7172
from numbers import Number
7273
import re
7374

7475
import numpy as np
75-
import matplotlib.cbook as cbook
76-
from matplotlib import docstring
76+
from matplotlib import cbook, docstring, scale
7777
from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
7878

7979

@@ -1139,61 +1139,67 @@ class DivergingNorm(TwoSlopeNorm):
11391139
...
11401140

11411141

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"))
11421200
class LogNorm(Normalize):
11431201
"""Normalize a given value to the 0-1 range on a log scale."""
11441202

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-
11971203
def autoscale(self, A):
11981204
# docstring inherited.
11991205
super().autoscale(np.ma.masked_less_equal(A, 0, copy=False))
@@ -1203,6 +1209,9 @@ def autoscale_None(self, A):
12031209
super().autoscale_None(np.ma.masked_less_equal(A, 0, copy=False))
12041210

12051211

1212+
@_make_norm_from_scale(
1213+
scale.SymmetricalLogScale,
1214+
init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False: None)
12061215
class SymLogNorm(Normalize):
12071216
"""
12081217
The symmetrical logarithmic scale is logarithmic in both the
@@ -1212,98 +1221,28 @@ class SymLogNorm(Normalize):
12121221
need to have a range around zero that is linear. The parameter
12131222
*linthresh* allows the user to specify the size of this range
12141223
(-*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
12631224
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+
"""
12971238

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
13021242

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
13071246

13081247

13091248
class PowerNorm(Normalize):

0 commit comments

Comments
 (0)