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

Skip to content

Commit 989b84f

Browse files
committed
Build lognorm/symlognorm from corresponding scales.
test_contour::test_contourf_log_extension has a tick move by one pixel, but actually looks better with the patch?
1 parent 100c7cf commit 989b84f

File tree

2 files changed

+84
-169
lines changed

2 files changed

+84
-169
lines changed

lib/matplotlib/colors.py

Lines changed: 84 additions & 169 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

@@ -1158,61 +1158,67 @@ class DivergingNorm(TwoSlopeNorm):
11581158
...
11591159

11601160

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

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-
12161222
def autoscale(self, A):
12171223
# docstring inherited.
12181224
super().autoscale(np.ma.masked_less_equal(A, 0, copy=False))
@@ -1222,6 +1228,10 @@ def autoscale_None(self, A):
12221228
super().autoscale_None(np.ma.masked_less_equal(A, 0, copy=False))
12231229

12241230

1231+
@_make_norm_from_scale(
1232+
scale.SymmetricalLogScale,
1233+
init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *,
1234+
base=10: None)
12251235
class SymLogNorm(Normalize):
12261236
"""
12271237
The symmetrical logarithmic scale is logarithmic in both the
@@ -1231,124 +1241,29 @@ class SymLogNorm(Normalize):
12311241
need to have a range around zero that is linear. The parameter
12321242
*linthresh* allows the user to specify the size of this range
12331243
(-*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.
12631244
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+
"""
13421259

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
13471263

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
13521267

13531268

13541269
class PowerNorm(Normalize):

0 commit comments

Comments
 (0)