Files
geMoldInsight/venv/lib/python3.11/site-packages/pyvista/plotting/text.py
T
2026-02-12 23:27:48 +08:00

875 lines
24 KiB
Python

"""Contains the pyvista.Text class."""
from __future__ import annotations
import pathlib
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Literal
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
from pyvista.core._typing_core import BoundsTuple
from pyvista.core.utilities.misc import _check_range
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .colors import Color
from .prop3d import _Prop3DMixin
from .themes import Theme
from .tools import FONTS
if TYPE_CHECKING:
from collections.abc import Sequence
from pyvista.core._typing_core import VectorLike
from ._typing import ColorLike
HorizontalOptions = Literal['left', 'center', 'right']
VerticalOptions = Literal['bottom', 'center', 'top']
class CornerAnnotation(
_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _NameMixin, _vtk.vtkCornerAnnotation
):
"""Text annotation in four corners.
This is an annotation object that manages four text actors / mappers to provide
annotation in the four corners of a viewport.
Parameters
----------
position : str | bool
Position of the text.
text : str
Text input.
prop : pyvista.TextProperty, optional
Text property.
linear_font_scale_factor : float, optional
Linear font scale factor.
name : str, optional
The name of this actor used when tracking on a plotter.
.. versionadded:: 0.45
Examples
--------
Create text annotation in four corners.
>>> from pyvista import CornerAnnotation
>>> text = CornerAnnotation(0, 'text')
>>> prop = text.prop
"""
@_deprecate_positional_args(allowed=['position', 'text'])
def __init__( # noqa: PLR0917
self, position, text, prop=None, linear_font_scale_factor=None, name=None
):
"""Initialize a new text annotation descriptor."""
super().__init__()
self.set_text(position, text)
if prop is None:
self.prop = TextProperty()
if linear_font_scale_factor is not None:
self.linear_font_scale_factor = linear_font_scale_factor
self._name = name
def get_text(self, position):
"""Get the text to be displayed for each corner.
Parameters
----------
position : str | bool
Position of the text.
Returns
-------
str
Text to be displayed for each corner.
"""
return self.GetText(position)
def set_text(self, position, text):
"""Set the text to be displayed for each corner.
Parameters
----------
position : str | bool
Position of the text.
text : str
Text to be displayed for each corner.
"""
corner_mappings = {
'lower_left': self.LowerLeft,
'lower_right': self.LowerRight,
'upper_left': self.UpperLeft,
'upper_right': self.UpperRight,
'lower_edge': self.LowerEdge,
'upper_edge': self.UpperEdge,
'left_edge': self.LeftEdge,
'right_edge': self.RightEdge,
}
corner_mappings['ll'] = corner_mappings['lower_left']
corner_mappings['lr'] = corner_mappings['lower_right']
corner_mappings['ul'] = corner_mappings['upper_left']
corner_mappings['ur'] = corner_mappings['upper_right']
corner_mappings['top'] = corner_mappings['upper_edge']
corner_mappings['bottom'] = corner_mappings['lower_edge']
corner_mappings['right'] = corner_mappings['right_edge']
corner_mappings['r'] = corner_mappings['right_edge']
corner_mappings['left'] = corner_mappings['left_edge']
corner_mappings['l'] = corner_mappings['left_edge']
if isinstance(position, str):
position = corner_mappings[position]
elif position is True:
position = corner_mappings['upper_left']
self.SetText(position, text)
@property
def prop(self) -> TextProperty:
"""Property of this actor.
Returns
-------
pyvista.TextProperty
Property of this actor.
"""
return self.GetTextProperty()
@prop.setter
def prop(self, prop: TextProperty):
self.SetTextProperty(prop)
@property
def linear_font_scale_factor(self) -> float:
"""Font scaling factors.
Returns
-------
float
Font scaling factors.
"""
return self.GetLinearFontScaleFactor()
@linear_font_scale_factor.setter
def linear_font_scale_factor(self, factor: float):
self.SetLinearFontScaleFactor(factor)
class Text(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _NameMixin, _vtk.vtkTextActor):
r"""Define text by default theme.
Parameters
----------
text : str, optional
Text string to be displayed.
"\n" is recognized as a carriage return/linefeed (line separator).
The characters must be in the UTF-8 encoding.
position : Sequence[float], optional
The position coordinate.
prop : pyvista.TextProperty, optional
The property of this actor.
name : str, optional
The name of this actor used when tracking on a plotter.
.. versionadded:: 0.45
Examples
--------
Create a text with text's property.
>>> from pyvista import Text
>>> text = Text()
>>> prop = text.prop
"""
@_deprecate_positional_args(allowed=['text'])
def __init__( # noqa: PLR0917
self, text=None, position=None, prop=None, name=None
):
"""Initialize a new text descriptor."""
super().__init__()
if text is not None:
self.input = text
if position is not None:
self.position = position
if prop is None:
self.prop = TextProperty()
self._name = name
@property
def input(self):
r"""Text string to be displayed.
Returns
-------
str
Text string to be displayed.
"\n" is recognized as a carriage return/linefeed (line separator).
The characters must be in the UTF-8 encoding.
"""
return self.GetInput()
@input.setter
def input(self, text: str):
self.SetInput(text)
@property
def prop(self):
"""Property of this actor.
Returns
-------
pyvista.TextProperty
Property of this actor.
"""
return self.GetTextProperty()
@prop.setter
def prop(self, prop: TextProperty):
self.SetTextProperty(prop)
@property
def position(self):
"""Position coordinate.
Returns
-------
Sequence[float]
Position coordinate.
"""
return self.GetPosition()
@position.setter
def position(self, position: Sequence[float]):
self.SetPosition(position[0], position[1])
class Label(_Prop3DMixin, Text):
"""2D label actor with a 3D position coordinate.
Unlike :class:`~pyvista.Text`, which uses 2D viewport coordinates to position text
in a plot, this class instead uses a 3D position coordinate. This class may be
positioned, oriented, and transformed in a manner similar to a 3D
:class:`~pyvista.Actor`.
In addition, this class supports an additional :attr:`relative_position` attribute.
In general, it is recommended to simply use :attr:`~pyvista.Prop3D.position` when positioning a
:class:`Label` by itself. However, if the position of the label depends on the
positioning of another actor, both :attr:`~pyvista.Prop3D.position` and
:attr:`relative_position` may be used together.
In these cases, the :attr:`~pyvista.Prop3D.position` of the label and actor
should be kept in-sync. See the examples below.
Parameters
----------
text : str, optional
Text string to be displayed.
position : VectorLike[float]
Position of the text in XYZ coordinates.
relative_position : VectorLike[float]
Position of the text in XYZ coordinates relative to its :attr:`~pyvista.Prop3D.position`.
size : int
Size of the text label.
prop : pyvista.TextProperty, optional
The property of this actor.
name : str, optional
The name of this actor used when tracking on a plotter.
.. versionadded:: 0.45
See Also
--------
pyvista.Plotter.add_point_labels
Examples
--------
Create a label for a point of interest. Here we add a label to the tip of a cone.
>>> import pyvista as pv
>>> cone_dataset = pv.Cone()
>>> tip = (0.5, 0, 0)
>>> label = pv.Label('tip', position=tip)
Plot the mesh and label.
>>> pl = pv.Plotter()
>>> cone_actor = pl.add_mesh(cone_dataset)
>>> _ = pl.add_actor(label)
>>> pl.show()
The previous example set the label's position as the cone's tip explicitly.
However, this means that the two actors now have different positions.
>>> cone_actor.position
(0.0, 0.0, 0.0)
>>> label.position
(0.5, 0.0, 0.0)
And if we change the 3D orientation of the cone and label, the label is no longer
positioned at the tip.
>>> cone_actor.orientation = 0, 0, 90
>>> label.orientation = 0, 0, 90
>>>
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(cone_actor)
>>> _ = pl.add_actor(label)
>>> pl.show()
This is because rotations by :class:`pyvista.Prop3D` are applied **before** the
actor is moved to its final position, and therefore the label's position is not
considered in the rotation. Hence, the final position of the label remains at
``(0.5, 0.0, 0.0)`` as it did earlier, despite changing its orientation.
If we want the position of the label to have the same positioning *relative* to the
cone, we can instead set its :attr:`relative_position`.
First, reset the label's position to match the cone's position.
>>> label.position = cone_actor.position
>>> label.position
(0.0, 0.0, 0.0)
Now set its :attr:`relative_position` to the tip of the cone.
>>> label.relative_position = tip
>>> label.relative_position
(0.5, 0.0, 0.0)
Plot the results. The label is now correctly positioned at the tip of the cone.
This is because the :attr:`relative_position` is considered as part of the
rotation.
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(cone_actor)
>>> _ = pl.add_actor(label)
>>> pl.show()
As long as the label and cone's :class:`pyvista.Prop3D` attributes are modified
together and synchronized, the label will remain at the tip of the cone.
Modify the position of the label and tip.
>>> cone_actor.position = (1.0, 2.0, 3.0)
>>> label.position = (1.0, 2.0, 3.0)
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(cone_actor)
>>> _ = pl.add_actor(label)
>>> _ = pl.add_axes_at_origin()
>>> pl.show()
"""
def __init__(
self,
text: str | None = None,
position: VectorLike[float] = (0.0, 0.0, 0.0),
relative_position: VectorLike[float] = (0.0, 0.0, 0.0),
*,
size: int = 50,
prop: pyvista.Property | None = None,
name: str = 'Label',
):
Text.__init__(self, text=text, prop=prop)
self.GetPositionCoordinate().SetCoordinateSystemToWorld()
self.SetTextScaleModeToNone() # Use font size to control size of text
self._name = name
_Prop3DMixin.__init__(self)
self.relative_position = relative_position
self.position = position
self.size = size
@property
def _label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the label in xyz space.
This is the "true" position of the label. Internally this is loosely
equal to :attr:`~pyvista.Prop3D.position` + :attr:`relative_position`.
"""
return self.GetPositionCoordinate().GetValue()
@_label_position.setter
def _label_position(self, position: VectorLike[float]):
valid_position = _validation.validate_array3(position)
self.GetPositionCoordinate().SetValue(valid_position)
@property
def size(self) -> int: # numpydoc ignore=RT01
"""Size of the text label.
Notes
-----
The text property's font size used to control the size of the label.
"""
return self.prop.font_size
@size.setter
def size(self, size: int):
self.prop.font_size = size
@property
def relative_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the label relative to its :attr:`~pyvista.Prop3D.position`."""
return tuple(self._relative_position.tolist())
@relative_position.setter
def relative_position(self, position: VectorLike[float]):
self._relative_position = _validation.validate_array3(position, dtype_out=float)
self._post_set_update()
def _post_set_update(self):
# Update the label's underlying text position
matrix4x4 = self._transformation_matrix
vector4 = (*self.relative_position, 1)
new_position = (matrix4x4 @ vector4)[:3]
self._label_position = new_position
def _get_bounds(self) -> BoundsTuple:
# Define its 3D position as its bounds
x, y, z = self._label_position
return BoundsTuple(x, x, y, y, z, z)
class TextProperty(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkTextProperty):
"""Define text's property.
Parameters
----------
theme : pyvista.plotting.themes.Theme, optional
Plot-specific theme.
color : pyvista.ColorLike, optional
Either a string, RGB list, or hex color string. For example:
``color='white'``, ``color='w'``, ``color=[1.0, 1.0, 1.0]``, or
``color='#FFFFFF'``. Color will be overridden if scalars are
specified.
font_family : str | None, optional
Font family or None.
orientation : float, optional
Text's orientation (in degrees).
font_size : int, optional
Font size.
font_file : str, optional
Font file path.
shadow : bool, optional
If enable the shadow.
justification_horizontal : str, optional
Text's horizontal justification.
Should be either "left", "center" or "right".
justification_vertical : str, optional
Text's vertical justification.
Should be either "bottom", "center" or "top".
italic : bool, default: False
Italicises title and bar labels.
bold : bool, default: True
Bolds title and bar labels.
background_color : pyvista.Color, optional
Background color of text.
background_opacity : pyvista.Color, optional
Background opacity of text.
Examples
--------
Create a text's property.
>>> from pyvista import TextProperty
>>> prop = TextProperty()
>>> prop.opacity = 0.5
>>> prop.background_color = 'b'
>>> prop.background_opacity = 0.5
>>> prop.show_frame = True
>>> prop.frame_color = 'b'
>>> prop.frame_width = 10
>>> prop.frame_color
Color(name='blue', hex='#0000ffff', opacity=255)
"""
_theme = Theme()
_color_set = None
_background_color_set = None
_font_family = None
@_deprecate_positional_args(allowed=['theme'])
def __init__( # noqa: PLR0917
self,
theme=None,
color=None,
font_family=None,
orientation=None,
font_size=None,
font_file=None,
shadow: bool = False, # noqa: FBT001, FBT002
justification_horizontal=None,
justification_vertical=None,
italic: bool = False, # noqa: FBT001, FBT002
bold: bool = False, # noqa: FBT001, FBT002
background_color=None,
background_opacity=None,
):
"""Initialize text's property."""
super().__init__()
if theme is None:
# copy global theme to ensure local property theme is fixed
# after creation.
self._theme.load_theme(pyvista.global_theme)
else:
self._theme.load_theme(theme)
self.color = color
self.font_family = font_family
if orientation is not None:
self.orientation = orientation
if font_size is not None:
self.font_size = font_size
if font_file is not None:
self.set_font_file(font_file)
if shadow:
self.enable_shadow()
if justification_horizontal is not None:
self.justification_horizontal = justification_horizontal
if justification_vertical is not None:
self.justification_vertical = justification_vertical
self.italic = italic
self.bold = bold
if background_color is not None:
self.background_color = background_color
if background_opacity is not None:
self.background_opacity = background_opacity
@property
def color(self) -> Color:
"""Color of text's property.
Returns
-------
pyvista.Color
Color of text's property.
"""
return Color(self.GetColor())
@color.setter
def color(self, color: ColorLike):
self._color_set = color is not None
rgb_color = Color(color, default_color=self._theme.font.color)
self.SetColor(rgb_color.float_rgb)
@property
def opacity(self) -> float:
"""Opacity of text's property.
Returns
-------
float
Opacity of the text. A single float value that will be applied globally
opacity of the text and uniformly applied everywhere. Between 0 and 1.
"""
return self.GetOpacity()
@opacity.setter
def opacity(self, opacity: float):
_check_range(opacity, (0, 1), 'opacity')
self.SetOpacity(opacity)
@property
def background_color(self) -> Color:
"""Background color of text's property.
Returns
-------
pyvista.Color
Background color of text's property.
"""
return Color(self.GetBackgroundColor())
@background_color.setter
def background_color(self, color: ColorLike):
self._background_color_set = color is not None
rgb_color = Color(color)
self.SetBackgroundColor(rgb_color.float_rgb)
@property
def background_opacity(self) -> float:
"""Background opacity of text's property.
Returns
-------
float
Background opacity of the text. A single float value that will be applied globally.
Background opacity of the text and uniformly applied everywhere. Between 0 and 1.
"""
return self.GetBackgroundOpacity()
@background_opacity.setter
def background_opacity(self, opacity: float):
_check_range(opacity, (0, 1), 'background_opacity')
self.SetBackgroundOpacity(opacity)
@property
def show_frame(self) -> bool:
"""Visibility of frame.
Returns
-------
bool:
If shows the frame.
"""
return bool(self.GetFrame())
@show_frame.setter
def show_frame(self, frame: bool):
self.SetFrame(frame)
@property
def frame_color(self) -> Color:
"""Frame color of text property.
Returns
-------
pyvista.Color
Frame color of text property.
"""
return Color(self.GetFrameColor())
@frame_color.setter
def frame_color(self, color):
self.SetFrameColor(Color(color).float_rgb)
@property
def frame_width(self) -> int:
"""Width of the frame.
Returns
-------
int
Width of the frame. The width is expressed in pixels.
The default is 1 pixel.
"""
return self.GetFrameWidth()
@frame_width.setter
def frame_width(self, width: int):
self.SetFrameWidth(width)
@property
def font_family(self) -> str | None:
"""Font family.
Returns
-------
str | None
Font family or None.
"""
return self._font_family
@font_family.setter
def font_family(self, font_family: str | None):
if font_family is None:
font_family = self._theme.font.family
self._font_family = font_family
self.SetFontFamily(FONTS[self._font_family].value)
@property
def font_size(self) -> int:
"""Font size.
Returns
-------
int
Font size.
"""
return self.GetFontSize()
@font_size.setter
def font_size(self, font_size: int):
self.SetFontSize(font_size)
def enable_shadow(self) -> None:
"""Enable the shadow."""
self.SetShadow(True)
@property
def orientation(self) -> float:
"""Text's orientation (in degrees).
Returns
-------
float
Text's orientation (in degrees).
"""
return self.GetOrientation()
@orientation.setter
def orientation(self, orientation: float):
self.SetOrientation(orientation)
def set_font_file(self, font_file: str):
"""Set the font file.
Parameters
----------
font_file : str
Font file path.
"""
path = pathlib.Path(font_file)
path = path.resolve()
if not Path(path).is_file():
msg = f'Unable to locate {path}'
raise FileNotFoundError(msg)
self.SetFontFamily(_vtk.VTK_FONT_FILE)
self.SetFontFile(str(path))
@property
def justification_horizontal(self) -> str:
"""Text's justification horizontal.
Returns
-------
str
Text's horizontal justification.
Should be either "left", "center" or "right".
"""
justification = self.GetJustificationAsString().lower()
if justification == 'centered':
justification = 'center'
return justification
@justification_horizontal.setter
def justification_horizontal(self, justification: str):
if justification.lower() == 'left':
self.SetJustificationToLeft()
elif justification.lower() == 'center':
self.SetJustificationToCentered()
elif justification.lower() == 'right':
self.SetJustificationToRight()
else:
msg = (
f'Invalid {justification} for justification_horizontal. '
'Should be either "left", "center" or "right".'
)
raise ValueError(msg)
@property
def justification_vertical(self) -> str:
"""Text's vertical justification.
Returns
-------
str
Text's vertical justification.
Should be either "bottom", "center" or "top".
"""
justification = self.GetVerticalJustificationAsString().lower()
if justification == 'centered':
justification = 'center'
return justification
@justification_vertical.setter
def justification_vertical(self, justification: str):
if justification.lower() == 'bottom':
self.SetVerticalJustificationToBottom()
elif justification.lower() == 'center':
self.SetVerticalJustificationToCentered()
elif justification.lower() == 'top':
self.SetVerticalJustificationToTop()
else:
msg = (
f'Invalid {justification} for justification_vertical. '
'Should be either "bottom", "center" or "top".'
)
raise ValueError(msg)
@property
def italic(self) -> bool:
"""Italic of text's property.
Returns
-------
bool
If text is italic.
"""
return bool(self.GetItalic())
@italic.setter
def italic(self, italic: bool):
self.SetItalic(italic)
@property
def bold(self) -> bool:
"""Bold of text's property.
Returns
-------
bool
If text is bold.
"""
return bool(self.GetBold())
@bold.setter
def bold(self, bold: bool):
self.SetBold(bold)
def shallow_copy(self, to_copy: TextProperty) -> None:
"""Create a shallow copy of the text's property.
Parameters
----------
to_copy : pyvista.TextProperty
Text's property to copy from.
"""
self.ShallowCopy(to_copy)