8000 ENH: Add PiecewiseLinearNorm and tests · jklymak/matplotlib@8714003 · GitHub
[go: up one dir, main page]

Skip to content

Commit 8714003

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`
8000
1 parent 9869fd7 commit 8714003

File tree

5 files changed

+363
-4
lines changed

5 files changed

+363
-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: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262

6363
from collections.abc import Sized
6464
import itertools
65+
import operator
6566
import re
6667

6768
import numpy as np
@@ -992,6 +993,164 @@ def scaled(self):
992993
return self.vmin is not None and self.vmax is not None
993994

994995

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

Loading

0 commit comments

Comments
 (0)
0