Skip to content

[MNT]: Revamp draw_path_collection() API #30547

@r3kste

Description

@r3kste

Summary

I am opening this issue in order to discuss the possibility of enhancing the API of draw_path_collection() for the long term. This was initially discussed in this and the following comments by @anntzer and @timhoffm.

Adding/ vectorizing new attributes in collections requires changing the signature of draw_path_collection(). This causes incompatibility with third party backends, such as mplcairo , because they still use the older signature.

To workaround this in #29044 (that added the hatchcolors argument) a provisional API was used, which relies on making an empty call to draw_path_collection() to infer the signature. This current API is temporary and requires a rewrite.

Proposed fix

A solution proposed by @anntzer in this comment is to use an API similar to draw_path() - send parameters through a dataclass and use getters to retrieve the required parameters, analogous to GraphicsContextBase in draw_path().
We could call this dataclass something like VectorizedGraphicsContextBase . It is similar to GraphicsContextBase, but each attribute is vectorized. A simple draft of the proposed solution is shown below.

Why is this change needed?

This would cause a one time break for third party backends. However, in the long term, it would become much simpler to add / vectorize new attributes in collections, without completely breaking third party backends. For example, it would greatly simplify #27937, that aims to vectorize the hatch attribute.

Code Draft

# backend_bases.py
class VectorizedGraphicsContextBase:
    def __init__(self):
        # vectorized attributes
        self._facecolor = [(0.0, 0.0, 0.0, 1.0)]
        self._rgb = [(0.0, 0.0, 0.0, 1.0)]
        self._linewidth = [1]
        ...

        # non-vectorized attributes
        self._cliprect = None
        self._clippath = None
        ...
# collections.py

# `vgc` is an instance of VectorizedGraphicsComtextBase
def draw(self, renderer):
    ...
    transform, offset_trf, offsets, paths = self._prepare_points()

    vgc = renderer.new_vgc()
    self._set_gc_clip(vgc)

    vgc._facecolor = self.get_facecolor()
    vgc._rgb = self.get_edgecolor()
    vgc._linewidth = self.get_linewidth()
    ...

    renderer.draw_path_collection(vgc, transform.frozen(), paths, self.get_transforms(),
                                  offsets, offset_trf)
# backend_bases.py
def draw_path_collection(self, vgc, master_transform, paths, all_transforms,
                         offsets, offset_trans):
    path_ids = self._iter_collection_raw_paths(
        master_transform, paths, all_transforms
    )

    for xo, yo, path_id, gc, rgbFace in self._iter_collection(
        vgc, list(path_ids), offsets, offset_trans
    ):
        path, transform = path_id

        if xo != 0 or yo != 0:
            transform = transform.frozen()
            transform.translate(xo, yo)
        self.draw_path(gc, path, transform, rgbFace)


def _iter_collection(self, vgc, path_ids, offsets, offset_trans):
    Npaths = len(path_ids)
    Noffsets = len(offsets)
    N = max(Npaths, Noffsets)
    Nfacecolors = len(vgc._facecolor)
    Nedgecolors = len(vgc._rgb)
    Nlinewidths = len(vgc._linewidth)

    pathids = cycle_or_default(path_ids)
    toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0))
    fcs = cycle_or_default(vgc._facecolor)
    ecs = cycle_or_default(vgc._rgb)
    lws = cycle_or_default(vgc._linewidth)

    gc = self.new_gc()
    # attributes that are not vectorized
    gc._clip_path = vgc._clip_path
    gc._clip_rect = vgc._clip_rect
    ...

    for pathid, (xo, yo), fc, ec, lw in itertools.islice(
        zip(pathids, toffsets, fcs, ecs, lws), N
    ):
        if not (np.isfinite(xo) and np.isfinite(yo)):
            continue

        if Nedgecolors:
            if Nlinewidths:
                gc.set_linewidth(lw)
            if len(ec) == 4 and ec[3] == 0.0:
                gc.set_linewidth(0)
            else:
                gc.set_foreground(ec)
        if fc is not None and len(fc) == 4 and fc[3] == 0:
            fc = None
        ...

        yield xo, yo, pathid, gc, fc

    gc.restore()

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions