Source code for porescene.config

# SPDX-License-Identifier: GPL-3.0-only
# Copyright (C) 2026 Felix Faber /
# Otto von Guericke University Magdeburg, Thermal Process Engineering

"""
Configuration objects that control how a :class:`PoreNetwork
<porescene.model.PoreNetwork>` and its images are rendered.

This module bundles the settings used throughout :mod:`porescene`:

* :class:`PropertyConfiguration` -- visualization of a single property.
* :class:`ImageConfiguration` -- resolution of the rendered images.
* :class:`VideoConfiguration` -- frame settings for rendered videos.
* :class:`SceneConfiguration` -- overall scene, material and property settings.
* :class:`AxesConfiguration` -- coordinate axes, ticks and labels.
"""

import math
from collections.abc import Callable, Iterator, Sequence
from importlib import resources
from pathlib import Path
from typing import Self

import numpy as np

from porescene.color import Color
from porescene.color.gradient import Gradient, SmoothGradient
from porescene.color.palette import Colormap, Palette
from porescene.utility import (
    CompassDirection,
    Orientation,
    UnitExponentMetric,
    UnitPrefixMetric,
)


[docs] class PropertyConfiguration: """ Settings for the visualization of a specific property of the system. """ def __init__( self, name: str, colors: Sequence[Color] = (), /, gradient_class: type[Gradient] = SmoothGradient, heading: str | None = None, subheading: str | None = None, text: Sequence[str] = (), align: CompassDirection = CompassDirection.NORTH, orientation: Orientation = Orientation.HORIZONTAL, precision: int = 0, min: float | None = None, max: float | None = None, use_global_boundaries: bool = False, factor: float = 1.0, func_transform: Callable = lambda v: v, fit: bool = True, color_nan: Color | None = None, color_below: Color | None = None, color_above: Color | None = None, ) -> None: self.name = name self.colors = colors self.color_nan = color_nan self.color_above = color_above self.color_below = color_below self.gradient_class = gradient_class self.align = align self.orientation = orientation self.precision = precision self.min = min self.max = max self.use_global_boundaries = use_global_boundaries self.factor = factor self.func_transform = func_transform self.fit = fit self.heading = heading self.subheading = subheading self.text = text @property def align(self) -> CompassDirection: """Point of alignment of the layout image.""" return self._align @align.setter def align(self, arg: CompassDirection): self._align = arg @property def color_nan(self) -> Color | None: """ Color of NaN values. See also :attr:`Gradient.color_nan <porescene.color.gradient.Gradient.color_nan>`. """ return self._color_nan @color_nan.setter def color_nan(self, arg: Color | None): self._color_nan = arg @property def color_below(self) -> Color | None: """ Color of values below the range. See also :attr:`.Gradient.color_below`. """ return self._color_below @color_below.setter def color_below(self, arg: Color | None): self._color_below = arg @property def color_above(self) -> Color | None: """ Color of values above the range. See also :attr:`.Gradient.color_above`. """ return self._color_above @color_above.setter def color_above(self, arg: Color | None): self._color_above = arg @property def colors(self) -> list[Color]: """ Colors of the gradient to visualize the property. See also :attr:`.Gradient.colors`. """ return self._colors @colors.setter def colors(self, arg: list[Color]): self._colors = arg @property def factor(self) -> float: """A factor to scale the values of a property on overlays. Does not apply to anything rendering or model related. """ return self._factor @factor.setter def factor(self, arg: float): self._factor = arg @property def func_transform(self) -> Callable: """A transformation function to scale the values of a property on overlays. Does not apply to anything rendering or model related. """ return self._func_transform @func_transform.setter def func_transform(self, arg: Callable): self._func_transform = arg @property def fit(self) -> bool: """ If ``True``, the gradient truncates all values in specified range. """ return self._fit @fit.setter def fit(self, arg: bool): self._fit = arg @property def use_global_boundaries(self) -> bool: """Whether to use colorbar limits across all states or per state.""" return self._use_global_boundaries @use_global_boundaries.setter def use_global_boundaries(self, arg: bool): self._use_global_boundaries = arg @property def max(self) -> float | None: """Maximum of the property.""" return self._max @max.setter def max(self, arg: float | None): self._max = arg @property def min(self) -> float | None: """Minimum of the property.""" return self._min @min.setter def min(self, arg: float | None): self._min = arg @property def name(self) -> str: """Name of the property.""" return self._name @name.setter def name(self, arg: str): self._name = arg @property def orientation(self) -> Orientation: """ Orientation of the layout image. There are two types available: vertical ('V') and horizontal ('H'). """ return self._orientation @orientation.setter def orientation(self, arg: Orientation): self._orientation = arg @property def precision(self) -> int: """Precision of the gradient boundaries on a layout.""" return self._precision @precision.setter def precision(self, arg: int): self._precision = arg @property def heading(self) -> str | None: """Heading of the layout image.""" return self._heading @heading.setter def heading(self, arg: str | None): self._heading = arg @property def subheading(self) -> str | None: """Subheading of the layout image.""" return self._subheading @subheading.setter def subheading(self, arg: str | None): self._subheading = arg @property def text(self) -> list[str]: """Text lines on the layout image.""" return self._text @text.setter def text(self, arg: list[str]): self._text = arg
[docs] class ImageConfiguration: """ Settings for the resolution of the rendered images. """ def __init__(self) -> None: self.width = 4096 self.height = 4096
[docs] @classmethod def from_dict(cls, data: dict) -> Self: """ Creates an :class:`ImageConfiguration` from a dictionary. Only the ``width`` and ``height`` keys are read; other keys are ignored. """ ins = cls() keys_valid = [ "width", "height", ] for key, value in data.items(): if key in keys_valid: setattr(ins, key, value) return ins
@property def width(self) -> int: """Width of the image in px.""" return self._width @width.setter def width(self, arg: int): self._width = arg @property def height(self) -> int: """Height of the image in px.""" return self._height @height.setter def height(self, arg: int): self._height = arg @property def resolution(self) -> list[int]: """Image resolution in px [width,height].""" return [self._width, self.height] @property def aspect_ratio(self) -> float: """Image aspect ratio.""" return self._width / self.height
[docs] class VideoConfiguration: """ Settings for the frames of a rendered video. """ @property def frames_fps(self) -> int: """Frames per second of the video.""" return self._frames_fps @frames_fps.setter def frames_fps(self, arg: int): self._frames_fps = arg @property def frames_solid(self) -> int: """Type of solid to display in generated frames.""" return self._frames_solid @frames_solid.setter def frames_solid(self, arg: int): self._frames_solid = arg @property def frames_speed(self) -> int: """Number of steps between two frames of the video.""" return self._frames_speed @frames_speed.setter def frames_speed(self, arg: int): self._frames_speed = arg
[docs] class SceneConfiguration: """ Settings for the visualization of a :class:`PoreNetwork <porescene.model.PoreNetwork>`. """ def __init__( self, enable_spheres: bool = True, enable_cylinders: bool = True, enable_clusters: bool = True, enable_axes: bool = True, enable_solid: bool = True, enable_void: bool = False, versions_solid=None, versions_void=None, material_spheres: str = "PLASTIC_ROUGH", material_cylinders: str = "PLASTIC_ROUGH", material_clusters: str = "PLASTIC_ROUGH", material_solid: str = "SOLID_DEFAULT", material_void: str = "ICE", palette: Palette = None, ): if versions_void is None: versions_void = {"COMPLETE", "BOTTOM", "LEFT", "RIGHT"} if versions_solid is None: versions_solid = {"COMPLETE", "BOTTOM", "LEFT", "RIGHT"} if palette is None: palette = Palette.load(Colormap.BATLOW) self._properties = [] self.enable_spheres = enable_spheres self.enable_cylinders = enable_cylinders self.enable_clusters = enable_clusters self.enable_axes = enable_axes self.enable_solid = enable_solid self.enable_void = enable_void self.material_spheres = material_spheres self.material_solid = material_solid self.material_cylinders = material_cylinders self.material_clusters = material_clusters self.material_void = material_void self.palette = palette self.versions_solid = versions_solid self.versions_void = versions_void def __iter__(self) -> Iterator[PropertyConfiguration]: """Returns a fresh iterator over the configured properties.""" return iter(self._properties) def __len__(self) -> int: """Returns the number of configured properties.""" return len(self._properties) def __setitem__(self, _, prop: PropertyConfiguration): """Adds a :class:`PropertyConfiguration`, see :meth:`add_property`.""" return self.add_property(prop) def __getitem__(self, name: str) -> PropertyConfiguration: """Returns the :class:`PropertyConfiguration` for given name.""" return self.get_property(name)
[docs] def add_property(self, prop: PropertyConfiguration): """ Add the :class:`PropertyConfiguration` for a property of the pnm. """ self._properties.append(prop)
[docs] def get_property(self, name: str) -> PropertyConfiguration: """Returns the :class:`PropertyConfiguration` for given name.""" for prop in self._properties: if prop.name == name: return prop raise ValueError(f"Unknown property with name '{name}'")
@property def enable_axes(self) -> bool: """Toggle to enable/disable scalebars in plots.""" return bool(self._enable_axes) @enable_axes.setter def enable_axes(self, arg: bool): self._enable_axes = arg @property def enable_clusters(self) -> bool: """Toggle to enable/disable clusters in plots.""" return bool(self._enable_clusters) @enable_clusters.setter def enable_clusters(self, arg: bool): self._enable_clusters = arg @property def enable_cylinders(self) -> bool: """Toggle to enable/disable cylinders in plots.""" return bool(self._enable_cylinders) @enable_cylinders.setter def enable_cylinders(self, arg: bool): self._enable_cylinders = arg @property def enable_solid(self) -> bool: """Toggle to enable/disable the solid structure in plots.""" return bool(self._enable_solid) @enable_solid.setter def enable_solid(self, arg: bool): self._enable_solid = arg @property def enable_spheres(self) -> bool: """Toggle to enable/disable spheres in plots.""" return bool(self._enable_spheres) @enable_spheres.setter def enable_spheres(self, arg: bool): self._enable_spheres = arg @property def enable_void(self) -> bool: """Toggle to enable/disable the void structure in plots.""" return bool(self._enable_void) @enable_void.setter def enable_void(self, arg: bool): self._enable_void = arg @property def material_spheres(self) -> str: """Returns the material type to render the pores with.""" return self._material_spheres.upper() @material_spheres.setter def material_spheres(self, arg): self._material_spheres = arg @property def material_solid(self) -> str: """Returns the material type to render the solid with.""" return self._material_solid.upper() @material_solid.setter def material_solid(self, arg): self._material_solid = arg @property def material_cylinders(self) -> str: """Returns the material type to render the throats with.""" return self._material_cylinders.upper() @material_cylinders.setter def material_cylinders(self, arg): self._material_cylinders = arg @property def material_clusters(self) -> str: """Returns the material type to render the clusters with.""" return self._material_clusters.upper() @material_clusters.setter def material_clusters(self, arg): self._material_clusters = arg @property def material_void(self) -> str: """Returns the material type to render the void with.""" return self._material_void.upper() @material_void.setter def material_void(self, arg: str): self._material_void = str(arg) @property def palette(self) -> Palette: """Color palette used to render the network.""" return self._palette @palette.setter def palette(self, arg: Palette): self._palette = arg @property def versions_solid(self) -> list[Path]: """Clipping versions of the solid structure to render.""" return self._versions_solid @versions_solid.setter def versions_solid(self, arg: list[Path]): self._versions_solid = arg @property def versions_void(self) -> list[Path]: """Clipping versions of the void structure to render.""" return self._versions_void @versions_void.setter def versions_void(self, arg: list[Path]): self._versions_void = arg
[docs] class AxesConfiguration: """ Settings for the coordinate axes, ticks and labels of a scene. """ def __init__( self, extent: np.ndarray, tick_interval: float | None = None, unit_display: str = "MICRO", num_ticks: int = 6, ) -> None: """ Creates an :class:`AxesConfiguration` calibrated to a physical domain. Parameters ---------- extent : np.ndarray Physical extent of the domain along x, y and z, in meters. tick_interval : float | None Spacing between major ticks, expressed in the displayed unit. Derived from ``extent`` when ``None``, see :attr:`tick_interval`. unit_display : str Metric prefix of the displayed tick values and axis labels, one of the names of :class:`~porescene.utility.UnitPrefixMetric`. num_ticks : int Number of major ticks aimed for along the longest axis, see :attr:`num_ticks`. Sizes the derived ``tick_interval``, so it takes effect here rather than when assigned later. """ self._extent = np.asarray(extent, dtype=float) self._tick_interval = tick_interval self._unit_display = unit_display self.font_size_labels = 0.5 self.font_size_ticks = 0.4 self.line_width = 0.03 self.spacing = 0.05 self.tick_length = 0.15 self.enable_ticks = (True, True, True) self.enable_ticks_minor = (True, True, True) self.enable_labels_ticks = None self.precision = (0, 0, 0) self.indent_ticks = False self.num_ticks = num_ticks self.num_ticks_minor = None self.position_tick_x = None self.position_tick_y = None self.position_tick_z = None ref = resources.files("porescene").joinpath("data/font/Inter-Regular.ttf") with resources.as_file(ref) as font_path: self.font_family = font_path self._calibrate() @staticmethod def _interval_round(span: float, num_ticks: int) -> float: """ Returns the tick interval from the 1-2-5-10 series that splits ``span`` into roughly ``num_ticks`` ticks, so they land on round values. The exact spacing ``span / (num_ticks - 1)`` is rounded to the closest member of the series, following Heckbert's *Nice Numbers for Graph Labels*. Sticking to that series keeps the ticks whole numbers in the displayed unit, which a finer series such as 1-2-2.5-5-10 would not, and the tick labels are rendered at a fixed :attr:`precision` that cannot show the extra digit. """ if not math.isfinite(span) or span <= 0: return 1.0 step = span / (num_ticks - 1) magnitude = 10 ** math.floor(math.log10(step)) residual = step / magnitude if residual < 1.5: return magnitude if residual < 3: return 2 * magnitude if residual < 7: return 5 * magnitude return 10 * magnitude def _calibrate(self) -> None: """ Derives axis labels, scaling factor, tick interval, ticks and tick values from :attr:`extent` and the displayed unit. Major ticks run from ``0`` to the extent of each axis in steps of :attr:`tick_interval`, so they land on round values in the displayed unit rather than on an even subdivision of the domain. The last tick is dropped when the extent is not a whole multiple of the interval. """ fac_unit = 10 ** UnitExponentMetric[self._unit_display].value fac_axis = 1 / fac_unit label_axis = UnitPrefixMetric[self._unit_display].value + "m" self.label_x = f"x [{label_axis}]" self.label_y = f"y [{label_axis}]" self.label_z = f"z [{label_axis}]" self.factor = (fac_axis, fac_axis, fac_axis) tick_start = (0.0, 0.0, 0.0) tick_end = self._extent * fac_axis # a single interval across all axes keeps the ticks of the scene to scale, # so it is sized by the longest axis and the shorter ones carry fewer ticks if self._tick_interval is None: self._interval = self._interval_round(float(max(tick_end)), self.num_ticks) else: self._interval = float(self._tick_interval) # nudge the stop value so an extent landing exactly on a tick keeps that tick margin = self._interval * 1e-6 def _ticks(dim: int) -> tuple[float, ...]: steps = np.arange(tick_start[dim], tick_end[dim] + margin, self._interval) return tuple(float(step) for step in steps) self.ticks_x = _ticks(0) self.ticks_y = _ticks(1) self.ticks_z = _ticks(2) self.value_start = tick_start self.value_end = tuple(float(end) for end in tick_end)
[docs] @classmethod def from_dict(cls, extent: np.ndarray, data: dict) -> Self: """ Creates an :class:`AxesConfiguration` from a dictionary. Ticks and axis labels are derived from ``extent`` (the size of the volume) together with the optional ``tick_interval`` and ``unit_display`` keys; the remaining keys override the derived values. """ ins = cls( extent, tick_interval=data.get("tick_interval"), unit_display=data.get("unit_display", "MICRO"), num_ticks=data.get("num_ticks", 6), ) keys_valid = [ "font_size_labels", "font_size_ticks", "line_width", "spacing", "tick_length", "label_x", "label_y", "label_z", "ticks_x", "ticks_y", "ticks_z", "enable_ticks", "enable_ticks_minor", "enable_labels_ticks", "precision", "num_ticks_minor", "indent_ticks", ] for key, value in data.items(): if key in keys_valid: setattr(ins, key, value) return ins
@property def extent(self) -> np.ndarray: """Physical extent of the domain along x, y and z, in meters.""" return self._extent @property def tick_interval(self) -> float: """ Spacing between major ticks, in the displayed unit. Unless given at construction, the interval is derived from :attr:`extent`: it is the value from the 1-2-5-10 series that puts roughly :attr:`num_ticks` ticks on the longest axis. All three axes share it, so the ticks stay to scale and a shorter axis simply carries fewer of them. """ return self._interval @property def factor(self) -> tuple[float, float, float]: """Factor to scale axis labels.""" return self._factor @factor.setter def factor(self, arg: tuple[float, float, float]): self._factor = arg @property def precision(self) -> tuple[int, int, int]: """Precision of axis labels. Applies after scaling.""" return self._precision @precision.setter def precision(self, arg: tuple[int, int, int]): self._precision = arg @property def enable_ticks(self) -> tuple[bool, bool, bool]: """Toggle to show/hide major axis ticks.""" return self._enable_ticks @enable_ticks.setter def enable_ticks(self, arg: bool | tuple[bool, bool, bool]): if isinstance(arg, bool): arg = (arg, arg, arg) self._enable_ticks = arg @property def enable_ticks_minor(self) -> tuple[bool, bool, bool]: """Toggle to show/hide minor axis ticks.""" return self._enable_ticks_minor @enable_ticks_minor.setter def enable_ticks_minor(self, arg: bool | tuple[bool, bool, bool]): if isinstance(arg, bool): arg = (arg, arg, arg) self._enable_ticks_minor = arg @property def enable_labels_ticks(self) -> tuple[bool, bool, bool]: """ Toggle to show/hide the labels of the major axis ticks. When set to ``None``, an axis is labelled only if it carries ticks -- either the ones calibrated from :attr:`extent` or ones assigned explicitly. An axis whose ticks were cleared falls back to the evenly spaced ticks of :attr:`num_ticks`, which carry no calibrated values, so it is drawn with bare ticks rather than misleading numbers. """ if self._enable_labels_ticks is not None: return self._enable_labels_ticks return ( len(self._ticks_x) > 0, len(self._ticks_y) > 0, len(self._ticks_z) > 0, ) @enable_labels_ticks.setter def enable_labels_ticks(self, arg: bool | tuple[bool, bool, bool] | None): if isinstance(arg, bool): arg = (arg, arg, arg) self._enable_labels_ticks = arg @property def num_ticks(self) -> int: """ Number of major ticks aimed for along the longest axis. Sizes the derived :attr:`tick_interval`, and is met only approximately there, since the interval is rounded to a value the ticks read well at. For an axis whose ticks were cleared, the count is instead exact: the ticks are spread evenly between :attr:`value_start` and :attr:`value_end`, and are drawn without labels unless :attr:`enable_labels_ticks` says otherwise. Values below ``2`` are raised to ``2``, since a single tick has no spacing to place minor ticks by. """ return self._num_ticks @num_ticks.setter def num_ticks(self, arg: int): self._num_ticks = max(2, int(arg)) @property def num_ticks_minor(self) -> int: """ Number of minor ticks drawn between two adjacent major ticks. When set to ``None``, a suitable number is derived from the spacing of the major ticks, following the convention of matplotlib's :class:`~matplotlib.ticker.AutoMinorLocator`: a major step whose leading digit is 1 or 5 -- such as 100 or 50 -- is divided into 5 intervals, giving 4 minor ticks, while any other step is divided into 4 intervals, giving 3 minor ticks. This keeps the minor ticks on round values instead of, say, splitting a step of 20 into fifths of 4. Falls back to ``4`` when the major tick spacing is still unknown. """ if self._num_ticks_minor is not None: return self._num_ticks_minor def _step_ticks_major() -> float | None: for ticks in (self.ticks_x, self.ticks_y, self.ticks_z): if len(ticks) > 1: step = abs(float(ticks[1]) - float(ticks[0])) if step > 0 and math.isfinite(step): return step return None step = _step_ticks_major() if step is None: return 4 digit_lead = round(10 ** (math.log10(step) % 1)) return 4 if digit_lead in (1, 5, 10) else 3 @num_ticks_minor.setter def num_ticks_minor(self, arg: int | None): self._num_ticks_minor = None if arg is None else int(arg) @property def line_width(self) -> float: """Axis line width.""" return self._line_width @line_width.setter def line_width(self, arg: float): self._line_width = arg @property def label_x(self) -> str: """x-axis label.""" return self._label_x @label_x.setter def label_x(self, arg: str): self._label_x = arg @property def label_y(self) -> str: """y-axis label.""" return self._label_y @label_y.setter def label_y(self, arg: str): self._label_y = arg @property def label_z(self) -> str: """z-axis label.""" return self._label_z @label_z.setter def label_z(self, arg: str): self._label_z = arg @property def font_family(self) -> Path | None: """ Font family for axis labels specified as :class:`Path <pathlib.Path>` to TTF file. """ return self._font_family @font_family.setter def font_family(self, arg: Path | None): self._font_family = arg @property def font_size_labels(self) -> float: """Axis label font-size.""" return self._font_size_labels @font_size_labels.setter def font_size_labels(self, arg: float): self._font_size_labels = arg @property def font_size_ticks(self) -> float: """Tick label font-size.""" return self._font_size_ticks @font_size_ticks.setter def font_size_ticks(self, arg: float): self._font_size_ticks = arg @property def spacing(self) -> float: """ Gap kept around the axes, in scene units. Offsets the axis lines from the volume and the tick labels from the outer end of their ticks, so neither sits flush against what it belongs to. """ return self._spacing @spacing.setter def spacing(self, arg: float): self._spacing = arg @property def tick_length(self) -> float: """Tick length.""" return self._tick_length @tick_length.setter def tick_length(self, arg: float): self._tick_length = arg def _ticks_fallback(self, dim: int) -> tuple[float, ...]: """ Returns :attr:`num_ticks` values spread evenly between :attr:`value_start` and :attr:`value_end` of the axis ``dim`` (0 = x, 1 = y, 2 = z). """ start = float(self.value_start[dim]) end = float(self.value_end[dim]) step = (end - start) / (self.num_ticks - 1) return tuple(start + step * i for i in range(self.num_ticks)) @property def ticks_x(self) -> Sequence[float]: """ Tick values shown along the x-axis. Falls back to evenly spaced ticks, see :attr:`num_ticks`. """ if len(self._ticks_x) == 0: return self._ticks_fallback(0) return self._ticks_x @ticks_x.setter def ticks_x(self, arg: Sequence[float]): self._ticks_x = arg @property def ticks_y(self) -> Sequence[float]: """ Tick values shown along the y-axis. Falls back to evenly spaced ticks, see :attr:`num_ticks`. """ if len(self._ticks_y) == 0: return self._ticks_fallback(1) return self._ticks_y @ticks_y.setter def ticks_y(self, arg: Sequence[float]): self._ticks_y = arg @property def ticks_z(self) -> Sequence[float]: """ Tick values shown along the z-axis. Falls back to evenly spaced ticks, see :attr:`num_ticks`. """ if len(self._ticks_z) == 0: return self._ticks_fallback(2) return self._ticks_z @ticks_z.setter def ticks_z(self, arg: Sequence[float]): self._ticks_z = arg @property def indent_ticks(self) -> bool: """Whether tick labels are indented.""" return self._indent_ticks @indent_ticks.setter def indent_ticks(self, arg: bool): self._indent_ticks = arg def _positions_from_ticks(self, dim: int) -> tuple[float, ...]: """ Returns the normalized positions (0-1) of the ticks of the axis ``dim`` (0 = x, 1 = y, 2 = z), by placing each tick value on the axis in proportion to the extent of the domain. Falls back to evenly spaced positions where the axis spans no length, as the ticks then carry no position to scale. """ ticks = (self.ticks_x, self.ticks_y, self.ticks_z)[dim] span = float(self.extent[dim]) * self.factor[dim] if span == 0: N = len(ticks) return tuple(i / (N - 1) for i in range(N)) if N > 1 else (0.0,) * N return tuple(float(tick) / span for tick in ticks) @property def position_tick_x(self) -> Sequence[float]: """ Normalized positions (0-1) of the x-axis ticks. Derived from :attr:`ticks_x` and :attr:`extent` unless set explicitly. """ if self._position_tick_x is not None: return self._position_tick_x return self._positions_from_ticks(0) @position_tick_x.setter def position_tick_x(self, arg: Sequence[float] | None): self._position_tick_x = arg @property def position_tick_y(self) -> Sequence[float]: """ Normalized positions (0-1) of the y-axis ticks. Derived from :attr:`ticks_y` and :attr:`extent` unless set explicitly. """ if self._position_tick_y is not None: return self._position_tick_y return self._positions_from_ticks(1) @position_tick_y.setter def position_tick_y(self, arg: Sequence[float] | None): self._position_tick_y = arg @property def position_tick_z(self) -> Sequence[float]: """ Normalized positions (0-1) of the z-axis ticks. Derived from :attr:`ticks_z` and :attr:`extent` unless set explicitly. """ if self._position_tick_z is not None: return self._position_tick_z return self._positions_from_ticks(2) @position_tick_z.setter def position_tick_z(self, arg: Sequence[float] | None): self._position_tick_z = arg @property def value_start(self) -> Sequence[float]: """Tick value at the start of each axis (x, y, z).""" return self._value_start @value_start.setter def value_start(self, arg: Sequence[float]): self._value_start = arg @property def value_end(self) -> Sequence[float]: """Tick value at the end of each axis (x, y, z).""" return self._value_end @value_end.setter def value_end(self, arg: Sequence[float]): self._value_end = arg
default_config = SceneConfiguration() # default_config.add_property( # PropertyConfiguration( # "coordination_number", # [ # fefacolors.lightblue, # fefacolors.lightgreen, # fefacolors.orange, # ], # heading="Coordination Number", # # gradient_class=DiscreteGradient, # ) # ) # default_config.add_property( # PropertyConfiguration( # "radius", # FeFaPalette().all(), # heading="Radius [μm]", # orientation=Orientation.HORIZONTAL, # factor=1e6, # precision=-1, # ) # ) # default_config.add_property( # PropertyConfiguration( # "saturation", # [ # fefacolors.yellow, # fefacolors.pink, # ], # heading="Saturation [-]", # precision=3, # frames_solid="LEFT", # ) # ) # default_config.add_property( # PropertyConfiguration( # "temperature", # [ # fefacolors.darkblue, # fefacolors.red, # ], # heading="Temperature [\u00B0C]", # \u00BC = degree # precision=0, # frames_solid="RIGHT", # ) # ) # default_config.add_property( # PropertyConfiguration( # "vapor_pressure", # [ # fefacolors.darkgreen, # fefacolors.orange, # ], # heading="Pressure [Pa]", # precision=0, # ) # )