Skip to content

Commit 617b35f

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 6f25240 commit 617b35f

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
@@ -68,6 +68,7 @@
6868
import base64
6969
from collections.abc import Sized
7070
import functools
71+
import inspect
7172
import io
7273
import itertools
7374
from numbers import Number
@@ -77,8 +78,7 @@
7778

7879
import matplotlib as mpl
7980
import numpy as np
80-
import matplotlib.cbook as cbook
81-
from matplotlib import docstring
81+
from matplotlib import cbook, docstring, scale
8282
from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
8383

8484

@@ -1203,61 +1203,67 @@ class DivergingNorm(TwoSlopeNorm):
12031203
...
12041204

12051205

1206+
def _make_norm_from_scale(scale_cls, base_cls=None, *, init=None):
1207+
if base_cls is None:
1208+
return functools.partial(_make_norm_from_scale, scale_cls, init=init)
1209+
1210+
if init is None:
1211+
def init(vmin=None, vmax=None, clip=False): pass
1212+
init_signature = inspect.signature(init)
1213+
1214+
class Norm(base_cls):
1215+
1216+
def __init__(self, *args, **kwargs):
1217+
ba = init_signature.bind(*args, **kwargs)
1218+
ba.apply_defaults()
1219+
super().__init__(
1220+
**{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]})
1221+
self._scale = scale_cls(axis=None, **ba.arguments)
1222+
self._trf = self._scale.get_transform()
1223+
self._inv_trf = self._trf.inverted()
1224+
1225+
def __call__(self, value, clip=None):
1226+
value, is_scalar = self.process_value(value)
1227+
self.autoscale_None(value)
1228+
if self.vmin > self.vmax:
1229+
raise ValueError("vmin must be less or equal to vmax")
1230+
if self.vmin == self.vmax:
1231+
return np.full_like(value, 0)
1232+
if clip is None:
1233+
clip = self.clip
1234+
if clip:
1235+
value = np.clip(value, self.vmin, self.vmax)
1236+
t_value = self._trf.transform(value).reshape(np.shape(value))
1237+
t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax])
1238+
if not np.isfinite([t_vmin, t_vmax]).all():
1239+
raise ValueError("Invalid vmin or vmax")
1240+
t_value -= t_vmin
1241+
t_value /= (t_vmax - t_vmin)
1242+
t_value = np.ma.masked_invalid(t_value, copy=False)
1243+
return t_value[0] if is_scalar else t_value
1244+
1245+
def inverse(self, value):
1246+
if not self.scaled():
1247+
raise ValueError("Not invertible until scaled")
1248+
if self.vmin > self.vmax:
1249+
raise ValueError("vmin must be less or equal to vmax")
1250+
t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax])
1251+
if not np.isfinite([t_vmin, t_vmax]).all():
1252+
raise ValueError("Invalid vmin or vmax")
1253+
rescaled = value * (t_vmax - t_vmin)
1254+
rescaled += t_vmin
1255+
return self._inv_trf.transform(rescaled).reshape(np.shape(value))
1256+
1257+
Norm.__name__ = base_cls.__name__
1258+
Norm.__qualname__ = base_cls.__qualname__
1259+
Norm.__module__ = base_cls.__module__
1260+
return Norm
1261+
1262+
1263+
@_make_norm_from_scale(functools.partial(scale.LogScale, nonpositive="mask"))
12061264
class LogNorm(Normalize):
12071265
"""Normalize a given value to the 0-1 range on a log scale."""
12081266

