Coverage for /usr/lib/python3/dist-packages/matplotlib/category.py: 39%

90 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

1""" 

2Plotting of string "category" data: ``plot(['d', 'f', 'a'], [1, 2, 3])`` will 

3plot three points with x-axis values of 'd', 'f', 'a'. 

4 

5See :doc:`/gallery/lines_bars_and_markers/categorical_variables` for an 

6example. 

7 

8The module uses Matplotlib's `matplotlib.units` mechanism to convert from 

9strings to integers and provides a tick locator, a tick formatter, and the 

10`.UnitData` class that creates and stores the string-to-integer mapping. 

11""" 

12 

13from collections import OrderedDict 

14import dateutil.parser 

15import itertools 

16import logging 

17 

18import numpy as np 

19 

20from matplotlib import _api, ticker, units 

21 

22 

23_log = logging.getLogger(__name__) 

24 

25 

26class StrCategoryConverter(units.ConversionInterface): 

27 @staticmethod 

28 def convert(value, unit, axis): 

29 """ 

30 Convert strings in *value* to floats using mapping information stored 

31 in the *unit* object. 

32 

33 Parameters 

34 ---------- 

35 value : str or iterable 

36 Value or list of values to be converted. 

37 unit : `.UnitData` 

38 An object mapping strings to integers. 

39 axis : `~matplotlib.axis.Axis` 

40 The axis on which the converted value is plotted. 

41 

42 .. note:: *axis* is unused. 

43 

44 Returns 

45 ------- 

46 float or ndarray[float] 

47 """ 

48 if unit is None: 

49 raise ValueError( 

50 'Missing category information for StrCategoryConverter; ' 

51 'this might be caused by unintendedly mixing categorical and ' 

52 'numeric data') 

53 StrCategoryConverter._validate_unit(unit) 

54 # dtype = object preserves numerical pass throughs 

55 values = np.atleast_1d(np.array(value, dtype=object)) 

56 # pass through sequence of non binary numbers 

57 with _api.suppress_matplotlib_deprecation_warning(): 

58 is_numlike = all(units.ConversionInterface.is_numlike(v) 

59 and not isinstance(v, (str, bytes)) 

60 for v in values) 

61 if values.size and is_numlike: 

62 _api.warn_deprecated( 

63 "3.5", message="Support for passing numbers through unit " 

64 "converters is deprecated since %(since)s and support will be " 

65 "removed %(removal)s; use Axis.convert_units instead.") 

66 return np.asarray(values, dtype=float) 

67 # force an update so it also does type checking 

68 unit.update(values) 

69 return np.vectorize(unit._mapping.__getitem__, otypes=[float])(values) 

70 

71 @staticmethod 

72 def axisinfo(unit, axis): 

73 """ 

74 Set the default axis ticks and labels. 

75 

76 Parameters 

77 ---------- 

78 unit : `.UnitData` 

79 object string unit information for value 

80 axis : `~matplotlib.axis.Axis` 

81 axis for which information is being set 

82 

83 .. note:: *axis* is not used 

84 

85 Returns 

86 ------- 

87 `~matplotlib.units.AxisInfo` 

88 Information to support default tick labeling 

89 

90 """ 

91 StrCategoryConverter._validate_unit(unit) 

92 # locator and formatter take mapping dict because 

93 # args need to be pass by reference for updates 

94 majloc = StrCategoryLocator(unit._mapping) 

95 majfmt = StrCategoryFormatter(unit._mapping) 

96 return units.AxisInfo(majloc=majloc, majfmt=majfmt) 

97 

98 @staticmethod 

99 def default_units(data, axis): 

100 """ 

101 Set and update the `~matplotlib.axis.Axis` units. 

102 

103 Parameters 

104 ---------- 

105 data : str or iterable of str 

106 axis : `~matplotlib.axis.Axis` 

107 axis on which the data is plotted 

108 

109 Returns 

110 ------- 

111 `.UnitData` 

112 object storing string to integer mapping 

113 """ 

114 # the conversion call stack is default_units -> axis_info -> convert 

115 if axis.units is None: 

116 axis.set_units(UnitData(data)) 

117 else: 

118 axis.units.update(data) 

119 return axis.units 

120 

121 @staticmethod 

122 def _validate_unit(unit): 

123 if not hasattr(unit, '_mapping'): 

124 raise ValueError( 

125 f'Provided unit "{unit}" is not valid for a categorical ' 

126 'converter, as it does not have a _mapping attribute.') 

127 

128 

129class StrCategoryLocator(ticker.Locator): 

130 """Tick at every integer mapping of the string data.""" 

131 def __init__(self, units_mapping): 

132 """ 

133 Parameters 

134 ---------- 

135 units_mapping : dict 

136 Mapping of category names (str) to indices (int). 

137 """ 

138 self._units = units_mapping 

139 

140 def __call__(self): 

141 # docstring inherited 

142 return list(self._units.values()) 

143 

144 def tick_values(self, vmin, vmax): 

145 # docstring inherited 

146 return self() 

147 

148 

149class StrCategoryFormatter(ticker.Formatter): 

150 """String representation of the data at every tick.""" 

151 def __init__(self, units_mapping): 

152 """ 

153 Parameters 

154 ---------- 

155 units_mapping : dict 

156 Mapping of category names (str) to indices (int). 

157 """ 

158 self._units = units_mapping 

159 

160 def __call__(self, x, pos=None): 

161 # docstring inherited 

162 return self.format_ticks([x])[0] 

163 

164 def format_ticks(self, values): 

165 # docstring inherited 

166 r_mapping = {v: self._text(k) for k, v in self._units.items()} 

167 return [r_mapping.get(round(val), '') for val in values] 

168 

169 @staticmethod 

170 def _text(value): 

171 """Convert text values into utf-8 or ascii strings.""" 

172 if isinstance(value, bytes): 

173 value = value.decode(encoding='utf-8') 

174 elif not isinstance(value, str): 

175 value = str(value) 

176 return value 

177 

178 

179class UnitData: 

180 def __init__(self, data=None): 

181 """ 

182 Create mapping between unique categorical values and integer ids. 

183 

184 Parameters 

185 ---------- 

186 data : iterable 

187 sequence of string values 

188 """ 

189 self._mapping = OrderedDict() 

190 self._counter = itertools.count() 

191 if data is not None: 

192 self.update(data) 

193 

194 @staticmethod 

195 def _str_is_convertible(val): 

196 """ 

197 Helper method to check whether a string can be parsed as float or date. 

198 """ 

199 try: 

200 float(val) 

201 except ValueError: 

202 try: 

203 dateutil.parser.parse(val) 

204 except (ValueError, TypeError): 

205 # TypeError if dateutil >= 2.8.1 else ValueError 

206 return False 

207 return True 

208 

209 def update(self, data): 

210 """ 

211 Map new values to integer identifiers. 

212 

213 Parameters 

214 ---------- 

215 data : iterable of str or bytes 

216 

217 Raises 

218 ------ 

219 TypeError 

220 If elements in *data* are neither str nor bytes. 

221 """ 

222 data = np.atleast_1d(np.array(data, dtype=object)) 

223 # check if convertible to number: 

224 convertible = True 

225 for val in OrderedDict.fromkeys(data): 

226 # OrderedDict just iterates over unique values in data. 

227 _api.check_isinstance((str, bytes), value=val) 

228 if convertible: 

229 # this will only be called so long as convertible is True. 

230 convertible = self._str_is_convertible(val) 

231 if val not in self._mapping: 

232 self._mapping[val] = next(self._counter) 

233 if data.size and convertible: 

234 _log.info('Using categorical units to plot a list of strings ' 

235 'that are all parsable as floats or dates. If these ' 

236 'strings should be plotted as numbers, cast to the ' 

237 'appropriate data type before plotting.') 

238 

239 

240# Register the converter with Matplotlib's unit framework 

241units.registry[str] = StrCategoryConverter() 

242units.registry[np.str_] = StrCategoryConverter() 

243units.registry[bytes] = StrCategoryConverter() 

244units.registry[np.bytes_] = StrCategoryConverter()