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

Skip to content

Commit 512f2d4

Browse files
phobsonjklymak
authored andcommitted
ENH: Add PiecewiseLinearNorm 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 9869fd7 commit 512f2d4

File tree

5 files changed

+362
-4
lines changed

5 files changed

+362
-4
lines changed

doc/users/whats_new.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ revision, see the :ref:`github-stats`.
1010
.. contents:: Table of Contents
1111
:depth: 4
1212

13-
1413
..
1514
For a release, add a new section after this, then comment out the include
1615
and toctree below by indenting them. Uncomment them after the release.

lib/matplotlib/colorbar.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -772,8 +772,8 @@ def _process_values(self, b=None):
772772
+ self._boundaries[1:])
773773
if isinstance(self.norm, colors.NoNorm):
774774
self._values = (self._values + 0.00001).astype(np.int16)
775-
return
776-
self._values = np.array(self.values)
775+
else:
776+
self._values = np.array(self.values)
777777
return
778778
if self.values is not None:
779779
self._values = np.array(self.values)

lib/matplotlib/colors.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,164 @@ def scaled(self):
992992
return self.vmin is not None and self.vmax is not None
993993

994994

995+
class PiecewiseLinearNorm(Normalize):
996+
"""
997+
Normalizes data into the ``[0.0, 1.0]`` interval over linear segments.
998+
"""
999+
def __init__(self, data_points=None, norm_points=None):
1000+
"""Normalize data linearly between the defined stop points.
1001+
1002+
Parameters
1003+
----------
1004+
data_points : tuple of floats
1005+
Floats spanning the data to be mapped between 0-1
1006+
1007+
norm_points : tuple of floats or None (default)
1008+
Floats spanning [0, 1] that the data points will map to. If
1009+
*None*, the data points will be mapped equally into [0, 1].
1010+
1011+
Examples
1012+
--------
1013+
Note this example is equivalent to the `.DivergingNorm` example.
1014+
>>> import matplotlib.colors as mcolors
1015+
>>> offset = mcolors.PiecewiseLinearNorm([-2, 0, 4])
1016+
>>> data = [-2., -1., 0., 1., 2., 3., 4.]
1017+
>>> offset(data)
1018+
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1019+
"""
1020+
self._data_points = (np.asarray(data_points)
1021+
if data_points is not None
1022+
else None)
1023+
self._norm_points = (np.asarray(norm_points)
1024+
if norm_points is not None
1025+
else None)
1026+
# self._norm_points = np.asarray(norm_points)
1027+
self.vmin = data_points[0]
1028+
self.vmax = data_points[-1]
1029+
1030+
def __call__(self, value, clip=None):
1031+
"""
1032+
Map value to the interval [0, 1]. The clip argument is unused.
1033+
"""
1034+
1035+
result, is_scalar = self.process_value(value)
1036+
self.autoscale_None(result)
1037+
vmin, vmax = self._data_points[0], self._data_points[-1]
1038+
1039+
# its possible some of the data points are less than vmin
1040+
# or vmax.
1041+
ind = np.where((self._data_points >= vmin) &
1042+
(self._data_points <= vmax))[0]
1043+
result = np.ma.masked_array(np.interp(result,
1044+
self._data_points[ind],
1045+
self._norm_points[ind]),
1046+
mask=np.ma.getmask(result))
1047+
if is_scalar:
1048+
result = np.atleast_1d(result)[0]
1049+
return result
1050+
1051+
def autoscale_None(self, A):
1052+
"""
1053+
Parameters:
1054+
-----------
1055+
1056+
A : tuple
1057+
data used for autoscaling, if appropriate.
1058+
1059+
If ``norm.vmin` is None, or ``norm.vmax`` are none, use
1060+
the min and max of ``A`` for the endpoints of the linear mapping.
1061+
"""
1062+
1063+
# allow autoscaling with the data if the user specifies
1064+
# vmin or vmax is None. Note we never set vmax/vmin in the class
1065+
# except at init.
1066+
vmin = self.vmin
1067+
if vmin is None:
1068+
vmin = np.ma.min(A)
1069+
vmax = self.vmax
1070+
if vmax is None:
1071+
vmax = np.ma.max(A)
1072+
1073+
if vmin > vmax:
1074+
raise ValueError('vmin must be less than or equal to vmax')
1075+
1076+
if self._data_points is None:
1077+
self._data_points = np.asarray([vmin, vmax])
1078+
if self._norm_points is None:
1079+
N = len(self._data_points)
1080+
self._norm_points = np.linspace(0, 1, N)
1081+
1082+
self._data_points[0] = vmin
1083+
self._data_points[-1] = vmax
1084+
1085+
1086+
class DivergingNorm(PiecewiseLinearNorm):
1087+
def __init__(self, vmin=None, vcenter=None, vmax=None):
1088+
"""
1089+
Normalize data with a set center.
1090+
1091+
Useful when mapping data with an unequal rates of change around a
1092+
conceptual center, e.g., data that range from -2 to 4, with 0 as
1093+
the midpoint.
1094+
1095+
Parameters
1096+
----------
1097+
vmin : float, optional
1098+
The data value that defines ``0.0`` in the normalization.
1099+
Defaults to the min value of the dataset.
1100+
vcenter : float, optional
1101+
The data value that defines ``0.5`` in the normalization.
1102+
If not defined, the normalization just linearly maps between
1103+
*vmin* and *vmax*.
1104+
vmax : float, optional
1105+
The data value that defines ``1.0`` in the normalization.
1106+
Defaults to the the max value of the dataset.
1107+
1108+
Examples
1109+
--------
1110+
This maps data value -4000 to 0., 0 to 0.5, and +10000 to 1.0; data
1111+
between is linearly interpolated::
1112+
1113+
>>> import matplotlib.colors as mcolors
1114+
>>> offset = mcolors.DivergingNorm(vmin=-4000.,
1115+
vcenter=0., vmax=10000)
1116+
>>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.]
1117+
>>> offset(data)
1118+
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1119+
"""
1120+
1121+
# vmin/vmax will set the first and last value, so they don't
1122+
# matter very much.
1123+
if vcenter is None:
1124+
stops = [0, 1]
1125+
else:
1126+
stops = [vcenter * 0.75, vcenter, vcenter * 1.5]
1127+
super(DivergingNorm, self).__init__(stops)
1128+
# if these have been specified we need to set them here so
1129+
# DivergingNorm knows they are user-set limits. Otherwise it
1130+
# will autoscale to data.
1131+
self.vmin = vmin
1132+
self.vmax = vmax
1133+
if vcenter is not None and vmax is not None and vcenter > vmax:
1134+
raise ValueError('vmin, vcenter, and vmax must be in '
1135+
'ascending order')
1136+
if vcenter is not None and vmin is not None and vcenter < vmin:
1137+
raise ValueError('vmin, vcenter, and vmax must be in '
1138+
'ascending order')
1139+
1140+
def __call__(value, clip=None):
1141+
"""
1142+
Map value to the interval [0, 1]. The clip argument is unused.
1143+
"""
1144+
try:
1145+
super().__call(value, clip=clip)
1146+
except ValueError:
1147+
# improve the more general error message.
1148+
raise ValueError('vmin, vcenter, and vmax must '
1149+
'increment monotonically for DivergingNorm '
1150+
'to work.')
1151+
1152+
9951153
class LogNorm(Normalize):
9961154
"""Normalize a given value to the 0-1 range on a log scale."""
9971155

