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):
-46 Bytes
Loading

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy