-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Refactor for issue 28444 [Introduce _Bbox3D to represent 3D axes limits] #30115
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
base: main
Are you sure you want to change the base?
Changes from all commits
56ec318
b6e8b43
8d39945
109aadc
7b8c5e1
224f9c0
8f9e5d6
8dc8584
4e132d6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
Collection, LineCollection, PolyCollection, PatchCollection, PathCollection) | ||
from matplotlib.patches import Patch | ||
from . import proj3d | ||
from .bbox3d import _Bbox3d | ||
|
||
|
||
def _norm_angle(a): | ||
|
@@ -97,6 +98,16 @@ | |
return mask | ||
|
||
|
||
def create_bbox3d_from_array(arr): | ||
arr = np.asarray(arr) | ||
if arr.ndim != 2 or arr.shape[1] != 3: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should also handle the case of a shape (0, 3) array |
||
raise ValueError("Expected array of shape (N, 3)") | ||
xmin, xmax = np.min(arr[:, 0]), np.max(arr[:, 0]) | ||
ymin, ymax = np.min(arr[:, 1]), np.max(arr[:, 1]) | ||
zmin, zmax = np.min(arr[:, 2]), np.max(arr[:, 2]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be slightly faster to do these in one pass: mins = np.min(arr, axis=0)
maxs = np.max(arr, axis=0)
xmin, ymin, zmin = mins
xmax, ymax, zmax = maxs There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May also need to handle nan values? In which cases |
||
return _Bbox3d(((xmin, xmax), (ymin, ymax), (zmin, zmax))) | ||
|
||
|
||
class Text3D(mtext.Text): | ||
""" | ||
Text object with 3D position and direction. | ||
|
@@ -331,6 +342,10 @@ | |
super().draw(renderer) | ||
self.stale = False | ||
|
||
def _get_datalim3d(self): | ||
xs, ys, zs = self._verts3d | ||
return create_bbox3d_from_array(np.column_stack((xs, ys, zs))) | ||
|
||
|
||
def line_2d_to_3d(line, zs=0, zdir='z', axlim_clip=False): | ||
""" | ||
|
@@ -513,6 +528,10 @@ | |
minz = np.nan | ||
return minz | ||
|
||
def _get_datalim3d(self): | ||
segments = np.concatenate(self._segments3d) | ||
return create_bbox3d_from_array(segments) | ||
|
||
|
||
def line_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): | ||
"""Convert a `.LineCollection` to a `.Line3DCollection` object.""" | ||
|
@@ -591,6 +610,9 @@ | |
self._path2d = mpath.Path(np.ma.column_stack([vxs, vys])) | ||
return min(vzs) | ||
|
||
def _get_datalim3d(self): | ||
return create_bbox3d_from_array(self._segment3d) | ||
|
||
|
||
class PathPatch3D(Patch3D): | ||
""" | ||
|
@@ -653,6 +675,9 @@ | |
self._path2d = mpath.Path(np.ma.column_stack([vxs, vys]), self._code3d) | ||
return min(vzs) | ||
|
||
def _get_datalim3d(self): | ||
return create_bbox3d_from_array(self._segment3d) | ||
|
||
|
||
def _get_patch_verts(patch): | ||
"""Return a list of vertices for the path of a patch.""" | ||
|
@@ -832,6 +857,10 @@ | |
return self.get_facecolor() | ||
return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) | ||
|
||
def _get_datalim3d(self): | ||
xs, ys, zs = self._offsets3d | ||
return create_bbox3d_from_array(np.column_stack((xs, ys, zs))) | ||
|
||
|
||
def _get_data_scale(X, Y, Z): | ||
""" | ||
|
@@ -1087,6 +1116,10 @@ | |
return self.get_facecolor() | ||
return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) | ||
|
||
def _get_datalim3d(self): | ||
xs, ys, zs = self._offsets3d | ||
return create_bbox3d_from_array(np.column_stack((xs, ys, zs))) | ||
|
||
|
||
def patch_collection_2d_to_3d( | ||
col, | ||
|
@@ -1464,6 +1497,9 @@ | |
self.do_3d_projection() | ||
return np.asarray(self._edgecolors2d) | ||
|
||
def _get_datalim3d(self): | ||
return create_bbox3d_from_array(self._faces.reshape(-1, 3)) | ||
|
||
|
||
def poly_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): | ||
""" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -608,6 +608,25 @@ def auto_scale_xyz(self, X, Y, Z=None, had_data=None): | |
# Let autoscale_view figure out how to use this data. | ||
self.autoscale_view() | ||
|
||
def auto_scale_lim(self, bbox3d, had_data=False): | ||
""" | ||
Expand the 3D axes data limits to include the given Bbox3d. | ||
|
||
Parameters | ||
---------- | ||
bbox3d : _Bbox3d | ||
The 3D bounding box to incorporate into the data limits. | ||
had_data : bool, default: False | ||
Whether the axes already had data limits set before. | ||
""" | ||
self.xy_dataLim.update_from_bbox(bbox3d.to_bbox_xy(), ignore=not had_data) | ||
self.zz_dataLim.update_from_bbox(bbox3d.to_bbox_zz(), ignore=not had_data) | ||
if not had_data: | ||
self._xy_dataLim_set = True | ||
self._zz_dataLim_set = True | ||
self.autoscale_view() | ||
|
||
|
||
def autoscale_view(self, tight=None, | ||
scalex=True, scaley=True, scalez=True): | ||
""" | ||
|
@@ -2887,19 +2906,9 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, | |
axlim_clip=axlim_clip) | ||
col.set_sort_zpos(zsortval) | ||
|
||
if autolim: | ||
if isinstance(col, art3d.Line3DCollection): | ||
self.auto_scale_xyz(*np.array(col._segments3d).transpose(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does anything use auto_scale_xyz internally after this? Should still keep it since it's a public method, but should make sure everything is switched over There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see in your initial comment that this is meant to lay the groundwork for switching everything over in a follow-on PR - I think that incrementalism is fine but also feel free to put everything into one if you prefer. |
||
had_data=had_data) | ||
elif isinstance(col, art3d.Poly3DCollection): | ||
self.auto_scale_xyz(col._faces[..., 0], | ||
col._faces[..., 1], | ||
col._faces[..., 2], 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 | ||
if autolim and hasattr(col, "_get_datalim3d"): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Are there any which do not? Should raise a warning in an elif case |
||
bbox3d = col._get_datalim3d() | ||
self.auto_scale_lim(bbox3d, had_data=had_data) | ||
|
||
collection = super().add_collection(col) | ||
return collection | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from matplotlib.transforms import Bbox | ||
|
||
|
||
class _Bbox3d: | ||
""" | ||
A helper class to represent a 3D bounding box. | ||
|
||
This class stores the minimum and maximum extents of data in 3D space | ||
(xmin, xmax, ymin, ymax, zmin, zmax). It provides methods to convert | ||
these extents into 2D bounding boxes (`Bbox`) for compatibility with | ||
existing matplotlib functionality. | ||
|
||
Attributes | ||
---------- | ||
xmin, xmax : float | ||
The minimum and maximum extents along the x-axis. | ||
ymin, ymax : float | ||
The minimum and maximum extents along the y-axis. | ||
zmin, zmax : float | ||
The minimum and maximum extents along the z-axis. | ||
|
||
Methods | ||
------- | ||
to_bbox_xy(): | ||
Converts the x and y extents into a 2D `Bbox`. | ||
to_bbox_zz(): | ||
Converts the z extents into a 2D `Bbox`, with the y-component unused. | ||
""" | ||
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.ymin), (self.xmax, self.ymax))) | ||
|
||
def to_bbox_zz(self): | ||
# first component contains z, second is unused | ||
return Bbox(((self.zmin, 0), (self.zmax, 0))) |
Uh oh!
There was an error while loading. Please reload this page.
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.
Could you add a little more clarity to the docstring here? My read is that this will strictly increase the size of the initial Bbox, or keep it the same if it already bounds the input bbox. Unless the ignore flag is True, in which case it will set the values to the input bbox.