Skip to content

colorization

pleat.colorization

Resolve per-element colours from color_key attributes and color_by classifiers.

A colour key is any hashable used by renderers to look up a colour. This module:

  • assigns hashable colour keys onto graph elements (:func:colorize), and
  • resolves those keys plus optional color_by classifiers into concrete RGB(A) colours using a matplotlib colormap (:func:resolve_colors).

is_color

is_color(obj: object) -> bool

Return True if obj looks like an RGB(A) tuple/list/array of numbers or a #RRGGBB string.

Source code in pleat/colorization.py
def is_color(obj: object) -> bool:
    """Return True if *obj* looks like an RGB(A) tuple/list/array of numbers or a ``#RRGGBB`` string."""
    if isinstance(obj, str):
        if obj.startswith("#") and len(obj) == 7:
            try:
                int(obj[1:], 16)
                return True
            except ValueError:
                return False
        return False
    if isinstance(obj, np.ndarray):
        return obj.ndim == 1 and obj.shape[0] in (3, 4) and np.issubdtype(obj.dtype, np.number)
    if not isinstance(obj, Iterable):
        return False
    seq = list(obj)
    return len(seq) in (3, 4) and all(isinstance(c, (int, float)) for c in seq)

colorize

colorize(
    graph, classifier: Classifier, key: str = "color_key"
) -> None

Assign face[key] = classifier.classify(face) for every face in graph.

Source code in pleat/colorization.py
def colorize(graph, classifier: Classifier, key: str = "color_key") -> None:
    """Assign ``face[key] = classifier.classify(face)`` for every face in *graph*."""
    for f in graph.faces:
        f[key] = classifier.classify(f)

congruency_colorize

congruency_colorize(graph, **kwargs) -> None

Colour faces by polygon congruence (same edge lengths and interior angles).

Source code in pleat/colorization.py
def congruency_colorize(graph, **kwargs) -> None:
    """Colour faces by polygon congruence (same edge lengths and interior angles)."""
    colorize(graph, congruency_classifier(), **kwargs)

resolve_colors

resolve_colors(
    elements,
    color_by,
    cmap,
    presets: dict[str, Callable[[], Classifier]],
    key: str = "color_key",
) -> dict[Any, np.ndarray]

Compute a {element → RGBA} map for the given elements.

Resolution rules (highest priority first):

  1. If element[key] is a colour literal (RGB(A) tuple / hex / array), use it as-is.
  2. If element[key] exists but is a non-colour hashable, treat it as a class index.
  3. Otherwise, if color_by is set, run its classifier on the element.
  4. Otherwise, leave the element unassigned (the renderer applies its own fallback).

Distinct class indices are then sorted by frequency (descending; iteration order breaks ties) and assigned palette slots in that order.

If color_by is set but every element already has a key attribute, a :class:UserWarning is emitted (the classifier had nothing to do).

Source code in pleat/colorization.py
def resolve_colors(
    elements,
    color_by,
    cmap,
    presets: dict[str, Callable[[], Classifier]],
    key: str = "color_key",
) -> dict[Any, np.ndarray]:
    """Compute a ``{element → RGBA}`` map for the given *elements*.

    Resolution rules (highest priority first):

    1. If ``element[key]`` is a colour literal (RGB(A) tuple / hex / array), use it as-is.
    2. If ``element[key]`` exists but is a non-colour hashable, treat it as a class index.
    3. Otherwise, if *color_by* is set, run its classifier on the element.
    4. Otherwise, leave the element unassigned (the renderer applies its own fallback).

    Distinct class indices are then sorted by frequency (descending; iteration
    order breaks ties) and assigned palette slots in that order.

    If *color_by* is set but every element already has a ``key`` attribute, a
    :class:`UserWarning` is emitted (the classifier had nothing to do).
    """
    elements = list(elements)
    classify = _make_classifier(color_by, presets)

    literal_colors: dict[Any, np.ndarray] = {}
    class_index: dict[Any, Any] = {}  # element → hashable class id
    class_counts: Counter = Counter()
    class_first_seen: dict[Any, int] = {}
    untouched_by_color_by = 0

    for el in elements:
        existing = el.attributes.get(key)
        if existing is not None and is_color(existing):
            literal_colors[el] = _to_rgba(existing)
            untouched_by_color_by += 1
            continue
        if existing is not None:
            idx = existing
            untouched_by_color_by += 1
        elif classify is not None:
            idx = classify(el)
        else:
            continue
        try:
            hash(idx)
        except TypeError as e:
            raise TypeError(f"colour class index {idx!r} is not hashable") from e
        class_index[el] = idx
        if idx not in class_first_seen:
            class_first_seen[idx] = len(class_first_seen)
        class_counts[idx] += 1

    if classify is not None and elements and untouched_by_color_by == len(elements):
        warnings.warn(
            "color_by was specified, but every element already has a color_key; classifier had no effect.",
            UserWarning,
            stacklevel=2,
        )

    if not class_counts:
        return literal_colors

    sorted_classes = sorted(class_counts, key=lambda i: (-class_counts[i], class_first_seen[i]))
    palette = _make_palette(len(sorted_classes), cmap)
    class_to_color = {idx: palette[rank] for rank, idx in enumerate(sorted_classes)}

    out = dict(literal_colors)
    for el, idx in class_index.items():
        out[el] = class_to_color[idx]
    return out