88
99Module containing Axes3D, an object which can plot 3D objects on a
10102D matplotlib figure.
11+
12+ Coordinate Systems
13+ ------------------
14+ 3D plotting involves several coordinate transformations:
15+
16+ 1. **Data coordinates**: The user's raw x, y, z values.
17+
18+ 2. **Transformed coordinates**: Data coordinates after applying axis scale
19+ transforms (log, symlog, etc.). For linear scales, these equal data
20+ coordinates. Zoom/pan operations work in this space to ensure uniform
21+ behavior with non-linear scales.
22+
23+ 3. **Normalized coordinates**: Transformed coordinates mapped to a [0, 1]
24+ unit cube based on the current axis limits.
25+
26+ 4. **Projected coordinates**: 2D coordinates after applying the 3D to 2D
27+ projection matrix, ready for display.
28+
29+ Artists receive data in data coordinates, apply scale transforms internally
30+ via ``do_3d_projection()``, then project to 2D for rendering.
1131"""
1232
1333from collections import defaultdict
@@ -239,13 +259,10 @@ def _transformed_cube(self, vals):
239259 axis scale transforms before being projected.
240260 """
241261 minx , maxx , miny , maxy , minz , maxz = vals
242- # Transform from data space to scaled space
243- x_trans = self .xaxis .get_transform ()
244- y_trans = self .yaxis .get_transform ()
245- z_trans = self .zaxis .get_transform ()
246- minx , maxx = x_trans .transform ([minx , maxx ])
247- miny , maxy = y_trans .transform ([miny , maxy ])
248- minz , maxz = z_trans .transform ([minz , maxz ])
262+ # Transform from data space to transformed coordinates
263+ minx , maxx = self .xaxis .get_transform ().transform ([minx , maxx ])
264+ miny , maxy = self .yaxis .get_transform ().transform ([miny , maxy ])
265+ minz , maxz = self .zaxis .get_transform ().transform ([minz , maxz ])
249266 xyzs = [(minx , miny , minz ),
250267 (maxx , miny , minz ),
251268 (maxx , maxy , minz ),
@@ -258,17 +275,14 @@ def _transformed_cube(self, vals):
258275
259276 def _update_transScale (self ):
260277 """
261- Override transScale to always use identity/linear transforms.
278+ Override transScale to always use identity transforms.
262279
263- In 3D axes, scale transforms (log, symlog, etc.) are applied during the
264- 3D projection in do_3d_projection() methods. The resulting 2D coordinates
265- are already in normalized display space and should NOT go through
266- additional log transforms. Using linear transforms here ensures that
267- negative projected coordinates (which are valid in 3D projection) are
268- not clipped by log transforms.
280+ In 2D axes, transScale applies scale transforms (log, symlog, etc.) to
281+ convert data coordinates to display coordinates. In 3D axes, scale
282+ transforms are applied to data coordinates before 3D projection via
283+ each axis's transform. The projected 2D coordinates are already in
284+ display space, so transScale must be identity to avoid double-scaling.
269285 """
270- # Always use identity transforms for 2D display, since 3D projection
271- # already handles scale transforms internally
272286 self .transScale .set (
273287 mtransforms .blended_transform_factory (
274288 mtransforms .IdentityTransform (),
@@ -1344,39 +1358,42 @@ def _get_scaled_limits(self):
13441358 zmin , zmax = self .zaxis .get_transform ().transform (self .get_zlim3d ())
13451359 return xmin , xmax , ymin , ymax , zmin , zmax
13461360
1347- def _inverse_scale_transform (self , x , y , z ):
1361+ def _untransform_point (self , x , y , z ):
13481362 """
1349- Apply inverse scale transforms to a point .
1363+ Convert a point from transformed coordinates to data coordinates .
13501364
1351- Converts from scaled space back to data space.
1365+ Parameters
1366+ ----------
1367+ x, y, z : float
1368+ A single point in transformed coordinates.
1369+
1370+ Returns
1371+ -------
1372+ x_data, y_data, z_data : float
1373+ The point in data coordinates.
13521374 """
13531375 x_data = self .xaxis .get_transform ().inverted ().transform ([x ])[0 ]
13541376 y_data = self .yaxis .get_transform ().inverted ().transform ([y ])[0 ]
13551377 z_data = self .zaxis .get_transform ().inverted ().transform ([z ])[0 ]
13561378 return x_data , y_data , z_data
13571379
1358- def _set_lims_from_scaled (self , xmin_s , xmax_s , ymin_s , ymax_s ,
1359- zmin_s , zmax_s ):
1380+ def _set_lims_from_transformed (self , xmin_t , xmax_t , ymin_t , ymax_t ,
1381+ zmin_t , zmax_t ):
13601382 """
1361- Transform scaled limits back to data space, validate, and set .
1383+ Set axis limits from transformed coordinates .
13621384
1363- Takes limits in scaled (transformed) space, converts back to data
1364- space, applies limit_range_for_scale validation, and sets the axis
1365- limits.
1385+ Converts limits from transformed coordinates back to data coordinates,
1386+ applies limit_range_for_scale validation, and sets the axis limits.
13661387
13671388 Parameters
13681389 ----------
1369- xmin_s, xmax_s, ymin_s, ymax_s, zmin_s, zmax_s : float
1370- Axis limits in scaled space .
1390+ xmin_t, xmax_t, ymin_t, ymax_t, zmin_t, zmax_t : float
1391+ Axis limits in transformed coordinates .
13711392 """
13721393 # Transform back to data space
1373- x_inv = self .xaxis .get_transform ().inverted ()
1374- y_inv = self .yaxis .get_transform ().inverted ()
1375- z_inv = self .zaxis .get_transform ().inverted ()
1376-
1377- xmin , xmax = x_inv .transform ([xmin_s , xmax_s ])
1378- ymin , ymax = y_inv .transform ([ymin_s , ymax_s ])
1379- zmin , zmax = z_inv .transform ([zmin_s , zmax_s ])
1394+ xmin , xmax = self .xaxis .get_transform ().inverted ().transform ([xmin_t , xmax_t ])
1395+ ymin , ymax = self .yaxis .get_transform ().inverted ().transform ([ymin_t , ymax_t ])
1396+ zmin , zmax = self .zaxis .get_transform ().inverted ().transform ([zmin_t , zmax_t ])
13801397
13811398 # Validate limits for scale constraints (e.g., positive for log scale)
13821399 xmin , xmax = self .xaxis ._scale .limit_range_for_scale (
@@ -1395,13 +1412,10 @@ def get_proj(self):
13951412 """Create the projection matrix from the current viewing position."""
13961413
13971414 # Transform to uniform world coordinates 0-1, 0-1, 0-1
1415+ box_aspect = self ._roll_to_vertical (self ._box_aspect )
13981416 # For non-linear scales, we use the scaled limits so the world
1399- # transformation maps scaled coordinates (not data coordinates)
1417+ # transformation maps transformed coordinates (not data coordinates)
14001418 # to the unit cube
1401- box_aspect = self ._roll_to_vertical (self ._box_aspect )
1402- # Use scaled limits for the world transformation. This ensures that
1403- # for non-linear scales (log, symlog, etc.), the world transformation
1404- # maps scaled coordinates to the unit cube.
14051419 scaled_limits = self ._get_scaled_limits ()
14061420 worldM = proj3d .world_transformation (
14071421 * scaled_limits ,
@@ -1663,19 +1677,19 @@ def _calc_coord(self, xv, yv, renderer=None):
16631677 else : # perspective projection
16641678 zv = - 1 / self ._focal_length
16651679
1666- # Convert point on view plane to scaled coordinates
1680+ # Convert point on view plane to transformed coordinates
16671681 # (inv_transform returns scaled coords because M was built with scaled limits)
16681682 p1 = np .array (proj3d .inv_transform (xv , yv , zv , self .invM )).ravel ()
16691683
16701684 # Get the vector from the camera to the point on the view plane
1671- # Camera location is in data space, so transform it to scaled space
1685+ # Camera location is in data space, so transform it to transformed coordinates
16721686 cam_data = self ._get_camera_loc ()
16731687 cam_scaled = np .array (art3d ._apply_scale_transforms (
16741688 cam_data [0 ], cam_data [1 ], cam_data [2 ], self )).ravel ()
16751689 vec = cam_scaled - p1
16761690
1677- # Get the pane locations for each of the axes ( in data space)
1678- # and transform to scaled space
1691+ # Get the pane locations for each of the axes in data space
1692+ # and transform to transformed coordinates
16791693 pane_locs_data = []
16801694 for axis in self ._axis_map .values ():
16811695 xys , loc = axis .active_pane ()
@@ -1693,11 +1707,11 @@ def _calc_coord(self, xv, yv, renderer=None):
16931707 pane_idx = np .argmin (abs (scales ))
16941708 scale = scales [pane_idx ]
16951709
1696- # Calculate the point on the closest pane ( in scaled space)
1710+ # Calculate the point on the closest pane in transformed coordinates
16971711 p2_scaled = p1 - scale * vec
16981712
16991713 # Convert back to data coordinates
1700- p2 = np .array (self ._inverse_scale_transform (
1714+ p2 = np .array (self ._untransform_point (
17011715 p2_scaled [0 ], p2_scaled [1 ], p2_scaled [2 ]))
17021716 return p2 , pane_idx
17031717
@@ -1858,14 +1872,14 @@ def drag_pan(self, button, key, x, y):
18581872 R = - R / self ._box_aspect * self ._dist
18591873 duvw_projected = R .T @ np .array ([du , dv , dw ])
18601874
1861- # Calculate pan distance in scaled space for proper non-linear scale handling
1875+ # Calculate pan distance in transformed coordinates for non-linear scale handling
18621876 minx , maxx , miny , maxy , minz , maxz = self ._get_scaled_limits ()
18631877 dx = (maxx - minx ) * duvw_projected [0 ]
18641878 dy = (maxy - miny ) * duvw_projected [1 ]
18651879 dz = (maxz - minz ) * duvw_projected [2 ]
18661880
1867- # Compute new limits in scaled space
1868- self ._set_lims_from_scaled (
1881+ # Compute new limits in transformed coordinates
1882+ self ._set_lims_from_transformed (
18691883 minx + dx , maxx + dx ,
18701884 miny + dy , maxy + dy ,
18711885 minz + dz , maxz + dz )
@@ -1984,7 +1998,7 @@ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
19841998 limits by scale factors. A scale factor > 1 zooms out and a scale
19851999 factor < 1 zooms in.
19862000
1987- For non-linear scales, the scaling happens in scaled space to ensure
2001+ For non-linear scales, the scaling happens in transformed coordinates to ensure
19882002 uniform zoom behavior.
19892003
19902004 Parameters
@@ -1996,29 +2010,29 @@ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
19962010 scale_z : float
19972011 Scale factor for the z data axis.
19982012 """
1999- # Get the axis centers and ranges ( in scaled space for non-linear scales)
2013+ # Get the axis centers and ranges in transformed coordinates
20002014 cx , cy , cz , dx , dy , dz = self ._get_w_centers_ranges ()
20012015
2002- # Compute new limits in scaled space and set
2003- self ._set_lims_from_scaled (
2016+ # Compute new limits in transformed coordinates and set
2017+ self ._set_lims_from_transformed (
20042018 cx - dx * scale_x / 2 , cx + dx * scale_x / 2 ,
20052019 cy - dy * scale_y / 2 , cy + dy * scale_y / 2 ,
20062020 cz - dz * scale_z / 2 , cz + dz * scale_z / 2 )
20072021
20082022 def _get_w_centers_ranges (self ):
20092023 """
2010- Get 3D world centers and axis ranges in scaled space .
2024+ Get 3D world centers and axis ranges in transformed coordinates .
20112025
20122026 For non-linear scales (log, symlog, etc.), centers and ranges are
2013- computed in scaled coordinates to ensure uniform zoom/pan behavior.
2027+ computed in transformed coordinates to ensure uniform zoom/pan behavior.
20142028 """
2015- # Get limits in scaled space for proper zoom/pan with non-linear scales
2029+ # Get limits in transformed coordinates for proper zoom/pan with non-linear scales
20162030 minx , maxx , miny , maxy , minz , maxz = self ._get_scaled_limits ()
20172031 cx = (maxx + minx )/ 2
20182032 cy = (maxy + miny )/ 2
20192033 cz = (maxz + minz )/ 2
20202034
2021- # Calculate range of axis limits in scaled space
2035+ # Calculate range of axis limits in transformed coordinates
20222036 dx = (maxx - minx )
20232037 dy = (maxy - miny )
20242038 dz = (maxz - minz )
0 commit comments