Files
geMoldInsight/venv/lib/python3.11/site-packages/pyvista/plotting/axes_assembly.py
T

2096 lines
72 KiB
Python
Raw Normal View History

2026-02-12 23:22:11 +08:00
"""Axes assembly module."""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Sequence
import itertools
from typing import TYPE_CHECKING
from typing import Any
from typing import Literal
from typing import NamedTuple
from typing import TypedDict
import numpy as np
import pyvista as pv
from pyvista import BoundsTuple
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
from pyvista.core._validation.validate import _validate_color_sequence
from pyvista.core.utilities.geometric_sources import AxesGeometrySource
from pyvista.core.utilities.geometric_sources import OrthogonalPlanesSource
from pyvista.core.utilities.geometric_sources import _AxisEnum
from pyvista.core.utilities.geometric_sources import _PartEnum
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from pyvista.core.utilities.misc import abstract_class
from pyvista.plotting import _vtk
from pyvista.plotting.actor import Actor
from pyvista.plotting.colors import Color
from pyvista.plotting.prop3d import Prop3D
from pyvista.plotting.prop3d import _Prop3DMixin
from pyvista.plotting.text import Label
from pyvista.plotting.text import TextProperty
if TYPE_CHECKING:
from collections.abc import Iterator
import sys
from pyvista.core._typing_core import MatrixLike
from pyvista.core._typing_core import VectorLike
from pyvista.core.dataset import DataSet
from pyvista.plotting._typing import ColorLike
if sys.version_info >= (3, 11):
from typing import Unpack
else:
from typing_extensions import Unpack
class _AxesPropTuple(NamedTuple):
x_shaft: float | str | ColorLike
y_shaft: float | str | ColorLike
z_shaft: float | str | ColorLike
x_tip: float | str | ColorLike
y_tip: float | str | ColorLike
z_tip: float | str | ColorLike
class _AxesGeometryKwargs(TypedDict):
shaft_type: AxesGeometrySource.GeometryTypes | DataSet
shaft_radius: float
shaft_length: float | VectorLike[float]
tip_type: AxesGeometrySource.GeometryTypes | DataSet
tip_radius: float
tip_length: float | VectorLike[float]
symmetric_bounds: bool
class _OrthogonalPlanesKwargs(TypedDict):
bounds: VectorLike[float]
resolution: int | VectorLike[int]
normal_sign: Literal['+', '-'] | Sequence[str]
class _XYZTuple(NamedTuple):
x: Any
y: Any
z: Any
@abstract_class
class _XYZAssembly(
_NoNewAttrMixin,
_vtk.DisableVtkSnakeCase,
_Prop3DMixin,
_NameMixin,
_vtk.vtkPropAssembly,
):
DEFAULT_LABELS = _XYZTuple('X', 'Y', 'Z')
def __init__(
self,
*,
xyz_actors: tuple[Any, Any, Any],
xyz_label_actors: tuple[Any, Any, Any],
x_label,
y_label,
z_label,
labels,
label_color,
show_labels,
label_position,
label_size,
x_color,
y_color,
z_color,
position: VectorLike[float],
orientation: VectorLike[float],
origin: VectorLike[float],
scale: float | VectorLike[float],
user_matrix: MatrixLike[float] | None,
name: str | None = None,
):
super().__init__()
def _make_xyz_tuple(xyz):
def _get_tuple(actor_or_actors):
return (
actor_or_actors if isinstance(actor_or_actors, tuple) else (actor_or_actors,)
)
actor_tuples = [_get_tuple(actors) for actors in xyz]
return _XYZTuple(*actor_tuples)
self._assembly_actors = _make_xyz_tuple(xyz_actors)
self._assembly_label_actors = _make_xyz_tuple(xyz_label_actors)
# Add all actors to assembly
for parts in (*self._assembly_actors, *self._assembly_label_actors):
for part in parts:
self.AddPart(part)
# Set colors
if x_color is None:
x_color = pv.global_theme.axes.x_color
if y_color is None:
y_color = pv.global_theme.axes.y_color
if z_color is None:
z_color = pv.global_theme.axes.z_color
self.x_color = x_color
self.y_color = y_color
self.z_color = z_color
# Set text labels
if labels is None:
self.x_label = self.DEFAULT_LABELS.x if x_label is None else x_label
self.y_label = self.DEFAULT_LABELS.y if y_label is None else y_label
self.z_label = self.DEFAULT_LABELS.z if z_label is None else z_label
else:
msg = (
"Cannot initialize '{}' and 'labels' properties together. "
'Specify one or the other, not both.'
)
if x_label is not None:
raise ValueError(msg.format('x_label'))
if y_label is not None:
raise ValueError(msg.format('y_label'))
if z_label is not None:
raise ValueError(msg.format('z_label'))
self.labels = labels
self.show_labels = show_labels
self.label_color = label_color
self.label_size = label_size
self.label_position = label_position
self.position = position
self.orientation = orientation
self.scale = scale # type: ignore[assignment]
self.origin = origin
self.user_matrix = user_matrix
self._name = name
@property
def parts(self):
collection = self.GetParts()
return tuple(collection.GetItemAsObject(i) for i in range(collection.GetNumberOfItems()))
@property
def _label_actor_iterator(self) -> Iterator[Label]:
return itertools.chain.from_iterable(self._assembly_label_actors)
def _post_set_update(self):
# Update prop3D attributes for all assembly parts
parts = self.parts
new_matrix = pv.array_from_vtkmatrix(self._prop3d.GetMatrix())
for part in parts:
if isinstance(part, (Prop3D, _Prop3DMixin)) and not np.array_equal(
part.user_matrix, new_matrix
):
part.user_matrix = new_matrix
def _get_bounds(self) -> BoundsTuple: # numpydoc ignore=RT01
return BoundsTuple(*self.GetBounds())
@property
def show_labels(self) -> bool: # numpydoc ignore=RT01
"""Show or hide the text labels for the axes."""
return self._show_labels
@show_labels.setter
def show_labels(self, value: bool):
self._show_labels = value
for label in self._label_actor_iterator:
label.SetVisibility(value)
@property
@abstractmethod
def labels(self): # numpydoc ignore=RT01
"""XYZ labels."""
@labels.setter
@abstractmethod
def labels(self, labels):
"""XYZ labels."""
@property
@abstractmethod
def x_label(self): # numpydoc ignore=RT01
"""Text label for the x-axis."""
@x_label.setter
@abstractmethod
def x_label(self, label):
"""Text label for the x-axis."""
@property
@abstractmethod
def y_label(self): # numpydoc ignore=RT01
"""Text label for the y-axis."""
@y_label.setter
@abstractmethod
def y_label(self, label):
"""Text label for the y-axis."""
@property
@abstractmethod
def z_label(self): # numpydoc ignore=RT01
"""Text label for the z-axis."""
@z_label.setter
@abstractmethod
def z_label(self, label):
"""Text label for the z-axis."""
@property
@abstractmethod
def label_size(self): # numpydoc ignore=RT01
"""Size of the text labels."""
@label_size.setter
@abstractmethod
def label_size(self, size):
"""Size of the text labels."""
@property
@abstractmethod
def label_position(self): # numpydoc ignore=RT01
"""Position of the text labels."""
@label_position.setter
@abstractmethod
def label_position(self, position):
"""Position of the text labels."""
@property
def label_color(self) -> Color: # numpydoc ignore=RT01
"""Color of the text labels."""
return self._label_color
@label_color.setter
def label_color(self, color: ColorLike):
valid_color = Color(color)
self._label_color = valid_color
for label in self._label_actor_iterator:
label.prop.color = valid_color
@property
@abstractmethod
def x_color(self): # numpydoc ignore=RT01
"""Color of the x-axis actors."""
@x_color.setter
@abstractmethod
def x_color(self, color):
"""Color of the x-axis actors."""
@property
@abstractmethod
def y_color(self): # numpydoc ignore=RT01
"""Color of the y-axis actors."""
@y_color.setter
@abstractmethod
def y_color(self, color):
"""Color of the y-axis actors."""
@property
@abstractmethod
def z_color(self): # numpydoc ignore=RT01
"""Color of the z-axis actors."""
@z_color.setter
@abstractmethod
def z_color(self, color):
"""Color of the z-axis actors."""
class AxesAssembly(_XYZAssembly):
"""Assembly of arrow-style axes parts.
The axes may be used as a widget or added to a scene.
Parameters
----------
x_label : str, default: 'X'
Text label for the x-axis. Alternatively, set the label with :attr:`labels`.
y_label : str, default: 'Y'
Text label for the y-axis. Alternatively, set the label with :attr:`labels`.
z_label : str, default: 'Z'
Text label for the z-axis. Alternatively, set the label with :attr:`labels`.
labels : Sequence[str], optional,
Text labels for the axes. This is an alternative parameter to using
:attr:`x_label`, :attr:`y_label`, and :attr:`z_label` separately.
label_color : ColorLike, default: 'black'
Color of the text labels.
show_labels : bool, default: True
Show or hide the text labels.
label_position : float | VectorLike[float], optional
Position of the text labels along each axis. By default, the labels are
positioned at the ends of the shafts.
label_size : int, default: 50
Size of the text labels.
x_color : ColorLike | Sequence[ColorLike], optional
Color of the x-axis shaft and tip.
y_color : ColorLike | Sequence[ColorLike], optional
Color of the y-axis shaft and tip.
z_color : ColorLike | Sequence[ColorLike], optional
Color of the z-axis shaft and tip.
position : VectorLike[float], default: (0.0, 0.0, 0.0)
Position of the axes in space.
orientation : VectorLike[float], default: (0, 0, 0)
Orientation angles of the axes which define rotations about the
world's x-y-z axes. The angles are specified in degrees and in
x-y-z order. However, the actual rotations are applied in the
around the y-axis first, then the x-axis, and finally the z-axis.
origin : VectorLike[float], default: (0.0, 0.0, 0.0)
Origin of the axes. This is the point about which all rotations take place. The
rotations are defined by the :attr:`~pyvista.Prop3D.orientation`.
scale : VectorLike[float], default: (1.0, 1.0, 1.0)
Scaling factor applied to the axes.
user_matrix : MatrixLike[float], optional
A 4x4 transformation matrix applied to the axes. Defaults to the identity matrix.
The user matrix is the last transformation applied to the actor.
name : str, optional
The name of this assembly used when tracking on a plotter.
.. versionadded:: 0.45
**kwargs
Keyword arguments passed to :class:`pyvista.AxesGeometrySource`.
See Also
--------
AxesAssemblySymmetric
:ref:`axes_objects_example`
Example showing different axes objects.
Examples
--------
Add axes to a plot.
>>> import pyvista as pv
>>> axes = pv.AxesAssembly()
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes)
>>> pl.show()
Customize the axes colors. Set each axis to a single color, or set the colors of
each shaft and tip separately with two colors.
>>> axes.x_color = ['cyan', 'blue']
>>> axes.y_color = ['magenta', 'red']
>>> axes.z_color = 'yellow'
Customize the label color too.
>>> axes.label_color = 'brown'
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes)
>>> pl.show()
Create axes with custom geometry. Use pyramid shafts and hemisphere tips and
modify the lengths.
>>> axes = pv.AxesAssembly(
... shaft_type='pyramid',
... tip_type='hemisphere',
... tip_length=0.1,
... shaft_length=(0.5, 1.0, 1.5),
... )
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes)
>>> pl.show()
Position and orient the axes in space.
>>> axes = pv.AxesAssembly(position=(1.0, 2.0, 3.0), orientation=(10, 20, 30))
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes)
>>> pl.show()
Add the axes as a custom orientation widget with
:func:`~pyvista.Renderer.add_orientation_widget`:
>>> import pyvista as pv
>>> axes = pv.AxesAssembly(symmetric_bounds=True)
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Cone())
>>> _ = pl.add_orientation_widget(
... axes,
... viewport=(0, 0, 0.5, 0.5),
... )
>>> pl.show()
"""
def _init_actors_from_source(self, geometry_source: AxesGeometrySource):
# Init shaft and tip actors
self._shaft_actors: tuple[Actor, Actor, Actor] = (Actor(), Actor(), Actor())
self._tip_actors: tuple[Actor, Actor, Actor] = (Actor(), Actor(), Actor())
self._shaft_and_tip_actors = (*self._shaft_actors, *self._tip_actors)
# Init shaft and tip datasets
self._shaft_and_tip_geometry_source = geometry_source
shaft_tip_datasets = self._shaft_and_tip_geometry_source.output
for actor, dataset in zip(self._shaft_and_tip_actors, shaft_tip_datasets):
actor.mapper = pv.DataSetMapper(dataset=dataset)
def __init__(
self,
*,
x_label: str | None = None,
y_label: str | None = None,
z_label: str | None = None,
labels: Sequence[str] | None = None,
label_color: ColorLike = 'black',
show_labels: bool = True,
label_position: float | VectorLike[float] | None = None,
label_size: int = 50,
x_color: ColorLike | Sequence[ColorLike] | None = None,
y_color: ColorLike | Sequence[ColorLike] | None = None,
z_color: ColorLike | Sequence[ColorLike] | None = None,
position: VectorLike[float] = (0.0, 0.0, 0.0),
orientation: VectorLike[float] = (0.0, 0.0, 0.0),
origin: VectorLike[float] = (0.0, 0.0, 0.0),
scale: float | VectorLike[float] = (1.0, 1.0, 1.0),
user_matrix: MatrixLike[float] | None = None,
name: str | None = None,
**kwargs: Unpack[_AxesGeometryKwargs],
):
# Init shaft and tip actors
self._init_actors_from_source(AxesGeometrySource(symmetric=False, **kwargs))
# Init label actors
self._label_actors = (Label(), Label(), Label())
_XYZAssembly.__init__(
self,
xyz_actors=tuple(zip(self._shaft_actors, self._tip_actors)), # type: ignore[arg-type]
xyz_label_actors=self._label_actors,
x_label=x_label,
y_label=y_label,
z_label=z_label,
labels=labels,
label_color=label_color,
show_labels=show_labels,
label_position=label_position,
label_size=label_size,
x_color=x_color,
y_color=y_color,
z_color=z_color,
position=position,
orientation=orientation,
origin=origin,
scale=scale,
user_matrix=user_matrix,
name=name,
)
self._set_default_label_props()
def _set_default_label_props(self):
# TODO: implement set_text_prop() and use that instead
for label in self._label_actor_iterator:
prop = label.prop
prop.bold = True
prop.italic = True
prop.enable_shadow()
prop.SetShadowOffset(-1, 1)
def __repr__(self):
"""Representation of the axes assembly."""
mat_info = 'Identity' if np.array_equal(self.user_matrix, np.eye(4)) else 'Set'
bnds = self.bounds
geometry_repr = repr(self._shaft_and_tip_geometry_source).splitlines()[1:]
attr = [
f'{type(self).__name__} ({hex(id(self))})',
*geometry_repr,
f" X label: '{self.x_label}'",
f" Y label: '{self.y_label}'",
f" Z label: '{self.z_label}'",
f' Label color: {self.label_color}',
f' Show labels: {self.show_labels}',
f' Label position: {self.label_position}',
' X Color: ',
f' Shaft {self.x_color[0]}',
f' Tip {self.x_color[1]}',
' Y Color: ',
f' Shaft {self.y_color[0]}',
f' Tip {self.y_color[1]}',
' Z Color: ',
f' Shaft {self.z_color[0]}',
f' Tip {self.z_color[1]}',
f' Position: {self.position}',
f' Orientation: {self.orientation}',
f' Origin: {self.origin}',
f' Scale: {self.scale}',
f' User matrix: {mat_info}',
f' X Bounds {bnds.x_min:.3E}, {bnds.x_max:.3E}',
f' Y Bounds {bnds.y_min:.3E}, {bnds.y_max:.3E}',
f' Z Bounds {bnds.z_min:.3E}, {bnds.z_max:.3E}',
]
return '\n'.join(attr)
@property
def labels(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
"""Return or set the axes labels.
This property may be used as an alternative to using :attr:`x_label`,
:attr:`y_label`, and :attr:`z_label` separately.
Examples
--------
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.labels = ['X Axis', 'Y Axis', 'Z Axis']
>>> axes_assembly.labels
('X Axis', 'Y Axis', 'Z Axis')
"""
return self.x_label, self.y_label, self.z_label
@labels.setter
def labels(self, labels: list[str] | tuple[str, str, str]):
labels = _validate_label_sequence(labels, n_labels=3, name='labels')
self.x_label = labels[0]
self.y_label = labels[1]
self.z_label = labels[2]
@property
def x_label(self) -> str: # numpydoc ignore=RT01
"""Text label for the x-axis.
Examples
--------
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.x_label = 'This axis'
>>> axes_assembly.x_label
'This axis'
"""
return self._label_actors[0].input
@x_label.setter
def x_label(self, label: str):
self._label_actors[0].input = label
@property
def y_label(self) -> str: # numpydoc ignore=RT01
"""Text label for the y-axis.
Examples
--------
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.y_label = 'This axis'
>>> axes_assembly.y_label
'This axis'
"""
return self._label_actors[1].input
@y_label.setter
def y_label(self, label: str):
self._label_actors[1].input = label
@property
def z_label(self) -> str: # numpydoc ignore=RT01
"""Text label for the z-axis.
Examples
--------
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.z_label = 'This axis'
>>> axes_assembly.z_label
'This axis'
"""
return self._label_actors[2].input
@z_label.setter
def z_label(self, label: str):
self._label_actors[2].input = label
@property
def label_size(self) -> int: # numpydoc ignore=RT01
"""Size of the text labels.
Must be a positive integer.
"""
return self._label_size
@label_size.setter
def label_size(self, size: int):
self._label_size = size
for label in self._label_actor_iterator:
label.size = size
@property
def label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the text label along each axis.
By default, the labels are positioned at the ends of the shafts.
Values must be non-negative.
Examples
--------
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.label_position
(0.8, 0.8, 0.8)
>>> axes_assembly.label_position = 0.3
>>> axes_assembly.label_position
(0.3, 0.3, 0.3)
>>> axes_assembly.label_position = (0.1, 0.4, 0.2)
>>> axes_assembly.label_position
(0.1, 0.4, 0.2)
"""
position = self._label_position
return self._shaft_and_tip_geometry_source.shaft_length if position is None else position
@label_position.setter
def label_position(self, position: float | VectorLike[float] | None):
self._label_position = (
None
if position is None
else _validation.validate_array3(
position,
broadcast=True,
must_be_in_range=[0, np.inf],
name='Label position',
dtype_out=float,
to_tuple=True,
)
)
self._update_label_positions()
@property
def x_color(self) -> tuple[Color, Color]: # numpydoc ignore=RT01
"""Color of the x-axis shaft and tip."""
return self.get_actor_prop('color')[_AxisEnum.x :: 3]
@x_color.setter
def x_color(self, color: ColorLike | Sequence[ColorLike]):
self.set_actor_prop('color', color, axis=_AxisEnum.x.value) # type: ignore[arg-type]
@property
def y_color(self) -> tuple[Color, Color]: # numpydoc ignore=RT01
"""Color of the y-axis shaft and tip."""
return self.get_actor_prop('color')[_AxisEnum.y :: 3]
@y_color.setter
def y_color(self, color: ColorLike | Sequence[ColorLike]):
self.set_actor_prop('color', color, axis=_AxisEnum.y.value) # type: ignore[arg-type]
@property
def z_color(self) -> tuple[Color, Color]: # numpydoc ignore=RT01
"""Color of the z-axis shaft and tip."""
return self.get_actor_prop('color')[_AxisEnum.z.value :: 3]
@z_color.setter
def z_color(self, color: ColorLike | Sequence[ColorLike]):
self.set_actor_prop('color', color, axis=_AxisEnum.z.value) # type: ignore[arg-type]
@_deprecate_positional_args(allowed=['name', 'value'])
def set_actor_prop( # noqa: PLR0917
self,
name: str,
value: float | str | ColorLike | Sequence[float | str | ColorLike],
axis: Literal['x', 'y', 'z', 'all'] = 'all',
part: Literal['shaft', 'tip', 'all'] = 'all',
):
"""Set :class:`~pyvista.Property` attributes for the axes shaft and/or tip actors.
This is a generalized setter method which sets the value of a specific
:class:`~pyvista.Property` attribute for any combination of axis shaft or tip
parts.
Parameters
----------
name : str
Name of the :class:`~pyvista.Property` attribute to set.
value : float | str | ColorLike | Sequence[float | str | ColorLike]
Value to set the attribute to. If a single value, set all specified axes
shaft(s) or tip(s) :class:`~pyvista.Property` attributes to this value.
If a sequence of values, set the specified parts to these values.
axis : str | int, default: 'all'
Set :class:`~pyvista.Property` attributes for a specific part of the axes.
Specify one of:
- ``'x'``: only set the property for the x-axis.
- ``'y'``: only set the property for the y-axis.
- ``'z'``: only set the property for the z-axis.
- ``'all'``: set the property for all three axes.
part : str | int, default: 'all'
Set the property for a specific part of the axes. Specify one of:
- ``'shaft'``: only set the property for the axes shafts.
- ``'tip'``: only set the property for the axes tips.
- ``'all'``: set the property for axes shafts and tips.
Examples
--------
Set :attr:`~pyvista.Property.ambient` for all axes shafts and tips to a
single value.
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.set_actor_prop('ambient', 0.7)
>>> axes_assembly.get_actor_prop('ambient') # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft=0.7,
y_shaft=0.7,
z_shaft=0.7,
x_tip=0.7,
y_tip=0.7,
z_tip=0.7)
Set the property again, but this time set separate values for each part.
>>> values = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
>>> axes_assembly.set_actor_prop('ambient', values)
>>> axes_assembly.get_actor_prop('ambient') # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft=0.1,
y_shaft=0.2,
z_shaft=0.3,
x_tip=0.4,
y_tip=0.5,
z_tip=0.6)
Set :attr:`~pyvista.Property.opacity` for the x-axis only. The property is set
for both the axis shaft and tip by default.
>>> axes_assembly.set_actor_prop('opacity', 0.5, axis='x')
>>> axes_assembly.get_actor_prop('opacity') # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft=0.5,
y_shaft=1.0,
z_shaft=1.0,
x_tip=0.5,
y_tip=1.0,
z_tip=1.0)
Set the property again, but this time set separate values for the shaft and tip.
>>> axes_assembly.set_actor_prop('opacity', [0.3, 0.7], axis='x')
>>> axes_assembly.get_actor_prop('opacity') # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft=0.3,
y_shaft=1.0,
z_shaft=1.0,
x_tip=0.7,
y_tip=1.0,
z_tip=1.0)
Set :attr:`~pyvista.Property.show_edges` for the axes shafts only. The property
is set for all axes by default.
>>> axes_assembly.set_actor_prop('show_edges', True, part='shaft')
>>> axes_assembly.get_actor_prop(
... 'show_edges'
... ) # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft=True,
y_shaft=True,
z_shaft=True,
x_tip=False,
y_tip=False,
z_tip=False)
Set the property again, but this time set separate values for each shaft.
>>> axes_assembly.set_actor_prop(
... 'show_edges', [True, False, True], part='shaft'
... )
>>> axes_assembly.get_actor_prop(
... 'show_edges'
... ) # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft=True,
y_shaft=False,
z_shaft=True,
x_tip=False,
y_tip=False,
z_tip=False)
Set :attr:`~pyvista.Property.style` for a single axis and specific part.
>>> axes_assembly.set_actor_prop('style', 'wireframe', axis='x', part='shaft')
>>> axes_assembly.get_actor_prop('style') # doctest: +NORMALIZE_WHITESPACE
_AxesPropTuple(x_shaft='Wireframe',
y_shaft='Surface',
z_shaft='Surface',
x_tip='Surface',
y_tip='Surface',
z_tip='Surface')
"""
actors = self._filter_part_actors(axis=axis, part=part)
values: Sequence[float | str | ColorLike]
# Validate input as a sequence of values
if 'color' in name:
# Special case for color inputs
if axis == 'all' and part == 'all':
n_values = 6
elif part == 'all':
n_values = 2
elif axis == 'all':
n_values = 3
else:
n_values = 1
values = _validate_color_sequence(value, n_values) # type: ignore[arg-type]
elif isinstance(value, Sequence) and not isinstance(value, str):
# Number sequence
values = value
else:
# Scalar number or string
values = [value] * len(actors)
if len(values) != len(actors):
msg = (
f'Number of values ({len(values)}) in {value} must match the number of '
f"actors ({len(actors)}) for axis '{axis}' and part '{part}'"
)
raise ValueError(msg)
# Sequence is valid, now set values
for actor, val in zip(actors, values):
setattr(actor.prop, name, val)
def get_actor_prop(self, name: str):
"""Get :class:`~pyvista.Property` attributes for the axes shaft and/or tip actors.
This is a generalized getter method which returns the value of
a specific :class:`pyvista.Property` attribute for all shafts and tips.
Parameters
----------
name : str
Name of the :class:`~pyvista.Property` attribute to get.
Returns
-------
tuple
Named tuple with attribute values for the axes shafts and tips.
The values are ordered ``(x_shaft, y_shaft, z_shaft, x_tip, y_tip, z_tip)``.
Examples
--------
Get the ambient property of the axes shafts and tips.
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssembly()
>>> axes_assembly.get_actor_prop('ambient')
_AxesPropTuple(x_shaft=0.0, y_shaft=0.0, z_shaft=0.0, x_tip=0.0, y_tip=0.0, z_tip=0.0)
"""
prop_values = [getattr(actor.prop, name) for actor in self._shaft_and_tip_actors]
return _AxesPropTuple(*prop_values)
def _filter_part_actors(
self,
axis: Literal['x', 'y', 'z', 'all'] = 'all',
part: Literal['shaft', 'tip', 'all'] = 'all',
):
valid_axis = [0, 1, 2, 'x', 'y', 'z', 'all']
valid_axis_official = valid_axis[3:]
if axis not in valid_axis:
msg = f'Axis must be one of {valid_axis_official}.'
raise ValueError(msg)
valid_part = [0, 1, 'shaft', 'tip', 'all']
valid_part_official = valid_part[2:]
if part not in valid_part:
msg = f'Part must be one of {valid_part_official}.'
raise ValueError(msg)
# Create ordered list of filtered actors
# Iterate over parts in <shaft-xyz> then <tip-xyz> order
actors: list[Actor] = []
for part_type, axis_num in itertools.product(_PartEnum, _AxisEnum):
if part in [part_type.name, part_type.value, 'all'] and axis in [
axis_num.name,
axis_num.value,
'all',
]:
# Add actor to list
if part_type == _PartEnum.shaft:
actors.append(self._shaft_actors[axis_num])
else:
actors.append(self._tip_actors[axis_num])
return actors
def _get_offset_label_position_vectors(self, position_scalars: tuple[float, float, float]):
# Create position vectors
position_vectors = np.diag(position_scalars)
# Offset label positions radially by the tip radius
tip_radius = self._shaft_and_tip_geometry_source.tip_radius
offset_array = np.diag([tip_radius] * 3)
radial_offset1 = np.roll(offset_array, shift=1, axis=1)
radial_offset2 = np.roll(offset_array, shift=-1, axis=1)
position_vectors += radial_offset1 + radial_offset2
return position_vectors
def _update_label_positions(self):
labels = self._label_actors
position_vectors = self._get_offset_label_position_vectors(self.label_position)
for label, position in zip(labels, position_vectors):
label.relative_position = position
def _validate_label_sequence(labels: Sequence[str], n_labels: int | Sequence[int], name: str):
_validation.check_instance(labels, (list, tuple), name=name)
_validation.check_iterable_items(labels, str, name=name)
_validation.check_length(labels, exact_length=n_labels, name=name)
return labels
class AxesAssemblySymmetric(AxesAssembly):
"""Symmetric assembly of arrow-style axes parts.
This class is similar to :class:`~pyvista.AxesAssembly` but the axes are
symmetric.
The axes may be used as a widget or added to a scene.
Parameters
----------
x_label : str, default: ('+X', '-X')
Text labels for the positive and negative x-axis. Specify two strings or a
single string. If a single string, plus ``'+'`` and minus ``'-'`` characters
are added. Alternatively, set the labels with :attr:`labels`.
y_label : str, default: ('+Y', '-Y')
Text labels for the positive and negative y-axis. Specify two strings or a
single string. If a single string, plus ``'+'`` and minus ``'-'`` characters
are added. Alternatively, set the labels with :attr:`labels`.
z_label : str, default: ('+Z', '-Z')
Text labels for the positive and negative z-axis. Specify two strings or a
single string. If a single string, plus ``'+'`` and minus ``'-'`` characters
are added. Alternatively, set the labels with :attr:`labels`.
labels : Sequence[str], optional
Text labels for the axes. Specify three strings, one for each axis, or
six strings, one for each +/- axis. If three strings plus ``'+'`` and minus
``'-'`` characters are added. This is an alternative parameter to using
:attr:`x_label`, :attr:`y_label`, and :attr:`z_label` separately.
label_color : ColorLike, default: 'black'
Color of the text labels.
show_labels : bool, default: True
Show or hide the text labels.
label_position : float | VectorLike[float], optional
Position of the text labels along each axis. By default, the labels are
positioned at the ends of the shafts.
label_size : int, default: 50
Size of the text labels.
x_color : ColorLike | Sequence[ColorLike], optional
Color of the x-axis shaft and tip.
y_color : ColorLike | Sequence[ColorLike], optional
Color of the y-axis shaft and tip.
z_color : ColorLike | Sequence[ColorLike], optional
Color of the z-axis shaft and tip.
position : VectorLike[float], default: (0.0, 0.0, 0.0)
Position of the axes in space.
orientation : VectorLike[float], default: (0, 0, 0)
Orientation angles of the axes which define rotations about the
world's x-y-z axes. The angles are specified in degrees and in
x-y-z order. However, the actual rotations are applied in the
around the y-axis first, then the x-axis, and finally the z-axis.
origin : VectorLike[float], default: (0.0, 0.0, 0.0)
Origin of the axes. This is the point about which all rotations take place. The
rotations are defined by the :attr:`~pyvista.Prop3D.orientation`.
scale : VectorLike[float], default: (1.0, 1.0, 1.0)
Scaling factor applied to the axes.
user_matrix : MatrixLike[float], optional
A 4x4 transformation matrix applied to the axes. Defaults to the identity matrix.
The user matrix is the last transformation applied to the actor.
name : str, optional
The name of this assembly used when tracking on a plotter.
.. versionadded:: 0.45
**kwargs
Keyword arguments passed to :class:`pyvista.AxesGeometrySource`.
See Also
--------
AxesAssembly
:ref:`axes_objects_example`
Example showing different axes objects.
Examples
--------
Add symmetric axes to a plot.
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssemblySymmetric()
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes_assembly)
>>> pl.show()
Customize the axes labels.
>>> axes_assembly.labels = [
... 'east',
... 'west',
... 'north',
... 'south',
... 'up',
... 'down',
... ]
>>> axes_assembly.label_color = 'darkgoldenrod'
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes_assembly)
>>> pl.show()
Add the axes as a custom orientation widget with
:func:`~pyvista.Renderer.add_orientation_widget`. We also configure the labels to
only show text for the positive axes.
>>> axes_assembly = pv.AxesAssemblySymmetric(
... x_label=('X', ''), y_label=('Y', ''), z_label=('Z', '')
... )
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Cone())
>>> _ = pl.add_orientation_widget(
... axes_assembly,
... viewport=(0, 0, 0.5, 0.5),
... )
>>> pl.show()
"""
def __init__(
self,
*,
x_label: str | Sequence[str] | None = None,
y_label: str | Sequence[str] | None = None,
z_label: str | Sequence[str] | None = None,
labels: Sequence[str] | None = None,
label_color: ColorLike = 'black',
show_labels: bool = True,
label_position: float | VectorLike[float] | None = None,
label_size: int = 50,
x_color: ColorLike | Sequence[ColorLike] | None = None,
y_color: ColorLike | Sequence[ColorLike] | None = None,
z_color: ColorLike | Sequence[ColorLike] | None = None,
position: VectorLike[float] = (0.0, 0.0, 0.0),
orientation: VectorLike[float] = (0.0, 0.0, 0.0),
origin: VectorLike[float] = (0.0, 0.0, 0.0),
scale: float | VectorLike[float] = (1.0, 1.0, 1.0),
user_matrix: MatrixLike[float] | None = None,
name: str | None = None,
**kwargs: Unpack[_AxesGeometryKwargs],
):
# Init shaft and tip actors
self._init_actors_from_source(AxesGeometrySource(symmetric=True, **kwargs))
# Init label actors
self._label_actors = (Label(), Label(), Label())
self._label_actors_symmetric = (Label(), Label(), Label())
_XYZAssembly.__init__(
self,
xyz_actors=tuple(zip(self._shaft_actors, self._tip_actors)), # type: ignore[arg-type]
xyz_label_actors=tuple(zip(self._label_actors, self._label_actors_symmetric)), # type: ignore[arg-type]
x_label=x_label,
y_label=y_label,
z_label=z_label,
labels=labels,
label_color=label_color,
show_labels=show_labels,
label_position=label_position,
label_size=label_size,
x_color=x_color,
y_color=y_color,
z_color=z_color,
position=position,
orientation=orientation,
origin=origin,
scale=scale,
user_matrix=user_matrix,
name=name,
)
self._set_default_label_props()
@property # type: ignore[override]
def labels(self) -> tuple[str, str, str, str, str, str]: # numpydoc ignore=RT01
"""Return or set the axes labels.
Specify three strings, one for each axis, or six strings, one for each +/- axis.
If three strings, plus ``'+'`` and minus ``'-'`` characters are added.
This property may be used as an alternative to using :attr:`x_label`,
:attr:`y_label`, and :attr:`z_label` separately.
Examples
--------
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssemblySymmetric()
Use three strings to set the labels. Plus ``'+'`` and minus ``'-'``
characters are added automatically.
>>> axes_assembly.labels = ['U', 'V', 'W']
>>> axes_assembly.labels
('+U', '-U', '+V', '-V', '+W', '-W')
Alternatively, use six strings to set the labels explicitly.
>>> axes_assembly.labels = [
... 'east',
... 'west',
... 'north',
... 'south',
... 'up',
... 'down',
... ]
>>> axes_assembly.labels
('east', 'west', 'north', 'south', 'up', 'down')
"""
return *self.x_label, *self.y_label, *self.z_label
@labels.setter
def labels(
self,
labels: list[str] | tuple[str, str, str] | tuple[str, str, str, str, str, str],
):
valid_labels = _validate_label_sequence(labels, n_labels=[3, 6], name='labels')
if len(valid_labels) == 3:
self.x_label = valid_labels[0]
self.y_label = valid_labels[1]
self.z_label = valid_labels[2]
else:
self.x_label = valid_labels[0:2]
self.y_label = valid_labels[2:4]
self.z_label = valid_labels[4:6]
def _get_axis_label(self, axis: _AxisEnum) -> tuple[str, str]:
label_plus = self._label_actors[axis].input
label_minus = self._label_actors_symmetric[axis].input
return label_plus, label_minus
def _set_axis_label(self, axis: _AxisEnum, label: str | list[str] | tuple[str, str]):
if isinstance(label, str):
label_plus, label_minus = '+' + label, '-' + label
else:
label_plus, label_minus = _validate_label_sequence(label, n_labels=2, name='label')
self._label_actors[axis].input = label_plus
self._label_actors_symmetric[axis].input = label_minus
@property # type: ignore[override]
def x_label(self) -> tuple[str, str]: # numpydoc ignore=RT01
"""Return or set the labels for the positive and negative x-axis.
The labels may be set with a single string or two strings. If a single string,
plus ``'+'`` and minus ``'-'`` characters are added. Alternatively, set the
labels with :attr:`labels`.
Examples
--------
Set the labels with a single string. Plus ``'+'`` and minus ``'-'``
characters are added automatically.
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssemblySymmetric()
>>> axes_assembly.x_label = 'Axis'
>>> axes_assembly.x_label
('+Axis', '-Axis')
Set the labels explicitly with two strings.
>>> axes_assembly.x_label = 'anterior', 'posterior'
>>> axes_assembly.x_label
('anterior', 'posterior')
"""
return self._get_axis_label(_AxisEnum.x)
@x_label.setter
def x_label(self, label: str | list[str] | tuple[str, str]):
self._set_axis_label(_AxisEnum.x, label)
@property # type: ignore[override]
def y_label(self) -> tuple[str, str]: # numpydoc ignore=RT01
"""Return or set the labels for the positive and negative y-axis.
The labels may be set with a single string or two strings. If a single string,
plus ``'+'`` and minus ``'-'`` characters are added. Alternatively, set the
labels with :attr:`labels`.
Examples
--------
Set the labels with a single string. Plus ``'+'`` and minus ``'-'``
characters are added automatically.
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssemblySymmetric()
>>> axes_assembly.y_label = 'Axis'
>>> axes_assembly.y_label
('+Axis', '-Axis')
Set the labels explicitly with two strings.
>>> axes_assembly.y_label = 'left', 'right'
>>> axes_assembly.y_label
('left', 'right')
"""
return self._get_axis_label(_AxisEnum.y)
@y_label.setter
def y_label(self, label: str | list[str] | tuple[str, str]):
self._set_axis_label(_AxisEnum.y, label)
@property # type: ignore[override]
def z_label(self) -> tuple[str, str]: # numpydoc ignore=RT01
"""Return or set the labels for the positive and negative z-axis.
The labels may be set with a single string or two strings. If a single string,
plus ``'+'`` and minus ``'-'`` characters are added. Alternatively, set the
labels with :attr:`labels`.
Examples
--------
Set the labels with a single string. Plus ``'+'`` and minus ``'-'``
characters are added automatically.
>>> import pyvista as pv
>>> axes_assembly = pv.AxesAssemblySymmetric()
>>> axes_assembly.z_label = 'Axis'
>>> axes_assembly.z_label
('+Axis', '-Axis')
Set the labels explicitly with two strings.
>>> axes_assembly.z_label = 'superior', 'inferior'
>>> axes_assembly.z_label
('superior', 'inferior')
"""
return self._get_axis_label(_AxisEnum.z)
@z_label.setter
def z_label(self, label: str | list[str] | tuple[str, str]):
self._set_axis_label(_AxisEnum.z, label)
def _update_label_positions(self):
# Update plus labels using parent method
AxesAssembly._update_label_positions(self)
# Update minus labels
label_position = self.label_position
label_position_minus = (
-label_position[0],
-label_position[1],
-label_position[2],
)
labels_minus = self._label_actors_symmetric
vector_position_minus = self._get_offset_label_position_vectors(label_position_minus)
for label, position in zip(labels_minus, vector_position_minus):
label.relative_position = position
class PlanesAssembly(_XYZAssembly):
"""Assembly of orthogonal planes.
Assembly of three orthogonal plane meshes with labels.
The labels can be 2D or 3D, and will follow the camera such that they have the
correct orientation and remain parallel to the edges of the planes.
The positioning of the labels may be customized using the :attr:`label_edge`,
:attr:`label_position`, and :attr:`label_offset` attributes.
.. warning::
The :attr:`camera` must be set before rendering the assembly. Otherwise,
attempting to render it will cause python to crash.
.. versionadded:: 0.45
Parameters
----------
x_label : str, default: 'YZ'
Text label for the yz-plane. Alternatively, set the label with :attr:`labels`.
y_label : str, default: 'ZX'
Text label for the zx-plane. Alternatively, set the label with :attr:`labels`.
z_label : str, default: 'XY'
Text label for the xy-plane. Alternatively, set the label with :attr:`labels`.
labels : Sequence[str], optional,
Text labels for the planes. This is an alternative parameter to using
:attr:`x_label`, :attr:`y_label`, and :attr:`z_label` separately.
label_color : ColorLike, default: 'black'
Color of the text labels.
show_labels : bool, default: True
Show or hide the text labels.
label_position : float | VectorLike[float], default: 0.5
Normalized relative position of the text labels along each plane's respective
:attr:`label_edge`. The positions are normalized to have a range of
``[-1.0, 1.0]`` such that ``0.0`` is at the center of the edge and ``-1.0`` and
``1.0`` are at the corners.
.. note::
The label text is centered horizontally at the specified positions.
label_edge : str, default: 'right'
Edge on which to position each plane's label. Can be ``'top'``, ``'bottom'``,
``'right'``, or ``'left'``. Use a single value to set the edge for all labels
or set each edge independently.
label_offset : float | VectorLike[float], optional
Vertical offset of the text labels. The offset is proportional to
the :attr:`~pyvista.Prop3D.length` of the assembly. Positive values move the labels away
from the center; negative values move them towards it.
label_size : int, default: 50
Size of the text labels. If :attr:`label_mode` is ``'2D'``, this is the
font size. If :attr:`label_mode` is ``'3D'``, the labels are scaled
proportional to the :attr:`~pyvista.Prop3D.length` of the assembly.
label_mode : '2D' | '3D', default: '2D'
Mode to use for text labels. In 2D mode, the label actors are always visible
and have a constant size regardless of window size. In 3D mode, the label actors
may be occluded by other geometry and will scale with changes to the window
size. The two modes also have minor differences in appearance and behavior in
terms of how they follow the camera.
x_color : ColorLike, optional
Color of the xy-plane.
y_color : ColorLike, optional
Color of the yz-plane.
z_color : ColorLike, optional
Color of the zx-plane.
opacity : float, default: 0.3
Opacity of the planes.
position : VectorLike[float], default: (0.0, 0.0, 0.0)
Position of the planes in space.
orientation : VectorLike[float], default: (0, 0, 0)
Orientation angles of the assembly which define rotations about the
world's x-y-z axes. The angles are specified in degrees and in
x-y-z order. However, the actual rotations are applied in the
around the y-axis first, then the x-axis, and finally the z-axis.
origin : VectorLike[float], default: (0.0, 0.0, 0.0)
Origin of the assembly. This is the point about which all rotations take place.
The rotations are defined by the :attr:`~pyvista.Prop3D.orientation`.
scale : VectorLike[float], default: (1.0, 1.0, 1.0)
Scaling factor applied to the assembly.
user_matrix : MatrixLike[float], optional
A 4x4 transformation matrix applied to the assembly. Defaults to the identity
matrix. The user matrix is the last transformation applied to the actor.
name : str, optional
The name of this assembly used when tracking on a plotter.
.. versionadded:: 0.45
**kwargs
Keyword arguments passed to :class:`pyvista.OrthogonalPlanesSource`.
Examples
--------
Fit planes to a model of a human.
>>> import numpy as np
>>> import pyvista as pv
>>> from pyvista import examples
>>> human = examples.download_human()
>>> labels = 'Sagittal', 'Coronal', 'Transverse'
>>> planes = pv.PlanesAssembly(
... bounds=human.bounds, labels=labels, label_size=25
... )
Plot the planes and the model.
>>> pl = pv.Plotter()
>>> human_actor = pl.add_mesh(human, scalars='Color', rgb=True)
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
Apply a transformation to the planes and the model.
>>> transform = np.array(
... [
... [0.70645893, 0.69636424, 0.12646197, 1.0],
... [-0.62246712, 0.69636424, -0.35722756, 2.0],
... [-0.33682409, 0.17364818, 0.92541658, 3.0],
... [0.0, 0.0, 0.0, 1.0],
... ]
... )
>>> planes.user_matrix = transform
>>> human_actor.user_matrix = transform
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(human_actor)
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
Create a new assembly and customize the colors and opacity.
>>> color = pv.global_theme.color
>>> planes = pv.PlanesAssembly(
... bounds=human.bounds,
... x_color=color,
... y_color=color,
... z_color=color,
... opacity=1.0,
... )
Since the planes are opaque, the 3D labels may be occluded. Use 2D labels instead
so the labels are always visible.
>>> planes.label_mode = '2D'
Offset the labels to position them inside the bounds of the planes.
>>> planes.label_offset = -0.05
Move the labels for the two larger planes closer to the corners.
>>> planes.label_position = (0.8, 0.8, 0.5)
Visualize the result.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(human, scalars='Color', rgb=True)
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
"""
DEFAULT_LABELS = _XYZTuple('YZ', 'ZX', 'XY')
def __init__(
self,
*,
x_label: str | None = None,
y_label: str | None = None,
z_label: str | None = None,
labels: Sequence[str] | None = None,
label_color: ColorLike = 'black',
show_labels: bool = True,
label_position: float | VectorLike[float] = 0.5,
label_edge: Literal['top', 'bottom', 'right', 'left'] | Sequence[str] = 'right',
label_offset: float = 0.05,
label_size: int = 50,
label_mode: Literal['2D', '3D'] = '3D',
x_color: ColorLike | None = None,
y_color: ColorLike | None = None,
z_color: ColorLike | None = None,
opacity: float | VectorLike[float] = 0.3,
position: VectorLike[float] = (0.0, 0.0, 0.0),
orientation: VectorLike[float] = (0.0, 0.0, 0.0),
origin: VectorLike[float] = (0.0, 0.0, 0.0),
scale: float | VectorLike[float] = (1.0, 1.0, 1.0),
user_matrix: MatrixLike[float] | None = None,
name: str | None = None,
**kwargs: Unpack[_OrthogonalPlanesKwargs],
):
self._camera = None
# Init plane actors
self._plane_actors = (Actor(), Actor(), Actor())
# Init planes from source
self._geometry_source = OrthogonalPlanesSource(**kwargs)
self._planes = self._geometry_source.output
self._plane_sources = self._geometry_source.sources
for actor, dataset in zip(self._plane_actors, self.planes):
actor.mapper = pv.DataSetMapper(dataset=dataset)
# Init label actors
self._axis_actors = (_AxisActor(), _AxisActor(), _AxisActor())
# Tempt init values for call to super class, will validate inputs later
self._label_offset = 0.05
self._label_edge = ('right', 'right', 'right')
self._label_position = 0.5, 0.5, 0.5
_XYZAssembly.__init__(
self,
xyz_actors=self._plane_actors,
xyz_label_actors=self._axis_actors,
x_label=x_label,
y_label=y_label,
z_label=z_label,
labels=labels,
label_color=label_color,
show_labels=show_labels,
label_position=label_position,
label_size=label_size,
x_color=x_color,
y_color=y_color,
z_color=z_color,
position=position,
orientation=orientation,
origin=origin,
scale=scale,
user_matrix=user_matrix,
name=name,
)
self.opacity = opacity # type: ignore[assignment]
self.label_mode = label_mode
self.label_offset = label_offset
self.label_edge = label_edge
# Set default properties
for actor in self._plane_actors:
prop = actor.prop
prop.show_edges = True
prop.line_width = 3
# Set default text properties
# TODO: implement set_text_prop() and use that instead
for label in self._label_actor_iterator:
prop = label.prop
prop.justification_vertical = 'center'
prop.justification_horizontal = 'center'
def __repr__(self):
"""Representation of the planes assembly."""
mat_info = 'Identity' if np.array_equal(self.user_matrix, np.eye(4)) else 'Set'
bnds = self.bounds
attr = [
f'{type(self).__name__} ({hex(id(self))})',
f' Resolution: {self._geometry_source.resolution}',
f' Normal sign: {self._geometry_source.normal_sign}',
f" X label: '{self.x_label}'",
f" Y label: '{self.y_label}'",
f" Z label: '{self.z_label}'",
f' Label color: {self.label_color}',
f' Show labels: {self.show_labels}',
f' Label position: {self.label_position}',
f' Label edge: {self.label_edge}',
f' Label offset: {self.label_offset}',
f" Label mode: '{self.label_mode}'",
f' X Color: {self.x_color}',
f' Y Color: {self.y_color}',
f' Z Color: {self.z_color}',
f' Position: {self.position}',
f' Orientation: {self.orientation}',
f' Origin: {self.origin}',
f' Scale: {self.scale}',
f' User matrix: {mat_info}',
f' X Bounds {bnds.x_min:.3E}, {bnds.x_max:.3E}',
f' Y Bounds {bnds.y_min:.3E}, {bnds.y_max:.3E}',
f' Z Bounds {bnds.z_min:.3E}, {bnds.z_max:.3E}',
]
return '\n'.join(attr)
@property
def labels(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
"""Return or set the labels for the planes.
This property may be used as an alternative to using :attr:`x_label`,
:attr:`y_label`, and :attr:`z_label` separately.
Examples
--------
>>> import pyvista as pv
>>> planes = pv.PlanesAssembly()
>>> planes.labels = ['Sagittal', 'Coronal', 'Transverse']
>>> planes.labels
('Sagittal', 'Coronal', 'Transverse')
"""
return self.x_label, self.y_label, self.z_label
@labels.setter
def labels(self, labels: list[str] | tuple[str, str, str]):
labels = _validate_label_sequence(labels, n_labels=3, name='labels')
self.x_label = labels[0]
self.y_label = labels[1]
self.z_label = labels[2]
@property
def x_label(self) -> str: # numpydoc ignore=RT01
"""Text label for the yz-plane.
Examples
--------
>>> import pyvista as pv
>>> planes = pv.PlanesAssembly()
>>> planes.x_label = 'This plane'
>>> planes.x_label
'This plane'
"""
return self._axis_actors[0].GetTitle()
@x_label.setter
def x_label(self, label: str):
self._axis_actors[0].SetTitle(label)
self.planes.set_block_name(0, label)
@property
def y_label(self) -> str: # numpydoc ignore=RT01
"""Text label for the zx-plane.
Examples
--------
>>> import pyvista as pv
>>> planes = pv.PlanesAssembly()
>>> planes.y_label = 'This plane'
>>> planes.y_label
'This plane'
"""
return self._axis_actors[1].GetTitle()
@y_label.setter
def y_label(self, label: str):
self._axis_actors[1].SetTitle(label)
self.planes.set_block_name(1, label)
@property
def z_label(self) -> str: # numpydoc ignore=RT01
"""Text label for the xy-plane.
Examples
--------
>>> import pyvista as pv
>>> planes = pv.PlanesAssembly()
>>> planes.z_label = 'This plane'
>>> planes.z_label
'This plane'
"""
return self._axis_actors[2].GetTitle()
@z_label.setter
def z_label(self, label: str):
self._axis_actors[2].SetTitle(label)
self.planes.set_block_name(2, label)
@property
def label_size(self) -> int: # numpydoc ignore=RT01
"""Size of the text labels.
Must be a positive integer.
"""
return self._label_size
@label_size.setter
def label_size(self, size: int):
valid_size = _validation.validate_number(
size,
must_be_in_range=[0, np.inf],
must_be_integer=True,
dtype_out=int,
name='label size',
)
self._label_size = valid_size
# 2D labels use font size (int) but 3D labels use a scaling factor (float)
# For 3D labels, we re-scale the text proportional to the planes assembly
# Values on the order of 0.01-0.05 seem to work best. Use a normalization
# factor so that input values are on the order of 10-50 and roughly match 2D sizes
NORM_FACTOR = 1000
scale_3d = self.planes.length * float(valid_size) / NORM_FACTOR
# In VTK 9.6+, the 3D label size depends on the 2D label size, so in the 3D case
# we need to reset the 2D font size to match the VTK default value of 12
font_size_2d = (
valid_size if hasattr(self, 'label_mode') and self.label_mode == '2D' else 12
)
for axis in self._axis_actors:
axis.GetTitleActor().SetScale(scale_3d) # 3D labels
axis.GetTitleTextProperty().SetFontSize(font_size_2d) # 2D labels
@property
def label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Normalized relative position of the text labels along the edge of each plane.
Labels are positioned relative to each plane's respective :attr:`label_edge`.
The positions are normalized to have a range of ``[-1.0, 1.0]`` such that ``0.0``
is at the center of the edge and ``-1.0`` and ``1.0`` are at the corners.
.. note::
The label text is centered horizontally at the specified positions.
Examples
--------
Position the labels at the center (along the edges) and plot the assembly.
>>> import pyvista as pv
>>> planes = pv.PlanesAssembly(label_position=0)
>>> planes.label_position
(0.0, 0.0, 0.0)
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
Position the labels at the corners.
>>> planes.label_position = 1.0
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
Vary the position of the labels independently for each plane. The values may be
negative and/or exceed a value of ``1.0``.
>>> planes.label_position = (-1.3, -1.0, -0.5)
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
"""
return self._label_position
@label_position.setter
def label_position(self, position: int | VectorLike[int]):
self._label_position = _validation.validate_array3(
position,
broadcast=True,
name='Label position',
dtype_out=float,
to_tuple=True,
)
self._update_label_positions()
@property
def label_edge(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
"""Edge on which to position each plane's label.
Edge can be ``'top'``,``'bottom'``,``'right'``, or ``'left'``, and can be
set independently for each plane or to the same edge for all planes.
The edge is relative to each plane's local ``i`` and ``j`` coordinates.
Examples
--------
Position the labels at the top edge and plot.
>>> import pyvista as pv
>>> planes = pv.PlanesAssembly(label_edge='top')
>>> planes.label_edge
('top', 'top', 'top')
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
Position the labels at the bottom.
>>> planes.label_edge = 'bottom'
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
Vary the edge of the labels independently for each plane.
>>> planes.label_edge = ('top', 'right', 'left')
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(planes)
>>> planes.camera = pl.camera
>>> pl.show()
"""
return self._label_edge
@label_edge.setter
def label_edge(self, edge: Literal['top', 'bottom', 'right', 'left'] | Sequence[str]):
valid_edge = (
[edge] * 3
if isinstance(edge, str)
else _validate_label_sequence(edge, n_labels=3, name='label edge')
)
for edge_ in valid_edge:
_validation.check_contains(
['top', 'bottom', 'right', 'left'],
must_contain=edge_,
name='label_edge',
)
self._label_edge = tuple(valid_edge)
self._update_label_positions()
@property
def label_offset(self) -> float: # numpydoc ignore=RT01
"""Vertical offset of the text labels.
The offset is proportional to the :attr:`~pyvista.Prop3D.length` of the assembly. Positive
values move the labels away from the center; negative values move them
towards it.
"""
return self._label_offset
@label_offset.setter
def label_offset(self, offset: float):
self._label_offset = _validation.validate_number(offset, dtype_out=float)
self._update_label_positions()
@property
def label_mode(self) -> Literal['2D', '3D']: # numpydoc ignore=RT01
"""Mode to use for text labels.
Mode must be either ``'2D'`` or ``'3D'``. In 2D mode, the label actors are
always visible and have a constant size regardless of window size. In 3D mode,
the label actors may be occluded by other geometry and will scale with changes
to the window size. The two modes also have minor differences in appearance as
well as behavior in terms of how they follow the camera.
"""
return self._label_mode
@label_mode.setter
def label_mode(self, mode: Literal['2D', '3D']):
_validation.check_contains(['2D', '3D'], must_contain=mode, name='label_mode')
self._label_mode = mode
use_2D = mode == '2D'
for axis in self._axis_actors:
axis.SetUse2DMode(use_2D)
# The 3D label size depends on the 2D label size so we need to reset this property
self.label_size = self.label_size
@property
def x_color(self) -> Color: # numpydoc ignore=RT01
"""Color of the yz-plane."""
return self._plane_actors[0].prop.color
@x_color.setter
def x_color(self, color: ColorLike):
self._plane_actors[0].prop.color = color
@property
def y_color(self) -> Color: # numpydoc ignore=RT01
"""Color of the zx-plane."""
return self._plane_actors[1].prop.color
@y_color.setter
def y_color(self, color: ColorLike):
self._plane_actors[1].prop.color = color
@property
def z_color(self) -> Color: # numpydoc ignore=RT01
"""Color of the xy-plane."""
return self._plane_actors[2].prop.color
@z_color.setter
def z_color(self, color: ColorLike):
self._plane_actors[2].prop.color = color
@property
def opacity(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Opacity of the planes."""
return self._opacity
@opacity.setter
def opacity(self, opacity: float):
valid_opacity = _validation.validate_array3(
opacity, broadcast=True, dtype_out=float, to_tuple=True
)
self._opacity = valid_opacity
for actor, opacity_ in zip(self._plane_actors, valid_opacity):
actor.prop.opacity = opacity_
@property
def camera(self): # numpydoc ignore=RT01
"""Camera to use for displaying the labels."""
return self._camera
@camera.setter
def camera(self, camera):
self._camera = camera
for axis in self._axis_actors:
axis.SetCamera(camera)
@property
def planes(self):
"""Get the orthogonal plane datasets of the assembly.
The planes are :class:`pyvista.PolyData` meshes stored as a
:class:`pyvista.MultiBlock`. The names of the blocks match the names of the
assembly's :attr:`labels`.
The planes are initially generated with :class:`pyvista.OrthogonalPlanesSource`.
Returns
-------
pyvista.MultiBlock
Composite mesh with three planes.
"""
return self._planes
def _update_label_positions(self):
axis_actors = self._axis_actors
plane_sources = self._plane_sources
transformation_matrix = self._transformation_matrix
def transform_point(point):
return (transformation_matrix @ (*point, 1))[:3]
def set_axis_location(plane_id, edge: str, position: float):
this_plane_source = plane_sources[plane_id]
this_axis_actor = axis_actors[plane_id]
# Get vectors which define the plane
origin, point1, point2 = (
np.array(this_plane_source.GetOrigin()),
np.array(this_plane_source.GetPoint1()),
np.array(this_plane_source.GetPoint2()),
)
vector1 = point1 - origin
vector2 = point2 - origin
# Define corners
corner_bottom_left = origin
corner_bottom_right = origin + vector1
corner_top_left = origin + vector2
corner_top_right = corner_bottom_right + vector2
# Define axis points in counter-clockwise order
if edge == 'top':
axis_point1, axis_point2 = corner_top_right, corner_top_left
elif edge == 'left':
axis_point1, axis_point2 = corner_top_left, corner_bottom_left
elif edge == 'bottom':
axis_point1, axis_point2 = corner_bottom_left, corner_bottom_right
else: # 'right'
axis_point1, axis_point2 = corner_bottom_right, corner_top_right
# Move axis to position along the edge
axis_vector = axis_point1 - axis_point2
# Define position relative to center of edge
position_vector = np.abs(axis_vector) * position * 0.5
axis_point1 += position_vector
axis_point2 += position_vector
# Add offset
axis_dir = axis_vector / np.linalg.norm(axis_vector)
offset_dir = np.cross(axis_dir, this_plane_source.GetNormal())
offset_dir = -1 * offset_dir / np.linalg.norm(offset_dir)
offset_mag = self.planes.length * self.label_offset
offset = offset_mag * offset_dir
axis_point1 += offset
axis_point2 += offset
# Set axis points
this_axis_actor.SetPoint1(transform_point(axis_point1))
this_axis_actor.SetPoint2(transform_point(axis_point2))
edge = self.label_edge
position = self.label_position
set_axis_location(0, edge[0], position[0])
set_axis_location(1, edge[1], position[1])
set_axis_location(2, edge[2], position[2])
def _post_set_update(self):
_XYZAssembly._post_set_update(self)
# Need to manually update axis actors
self._update_label_positions()
class _AxisActor(_vtk.DisableVtkSnakeCase, _vtk.vtkAxisActor):
def __init__(self):
super().__init__()
# Only show the title
self.TitleVisibilityOn()
self.MinorTicksVisibleOff()
self.TickVisibilityOff()
self.DrawGridlinesOff()
self.AxisVisibilityOff() # Turn this on for debugging
# Set empty tick labels
labels = _vtk.vtkStringArray()
labels.SetNumberOfTuples(0)
# labels.SetValue(0, "")
self.SetLabels(labels)
# Ignore the axis bounds when rendering. Otherwise, the bounds must be
# set with SetBounds() every time the axis is updated
self.SetUseBounds(False)
# Format title positioning
offset = (0,) if pv.vtk_version_info < (9, 3) else (0, 0)
self.SetTitleOffset(*offset)
self.SetLabelOffset(0)
# For 2D mode only
self.SetVerticalOffsetXTitle2D(0)
self.SetHorizontalOffsetYTitle2D(0)
text_prop = TextProperty()
text_prop.justification_vertical = 'center'
self.SetTitleTextProperty(text_prop)
self.GetTitleActor()
# For 3D mode only
self.GetProperty().SetLighting(False)
@property
def prop(self) -> TextProperty:
return self.GetTitleTextProperty()