8000 Add symlog-normalization to colors.py, with tests. · matplotlib/matplotlib@97cd899 · GitHub
[go: up one dir, main page]

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 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)
0