Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit af8eb61

Browse filesBrowse 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
Copy full SHA for af8eb61

File tree

Expand file treeCollapse file tree

2 files changed

+83
-160
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+83
-160
lines changed

‎lib/matplotlib/colors.py

Copy file name to clipboardExpand all lines: lib/matplotlib/colors.py
+83-160Lines 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):
Loading

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.