"""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)