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 c62a6cc

Browse filesBrowse files
committed
Merge pull request #1355 from Tillsten/symlognorm
Add sym-log normalization.
2 parents 4f902fa + f824c05 commit c62a6cc
Copy full SHA for c62a6cc

File tree

Expand file treeCollapse file tree

3 files changed

+163
-1
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+163
-1
lines changed

‎CHANGELOG

Copy file name to clipboardExpand all lines: CHANGELOG
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
2012-11-13 Add a symmetric log normalization class to colors.py.
2+
Also added some tests for the normalization class.
3+
Till Stensitzki
4+
15
2012-10-05 Add support for saving animations as animated GIFs. - JVDP
26

37
2012-08-11 Fix path-closing bug in patches.Polygon, so that regardless

‎lib/matplotlib/colors.py

Copy file name to clipboardExpand all lines: lib/matplotlib/colors.py
+115Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_colors.py
+44-1Lines 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

@@ -46,3 +46,46 @@ def test_LogNorm():
4646
ln = mcolors.LogNorm(clip=True, vmax=5)
4747
assert_array_equal(ln([1, 6]), [0, 1.0])
4848

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

0 commit comments

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