Source code for geovista.themes
# Copyright (c) 2021, GeoVista Contributors.
#
# This file is part of GeoVista and is distributed under the 3-Clause BSD license.
# See the LICENSE file in the package root directory for licensing details.
"""Configures custom :doc:`pyvista <pyvista:index>` themes for ``geovista``.
These themes are discoverable by ``pyvista`` and registered
through ``[project.entry-points]`` TOML table metadata (``PEP621``) in our
``pyproject.toml``.
Registered themes may be enabled through :func:`geovista.set_plot_theme`,
:func:`pyvista.set_plot_theme` or the ``PYVISTA_PLOT_THEME`` environment variable.
See Also
--------
:func:`pyvista.registered_themes`
Enumeration of available registered themes.
Notes
-----
.. versionadded:: 0.6.0
"""
from __future__ import annotations
import copy
import os
from typing import ClassVar
import lazy_loader as lazy
from pyvista.plotting.theme_registry import (
_available_theme_names,
_resolve_dotted_path,
_resolve_theme,
)
from pyvista.plotting.themes import DocumentTheme, Theme
import geovista.config as gvc
# lazy import third-party dependencies
pv = lazy.load("pyvista")
__all__ = [
"GeoVistaDocumentTheme",
"GeoVistaTheme",
"ThemeMixin",
"resolve_theme_name",
"restore_plot_theme",
"set_plot_theme",
]
# support theme restoration with a stack of previous themes
_cached_themes: list[Theme] = []
[docs]
class ThemeMixin:
"""Common ``geovista`` plotting theme property state.
Notes
-----
.. versionadded:: 0.6.0
"""
[docs]
def mixin_state(self) -> None:
"""Configure common property state."""
self.allow_empty_mesh = True
self.background = "white"
self.cmap = "balance"
self.color = "lightgray"
self.edge_color = "gray"
self.font.label_size = None # type: ignore[attr-defined]
self.font.title_size = None # type: ignore[attr-defined]
self.font.color = "black" # type: ignore[attr-defined]
self.outline_color = "black"
self.title = "GeoVista"
[docs]
class GeoVistaTheme(Theme, ThemeMixin): # type: ignore[misc]
"""Default ``geovista`` plot theme.
Examples
--------
Make the ``geovista`` theme the global default using an
instance of the theme.
>>> import pyvista as pv
>>> from geovista.themes import GeoVistaTheme
>>> pv.set_plot_theme(GeoVistaTheme())
Alternatively, enable the theme via its string name.
>>> pv.set_plot_theme("geovista")
Notes
-----
.. versionadded:: 0.6.0
"""
_default_name: ClassVar[str] = "geovista"
def __init__(self) -> None:
"""Default plotting theme for ``geovista``."""
super().__init__()
# apply common theme state to the theme instance
self.mixin_state()
[docs]
class GeoVistaDocumentTheme(DocumentTheme, ThemeMixin): # type: ignore[misc]
"""Theme used for building the documentation.
Examples
--------
Make the ``geovista_document`` theme the global default using an
instance of the theme.
>>> import pyvista as pv
>>> from geovista.themes import GeoVistaDocumentTheme
>>> pv.set_plot_theme(GeoVistaDocumentTheme())
Alternatively, enable the theme via its string name.
>>> pv.set_plot_theme("geovista_document")
Notes
-----
.. versionadded:: 0.6.0
"""
_default_name: ClassVar[str] = "geovista_document"
def __init__(self) -> None:
"""Documentation plotting theme for ``geovista``."""
super().__init__()
# apply common theme state to the theme instance
self.mixin_state()
[docs]
def resolve_theme_name(name: str) -> Theme | None:
"""Create an instance of the registered theme or dotted path theme class.
Parameters
----------
name : str
The name of the registered theme to lookup or the dotted path
``package.module:ClassName`` theme to be created.
Returns
-------
Theme | None
An instance of the theme or ``None`` if the requested theme
is not registered.
Raises
------
ValueError
When `name` is an invalid dotted path specification or cannot be imported.
Notes
-----
.. versionadded:: 0.6.0
"""
if ":" in name:
cls = _resolve_dotted_path(name)
theme = cls()
else:
theme = _resolve_theme(name)
return theme
[docs]
def restore_plot_theme() -> Theme | None:
"""Activate the previous plot theme.
Provides a convenience to undo the last call to
:func:`geovista.themes.set_plot_theme`. Note that the entire call stack
history is cached, so multiple calls to :func:`geovista.themes.set_plot_theme`
may be undone in reverse order to restore a previous theme.
When no cached theme is available, the current theme will remain active and
``None`` is returned.
Returns
-------
Theme | None
The previously cached theme if available, otherwise ``None``.
Notes
-----
.. versionadded:: 0.6.0
"""
previous_theme = _cached_themes.pop() if _cached_themes else None
if previous_theme is not None:
pv.set_plot_theme(previous_theme)
return previous_theme
[docs]
def set_plot_theme(
theme: Theme | str,
/,
*,
bootstrap: bool | None = False,
) -> bool:
"""Set plotting parameters to a predefined theme.
The enabled plot theme will be used by all plotters and rendering.
Requests to enable a plot theme will be ignored when the environment variable
``GEOVISTA_DISABLE_PLOT_THEME`` is set.
Note that the replaced theme is cached and may be restored with
:func:`geovista.themes.restore_plot_theme`.
Parameters
----------
theme : Theme or str
The theme to apply, which may be either:
* The string name of a registered theme. See :func:`pyvista.registered_themes`
for the available :doc:`pyvista <pyvista:index>` built-in and
third-party entry-point group themes.
* A string ``package.module:ClassName`` dotted path to an importable
:class:`pyvista.plotting.themes.Theme` subclass.
* A :class:`pyvista.plotting.themes.Theme` subclass instance.
bootstrap : bool, default=False
When ``True``, theme configuration is skipped if the environment
variable ``PYVISTA_PLOT_THEME`` is set. Otherwise, the provided `theme`
will be enabled regardless of that environment variable. This behaviour
allows a pre-defined ``PYVISTA_PLOT_THEME`` to take precedence.
Returns
-------
bool
``True`` if the theme was successfully enabled, otherwise ``False``.
Raises
------
ValueError
When `theme` is an invalid dotted path specification or cannot be imported.
See Also
--------
:func:`geovista.themes.restore_plot_theme`
Reinstates the plot theme to the previous replaced theme.
:func:`pyvista.registered_themes`
The list of available registered themes.
:class:`pyvista.plotting.themes.Theme`
Base class for all themes. Subclasses with the class property
``_default_name`` are discoverable by :doc:`pyvista <pyvista:index>`.
Notes
-----
.. versionadded:: 0.6.0
Examples
--------
Set the default ``geovista`` theme.
>>> import geovista as gv
>>> gv.set_plot_theme("geovista")
True
Set the pyvista dark theme.
>>> gv.set_plot_theme("dark")
True
Load a theme from an importable package module specified as a dotted path.
>>> gv.set_plot_theme("geovista.themes:GeoVistaTheme")
True
Set the pyvista paraview theme.
>>> from pyvista import themes
>>> gv.set_plot_theme(themes.ParaViewTheme())
True
"""
if bootstrap and os.environ.get("PYVISTA_PLOT_THEME"):
return False
snapshot = copy.deepcopy(pv.global_theme)
if not (gvc.GEOVISTA_DISABLE_PLOT_THEME or gvc.GEOVISTA_IMAGE_TESTING):
if isinstance(theme, str):
resolved = resolve_theme_name(theme)
if resolved is None:
allowed = ", ".join(_available_theme_names())
emsg = (
f'Theme "{theme}" not found. Available themes: {allowed}. '
'To load from an arbitrary module use "package.module:ClassName".'
)
raise ValueError(emsg)
pv.set_plot_theme(resolved)
elif isinstance(theme, Theme):
pv.set_plot_theme(theme)
else:
emsg = (
f'Expected a theme type of "pyvista.plotting.themes.Theme" or "str", '
f'got "{type(theme).__name__}" instead.'
)
raise TypeError(emsg)
_cached_themes.append(snapshot)
result = True
else:
result = False
return result