-
-
Notifications
You must be signed in to change notification settings - Fork 8k
Description
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()