Source code for moptipy.evaluation.styler

"""Styler allows to discover groups of data and associate styles with them."""
from typing import Any, Callable, Final, Iterable, cast

from matplotlib.artist import Artist  # type: ignore
from matplotlib.lines import Line2D  # type: ignore
from pycommons.types import type_error

from moptipy.utils.plot_defaults import create_line_style


[docs] class Styler: """A class for determining groups of elements and styling them.""" #: The tuple with the names becomes valid after compilation. names: tuple[str, ...] #: The tuple with the keys becomes valid after compilation. keys: tuple[Any, ...] #: The dictionary mapping keys to indices; only valid after compilation. __indexes: dict[Any, int] #: Is there a None key? Valid after compilation. has_none: bool #: The number of registered keys. count: int #: Does this styler have any style associated with it? has_style: bool def __init__(self, key_func: Callable = lambda x: x, namer: Callable[[Any], str] = str, none_name: str = "None", priority: int | float = 0, name_sort_function: Callable[[str], Any] | None = lambda s: s): """ Initialize the style grouper. :param key_func: the key function, obtaining keys from objects :param namer: the name function, turning keys into names :param none_name: the name for the none-key :param priority: the base priority of this grouper :param name_sort_function: the function for sorting names, or `None` if no name-based sorting shall be performed """ if not callable(key_func): raise type_error(key_func, "key function", call=True) if not callable(namer): raise type_error(namer, "namer function", call=True) if not isinstance(none_name, str): raise type_error(none_name, "none_name", str) if not isinstance(priority, float | int): raise type_error(priority, "priority", (int, float)) if (name_sort_function is not None) \ and (not callable(name_sort_function)): raise type_error(name_sort_function, "name_sort_function", type(None), call=True) def __namer(key, __namer: Callable[[Any], str] = namer, __none_name: str = none_name) -> str: rv = __none_name if key is None else __namer(key) if not isinstance(rv, str): raise type_error(rv, f"name for key {key!r}", str) rv = rv.strip() if len(rv) <= 0: raise ValueError( "name cannot be empty or just consist of white space") return rv #: the name sort function self.__name_sort_function: Final[Callable[[str], Any] | None] = \ name_sort_function #: The key function of the grouper self.key_func: Final[Callable] = key_func #: The name function of the grouper self.name_func: Final[Callable[[Any], str]] = \ cast(Callable[[Any], str], __namer) #: The base priority of this grouper self.priority: float = float(priority) #: The internal collection. self.__collection: set = set() #: the line colors self.__line_colors: tuple | None = None #: the line dashes self.__line_dashes: tuple | None = None #: the line widths self.__line_widths: tuple[float, ...] | None = None #: the optional line alpha self.__line_alphas: tuple[float, ...] | None = None
[docs] def add(self, obj) -> None: """ Add an object to the style collection. :param obj: the object """ self.__collection.add(self.key_func(obj))
[docs] def finalize(self) -> None: """Compile the styler collection.""" self.has_none = (None in self.__collection) if self.has_none: self.__collection.remove(None) nsf: Final[Callable[[str], Any] | None] = self.__name_sort_function if nsf is None: data = [(k, self.name_func(k)) for k in self.__collection] data.sort() if self.has_none: data.insert(0, (None, self.name_func(None))) self.keys = tuple([x[0] for x in data]) self.names = tuple([x[1] for x in data]) else: data = [(self.name_func(k), k) for k in self.__collection] data.sort(key=cast(Callable, lambda x, nsf2=nsf: nsf2(x[0]))) if self.has_none: data.insert(0, (self.name_func(None), None)) self.names = tuple([x[0] for x in data]) self.keys = tuple([x[1] for x in data]) del self.__collection del data self.__indexes = {k: i for i, k in enumerate(self.keys)} self.count = len(self.names) self.priority += self.count self.has_style = False
def __lt__(self, other) -> bool: """ Check whether this styler is more important than another one. :param other: the other styler :return: `True` if it is, `False` if it is not. """ if self.priority > other.priority: return True if self.priority < other.priority: return False c1 = self.count if self.has_none: c1 -= 1 c2 = other.count if other.has_none: c2 -= 1 if c1 > c2: return True return False
[docs] def set_line_color(self, line_color_func: Callable) -> None: """ Set that this styler should apply line colors. :param line_color_func: a function returning the palette """ tmp = line_color_func(self.count) if not isinstance(tmp, Iterable): raise type_error(tmp, "result of line color func", Iterable) self.__line_colors = tuple(tmp) if len(self.__line_colors) != self.count: raise ValueError(f"There must be {self.count} line colors," f"but found only {len(self.__line_colors)}.") self.has_style = True
[docs] def set_line_dash(self, line_dash_func: Callable) -> None: """ Set that this styler should apply line dashes. :param line_dash_func: a function returning the dashes """ tmp = line_dash_func(self.count) if not isinstance(tmp, Iterable): raise type_error(tmp, "result of line dash func", Iterable) self.__line_dashes = tuple(tmp) if len(self.__line_dashes) != self.count: raise ValueError(f"There must be {self.count} line dashes," f"but found only {len(self.__line_dashes)}.") self.has_style = True
[docs] def set_line_width(self, line_width_func: Callable) -> None: """ Set that this styler should apply a line width. :param line_width_func: the line width function """ tmp = line_width_func(self.count) if not isinstance(tmp, Iterable): raise type_error(tmp, "result of line width func", Iterable) self.__line_widths = tuple(tmp) if len(self.__line_widths) != self.count: raise ValueError(f"There must be {self.count} line widths," f"but found only {len(self.__line_widths)}.") self.has_style = True
[docs] def set_line_alpha(self, line_alpha_func: Callable) -> None: """ Set that this styler should apply a line alpha. :param line_alpha_func: the line alpha function """ tmp = line_alpha_func(self.count) if not isinstance(tmp, Iterable): raise type_error(tmp, "result of line alpha func", Iterable) self.__line_alphas = tuple(tmp) if len(self.__line_alphas) != self.count: raise ValueError(f"There must be {self.count} line alphas," f"but found only {len(self.__line_alphas)}.") self.has_style = True
[docs] def add_line_style(self, obj, style: dict[str, object]) -> None: """ Apply this styler's contents based on the given object. :param obj: the object for which the style should be created :param style: the decode to which the styles should be added """ key = self.key_func(obj) index = self.__indexes.setdefault(key, -1) if index >= 0: self.__add_line_style(index, style)
def __add_line_style(self, index, style: dict[str, object]) -> None: """ Apply this styler's contents based on the given object. :param index: the index to be processed :param style: the decode to which the styles should be added """ if self.__line_colors is not None: style["color"] = self.__line_colors[index] if self.__line_dashes is not None: style["linestyle"] = self.__line_dashes[index] if self.__line_widths is not None: style["linewidth"] = self.__line_widths[index] if self.__line_alphas is not None: style["alpha"] = self.__line_alphas[index]
[docs] def add_to_legend(self, consumer: Callable[[Artist], Any]) -> None: """ Add this styler to the legend. :param consumer: the consumer to add to """ if not callable(consumer): raise type_error(consumer, "consumer", call=True) for i, name in enumerate(self.names): style = create_line_style() self.__add_line_style(i, style) style["label"] = name style["xdata"] = [] style["ydata"] = [] consumer(Line2D(**style)) # type: ignore