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

Skip to content

Commit 05c1b76

Browse files
phobsonOceanWolf
authored andcommitted
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 b9bc7d1 commit 05c1b76

File tree

2 files changed

+306
-2
lines changed

2 files changed

+306
-2
lines changed

lib/matplotlib/colors.py

Lines changed: 122 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: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import itertools
66
from distutils.version import LooseVersion as V
77

8-
from nose.tools import assert_raises, assert_equal, assert_true
8+
from nose.tools import (assert_raises, assert_equal, assert_true, assert_false
9+
raises)
910

1011
import numpy as np
1112
from numpy.testing.utils import assert_array_equal, assert_array_almost_equal
@@ -163,6 +164,182 @@ def test_Normalize():
163164
_mask_tester(norm, vals)
164165

165166

167+
class _base_NormMixin(object):
168+
def test_call(self):
169+
normed_vals = self.norm(self.vals)
170+
assert_array_almost_equal(normed_vals, self.expected)
171+
172+
def test_inverse(self):
173+
_inverse_tester(self.norm, self.vals)
174+
175+
def test_scalar(self):
176+
_scalar_tester(self.norm, self.vals)
177+
178+
def test_mask(self):
179+
_mask_tester(self.norm, self.vals)
180+
181+
def test_autoscale(self):
182+
norm = self.normclass()
183+
norm.autoscale([10, 20, 30, 40])
184+
assert_equal(norm.vmin, 10.)
185+
assert_equal(norm.vmax, 40.)
186+
187+
def test_autoscale_None_vmin(self):
188+
norm = self.normclass(vmin=0, vmax=None)
189+
norm.autoscale_None([1, 2, 3, 4, 5])
190+
assert_equal(norm.vmin, 0.)
191+
assert_equal(norm.vmax, 5.)
192+
193+
def test_autoscale_None_vmax(self):
194+
norm = self.normclass(vmin=None, vmax=10)
195+
norm.autoscale_None([1, 2, 3, 4, 5])
196+
assert_equal(norm.vmin, 1.)
197+
assert_equal(norm.vmax, 10.)
198+
199+
def test_scale(self):
200+
norm = self.normclass()
201+
assert_false(norm.scaled())
202+
203+
norm([1, 2, 3, 4])
204+
assert_true(norm.scaled())
205+
206+
def test_process_value_scalar(self):
207+
res, is_scalar = mcolors.Normalize.process_value(5)
208+
assert_true(is_scalar)
209+
assert_array_equal(res, np.array([5.]))
210+
211+
def test_process_value_list(self):
212+
res, is_scalar = mcolors.Normalize.process_value([5, 10])
213+
assert_false(is_scalar)
214+
assert_array_equal(res, np.array([5., 10.]))
215+
216+
def test_process_value_tuple(self):
217+
res, is_scalar = mcolors.Normalize.process_value((5, 10))
218+
assert_false(is_scalar)
219+
assert_array_equal(res, np.array([5., 10.]))
220+
221+
def test_process_value_array(self):
222+
res, is_scalar = mcolors.Normalize.process_value(np.array([5, 10]))
223+
assert_false(is_scalar)
224+
assert_array_equal(res, np.array([5., 10.]))
225+
226+
227+
class test_OffsetNorm_Even(_base_NormMixin):
228+
def setup(self):
229+
self.normclass = mcolors.OffsetNorm
230+
self.norm = self.normclass(vmin=-1, vcenter=0, vmax=4)
231+
self.vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
232+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
233+
234+
235+
class test_OffsetNorm_Odd(_base_NormMixin):
236+
def setup(self):
237+
self.normclass = mcolors.OffsetNorm
238+
self.norm = self.normclass(vmin=-2, vcenter=0, vmax=5)
239+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
240+
self.expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
241+
242+
243+
class test_OffsetNorm_AllNegative(_base_NormMixin):
244+
def setup(self):
245+
self.normclass = mcolors.OffsetNorm
246+
self.norm = self.normclass(vmin=-10, vcenter=-8, vmax=-2)
247+
self.vals = np.array([-10., -9., -8., -6., -4., -2.])
248+
self.expected = np.array([0.0, 0.25, 0.5, 0.666667, 0.833333, 1.0])
249+
250+
251+
class test_OffsetNorm_AllPositive(_base_NormMixin):
252+
def setup(self):
253+
self.normclass = mcolors.OffsetNorm
254+
self.norm = self.normclass(vmin=0, vcenter=3, vmax=9)
255+
self.vals = np.array([0., 1.5, 3., 4.5, 6.0, 7.5, 9.])
256+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
257+
258+
259+
class test_OffsetNorm_NoVs(_base_NormMixin):
260+
def setup(self):
261+
self.normclass = mcolors.OffsetNorm
262+
self.norm = self.normclass(vmin=None, vcenter=None, vmax=None)
263+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
264+
self.expected = np.array([0., 0.16666667, 0.33333333,
265+
0.5, 0.66666667, 0.83333333, 1.0])
266+
self.expected_vmin = -2
267+
self.expected_vcenter = 1
268+
self.expected_vmax = 4
269+
270+
def test_vmin(self):
271+
assert_true(self.norm.vmin is None)
272+
self.norm(self.vals)
273+
assert_equal(self.norm.vmin, self.expected_vmin)
274+
275+
def test_vcenter(self):
276+
assert_true(self.norm.vcenter is None)
277+
self.norm(self.vals)
278+
assert_equal(self.norm.vcenter, self.expected_vcenter)
279+
280+
def test_vmax(self):
281+
assert_true(self.norm.vmax is None)
282+
self.norm(self.vals)
283+
assert_equal(self.norm.vmax, self.expected_vmax)
284+
285+
286+
class test_OffsetNorm_VminEqualsVcenter(_base_NormMixin):
287+
def setup(self):
288+
self.normclass = mcolors.OffsetNorm
289+
self.norm = self.normclass(vmin=-2, vcenter=-2, vmax=2)
290+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
291+
self.expected = np.array([0.5, 0.625, 0.75, 0.875, 1.0])
292+
293+
294+
class test_OffsetNorm_VmaxEqualsVcenter(_base_NormMixin):
295+
def setup(self):
296+
self.normclass = mcolors.OffsetNorm
297+
self.norm = self.normclass(vmin=-2, vcenter=2, vmax=2)
298+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
299+
self.expected = np.array([0.0, 0.125, 0.25, 0.375, 0.5])
300+
301+
302+
class test_OffsetNorm_VsAllEqual(_base_NormMixin):
303+
def setup(self):
304+
self.v = 10
305+
self.normclass = mcolors.OffsetNorm
306+
self.norm = self.normclass(vmin=self.v, vcenter=self.v, vmax=self.v)
307+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
308+
self.expected = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
309+
self.expected_inv = self.expected + self.v
310+
311+
def test_inverse(self):
312+
assert_array_almost_equal(
313+
self.norm.inverse(self.norm(self.vals)),
314+
self.expected_inv
315+
)
316+
317+
318+
class test_OffsetNorm_Errors(object):
319+
def setup(self):
320+
self.vals = np.arange(50)
321+
322+
@raises(ValueError)
323+
def test_VminGTVcenter(self):
324+
norm = mcolors.OffsetNorm(vmin=10, vcenter=0, vmax=20)
325+
norm(self.vals)
326+
327+
@raises(ValueError)
328+
def test_VminGTVmax(self):
329+
norm = mcolors.OffsetNorm(vmin=10, vcenter=0, vmax=5)
330+
norm(self.vals)
331+
332+
@raises(ValueError)
333+
def test_VcenterGTVmax(self):
334+
norm = mcolors.OffsetNorm(vmin=10, vcenter=25, vmax=20)
335+
norm(self.vals)
336+
337+
@raises(ValueError)
338+
def test_premature_scaling(self):
339+
norm = mcolors.OffsetNorm()
340+
norm.inverse(np.array([0.1, 0.5, 0.9]))
341+
342+
166343
def test_SymLogNorm():
167344
"""
168345
Test SymLogNorm behavior
@@ -281,7 +458,12 @@ def test_cmap_and_norm_from_levels_and_colors2():
281458
'Wih extend={0!r} and data '
282459
'value={1!r}'.format(extend, d_val))
283460

284-
assert_raises(ValueError, mcolors.from_levels_and_colors, levels, colors)
461+
assert_raises(
462+
ValueError,
463+
mcolors.from_levels_and_colors,
464+
levels,
465+
colors
466+
)
285467

286468

287469
def test_rgb_hsv_round_trip():

0 commit comments

Comments
 (0)