Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 24, 2024

Conversation

scottshambaugh
Copy link
Contributor

@scottshambaugh scottshambaugh commented Jun 16, 2024

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

@scottshambaugh scottshambaugh added topic: mplot3d PR: bugfix Pull requests that fix identified bugs labels Jun 16, 2024
@scottshambaugh scottshambaugh added this to the v3.9.1 milestone Jun 16, 2024
@scottshambaugh scottshambaugh force-pushed the 3d_collection_autoscaling branch from 69ea30c to 195a208 Compare June 16, 2024 03:45
@scottshambaugh scottshambaugh force-pushed the 3d_collection_autoscaling branch from 49ee773 to 0c7b7db Compare June 17, 2024 00:11
@github-actions github-actions bot added the Documentation: examples files in galleries/examples label Jun 17, 2024
Comment on lines +2786 to +2795
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
Copy link
Member

@timhoffm timhoffm Jun 18, 2024

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 coordinates xyz from the artist and feed them into auto_scale_xyz().

This updates the xy_dataLim, zz_dataLim BBoxes via .Bbox.update_from_data_xy/x/y(), which in turn creates a Path 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 some Bbox.update_from_box, a kind of an in-place Bbox.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 and zz_dataLim.

The minimal approach - if we don't want to redesign for proper 3d boxes, would be to let get_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 of Collection.get_datalim.

def get_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()
if not (isinstance(offset_trf, transforms.IdentityTransform)
or offset_trf.contains_branch(transData)):
# if the offsets are in some coords other than data,
# then don't use them for autoscaling.
return transforms.Bbox.null()
paths = self.get_paths()
if not len(paths):
# No paths to transform
return transforms.Bbox.null()
if not transform.is_affine:
paths = [transform.transform_path_non_affine(p) for p in paths]
# 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()
if any(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).
if isinstance(offsets, np.ma.MaskedArray):
offsets = offsets.filled(np.nan)
# get_path_collection_extents handles nan but not masked arrays
return mpath.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
if self._offsets is not None:
# 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)
if not offsets.mask.all():
bbox = transforms.Bbox.null()
bbox.update_from_data_xy(offsets)
return bbox
return transforms.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())

Copy link
Contributor Author

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?

@greglucas greglucas merged commit 7ee56ce into matplotlib:main Jun 24, 2024
41 of 42 checks passed
Copy link

lumberbot-app bot commented Jun 24, 2024

Owee, I'm MrMeeseeks, Look at me.

There seem to be a conflict, please backport manually. Here are approximate instructions:

  1. Checkout backport branch and update it.
git checkout v3.9.x
git pull
  1. Cherry pick the first parent branch of the this PR on top of the older branch:
git cherry-pick -x -m1 7ee56ce31f3c2719ee774132ed36f5b1ca549afa
  1. You will likely have some merge/cherry-pick conflict here, fix them and commit:
git commit -am 'Backport PR #28403: FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection'
  1. Push to a named branch:
git push YOURFORK v3.9.x:auto-backport-of-pr-28403-on-v3.9.x
  1. Create a PR against branch v3.9.x, I would have named this PR:

"Backport PR #28403 on branch v3.9.x (FIX: Autoscale support in add_collection3d for Line3DCollection and Poly3DCollection)"

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 Still Needs Manual Backport label once the PR gets merged.

If these instructions are inaccurate, feel free to suggest an improvement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Documentation: examples files in galleries/examples PR: bugfix Pull requests that fix identified bugs topic: mplot3d
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: add_collection3d does not update view limits autoscale_view is not working with Line3DCollection
4 participants