Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.9k
FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection#28403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection#28403
Uh oh!
There was an error while loading.Please reload this page.
Conversation
69ea30c
to195a208
CompareUh oh!
There was an error while loading.Please reload this page.
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
49ee773
to0c7b7db
CompareUh oh!
There was an error while loading.Please reload this page.
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
if isinstance(col, art3d.Line3DCollection): | ||
self.auto_scale_xyz(*np.array(col._segments3d).transpose(), | ||
had_data=had_data) | ||
elif isinstance(col, art3d.Poly3DCollection): | ||
self.auto_scale_xyz(*col._vec[:-1], had_data=had_data) | ||
elif isinstance(col, art3d.Patch3DCollection): | ||
pass | ||
# FIXME: Implement auto-scaling function for Patch3DCollection | ||
# Currently unable to do so due to issues with Patch3DCollection | ||
# See https://github.com/matplotlib/matplotlib/issues/14298 for details |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
While technically correct, I believe we should move the calculation of data lims down into the artists.
Current logic
Extract all data coordinatesxyz from the artist and feed them into
auto_scale_xyz()
.
This updates thexy_dataLim
,zz_dataLim
BBoxes via.Bbox.update_from_data_xy/x/y()
, which in turn creates aPath
and calls.Bbox.update_from_path()
.
This is quite a lot data pushing (and maybe copying) just to update dataLim bboxes.
Proposed logic
Separation of concerns: The Artists should have a function to get their data lims. These limits should be used to update the Bbox. Basically someBbox.update_from_box
, a kind of an in-placeBbox.union
, which we don't seem to have.
This whole block should read something like
if autolim: self.auto_scale_lim(col.get_data_lims())
Note: "data lim" is intentionally vague as we currently don't have a good structure to describe it (we use two 2D Bboxes,xy_dataLim
andzz_dataLim
.
The minimal approach - if we don't want to redesign for proper 3d boxes, would be to letget_data_lims()
return two BBoxes as well. In that case, I would keep this interface private_get_data_lims()
so that we can still change to a single 3D instance later.
Edit: This is basically a generalization ofCollection.get_datalim
.
matplotlib/lib/matplotlib/collections.py
Lines 239 to 306 infa16860
defget_datalim(self,transData): | |
# Calculate the data limits and return them as a `.Bbox`. | |
# | |
# This operation depends on the transforms for the data in the | |
# collection and whether the collection has offsets: | |
# | |
# 1. offsets = None, transform child of transData: use the paths for | |
# the automatic limits (i.e. for LineCollection in streamline). | |
# 2. offsets != None: offset_transform is child of transData: | |
# | |
# a. transform is child of transData: use the path + offset for | |
# limits (i.e for bar). | |
# b. transform is not a child of transData: just use the offsets | |
# for the limits (i.e. for scatter) | |
# | |
# 3. otherwise return a null Bbox. | |
transform=self.get_transform() | |
offset_trf=self.get_offset_transform() | |
ifnot (isinstance(offset_trf,transforms.IdentityTransform) | |
oroffset_trf.contains_branch(transData)): | |
# if the offsets are in some coords other than data, | |
# then don't use them for autoscaling. | |
returntransforms.Bbox.null() | |
paths=self.get_paths() | |
ifnotlen(paths): | |
# No paths to transform | |
returntransforms.Bbox.null() | |
ifnottransform.is_affine: | |
paths= [transform.transform_path_non_affine(p)forpinpaths] | |
# Don't convert transform to transform.get_affine() here because | |
# we may have transform.contains_branch(transData) but not | |
# transforms.get_affine().contains_branch(transData). But later, | |
# be careful to only apply the affine part that remains. | |
offsets=self.get_offsets() | |
ifany(transform.contains_branch_seperately(transData)): | |
# collections that are just in data units (like quiver) | |
# can properly have the axes limits set by their shape + | |
# offset. LineCollections that have no offsets can | |
# also use this algorithm (like streamplot). | |
ifisinstance(offsets,np.ma.MaskedArray): | |
offsets=offsets.filled(np.nan) | |
# get_path_collection_extents handles nan but not masked arrays | |
returnmpath.get_path_collection_extents( | |
transform.get_affine()-transData,paths, | |
self.get_transforms(), | |
offset_trf.transform_non_affine(offsets), | |
offset_trf.get_affine().frozen()) | |
# NOTE: None is the default case where no offsets were passed in | |
ifself._offsetsisnotNone: | |
# this is for collections that have their paths (shapes) | |
# in physical, axes-relative, or figure-relative units | |
# (i.e. like scatter). We can't uniquely set limits based on | |
# those shapes, so we just set the limits based on their | |
# location. | |
offsets= (offset_trf-transData).transform(offsets) | |
# note A-B means A B^{-1} | |
offsets=np.ma.masked_invalid(offsets) | |
ifnotoffsets.mask.all(): | |
bbox=transforms.Bbox.null() | |
bbox.update_from_data_xy(offsets) | |
returnbbox | |
returntransforms.Bbox.null() |
Rough outline: We may start minimal with rolling a small
class _Bbox3d: def __init__(self, points): ((self.xmin, self.xmax), (self.ymin, self.ymax), (self.zmin, self.zmax)) = points def to_bbox_xy(self): return Bbox(((self.xmin, self.xmax), (self.ymin, self.ymax))) def to_bbox_zz(self): # first component contains z, second is unused return Bbox(((self.zmin, self.zmax), (0, 0)))
and implement_get_datalim3d() -> _Bbox3d
on the 3d collections.
Then
def add_collection(col, autolim=True): ... if autolim: self.auto_scale_lim(col.get_datalim3d())
and
def auto_scale_lim(bbox3d): ... self.dataLim_xy.update_from_box(bbox3d.to_bbox_xy()) self.dataLim_zz.update_from_box(bbox3d.to_bbox_zz())
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others.Learn more.
I like this structure, but could we make that refactor its own issue?
7ee56ce
intomatplotlib:mainUh oh!
There was an error while loading.Please reload this page.
Owee, I'm MrMeeseeks, Look at me. There seem to be a conflict, please backport manually. Here are approximate instructions:
And apply the correct labels and milestones. Congratulations — you did some good work! Hopefully your backport PR will be tested by the continuous integration and merged soon! Remember to remove the If these instructions are inaccurate, feel free tosuggest an improvement. |
…n3d for Line3DCollection and Poly3DCollection
…03-on-v3.9.xBackport PR#28403 on branch v3.9.x (FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection
Uh oh!
There was an error while loading.Please reload this page.
PR summary
Closes#23317
Closes#17130
Code copied in large part from@lganic's work in#25751 (thank you!), which he closed.
I would consider this a bugfix for a missing kwarg, but if we want to consider this an enhancement for 3.10.0 please feel free to shift the milestone.
PR checklist