781 lines
24 KiB
Python
781 lines
24 KiB
Python
"""Module containing useful plotting tools."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
import os
|
|
import platform
|
|
import subprocess
|
|
from subprocess import PIPE
|
|
from subprocess import Popen
|
|
from subprocess import TimeoutExpired
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
import pyvista
|
|
from pyvista._deprecate_positional_args import _deprecate_positional_args
|
|
|
|
from . import _vtk
|
|
from .colors import Color
|
|
|
|
|
|
class FONTS(Enum):
|
|
"""Font families available to PyVista."""
|
|
|
|
arial = _vtk.VTK_ARIAL
|
|
courier = _vtk.VTK_COURIER
|
|
times = _vtk.VTK_TIMES
|
|
|
|
|
|
# Track render window support and plotting
|
|
SUPPORTS_OPENGL = None
|
|
SUPPORTS_PLOTTING = None
|
|
|
|
|
|
def supports_open_gl():
|
|
"""Return if the system supports OpenGL.
|
|
|
|
This function checks if the system supports OpenGL by creating a VTK render
|
|
window and querying its OpenGL support.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if the system supports OpenGL, ``False`` otherwise.
|
|
|
|
"""
|
|
global SUPPORTS_OPENGL # noqa: PLW0603
|
|
if SUPPORTS_OPENGL is None:
|
|
ren_win = _vtk.vtkRenderWindow()
|
|
SUPPORTS_OPENGL = bool(ren_win.SupportsOpenGL())
|
|
return SUPPORTS_OPENGL
|
|
|
|
|
|
def _system_supports_plotting(): # noqa: PLR0911
|
|
"""Check if the environment supports plotting on Windows, Linux, or Mac OS.
|
|
|
|
Returns
|
|
-------
|
|
system_supports_plotting : bool
|
|
``True`` when system supports plotting.
|
|
|
|
"""
|
|
if os.environ.get('ALLOW_PLOTTING', '').lower() == 'true':
|
|
return True
|
|
|
|
# Windows case
|
|
if os.name == 'nt':
|
|
# actually have to check here. Somewhat expensive.
|
|
return supports_open_gl()
|
|
|
|
# mac case
|
|
if platform.system() == 'Darwin':
|
|
# check if finder available
|
|
proc = Popen(['pgrep', '-qx', 'Finder'], stdout=PIPE, stderr=PIPE, encoding='utf8')
|
|
try:
|
|
proc.communicate(timeout=10)
|
|
except TimeoutExpired:
|
|
return False
|
|
if proc.returncode == 0:
|
|
return True
|
|
|
|
# display variable set, likely available
|
|
return 'DISPLAY' in os.environ
|
|
|
|
# Linux case
|
|
try:
|
|
proc = Popen(['xset', '-q'], stdout=PIPE, stderr=PIPE, encoding='utf8')
|
|
proc.communicate(timeout=10)
|
|
except (OSError, TimeoutExpired):
|
|
return False
|
|
else: # pragma: no cover
|
|
return proc.returncode == 0
|
|
|
|
|
|
def system_supports_plotting():
|
|
"""Check if the environment supports plotting.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` when system supports plotting.
|
|
|
|
"""
|
|
global SUPPORTS_PLOTTING # noqa: PLW0603
|
|
if SUPPORTS_PLOTTING is None:
|
|
SUPPORTS_PLOTTING = _system_supports_plotting()
|
|
|
|
# always use the cached response
|
|
return SUPPORTS_PLOTTING
|
|
|
|
|
|
def _update_axes_label_color(axes_actor, color=None):
|
|
"""Set the axes label color (internal helper)."""
|
|
color = Color(color, default_color=pyvista.global_theme.font.color)
|
|
if isinstance(axes_actor, _vtk.vtkAxesActor):
|
|
prop_x = axes_actor.GetXAxisCaptionActor2D().GetCaptionTextProperty()
|
|
prop_y = axes_actor.GetYAxisCaptionActor2D().GetCaptionTextProperty()
|
|
prop_z = axes_actor.GetZAxisCaptionActor2D().GetCaptionTextProperty()
|
|
for prop in [prop_x, prop_y, prop_z]:
|
|
prop.SetColor(color.float_rgb)
|
|
prop.SetShadow(False)
|
|
elif isinstance(axes_actor, _vtk.vtkAnnotatedCubeActor):
|
|
axes_actor.GetTextEdgesProperty().SetColor(color.float_rgb)
|
|
|
|
|
|
@_deprecate_positional_args
|
|
def create_axes_marker( # noqa: PLR0917
|
|
label_color=None,
|
|
x_color=None,
|
|
y_color=None,
|
|
z_color=None,
|
|
xlabel='X',
|
|
ylabel='Y',
|
|
zlabel='Z',
|
|
labels_off: bool = False, # noqa: FBT001, FBT002
|
|
line_width=2,
|
|
cone_radius=0.4,
|
|
shaft_length=0.8,
|
|
tip_length=0.2,
|
|
ambient=0.5,
|
|
label_size=(0.25, 0.1),
|
|
) -> _vtk.vtkAxesActor:
|
|
"""Create an axis actor.
|
|
|
|
Parameters
|
|
----------
|
|
label_color : ColorLike, optional
|
|
Color of the label text.
|
|
|
|
x_color : ColorLike, optional
|
|
Color of the x-axis text.
|
|
|
|
y_color : ColorLike, optional
|
|
Color of the y-axis text.
|
|
|
|
z_color : ColorLike, optional
|
|
Color of the z-axis text.
|
|
|
|
xlabel : str, default: "X"
|
|
Text used for the x-axis.
|
|
|
|
ylabel : str, default: "Y"
|
|
Text used for the y-axis.
|
|
|
|
zlabel : str, default: "Z"
|
|
Text used for the z-axis.
|
|
|
|
labels_off : bool, default: False
|
|
Enable or disable the text labels for the axes.
|
|
|
|
line_width : float, default: 2
|
|
The width of the marker lines.
|
|
|
|
cone_radius : float, default: 0.4
|
|
The radius of the axes arrow tips.
|
|
|
|
shaft_length : float, default: 0.8
|
|
The length of the axes arrow shafts.
|
|
|
|
tip_length : float, default: 0.2
|
|
Length of the tip.
|
|
|
|
ambient : float, default: 0.5
|
|
The ambient of the axes arrows. Value should be between 0 and 1.
|
|
|
|
label_size : sequence[float], default: (0.25, 0.1)
|
|
The width and height of the axes label actors. Values should be between
|
|
0 and 1. For example ``(0.2, 0.1)``.
|
|
|
|
Returns
|
|
-------
|
|
:vtk:`vtkAxesActor`
|
|
Axes actor.
|
|
|
|
Examples
|
|
--------
|
|
Create the default axes marker.
|
|
|
|
>>> import pyvista as pv
|
|
>>> marker = pv.create_axes_marker()
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(marker)
|
|
>>> pl.show()
|
|
|
|
Create an axes marker at the origin with custom colors and axis labels.
|
|
|
|
>>> import pyvista as pv
|
|
>>> marker = pv.create_axes_marker(
|
|
... line_width=4,
|
|
... ambient=0.0,
|
|
... x_color='#378df0',
|
|
... y_color='#ab2e5d',
|
|
... z_color='#f7fb9a',
|
|
... xlabel='X Axis',
|
|
... ylabel='Y Axis',
|
|
... zlabel='Z Axis',
|
|
... label_size=(0.1, 0.1),
|
|
... )
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(marker)
|
|
>>> pl.show()
|
|
|
|
"""
|
|
x_color = Color(x_color, default_color=pyvista.global_theme.axes.x_color)
|
|
y_color = Color(y_color, default_color=pyvista.global_theme.axes.y_color)
|
|
z_color = Color(z_color, default_color=pyvista.global_theme.axes.z_color)
|
|
axes_actor = _vtk.vtkAxesActor()
|
|
axes_actor.GetXAxisShaftProperty().SetColor(x_color.float_rgb)
|
|
axes_actor.GetXAxisTipProperty().SetColor(x_color.float_rgb)
|
|
axes_actor.GetYAxisShaftProperty().SetColor(y_color.float_rgb)
|
|
axes_actor.GetYAxisTipProperty().SetColor(y_color.float_rgb)
|
|
axes_actor.GetZAxisShaftProperty().SetColor(z_color.float_rgb)
|
|
axes_actor.GetZAxisTipProperty().SetColor(z_color.float_rgb)
|
|
# Set labels
|
|
axes_actor.SetXAxisLabelText(xlabel)
|
|
axes_actor.SetYAxisLabelText(ylabel)
|
|
axes_actor.SetZAxisLabelText(zlabel)
|
|
if labels_off:
|
|
axes_actor.AxisLabelsOff()
|
|
# Set Line width
|
|
axes_actor.GetXAxisShaftProperty().SetLineWidth(line_width)
|
|
axes_actor.GetYAxisShaftProperty().SetLineWidth(line_width)
|
|
axes_actor.GetZAxisShaftProperty().SetLineWidth(line_width)
|
|
|
|
axes_actor.SetConeRadius(cone_radius)
|
|
axes_actor.SetNormalizedShaftLength([shaft_length] * 3)
|
|
axes_actor.SetNormalizedTipLength([tip_length] * 3)
|
|
axes_actor.GetXAxisShaftProperty().SetAmbient(ambient)
|
|
axes_actor.GetYAxisShaftProperty().SetAmbient(ambient)
|
|
axes_actor.GetZAxisShaftProperty().SetAmbient(ambient)
|
|
axes_actor.GetXAxisTipProperty().SetAmbient(ambient)
|
|
axes_actor.GetYAxisTipProperty().SetAmbient(ambient)
|
|
axes_actor.GetZAxisTipProperty().SetAmbient(ambient)
|
|
|
|
for label_actor in [
|
|
axes_actor.GetXAxisCaptionActor2D(),
|
|
axes_actor.GetYAxisCaptionActor2D(),
|
|
axes_actor.GetZAxisCaptionActor2D(),
|
|
]:
|
|
label_actor.SetWidth(label_size[0])
|
|
label_actor.SetHeight(label_size[1])
|
|
|
|
_update_axes_label_color(axes_actor, label_color)
|
|
|
|
return axes_actor
|
|
|
|
|
|
@_deprecate_positional_args
|
|
def create_axes_orientation_box( # noqa: PLR0917
|
|
line_width=1,
|
|
text_scale=0.366667,
|
|
edge_color='black',
|
|
x_color=None,
|
|
y_color=None,
|
|
z_color=None,
|
|
xlabel='X',
|
|
ylabel='Y',
|
|
zlabel='Z',
|
|
x_face_color='red',
|
|
y_face_color='green',
|
|
z_face_color='blue',
|
|
color_box: bool = False, # noqa: FBT001, FBT002
|
|
label_color=None,
|
|
labels_off: bool = False, # noqa: FBT001, FBT002
|
|
opacity=0.5,
|
|
show_text_edges: bool = False, # noqa: FBT001, FBT002
|
|
):
|
|
"""Create a Box axes orientation widget with labels.
|
|
|
|
Parameters
|
|
----------
|
|
line_width : float, optional
|
|
The width of the marker lines.
|
|
|
|
text_scale : float, optional
|
|
Size of the text relative to the faces.
|
|
|
|
edge_color : ColorLike, optional
|
|
Color of the edges.
|
|
|
|
x_color : ColorLike, optional
|
|
Color of the x-axis text.
|
|
|
|
y_color : ColorLike, optional
|
|
Color of the y-axis text.
|
|
|
|
z_color : ColorLike, optional
|
|
Color of the z-axis text.
|
|
|
|
xlabel : str, optional
|
|
Text used for the x-axis.
|
|
|
|
ylabel : str, optional
|
|
Text used for the y-axis.
|
|
|
|
zlabel : str, optional
|
|
Text used for the z-axis.
|
|
|
|
x_face_color : ColorLike, optional
|
|
Color used for the x-axis arrow. Defaults to theme axes
|
|
parameters.
|
|
|
|
y_face_color : ColorLike, optional
|
|
Color used for the y-axis arrow. Defaults to theme axes
|
|
parameters.
|
|
|
|
z_face_color : ColorLike, optional
|
|
Color used for the z-axis arrow. Defaults to theme axes
|
|
parameters.
|
|
|
|
color_box : bool, optional
|
|
Enable or disable the face colors. Otherwise, box is white.
|
|
|
|
label_color : ColorLike, optional
|
|
Color of the labels.
|
|
|
|
labels_off : bool, optional
|
|
Enable or disable the text labels for the axes.
|
|
|
|
opacity : float, optional
|
|
Opacity in the range of ``[0, 1]`` of the orientation box.
|
|
|
|
show_text_edges : bool, optional
|
|
Enable or disable drawing the vector text edges.
|
|
|
|
Returns
|
|
-------
|
|
:vtk:`vtkAnnotatedCubeActor`
|
|
Annotated cube actor.
|
|
|
|
Examples
|
|
--------
|
|
Create and plot an orientation box
|
|
|
|
>>> import pyvista as pv
|
|
>>> actor = pv.create_axes_orientation_box(
|
|
... line_width=1,
|
|
... text_scale=0.53,
|
|
... edge_color='black',
|
|
... x_color='k',
|
|
... y_color=None,
|
|
... z_color=None,
|
|
... xlabel='X',
|
|
... ylabel='Y',
|
|
... zlabel='Z',
|
|
... color_box=False,
|
|
... labels_off=False,
|
|
... opacity=1.0,
|
|
... )
|
|
>>> pl = pv.Plotter()
|
|
>>> _ = pl.add_actor(actor)
|
|
>>> pl.show()
|
|
|
|
"""
|
|
x_color = Color(x_color, default_color=pyvista.global_theme.axes.x_color)
|
|
y_color = Color(y_color, default_color=pyvista.global_theme.axes.y_color)
|
|
z_color = Color(z_color, default_color=pyvista.global_theme.axes.z_color)
|
|
edge_color = Color(edge_color, default_color=pyvista.global_theme.edge_color)
|
|
x_face_color = Color(x_face_color)
|
|
y_face_color = Color(y_face_color)
|
|
z_face_color = Color(z_face_color)
|
|
axes_actor = _vtk.vtkAnnotatedCubeActor()
|
|
axes_actor.SetFaceTextScale(text_scale)
|
|
if xlabel is not None:
|
|
axes_actor.SetXPlusFaceText(f'+{xlabel}')
|
|
axes_actor.SetXMinusFaceText(f'-{xlabel}')
|
|
if ylabel is not None:
|
|
axes_actor.SetYPlusFaceText(f'+{ylabel}')
|
|
axes_actor.SetYMinusFaceText(f'-{ylabel}')
|
|
if zlabel is not None:
|
|
axes_actor.SetZPlusFaceText(f'+{zlabel}')
|
|
axes_actor.SetZMinusFaceText(f'-{zlabel}')
|
|
axes_actor.SetFaceTextVisibility(not labels_off)
|
|
axes_actor.SetTextEdgesVisibility(show_text_edges)
|
|
# https://github.com/pyvista/pyvista/pull/5382
|
|
# axes_actor.GetTextEdgesProperty().SetColor(edge_color.float_rgb)
|
|
axes_actor.GetTextEdgesProperty().SetLineWidth(line_width)
|
|
axes_actor.GetXPlusFaceProperty().SetColor(x_color.float_rgb)
|
|
axes_actor.GetXMinusFaceProperty().SetColor(x_color.float_rgb)
|
|
axes_actor.GetYPlusFaceProperty().SetColor(y_color.float_rgb)
|
|
axes_actor.GetYMinusFaceProperty().SetColor(y_color.float_rgb)
|
|
axes_actor.GetZPlusFaceProperty().SetColor(z_color.float_rgb)
|
|
axes_actor.GetZMinusFaceProperty().SetColor(z_color.float_rgb)
|
|
|
|
axes_actor.GetCubeProperty().SetOpacity(opacity)
|
|
axes_actor.GetCubeProperty().SetEdgeColor(edge_color.float_rgb)
|
|
axes_actor.GetCubeProperty().SetEdgeVisibility(True)
|
|
axes_actor.GetCubeProperty().BackfaceCullingOn()
|
|
if opacity < 1.0:
|
|
# Hide the text edges
|
|
axes_actor.GetTextEdgesProperty().SetOpacity(0)
|
|
|
|
if color_box:
|
|
# Hide the cube so we can color each face
|
|
axes_actor.GetCubeProperty().SetOpacity(0)
|
|
axes_actor.GetCubeProperty().SetEdgeVisibility(False)
|
|
|
|
cube = pyvista.Cube()
|
|
cube.clear_data() # remove normals
|
|
face_colors = np.array(
|
|
[
|
|
x_face_color.int_rgb,
|
|
x_face_color.int_rgb,
|
|
y_face_color.int_rgb,
|
|
y_face_color.int_rgb,
|
|
z_face_color.int_rgb,
|
|
z_face_color.int_rgb,
|
|
],
|
|
np.uint8,
|
|
)
|
|
cube.cell_data['face_colors'] = face_colors
|
|
|
|
cube_mapper = _vtk.vtkPolyDataMapper()
|
|
cube_mapper.SetInputData(cube)
|
|
cube_mapper.SetColorModeToDirectScalars()
|
|
cube_mapper.Update()
|
|
|
|
cube_actor = pyvista.Actor(mapper=cube_mapper)
|
|
cube_actor.prop.culling = 'back'
|
|
cube_actor.prop.opacity = opacity
|
|
|
|
prop_assembly = _vtk.vtkPropAssembly()
|
|
prop_assembly.AddPart(axes_actor)
|
|
prop_assembly.AddPart(cube_actor)
|
|
actor = prop_assembly
|
|
else:
|
|
actor = axes_actor # type: ignore[assignment]
|
|
|
|
_update_axes_label_color(actor, label_color)
|
|
|
|
return actor
|
|
|
|
|
|
def create_north_arrow():
|
|
"""Create a north arrow mesh.
|
|
|
|
.. versionadded:: 0.44.0
|
|
|
|
Returns
|
|
-------
|
|
pyvista.PolyData
|
|
North arrow mesh.
|
|
|
|
"""
|
|
points = np.array(
|
|
[
|
|
[0.0, 5.0, 0.0],
|
|
[-2.0, 0.0, 0.0],
|
|
[0.0, 1.5, 0.0],
|
|
[2.0, 0.0, 0.0],
|
|
[0.0, 5.0, 1.0],
|
|
[-2.0, 0.0, 1.0],
|
|
[0.0, 1.5, 1.0],
|
|
[2.0, 0.0, 1.0],
|
|
],
|
|
)
|
|
faces = np.array(
|
|
[
|
|
4,
|
|
3,
|
|
7,
|
|
4,
|
|
0,
|
|
4,
|
|
2,
|
|
6,
|
|
7,
|
|
3,
|
|
4,
|
|
1,
|
|
5,
|
|
6,
|
|
2,
|
|
4,
|
|
0,
|
|
4,
|
|
5,
|
|
1,
|
|
4,
|
|
0,
|
|
1,
|
|
2,
|
|
3,
|
|
4,
|
|
4,
|
|
7,
|
|
6,
|
|
5,
|
|
],
|
|
)
|
|
return pyvista.PolyData(points, faces)
|
|
|
|
|
|
def normalize(x, minimum=None, maximum=None):
|
|
"""Normalize the given value between [minimum, maximum].
|
|
|
|
Parameters
|
|
----------
|
|
x : numpy.ndarray
|
|
The array of values to normalize.
|
|
minimum : float, optional
|
|
The minimum value to which ``x`` should be normalized. If not specified,
|
|
the minimum value in ``x`` will be used.
|
|
maximum : float, optional
|
|
The maximum value to which ``x`` should be normalized. If not specified,
|
|
the maximum value in ``x`` will be used.
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray
|
|
The normalized array of values, where the values are scaled to the
|
|
range ``[minimum, maximum]``.
|
|
|
|
"""
|
|
if minimum is None:
|
|
minimum = np.nanmin(x)
|
|
if maximum is None:
|
|
maximum = np.nanmax(x)
|
|
return (x - minimum) / (maximum - minimum)
|
|
|
|
|
|
@_deprecate_positional_args(allowed=['mapping', 'n_colors'])
|
|
def opacity_transfer_function( # noqa: PLR0917
|
|
mapping,
|
|
n_colors,
|
|
interpolate: bool = True, # noqa: FBT001, FBT002
|
|
kind='linear',
|
|
):
|
|
"""Get the opacity transfer function for a mapping.
|
|
|
|
These values will map on to a scalar bar range and thus the number of
|
|
colors (``n_colors``) must correspond to the number of colors in the color
|
|
mapping that these opacities are associated to.
|
|
|
|
If interpolating, ``scipy.interpolate.interp1d`` is used if available,
|
|
otherwise ``np.interp`` is used. The ``kind`` argument controls the kind of
|
|
interpolation for ``interp1d``.
|
|
|
|
This returns the opacity range from 0 to 255, where 0 is totally
|
|
transparent and 255 is totally opaque.
|
|
|
|
The equation to create the sigmoid mapping is: ``1 / (1 + exp(-x))`` where
|
|
``x`` is the range from ``-a`` to ``+a`` and ``a`` is the value given in
|
|
the ``mapping`` string. Default is ``a=10`` for 'sigmoid' mapping.
|
|
|
|
Parameters
|
|
----------
|
|
mapping : list[float] | str
|
|
The opacity mapping to use. Can be a ``str`` name of a predefined
|
|
mapping including ``'linear'``, ``'geom'``, ``'sigmoid'``,
|
|
``'sigmoid_1-10,15,20'``, and ``foreground``. Append an ``'_r'`` to any
|
|
of those names (except ``foreground``) to reverse that mapping.
|
|
The mapping can also be a custom user-defined array/list of values
|
|
that will be interpolated across the ``n_color`` range.
|
|
|
|
n_colors : int
|
|
The number of colors that the opacities must be mapped to.
|
|
|
|
interpolate : bool
|
|
Flag on whether or not to interpolate the opacity mapping for all
|
|
colors.
|
|
|
|
kind : str
|
|
The interpolation kind if ``interpolate`` is ``True`` and ``scipy``
|
|
is available. If ``scipy`` is not available, linear interpolation
|
|
is always used. Options are:
|
|
|
|
- ``'linear'``
|
|
- ``'nearest'``
|
|
- ``'zero'``
|
|
- ``'slinear'``
|
|
- ``'quadratic'``
|
|
- ``'cubic'``
|
|
- ``'previous'``
|
|
- ``'next'``
|
|
|
|
.. versionchanged:: 0.46
|
|
|
|
Linear interpolation is now always used by default. Previously,
|
|
quadratic interpolation was used if ``scipy`` was installed.
|
|
|
|
Returns
|
|
-------
|
|
numpy.ndarray
|
|
Array of ``numpy.uint8`` values ``n_colors`` long containing the
|
|
[0-255] opacity mapping values.
|
|
|
|
Examples
|
|
--------
|
|
>>> import pyvista as pv
|
|
>>> # Fetch the `sigmoid` mapping between 0 and 255
|
|
>>> tf = pv.opacity_transfer_function('sigmoid', 256)
|
|
>>> # Fetch the `geom_r` mapping between 0 and 1
|
|
>>> tf = pv.opacity_transfer_function('geom_r', 256).astype(float) / 255.0
|
|
>>> # Interpolate a user defined opacity mapping
|
|
>>> opacity = [0, 0.2, 0.9, 0.6, 0.3]
|
|
>>> tf = pv.opacity_transfer_function(opacity, 256)
|
|
|
|
"""
|
|
sigmoid = lambda x: np.array(1 / (1 + np.exp(-x)) * 255, dtype=np.uint8)
|
|
transfer_func = {
|
|
'linear': np.linspace(0, 255, n_colors, dtype=np.uint8),
|
|
'geom': np.geomspace(1e-6, 255, n_colors, dtype=np.uint8),
|
|
'geom_r': np.geomspace(255, 1e-6, n_colors, dtype=np.uint8),
|
|
'sigmoid': sigmoid(np.linspace(-10.0, 10.0, n_colors)),
|
|
'sigmoid_1': sigmoid(np.linspace(-1.0, 1.0, n_colors)),
|
|
'sigmoid_2': sigmoid(np.linspace(-2.0, 2.0, n_colors)),
|
|
'sigmoid_3': sigmoid(np.linspace(-3.0, 3.0, n_colors)),
|
|
'sigmoid_4': sigmoid(np.linspace(-4.0, 4.0, n_colors)),
|
|
'sigmoid_5': sigmoid(np.linspace(-5.0, 5.0, n_colors)),
|
|
'sigmoid_6': sigmoid(np.linspace(-6.0, 6.0, n_colors)),
|
|
'sigmoid_7': sigmoid(np.linspace(-7.0, 7.0, n_colors)),
|
|
'sigmoid_8': sigmoid(np.linspace(-8.0, 8.0, n_colors)),
|
|
'sigmoid_9': sigmoid(np.linspace(-9.0, 9.0, n_colors)),
|
|
'sigmoid_10': sigmoid(np.linspace(-10.0, 10.0, n_colors)),
|
|
'sigmoid_15': sigmoid(np.linspace(-15.0, 15.0, n_colors)),
|
|
'sigmoid_20': sigmoid(np.linspace(-20.0, 20.0, n_colors)),
|
|
'foreground': np.hstack((0, [255] * (n_colors - 1))).astype(np.uint8),
|
|
}
|
|
transfer_func['linear_r'] = transfer_func['linear'][::-1]
|
|
transfer_func['sigmoid_r'] = transfer_func['sigmoid'][::-1]
|
|
for i in range(3, 11):
|
|
k = f'sigmoid_{i}'
|
|
rk = f'{k}_r'
|
|
transfer_func[rk] = transfer_func[k][::-1]
|
|
if isinstance(mapping, str):
|
|
try:
|
|
return transfer_func[mapping]
|
|
except KeyError:
|
|
msg = (
|
|
f'Opacity transfer function ({mapping}) unknown. '
|
|
f'Valid options: {list(transfer_func.keys())}'
|
|
)
|
|
raise ValueError(msg) from None
|
|
elif isinstance(mapping, (np.ndarray, list, tuple)):
|
|
mapping = np.array(mapping)
|
|
if mapping.size == n_colors:
|
|
# User could pass transfer function ready for lookup table
|
|
pass
|
|
elif mapping.size < n_colors:
|
|
# User pass custom transfer function to be linearly interpolated
|
|
if np.max(mapping) > 1.0 or np.min(mapping) < 0.0:
|
|
mapping = normalize(mapping)
|
|
# Interpolate transfer function to match lookup table
|
|
xo = np.linspace(0, n_colors, len(mapping), dtype=np.int_)
|
|
xx = np.linspace(0, n_colors, n_colors, dtype=np.int_)
|
|
try:
|
|
if not interpolate:
|
|
msg = 'No interpolation.'
|
|
raise ValueError(msg)
|
|
from scipy.interpolate import interp1d # noqa: PLC0415
|
|
|
|
f = interp1d(xo, mapping, kind=kind)
|
|
vals = f(xx)
|
|
vals[vals < 0] = 0.0
|
|
vals[vals > 1.0] = 1.0
|
|
mapping = (vals * 255.0).astype(np.uint8)
|
|
|
|
except (ImportError, ValueError):
|
|
# Otherwise use simple linear interp
|
|
mapping = (np.interp(xx, xo, mapping) * 255).astype(np.uint8)
|
|
else:
|
|
msg = (
|
|
f'Transfer function cannot have more values than `n_colors`. '
|
|
f'This has {mapping.size} elements'
|
|
)
|
|
raise RuntimeError(msg)
|
|
return mapping
|
|
msg = f'Transfer function type ({type(mapping)}) not understood'
|
|
raise TypeError(msg)
|
|
|
|
|
|
def parse_font_family(font_family: str) -> int:
|
|
"""Check and validate the given font family name.
|
|
|
|
Parameters
|
|
----------
|
|
font_family : str
|
|
Font family name to validate. Must be one of the font names defined in
|
|
the ``FONTS`` enum class.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
Corresponding integer value of the valid font family name in the
|
|
``FONTS`` enum class.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
If the font_family is not one of the defined font names in the ``FONTS``
|
|
enum class.
|
|
|
|
"""
|
|
font_family = font_family.lower()
|
|
fonts = [font.name for font in FONTS]
|
|
if font_family not in fonts:
|
|
msg = f'Font must one of the following:\n{", ".join(fonts)}'
|
|
raise ValueError(msg)
|
|
return FONTS[font_family].value
|
|
|
|
|
|
def check_matplotlib_vtk_compatibility():
|
|
"""Check if VTK and Matplotlib versions are compatible for MathText rendering.
|
|
|
|
This function is primarily geared towards checking if MathText rendering is
|
|
supported with the given versions of VTK and Matplotlib. It follows the
|
|
version constraints:
|
|
|
|
* VTK <= 9.2.2 requires Matplotlib < 3.6
|
|
* VTK > 9.2.2 requires Matplotlib >= 3.6
|
|
|
|
Other version combinations of VTK and Matplotlib will work without
|
|
errors, but some features (like MathText/LaTeX rendering) may
|
|
silently fail.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
True if the versions of VTK and Matplotlib are compatible for MathText
|
|
rendering, False otherwise.
|
|
|
|
Raises
|
|
------
|
|
RuntimeError
|
|
If the versions of VTK and Matplotlib cannot be checked.
|
|
|
|
"""
|
|
import matplotlib as mpl # noqa: PLC0415
|
|
|
|
mpl_vers = tuple(map(int, mpl.__version__.split('.')[:2]))
|
|
if pyvista.vtk_version_info <= (9, 2, 2):
|
|
return not mpl_vers >= (3, 6)
|
|
elif pyvista.vtk_version_info > (9, 2, 2):
|
|
return mpl_vers >= (3, 6)
|
|
msg = 'Uncheckable versions.' # pragma: no cover
|
|
raise RuntimeError(msg) # pragma: no cover
|
|
|
|
|
|
def check_math_text_support():
|
|
"""Check if MathText and LaTeX symbols are supported.
|
|
|
|
Returns
|
|
-------
|
|
bool
|
|
``True`` if both MathText and LaTeX symbols are supported, ``False``
|
|
otherwise.
|
|
|
|
"""
|
|
# Something seriously sketchy is happening with this VTK code
|
|
# It seems to hijack stdout and stderr?
|
|
# See https://github.com/pyvista/pyvista/issues/4732
|
|
# This is a hack to get around that by executing the code in a subprocess
|
|
# and capturing the output:
|
|
# _vtk.vtkMathTextFreeTypeTextRenderer().MathTextIsSupported()
|
|
_cmd = 'import vtk;print(vtk.vtkMathTextFreeTypeTextRenderer().MathTextIsSupported());'
|
|
proc = subprocess.run([sys.executable, '-c', _cmd], check=False, capture_output=True)
|
|
math_text_support = False if proc.returncode else proc.stdout.decode().strip() == 'True'
|
|
return math_text_support and check_matplotlib_vtk_compatibility()
|