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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ def draw(self, renderer):
locator = self.get_axes_locator()
self.apply_aspect(locator(self, renderer) if locator else None)

# add the projection matrix to the renderer
# add the projection matrix to the axes
self.M = self.get_proj()
self.invM = np.linalg.inv(self.M)

Expand Down
179 changes: 109 additions & 70 deletions lib/mpl_toolkits/mplot3d/axis3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
# Created: 23 Sep 2005
# Parts rewritten by Reinier Heeres <[email protected]>

from dataclasses import dataclass
import inspect

import numpy as np

import matplotlib as mpl
from matplotlib import (
_api, artist, lines as mlines, axis as maxis, patches as mpatches,
transforms as mtransforms, colors as mcolors)
text as mtext, transforms as mtransforms, colors as mcolors)
from . import art3d, proj3d


Expand All @@ -35,6 +36,32 @@ def _tick_update_position(tick, tickxs, tickys, labelpos):
tick.gridline.set_data([0], [0])


@dataclass
class _UpdatedArtists:
"""
Class to supply artists that have been updated ready to draw.

We track the types of artists individually, because they need
different handling in the tightbox calculation.
"""
line: mlines.Line2D = None
ticks: list = None
offset_text: mtext.Text = None
label: mtext.Text = None

def __iter__(self):
"""Yield all updated artists in the correct order for drawing"""
if self.line is not None:
yield self.line
if self.ticks is not None:
for tick in self.ticks:
yield tick
if self.offset_text is not None:
yield self.offset_text
if self.label is not None:
yield self.label


class Axis(maxis.XAxis):
"""An Axis class for the 3D plots."""
# These points from the unit cube make up the x, y and z-planes
Expand Down Expand Up @@ -439,8 +466,9 @@ def _axmask(self):
axmask[self._axinfo["i"]] = False
return axmask

def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,
deltas_per_point, pos):
def _get_updated_ticks(self, edgep1, centers, deltas, highs,
deltas_per_point, pos):
"""Update the ticks ready for draw and return in a list."""
ticks = self._update_ticks()
info = self._axinfo
index = info["i"]
Expand All @@ -453,7 +481,7 @@ def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,
axis = [self.axes.xaxis, self.axes.yaxis, self.axes.zaxis][index]
axis_trans = axis.get_transform()

# Draw ticks:
# Update ticks:
tickdir = self._get_tickdir(pos)
tickdelta = deltas[tickdir] if highs[tickdir] else -deltas[tickdir]

Expand Down Expand Up @@ -485,10 +513,12 @@ def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,

_tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly))
tick.tick1line.set_linewidth(tick_lw[tick._major])
tick.draw(renderer)

def _draw_offset_text(self, renderer, edgep1, edgep2, labeldeltas, centers,
highs, pep, dx, dy):
return ticks

def _update_offset_text(self, edgep1, edgep2, labeldeltas, centers,
highs, pep, dx, dy):
"""Update the offset text ready to draw."""
# Get general axis information:
info = self._axinfo
index = info["i"]
Expand Down Expand Up @@ -556,12 +586,11 @@ def _draw_offset_text(self, renderer, edgep1, edgep2, labeldeltas, centers,

self.offsetText.set_va('center')
self.offsetText.set_ha(align)
self.offsetText.draw(renderer)

def _draw_labels(self, renderer, edgep1, edgep2, labeldeltas, centers, dx, dy):
def _update_label_position(self, edgep1, edgep2, labeldeltas, centers, dx, dy):
"""Update the axis label position ready to draw."""
label = self._axinfo["label"]

# Draw labels
lxyz = 0.5 * (edgep1 + edgep2)
lxyz = _move_from_center(lxyz, centers, labeldeltas, self._axmask())
tlx, tly, tlz = proj3d.proj_transform(*lxyz, self.axes.M)
Expand All @@ -572,13 +601,26 @@ def _draw_labels(self, renderer, edgep1, edgep2, labeldeltas, centers, dx, dy):
self.label.set_va(label['va'])
self.label.set_ha(label['ha'])
self.label.set_rotation_mode(label['rotation_mode'])
self.label.draw(renderer)

@artist.allow_rasterization
def draw(self, renderer):
def _update_all_positions(self):
"""
Update positions of all child artists ready to draw. Artists may be drawn
twice: if tick_position is 'both', the ticks, line and offset text show on both
sides of the axes; if label_position is 'both' the label shows on both sides.
Since tightbox calculation needs individual handling of the different Artist
types, we return them through the dataclass `_UpdatedArtists` (as opposed to a
plain list).
Since we reuse the same artists with different positions, the calling function
requires them after every position update, so we yield `_UpdatedArtists` up to
four times with different content:

- line, ticks, offset_text on one side
- line, ticks, offset_text on the other side
- label on one side
- label on the other side
"""
self.label._transform = self.axes.transData
self.offsetText._transform = self.axes.transData
renderer.open_group("axis3d", gid=self.get_gid())

# Get general axis information:
mins, maxs, tc, highs = self._get_coord_info()
Expand Down Expand Up @@ -613,28 +655,42 @@ def draw(self, renderer):
dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
self.axes.transAxes.transform([pep[0:2, 0]]))[0]

