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

Skip to content

Commit 97cd899

Browse files
committed
Add symlog-normalization to colors.py, with tests.
1 parent bb3ea55 commit 97cd899

File tree

2 files changed

+160
-2
lines changed

2 files changed

+160
-2
lines changed

lib/matplotlib/colors.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ def __call__(self, value, clip=None):
953953
else:
954954
if clip:
955955
mask = ma.getmask(result)
956-
val = ma.array(np.clip(result.filled(vmax), vmin, vmax),
956+
result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
957957
mask=mask)
958958
# in-place equivalent of above can be much faster
959959
resdat = result.data
@@ -999,6 +999,121 @@ def autoscale_None(self, A):
999999
self.vmin = ma.min(A)
10001000
if self.vmax is None:
10011001
self.vmax = ma.max(A)
1002+
1003+
1004+
class SymLogNorm(Normalize):
1005+
"""
1006+
The symmetrical logarithmic scale is logarithmic in both the
1007+
positive and negative directions from the origin.
1008+
1009+
Since the values close to zero tend toward infinity, there is a
1010+
need to have a range around zero that is linear. The parameter
1011+
*linthresh* allows the user to specify the size of this range
1012+
(-*linthresh*, *linthresh*).
1013+
"""
1014+
def __init__(self, linthresh, linscale=1.0,
1015+
vmin=None, vmax=None, clip=False):
1016+
"""
1017+
*linthresh*:
1018+
The range within which the plot is linear (to
1019+
avoid having the plot go to infinity around zero).
1020+
1021+
*linscale*:
1022+
This allows the linear range (-*linthresh* to *linthresh*)
1023+
to be stretched relative to the logarithmic range. Its
1024+
value is the number of decades to use for each half of the
1025+
linear range. For example, when *linscale* == 1.0 (the
1026+
default), the space used for the positive and negative
1027+
halves of the linear range will be equal to one decade in
1028+
the logarithmic range. Defaults to 1.
1029+
"""
1030+
Normalize.__init__(self, vmin, vmax, clip)
1031+
self.linthresh = linthresh
1032+
self._linscale_adj = (linscale / (1.0 - np.e ** -1))
1033+
1034+
def __call__(self, value, clip=None):
1035+
if clip is None:
1036+
clip = self.clip
1037+
1038+
result, is_scalar = self.process_value(value)
1039+
self.autoscale_None(result)
1040+
vmin, vmax = self.vmin, self.vmax
1041+
1042+
if vmin > vmax:
1043+
raise ValueError("minvalue must be less than or equal to maxvalue")
1044+
elif vmin == vmax:
1045+
result.fill(0)
1046+
else:
1047+
if clip:
1048+
mask = ma.getmask(result)
1049+
result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
1050+
mask=mask)
1051+
# in-place equivalent of above can be much faster
1052+
resdat = self._transform(result.data)
1053+
resdat -= self._lower
1054+
resdat /= (self._upper - self._lower)
1055+
1056+
if is_scalar:
1057+
result = result[0]
1058+
return result
1059+
1060+
def _transform(self, a):
1061+
"""
1062+
Inplace transformation.
1063+
"""
1064+
masked = np.abs(a) > self.linthresh
1065+
sign = np.sign(a[masked])
1066+
log = (self._linscale_adj + np.log(np.abs(a[masked]) / self.linthresh))
1067+
log *= sign * self.linthresh
1068+
a[masked] = log
1069+
a[~masked] *= self._linscale_adj
1070+
return a
1071+
1072+
def _inv_transform(self, a):
1073+
"""
1074+
Inverse inplace Transformation.
1075+
"""
1076+
masked = np.abs(a) > (self.linthresh * self._linscale_adj)
1077+
sign = np.sign(a[masked])
1078+
exp = np.exp(sign * a[masked] / self.linthresh - self._linscale_adj)
1079+
exp *= sign * self.linthresh
1080+
a[masked] = exp
1081+
a[~masked] /= self._linscale_adj
1082+
return a
1083+
1084+
def _transform_vmin_vmax(self):
1085+
"""
1086+
Calculates vmin and vmax in the transformed system.
1087+
"""
1088+
vmin, vmax = self.vmin, self.vmax
1089+
arr = np.array([vmax, vmin])
1090+
self._upper, self._lower = self._transform(arr)
1091+
1092+
1093+
def inverse(self, value):
1094+
if not self.scaled():
1095+
raise ValueError("Not invertible until scaled")
1096+
val = ma.asarray(value)
1097+
val = val * (self._upper - self._lower) + self._lower
1098+
return self._inv_transform(val)
1099+
1100+
def autoscale(self, A):
1101+
"""
1102+
Set *vmin*, *vmax* to min, max of *A*.
1103+
"""
1104+
self.vmin = ma.min(A)
1105+
self.vmax = ma.max(A)
1106+
self._transform_vmin_vmax()
1107+
1108+
def autoscale_None(self, A):
1109+
""" autoscale only None-valued vmin or vmax """
1110+
if self.vmin is not None and self.vmax is not None:
1111+
pass
1112+
if self.vmin is None:
1113+
self.vmin = ma.min(A)
1114+
if self.vmax is None:
1115+
self.vmax = ma.max(A)
1116+
self._transform_vmin_vmax()
10021117

10031118

10041119
class BoundaryNorm(Normalize):

lib/matplotlib/tests/test_colors.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from __future__ import print_function
66
import numpy as np
7-
from numpy.testing.utils import assert_array_equal
7+
from numpy.testing.utils import assert_array_equal, assert_array_almost_equal
88
import matplotlib.colors as mcolors
99
import matplotlib.cm as cm
1010

@@ -37,3 +37,46 @@ def test_BoundaryNorm():
3737
bn = mcolors.BoundaryNorm(boundaries, ncolors)
3838
assert_array_equal(bn(vals), expected)
3939

40+
def test_Normalize():
41+
norm = mcolors.Normalize()
42+
vals = np.arange(-10, 10, 1, dtype=np.float)
43+
_inverse_tester(norm, vals)
44+
_scalar_tester(norm, vals)
45+
_mask_tester(norm, vals)
46+
47+
48+
def test_SymLogNorm():
49+
"""
50+
Test SymLogNorm behavior
51+
"""
52+
norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2)
53+
vals = np.array([-30, -1, 2, 6], dtype=np.float)
54+
normed_vals = norm(vals)
55+
expected = [ 0., 0.53980074, 0.826991, 1.02758204]
56+
assert_array_almost_equal(normed_vals, expected)
57+
_inverse_tester(norm, vals)
58+
_scalar_tester(norm, vals)
59+
_mask_tester(norm, vals)
60+
61+
62+
def _inverse_tester(norm_instance, vals):
63+
"""
64+
Checks if the inverse of the given normalization is working.
65+
"""
66+
assert_array_almost_equal(norm_instance.inverse(norm_instance(vals)), vals)
67+
68+
def _scalar_tester(norm_instance, vals):
69+
"""
70+
Checks if scalars and arrays are handled the same way.
71+
Tests only for float.
72+
"""
73+
scalar_result = [norm_instance(float(v)) for v in vals]
74+
assert_array_almost_equal(scalar_result, norm_instance(vals))
75+
76+
def _mask_tester(norm_instance, vals):
77+
"""
78+
Checks mask handling
79+
"""
80+
masked_array = np.ma.array(vals)
81+
masked_array[0] = np.ma.masked
82+
assert_array_equal(masked_array.mask, norm_instance(masked_array).mask)

0 commit comments

Comments
 (0)