123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- """
- The classes here provide support for using custom classes with
- Matplotlib, e.g., those that do not expose the array interface but know
- how to convert themselves to arrays. It also supports classes with
- units and units conversion. Use cases include converters for custom
- objects, e.g., a list of datetime objects, as well as for objects that
- are unit aware. We don't assume any particular units implementation;
- rather a units implementation must register with the Registry converter
- dictionary and provide a `ConversionInterface`. For example,
- here is a complete implementation which supports plotting with native
- datetime objects::
- import matplotlib.units as units
- import matplotlib.dates as dates
- import matplotlib.ticker as ticker
- import datetime
- class DateConverter(units.ConversionInterface):
- @staticmethod
- def convert(value, unit, axis):
- "Convert a datetime value to a scalar or array."
- return dates.date2num(value)
- @staticmethod
- def axisinfo(unit, axis):
- "Return major and minor tick locators and formatters."
- if unit != 'date':
- return None
- majloc = dates.AutoDateLocator()
- majfmt = dates.AutoDateFormatter(majloc)
- return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='date')
- @staticmethod
- def default_units(x, axis):
- "Return the default unit for x or None."
- return 'date'
- # Finally we register our object type with the Matplotlib units registry.
- units.registry[datetime.date] = DateConverter()
- """
- from decimal import Decimal
- from numbers import Number
- import numpy as np
- from numpy import ma
- from matplotlib import cbook
- class ConversionError(TypeError):
- pass
- def _is_natively_supported(x):
- """
- Return whether *x* is of a type that Matplotlib natively supports or an
- array of objects of such types.
- """
- # Matplotlib natively supports all number types except Decimal.
- if np.iterable(x):
- # Assume lists are homogeneous as other functions in unit system.
- for thisx in x:
- if thisx is ma.masked:
- continue
- return isinstance(thisx, Number) and not isinstance(thisx, Decimal)
- else:
- return isinstance(x, Number) and not isinstance(x, Decimal)
- class AxisInfo:
- """
- Information to support default axis labeling, tick labeling, and limits.
- An instance of this class must be returned by
- `ConversionInterface.axisinfo`.
- """
- def __init__(self, majloc=None, minloc=None,
- majfmt=None, minfmt=None, label=None,
- default_limits=None):
- """
- Parameters
- ----------
- majloc, minloc : Locator, optional
- Tick locators for the major and minor ticks.
- majfmt, minfmt : Formatter, optional
- Tick formatters for the major and minor ticks.
- label : str, optional
- The default axis label.
- default_limits : optional
- The default min and max limits of the axis if no data has
- been plotted.
- Notes
- -----
- If any of the above are ``None``, the axis will simply use the
- default value.
- """
- self.majloc = majloc
- self.minloc = minloc
- self.majfmt = majfmt
- self.minfmt = minfmt
- self.label = label
- self.default_limits = default_limits
- class ConversionInterface:
- """
- The minimal interface for a converter to take custom data types (or
- sequences) and convert them to values Matplotlib can use.
- """
- @staticmethod
- def axisinfo(unit, axis):
- """Return an `.AxisInfo` for the axis with the specified units."""
- return None
- @staticmethod
- def default_units(x, axis):
- """Return the default unit for *x* or ``None`` for the given axis."""
- return None
- @staticmethod
- def convert(obj, unit, axis):
- """
- Convert *obj* using *unit* for the specified *axis*.
- If *obj* is a sequence, return the converted sequence. The output must
- be a sequence of scalars that can be used by the numpy array layer.
- """
- return obj
- class DecimalConverter(ConversionInterface):
- """Converter for decimal.Decimal data to float."""
- @staticmethod
- def convert(value, unit, axis):
- """
- Convert Decimals to floats.
- The *unit* and *axis* arguments are not used.
- Parameters
- ----------
- value : decimal.Decimal or iterable
- Decimal or list of Decimal need to be converted
- """
- if isinstance(value, Decimal):
- return float(value)
- # value is Iterable[Decimal]
- elif isinstance(value, ma.MaskedArray):
- return ma.asarray(value, dtype=float)
- else:
- return np.asarray(value, dtype=float)
- # axisinfo and default_units can be inherited as Decimals are Numbers.
- class Registry(dict):
- """Register types with conversion interface."""
- def get_converter(self, x):
- """Get the converter interface instance for *x*, or None."""
- # Unpack in case of e.g. Pandas or xarray object
- x = cbook._unpack_to_numpy(x)
- if isinstance(x, np.ndarray):
- # In case x in a masked array, access the underlying data (only its
- # type matters). If x is a regular ndarray, getdata() just returns
- # the array itself.
- x = np.ma.getdata(x).ravel()
- # If there are no elements in x, infer the units from its dtype
- if not x.size:
- return self.get_converter(np.array([0], dtype=x.dtype))
- for cls in type(x).__mro__: # Look up in the cache.
- try:
- return self[cls]
- except KeyError:
- pass
- try: # If cache lookup fails, look up based on first element...
- first = cbook._safe_first_finite(x)
- except (TypeError, StopIteration):
- pass
- else:
- # ... and avoid infinite recursion for pathological iterables for
- # which indexing returns instances of the same iterable class.
- if type(first) is not type(x):
- return self.get_converter(first)
- return None
- registry = Registry()
- registry[Decimal] = DecimalConverter()
|