@@ -998,80 +998,160 @@ class PiecewiseLinearNorm(Normalize):
998
998
999
999
Normalizes data into the ``[0.0, 1.0]`` interval.
1000
1000
"""
1001
- def __init__ ( self , vmin = None , vcenter = None , vmax = None ):
1002
- """Normalize data with an offset midpoint
1003
-
1004
- Useful when mapping data unequally centered around a conceptual
1005
- center, e.g., data that range from -2 to 4, with 0 as the midpoint.
1001
+ # TODO rewrite the internals of this class once we support OrderedDicts
1002
+ # i.e. after we drop support for python 2.6
1003
+ def __init__ ( self , stops = None ):
1004
+ """Normalize data linearly between the defined stop points.
1005
+ Use this as more generic form of ``DivergingNorm``
1006
1006
1007
1007
Parameters
1008
1008
----------
1009
- vmin : float, optional
1010
- The data value that defines ``0.0`` in the normalized data.
1011
- Defaults to the min value of the dataset.
1012
-
1013
- vcenter : float, optional
1014
- The data value that defines ``0.5`` in the normalized data.
1015
- Defaults to halfway between *vmin* and *vmax*.
1016
-
1017
- vmax : float, optional
1018
- The data value that defines ``1.0`` in the normalized data.
1019
- Defaults to the the max value of the dataset.
1009
+ stops : dict-like, optional
1010
+ Accepts a dictionary or anything that can get converted to a
1011
+ dictionary which maps the space [0.0, 1.0] to data point, i.e. key
1012
+ value pairs.
1020
1013
1021
1014
Examples
1022
1015
--------
1016
+ Note this example is equivalent to the DivergingNorm example.
1023
1017
>>> import matplotlib.colors as mcolors
1024
- >>> offset = mcolors.PiecewiseLinearNorm(vmin= -2., vcenter=0., vmax =4.)
1018
+ >>> offset = mcolors.PiecewiseLinearNorm({0.: -2., 0.5: 0., 1. =4.} )
1025
1019
>>> data = [-2., -1., 0., 1., 2., 3., 4.]
1026
1020
>>> offset(data)
1027
1021
array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1028
1022
1029
1023
"""
1024
+ self ._set_stops (stops )
1025
+
1026
+ @property
1027
+ def vmin (self ):
1028
+ try :
1029
+ if self ._stops [0 ][0 ] == 0 :
1030
+ return self ._stops [0 ][1 ]
1031
+ except IndexError :
1032
+ return None
1033
+
1034
+ @vmin .setter
1035
+ def vmin (self , vmin ):
1036
+ try :
1037
+ if self ._stops [0 ][0 ] == 0 :
1038
+ self ._stops [0 ] = (self ._stops [0 ][0 ], vmin )
1039
+ return
1040
+ except IndexError :
1041
+ pass
1042
+ self .append_stop (0. , vmin )
1043
+
1044
+ @property
1045
+ def vmax (self ):
1046
+ try :
1047
+ if self ._stops [- 1 ][0 ] == 1 :
1048
+ return self ._stops [- 1 ][1 ]
1049
+ except IndexError :
1050
+ return None
1051
+
1052
+ @vmax .setter
1053
+ def vmax (self , vmax ):
1054
+ try :
1055
+ if self ._stops [- 1 ][0 ] == 1 :
1056
+ self ._stops [- 1 ] = (self ._stops [- 1 ][0 ], vmax )
1057
+ return
1058
+ except IndexError :
1059
+ pass
1060
+ self .append_stop (1. , vmax )
1061
+
1062
+ # TODO Change this to a property when we drop 2.6 and use Ordered Dicts
1063
+ def _set_stops (self , stops ):
1064
+ if not stops :
1065
+ self ._stops = []
1066
+ return
1067
+
1068
+ stops = dict (stops )
1069
+ self ._stops = sorted (stops .items (), key = operator .itemgetter (0 ))
1070
+ map_points , data_points = zip (* self ._stops )
1071
+ if not np .all (np .diff (data_points ) > 0 ):
1072
+ raise ValueError ("stops must increase monotonically" )
1073
+
1074
+ def append_stop (self , cmap_fraction , data_value ):
1075
+ i = - 1
1076
+ for i , (map_point , data_point ) in enumerate (self ._stops ):
1077
+ if map_point >= cmap_fraction :
1078
+ d1 = data_point # the current index
1079
+ break
1080
+ else :
1081
+ i += 1 # the index to insert before
1082
+ d1 = np .inf
1083
+
1084
+ if i > 0 :
1085
+ d0 = self ._stops [i - 1 ][1 ]
1086
+ else :
1087
+ d0 = - np .inf
1030
1088
1031
- self .vmin = vmin
1032
- self .vcenter = vcenter
1033
- self .vmax = vmax
1089
+ if not (d0 < data_value < d1 ):
1090
+ raise ValueError (('Stops must increase monotonically, due to the '
1091
+ + 'stops already set, the cmap_fraction specified'
1092
+ + ' (%f) means that the data_value must lie '
1093
+ + 'between %f and %f, but %f given' ) %
1094
+ (cmap_fraction , d0 , d1 , data_value ))
1095
+
1096
+ self ._stops .insert (i , (cmap_fraction , data_value ))
1034
1097
1035
1098
def __call__ (self , value , clip = None ):
1036
1099
"""Map value to the interval [0, 1]. The clip argument is unused."""
1037
1100
1038
1101
result , is_scalar = self .process_value (value )
1039
-
1040
1102
self .autoscale_None (result )
1041
- vmin , vcenter , vmax = self .vmin , self .vcenter , self .vmax
1042
- if vmin == vmax == vcenter :
1043
- result .fill (0 )
1044
- elif not vmin <= vcenter <= vmax :
1045
- raise ValueError ("minvalue must be less than or equal to "
1046
- "centervalue which must be less than or "
1047
- "equal to maxvalue" )
1048
- else :
1049
- vmin = float (vmin )
1050
- vcenter = float (vcenter )
1051
- vmax = float (vmax )
1052
- # in degenerate cases, prefer the center value to the extremes
1053
- degen = (result == vcenter ) if vcenter == vmax else None
1054
-
1055
- x , y = [vmin , vcenter , vmax ], [0 , 0.5 , 1 ]
1056
- result = ma .masked_array (np .interp (result , x , y ),
1057
- mask = ma .getmask (result ))
1058
- if degen is not None :
1059
- result [degen ] = 0.5
1060
1103
1104
+ map_points , data_points = zip (* self ._stops )
1105
+ result = ma .masked_array (np .interp (result , data_points , map_points ),
1106
+ mask = ma .getmask (result ))
1061
1107
if is_scalar :
1062
1108
result = np .atleast_1d (result )[0 ]
1063
1109
return result
1064
1110
1065
1111
def autoscale_None (self , A ):
1066
- ' autoscale only None-valued vmin or vmax'
1067
- if self .vmin is None and np .size (A ) > 0 :
1068
- self .vmin = ma .min (A )
1112
+ """Ensures we have the upper and lower bounds set, using the data A"""
1113
+ if len (self ._stops ) == 0 or self ._stops [0 ][0 ] != 0 :
1114
+ self .append_stop (0. , ma .min (A ))
1115
+ if self ._stops [- 1 ][0 ] != 1 :
1116
+ self .append_stop (1. , ma .max (A ))
1117
+
1118
+
1119
+ class DivergingNorm (PiecewiseLinearNorm ):
1120
+ def __init__ (self , vmin = None , vcenter = None , vmax = None ):
1121
+ """Normalize data with an offset midpoint
1069
1122
1070
- if self .vmax is None and np .size (A ) > 0 :
1071
- self .vmax = ma .max (A )
1123
+ Useful when mapping data unequally centered around a conceptual
1124
+ center, e.g., data that range from -2 to 4, with 0 as the midpoint.
1125
+
1126
+ Parameters
1127
+ ----------
1128
+ vmin : float, optional
1129
+ The data value that defines ``0.0`` in the normalized data.
1130
+ Defaults to the min value of the dataset.
1072
1131
1073
- if self .vcenter is None :
1074
- self .vcenter = (self .vmax + self .vmin ) * 0.5
1132
+ vcenter : float, optional
1133
+ The data value that defines ``0.5`` in the normalized data.
1134
+ Defaults to halfway between *vmin* and *vmax*.
1135
+
1136
+ vmax : float, optional
1137
+ The data value that defines ``1.0`` in the normalized data.
1138
+ Defaults to the the max value of the dataset.
1139
+
1140
+ Examples
1141
+ --------
1142
+ >>> import matplotlib.colors as mcolors
1143
+ >>> offset = mcolors.PiecewiseLinearNorm(vmin=-2., vcenter=0., vmax=4.)
1144
+ >>> data = [-2., -1., 0., 1., 2., 3., 4.]
1145
+ >>> offset(data)
1146
+ array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1147
+ stops = {}
1148
+ if vmin is not None:
1149
+ stops[0.] = vmin
1150
+ if vcenter is not None:
1151
+ stops[0.5] = vcenter
1152
+ if vmax is not None:
1153
+ stops[1.] = vmax
1154
+ super(DivergingNorm, self).__init__(stops)
1075
1155
1076
1156
1077
1157
class LogNorm(Normalize):
0 commit comments