Source code for ogstools.plot.contourplots_pv

# Copyright (c) 2012-2025, OpenGeoSys Community (http://www.opengeosys.org)
#            Distributed under a Modified BSD License.
#            See accompanying file LICENSE.txt or
#            http://www.opengeosys.org/project/license
#

"""3D Plotting functions."""

from typing import Any

import numpy as np
import pyvista as pv
from matplotlib.colors import ListedColormap

from ogstools.plot import setup, utils
from ogstools.plot.levels import combined_levels
from ogstools.variables import Variable


[docs] def contourf_pv( mesh: pv.UnstructuredGrid, variable: str | Variable, show_edges: bool = True, opacities: dict[int, float] | None = None, lighting: bool = False, categoric: bool | None = None, **kwargs: Any, ) -> pv.Plotter: """General 3D plot using pyvista :param mesh: The mesh to be plotted with pyvista. :param variable: The variable which should be shown. :param show_edges: If True, draw edges as black lines :param opacities: Dictionary, mapping opacities to material ids. Default None (all opaque), Example: {1:0.0 # transparent, 2:1 # opaque, 3:0.5} All not provided ids are rendered as opaque. :param lighting: If True, use lighting in the visualization. :param categoric: If True, use a categoric colormap. By default it uses the variable to determine if this should be True. :returns: A pyvista Plotter object. Use .show() to display the scene. """ plot_mesh: pv.UnstructuredGrid = mesh.copy() variable_ = Variable.find(variable, mesh) # converting to float as the interactive plot seems to have trouble with int # taking magnitude of pint quantities to prevent numpy warning plot_var = variable_.replace(func=lambda x: variable_.func(x).astype(float)) levels = combined_levels(np.array([mesh]), plot_var) if categoric is None: categoric = plot_var.categoric pv_levels = ( np.arange(len(levels)) if categoric else np.linspace(levels[0], levels[-1], 255) ) pv_levels = np.asarray(kwargs.get("levels", pv_levels)) cmap = utils.get_cmap_norm(pv_levels, plot_var)[0] plotter = pv.Plotter( off_screen=True, window_size=(720, 400), border=False, image_scale=2 ) plot_mesh[plot_var.output_name] = plot_var.transform(mesh) if plot_var.mask_used(plot_mesh): plot_mesh = plot_mesh.ctp(True).threshold( value=[1, 1], scalars=variable_.mask ) if opacities is None: plotter.add_mesh( plot_mesh, scalars=plot_var.output_name, show_scalar_bar=False, show_edges=show_edges, cmap=cmap, culling=True, categories=categoric, lighting=lighting, **kwargs ) # fmt: skip else: # Plot each region with its individual opacity for idx, matID in enumerate(np.unique(plot_mesh[plot_var.output_name])): region = plot_mesh.threshold([matID, matID], plot_var.output_name) region[plot_var.output_name] = plot_var.transform(region) opacity_kwarg = ( {"opacity": opacities[matID]} if matID in opacities else {} ) plotter.add_mesh( region, scalars=plot_var.output_name, show_scalar_bar=False, show_edges=show_edges and matID not in opacities, culling=True, lighting=lighting, cmap=ListedColormap([cmap(int(idx))]), categories=categoric, clim=pv_levels[[0, -1]], **opacity_kwarg, **kwargs ) # fmt:skip if categoric: # pyvista needs strings as data for proper rendering of categoric data # unfortunately the categoric values are then sorted alphanumerically, # thus we need to add leading zeros. As this is can be performance heavy # we do it for the smallest viable subset. categoric_var = plot_var.replace( func=lambda x: np.char.zfill( plot_var.func(getattr(x, "magnitude", x)) .astype(int).astype(str), 2 ) ) # fmt: skip plot_mesh = plot_mesh.extract_cells( np.unique(plot_mesh[plot_var.data_name], return_index=True)[1] ) plot_mesh[plot_var.output_name] = categoric_var.transform(plot_mesh) # Latex in vtk renderer is broken for now, thus removing special characters # see https://github.com/pyvista/pyvista/discussions/2928 label = plot_var.get_label() for s in ["$", "\\", "{", "}"]: label = label.replace(s, "") scalar_bar_args = dict( # noqa: C408 vertical=True, position_x=0.8, position_y=0.05, height=0.9, title=label ) # optionally adding regions names if plot_var.data_name == "MaterialIDs" and setup.material_names is not None: plot_mesh["MaterialIDs"] = [ ":".join([matID, setup.material_names.get(int(matID), "")]) for matID in plot_mesh["MaterialIDs"] ] # Finally, adding an invisible mesh solely for the scalarbar. plotter.add_mesh( plot_mesh, scalars=plot_var.output_name, show_edges=show_edges, cmap=cmap, culling=True, categories=categoric, lighting=lighting, scalar_bar_args=scalar_bar_args, opacity=0.0, **kwargs ) # fmt: skip plotter.show_axes() return plotter