|
62 | 62 |
|
63 | 63 | from collections.abc import Sized
|
64 | 64 | import itertools
|
| 65 | +import operator |
65 | 66 | import re
|
66 | 67 |
|
67 | 68 | import numpy as np
|
@@ -992,6 +993,164 @@ def scaled(self):
|
992 | 993 | return self.vmin is not None and self.vmax is not None
|
993 | 994 |
|
994 | 995 |
|
| 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 | + |
995 | 1154 | class LogNorm(Normalize):
|
996 | 1155 | """Normalize a given value to the 0-1 range on a log scale."""
|
997 | 1156 |
|
|
0 commit comments