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

Skip to content

Commit af8eb61

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 dc1f0d9 commit af8eb61

File tree

2 files changed

+83
-160
lines changed

2 files changed

+83
-160
lines changed

lib/matplotlib/colors.py

Lines changed: 83 additions & 160 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

@@ -1127,61 +1127,67 @@ class DivergingNorm(TwoSlopeNorm):
11271127
...
11281128

11291129

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

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-
11851191
def autoscale(self, A):
11861192
# docstring inherited.
11871193
super().autoscale(np.ma.masked_less_equal(A, 0, copy=False))
@@ -1191,6 +1197,10 @@ def autoscale_None(self, A):
11911197
super().autoscale_None(np.ma.masked_less_equal(A, 0, copy=False))
11921198

11931199

1200+
@_make_norm_from_scale(
1201+
scale.SymmetricalLogScale,
1202+
init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *,
1203+
base=10: None)
11941204
class SymLogNorm(Normalize):
11951205
"""
11961206
The symmetrical logarithmic scale is logarithmic in both the
@@ -1200,115 +1210,28 @@ class SymLogNorm(Normalize):
12001210
need to have a range around zero that is linear. The parameter
12011211
*linthresh* allows the user to specify the size of this range
12021212
(-*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
12481213
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+
"""
13021227

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
13071231

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
13121235

13131236

13141237
class PowerNorm(Normalize):

0 commit comments

Comments
 (0)