# Draw the lines
# Handle the lines
self.line.set_data(pep[0], pep[1])
self.line.draw(renderer)

# Draw ticks
self._draw_ticks(renderer, edgep1, centers, deltas, highs,
deltas_per_point, pos)
# Handle ticks
ticks = self._get_updated_ticks(
edgep1, centers, deltas, highs, deltas_per_point, pos)

# Draw Offset text
self._draw_offset_text(renderer, edgep1, edgep2, labeldeltas,
centers, highs, pep, dx, dy)
# Handle Offset text
self._update_offset_text(
edgep1, edgep2, labeldeltas, centers, highs, pep, dx, dy)

for edgep1, edgep2, pos in zip(*self._get_all_axis_line_edge_points(
minmax, maxmin, self._label_position)):
# See comments above
pep = proj3d._proj_trans_points([edgep1, edgep2], self.axes.M)
pep = np.asarray(pep)
dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
self.axes.transAxes.transform([pep[0:2, 0]]))[0]
yield _UpdatedArtists(line=self.line, ticks=ticks,
offset_text=self.offsetText)

if self.label.get_visible() and self.label.get_text():
# Handle label
for edgep1, edgep2, pos in zip(*self._get_all_axis_line_edge_points(
minmax, maxmin, self._label_position)):
# See comments above
pep = proj3d._proj_trans_points([edgep1, edgep2], self.axes.M)
pep = np.asarray(pep)
dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
self.axes.transAxes.transform([pep[0:2, 0]]))[0]

self._update_label_position(
edgep1, edgep2, labeldeltas, centers, dx, dy)

yield _UpdatedArtists(label=self.label)

@artist.allow_rasterization
def draw(self, renderer):
renderer.open_group('axis3d', gid=self.get_gid())

# Draw labels
self._draw_labels(renderer, edgep1, edgep2, labeldeltas, centers, dx, dy)
for updated_artists in self._update_all_positions():
for art in updated_artists:
art.draw(renderer)

renderer.close_group('axis3d')
self.stale = False
Expand Down Expand Up @@ -689,49 +745,32 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False):
# docstring inherited
if not self.get_visible():
return
# We have to directly access the internal data structures
# (and hope they are up to date) because at draw time we
# shift the ticks and their labels around in (x, y) space
# based on the projection, the current view port, and their
# position in 3D space. If we extend the transforms framework
# into 3D we would not need to do this different book keeping
# than we do in the normal axis
major_locs = self.get_majorticklocs()
minor_locs = self.get_minorticklocs()

ticks = [*self.get_minor_ticks(len(minor_locs)),
*self.get_major_ticks(len(major_locs))]
view_low, view_high = self.get_view_interval()
if view_low > view_high:
view_low, view_high = view_high, view_low
interval_t = self.get_transform().transform([view_low, view_high])

ticks_to_draw = []
for tick in ticks:
try:
loc_t = self.get_transform().transform(tick.get_loc())
except AssertionError:
# Transform.transform doesn't allow masked values but
# some scales might make them, so we need this try/except.
pass
else:
if mtransforms._interval_contains_close(interval_t, loc_t):
ticks_to_draw.append(tick)

ticks = ticks_to_draw
if self.axes.M is None:
self.axes.M = self.axes.get_proj()
self.axes.invM = np.linalg.inv(self.axes.M)

bboxes = []

bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer)
other = []
for updated_artists in self._update_all_positions():
if (ticks := updated_artists.ticks) is not None:
for tlabel_bbs in self._get_ticklabel_bboxes(ticks, renderer):
bboxes.extend(tlabel_bbs)