lib/matplotlib/tests/test_colors.py

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import copy
22
import itertools
3-
43
import numpy as np
54
import pytest
65

@@ -221,6 +220,208 @@ def test_Normalize():
221220
assert 0 < norm(1 + 50 * eps) < 1
222221

223222

223+
class BaseNormMixin(object):
224+
def test_call(self):
225+
normed_vals = self.norm(self.vals)
226+
np.assert_array_almost_equal(normed_vals, self.expected)
227+
228+
def test_inverse(self):
229+
if self.test_inverse:
230+
_inverse_tester(self.norm, self.vals)
231+
else:
232+
pass
233+
234+
def test_scalar(self):
235+
_scalar_tester(self.norm, self.vals)
236+
237+
def test_mask(self):
238+
_mask_tester(self.norm, self.vals)
239+
240+
def test_autoscale(self):
241+
norm = self.normclass()
242+
norm.autoscale([10, 20, 30, 40])
243+
np.assert_equal(norm.vmin, 10.)
244+
np.assert_equal(norm.vmax, 40.)
245+
246+
def test_autoscale_None_vmin(self):
247+
norm = self.normclass(vmin=0, vmax=None)
248+
norm.autoscale_None([1, 2, 3, 4, 5])
249+
np.assert_equal(norm.vmin, 0.)
250+
np.assert_equal(norm.vmax, 5.)
251+
252+
def test_autoscale_None_vmax(self):
253+
norm = self.normclass(vmin=None, vmax=10)
254+
norm.autoscale_None([1, 2, 3, 4, 5])
255+
np.assert_equal(norm.vmin, 1.)
256+
np.assert_equal(norm.vmax, 10.)
257+
258+
def test_scale(self):
259+
norm = self.normclass()
260+
np.assert_false(norm.scaled())
261+
262+
norm([1, 2, 3, 4])
263+
np.assert_true(norm.scaled())
264+
265+
def test_process_value_scalar(self):
266+
res, is_scalar = mcolors.Normalize.process_value(5)
267+
np.assert_true(is_scalar)
268+
assert_array_equal(res, np.array([5.]))
269+
270+
def test_process_value_list(self):
271+
res, is_scalar = mcolors.Normalize.process_value([5, 10])
272+
np.assert_false(is_scalar)
273+
np.assert_array_equal(res, np.array([5., 10.]))
274+
275+
def test_process_value_tuple(self):
276+
res, is_scalar = mcolors.Normalize.process_value((5, 10))
277+
np.assert_false(is_scalar)
278+
np.assert_array_equal(res, np.array([5., 10.]))
279+
280+
def test_process_value_array(self):
281+
res, is_scalar = mcolors.Normalize.process_value(np.array([5, 10]))
282+
np.assert_false(is_scalar)
283+
np.assert_array_equal(res, np.array([5., 10.]))
284+
285+
286+
class BaseDivergingNorm(BaseNormMixin):
287+
normclass = mcolors.DivergingNorm
288+
test_inverse = False
289+
290+
291+
class test_DivergingNorm_Even(BaseDivergingNorm):
292+
def setup(self):
293+
self.norm = self.normclass(vmin=-1, vcenter=0, vmax=4)
294+
self.vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
295+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
296+
297+
298+
class test_DivergingNorm_Odd(BaseDivergingNorm):
299+
def setup(self):
300+
self.normclass = mcolors.DivergingNorm
301+
self.norm = self.normclass(vmin=-2, vcenter=0, vmax=5)
302+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
303+
self.expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
304+
305+
306+
class test_DivergingNorm_AllNegative(BaseDivergingNorm):
307+
def setup(self):
308+
self.normclass = mcolors.DivergingNorm
309+
self.norm = self.normclass(vmin=-10, vcenter=-8, vmax=-2)
310+
self.vals = np.array([-10., -9., -8., -6., -4., -2.])
311+
self.expected = np.array([0.0, 0.25, 0.5, 0.666667, 0.833333, 1.0])
312+
313+
314+
class test_DivergingNorm_AllPositive(BaseDivergingNorm):
315+
def setup(self):
316+
self.normclass = mcolors.DivergingNorm
317+
self.norm = self.normclass(vmin=0, vcenter=3, vmax=9)
318+
self.vals = np.array([0., 1.5, 3., 4.5, 6.0, 7.5, 9.])
319+
self.expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
320+
321+
322+
class test_DivergingNorm_NoVs(BaseDivergingNorm):
323+
def setup(self):
324+
self.normclass = mcolors.DivergingNorm
325+
self.norm = self.normclass(vmin=None, vcenter=None, vmax=None)
326+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0])
327+
self.expected = np.array([0., 0.16666667, 0.33333333,
328+
0.5, 0.66666667, 0.83333333, 1.0])
329+
self.expected_vmin = -2
330+
self.expected_vcenter = 1
331+
self.expected_vmax = 4
332+
333+
def test_vmin(self):
334+
np.assert_true(self.norm.vmin is None)
335+
self.norm(self.vals)
336+
np.assert_equal(self.norm.vmin, self.expected_vmin)
337+
338+
def test_vcenter(self):
339+
np.assert_true(self.norm.vcenter is None)
340+
self.norm(self.vals)
341+
np.assert_equal(self.norm.vcenter, self.expected_vcenter)
342+
343+
def test_vmax(self):
344+
np.assert_true(self.norm.vmax is None)
345+
self.norm(self.vals)
346+
np.assert_equal(self.norm.vmax, self.expected_vmax)
347+
348+
349+
class test_DivergingNorm_VminEqualsVcenter(BaseDivergingNorm):
350+
def setup(self):
351+
self.normclass = mcolors.DivergingNorm
352+
self.norm = self.normclass(vmin=-2, vcenter=-2, vmax=2)
353+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
354+
self.expected = np.array([0.5, 0.625, 0.75, 0.875, 1.0])
355+
356+
357+
class test_DivergingNorm_VmaxEqualsVcenter(BaseDivergingNorm):
358+
def setup(self):
359+
self.normclass = mcolors.DivergingNorm
360+
self.norm = self.normclass(vmin=-2, vcenter=2, vmax=2)
361+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
362+
self.expected = np.array([0.0, 0.125, 0.25, 0.375, 0.5])
363+
364+
365+
class test_DivergingNorm_VsAllEqual(BaseDivergingNorm):
366+
def setup(self):
367+
self.v = 10
368+
self.normclass = mcolors.DivergingNorm
369+
self.norm = self.normclass(vmin=self.v, vcenter=self.v, vmax=self.v)
370+
self.vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0])
371+
self.expected = np.array([0.0, 0.0, 0.0, 0.0, 0.0])
372+
self.expected_inv = self.expected + self.v
373+
374+
def test_inverse(self):
375+
assert_array_almost_equal(
376+
self.norm.inverse(self.norm(self.vals)),
377+
self.expected_inv
378+
)
379+
380+
381+
class test_DivergingNorm_Errors(object):
382+
def setup(self):
383+
self.vals = np.arange(50)
384+
385+
def test_VminGTVcenter(self):
386+
with pytest.raises(ValueError):
387+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=20)
388+
norm(self.vals)
389+
390+
def test_VminGTVmax(self):
391+
with pytest.raises(ValueError):
392+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)
393+
norm(self.vals)
394+
395+
def test_VcenterGTVmax(self):
396+
with pytest.raises(ValueError):
397+
norm = mcolors.DivergingNorm(vmin=10, vcenter=25, vmax=20)
398+
norm(self.vals)
399+
400+
def test_premature_scaling(self):
401+
with pytest.raises(ValueError):
402+
norm = mcolors.DivergingNorm()
403+
norm.inverse(np.array([0.1, 0.5, 0.9]))
404+
405+
406+
@image_comparison(baseline_images=['test_offset_norm'], extensions=['png'],
407+
style='mpl20')
408+
def test_offset_norm_img():
409+
x = np.linspace(-2, 7)
410+
y = np.linspace(-1*np.pi, np.pi)
411+
X, Y = np.meshgrid(x, y)
412+
Z = x * np.sin(Y)**2
413+
414+
fig, (ax1, ax2) = plt.subplots(ncols=2)
415+
cmap = plt.cm.coolwarm
416+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=7)
417+
418+
img1 = ax1.pcolormesh(Z, cmap=cmap, norm=None)
419+
cbar1 = fig.colorbar(img1, ax=ax1)
420+
421+
img2 = ax2.pcolormesh(Z, cmap=cmap, norm=norm)
422+
cbar2 = fig.colorbar(img2, ax=ax2)
423+
424+
224425
def test_SymLogNorm():
225426
"""
226427
Test SymLogNorm behavior

0 commit comments

Comments
 (0)