"""Pyvista plotting module.""" import collections.abc import logging import os import time import warnings from functools import wraps from threading import Thread import imageio import numpy as np import scooby import vtk from vtk.util import numpy_support as VN from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy import pyvista from pyvista.utilities import (assert_empty_kwargs, convert_array, convert_string_array, get_array, is_pyvista_dataset, numpy_to_texture, abstract_class, raise_not_matching, try_callback, wrap) from .background_renderer import BackgroundRenderer from .colors import get_cmap_safe from .export_vtkjs import export_plotter_vtkjs from .mapper import make_mapper from .picking import PickingHelper from .renderer import Renderer from .theme import (FONT_KEYS, MAX_N_COLOR_BARS, parse_color, parse_font_family, rcParams) from .tools import normalize, opacity_transfer_function from .widgets import WidgetHelper try: import matplotlib has_matplotlib = True except ImportError: has_matplotlib = False _ALL_PLOTTERS = {} def close_all(): """Close all open/active plotters and clean up memory.""" for key, p in _ALL_PLOTTERS.items(): if not p._closed: p.close() p.deep_clean() _ALL_PLOTTERS.clear() return True log = logging.getLogger(__name__) log.setLevel('CRITICAL') log.addHandler(logging.StreamHandler()) @abstract_class class BasePlotter(PickingHelper, WidgetHelper): """To be used by the Plotter and pyvistaqt.QtInteractor classes. Parameters ---------- shape : list or tuple, optional Number of sub-render windows inside of the main window. Specify two across with ``shape=(2, 1)`` and a two by two grid with ``shape=(2, 2)``. By default there is only one renderer. Can also accept a string descriptor as shape. E.g.: * ``shape="3|1"`` means 3 plots on the left and 1 on the right, * ``shape="4/2"`` means 4 plots on top and 2 at the bottom. border : bool, optional Draw a border around each render window. Default False. border_color : string or 3 item list, optional, defaults to white Either a string, rgb list, or hex color string. For example: * ``color='white'`` * ``color='w'`` * ``color=[1, 1, 1]`` * ``color='#FFFFFF'`` border_width : float, optional Width of the border in pixels when enabled. title : str, optional Window title of the scalar bar """ mouse_position = None click_position = None def __init__(self, shape=(1, 1), border=None, border_color='k', border_width=2.0, title=None, splitting_position=None, groups=None, row_weights=None, col_weights=None): """Initialize base plotter.""" log.debug('BasePlotter init start') self.image_transparent_background = rcParams['transparent_background'] self._store_image = False self.mesh = None if title is None: title = rcParams['title'] self.title = str(title) # by default add border for multiple plots if border is None: if shape != (1, 1): border = True else: border = False # add render windows self._active_renderer_index = 0 self.renderers = [] self.groups = np.empty((0,4),dtype=int) if isinstance(shape, str): if '|' in shape: n = int(shape.split('|')[0]) m = int(shape.split('|')[1]) rangen = reversed(range(n)) rangem = reversed(range(m)) else: m = int(shape.split('/')[0]) n = int(shape.split('/')[1]) rangen = range(n) rangem = range(m) if splitting_position is None: splitting_position = rcParams['multi_rendering_splitting_position'] if splitting_position is None: if n >= m: xsplit = m/(n+m) else: xsplit = 1-n/(n+m) else: xsplit = splitting_position for i in rangen: arenderer = Renderer(self, border, border_color, border_width) if '|' in shape: arenderer.SetViewport(0, i/n, xsplit, (i+1)/n) else: arenderer.SetViewport(i/n, 0, (i+1)/n, xsplit) self.renderers.append(arenderer) for i in rangem: arenderer = Renderer(self, border, border_color, border_width) if '|' in shape: arenderer.SetViewport(xsplit, i/m, 1, (i+1)/m) else: arenderer.SetViewport(i/m, xsplit, (i+1)/m, 1) self.renderers.append(arenderer) self.shape = (n+m,) self._render_idxs = np.arange(n+m) else: if not isinstance(shape, (np.ndarray, collections.abc.Sequence)): raise TypeError('"shape" should be a list, tuple or string descriptor') if len(shape) != 2: raise ValueError('"shape" must have length 2.') shape = np.asarray(shape) if not np.issubdtype(shape.dtype, np.integer) or (shape <= 0).any(): raise ValueError('"shape" must contain only positive integers.') # always assign shape as a tuple self.shape = tuple(shape) self._render_idxs = np.empty(self.shape,dtype=int) # Check if row and col weights correspond to given shape, or initialize them to defaults (equally weighted) # and convert to normalized offsets if row_weights is None: row_weights = np.ones(shape[0]) if col_weights is None: col_weights = np.ones(shape[1]) assert(np.array(row_weights).size==shape[0]) assert(np.array(col_weights).size==shape[1]) row_off = np.cumsum(np.abs(row_weights))/np.sum(np.abs(row_weights)) row_off = 1-np.concatenate(([0],row_off)) col_off = np.cumsum(np.abs(col_weights))/np.sum(np.abs(col_weights)) col_off = np.concatenate(([0],col_off)) # Check and convert groups to internal format (Nx4 matrix where every row contains the row and col index of the top left cell # together with the row and col index of the bottom right cell) if groups is not None: assert isinstance(groups, collections.abc.Sequence), '"groups" should be a list or tuple' for group in groups: assert isinstance(group, collections.abc.Sequence) and len(group)==2, 'each group entry should be a list or tuple of 2 elements' rows = group[0] if isinstance(rows,slice): rows = np.arange(self.shape[0],dtype=int)[rows] cols = group[1] if isinstance(cols,slice): cols = np.arange(self.shape[1],dtype=int)[cols] # Get the normalized group, i.e. extract top left corner and bottom right corner from the given rows and cols norm_group = [np.min(rows),np.min(cols),np.max(rows),np.max(cols)] # Check for overlap with already defined groups: for i in range(norm_group[0],norm_group[2]+1): for j in range(norm_group[1],norm_group[3]+1): assert self.loc_to_group((i,j)) is None, 'groups cannot overlap' self.groups = np.concatenate((self.groups,np.array([norm_group],dtype=int)),axis=0) # Create subplot renderers for row in range(shape[0]): for col in range(shape[1]): group = self.loc_to_group((row,col)) nb_rows = None nb_cols = None if group is not None: if row==self.groups[group,0] and col==self.groups[group,1]: # Only add renderer for first location of the group nb_rows = 1+self.groups[group,2]-self.groups[group,0] nb_cols = 1+self.groups[group,3]-self.groups[group,1] else: nb_rows = 1 nb_cols = 1 if nb_rows is not None: renderer = Renderer(self, border, border_color, border_width) x0 = col_off[col] y0 = row_off[row+nb_rows] x1 = col_off[col+nb_cols] y1 = row_off[row] renderer.SetViewport(x0, y0, x1, y1) self._render_idxs[row,col] = len(self.renderers) self.renderers.append(renderer) else: self._render_idxs[row,col] = self._render_idxs[self.groups[group,0],self.groups[group,1]] # each render will also have an associated background renderer self._background_renderers = [None for _ in range(len(self.renderers))] # This keeps track of scalars names already plotted and their ranges self._scalar_bar_ranges = {} self._scalar_bar_mappers = {} self._scalar_bar_actors = {} self._scalar_bar_widgets = {} # track if the camera has been setup # self.camera_set = False self._first_time = True # Keep track of the scale self._labels = [] # Set default style self._style = vtk.vtkInteractorStyleRubberBandPick() # this helps managing closed plotters self._closed = False # Add self to open plotters self._id_name = "{}-{}".format(str(hex(id(self))), len(_ALL_PLOTTERS)) _ALL_PLOTTERS[self._id_name] = self # lighting style self.lighting = vtk.vtkLightKit() # self.lighting.SetHeadLightWarmth(1.0) # self.lighting.SetHeadLightWarmth(1.0) for renderer in self.renderers: self.lighting.AddLightsToRenderer(renderer) renderer.LightFollowCameraOn() # Key bindings self.reset_key_events() log.debug('BasePlotter init stop') #### Manage the active Renderer #### def loc_to_group(self, loc): """Return group id of the given location index. Or None if this location is not part of any group.""" group_idxs = np.arange(self.groups.shape[0]) I = (loc[0]>=self.groups[:,0]) & (loc[0]<=self.groups[:,2]) & (loc[1]>=self.groups[:,1]) & (loc[1]<=self.groups[:,3]) group = group_idxs[I] return None if group.size==0 else group[0] def loc_to_index(self, loc): """Return index of the render window given a location index. Parameters ---------- loc : int, tuple, or list Index of the renderer to add the actor to. For example, ``loc=2`` or ``loc=(1, 1)``. Return ------ idx : int Index of the render window. """ if loc is None: return self._active_renderer_index elif isinstance(loc, (int, np.integer)): return loc elif isinstance(loc, (np.ndarray, collections.abc.Sequence)): if not len(loc) == 2: raise ValueError('"loc" must contain two items') index_row = loc[0] index_column = loc[1] if index_row < 0 or index_row >= self.shape[0]: raise IndexError('Row index is out of range ({})'.format(self.shape[0])) if index_column < 0 or index_column >= self.shape[1]: raise IndexError('Column index is out of range ({})'.format(self.shape[1])) return self._render_idxs[index_row,index_column] else: raise TypeError('"loc" must be an integer or a sequence.') def index_to_loc(self, index): """Convert a 1D index location to the 2D location on the plotting grid.""" if not isinstance(index, (int, np.integer)): raise TypeError('"index" must be a scalar integer.') if len(self.shape) == 1: return index args = np.argwhere(self._render_idxs == index) if len(args) < 1: raise IndexError('Index ({}) is out of range.') return args[0] @property def renderer(self): """Return the active renderer.""" return self.renderers[self._active_renderer_index] @property def store_image(self): """Return if an image will be saved on close.""" return self._store_image @store_image.setter def store_image(self, value): """Store last rendered frame on close.""" self._store_image = bool(value) def subplot(self, index_row, index_column=None): """Set the active subplot. Parameters ---------- index_row : int Index of the subplot to activate along the rows. index_column : int Index of the subplot to activate along the columns. """ if len(self.shape) == 1: self._active_renderer_index = index_row return if index_row < 0 or index_row >= self.shape[0]: raise IndexError('Row index is out of range ({})'.format(self.shape[0])) if index_column < 0 or index_column >= self.shape[1]: raise IndexError('Column index is out of range ({})'.format(self.shape[1])) self._active_renderer_index = self.loc_to_index((index_row, index_column)) #### Wrap Renderer methods #### @wraps(Renderer.add_floor) def add_floor(self, *args, **kwargs): """Wrap ``Renderer.add_floor``.""" return self.renderer.add_floor(*args, **kwargs) @wraps(Renderer.remove_floors) def remove_floors(self, *args, **kwargs): """Wrap ``Renderer.remove_floors``.""" return self.renderer.remove_floors(*args, **kwargs) @wraps(Renderer.enable_anti_aliasing) def enable_anti_aliasing(self, *args, **kwargs): """Wrap ``Renderer.enable_anti_aliasing``.""" self.renderer.enable_anti_aliasing(*args, **kwargs) @wraps(Renderer.disable_anti_aliasing) def disable_anti_aliasing(self, *args, **kwargs): """Wrap ``Renderer.disable_anti_aliasing``.""" self.renderer.disable_anti_aliasing(*args, **kwargs) @wraps(Renderer.set_focus) def set_focus(self, *args, **kwargs): """Wrap ``Renderer.set_focus``.""" self.renderer.set_focus(*args, **kwargs) self.render() @wraps(Renderer.set_position) def set_position(self, *args, **kwargs): """Wrap ``Renderer.set_position``.""" self.renderer.set_position(*args, **kwargs) self.render() @wraps(Renderer.set_viewup) def set_viewup(self, *args, **kwargs): """Wrap ``Renderer.set_viewup``.""" self.renderer.set_viewup(*args, **kwargs) self.render() @wraps(Renderer.add_orientation_widget) def add_orientation_widget(self, *args, **kwargs): """Wrap ``Renderer.add_orientation_widget``.""" return self.renderer.add_orientation_widget(*args, **kwargs) @wraps(Renderer.add_axes) def add_axes(self, *args, **kwargs): """Wrap ``Renderer.add_axes``.""" return self.renderer.add_axes(*args, **kwargs) @wraps(Renderer.hide_axes) def hide_axes(self, *args, **kwargs): """Wrap ``Renderer.hide_axes``.""" return self.renderer.hide_axes(*args, **kwargs) @wraps(Renderer.show_axes) def show_axes(self, *args, **kwargs): """Wrap ``Renderer.show_axes``.""" return self.renderer.show_axes(*args, **kwargs) @wraps(Renderer.update_bounds_axes) def update_bounds_axes(self, *args, **kwargs): """Wrap ``Renderer.update_bounds_axes``.""" return self.renderer.update_bounds_axes(*args, **kwargs) @wraps(Renderer.add_actor) def add_actor(self, *args, **kwargs): """Wrap ``Renderer.add_actor``.""" return self.renderer.add_actor(*args, **kwargs) @wraps(Renderer.enable_parallel_projection) def enable_parallel_projection(self, *args, **kwargs): """Wrap ``Renderer.enable_parallel_projection``.""" return self.renderer.enable_parallel_projection(*args, **kwargs) @wraps(Renderer.disable_parallel_projection) def disable_parallel_projection(self, *args, **kwargs): """Wrap ``Renderer.disable_parallel_projection``.""" return self.renderer.disable_parallel_projection(*args, **kwargs) @wraps(Renderer.add_axes_at_origin) def add_axes_at_origin(self, *args, **kwargs): """Wrap ``Renderer.add_axes_at_origin``.""" return self.renderer.add_axes_at_origin(*args, **kwargs) @wraps(Renderer.show_bounds) def show_bounds(self, *args, **kwargs): """Wrap ``Renderer.show_bounds``.""" return self.renderer.show_bounds(*args, **kwargs) @wraps(Renderer.add_bounds_axes) def add_bounds_axes(self, *args, **kwargs): """Wrap ``add_bounds_axes``.""" return self.renderer.add_bounds_axes(*args, **kwargs) @wraps(Renderer.add_bounding_box) def add_bounding_box(self, *args, **kwargs): """Wrap ``Renderer.add_bounding_box``.""" return self.renderer.add_bounding_box(*args, **kwargs) @wraps(Renderer.remove_bounding_box) def remove_bounding_box(self, *args, **kwargs): """Wrap ``Renderer.remove_bounding_box``.""" return self.renderer.remove_bounding_box(*args, **kwargs) @wraps(Renderer.remove_bounds_axes) def remove_bounds_axes(self, *args, **kwargs): """Wrap ``Renderer.remove_bounds_axes``.""" return self.renderer.remove_bounds_axes(*args, **kwargs) @wraps(Renderer.show_grid) def show_grid(self, *args, **kwargs): """Wrap ``Renderer.show_grid``.""" return self.renderer.show_grid(*args, **kwargs) @wraps(Renderer.set_scale) def set_scale(self, *args, **kwargs): """Wrap ``Renderer.set_scale``.""" return self.renderer.set_scale(*args, **kwargs) @wraps(Renderer.enable_eye_dome_lighting) def enable_eye_dome_lighting(self, *args, **kwargs): """Wrap ``Renderer.enable_eye_dome_lighting``.""" return self.renderer.enable_eye_dome_lighting(*args, **kwargs) @wraps(Renderer.disable_eye_dome_lighting) def disable_eye_dome_lighting(self, *args, **kwargs): """Wrap ``Renderer.disable_eye_dome_lighting``.""" return self.renderer.disable_eye_dome_lighting(*args, **kwargs) @wraps(Renderer.reset_camera) def reset_camera(self, *args, **kwargs): """Wrap ``Renderer.reset_camera``.""" self.renderer.reset_camera(*args, **kwargs) self.render() @wraps(Renderer.isometric_view) def isometric_view(self, *args, **kwargs): """Wrap ``Renderer.isometric_view``.""" return self.renderer.isometric_view(*args, **kwargs) @wraps(Renderer.view_isometric) def view_isometric(self, *args, **kwarg): """Wrap ``Renderer.view_isometric``.""" return self.renderer.view_isometric(*args, **kwarg) @wraps(Renderer.view_vector) def view_vector(self, *args, **kwarg): """Wrap ``Renderer.view_vector``.""" return self.renderer.view_vector(*args, **kwarg) @wraps(Renderer.view_xy) def view_xy(self, *args, **kwarg): """Wrap ``Renderer.view_xy``.""" return self.renderer.view_xy(*args, **kwarg) @wraps(Renderer.view_yx) def view_yx(self, *args, **kwarg): """Wrap ``Renderer.view_yx``.""" return self.renderer.view_yx(*args, **kwarg) @wraps(Renderer.view_xz) def view_xz(self, *args, **kwarg): """Wrap ``Renderer.view_xz``.""" return self.renderer.view_xz(*args, **kwarg) @wraps(Renderer.view_zx) def view_zx(self, *args, **kwarg): """Wrap ``Renderer.view_zx``.""" return self.renderer.view_zx(*args, **kwarg) @wraps(Renderer.view_yz) def view_yz(self, *args, **kwarg): """Wrap ``Renderer.view_yz``.""" return self.renderer.view_yz(*args, **kwarg) @wraps(Renderer.view_zy) def view_zy(self, *args, **kwarg): """Wrap ``Renderer.view_zy``.""" return self.renderer.view_zy(*args, **kwarg) @wraps(Renderer.disable) def disable(self, *args, **kwarg): """Wrap ``Renderer.disable``.""" return self.renderer.disable(*args, **kwarg) @wraps(Renderer.enable) def enable(self, *args, **kwarg): """Wrap ``Renderer.enable``.""" return self.renderer.enable(*args, **kwarg) @wraps(Renderer.enable_depth_peeling) def enable_depth_peeling(self, *args, **kwargs): """Wrap ``Renderer.enable_depth_peeling``.""" if hasattr(self, 'ren_win'): result = self.renderer.enable_depth_peeling(*args, **kwargs) if result: self.ren_win.AlphaBitPlanesOn() return result @wraps(Renderer.disable_depth_peeling) def disable_depth_peeling(self): """Wrap ``Renderer.disable_depth_peeling``.""" if hasattr(self, 'ren_win'): self.ren_win.AlphaBitPlanesOff() return self.renderer.disable_depth_peeling() @wraps(Renderer.get_default_cam_pos) def get_default_cam_pos(self, *args, **kwargs): """Wrap ``Renderer.get_default_cam_pos``.""" return self.renderer.get_default_cam_pos(*args, **kwargs) @wraps(Renderer.remove_actor) def remove_actor(self, actor, reset_camera=False): """Wrap ``Renderer.remove_actor``.""" for renderer in self.renderers: renderer.remove_actor(actor, reset_camera) return True #### Properties from Renderer #### @property def camera(self): """Return the active camera of the active renderer.""" return self.renderer.camera @camera.setter def camera(self, camera): """Set the active camera for the rendering scene.""" self.renderer.camera = camera @property def camera_set(self): """Return if the camera of the active renderer has been set.""" return self.renderer.camera_set @camera_set.setter def camera_set(self, is_set): """Set if the camera has been set on the active renderer.""" self.renderer.camera_set = is_set @property def bounds(self): """Return the bounds of the active renderer.""" return self.renderer.bounds @property def length(self): """Return the length of the diagonal of the bounding box of the scene.""" return self.renderer.length @property def center(self): """Return the center of the active renderer.""" return self.renderer.center @property def _scalar_bar_slots(self): """Return the scalar bar slots of the active renderer.""" return self.renderer._scalar_bar_slots @property def _scalar_bar_slot_lookup(self): """Return the scalar bar slot lookup of the active renderer.""" return self.renderer._scalar_bar_slot_lookup @_scalar_bar_slots.setter def _scalar_bar_slots(self, value): """Set the scalar bar slots of the active renderer.""" self.renderer._scalar_bar_slots = value @_scalar_bar_slot_lookup.setter def _scalar_bar_slot_lookup(self, value): """Set the scalar bar slot lookup of the active renderer.""" self.renderer._scalar_bar_slot_lookup = value @property def scale(self): """Return the scaling of the active renderer.""" return self.renderer.scale @scale.setter def scale(self, scale): """Set the scaling of the active renderer.""" self.renderer.set_scale(*scale) @property def camera_position(self): """Return camera position of the active render window.""" return self.renderer.camera_position @camera_position.setter def camera_position(self, camera_location): """Set camera position of the active render window.""" self.renderer.camera_position = camera_location @property def background_color(self): """Return the background color of the first render window.""" return self.renderers[0].GetBackground() @background_color.setter def background_color(self, color): """Set the background color of all the render windows.""" self.set_background(color) #### Properties of the BasePlotter #### @property def window_size(self): """Return the render window size.""" return list(self.ren_win.GetSize()) @window_size.setter def window_size(self, window_size): """Set the render window size.""" self.ren_win.SetSize(window_size[0], window_size[1]) @property def image_depth(self): """Return a depth image representing current render window. Helper attribute for ``get_image_depth``. """ return self.get_image_depth() @property def image(self): """Return an image array of current render window. To retrieve an image after the render window has been closed, set: `plotter.store_image = True` """ if not hasattr(self, 'ren_win') and hasattr(self, 'last_image'): return self.last_image ifilter = vtk.vtkWindowToImageFilter() ifilter.SetInput(self.ren_win) ifilter.ReadFrontBufferOff() if self.image_transparent_background: ifilter.SetInputBufferTypeToRGBA() else: ifilter.SetInputBufferTypeToRGB() return self._run_image_filter(ifilter) #### Everything else #### def render(self): """Render the main window. If this is called before ``show()``, nothing will happen. """ if hasattr(self, 'ren_win') and not self._first_time: self.ren_win.Render() # Not sure if this is ever needed but here as a reminder # if hasattr(self, 'iren') and not self._first_time: # self.iren.Render() return def add_key_event(self, key, callback): """Add a function to callback when the given key is pressed. These are non-unique - thus a key could map to many callback functions. The callback function must not have any arguments. Parameters ---------- key : str The key to trigger the event callback : callable A callable that takes no arguments """ if not hasattr(callback, '__call__'): raise TypeError('callback must be callable.') self._key_press_event_callbacks[key].append(callback) def _add_observer(self, event, call): if hasattr(self, 'iren'): self._observers[event] = self.iren.AddObserver(event, call) def _remove_observer(self, event): if hasattr(self, 'iren') and event in self._observers: self.iren.RemoveObserver(event) del self._observers[event] def clear_events_for_key(self, key): """Remove the callbacks associated to the key.""" self._key_press_event_callbacks.pop(key) def store_mouse_position(self, *args): """Store mouse position.""" if not hasattr(self, "iren"): raise AttributeError("This plotting window is not interactive.") self.mouse_position = self.iren.GetEventPosition() def store_click_position(self, *args): """Store click position in viewport coordinates.""" if not hasattr(self, "iren"): raise AttributeError("This plotting window is not interactive.") self.click_position = self.iren.GetEventPosition() self.mouse_position = self.click_position def track_mouse_position(self): """Keep track of the mouse position. This will potentially slow down the interactor. No callbacks supported here - use :func:`pyvista.BasePlotter.track_click_position` instead. """ if hasattr(self, "iren"): self._add_observer(vtk.vtkCommand.MouseMoveEvent, self.store_mouse_position) def untrack_mouse_position(self): """Stop tracking the mouse position.""" self._remove_observer(vtk.vtkCommand.MouseMoveEvent) def track_click_position(self, callback=None, side="right", viewport=False): """Keep track of the click position. By default, it only tracks right clicks. Parameters ---------- callback : callable A callable method that will use the click position. Passes the click position as a length two tuple. side : str The side of the mouse for the button to track (left or right). Default is left. Also accepts ``'r'`` or ``'l'``. viewport: bool If ``True``, uses the normalized viewport coordinate system (values between 0.0 and 1.0 and support for HiDPI) when passing the click position to the callback """ if not hasattr(self, "iren"): return side = str(side).lower() if side in ["right", "r"]: event = vtk.vtkCommand.RightButtonPressEvent elif side in ["left", "l"]: event = vtk.vtkCommand.LeftButtonPressEvent else: raise TypeError("Side ({}) not supported. Try `left` or `right`".format(side)) def _click_callback(obj, event): self.store_click_position() if hasattr(callback, '__call__'): if viewport: try_callback(callback, self.click_position) else: try_callback(callback, self.pick_click_position()) self._add_observer(event, _click_callback) def untrack_click_position(self): """Stop tracking the click position.""" if hasattr(self, "_click_observer"): self.iren.RemoveObserver(self._click_observer) del self._click_observer def _prep_for_close(self): """Make sure a screenshot is acquired before closing. This doesn't actually close anything! It just preps the plotter for closing. """ # Grab screenshot right before renderer closes self.last_image = self.screenshot(True, return_img=True) self.last_image_depth = self.get_image_depth() def increment_point_size_and_line_width(self, increment): """Increment point size and line width of all actors. For every actor in the scene, increment both its point size and line width by the given value. """ for renderer in self.renderers: for actor in renderer._actors.values(): if hasattr(actor, "GetProperty"): prop = actor.GetProperty() if hasattr(prop, "SetPointSize"): prop.SetPointSize(prop.GetPointSize() + increment) if hasattr(prop, "SetLineWidth"): prop.SetLineWidth(prop.GetLineWidth() + increment) self.render() return def reset_key_events(self): """Reset all of the key press events to their defaults.""" self._key_press_event_callbacks = collections.defaultdict(list) self.add_key_event('q', self._prep_for_close) # Add no matter what b_left_down_callback = lambda: self._add_observer('LeftButtonPressEvent', self.left_button_down) self.add_key_event('b', b_left_down_callback) self.add_key_event('v', lambda: self.isometric_view_interactive()) self.add_key_event('f', self.fly_to_mouse_position) self.add_key_event('C', lambda: self.enable_cell_picking()) self.add_key_event('Up', lambda: self.camera.Zoom(1.05)) self.add_key_event('Down', lambda: self.camera.Zoom(0.95)) self.add_key_event('plus', lambda: self.increment_point_size_and_line_width(1)) self.add_key_event('minus', lambda: self.increment_point_size_and_line_width(-1)) def key_press_event(self, obj, event): """Listen for key press event.""" try: key = self.iren.GetKeySym() log.debug('Key %s pressed' % key) self._last_key = key if key in self._key_press_event_callbacks.keys(): # Note that defaultdict's will never throw a key error callbacks = self._key_press_event_callbacks[key] for func in callbacks: func() except Exception as e: log.error('Exception encountered for keypress "%s": %s' % (key, e)) def left_button_down(self, obj, event_type): """Register the event for a left button down click.""" if hasattr(self.ren_win, 'GetOffScreenFramebuffer'): if not self.ren_win.GetOffScreenFramebuffer().GetFBOIndex(): # must raise a runtime error as this causes a segfault on VTK9 raise ValueError('Invoking helper with no framebuffer') # Get 2D click location on window click_pos = self.iren.GetEventPosition() # Get corresponding click location in the 3D plot picker = vtk.vtkWorldPointPicker() picker.Pick(click_pos[0], click_pos[1], 0, self.renderer) self.pickpoint = np.asarray(picker.GetPickPosition()).reshape((-1, 3)) if np.any(np.isnan(self.pickpoint)): self.pickpoint[:] = 0 def update_style(self): """Update the camera interactor style.""" if not hasattr(self, '_style'): self._style = vtk.vtkInteractorStyleTrackballCamera() if hasattr(self, 'iren'): return self.iren.SetInteractorStyle(self._style) def enable_trackball_style(self): """Set the interactive style to trackball camera. The trackball camera is the default interactor style. """ self._style = vtk.vtkInteractorStyleTrackballCamera() return self.update_style() def enable_trackball_actor_style(self): """Set the interactive style to trackball actor. This allows to rotate actors around the scene. """ self._style = vtk.vtkInteractorStyleTrackballActor() return self.update_style() def enable_image_style(self): """Set the interactive style to image. Controls: - Left Mouse button triggers window level events - CTRL Left Mouse spins the camera around its view plane normal - SHIFT Left Mouse pans the camera - CTRL SHIFT Left Mouse dollys (a positional zoom) the camera - Middle mouse button pans the camera - Right mouse button dollys the camera. - SHIFT Right Mouse triggers pick events """ self._style = vtk.vtkInteractorStyleImage() return self.update_style() def enable_joystick_style(self): """Set the interactive style to joystick. It allows the user to move (rotate, pan, etc.) the camera, the point of view for the scene. The position of the mouse relative to the center of the scene determines the speed at which the camera moves, and the speed of the mouse movement determines the acceleration of the camera, so the camera continues to move even if the mouse if not moving. For a 3-button mouse, the left button is for rotation, the right button for zooming, the middle button for panning, and ctrl + left button for spinning. (With fewer mouse buttons, ctrl + shift + left button is for zooming, and shift + left button is for panning.) """ self._style = vtk.vtkInteractorStyleJoystickCamera() return self.update_style() def enable_zoom_style(self): """Set the interactive style to rubber band zoom. This interactor style allows the user to draw a rectangle in the render window using the left mouse button. When the mouse button is released, the current camera zooms by an amount determined from the shorter side of the drawn rectangle. """ self._style = vtk.vtkInteractorStyleRubberBandZoom() return self.update_style() def enable_terrain_style(self): """Set the interactive style to terrain. Used to manipulate a camera which is viewing a scene with a natural view up, e.g., terrain. The camera in such a scene is manipulated by specifying azimuth (angle around the view up vector) and elevation (the angle from the horizon). """ self._style = vtk.vtkInteractorStyleTerrain() return self.update_style() def enable_rubber_band_style(self): """Set the interactive style to rubber band picking. This interactor style allows the user to draw a rectangle in the render window by hitting 'r' and then using the left mouse button. When the mouse button is released, the attached picker operates on the pixel in the center of the selection rectangle. If the picker happens to be a vtkAreaPicker it will operate on the entire selection rectangle. When the 'p' key is hit the above pick operation occurs on a 1x1 rectangle. In other respects it behaves the same as its parent class. """ self._style = vtk.vtkInteractorStyleRubberBandPick() return self.update_style() def hide_axes_all(self): """Hide the axes orientation widget in all renderers.""" for renderer in self.renderers: renderer.hide_axes() return def show_axes_all(self): """Show the axes orientation widget in all renderers.""" for renderer in self.renderers: renderer.show_axes() return def isometric_view_interactive(self): """Set the current interactive render window to isometric view.""" interactor = self.iren.GetInteractorStyle() renderer = interactor.GetCurrentRenderer() if renderer is None: renderer = self.renderer renderer.view_isometric() def update(self, stime=1, force_redraw=True): """Update window, redraw, process messages query. Parameters ---------- stime : int, optional Duration of timer that interrupt vtkRenderWindowInteractor in milliseconds. force_redraw : bool, optional Call ``render`` immediately. """ if stime <= 0: stime = 1 curr_time = time.time() if Plotter.last_update_time > curr_time: Plotter.last_update_time = curr_time if not hasattr(self, 'iren'): return update_rate = self.iren.GetDesiredUpdateRate() if (curr_time - Plotter.last_update_time) > (1.0/update_rate): self.right_timer_id = self.iren.CreateRepeatingTimer(stime) self.iren.Start() self.iren.DestroyTimer(self.right_timer_id) self.render() Plotter.last_update_time = curr_time elif force_redraw: self.render() def add_mesh(self, mesh, color=None, style=None, scalars=None, clim=None, show_edges=None, edge_color=None, point_size=5.0, line_width=None, opacity=1.0, flip_scalars=False, lighting=None, n_colors=256, interpolate_before_map=True, cmap=None, label=None, reset_camera=None, scalar_bar_args=None, show_scalar_bar=None, stitle=None, multi_colors=False, name=None, texture=None, render_points_as_spheres=None, render_lines_as_tubes=False, smooth_shading=False, ambient=0.0, diffuse=1.0, specular=0.0, specular_power=100.0, nan_color=None, nan_opacity=1.0, culling=None, rgb=False, categories=False, use_transparency=False, below_color=None, above_color=None, annotations=None, pickable=True, preference="point", log_scale=False, **kwargs): """Add any PyVista/VTK mesh or dataset that PyVista can wrap to the scene. This method is using a mesh representation to view the surfaces and/or geometry of datasets. For volume rendering, see :func:`pyvista.BasePlotter.add_volume`. Parameters ---------- mesh : pyvista.Common or pyvista.MultiBlock Any PyVista or VTK mesh is supported. Also, any dataset that :func:`pyvista.wrap` can handle including NumPy arrays of XYZ points. color : string or 3 item list, optional, defaults to white Use to make the entire mesh have a single solid color. Either a string, RGB list, or hex color string. For example: ``color='white'``, ``color='w'``, ``color=[1, 1, 1]``, or ``color='#FFFFFF'``. Color will be overridden if scalars are specified. style : string, optional Visualization style of the mesh. One of the following: ``style='surface'``, ``style='wireframe'``, ``style='points'``. Defaults to ``'surface'``. Note that ``'wireframe'`` only shows a wireframe of the outer geometry. scalars : str or numpy.ndarray, optional Scalars used to "color" the mesh. Accepts a string name of an array that is present on the mesh or an array equal to the number of cells or the number of points in the mesh. Array should be sized as a single vector. If both ``color`` and ``scalars`` are ``None``, then the active scalars are used. clim : 2 item list, optional Color bar range for scalars. Defaults to minimum and maximum of scalars array. Example: ``[-1, 2]``. ``rng`` is also an accepted alias for this. show_edges : bool, optional Shows the edges of a mesh. Does not apply to a wireframe representation. edge_color : string or 3 item list, optional, defaults to black The solid color to give the edges when ``show_edges=True``. Either a string, RGB list, or hex color string. point_size : float, optional Point size of any nodes in the dataset plotted. Also applicable when style='points'. Default ``5.0`` line_width : float, optional Thickness of lines. Only valid for wireframe and surface representations. Default None. opacity : float, str, array-like Opacity of the mesh. If a siblge float value is given, it will be the global opacity of the mesh and uniformly applied everywhere - should be between 0 and 1. A string can also be specified to map the scalars range to a predefined opacity transfer function (options include: 'linear', 'linear_r', 'geom', 'geom_r'). A string could also be used to map a scalars array from the mesh to the opacity (must have same number of elements as the ``scalars`` argument). Or you can pass a custum made transfer function that is an array either ``n_colors`` in length or shorter. flip_scalars : bool, optional Flip direction of cmap. Most colormaps allow ``*_r`` suffix to do this as well. lighting : bool, optional Enable or disable view direction lighting. Default False. n_colors : int, optional Number of colors to use when displaying scalars. Defaults to 256. The scalar bar will also have this many colors. interpolate_before_map : bool, optional Enabling makes for a smoother scalars display. Default is True. When False, OpenGL will interpolate the mapped colors which can result is showing colors that are not present in the color map. cmap : str, list, optional Name of the Matplotlib colormap to us when mapping the ``scalars``. See available Matplotlib colormaps. Only applicable for when displaying ``scalars``. Requires Matplotlib to be installed. ``colormap`` is also an accepted alias for this. If ``colorcet`` or ``cmocean`` are installed, their colormaps can be specified by name. You can also specify a list of colors to override an existing colormap with a custom one. For example, to create a three color colormap you might specify ``['green', 'red', 'blue']`` label : str, optional String label to use when adding a legend to the scene with :func:`pyvista.BasePlotter.add_legend` reset_camera : bool, optional Reset the camera after adding this mesh to the scene scalar_bar_args : dict, optional Dictionary of keyword arguments to pass when adding the scalar bar to the scene. For options, see :func:`pyvista.BasePlotter.add_scalar_bar`. show_scalar_bar : bool If False, a scalar bar will not be added to the scene. Defaults to ``True``. stitle : string, optional Scalar bar title. By default the scalar bar is given a title of the the scalars array used to color the mesh. To create a bar with no title, use an empty string (i.e. ''). multi_colors : bool, optional If a ``MultiBlock`` dataset is given this will color each block by a solid color using matplotlib's color cycler. name : str, optional The name for the added mesh/actor so that it can be easily updated. If an actor of this name already exists in the rendering window, it will be replaced by the new actor. texture : vtk.vtkTexture or np.ndarray or boolean, optional A texture to apply if the input mesh has texture coordinates. This will not work with MultiBlock datasets. If set to ``True``, the first available texture on the object will be used. If a string name is given, it will pull a texture with that name associated to the input mesh. render_points_as_spheres : bool, optional render_lines_as_tubes : bool, optional smooth_shading : bool, optional ambient : float, optional When lighting is enabled, this is the amount of light from 0 to 1 that reaches the actor when not directed at the light source emitted from the viewer. Default 0.0 diffuse : float, optional The diffuse lighting coefficient. Default 1.0 specular : float, optional The specular lighting coefficient. Default 0.0 specular_power : float, optional The specular power. Between 0.0 and 128.0 nan_color : string or 3 item list, optional, defaults to gray The color to use for all ``NaN`` values in the plotted scalar array. nan_opacity : float, optional Opacity of ``NaN`` values. Should be between 0 and 1. Default 1.0 culling : str, optional Does not render faces that are culled. Options are ``'front'`` or ``'back'``. This can be helpful for dense surface meshes, especially when edges are visible, but can cause flat meshes to be partially displayed. Defaults ``False``. rgb : bool, optional If an 2 dimensional array is passed as the scalars, plot those values as RGB(A) colors! ``rgba`` is also accepted alias for this. Opacity (the A) is optional. categories : bool, optional If set to ``True``, then the number of unique values in the scalar array will be used as the ``n_colors`` argument. use_transparency : bool, optional Invert the opacity mappings and make the values correspond to transparency. below_color : string or 3 item list, optional Solid color for values below the scalars range (``clim``). This will automatically set the scalar bar ``below_label`` to ``'Below'`` above_color : string or 3 item list, optional Solid color for values below the scalars range (``clim``). This will automatically set the scalar bar ``above_label`` to ``'Above'`` annotations : dict, optional Pass a dictionary of annotations. Keys are the float values in the scalars range to annotate on the scalar bar and the values are the the string annotations. pickable : bool Set whether this mesh is pickable Return ------ actor: vtk.vtkActor VTK actor of the mesh. """ # Convert the VTK data object to a pyvista wrapped object if necessary if not is_pyvista_dataset(mesh): mesh = wrap(mesh) if not is_pyvista_dataset(mesh): raise TypeError('Object type ({}) not supported for plotting in PyVista.'.format(type(mesh))) ##### Parse arguments to be used for all meshes ##### if scalar_bar_args is None: scalar_bar_args = {} if show_edges is None: show_edges = rcParams['show_edges'] if edge_color is None: edge_color = rcParams['edge_color'] if show_scalar_bar is None: show_scalar_bar = rcParams['show_scalar_bar'] if lighting is None: lighting = rcParams['lighting'] # supported aliases clim = kwargs.pop('rng', clim) cmap = kwargs.pop('colormap', cmap) culling = kwargs.pop("backface_culling", culling) if render_points_as_spheres is None: render_points_as_spheres = rcParams['render_points_as_spheres'] if name is None: name = '{}({})'.format(type(mesh).__name__, mesh.memory_address) if nan_color is None: nan_color = rcParams['nan_color'] nan_color = list(parse_color(nan_color)) nan_color.append(nan_opacity) if color is True: color = rcParams['color'] if texture is False: texture = None if culling is True: culling = 'backface' rgb = kwargs.pop('rgba', rgb) if "scalar" in kwargs: raise TypeError("`scalar` is an invalid keyword argument for `add_mesh`. Perhaps you mean `scalars` with an s?") assert_empty_kwargs(**kwargs) ##### Handle composite datasets ##### if isinstance(mesh, pyvista.MultiBlock): # first check the scalars if clim is None and scalars is not None: # Get the data range across the array for all blocks # if scalars specified if isinstance(scalars, str): clim = mesh.get_data_range(scalars) else: # TODO: an array was given... how do we deal with # that? Possibly a 2D arrays or list of # arrays where first index corresponds to # the block? This could get complicated real # quick. raise TypeError('scalars array must be given as a string name for multiblock datasets.') the_arguments = locals() the_arguments.pop('self') the_arguments.pop('mesh') the_arguments.pop('kwargs') if multi_colors: # Compute unique colors for each index of the block if has_matplotlib: from itertools import cycle cycler = matplotlib.rcParams['axes.prop_cycle'] colors = cycle(cycler) else: multi_colors = False logging.warning('Please install matplotlib for color cycles') # Now iteratively plot each element of the multiblock dataset actors = [] for idx in range(mesh.GetNumberOfBlocks()): if mesh[idx] is None: continue # Get a good name to use next_name = '{}-{}'.format(name, idx) # Get the data object if not is_pyvista_dataset(mesh[idx]): data = wrap(mesh.GetBlock(idx)) if not is_pyvista_dataset(mesh[idx]): continue # move on if we can't plot it else: data = mesh.GetBlock(idx) if data is None or (not isinstance(data, pyvista.MultiBlock) and data.n_points < 1): # Note that a block can exist but be None type # or it could have zeros points (be empty) after filtering continue # Now check that scalars is available for this dataset if isinstance(data, vtk.vtkMultiBlockDataSet) or get_array(data, scalars) is None: ts = None else: ts = scalars if multi_colors: color = next(colors)['color'] ## Add to the scene the_arguments['color'] = color the_arguments['scalars'] = ts the_arguments['name'] = next_name the_arguments['texture'] = None a = self.add_mesh(data, **the_arguments) actors.append(a) if (reset_camera is None and not self.camera_set) or reset_camera: cpos = self.get_default_cam_pos() self.camera_position = cpos self.camera_set = False self.reset_camera() return actors ##### Plot a single PyVista mesh ##### # Compute surface normals if using smooth shading if smooth_shading: # extract surface if mesh is exterior if not isinstance(mesh, pyvista.PolyData): grid = mesh mesh = grid.extract_surface() ind = mesh.point_arrays['vtkOriginalPointIds'] # remap scalars if isinstance(scalars, np.ndarray): scalars = scalars[ind] mesh.compute_normals(cell_normals=False, inplace=True) if mesh.n_points < 1: raise ValueError('Empty meshes cannot be plotted. Input mesh has zero points.') # Try to plot something if no preference given if scalars is None and color is None and texture is None: # Prefer texture first if len(list(mesh.textures.keys())) > 0: texture = True # If no texture, plot any active scalar else: # Make sure scalars components are not vectors/tuples scalars = mesh.active_scalars_name # Don't allow plotting of string arrays by default if scalars is not None:# and np.issubdtype(mesh.active_scalars.dtype, np.number): if stitle is None: stitle = scalars else: scalars = None # set main values self.mesh = mesh self.mapper = make_mapper(vtk.vtkDataSetMapper) self.mapper.SetInputData(self.mesh) self.mapper.GetLookupTable().SetNumberOfTableValues(n_colors) if interpolate_before_map: self.mapper.InterpolateScalarsBeforeMappingOn() actor, prop = self.add_actor(self.mapper, reset_camera=reset_camera, name=name, culling=culling, pickable=pickable) # Make sure scalars is a numpy array after this point original_scalar_name = None if isinstance(scalars, str): self.mapper.SetArrayName(scalars) original_scalar_name = scalars scalars = get_array(mesh, scalars, preference=preference, err=True) if stitle is None: stitle = original_scalar_name if texture is True or isinstance(texture, (str, int)): texture = mesh._activate_texture(texture) if texture: if isinstance(texture, np.ndarray): texture = numpy_to_texture(texture) if not isinstance(texture, (vtk.vtkTexture, vtk.vtkOpenGLTexture)): raise TypeError('Invalid texture type ({})'.format(type(texture))) if mesh.GetPointData().GetTCoords() is None: raise ValueError('Input mesh does not have texture coordinates to support the texture.') actor.SetTexture(texture) # Set color to white by default when using a texture if color is None: color = 'white' if scalars is None: show_scalar_bar = False self.mapper.SetScalarModeToUsePointFieldData() # Handle making opacity array ========================================= _custom_opac = False if isinstance(opacity, str): try: # Get array from mesh opacity = get_array(mesh, opacity, preference=preference, err=True) opacity = normalize(opacity) _custom_opac = True except: # Or get opacity transfer function opacity = opacity_transfer_function(opacity, n_colors) else: if scalars.shape[0] != opacity.shape[0]: raise ValueError('Opacity array and scalars array must have the same number of elements.') elif isinstance(opacity, (np.ndarray, list, tuple)): opacity = np.array(opacity) if scalars.shape[0] == opacity.shape[0]: # User could pass an array of opacities for every point/cell pass else: opacity = opacity_transfer_function(opacity, n_colors) if use_transparency and np.max(opacity) <= 1.0: opacity = 1 - opacity elif use_transparency and isinstance(opacity, np.ndarray): opacity = 255 - opacity # Scalars formatting ================================================== if cmap is None: # Set default map if matplotlib is available if has_matplotlib: cmap = rcParams['cmap'] # Set the array title for when it is added back to the mesh if _custom_opac: title = '__custom_rgba' elif stitle is None: title = 'Data' else: title = stitle if scalars is not None: # if scalars is a string, then get the first array found with that name if not isinstance(scalars, np.ndarray): scalars = np.asarray(scalars) _using_labels = False if not np.issubdtype(scalars.dtype, np.number): # raise TypeError('Non-numeric scalars are currently not supported for plotting.') # TODO: If str array, digitive and annotate cats, scalars = np.unique(scalars.astype('|S'), return_inverse=True) values = np.unique(scalars) clim = [np.min(values) - 0.5, np.max(values) + 0.5] title = '{}-digitized'.format(title) n_colors = len(cats) scalar_bar_args.setdefault('n_labels', 0) _using_labels = True if rgb: if scalars.ndim != 2 or scalars.shape[1] < 3 or scalars.shape[1] > 4: raise ValueError('RGB array must be n_points/n_cells by 3/4 in shape.') if scalars.ndim != 1: if rgb: pass elif scalars.ndim == 2 and (scalars.shape[0] == mesh.n_points or scalars.shape[0] == mesh.n_cells): scalars = np.linalg.norm(scalars.copy(), axis=1) title = '{}-normed'.format(title) else: scalars = scalars.ravel() if scalars.dtype == np.bool_: scalars = scalars.astype(np.float_) def prepare_mapper(scalars): # Scalars interpolation approach if scalars.shape[0] == mesh.n_points: self.mesh._add_point_array(scalars, title, True) self.mapper.SetScalarModeToUsePointData() elif scalars.shape[0] == mesh.n_cells: self.mesh._add_cell_array(scalars, title, True) self.mapper.SetScalarModeToUseCellData() else: raise_not_matching(scalars, mesh) # Common tasks self.mapper.GetLookupTable().SetNumberOfTableValues(n_colors) if interpolate_before_map: self.mapper.InterpolateScalarsBeforeMappingOn() if rgb or _custom_opac: self.mapper.SetColorModeToDirectScalars() else: self.mapper.SetColorModeToMapScalars() return prepare_mapper(scalars) table = self.mapper.GetLookupTable() if log_scale: table.SetScaleToLog10() if _using_labels: table.SetAnnotations(convert_array(values), convert_string_array(cats)) if isinstance(annotations, dict): for val, anno in annotations.items(): table.SetAnnotation(float(val), str(anno)) # Set scalars range if clim is None: clim = [np.nanmin(scalars), np.nanmax(scalars)] elif isinstance(clim, float) or isinstance(clim, int): clim = [-clim, clim] if np.any(clim) and not rgb: self.mapper.scalar_range = clim[0], clim[1] table.SetNanColor(nan_color) if above_color: table.SetUseAboveRangeColor(True) table.SetAboveRangeColor(*parse_color(above_color, opacity=1)) scalar_bar_args.setdefault('above_label', 'Above') if below_color: table.SetUseBelowRangeColor(True) table.SetBelowRangeColor(*parse_color(below_color, opacity=1)) scalar_bar_args.setdefault('below_label', 'Below') if cmap is not None: if not has_matplotlib: cmap = None logging.warning('Please install matplotlib for color maps.') cmap = get_cmap_safe(cmap) if categories: if categories is True: n_colors = len(np.unique(scalars)) elif isinstance(categories, int): n_colors = categories ctable = cmap(np.linspace(0, 1, n_colors))*255 ctable = ctable.astype(np.uint8) # Set opactities if isinstance(opacity, np.ndarray) and not _custom_opac: ctable[:,-1] = opacity if flip_scalars: ctable = np.ascontiguousarray(ctable[::-1]) table.SetTable(VN.numpy_to_vtk(ctable)) if _custom_opac: hue = normalize(scalars, minimum=clim[0], maximum=clim[1]) scalars = cmap(hue)[:, :3] # combine colors and alpha into a Nx4 matrix scalars = np.concatenate((scalars, opacity[:, None]), axis=1) scalars = (scalars * 255).astype(np.uint8) prepare_mapper(scalars) else: # no cmap specified if flip_scalars: table.SetHueRange(0.0, 0.66667) else: table.SetHueRange(0.66667, 0.0) else: self.mapper.SetScalarModeToUseFieldData() # Set actor properties ================================================ # select view style if not style: style = 'surface' style = style.lower() if style == 'wireframe': prop.SetRepresentationToWireframe() if color is None: color = rcParams['outline_color'] elif style == 'points': prop.SetRepresentationToPoints() elif style == 'surface': prop.SetRepresentationToSurface() else: raise ValueError('Invalid style. Must be one of the following:\n' '\t"surface"\n' '\t"wireframe"\n' '\t"points"\n') prop.SetPointSize(point_size) prop.SetAmbient(ambient) prop.SetDiffuse(diffuse) prop.SetSpecular(specular) prop.SetSpecularPower(specular_power) if smooth_shading: prop.SetInterpolationToPhong() else: prop.SetInterpolationToFlat() # edge display style if show_edges: prop.EdgeVisibilityOn() rgb_color = parse_color(color) prop.SetColor(rgb_color) if isinstance(opacity, (float, int)): prop.SetOpacity(opacity) prop.SetEdgeColor(parse_color(edge_color)) if render_points_as_spheres: prop.SetRenderPointsAsSpheres(render_points_as_spheres) if render_lines_as_tubes: prop.SetRenderLinesAsTubes(render_lines_as_tubes) # legend label if label: if not isinstance(label, str): raise TypeError('Label must be a string') geom = pyvista.single_triangle() if scalars is not None: geom = pyvista.Box() rgb_color = parse_color('black') geom.points -= geom.center self._labels.append([geom, label, rgb_color]) # lighting display style if not lighting: prop.LightingOff() # set line thickness if line_width: prop.SetLineWidth(line_width) # Add scalar bar if available if stitle is not None and show_scalar_bar and (not rgb or _custom_opac): self.add_scalar_bar(stitle, **scalar_bar_args) self.renderer.Modified() return actor def add_volume(self, volume, scalars=None, clim=None, resolution=None, opacity='linear', n_colors=256, cmap=None, flip_scalars=False, reset_camera=None, name=None, ambient=0.0, categories=False, culling=False, multi_colors=False, blending='composite', mapper=None, stitle=None, scalar_bar_args=None, show_scalar_bar=None, annotations=None, pickable=True, preference="point", opacity_unit_distance=None, shade=False, diffuse=0.7, specular=0.2, specular_power=10.0, **kwargs): """Add a volume, rendered using a smart mapper by default. Requires a 3D :class:`numpy.ndarray` or :class:`pyvista.UniformGrid`. Parameters ---------- volume : 3D numpy.ndarray or pyvista.UniformGrid The input volume to visualize. 3D numpy arrays are accepted. scalars : str or numpy.ndarray, optional Scalars used to "color" the mesh. Accepts a string name of an array that is present on the mesh or an array equal to the number of cells or the number of points in the mesh. Array should be sized as a single vector. If ``scalars`` is ``None``, then the active scalars are used. clim : 2 item list, optional Color bar range for scalars. Defaults to minimum and maximum of scalars array. Example: ``[-1, 2]``. ``rng`` is also an accepted alias for this. opacity : string or numpy.ndarray, optional Opacity mapping for the scalars array. A string can also be specified to map the scalars range to a predefined opacity transfer function (options include: 'linear', 'linear_r', 'geom', 'geom_r'). Or you can pass a custum made transfer function that is an array either ``n_colors`` in length or shorter. n_colors : int, optional Number of colors to use when displaying scalars. Defaults to 256. The scalar bar will also have this many colors. cmap : str, optional Name of the Matplotlib colormap to us when mapping the ``scalars``. See available Matplotlib colormaps. Only applicable for when displaying ``scalars``. Requires Matplotlib to be installed. ``colormap`` is also an accepted alias for this. If ``colorcet`` or ``cmocean`` are installed, their colormaps can be specified by name. flip_scalars : bool, optional Flip direction of cmap. Most colormaps allow ``*_r`` suffix to do this as well. reset_camera : bool, optional Reset the camera after adding this mesh to the scene name : str, optional The name for the added actor so that it can be easily updated. If an actor of this name already exists in the rendering window, it will be replaced by the new actor. ambient : float, optional When lighting is enabled, this is the amount of light from 0 to 1 that reaches the actor when not directed at the light source emitted from the viewer. Default 0.0. culling : str, optional Does not render faces that are culled. Options are ``'front'`` or ``'back'``. This can be helpful for dense surface meshes, especially when edges are visible, but can cause flat meshes to be partially displayed. Defaults ``False``. categories : bool, optional If set to ``True``, then the number of unique values in the scalar array will be used as the ``n_colors`` argument. multi_colors : bool, optional Whether or not to use multiple colors when plotting MultiBlock object. Blocks will be colored sequentially as 'Reds', 'Greens', 'Blues', and 'Grays'. blending : str, optional Blending mode for visualisation of the input object(s). Can be one of 'additive', 'maximum', 'minimum', 'composite', or 'average'. Defaults to 'additive'. mapper : str, optional Volume mapper to use given by name. Options include: ``'fixed_point'``, ``'gpu'``, ``'open_gl'``, and ``'smart'``. If ``None`` the ``"volume_mapper"`` in the ``rcParams`` is used. scalar_bar_args : dict, optional Dictionary of keyword arguments to pass when adding the scalar bar to the scene. For options, see :func:`pyvista.BasePlotter.add_scalar_bar`. show_scalar_bar : bool If False, a scalar bar will not be added to the scene. Defaults to ``True``. stitle : string, optional Scalar bar title. By default the scalar bar is given a title of the the scalars array used to color the mesh. To create a bar with no title, use an empty string (i.e. ''). annotations : dict, optional Pass a dictionary of annotations. Keys are the float values in the scalars range to annotate on the scalar bar and the values are the the string annotations. opacity_unit_distance : float Set/Get the unit distance on which the scalar opacity transfer function is defined. Meaning that over that distance, a given opacity (from the transfer function) is accumulated. This is adjusted for the actual sampling distance during rendering. By default, this is the length of the diagonal of the bounding box of the volume divided by the dimensions. shade : bool Default off. If shading is turned on, the mapper may perform shading calculations - in some cases shading does not apply (for example, in a maximum intensity projection) and therefore shading will not be performed even if this flag is on. diffuse : float, optional The diffuse lighting coefficient. Default 1.0 specular : float, optional The specular lighting coefficient. Default 0.0 specular_power : float, optional The specular power. Between 0.0 and 128.0 Return ------ actor: vtk.vtkVolume VTK volume of the input data. """ # Handle default arguments # Supported aliases clim = kwargs.pop('rng', clim) cmap = kwargs.pop('colormap', cmap) culling = kwargs.pop("backface_culling", culling) if "scalar" in kwargs: raise TypeError("`scalar` is an invalid keyword argument for `add_mesh`. Perhaps you mean `scalars` with an s?") assert_empty_kwargs(**kwargs) if scalar_bar_args is None: scalar_bar_args = {} if show_scalar_bar is None: show_scalar_bar = rcParams['show_scalar_bar'] if culling is True: culling = 'backface' if mapper is None: mapper = rcParams["volume_mapper"] # Convert the VTK data object to a pyvista wrapped object if necessary if not is_pyvista_dataset(volume): if isinstance(volume, np.ndarray): volume = wrap(volume) if resolution is None: resolution = [1,1,1] elif len(resolution) != 3: raise ValueError('Invalid resolution dimensions.') volume.spacing = resolution else: volume = wrap(volume) if not is_pyvista_dataset(volume): raise TypeError('Object type ({}) not supported for plotting in PyVista.'.format(type(volume))) else: # HACK: Make a copy so the original object is not altered. # Also, place all data on the nodes as issues arise when # volume rendering on the cells. volume = volume.cell_data_to_point_data() if name is None: name = '{}({})'.format(type(volume).__name__, volume.memory_address) if isinstance(volume, pyvista.MultiBlock): from itertools import cycle cycler = cycle(['Reds', 'Greens', 'Blues', 'Greys', 'Oranges', 'Purples']) # Now iteratively plot each element of the multiblock dataset actors = [] for idx in range(volume.GetNumberOfBlocks()): if volume[idx] is None: continue # Get a good name to use next_name = '{}-{}'.format(name, idx) # Get the data object block = wrap(volume.GetBlock(idx)) if resolution is None: try: block_resolution = block.GetSpacing() except AttributeError: block_resolution = resolution else: block_resolution = resolution if multi_colors: color = next(cycler) else: color = cmap a = self.add_volume(block, resolution=block_resolution, opacity=opacity, n_colors=n_colors, cmap=color, flip_scalars=flip_scalars, reset_camera=reset_camera, name=next_name, ambient=ambient, categories=categories, culling=culling, clim=clim, mapper=mapper, pickable=pickable, opacity_unit_distance=opacity_unit_distance, shade=shade, diffuse=diffuse, specular=specular, specular_power=specular_power) actors.append(a) return actors if not isinstance(volume, pyvista.UniformGrid): raise TypeError('Type {} not supported for volume rendering at this time. Use `pyvista.UniformGrid`.'.format(type(volume))) if opacity_unit_distance is None: opacity_unit_distance = volume.length / (np.mean(volume.dimensions) - 1) if scalars is None: # Make sure scalars components are not vectors/tuples scalars = volume.active_scalars # Don't allow plotting of string arrays by default if scalars is not None and np.issubdtype(scalars.dtype, np.number): if stitle is None: stitle = volume.active_scalars_info[1] else: raise ValueError('No scalars to use for volume rendering.') elif isinstance(scalars, str): pass ############## title = 'Data' if stitle is None else stitle if isinstance(scalars, str): title = scalars scalars = get_array(volume, scalars, preference=preference, err=True) if stitle is None: stitle = title if not isinstance(scalars, np.ndarray): scalars = np.asarray(scalars) if not np.issubdtype(scalars.dtype, np.number): raise TypeError('Non-numeric scalars are currently not supported for volume rendering.') if scalars.ndim != 1: scalars = scalars.ravel() if scalars.dtype == np.bool_ or scalars.dtype == np.uint8: scalars = scalars.astype(np.float_) # Define mapper, volume, and add the correct properties mappers = { 'fixed_point': vtk.vtkFixedPointVolumeRayCastMapper, 'gpu': vtk.vtkGPUVolumeRayCastMapper, 'open_gl': vtk.vtkOpenGLGPUVolumeRayCastMapper, 'smart': vtk.vtkSmartVolumeMapper, } if not isinstance(mapper, str) or mapper not in mappers.keys(): raise TypeError('Mapper ({}) unknown. Available volume mappers include: {}'.format(mapper, ', '.join(mappers.keys()))) self.mapper = make_mapper(mappers[mapper]) # Scalars interpolation approach if scalars.shape[0] == volume.n_points: volume._add_point_array(scalars, title, True) self.mapper.SetScalarModeToUsePointData() elif scalars.shape[0] == volume.n_cells: volume._add_cell_array(scalars, title, True) self.mapper.SetScalarModeToUseCellData() else: raise_not_matching(scalars, volume) # Set scalars range if clim is None: clim = [np.nanmin(scalars), np.nanmax(scalars)] elif isinstance(clim, float) or isinstance(clim, int): clim = [-clim, clim] ############### scalars = scalars.astype(np.float_) with np.errstate(invalid='ignore'): idxs0 = scalars < clim[0] idxs1 = scalars > clim[1] scalars[idxs0] = clim[0] scalars[idxs1] = clim[1] scalars = ((scalars - np.nanmin(scalars)) / (np.nanmax(scalars) - np.nanmin(scalars))) * 255 # scalars = scalars.astype(np.uint8) volume[title] = scalars self.mapper.scalar_range = clim # Set colormap and build lookup table table = vtk.vtkLookupTable() # table.SetNanColor(nan_color) # NaN's are chopped out with current implementation # above/below colors not supported with volume rendering if isinstance(annotations, dict): for val, anno in annotations.items(): table.SetAnnotation(float(val), str(anno)) if cmap is None: # Set default map if matplotlib is available if has_matplotlib: cmap = rcParams['cmap'] if cmap is not None: if not has_matplotlib: raise ImportError('Please install matplotlib for volume rendering.') cmap = get_cmap_safe(cmap) if categories: if categories is True: n_colors = len(np.unique(scalars)) elif isinstance(categories, int): n_colors = categories if flip_scalars: cmap = cmap.reversed() color_tf = vtk.vtkColorTransferFunction() for ii in range(n_colors): color_tf.AddRGBPoint(ii, *cmap(ii)[:-1]) # Set opacities if isinstance(opacity, (float, int)): opacity_values = [opacity] * n_colors elif isinstance(opacity, str): opacity_values = pyvista.opacity_transfer_function(opacity, n_colors) elif isinstance(opacity, (np.ndarray, list, tuple)): opacity = np.array(opacity) opacity_values = opacity_transfer_function(opacity, n_colors) opacity_tf = vtk.vtkPiecewiseFunction() for ii in range(n_colors): opacity_tf.AddPoint(ii, opacity_values[ii] / n_colors) # Now put color tf and opacity tf into a lookup table for the scalar bar table.SetNumberOfTableValues(n_colors) lut = cmap(np.array(range(n_colors))) * 255 lut[:,3] = opacity_values lut = lut.astype(np.uint8) table.SetTable(VN.numpy_to_vtk(lut)) table.SetRange(*clim) self.mapper.lookup_table = table self.mapper.SetInputData(volume) blending = blending.lower() if blending in ['additive', 'add', 'sum']: self.mapper.SetBlendModeToAdditive() elif blending in ['average', 'avg', 'average_intensity']: self.mapper.SetBlendModeToAverageIntensity() elif blending in ['composite', 'comp']: self.mapper.SetBlendModeToComposite() elif blending in ['maximum', 'max', 'maximum_intensity']: self.mapper.SetBlendModeToMaximumIntensity() elif blending in ['minimum', 'min', 'minimum_intensity']: self.mapper.SetBlendModeToMinimumIntensity() else: raise ValueError('Blending mode \'{}\' invalid. '.format(blending) + 'Please choose one ' + 'of \'additive\', ' '\'composite\', \'minimum\' or ' + '\'maximum\'.') self.mapper.Update() self.volume = vtk.vtkVolume() self.volume.SetMapper(self.mapper) prop = vtk.vtkVolumeProperty() prop.SetColor(color_tf) prop.SetScalarOpacity(opacity_tf) prop.SetAmbient(ambient) prop.SetScalarOpacityUnitDistance(opacity_unit_distance) prop.SetShade(shade) prop.SetDiffuse(diffuse) prop.SetSpecular(specular) prop.SetSpecularPower(specular_power) self.volume.SetProperty(prop) actor, prop = self.add_actor(self.volume, reset_camera=reset_camera, name=name, culling=culling, pickable=pickable) # Add scalar bar if stitle is not None and show_scalar_bar: self.add_scalar_bar(stitle, **scalar_bar_args) self.renderer.Modified() return actor def update_scalar_bar_range(self, clim, name=None): """Update the value range of the active or named scalar bar. Parameters ---------- 2 item list The new range of scalar bar. Example: ``[-1, 2]``. name : str, optional The title of the scalar bar to update """ if isinstance(clim, float) or isinstance(clim, int): clim = [-clim, clim] if len(clim) != 2: raise TypeError('clim argument must be a length 2 iterable of values: (min, max).') if name is None: if not hasattr(self, 'mapper'): raise AttributeError('This plotter does not have an active mapper.') self.mapper.scalar_range = clim return # Use the name to find the desired actor def update_mapper(mapper_helper): mapper_helper.scalar_range = clim return try: for mh in self._scalar_bar_mappers[name]: update_mapper(mh) except KeyError: raise KeyError('Name ({}) not valid/not found in this plotter.') return def clear(self): """Clear plot by removing all actors and properties.""" for renderer in self.renderers: renderer.clear() for renderer in self._background_renderers: if renderer is not None: renderer.clear() self._scalar_bar_slots = set(range(MAX_N_COLOR_BARS)) self._scalar_bar_slot_lookup = {} self._scalar_bar_ranges = {} self._scalar_bar_mappers = {} self._scalar_bar_actors = {} self._scalar_bar_widgets = {} self.mesh = None def link_views(self, views=0): """Link the views' cameras. Parameters ---------- views : int | tuple or list If ``views`` is int, link the views to the given view index or if ``views`` is a tuple or a list, link the given views cameras. """ if isinstance(views, (int, np.integer)): for renderer in self.renderers: renderer.camera = self.renderers[views].camera return views = np.asarray(views) if np.issubdtype(views.dtype, np.integer): for view_index in views: self.renderers[view_index].camera = \ self.renderers[views[0]].camera else: raise TypeError('Expected type is int, list or tuple:' '{} is given'.format(type(views))) def unlink_views(self, views=None): """Unlink the views' cameras. Parameters ---------- views : None | int | tuple or list If ``views`` is None unlink all the views, if ``views`` is int unlink the selected view's camera or if ``views`` is a tuple or a list, unlink the given views cameras. """ if views is None: for renderer in self.renderers: renderer.camera = vtk.vtkCamera() renderer.reset_camera() elif isinstance(views, int): self.renderers[views].camera = vtk.vtkCamera() self.renderers[views].reset_camera() elif isinstance(views, collections.abc.Iterable): for view_index in views: self.renderers[view_index].camera = vtk.vtkCamera() self.renderers[view_index].reset_camera() else: raise TypeError('Expected type is None, int, list or tuple:' '{} is given'.format(type(views))) def add_scalar_bar(self, title=None, n_labels=5, italic=False, bold=False, title_font_size=None, label_font_size=None, color=None, font_family=None, shadow=False, mapper=None, width=None, height=None, position_x=None, position_y=None, vertical=None, interactive=None, fmt=None, use_opacity=True, outline=False, nan_annotation=False, below_label=None, above_label=None, background_color=None, n_colors=None, fill=False): """Create scalar bar using the ranges as set by the last input mesh. Parameters ---------- title : string, optional Title of the scalar bar. Default None n_labels : int, optional Number of labels to use for the scalar bar. italic : bool, optional Italicises title and bar labels. Default False. bold : bool, optional Bolds title and bar labels. Default True title_font_size : float, optional Sets the size of the title font. Defaults to None and is sized automatically. label_font_size : float, optional Sets the size of the title font. Defaults to None and is sized automatically. color : string or 3 item list, optional, defaults to white Either a string, rgb list, or hex color string. For example: color='white' color='w' color=[1, 1, 1] color='#FFFFFF' font_family : string, optional Font family. Must be either courier, times, or arial. shadow : bool, optional Adds a black shadow to the text. Defaults to False width : float, optional The percentage (0 to 1) width of the window for the colorbar height : float, optional The percentage (0 to 1) height of the window for the colorbar position_x : float, optional The percentage (0 to 1) along the windows's horizontal direction to place the bottom left corner of the colorbar position_y : float, optional The percentage (0 to 1) along the windows's vertical direction to place the bottom left corner of the colorbar interactive : bool, optional Use a widget to control the size and location of the scalar bar. use_opacity : bool, optional Optionally display the opacity mapping on the scalar bar outline : bool, optional Optionally outline the scalar bar to make opacity mappings more obvious. nan_annotation : bool, optional Annotate the NaN color below_label : str, optional String annotation for values below the scalars range above_label : str, optional String annotation for values above the scalars range background_color : array, optional The color used for the background in RGB format. n_colors : int, optional The maximum number of color displayed in the scalar bar. fill : bool Draw a filled box behind the scalar bar with the ``background_color`` Notes ----- Setting title_font_size, or label_font_size disables automatic font sizing for both the title and label. """ if interactive is None: interactive = rcParams['interactive'] if font_family is None: font_family = rcParams['font']['family'] if label_font_size is None: label_font_size = rcParams['font']['label_size'] if title_font_size is None: title_font_size = rcParams['font']['title_size'] if color is None: color = rcParams['font']['color'] if fmt is None: fmt = rcParams['font']['fmt'] if vertical is None: if rcParams['colorbar_orientation'].lower() == 'vertical': vertical = True # Automatically choose size if not specified if width is None: if vertical: width = rcParams['colorbar_vertical']['width'] else: width = rcParams['colorbar_horizontal']['width'] if height is None: if vertical: height = rcParams['colorbar_vertical']['height'] else: height = rcParams['colorbar_horizontal']['height'] # check if maper exists if mapper is None: if not hasattr(self, 'mapper') or self.mapper is None: raise AttributeError('Mapper does not exist. ' 'Add a mesh with scalars first.') mapper = self.mapper if title: # Check that this data hasn't already been plotted if title in list(self._scalar_bar_ranges.keys()): clim = list(self._scalar_bar_ranges[title]) newrng = mapper.scalar_range oldmappers = self._scalar_bar_mappers[title] # get max for range and reset everything if newrng[0] < clim[0]: clim[0] = newrng[0] if newrng[1] > clim[1]: clim[1] = newrng[1] for mh in oldmappers: mh.scalar_range = clim[0], clim[1] mapper.scalar_range = clim[0], clim[1] self._scalar_bar_mappers[title].append(mapper) self._scalar_bar_ranges[title] = clim # Color bar already present and ready to be used so returning return # Automatically choose location if not specified if position_x is None or position_y is None: try: slot = min(self._scalar_bar_slots) self._scalar_bar_slots.remove(slot) self._scalar_bar_slot_lookup[title] = slot except: raise RuntimeError('Maximum number of color bars reached.') if position_x is None: if vertical: position_x = rcParams['colorbar_vertical']['position_x'] position_x -= slot * (width + 0.2 * width) else: position_x = rcParams['colorbar_horizontal']['position_x'] if position_y is None: if vertical: position_y = rcParams['colorbar_vertical']['position_y'] else: position_y = rcParams['colorbar_horizontal']['position_y'] position_y += slot * height # Adjust to make sure on the screen if position_x + width > 1: position_x -= width if position_y + height > 1: position_y -= height # parse color color = parse_color(color) # Create scalar bar self.scalar_bar = vtk.vtkScalarBarActor() if background_color is not None: background_color = parse_color(background_color, opacity=1.0) background_color = np.array(background_color) * 255 self.scalar_bar.GetBackgroundProperty().SetColor(background_color[0:3]) if fill: self.scalar_bar.DrawBackgroundOn() lut = vtk.vtkLookupTable() lut.DeepCopy(mapper.lookup_table) ctable = vtk_to_numpy(lut.GetTable()) alphas = ctable[:, -1][:, np.newaxis] / 255. use_table = ctable.copy() use_table[:, -1] = 255. ctable = (use_table * alphas) + background_color * (1 - alphas) lut.SetTable(numpy_to_vtk(ctable, array_type=vtk.VTK_UNSIGNED_CHAR)) else: lut = mapper.lookup_table self.scalar_bar.SetLookupTable(lut) if n_colors is not None: self.scalar_bar.SetMaximumNumberOfColors(n_colors) if n_labels < 1: self.scalar_bar.DrawTickLabelsOff() else: self.scalar_bar.DrawTickLabelsOn() self.scalar_bar.SetNumberOfLabels(n_labels) if nan_annotation: self.scalar_bar.DrawNanAnnotationOn() if above_label: self.scalar_bar.DrawAboveRangeSwatchOn() self.scalar_bar.SetAboveRangeAnnotation(above_label) if below_label: self.scalar_bar.DrawBelowRangeSwatchOn() self.scalar_bar.SetBelowRangeAnnotation(below_label) # edit the size of the colorbar self.scalar_bar.SetHeight(height) self.scalar_bar.SetWidth(width) self.scalar_bar.SetPosition(position_x, position_y) if fmt is not None: self.scalar_bar.SetLabelFormat(fmt) if vertical: self.scalar_bar.SetOrientationToVertical() else: self.scalar_bar.SetOrientationToHorizontal() if label_font_size is not None or title_font_size is not None: self.scalar_bar.UnconstrainedFontSizeOn() self.scalar_bar.AnnotationTextScalingOn() label_text = self.scalar_bar.GetLabelTextProperty() anno_text = self.scalar_bar.GetAnnotationTextProperty() label_text.SetColor(color) anno_text.SetColor(color) label_text.SetShadow(shadow) anno_text.SetShadow(shadow) # Set font label_text.SetFontFamily(parse_font_family(font_family)) anno_text.SetFontFamily(parse_font_family(font_family)) label_text.SetItalic(italic) anno_text.SetItalic(italic) label_text.SetBold(bold) anno_text.SetBold(bold) if label_font_size: label_text.SetFontSize(label_font_size) anno_text.SetFontSize(label_font_size) # Set properties if title: clim = mapper.scalar_range self._scalar_bar_ranges[title] = clim self._scalar_bar_mappers[title] = [mapper] self.scalar_bar.SetTitle(title) title_text = self.scalar_bar.GetTitleTextProperty() title_text.SetJustificationToCentered() title_text.SetItalic(italic) title_text.SetBold(bold) title_text.SetShadow(shadow) if title_font_size: title_text.SetFontSize(title_font_size) # Set font title_text.SetFontFamily(parse_font_family(font_family)) # set color title_text.SetColor(color) self._scalar_bar_actors[title] = self.scalar_bar if interactive is None: interactive = rcParams['interactive'] if self.shape != (1, 1): interactive = False elif interactive and self.shape != (1, 1): raise ValueError('Interactive scalar bars disabled for multi-renderer plots') if interactive and hasattr(self, 'iren'): self.scalar_widget = vtk.vtkScalarBarWidget() self.scalar_widget.SetScalarBarActor(self.scalar_bar) self.scalar_widget.SetInteractor(self.iren) self.scalar_widget.SetEnabled(1) rep = self.scalar_widget.GetRepresentation() # self.scalar_widget.On() if vertical is True or vertical is None: rep.SetOrientation(1) # 0 = Horizontal, 1 = Vertical else: rep.SetOrientation(0) # 0 = Horizontal, 1 = Vertical self._scalar_bar_widgets[title] = self.scalar_widget if use_opacity: self.scalar_bar.SetUseOpacity(True) if outline: self.scalar_bar.SetDrawFrame(True) frame_prop = self.scalar_bar.GetFrameProperty() frame_prop.SetColor(color) else: self.scalar_bar.SetDrawFrame(False) self.add_actor(self.scalar_bar, reset_camera=False, pickable=False) return self.scalar_bar # return the actor def update_scalars(self, scalars, mesh=None, render=True): """Update scalars of an object in the plotter. Parameters ---------- scalars : np.ndarray Scalars to replace existing scalars. mesh : vtk.PolyData or vtk.UnstructuredGrid, optional Object that has already been added to the Plotter. If None, uses last added mesh. render : bool, optional Forces an update to the render window. Default True. """ if mesh is None: mesh = self.mesh if isinstance(mesh, (collections.abc.Iterable, pyvista.MultiBlock)): # Recursive if need to update scalars on many meshes for m in mesh: self.update_scalars(scalars, mesh=m, render=False) if render: self.render() return if isinstance(scalars, str): # Grab scalars array if name given scalars = get_array(mesh, scalars) if scalars is None: if render: self.render() return if scalars.shape[0] == mesh.GetNumberOfPoints(): data = mesh.GetPointData() elif scalars.shape[0] == mesh.GetNumberOfCells(): data = mesh.GetCellData() else: raise_not_matching(scalars, mesh) vtk_scalars = data.GetScalars() if vtk_scalars is None: raise ValueError('No active scalars') s = convert_array(vtk_scalars) s[:] = scalars data.Modified() try: # Why are the points updated here? Not all datasets have points # and only the scalars array is modified by this function... mesh.GetPoints().Modified() except: pass if render: self.render() def update_coordinates(self, points, mesh=None, render=True): """Update the points of an object in the plotter. Parameters ---------- points : np.ndarray Points to replace existing points. mesh : vtk.PolyData or vtk.UnstructuredGrid, optional Object that has already been added to the Plotter. If None, uses last added mesh. render : bool, optional Forces an update to the render window. Default True. """ if mesh is None: mesh = self.mesh mesh.points = points if render: self.render() def _clear_ren_win(self): """Clear the render window.""" if hasattr(self, 'ren_win'): self.ren_win.Finalize() del self.ren_win def close(self): """Close the render window.""" # must close out widgets first super().close() # Renderer has an axes widget, so close it for renderer in self.renderers: renderer.close() # Grab screenshots of last render if self._store_image: self.last_image = self.screenshot(None, return_img=True) self.last_image_depth = self.get_image_depth() if hasattr(self, 'scalar_widget'): del self.scalar_widget # reset scalar bar stuff self.clear() self._clear_ren_win() if hasattr(self, '_style'): del self._style if hasattr(self, 'iren'): # self.iren.RemoveAllObservers() for obs in self._observers.values(): self.iren.RemoveObservers(obs) del self._observers self.iren.TerminateApp() del self.iren if hasattr(self, 'textActor'): del self.textActor # end movie if hasattr(self, 'mwriter'): try: self.mwriter.close() except BaseException: pass # this helps managing closed plotters self._closed = True def deep_clean(self): """Clean the plotter of the memory.""" for renderer in self.renderers: renderer.deep_clean() for renderer in self._background_renderers: if renderer is not None: renderer.deep_clean() # Do not remove the renderers on the clean self.mesh = None self.mapper = None def add_text(self, text, position='upper_left', font_size=18, color=None, font=None, shadow=False, name=None, viewport=False): """Add text to plot object in the top left corner by default. Parameters ---------- text : str The text to add the rendering position : str, tuple(float) Position to place the bottom left corner of the text box. If tuple is used, the position of the text uses the pixel coordinate system (default). In this case, it returns a more general `vtkOpenGLTextActor`. If string name is used, it returns a `vtkCornerAnnotation` object normally used for fixed labels (like title or xlabel). Default is to find the top left corner of the rendering window and place text box up there. Available position: ``'lower_left'``, ``'lower_right'``, ``'upper_left'``, ``'upper_right'``, ``'lower_edge'``, ``'upper_edge'``, ``'right_edge'``, and ``'left_edge'`` font : string, optional Font name may be courier, times, or arial shadow : bool, optional Adds a black shadow to the text. Defaults to False name : str, optional The name for the added actor so that it can be easily updated. If an actor of this name already exists in the rendering window, it will be replaced by the new actor. viewport: bool If True and position is a tuple of float, uses the normalized viewport coordinate system (values between 0.0 and 1.0 and support for HiDPI). Return ------ textActor : vtk.vtkTextActor Text actor added to plot """ if font is None: font = rcParams['font']['family'] if font_size is None: font_size = rcParams['font']['size'] if color is None: color = rcParams['font']['color'] if position is None: # Set the position of the text to the top left corner window_size = self.window_size x = (window_size[0] * 0.02) / self.shape[0] y = (window_size[1] * 0.85) / self.shape[0] position = [x, y] corner_mappings = { 'lower_left': vtk.vtkCornerAnnotation.LowerLeft, 'lower_right': vtk.vtkCornerAnnotation.LowerRight, 'upper_left': vtk.vtkCornerAnnotation.UpperLeft, 'upper_right': vtk.vtkCornerAnnotation.UpperRight, 'lower_edge': vtk.vtkCornerAnnotation.LowerEdge, 'upper_edge': vtk.vtkCornerAnnotation.UpperEdge, 'left_edge': vtk.vtkCornerAnnotation.LeftEdge, 'right_edge': vtk.vtkCornerAnnotation.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, (int, str, bool)): if isinstance(position, str): position = corner_mappings[position] elif position is True: position = corner_mappings['upper_left'] self.textActor = vtk.vtkCornerAnnotation() # This is how you set the font size with this actor self.textActor.SetLinearFontScaleFactor(font_size // 2) self.textActor.SetText(position, text) else: self.textActor = vtk.vtkTextActor() self.textActor.SetInput(text) self.textActor.SetPosition(position) if viewport: self.textActor.GetActualPositionCoordinate().SetCoordinateSystemToNormalizedViewport() self.textActor.GetActualPosition2Coordinate().SetCoordinateSystemToNormalizedViewport() self.textActor.GetTextProperty().SetFontSize(int(font_size * 2)) self.textActor.GetTextProperty().SetColor(parse_color(color)) self.textActor.GetTextProperty().SetFontFamily(FONT_KEYS[font]) self.textActor.GetTextProperty().SetShadow(shadow) self.add_actor(self.textActor, reset_camera=False, name=name, pickable=False) return self.textActor def open_movie(self, filename, framerate=24): """Establish a connection to the ffmpeg writer. Parameters ---------- filename : str Filename of the movie to open. Filename should end in mp4, but other filetypes may be supported. See "imagio.get_writer" framerate : int, optional Frames per second. """ if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename): filename = os.path.join(pyvista.FIGURE_PATH, filename) self.mwriter = imageio.get_writer(filename, fps=framerate) def open_gif(self, filename): """Open a gif file. Parameters ---------- filename : str Filename of the gif to open. Filename must end in gif. """ if filename[-3:] != 'gif': raise ValueError('Unsupported filetype. Must end in .gif') if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename): filename = os.path.join(pyvista.FIGURE_PATH, filename) self._gif_filename = os.path.abspath(filename) self.mwriter = imageio.get_writer(filename, mode='I') def write_frame(self): """Write a single frame to the movie file.""" if not hasattr(self, 'mwriter'): raise RuntimeError('This plotter has not opened a movie or GIF file.') self.mwriter.append_data(self.image) def _run_image_filter(self, ifilter): # Update filter and grab pixels ifilter.Modified() ifilter.Update() image = pyvista.wrap(ifilter.GetOutput()) img_size = image.dimensions img_array = pyvista.utilities.point_array(image, 'ImageScalars') # Reshape and write tgt_size = (img_size[1], img_size[0], -1) return img_array.reshape(tgt_size)[::-1] def get_image_depth(self, fill_value=np.nan, reset_camera_clipping_range=True): """Return a depth image representing current render window. Parameters ---------- fill_value : float Fill value for points in image that don't include objects in scene. To not use a fill value, pass ``None``. reset_camera_clipping_range : bool Reset the camera clipping range to include data in view? Return ------ image_depth : numpy.ndarray Image of depth values from camera orthogonal to image plane Notes ----- Values in image_depth are negative to adhere to a right-handed coordinate system. """ if not hasattr(self, 'ren_win') and hasattr(self, 'last_image_depth'): zval = self.last_image_depth.copy() if fill_value is not None: zval[self._image_depth_null] = fill_value return zval # Ensure points in view are within clipping range of renderer? if reset_camera_clipping_range: self.renderer.ResetCameraClippingRange() # Get the z-buffer image ifilter = vtk.vtkWindowToImageFilter() ifilter.SetInput(self.ren_win) ifilter.ReadFrontBufferOff() ifilter.SetInputBufferTypeToZBuffer() zbuff = self._run_image_filter(ifilter)[:, :, 0] # Convert z-buffer values to depth from camera with warnings.catch_warnings(): warnings.filterwarnings('ignore') near, far = self.camera.GetClippingRange() if self.camera.GetParallelProjection(): zval = (zbuff - near) / (far - near) else: zval = 2 * near * far / ((zbuff - 0.5) * 2 * (far - near) - near - far) # Consider image values outside clipping range as nans args = np.logical_or(zval < -far, np.isclose(zval, -far)) self._image_depth_null = args if fill_value is not None: zval[args] = fill_value return zval def add_lines(self, lines, color=(1, 1, 1), width=5, label=None, name=None): """Add lines to the plotting object. Parameters ---------- lines : np.ndarray or pyvista.PolyData Points representing line segments. For example, two line segments would be represented as: np.array([[0, 0, 0], [1, 0, 0], [1, 0, 0], [1, 1, 0]]) color : string or 3 item list, optional, defaults to white Either a string, rgb list, or hex color string. For example: color='white' color='w' color=[1, 1, 1] color='#FFFFFF' width : float, optional Thickness of lines name : str, optional The name for the added actor so that it can be easily updated. If an actor of this name already exists in the rendering window, it will be replaced by the new actor. Return ------ actor : vtk.vtkActor Lines actor. """ if not isinstance(lines, np.ndarray): raise TypeError('Input should be an array of point segments') lines = pyvista.lines_from_points(lines) # Create mapper and add lines mapper = vtk.vtkDataSetMapper() mapper.SetInputData(lines) rgb_color = parse_color(color) # legend label if label: if not isinstance(label, str): raise TypeError('Label must be a string') self._labels.append([lines, label, rgb_color]) # Create actor actor = vtk.vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetLineWidth(width) actor.GetProperty().EdgeVisibilityOn() actor.GetProperty().SetEdgeColor(rgb_color) actor.GetProperty().SetColor(rgb_color) actor.GetProperty().LightingOff() # Add to renderer self.add_actor(actor, reset_camera=False, name=name, pickable=False) return actor def remove_scalar_bar(self): """Remove the scalar bar.""" if hasattr(self, 'scalar_bar'): self.remove_actor(self.scalar_bar, reset_camera=False) def add_point_labels(self, points, labels, italic=False, bold=True, font_size=None, text_color=None, font_family=None, shadow=False, show_points=True, point_color=None, point_size=5, name=None, shape_color='grey', shape='rounded_rect', fill_shape=True, margin=3, shape_opacity=1.0, pickable=False, render_points_as_spheres=False, tolerance=0.001, reset_camera=None): """Create a point actor with one label from list labels assigned to each point. Parameters ---------- points : np.ndarray or pyvista.Common n x 3 numpy array of points or pyvista dataset with points labels : list or str List of labels. Must be the same length as points. If a string name is given with a pyvista.Common input for points, then these are fetched. italic : bool, optional Italicises title and bar labels. Default False. bold : bool, optional Bolds title and bar labels. Default True font_size : float, optional Sets the size of the title font. Defaults to 16. text_color : string or 3 item list, optional Color of text. Either a string, rgb list, or hex color string. text_color='white' text_color='w' text_color=[1, 1, 1] text_color='#FFFFFF' font_family : string, optional Font family. Must be either courier, times, or arial. shadow : bool, optional Adds a black shadow to the text. Defaults to False show_points : bool, optional Controls if points are visible. Default True point_color : string or 3 item list, optional. Color of points (if visible). Either a string, rgb list, or hex color string. For example: text_color='white' text_color='w' text_color=[1, 1, 1] text_color='#FFFFFF' point_size : float, optional Size of points (if visible) name : str, optional The name for the added actor so that it can be easily updated. If an actor of this name already exists in the rendering window, it will be replaced by the new actor. shape_color : string or 3 item list, optional. Color of points (if visible). Either a string, rgb list, or hex color string. For example: shape : str, optional The string name of the shape to use. Options are ``'rect'`` or ``'rounded_rect'``. If you want no shape, pass ``None`` fill_shape : bool, optional Fill the shape with the ``shape_color``. Outlines if ``False``. margin : int, optional The size of the margin on the label background shape. Default is 3. shape_opacity : float The opacity of the shape between zero and one. tolerance : float a tolerance to use to determine whether a point label is visible. A tolerance is usually required because the conversion from world space to display space during rendering introduces numerical round-off. reset_camera : bool, optional Reset the camera after adding the points to the scene. Return ------ labelActor : vtk.vtkActor2D VTK label actor. Can be used to change properties of the labels. """ if font_family is None: font_family = rcParams['font']['family'] if font_size is None: font_size = rcParams['font']['size'] if point_color is None: point_color = rcParams['color'] if text_color is None: text_color = rcParams['font']['color'] if isinstance(points, (list, tuple)): points = np.array(points) if isinstance(points, np.ndarray): vtkpoints = pyvista.PolyData(points) # Cast to poly data elif is_pyvista_dataset(points): vtkpoints = pyvista.PolyData(points.points) if isinstance(labels, str): labels = points.point_arrays[labels].astype(str) else: raise TypeError('Points type not usable: {}'.format(type(points))) if len(vtkpoints.points) != len(labels): raise ValueError('There must be one label for each point') if name is None: name = '{}({})'.format(type(vtkpoints).__name__, vtkpoints.memory_address) vtklabels = vtk.vtkStringArray() vtklabels.SetName('labels') for item in labels: vtklabels.InsertNextValue(str(item)) vtkpoints.GetPointData().AddArray(vtklabels) # Only show visible points vis_points = vtk.vtkSelectVisiblePoints() vis_points.SetInputData(vtkpoints) vis_points.SetRenderer(self.renderer) vis_points.SetTolerance(tolerance) # Create hierarchy hier = vtk.vtkPointSetToLabelHierarchy() hier.SetInputConnection(vis_points.GetOutputPort()) hier.SetLabelArrayName('labels') # create label mapper labelMapper = vtk.vtkLabelPlacementMapper() labelMapper.SetInputConnection(hier.GetOutputPort()) if not isinstance(shape, str): labelMapper.SetShapeToNone() elif shape.lower() in 'rect': labelMapper.SetShapeToRect() elif shape.lower() in 'rounded_rect': labelMapper.SetShapeToRoundedRect() else: raise ValueError('Shape ({}) not understood'.format(shape)) if fill_shape: labelMapper.SetStyleToFilled() else: labelMapper.SetStyleToOutline() labelMapper.SetBackgroundColor(parse_color(shape_color)) labelMapper.SetBackgroundOpacity(shape_opacity) labelMapper.SetMargin(margin) textprop = hier.GetTextProperty() textprop.SetItalic(italic) textprop.SetBold(bold) textprop.SetFontSize(font_size) textprop.SetFontFamily(parse_font_family(font_family)) textprop.SetColor(parse_color(text_color)) textprop.SetShadow(shadow) self.remove_actor('{}-points'.format(name), reset_camera=False) self.remove_actor('{}-labels'.format(name), reset_camera=False) # add points if show_points: style = 'points' else: style = 'surface' self.add_mesh(vtkpoints, style=style, color=point_color, point_size=point_size, name='{}-points'.format(name), pickable=pickable, render_points_as_spheres=render_points_as_spheres, reset_camera=reset_camera) labelActor = vtk.vtkActor2D() labelActor.SetMapper(labelMapper) self.add_actor(labelActor, reset_camera=False, name='{}-labels'.format(name), pickable=False) return labelActor def add_point_scalar_labels(self, points, labels, fmt=None, preamble='', **kwargs): """Label the points from a dataset with the values of their scalars. Wrapper for :func:`pyvista.BasePlotter.add_point_labels`. Parameters ---------- points : np.ndarray or pyvista.Common n x 3 numpy array of points or pyvista dataset with points labels : str String name of the point data array to use. fmt : str String formatter used to format numerical data """ if not is_pyvista_dataset(points): raise TypeError('input points must be a pyvista dataset, not: {}'.format(type(points))) if not isinstance(labels, str): raise TypeError('labels must be a string name of the scalars array to use') if fmt is None: fmt = rcParams['font']['fmt'] if fmt is None: fmt = '%.6e' scalars = points.point_arrays[labels] phrase = '{} {}'.format(preamble, '%.3e') labels = [phrase % val for val in scalars] return self.add_point_labels(points, labels, **kwargs) def add_points(self, points, **kwargs): """Add points to a mesh.""" kwargs['style'] = 'points' return self.add_mesh(points, **kwargs) def add_arrows(self, cent, direction, mag=1, **kwargs): """Add arrows to plotting object.""" direction = direction.copy() if cent.ndim != 2: cent = cent.reshape((-1, 3)) if direction.ndim != 2: direction = direction.reshape((-1, 3)) direction[:,0] *= mag direction[:,1] *= mag direction[:,2] *= mag pdata = pyvista.vector_poly_data(cent, direction) # Create arrow object arrow = vtk.vtkArrowSource() arrow.Update() glyph3D = vtk.vtkGlyph3D() glyph3D.SetSourceData(arrow.GetOutput()) glyph3D.SetInputData(pdata) glyph3D.SetVectorModeToUseVector() glyph3D.Update() arrows = wrap(glyph3D.GetOutput()) return self.add_mesh(arrows, **kwargs) @staticmethod def _save_image(image, filename, return_img=None): """Save a NumPy image array. This is an internal helper. """ if not image.size: raise ValueError('Empty image. Have you run plot() first?') # write screenshot to file supported_formats = [".png", ".jpeg", ".jpg", ".bmp", ".tif", ".tiff"] if isinstance(filename, str): if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename): filename = os.path.join(pyvista.FIGURE_PATH, filename) if not any([filename.lower().endswith(ext) for ext in supported_formats]): filename += ".png" filename = os.path.abspath(os.path.expanduser(filename)) w = imageio.imwrite(filename, image) if not return_img: return w return image def save_graphic(self, filename, title='PyVista Export', raster=True, painter=True): """Save a screenshot of the rendering window as a graphic file. The supported formats are: '.svg', '.eps', '.ps', '.pdf', '.tex' """ if not hasattr(self, 'ren_win'): raise AttributeError('This plotter is closed and unable to save a screenshot.') if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename): filename = os.path.join(pyvista.FIGURE_PATH, filename) filename = os.path.abspath(os.path.expanduser(filename)) extension = pyvista.fileio.get_ext(filename) valid = ['.svg', '.eps', '.ps', '.pdf', '.tex'] if extension not in valid: raise ValueError('Extension ({}) is an invalid choice. Valid options include: {}'.format(extension, ', '.join(valid))) writer = vtk.vtkGL2PSExporter() modes = { '.svg': writer.SetFileFormatToSVG, '.eps': writer.SetFileFormatToEPS, '.ps': writer.SetFileFormatToPS, '.pdf': writer.SetFileFormatToPDF, '.tex': writer.SetFileFormatToTeX, } writer.CompressOff() writer.SetFilePrefix(filename.replace(extension, '')) writer.SetInput(self.ren_win) modes[extension]() writer.SetTitle(title) writer.SetWrite3DPropsAsRasterImage(raster) if painter: writer.UsePainterSettings() writer.Update() return def screenshot(self, filename=None, transparent_background=None, return_img=None, window_size=None): """Take screenshot at current camera position. Parameters ---------- filename : str, optional Location to write image to. If None, no image is written. transparent_background : bool, optional Makes the background transparent. Default False. return_img : bool, optional If a string filename is given and this is true, a NumPy array of the image will be returned. Return ------ img : numpy.ndarray Array containing pixel RGB and alpha. Sized: [Window height x Window width x 3] for transparent_background=False [Window height x Window width x 4] for transparent_background=True Examples -------- >>> import pyvista >>> sphere = pyvista.Sphere() >>> plotter = pyvista.Plotter() >>> actor = plotter.add_mesh(sphere) >>> plotter.screenshot('screenshot.png') # doctest:+SKIP """ if window_size is not None: self.window_size = window_size # configure image filter if transparent_background is None: transparent_background = rcParams['transparent_background'] self.image_transparent_background = transparent_background # This if statement allows you to save screenshots of closed plotters # This is needed for the sphinx-gallery work if not hasattr(self, 'ren_win'): # If plotter has been closed... # check if last_image exists if hasattr(self, 'last_image'): # Save last image return self._save_image(self.last_image, filename, return_img) # Plotter hasn't been rendered or was improperly closed raise AttributeError('This plotter is closed and unable to save a screenshot.') self.render() # debug: this needs to be called twice for some reason, img = self.image img = self.image return self._save_image(img, filename, return_img) def add_legend(self, labels=None, bcolor=(0.5, 0.5, 0.5), border=False, size=None, name=None): """Add a legend to render window. Entries must be a list containing one string and color entry for each item. Parameters ---------- labels : list, optional When set to None, uses existing labels as specified by - add_mesh - add_lines - add_points List containing one entry for each item to be added to the legend. Each entry must contain two strings, [label, color], where label is the name of the item to add, and color is the color of the label to add. bcolor : list or string, optional Background color, either a three item 0 to 1 RGB color list, or a matplotlib color string (e.g. 'w' or 'white' for a white color). If None, legend background is disabled. border : bool, optional Controls if there will be a border around the legend. Default False. size : list, optional Two float list, each float between 0 and 1. For example [0.1, 0.1] would make the legend 10% the size of the entire figure window. name : str, optional The name for the added actor so that it can be easily updated. If an actor of this name already exists in the rendering window, it will be replaced by the new actor. Return ------ legend : vtk.vtkLegendBoxActor Actor for the legend. Examples -------- >>> import pyvista >>> from pyvista import examples >>> mesh = examples.load_hexbeam() >>> othermesh = examples.load_uniform() >>> plotter = pyvista.Plotter() >>> _ = plotter.add_mesh(mesh, label='My Mesh') >>> _ = plotter.add_mesh(othermesh, 'k', label='My Other Mesh') >>> _ = plotter.add_legend() >>> plotter.show() # doctest:+SKIP Alternative manual example >>> import pyvista >>> from pyvista import examples >>> mesh = examples.load_hexbeam() >>> othermesh = examples.load_uniform() >>> legend_entries = [] >>> legend_entries.append(['My Mesh', 'w']) >>> legend_entries.append(['My Other Mesh', 'k']) >>> plotter = pyvista.Plotter() >>> _ = plotter.add_mesh(mesh) >>> _ = plotter.add_mesh(othermesh, 'k') >>> _ = plotter.add_legend(legend_entries) >>> plotter.show() # doctest:+SKIP """ self.legend = vtk.vtkLegendBoxActor() if labels is None: # use existing labels if not self._labels: raise ValueError('No labels input.\n\n' 'Add labels to individual items when adding them to' 'the plotting object with the "label=" parameter. ' 'or enter them as the "labels" parameter.') self.legend.SetNumberOfEntries(len(self._labels)) for i, (vtk_object, text, color) in enumerate(self._labels): self.legend.SetEntry(i, vtk_object, text, parse_color(color)) else: self.legend.SetNumberOfEntries(len(labels)) legendface = pyvista.single_triangle() for i, (text, color) in enumerate(labels): self.legend.SetEntry(i, legendface, text, parse_color(color)) if size: self.legend.SetPosition2(size[0], size[1]) if bcolor is None: self.legend.UseBackgroundOff() else: self.legend.UseBackgroundOn() self.legend.SetBackgroundColor(bcolor) if border: self.legend.BorderOn() else: self.legend.BorderOff() # Add to renderer self.add_actor(self.legend, reset_camera=False, name=name, pickable=False) return self.legend def set_background(self, color, top=None, all_renderers=True): """Set the background color. Parameters ---------- color : string or 3 item list, optional, defaults to white Either a string, rgb list, or hex color string. For example: color='white' color='w' color=[1, 1, 1] color='#FFFFFF' top : string or 3 item list, optional, defaults to None If given, this will enable a gradient background where the ``color`` argument is at the bottom and the color given in ``top`` will be the color at the top of the renderer. all_renderers : bool If True, applies to all renderers in subplots. If False, then only applies to the active renderer. """ if all_renderers: for renderer in self.renderers: renderer.set_background(color, top=top) else: self.renderer.set_background(color, top=top) def remove_legend(self): """Remove the legend actor.""" if hasattr(self, 'legend'): self.remove_actor(self.legend, reset_camera=False) self.render() def generate_orbital_path(self, factor=3., n_points=20, viewup=None, shift=0.0): """Generate an orbital path around the data scene. Parameters ---------- factor : float A scaling factor when biulding the orbital extent n_points : int number of points on the orbital path viewup : list(float) the normal to the orbital plane shift : float, optional shift the plane up/down from the center of the scene by this amount """ if viewup is None: viewup = rcParams['camera']['viewup'] center = np.array(self.center) bnds = np.array(self.bounds) radius = (bnds[1] - bnds[0]) * factor y = (bnds[3] - bnds[2]) * factor if y > radius: radius = y center += np.array(viewup) * shift return pyvista.Polygon(center=center, radius=radius, normal=viewup, n_sides=n_points) def fly_to(self, point): """Move the current camera's focal point to a position point. The movement is animated over the number of frames specified in NumberOfFlyFrames. The LOD desired frame rate is used. """ if not hasattr(self, 'iren'): raise AttributeError('This plotter does not have an interactive window') return self.iren.FlyTo(self.renderer, *point) def orbit_on_path(self, path=None, focus=None, step=0.5, viewup=None, write_frames=False, threaded=False): """Orbit on the given path focusing on the focus point. Parameters ---------- path : pyvista.PolyData Path of orbital points. The order in the points is the order of travel focus : list(float) of length 3, optional The point of focus the camera. step : float, optional The timestep between flying to each camera position viewup : list(float) the normal to the orbital plane write_frames : bool Assume a file is open and write a frame on each camera view during the orbit. threaded : bool, optional Run this as a background thread. Generally used within a GUI (i.e. PyQt). """ if focus is None: focus = self.center if viewup is None: viewup = rcParams['camera']['viewup'] if path is None: path = self.generate_orbital_path(viewup=viewup) if not is_pyvista_dataset(path): path = pyvista.PolyData(path) points = path.points # Make sure the whole scene is visible self.camera.SetThickness(path.length) def orbit(): """Define the internal thread for running the orbit.""" for point in points: self.set_position(point) self.set_focus(focus) self.set_viewup(viewup) self.renderer.ResetCameraClippingRange() self.render() time.sleep(step) if write_frames: self.write_frame() if threaded: thread = Thread(target=orbit) thread.start() else: orbit() return def export_vtkjs(self, filename, compress_arrays=False): """Export the current rendering scene as a VTKjs scene. It can be used for rendering in a web browser. """ if not hasattr(self, 'ren_win'): raise RuntimeError('Export must be called before showing/closing the scene.') if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename): filename = os.path.join(pyvista.FIGURE_PATH, filename) else: filename = os.path.abspath(os.path.expanduser(filename)) return export_plotter_vtkjs(self, filename, compress_arrays=compress_arrays) def export_obj(self, filename): """Export scene to OBJ format.""" if not hasattr(self, "ren_win"): raise RuntimeError("This plotter must still have a render window open.") if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename): filename = os.path.join(pyvista.FIGURE_PATH, filename) else: filename = os.path.abspath(os.path.expanduser(filename)) exporter = vtk.vtkOBJExporter() exporter.SetFilePrefix(filename) exporter.SetRenderWindow(self.ren_win) return exporter.Write() def __del__(self): """Delete the plotter.""" if not self._closed: self.close() self.deep_clean() del self.renderers def add_background_image(self, image_path, scale=1, auto_resize=True, as_global=True): """Add a background image to a plot. Parameters ---------- image_path : str Path to an image file. scale : float, optional Scale the image larger or smaller relative to the size of the window. For example, a scale size of 2 will make the largest dimension of the image twice as large as the largest dimension of the render window. Defaults to 1. auto_resize : bool, optional Resize the background when the render window changes size. as_global : bool, optional When multiple render windows are present, setting ``as_global=False`` will cause the background to only appear in one window. Examples -------- >>> import pyvista >>> from pyvista import examples >>> plotter = pyvista.Plotter() >>> actor = plotter.add_mesh(pyvista.Sphere()) >>> plotter.add_background_image(examples.mapfile) >>> plotter.show() # doctest:+SKIP """ # verify no render exists if self._background_renderers[self._active_renderer_index] is not None: raise RuntimeError('A background image already exists. ' 'Remove it with remove_background_image ' 'before adding one') # Need to change the number of layers to support an additional # background layer self.ren_win.SetNumberOfLayers(2) if as_global: for renderer in self.renderers: renderer.SetLayer(1) view_port = None else: self.renderer.SetLayer(1) view_port = self.renderer.GetViewport() renderer = BackgroundRenderer(self, image_path, scale, view_port) self.ren_win.AddRenderer(renderer) self._background_renderers[self._active_renderer_index] = renderer # setup autoscaling of the image if auto_resize and hasattr(self, 'iren'): # pragma: no cover self._add_observer('ModifiedEvent', renderer.resize) def remove_background_image(self): """Remove the background image from the current subplot.""" renderer = self._background_renderers[self._active_renderer_index] if renderer is None: raise RuntimeError('No background image to remove at this subplot') renderer.deep_clean() self._background_renderers[self._active_renderer_index] = None def reset_camera_clipping_range(self): """Reset camera clipping planes.""" self.renderer.ResetCameraClippingRange() class Plotter(BasePlotter): """Plotting object to display vtk meshes or numpy arrays. Example ------- >>> import pyvista >>> from pyvista import examples >>> mesh = examples.load_hexbeam() >>> another_mesh = examples.load_uniform() >>> plotter = pyvista.Plotter() >>> _ = plotter.add_mesh(mesh, color='red') >>> _ = plotter.add_mesh(another_mesh, color='blue') >>> plotter.show() # doctest:+SKIP Parameters ---------- off_screen : bool, optional Renders off screen when True. Useful for automated screenshots. notebook : bool, optional When True, the resulting plot is placed inline a jupyter notebook. Assumes a jupyter console is active. Automatically enables off_screen. shape : list or tuple, optional Number of sub-render windows inside of the main window. Specify two across with ``shape=(2, 1)`` and a two by two grid with ``shape=(2, 2)``. By default there is only one render window. Can also accept a string descriptor as shape. E.g.: * ``shape="3|1"`` means 3 plots on the left and 1 on the right, * ``shape="4/2"`` means 4 plots on top and 2 at the bottom. border : bool, optional Draw a border around each render window. Default False. border_color : string or 3 item list, optional, defaults to white Either a string, rgb list, or hex color string. For example: * ``color='white'`` * ``color='w'`` * ``color=[1, 1, 1]`` * ``color='#FFFFFF'`` window_size : list, optional Window size in pixels. Defaults to [1024, 768] multi_samples : int The number of multi-samples used to mitigate aliasing. 4 is a good default but 8 will have better results with a potential impact on performance. line_smoothing : bool If True, enable line smothing point_smoothing : bool If True, enable point smothing polygon_smoothing : bool If True, enable polygon smothing """ last_update_time = 0.0 right_timer_id = -1 def __init__(self, off_screen=None, notebook=None, shape=(1, 1), groups=None, row_weights=None, col_weights=None, border=None, border_color='k', border_width=2.0, window_size=None, multi_samples=None, line_smoothing=False, point_smoothing=False, polygon_smoothing=False, splitting_position=None, title=None): """Initialize a vtk plotting object.""" super().__init__(shape=shape, border=border, border_color=border_color, border_width=border_width, groups=groups, row_weights=row_weights, col_weights=col_weights, splitting_position=splitting_position, title=title) log.debug('Plotter init start') def on_timer(iren, event_id): """Exit application if interactive renderer stops.""" if event_id == 'TimerEvent': self.iren.TerminateApp() if off_screen is None: off_screen = pyvista.OFF_SCREEN if notebook is None: notebook = scooby.in_ipykernel() self.notebook = notebook if self.notebook: off_screen = True self.off_screen = off_screen if window_size is None: window_size = rcParams['window_size'] self.__prior_window_size = window_size if multi_samples is None: multi_samples = rcParams['multi_samples'] # initialize render window self.ren_win = vtk.vtkRenderWindow() self.ren_win.SetMultiSamples(multi_samples) self.ren_win.SetBorders(True) if line_smoothing: self.ren_win.LineSmoothingOn() if point_smoothing: self.ren_win.PointSmoothingOn() if polygon_smoothing: self.ren_win.PolygonSmoothingOn() for renderer in self.renderers: self.ren_win.AddRenderer(renderer) if self.off_screen: self.ren_win.SetOffScreenRendering(1) else: # Allow user to interact self.iren = vtk.vtkRenderWindowInteractor() self.iren.LightFollowCameraOff() self.iren.SetDesiredUpdateRate(30.0) self.iren.SetRenderWindow(self.ren_win) self.enable_trackball_style() self._observers = {} # Map of events to observers of self.iren self._add_observer("KeyPressEvent", self.key_press_event) self.update_style() # Set background self.set_background(rcParams['background']) # Set window size self.window_size = window_size # add timer event if interactive render exists self._add_observer(vtk.vtkCommand.TimerEvent, on_timer) if rcParams["depth_peeling"]["enabled"]: if self.enable_depth_peeling(): for renderer in self.renderers: renderer.enable_depth_peeling() log.debug('Plotter init stop') def show(self, title=None, window_size=None, interactive=True, auto_close=None, interactive_update=False, full_screen=False, screenshot=False, return_img=False, use_panel=None, cpos=None, height=400): """Display the plotting window. Notes ----- Please use the ``q``-key to close the plotter as some operating systems (namely Windows) will experience issues saving a screenshot if the exit button in the GUI is prressed. Parameters ---------- title : string, optional Title of plotting window. window_size : list, optional Window size in pixels. Defaults to [1024, 768] interactive : bool, optional Enabled by default. Allows user to pan and move figure. auto_close : bool, optional Enabled by default. Exits plotting session when user closes the window when interactive is True. interactive_update: bool, optional Disabled by default. Allows user to non-blocking draw, user should call Update() in each iteration. full_screen : bool, optional Opens window in full screen. When enabled, ignores window_size. Default False. use_panel : bool, optional If False, the interactive rendering from panel will not be used in notebooks cpos : list(tuple(floats)) The camera position to use height : int, optional height for panel pane. Only used with panel. Return ------ cpos : list List of camera position, focal point, and view up """ if use_panel is None: use_panel = rcParams['use_panel'] if auto_close is None: auto_close = rcParams['auto_close'] if not hasattr(self, "ren_win"): raise RuntimeError("This plotter has been closed and cannot be shown.") # reset unless camera for the first render unless camera is set if self._first_time: # and not self.camera_set: for renderer in self.renderers: if not renderer.camera_set and cpos is None: renderer.camera_position = renderer.get_default_cam_pos() renderer.ResetCamera() elif cpos is not None: renderer.camera_position = cpos self._first_time = False # if full_screen: if full_screen: self.ren_win.SetFullScreen(True) self.ren_win.BordersOn() # super buggy when disabled else: if window_size is None: window_size = self.window_size self.ren_win.SetSize(window_size[0], window_size[1]) # Render log.debug('Rendering') self.render() # This has to be after the first render for some reason if title is None: title = self.title if title: self.ren_win.SetWindowName(title) self.title = title # Keep track of image for sphinx-gallery if pyvista.BUILDING_GALLERY or screenshot: # always save screenshots for sphinx_gallery self.last_image = self.screenshot(screenshot, return_img=True) self.last_image_depth = self.get_image_depth() disp = None self.update() # For Windows issues. Resolves #186 # See: https://github.com/pyvista/pyvista/issues/186#issuecomment-550993270 if interactive and (not self.off_screen): try: # interrupts will be caught here log.debug('Starting iren') self.update_style() self.iren.Initialize() if not interactive_update: self.iren.Start() except KeyboardInterrupt: log.debug('KeyboardInterrupt') self.close() raise KeyboardInterrupt elif self.notebook and use_panel and not hasattr(self, 'volume'): try: from panel.pane import VTK as panel_display disp = panel_display(self.ren_win, sizing_mode='stretch_width', height=height) except: pass # In the event that the user hits the exit-button on the GUI (on # Windows OS) then it must be finalized and deleted as accessing it # will kill the kernel. # Here we check for that and clean it up before moving on to any of # the closing routines that might try to still access that # render window. if not self.ren_win.IsCurrent(): self._clear_ren_win() # The ren_win is deleted # proper screenshots cannot be saved if this happens if not auto_close: warnings.warn("`auto_close` ignored: by clicking the exit button, you have destroyed the render window and we have to close it out.") auto_close = True # NOTE: after this point, nothing from the render window can be accessed # as if a user presed the close button, then it destroys the # the render view and a stream of errors will kill the Python # kernel if code here tries to access that renderer. # See issues #135 and #186 for insight before editing the # remainder of this function. # Get camera position before closing cpos = self.camera_position # NOTE: our conversion to panel currently does not support mult-view # so we should display the static screenshot in notebooks for # multi-view plots until we implement this feature # If notebook is true and panel display failed: if self.notebook and (disp is None or self.shape != (1, 1)): import PIL.Image # sanity check try: import IPython except ImportError: raise ImportError('Install IPython to display image in a notebook') if not hasattr(self, 'last_image'): self.last_image = self.screenshot(screenshot, return_img=True) disp = IPython.display.display(PIL.Image.fromarray(self.last_image)) # Cleanup if auto_close: self.close() # Return the notebook display: either panel object or image display if self.notebook: return disp # If user asked for screenshot, return as numpy array after camera # position if return_img or screenshot is True: return cpos, self.last_image # default to returning last used camera position return cpos def plot(self, *args, **kwargs): """Create a plotting window. Present for backwards compatibility. DEPRECATED: Please use `show()` instead. """ logging.warning("`.plot()` is deprecated. Please use `.show()` instead.") return self.show(*args, **kwargs)