if self.offsetText.get_visible() and self.offsetText.get_text():
other.append(self.offsetText.get_window_extent(renderer))
if self.line.get_visible():
other.append(self.line.get_window_extent(renderer))
if (self.label.get_visible() and not for_layout_only and
self.label.get_text()):
other.append(self.label.get_window_extent(renderer))
if (line := updated_artists.line) is not None and line.get_visible():
bboxes.append(line.get_window_extent(renderer))

return mtransforms.Bbox.union([*bb_1, *bb_2, *other])
if ((offset_text := updated_artists.offset_text) is not None and
offset_text.get_visible() and offset_text.get_text()):
bboxes.append(offset_text.get_window_extent(renderer))

if updated_artists.label is not None and not for_layout_only:
bboxes.append(updated_artists.label.get_window_extent(renderer))

if bboxes:
return mtransforms.Bbox.union(bboxes)
else:
return None

d_interval = _api.deprecated(
"3.6", alternative="get_data_interval", pending=True)(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 42 additions & 23 deletions lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,13 @@ def test_axes3d_primary_views():
(0, 180, 0)] # -YZ
# When viewing primary planes, draw the two visible axes so they intersect
# at their low values
fig, axs = plt.subplots(2, 3, subplot_kw={'projection': '3d'})
fig, axs = plt.subplots(2, 3, subplot_kw={'projection': '3d'}, layout='tight')
for i, ax in enumerate(axs.flat):
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_proj_type('ortho')
ax.view_init(elev=views[i][0], azim=views[i][1], roll=views[i][2])
plt.tight_layout()


@mpl3d_image_comparison(['bar3d.png'], style='mpl20')
Expand Down Expand Up @@ -2860,6 +2859,47 @@ def test_axis_get_tightbbox_includes_offset_text():
f"bbox.y1 ({bbox.y1}) should be >= offset_bbox.y1 ({offset_bbox.y1})"


@pytest.mark.parametrize('labeltype', ['ticks', 'axis label'])
def test_axes3d_tightbbox_includes_labels(labeltype):
fig = plt.figure()
ax = fig.add_subplot(projection='3d')

renderer = fig._get_renderer()

tight_bbs = {}

if labeltype == 'axis label':
ax.set_zlabel('foo')

# Remove ticks so they don't affect the result
ax.zaxis.set_ticks_position('none')

for pos in ['lower', 'upper', 'both', 'none']:
if labeltype == 'ticks':
ax.zaxis.set_ticks_position(pos)
else:
ax.zaxis.set_label_position(pos)
tight_bbs[pos] = ax.get_tightbbox(renderer)

for pos in ['lower', 'both']:
# Should make space for labels on the left
assert tight_bbs[pos].xmin < tight_bbs['none'].xmin, \
f'No space for labels on left with {labeltype} position "{pos}"'

for pos in ['upper', 'both']:
# Should make space for labels on the right
assert tight_bbs[pos].xmax > tight_bbs['none'].xmax, \
f'No space for labels on right with {labeltype} position "{pos}"'

# No space on the right for 'lower'
assert tight_bbs['lower'].xmax == tight_bbs['none'].xmax, \
f'Unexpected space for labels on right with {labeltype} position "lower"'

# No space on the left for 'upper'
assert tight_bbs['upper'].xmin == tight_bbs['none'].xmin, \
f'Unexpected space for labels on left with {labeltype} position "{pos}"'


def test_ctrl_rotation_snaps_to_5deg():
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
Expand Down Expand Up @@ -3270,24 +3310,3 @@ def test_plot_surface_log_scale_invalid_values():

zmin, zmax = ax.get_zlim()
assert 1e-3 < zmin < zmax < 1e3, f"zlim corrupted: {(zmin, zmax)}"


def test_axes3d_tightbbox_includes_axis_labels():
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter([1], [1], [1])

fig.draw_without_rendering()
renderer = fig._get_renderer()
bb_no_labels = ax.get_tightbbox(renderer)

ax.set_xlabel('X label')
ax.set_ylabel('Y label')
ax.set_zlabel('Z label')

fig.draw_without_rendering()
bb_full = ax.get_tightbbox(renderer)

# The full bbox must be strictly larger in at least one dimension than the
# bbox not including labels.
assert bb_full.width > bb_no_labels.width or bb_full.height > bb_no_labels.height
Loading