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

Skip to content

Commit 57ca4fa

Browse files
committed
Merge branch 'add-asym-norm'
2 parents 48bd557 + 1d8eb25 commit 57ca4fa

File tree

4 files changed

+303
-3
lines changed

4 files changed

+303
-3
lines changed

doc/users/whats_new.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ Added a :code:`pivot` kwarg to :func:`~mpl_toolkits.mplot3d.Axes3D.quiver`
6060
that controls the pivot point around which the quiver line rotates. This also
6161
determines the placement of the arrow head along the quiver line.
6262

63+
Offset Normalizers for Colormaps
64+
````````````````````````````````
65+
Paul Hobson/Geosyntec Consultants added a new :class:`matplotlib.colors.PiecewiseLinearNorm`
66+
class with the help of Till Stensitzki. This is particularly useful when using a
67+
diverging colormap on data that are asymetrically centered around a logical value
68+
(e.g., 0 when data range from -2 to 4).
69+
6370
New backend selection
6471
---------------------
6572

lib/matplotlib/colors.py

Lines changed: 88 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,93 @@ def scaled(self):
963964
return (self.vmin is not None and self.vmax is not None)
964965

965966

967+
class PiecewiseLinearNorm(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 : float, optional
982+
The data value that defines ``0.0`` in the normalized data.
983+
Defaults to the min value of the dataset.
984+
985+
vcenter : float, optional
986+
The data value that defines ``0.5`` in the normalized data.
987+
Defaults to halfway between *vmin* and *vmax*.
988+
989+
vmax : float, optional
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 : bool, optional (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.PiecewiseLinearNorm(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+
x, y = [vmin, vcenter, vmax], [0, 0.5, 1]
1037+
# returns a scalar if shape == (1,)
1038+
result = np.ma.masked_array(np.interp(value, x, y))
1039+
1040+
return result
1041+
1042+
def autoscale_None(self, A):
1043+
' autoscale only None-valued vmin or vmax'
1044+
if self.vmin is None and np.size(A) > 0:
1045+
self.vmin = ma.min(A)
1046+
1047+
if self.vmax is None and np.size(A) > 0:
1048+
self.vmax = ma.max(A)
1049+
1050+
if self.vcenter is None:
1051+
self.vcenter = (self.vmax + self.vmin) * 0.5
1052+
1053+
9661054
class LogNorm(Normalize):
9671055
"""
9681056
Normalize a given value to the 0-1 range on a log scale

lib/matplotlib/tests/test_colors.py

Lines changed: 208 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,205 @@ def test_Normalize():
9899
_mask_tester(norm, vals)
99100

100101

102+
class BaseNormMixin(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+
if self.test_inverse:
109+
_inverse_tester(self.norm, self.vals)
110+
else:
111+
pass
112+
113+
def test_scalar(self):
114+
_scalar_tester(self.norm, self.vals)
115+
116+
def test_mask(self):
117+
_mask_tester(self.norm, self.vals)
118+
119+
def test_autoscale(self):
120+
norm = self.normclass()
121+
norm.autoscale([10, 20, 30, 40])
122+
nt.assert_equal(norm.vmin, 10.)
123+
nt.assert_equal(norm.vmax, 40.)
124+
125+
def test_autoscale_None_vmin(self):
126+
norm = self.normclass(vmin=0, vmax=None)
127+
norm.autoscale_None([1, 2, 3, 4, 5])
128+
nt.assert_equal(norm.vmin, 0.)
129+
nt.assert_equal(norm.vmax, 5.)
130+
131+
def test_autoscale_None_vmax(self):
132+
norm = self.normclass(vmin=None, vmax=10)
133+
norm.autoscale_None([1, 2, 3, 4, 5])
134+
nt.assert_equal(norm.vmin, 1.)
135+
nt.assert_equal(norm.vmax, 10.)
136+
137+
def test_scale(self):
138+
norm = self.normclass()
139+
nt.assert_false(norm.scaled())
140+
141+
norm([1, 2, 3, 4])
142+
nt.assert_true(norm.scaled())
143+
144+
def test_process_value_scalar(self):
145+
res, is_scalar = mcolors.Normalize.process_value(5)
146+
nt.assert_true(is_scalar)
147+
assert_array_equal(res, np.array([5.]))
148+
149+
def test_process_value_list(self):
150+
res, is_scalar = mcolors.Normalize.process_value([5, 10])
151+
nt.assert_false(is_scalar)
152+
assert_array_equal(res, np.array([5., 10.]))
153+
154+
def test_process_value_tuple(self):
155+
res, is_scalar = mcolors.Normalize.process_value((5, 10))
156+
nt.assert_false(is_scalar)
157+
assert_array_equal(res, np.array([5., 10.]))
158+
159+
def test_process_value_array(self):
160+
res, is_scalar = mcolors.Normalize.process_value(np.array([5, 10]))
161+
nt.assert_false(is_scalar)
162+
assert_array_equal(res, np.array([5., 10.]))
163+
164+
165+
class BasePiecewiseLinearNorm(BaseNormMixin):
166+
normclass = mcolors.PiecewiseLinearNorm
167+
test_inverse = False
168+
169+
class test_PiecewiseLinearNorm_Even(BasePiecewiseLinearNorm):
170+
def setup(self):
171+
self.norm = self.normclass(vmin=-1, vcenter=0, vmax=4)
172+
self.vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
173+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
174+
175+
176+
class test_PiecewiseLinearNorm_Odd(BasePiecewiseLinearNorm):
177+
def setup(self):
178+
self.normclass = mcolors.PiecewiseLinearNorm
179+
self.norm = self.normclass(vmin=-2, vcenter=0, vmax=5)
180+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
181+
self.expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
182+
183+
184+
class test_PiecewiseLinearNorm_AllNegative(BasePiecewiseLinearNorm):
185+
def setup(self):
186+
self.normclass = mcolors.PiecewiseLinearNorm
187+
self.norm = self.normclass(vmin=-10, vcenter=-8, vmax=-2)
188+
self.vals = np.array([-10., -9., -8., -6., -4., -2.])
189+
self.expected = np.array([0.0, 0.25, 0.5, 0.666667, 0.833333, 1.0])
190+
191+
192+
class test_PiecewiseLinearNorm_AllPositive(BasePiecewiseLinearNorm):
193+
def setup(self):
194+
self.normclass = mcolors.PiecewiseLinearNorm
195+
self.norm = self.normclass(vmin=0, vcenter=3, vmax=9)
196+
self.vals = np.array([0., 1.5, 3., 4.5, 6.0, 7.5, 9.])
197+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
198+
199+
200+
class test_PiecewiseLinearNorm_NoVs(BasePiecewiseLinearNorm):
201+
def setup(self):
202+
self.normclass = mcolors.PiecewiseLinearNorm
203+
self.norm = self.normclass(vmin=None, vcenter=None, vmax=None)
204+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
205+
self.expected = np.array([0., 0.16666667, 0.33333333,
206+
0.5, 0.66666667, 0.83333333, 1.0])
207+
self.expected_vmin = -2
208+
self.expected_vcenter = 1
209+
self.expected_vmax = 4
210+
211+
def test_vmin(self):
212+
nt.assert_true(self.norm.vmin is None)
213+
self.norm(self.vals)
214+
nt.assert_equal(self.norm.vmin, self.expected_vmin)
215+
216+
def test_vcenter(self):
217+
nt.assert_true(self.norm.vcenter is None)
218+
self.norm(self.vals)
219+
nt.assert_equal(self.norm.vcenter, self.expected_vcenter)
220+
221+
def test_vmax(self):
222+
nt.assert_true(self.norm.vmax is None)
223+
self.norm(self.vals)
224+
nt.assert_equal(self.norm.vmax, self.expected_vmax)
225+
226+
227+
class test_PiecewiseLinearNorm_VminEqualsVcenter(BasePiecewiseLinearNorm):
228+
def setup(self):
229+
self.normclass = mcolors.PiecewiseLinearNorm
230+
self.norm = self.normclass(vmin=-2, vcenter=-2, vmax=2)
231+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
232+
self.expected = np.array([0.5, 0.625, 0.75, 0.875, 1.0])
233+
234+
235+
class test_PiecewiseLinearNorm_VmaxEqualsVcenter(BasePiecewiseLinearNorm):
236+
def setup(self):
237+
self.normclass = mcolors.PiecewiseLinearNorm
238+
self.norm = self.normclass(vmin=-2, vcenter=2, vmax=2)
239+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
240+
self.expected = np.array([0.0, 0.125, 0.25, 0.375, 0.5])
241+
242+
243+
class test_PiecewiseLinearNorm_VsAllEqual(BasePiecewiseLinearNorm):
244+
def setup(self):
245+
self.v = 10
246+
self.normclass = mcolors.PiecewiseLinearNorm
247+
self.norm = self.normclass(vmin=self.v, vcenter=self.v, vmax=self.v)
248+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
249+
self.expected = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
250+
self.expected_inv = self.expected + self.v
251+
252+
def test_inverse(self):
253+
assert_array_almost_equal(
254+
self.norm.inverse(self.norm(self.vals)),
255+
self.expected_inv
256+
)
257+
258+
259+
class test_PiecewiseLinearNorm_Errors(object):
260+
def setup(self):
261+
self.vals = np.arange(50)
262+
263+
@nt.raises(ValueError)
264+
def test_VminGTVcenter(self):
265+
norm = mcolors.PiecewiseLinearNorm(vmin=10, vcenter=0, vmax=20)
266+
norm(self.vals)
267+
268+
@nt.raises(ValueError)
269+
def test_VminGTVmax(self):
270+
norm = mcolors.PiecewiseLinearNorm(vmin=10, vcenter=0, vmax=5)
271+
norm(self.vals)
272+
273+
@nt.raises(ValueError)
274+
def test_VcenterGTVmax(self):
275+
norm = mcolors.PiecewiseLinearNorm(vmin=10, vcenter=25, vmax=20)
276+
norm(self.vals)
277+
278+
@nt.raises(ValueError)
279+
def test_premature_scaling(self):
280+
norm = mcolors.PiecewiseLinearNorm()
281+
norm.inverse(np.array([0.1, 0.5, 0.9]))
282+
283+
284+
@image_comparison(baseline_images=['test_offset_norm'], extensions=['png'])
285+
def test_offset_norm_img():
286+
x = np.linspace(-2, 7)
287+
y = np.linspace(-1*np.pi, np.pi)
288+
X, Y = np.meshgrid(x, y)
289+
Z = x * np.sin(Y)**2
290+
291+
fig, (ax1, ax2) = plt.subplots(ncols=2)
292+
cmap = plt.cm.coolwarm
293+
norm = mcolors.PiecewiseLinearNorm(vmin=-2, vcenter=0, vmax=7)
294+
295+
img1 = ax1.imshow(Z, cmap=cmap, norm=None)
296+
cbar1 = fig.colorbar(img1, ax=ax1)
297+
298+
img2 = ax2.imshow(Z, cmap=cmap, norm=norm)
299+
cbar2 = fig.colorbar(img2, ax=ax2)
300+
101301
def test_SymLogNorm():
102302
"""
103303
Test SymLogNorm behavior
@@ -216,7 +416,12 @@ def test_cmap_and_norm_from_levels_and_colors2():
216416
'Wih extend={0!r} and data '
217417
'value={1!r}'.format(extend, d_val))
218418

219-
assert_raises(ValueError, mcolors.from_levels_and_colors, levels, colors)
419+
nt.assert_raises(
420+
ValueError,
421+
mcolors.from_levels_and_colors,
422+
levels,
423+
colors
424+
)
220425

221426

222427
def test_rgb_hsv_round_trip():
@@ -246,8 +451,8 @@ def gray_from_float_rgb():
246451
def gray_from_float_rgba():
247452
return mcolors.colorConverter.to_rgba(0.4)
248453

249-
assert_raises(ValueError, gray_from_float_rgb)
250-
assert_raises(ValueError, gray_from_float_rgba)
454+
nt.assert_raises(ValueError, gray_from_float_rgb)
455+
nt.assert_raises(ValueError, gray_from_float_rgba)
251456

252457

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

0 commit comments

Comments
 (0)