FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

ENH: Add PiecewiseLinearNorm and tests · jklymak/matplotlib@cc17eb1 · GitHub

Repository navigation

Commit cc17eb1

Browse files
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` TST: add tests for DivergingNorm DOC: add to colors_api.rst
1 parent 922dea2 commit cc17eb1

6 files changed

Lines changed: 276 additions & 5 deletions

File tree

‎doc/api/colors_api.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ Classes
2525

2626
BoundaryNorm
2727
Colormap
28+
DivergingNorm
2829
LightSource
2930
LinearSegmentedColormap
3031
ListedColormap
3132
LogNorm
3233
NoNorm
3334
Normalize
35+
PiecewiseLinearNorm
3436
PowerNorm
3537
SymLogNorm
3638

‎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
@@ -873,8 +873,8 @@ def _process_values(self, b=None):
873873
+ self._boundaries[1:])
874874
if isinstance(self.norm, colors.NoNorm):
875875
self._values = (self._values + 0.00001).astype(np.int16)
876-
return
877-
self._values = np.array(self.values)
876+
else:
877+
self._values = np.array(self.values)
878878
return
879879
if self.values is not None:
880880
self._values = np.array(self.values)

‎lib/matplotlib/colors.py‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,176 @@ def scaled(self):
955955
return self.vmin is not None and self.vmax is not None
956956

957957

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

24.8 KB
LoadingViewer requires iframe.

‎lib/matplotlib/tests/test_colors.py‎

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import copy
22
import itertools
3-
43
import numpy as np
54
import pytest
65

7-
from numpy.testing import assert_array_equal, assert_array_almost_equal
6+
from numpy.testing import (assert_array_equal, assert_array_almost_equal)
87

98
from matplotlib import cycler
109
import matplotlib
@@ -221,6 +220,107 @@ def test_Normalize():
221220
assert 0 < norm(1 + 50 * eps) < 1
222221

223222

223+
def test_DivergingNorm_autoscale():
224+
norm = mcolors.DivergingNorm()
225+
norm.autoscale([10, 20, 30, 40])
226+
assert norm.vmin == 10.
227+
assert norm.vmax == 40.
228+
229+
230+
def test_DivergingNorm_autoscale_None_vmin():
231+
norm = mcolors.DivergingNorm(vmin=0, vmax=None)
232+
norm.autoscale_None([1, 2, 3, 4, 5])
233+
assert norm(5) == 1
234+
assert norm.vmax == 5
235+
236+
237+
def test_DivergingNorm_autoscale_None_vmax():
238+
norm = mcolors.DivergingNorm(vmin=None, vmax=10)
239+
norm.autoscale_None([1, 2, 3, 4, 5])
240+
assert norm(1) == 0
241+
assert norm.vmin == 1
242+
243+
244+
def test_DivergingNorm_scale():
245+
norm = mcolors.DivergingNorm()
246+
assert norm.scaled() is False
247+
norm([1, 2, 3, 4])
248+
assert norm.scaled() is True
249+
250+
251+
def test_DivergingNorm_scaleout_center():
252+
norm = mcolors.DivergingNorm(vcenter=0)
253+
# if the center is defined outside the data limits, should raise an err.
254+
with pytest.raises(ValueError):
255+
norm([1, 2, 3, 5])
256+
257+
258+
def test_DivergingNorm_Even():
259+
norm = mcolors.DivergingNorm(vmin=-1, vcenter=0, vmax=4)
260+
vals = np.array([-1.0, -0.5, 0.0, 1.0, 2.0, 3.0, 4.0])
261+
expected = np.array([0.0, 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
262+
assert_array_equal(norm(vals), expected)
263+
264+
265+
def test_DivergingNorm_Odd():
266+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=5)
267+
vals = np.array([-2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0])
268+
expected = np.array([0.0, 0.25, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
269+
assert_array_equal(norm(vals), expected)
270+
271+
272+
def test_DivergingNorm_VminEqualsVcenter():
273+
with pytest.raises(ValueError):
274+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=-2, vmax=2)
275+
276+
277+
def test_DivergingNorm_VmaxEqualsVcenter():
278+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=2, vmax=2)
279+
with pytest.raises(ValueError):
280+
norm(1.5)
281+
282+
283+
def test_DivergingNorm_VminGTVcenter():
284+
with pytest.raises(ValueError):
285+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=20)
286+
287+
288+
def test_DivergingNorm_DivergingNorm_VminGTVmax():
289+
with pytest.raises(ValueError):
290+
norm = mcolors.DivergingNorm(vmin=10, vcenter=0, vmax=5)
291+
292+
293+
def test_DivergingNorm_VcenterGTVmax():
294+
vals = np.arange(50)
295+
with pytest.raises(ValueError):
296+
norm = mcolors.DivergingNorm(vmin=10, vcenter=25, vmax=20)
297+
298+
299+
def test_DivergingNorm_premature_scaling():
300+
with pytest.raises(ValueError):
301+
norm = mcolors.DivergingNorm()
302+
norm.inverse(np.array([0.1, 0.5, 0.9]))
303+
304+
305+
@image_comparison(baseline_images=['test_offset_norm'], extensions=['png'],
306+
style='mpl20')
307+
def test_offset_norm_img():
308+
x = np.linspace(-2, 7)
309+
y = np.linspace(-1*np.pi, np.pi)
310+
X, Y = np.meshgrid(x, y)
311+
Z = x * np.sin(Y)**2
312+
313+
fig, (ax1, ax2) = plt.subplots(ncols=2)
314+
cmap = plt.cm.coolwarm
315+
norm = mcolors.DivergingNorm(vmin=-2, vcenter=0, vmax=7)
316+
317+
img1 = ax1.pcolormesh(Z, cmap=cmap, norm=None)
318+
cbar1 = fig.colorbar(img1, ax=ax1)
319+
320+
img2 = ax2.pcolormesh(Z, cmap=cmap, norm=norm)
321+
cbar2 = fig.colorbar(img2, ax=ax2)
322+
323+
224324
def test_SymLogNorm():
225325
"""
226326
Test SymLogNorm behavior

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL