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
« 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'.
5See :doc:`/gallery/lines_bars_and_markers/categorical_variables` for an
6example.
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"""
13from collections import OrderedDict
14import dateutil.parser
15import itertools
16import logging
18import numpy as np
20from matplotlib import _api, ticker, units
23_log = logging.getLogger(__name__)
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.
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.
42 .. note:: *axis* is unused.
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)
71 @staticmethod
72 def axisinfo(unit, axis):
73 """
74 Set the default axis ticks and labels.
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
83 .. note:: *axis* is not used
85 Returns
86 -------
87 `~matplotlib.units.AxisInfo`
88 Information to support default tick labeling
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)
98 @staticmethod
99 def default_units(data, axis):
100 """
101 Set and update the `~matplotlib.axis.Axis` units.
103 Parameters
104 ----------
105 data : str or iterable of str
106 axis : `~matplotlib.axis.Axis`
107 axis on which the data is plotted
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
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.')
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
140 def __call__(self):
141 # docstring inherited
142 return list(self._units.values())
144 def tick_values(self, vmin, vmax):
145 # docstring inherited
146 return self()
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
160 def __call__(self, x, pos=None):
161 # docstring inherited
162 return self.format_ticks([x])[0]
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]
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
179class UnitData:
180 def __init__(self, data=None):
181 """
182 Create mapping between unique categorical values and integer ids.
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)
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
209 def update(self, data):
210 """
211 Map new values to integer identifiers.
213 Parameters
214 ----------
215 data : iterable of str or bytes
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.')
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()