8000 ENH: Add OffsetNorm and tests · matplotlib/matplotlib@7e93605 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7e93605

Browse files
committed
ENH: Add OffsetNorm and tests
Borrows heavily from @Tillsen's solution found on StackOverflow here: http://goo.gl/RPXMYB Used with his permission dicussesd on Github here: https://github.com/matplotlib/matplotlib/pull/3858`
1 parent bf6e86d commit 7e93605

File tree

2 files changed

+307
-3
lines changed

2 files changed

+307
-3
lines changed

lib/matplotlib/colors.py

Lines changed: 1 8000 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def rgb2hex(rgb):
225225
a = '#%02x%02x%02x' % tuple([int(np.round(val * 255)) for val in rgb[:3]])
226226
return a
227227

228+
228229
hexColorPattern = re.compile("\A#[a-fA-F0-9]{6}\Z")
229230

230231

@@ -963,6 +964,127 @@ def scaled(self):
963964
return (self.vmin is not None and self.vmax is not None)
964965

965966

967+
class OffsetNorm(Normalize):
968+
"""
969+
A subclass of matplotlib.colors.Normalize.
970+
971+
Normalizes data into the ``[0.0, 1.0]`` interval.
972+
"""
973+
def __init__(self, vmin=None, vcenter=None, vmax=None, clip=False):
974+
"""Normalize data with an offset midpoint
975+
976+
Useful when mapping data unequally centered around a conceptual
977+
center, e.g., data that range from -2 to 4, with 0 as the midpoint.
978+
979+
Parameters
980+
----------
981+
vmin : optional float
982+
The data value that defines ``0.0`` in the normalized data.
983+
Defaults to the min value of the dataset.
984+
985+
vcenter : optional float
986+
The data value that defines ``0.5`` in the normalized data.
987+
Defaults to halfway between *vmin* and *vmax*.
988+
989+
vmax : option float
990+
The data value that defines ``1.0`` in the normalized data.
991+
Defaults to the the max value of the dataset.
992+
993+
clip : optional bool (default is False)
994+
If *clip* is True, values beyond *vmin* and *vmax* will be set
995+
to ``0.0`` or ``1.0``, respectively. Otherwise, values outside
996+
the ``[0.0, 1.0]`` will be returned.
997+
998+
Examples
999+
--------
1000+
>>> import matplotlib.colors as mcolors
1001+
>>> offset = mcolors.OffsetNorm(vmin=-2., vcenter=0., vmax=4.)
1002+
>>> data = [-2., -1., 0., 1., 2., 3., 4.]
1003+
>>> offset(data)
1004+
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1005+
1006+
"""
1007+
1008+
self.vmin = vmin
1009+
self.vcenter = vcenter
1010+
self.vmax = vmax
1011+
self.clip = clip
1012+
1013+
def __call__(self, value, clip=None):
1014+
if clip is None:
1015+
clip = self.clip
1016+
1017+
result, is_scalar = self.process_value(value)
1018+
1019+
self.autoscale_None(result)
1020+
vmin, vcenter, vmax = self.vmin, self.vcenter, self.vmax
1021+
if vmin == vmax == vcenter:
1022+
result.fill(0)
1023+
elif not vmin <= vcenter <= vmax:
1024+
raise ValueError("minvalue must be less than or equal to "
1025+
"centervalue which must be less than or "
1026+
"equal to maxvalue")
1027+
else:
1028+
vmin = float(vmin)
1029+
vcenter = float(vcenter)
1030+
vmax = float(vmax)
1031+
if clip:
1032+
mask = ma.getmask(result)
1033+
result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
1034+
mask=mask)
1035+
1036+
# ma division is very slow; we can take a shortcut
1037+
resdat = result.data
1038+
1039+
#First scale to -1 to 1 range, than to from 0 to 1.
1040+
resdat -= vcenter
1041+
resdat[resdat > 0] /= abs(vmax - vcenter)
1042+
resdat[resdat < 0] /= abs(vmin - vcenter)
1043+
1044+
resdat /= 2.
1045+
resdat += 0.5
1046+
result = np.ma.array(resdat, mask=result.mask, copy=False)
1047+
1048+
if is_scalar:
1049+
result = result[0]
1050+
1051+
return result
1052+
1053+
def inverse(self, value):
1054+
if not self.scaled():
1055+
raise ValueError("Not invertible until scaled")
1056+
1057+
vmin, vcenter, vmax = self.vmin, self.vcenter, self.vmax
1058+
vmin = float(self.vmin)
1059+
vcenter = float(self.vcenter)
1060+
vmax = float(self.vmax)
1061+
1062+
if cbook.iterable(value):
1063+
val = ma.asarray(value)
1064+
val = 2 * (val - 0.5)
1065+
val[val > 0] *= abs(vmax - vcenter)
1066+
val[val < 0] *= abs(vmin - vcenter)
1067+
val += vcenter
1068+
return val
1069+
else:
1070+
val = 2 * (val - 0.5)
1071+
if val < 0:
1072+
return val * abs(vmin - vcenter) + vcenter
1073+
else:
1074+
return val * abs(vmax - vcenter) + vcenter
1075+
1076+
def autoscale_None(self, A):
1077+
' autoscale only None-valued vmin or vmax'
1078+
if self.vmin is None and np.size(A) > 0:
1079+
self.vmin = ma.min(A)
1080+
1081+
if self.vmax is None and np.size(A) > 0:
1082+
self.vmax = ma.max(A)
1083+
1084+
if self.vcenter is None:
1085+
self.vcenter = (self.vmax + self.vmin) * 0.5
1086+
1087+
9661088
class LogNorm(Normalize):
9671089
"""
9681090
Normalize a given value to the 0-1 range on a log scale

lib/matplotlib/tests/test_colors.py

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from distutils.version import LooseVersion as V
77

88
from nose.tools import assert_raises, assert_equal
9+
import nose.tools as nt
910

1011
import numpy as np
1112
from numpy.testing.utils import assert_array_equal, assert_array_almost_equal
@@ -98,6 +99,182 @@ def test_Normalize():
9899
_mask_tester(norm, vals)
99100

100101

102+
class _base_NormMixin(object):
103+
def test_call(self):
104+
normed_vals = self.norm(self.vals)
105+
assert_array_almost_equal(normed_vals, self.expected)
106+
107+
def test_inverse(self):
108+
_inverse_tester(self.norm, self.vals)
109+
110+
def test_scalar(self):
111+
_scalar_tester(self.norm, self.vals)
112+
113+
def test_mask(self):
114+
_mask_tester(self.norm, self.vals)
115+
116+
def test_autoscale(self):
117+
norm = self.normclass()
118+
norm.autoscale([10, 20, 30, 40])
119+
nt.assert_equal(norm.vmin, 10.)
120+
nt.assert_equal(norm.vmax, 40.)
121+
122+
def test_autoscale_None_vmin(self):
123+
norm = self.normclass(vmin=0, vmax=None)
124+
norm.autoscale_None([1, 2, 3, 4, 5])
125+
nt.assert_equal(norm.vmin, 0.)
126+
nt.assert_equal(norm.vmax, 5.)
127+
128+
def test_autoscale_None_vmax(self):
129+
norm = self.normclass(vmin=None, vmax=10)
130+
norm.autoscale_None([1, 2, 3, 4, 5])
131+
nt.assert_equal(norm.vmin, 1.)
132+
nt.assert_equal(norm.vmax, 10.)
133+
134+
def test_scale(self):
135+
norm = self.normclass()
136+
nt.assert_false(norm.scaled())
137+
138+
norm([1, 2, 3, 4])
139+
nt.assert_true(norm.scaled())
140+
141+
def test_process_value_scalar(self):
142+
res, is_scalar = mcolors.Normalize.process_value(5)
143+
nt.assert_true(is_scalar)
144+
assert_array_equal(res, np.array([5.]))
145+
146+
def test_process_value_list(self):
147+
res, is_scalar = mcolors.Normalize.process_value([5, 10])
148+
nt.assert_false(is_scalar)
149+
assert_array_equal(res, np.array([5., 10.]))
150+
151+
def test_process_value_tuple(self):
152+
res, is_scalar = mcolors.Normalize.process_value((5, 10))
153+
nt.assert_false(is_scalar)
154+
assert_array_equal(res, np.array([5., 10.]))
155+
156+
def test_process_value_array(self):
157+
res, is_scalar = mcolors.Normalize.process_value(np.array([5, 10]))
158+
nt.assert_false(is_scalar)
159+
assert_array_equal(res, np.array([5., 10.]))
160+
161+
162+
class test_OffsetNorm_Even(_base_NormMixin):
163+
def setup(self):
164+
self.normclass = mcolors.OffsetNorm
165+
self.norm = self.normclass(vmin=-1, vcenter=0, vmax=4)
166+
self.vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
167+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
168+
169+
170+
class test_OffsetNorm_Odd(_base_NormMixin):
171+
def setup(self):
172+
self.normclass = mcolors.OffsetNorm
173+
self.norm = self.normclass(vmin=-2, vcenter=0, vmax=5)
174+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
175+
self.expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
176+
177+
178+
class test_OffsetNorm_AllNegative(_base_NormMixin):
179+
def setup(self):
180+
self.normclass = mcolors.OffsetNorm
181+
self.norm = self.normclass(vmin=-10, vcenter=-8, vmax=-2)
182+
self.vals = np.array([-10., -9., -8., -6., -4., -2.])
183+
self.expected = np.array([0.0, 0.25, 0.5, 0.666667, 0.833333, 1.0])
184+
185+
186+
class test_OffsetNorm_AllPositive(_base_NormMixin):
187+
def setup(self):
188+
self.normclass = mcolors.OffsetNorm
189+
self.norm = self.normclass(vmin=0, vcenter=3, vmax=9)
190+
self.vals = np.array([0., 1.5, 3., 4.5, 6.0, 7.5, 9.])
191+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
192+
193+
194+
class test_OffsetNorm_NoVs(_base_NormMixin):
195+
def setup(self):
196+
self.normclass = mcolors.OffsetNorm
197+
self.norm = self.normclass(vmin=None, vcenter=None, vmax=None)
198+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
199+
self.expected = np.array([0., 0.16666667, 0.33333333,
200+
0.5, 0.66666667, 0.83333333, 1.0])
201+
self.expected_vmin = -2
202+
self.expected_vcenter = 1
203+
self.expected_vmax = 4
204+
205+
def test_vmin(self):
206+
nt.assert_true(self.norm.vmin is None)
207+
self.norm(self.vals)
208+
nt.assert_equal(self.norm.vmin, self.expected_vmin)
209+
210+
def test_vcenter(self):
211+
nt.assert_true(self.norm.vcenter is None)
212+
self.norm(self.vals)
213+
nt.assert_equal(self.norm.vcenter, self.expected_vcenter)
214+
215+
def test_vmax(self):
216+
nt.assert_true(self.norm.vmax is None)
217+
self.norm(self.vals)
218+
nt.assert_equal(self.norm.vmax, self.expected_vmax)
219+
220+
221+
class test_OffsetNorm_VminEqualsVcenter(_base_NormMixin):
222+
def setup(self):
223+
self.normclass = mcolors.OffsetNorm
224+
self.norm = self.normclass(vmin=-2, vcenter=-2, vmax=2)
225+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
226+
self.expected = np.array([0.5, 0.625, 0.75, 0.875, 1.0])
227+
228+
229+
class test_OffsetNorm_VmaxEqualsVcenter(_base_NormMixin):
230+
def setup(self):
231+
self.normclass = mcolors.OffsetNorm
232+
self.norm = self.normclass(vmin=-2, vcenter=2, vmax=2)
233+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
234+
self.expected = np.array([0.0, 0.125, 0.25, 0.375, 0.5])
235+
236+
237+
class test_OffsetNorm_VsAllEqual(_base_NormMixin):
238+
def setup(self):
239+
self.v = 10
240+
self.normclass = mcolors.OffsetNorm
241+
self.norm = self.normclass(vmin=self.v, vcenter=self.v, vmax=self.v)
242+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
243+
self.expected = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
244+
self.expected_inv = self.expected + self.v
245+
246+
def test_inverse(self):
247+
assert_array_almost_equal(
248+
self.norm.inverse(self.norm(self.vals)),
249+
self.expected_inv
250+
)
251+
252+
253+
class test_OffsetNorm_Errors(object):
254+
def setup(self):
255+
self.vals = np.arange(50)
256+
257+
@nt.raises(ValueError)
258+
def test_VminGTVcenter(self):
259+
norm = mcolors.OffsetNorm(vmin=10, vcenter=0, vmax=20)
260+
norm(self.vals)
261+
262+
@nt.raises(ValueError)
263+
def test_VminGTVmax(self):
264+
norm = mcolors.OffsetNorm(vmin=10, vcenter=0, vmax=5)
265+
norm(self.vals)
266+
267+
@nt.raises(ValueError)
268+
def test_VcenterGTVmax(self):
269+
norm = mcolors.OffsetNorm(vmin=10, vcenter=25, vmax=20)
270+
norm(self.vals)
271+
272+
@nt.raises(ValueError)
273+
def test_premature_scaling(self):
274+
norm = mcolors.OffsetNorm()
275+
norm.inverse(np.array([0.1, 0.5, 0.9]))
276+
277+
101278
def test_SymLogNorm():
102279
"""
103280
Test SymLogNorm behavior
@@ -216,7 +393,12 @@ def test_cmap_and_norm_from_levels_and_colors2():
216393
'Wih extend={0!r} and data '
217394
'value={1!r}'.format(extend, d_val))
218395

219-
assert_raises(ValueError, mcolors.from_levels_and_colors, levels, colors)
396+
nt.assert_raises(
397+
ValueError,
398+
mcolors.from_levels_and_colors,
399+
levels,
400+
colors
401+
)
220402

221403

222404
def test_rgb_hsv_round_trip():
@@ -246,8 +428,8 @@ def gray_from_float_rgb():
246428
def gray_from_float_rgba():
247429
return mcolors.colorConverter.to_rgba(0.4)
248430

249-
assert_raises(ValueError, gray_from_float_rgb)
250-
assert_raises(ValueError, gray_from_float_rgba)
431+
nt.assert_raises(ValueError, gray_from_float_rgb)
432+
nt.assert_raises(ValueError, gray_from_float_rgba)
251433

252434

253435
@image_comparison(baseline_images=['light_source_shading_topo'],

0 commit comments

Comments
 (0)
0