1209-
def _check_vmin_vmax(self):
1210-
if self.vmin > self.vmax:
1211-
raise ValueError("minvalue must be less than or equal to maxvalue")
1212-
elif self.vmin <= 0:
1213-
raise ValueError("minvalue must be positive")
1214-
1215-
def __call__(self, value, clip=None):
1216-
if clip is None:
1217-
clip = self.clip
1218-
1219-
result, is_scalar = self.process_value(value)
1220-
1221-
result = np.ma.masked_less_equal(result, 0, copy=False)
1222-
1223-
self.autoscale_None(result)
1224-
self._check_vmin_vmax()
1225-
vmin, vmax = self.vmin, self.vmax
1226-
if vmin == vmax:
1227-
result.fill(0)
1228-
else:
1229-
if clip:
1230-
mask = np.ma.getmask(result)
1231-
result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
1232-
mask=mask)
1233-
# in-place equivalent of above can be much faster
1234-
resdat = result.data
1235-
mask = result.mask
1236-
if mask is np.ma.nomask:
1237-
mask = (resdat <= 0)
1238-
else:
1239-
mask |= resdat <= 0
1240-
np.copyto(resdat, 1, where=mask)
1241-
np.log(resdat, resdat)
1242-
resdat -= np.log(vmin)
1243-
resdat /= (np.log(vmax) - np.log(vmin))
1244-
result = np.ma.array(resdat, mask=mask, copy=False)
1245-
if is_scalar:
1246-
result = result[0]
1247-
return result
1248-
1249-
def inverse(self, value):
1250-
if not self.scaled():
1251-
raise ValueError("Not invertible until scaled")
1252-
self._check_vmin_vmax()
1253-
vmin, vmax = self.vmin, self.vmax
1254-
1255-
if np.iterable(value):
1256-
val = np.ma.asarray(value)
1257-
return vmin * np.ma.power((vmax / vmin), val)
1258-
else:
1259-
return vmin * pow((vmax / vmin), value)
1260-
12611267
def autoscale(self, A):
12621268
# docstring inherited.
12631269
super().autoscale(np.ma.masked_less_equal(A, 0, copy=False))
@@ -1267,6 +1273,10 @@ def autoscale_None(self, A):
12671273
super().autoscale_None(np.ma.masked_less_equal(A, 0, copy=False))
12681274

12691275

1276+
@_make_norm_from_scale(
1277+
scale.SymmetricalLogScale,
1278+
init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *,
1279+
base=10: None)
12701280
class SymLogNorm(Normalize):
12711281
"""
12721282
The symmetrical logarithmic scale is logarithmic in both the
@@ -1276,124 +1286,29 @@ class SymLogNorm(Normalize):
12761286
need to have a range around zero that is linear. The parameter
12771287
*linthresh* allows the user to specify the size of this range
12781288
(-*linthresh*, *linthresh*).
1279-
"""
1280-
def __init__(self, linthresh, linscale=1.0, vmin=None, vmax=None,
1281-
clip=False, *, base=None):
1282-
"""
1283-
Parameters
1284-
----------
1285-
linthresh : float
1286-
The range within which the plot is linear (to avoid having the plot
1287-
go to infinity around zero).
1288-
1289-
linscale : float, default: 1
1290-
This allows the linear range (-*linthresh* to *linthresh*)
1291-
to be stretched relative to the logarithmic range. Its
1292-
value is the number of powers of *base* to use for each
1293-
half of the linear range.
1294-
1295-
For example, when *linscale* == 1.0 (the default) and
1296-
``base=10``, then space used for the positive and negative
1297-
halves of the linear range will be equal to a decade in
1298-
the logarithmic.
1299-
1300-
base : float, default: None
1301-
If not given, defaults to ``np.e`` (consistent with prior
1302-
behavior) and warns.
1303-
1304-
In v3.3 the default value will change to 10 to be consistent with
1305-
`.SymLogNorm`.
1306-
1307-
To suppress the warning pass *base* as a keyword argument.
13081289
1309-
"""
1310-
Normalize.__init__(self, vmin, vmax, clip)
1311-
if base is None:
1312-
self._base = np.e
1313-
cbook.warn_deprecated(
1314-
"3.2", removal="3.4", message="default base will change from "
1315-
"np.e to 10 %(removal)s. To suppress this warning specify "
1316-
"the base keyword argument.")
1317-
else:
1318-
self._base = base
1319-
self._log_base = np.log(self._base)
1320-
1321-
self.linthresh = float(linthresh)
1322-
self._linscale_adj = (linscale / (1.0 - self._base ** -1))
1323-
if vmin is not None and vmax is not None:
1324-
self._transform_vmin_vmax()
1325-
1326-
def __call__(self, value, clip=None):
1327-
if clip is None:
1328-
clip = self.clip
1329-
1330-
result, is_scalar = self.process_value(value)
1331-
self.autoscale_None(result)
1332-
vmin, vmax = self.vmin, self.vmax
1333-
1334-
if vmin > vmax:
1335-
raise ValueError("minvalue must be less than or equal to maxvalue")
1336-
elif vmin == vmax:
1337-
result.fill(0)
1338-
else:
1339-
if clip:
1340-
mask = np.ma.getmask(result)
1341-
result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
1342-
mask=mask)
1343-
# in-place equivalent of above can be much faster
1344-
resdat = self._transform(result.data)
1345-
resdat -= self._lower
1346-
resdat /= (self._upper - self._lower)
1347-
1348-
if is_scalar:
1349-
result = result[0]
1350-
return result
1351-
1352-
def _transform(self, a):
1353-
"""Inplace transformation."""
1354-
with np.errstate(invalid="ignore"):
1355-
masked = np.abs(a) > self.linthresh
1356-
sign = np.sign(a[masked])
1357-
log = (self._linscale_adj +
1358-
np.log(np.abs(a[masked]) / self.linthresh) / self._log_base)
1359-
log *= sign * self.linthresh
1360-
a[masked] = log
1361-
a[~masked] *= self._linscale_adj
1362-
return a
1363-
1364-
def _inv_transform(self, a):
1365-
"""Inverse inplace Transformation."""
1366-
masked = np.abs(a) > (self.linthresh * self._linscale_adj)
1367-
sign = np.sign(a[masked])
1368-
exp = np.power(self._base,
1369-
sign * a[masked] / self.linthresh - self._linscale_adj)
1370-
exp *= sign * self.linthresh
1371-
a[masked] = exp
1372-
a[~masked] /= self._linscale_adj
1373-
return a
1374-
1375-
def _transform_vmin_vmax(self):
1376-
"""Calculate vmin and vmax in the transformed system."""
1377-
vmin, vmax = self.vmin, self.vmax
1378-
arr = np.array([vmax, vmin]).astype(float)
1379-
self._upper, self._lower = self._transform(arr)
1380-
1381-
def inverse(self, value):
1382-
if not self.scaled():
1383-
raise ValueError("Not invertible until scaled")
1384-
val = np.ma.asarray(value)
1385-
val = val * (self._upper - self._lower) + self._lower
1386-
return self._inv_transform(val)
1290+
Parameters
1291+
----------
1292+
linthresh : float
1293+
The range within which the plot is linear (to avoid having the plot
1294+
go to infinity around zero).
1295+
linscale : float, default: 1
1296+
This allows the linear range (-*linthresh* to *linthresh*) to be
1297+
stretched relative to the logarithmic range. Its value is the
1298+
number of decades to use for each half of the linear range. For
1299+
example, when *linscale* == 1.0 (the default), the space used for
1300+
the positive and negative halves of the linear range will be equal
1301+
to one decade in the logarithmic range.
1302+
base : float, default: 10
1303+
"""
13871304

1388-
def autoscale(self, A):
1389-
# docstring inherited.
1390-
super().autoscale(A)
1391-
self._transform_vmin_vmax()
1305+
@property
1306+
def linthresh(self):
1307+
return self._scale.linthresh
13921308

1393-
def autoscale_None(self, A):
1394-
# docstring inherited.
1395-
super().autoscale_None(A)
1396-
self._transform_vmin_vmax()
1309+
@linthresh.setter
1310+
def linthresh(self, value):
1311+
self._scale.linthresh = value
13971312

13981313

13991314
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