Source code for fieldview.utils

"""Shared enums and helpers for the FieldView Python package."""

from __future__ import annotations

import os
from dataclasses import dataclass, field
from enum import Enum
from typing import (
    Protocol,
    SupportsFloat,
    SupportsIndex,
    SupportsInt,
    TypeVar,
    cast,
)
from collections.abc import Callable, Mapping, Set

from . import constant
from .exceptions import InvalidArgumentError

__all__ = [
    "TripletLike",
    "Range",
    "RangedValue",
    "CuttingPlane",
    "Plot2DGridLinesOptions",
    "Plot2DTickMarksOptions",
    "Plot2DAxisOptions",
    "Plot2DSamplingOptions",
    "Plot2DAnnotationOptions",
    "Plot2DOptions",
    "LegendSpectrum",
    "LegendContour",
    "LegendLabels",
    "LegendAnnotationText",
    "LegendAnnotation",
    "Legend",
    "VectorOptions",
    "RuledGridAxisOptions",
    "RuledGridOptions",
    "ScalarAnnotation",
    "ScalarMinMax",
    "Colormap",
]

Number = int | float
NumberOrNone = Number | None
PayloadDict = dict[str, float]
StateDict = dict[str, object]
_EnumT = TypeVar("_EnumT", bound=Enum)
_IntLike = SupportsInt | SupportsIndex | str | bytes
_FloatLike = SupportsFloat | SupportsIndex | str | bytes


class TripletLike(Protocol):
    def __len__(self) -> int: ...

    def __getitem__(self, index: int, /) -> object: ...


class _PairLike(Protocol):
    def __len__(self) -> int: ...

    def __getitem__(self, index: int, /) -> object: ...


def _enum_payload(value: Enum) -> object:
    return cast(object, value.value)


def _parse_enum_or_str(enum_cls: type[_EnumT], value: object) -> _EnumT | str | None:
    if value is None:
        return None
    try:
        return enum_cls(str(value))
    except ValueError:
        return str(value)


def _payload_bool(value: object) -> bool | None:
    if value is None:
        return None
    return bool(value)


def _coerce_int(value: object) -> int:
    return int(cast(_IntLike, value))


def _coerce_float(value: object) -> float:
    return float(cast(_FloatLike, value))


def _coerce_pathlike_str(value: object, label: str) -> str:
    if isinstance(value, bool):
        raise InvalidArgumentError(f"{label} must be a string or path-like object")
    if not isinstance(value, (str, os.PathLike)):
        raise InvalidArgumentError(f"{label} must be a string or path-like object")
    path = os.fspath(cast(str | os.PathLike[str], value))
    if not isinstance(path, str):
        raise InvalidArgumentError(f"{label} must be a string or path-like object")
    return path


def _coerce_xyz_triplet(value: object, label: str) -> tuple[float, float, float]:
    if isinstance(value, (str, bytes, bytearray, memoryview, Mapping, Set)):
        raise InvalidArgumentError(f"{label} must be a 3-number sequence")
    if not hasattr(value, "__len__") or not hasattr(value, "__getitem__"):
        raise InvalidArgumentError(f"{label} must be a 3-number sequence")
    indexable = cast(TripletLike, value)
    if len(indexable) != 3:
        raise InvalidArgumentError(f"{label} must be a 3-number sequence")

    result: list[float] = []
    for index in range(3):
        component = indexable[index]
        if isinstance(component, bool) or isinstance(component, (str, bytes)):
            raise InvalidArgumentError(f"{label}[{index + 1}] must be a number")
        try:
            result.append(_coerce_float(component))
        except (OverflowError, TypeError, ValueError) as exc:
            raise InvalidArgumentError(
                f"{label}[{index + 1}] must be a number"
            ) from exc
    return result[0], result[1], result[2]


def _coerce_xy_pair(value: object, label: str) -> tuple[float, float]:
    if isinstance(value, (str, bytes, bytearray, memoryview, Mapping, Set)):
        raise InvalidArgumentError(f"{label} must be a 2-value sequence")
    if not hasattr(value, "__len__") or not hasattr(value, "__getitem__"):
        raise InvalidArgumentError(f"{label} must be a 2-value sequence")
    indexable = cast(_PairLike, value)
    if len(indexable) != 2:
        raise InvalidArgumentError(f"{label} must be a 2-value sequence")

    result: list[float] = []
    for index in range(2):
        component = indexable[index]
        if isinstance(component, bool) or isinstance(component, (str, bytes)):
            raise InvalidArgumentError(f"{label}[{index + 1}] must be a number")
        try:
            result.append(_coerce_float(component))
        except (OverflowError, TypeError, ValueError) as exc:
            raise InvalidArgumentError(
                f"{label}[{index + 1}] must be a number"
            ) from exc
    return result[0], result[1]


def _payload_int(value: object) -> int | None:
    if value is None:
        return None
    return _coerce_int(value)


def _payload_float(value: object) -> float | None:
    if value is None:
        return None
    return _coerce_float(value)


def _payload_str(value: object) -> str | None:
    if value is None:
        return None
    return str(value)


def _payload_geometric_color(
    value: object,
) -> constant.GeometricColor | int | None:
    if value is None:
        return None
    color_value = _coerce_int(value)
    try:
        return constant.GeometricColor(color_value)
    except ValueError:
        return color_value


def _required_float(payload: StateDict, key: str) -> float:
    return _coerce_float(payload[key])


[docs] class Range: """Generic numeric range container.""" _min: NumberOrNone _max: NumberOrNone _abs_min: NumberOrNone _abs_max: NumberOrNone def __init__( self, min: NumberOrNone = None, max: NumberOrNone = None, abs_min: NumberOrNone = None, abs_max: NumberOrNone = None, ) -> None: self._min = min self._max = max self._abs_min = abs_min self._abs_max = abs_max self._validate() def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) @property def min(self) -> NumberOrNone: return self._min @min.setter def min(self, value: NumberOrNone) -> None: previous = self._min self._min = value try: self._validate() except Exception: self._min = previous raise @property def max(self) -> NumberOrNone: return self._max @max.setter def max(self, value: NumberOrNone) -> None: previous = self._max self._max = value try: self._validate() except Exception: self._max = previous raise @property def abs_min(self) -> NumberOrNone: return self._abs_min @abs_min.setter def abs_min(self, value: NumberOrNone) -> None: previous = self._abs_min self._abs_min = value try: self._validate() except Exception: self._abs_min = previous raise @property def abs_max(self) -> NumberOrNone: return self._abs_max @abs_max.setter def abs_max(self, value: NumberOrNone) -> None: previous = self._abs_max self._abs_max = value try: self._validate() except Exception: self._abs_max = previous raise def to_payload(self) -> PayloadDict: """Return a dict payload for range updates.""" payload: PayloadDict = {} if self.min is not None: payload["min"] = float(self.min) if self.max is not None: payload["max"] = float(self.max) if not payload: raise InvalidArgumentError("Range must define at least one of min/max") return payload def _validate(self) -> None: for name in ("min", "max", "abs_min", "abs_max"): value = cast(object, getattr(self, name)) if value is None: continue if isinstance(value, bool) or not isinstance(value, (int, float)): raise InvalidArgumentError(f"Range.{name} must be a number") if self.min is not None and self.max is not None and self.min > self.max: raise InvalidArgumentError("Range.min must be <= Range.max") if ( self.abs_min is not None and self.abs_max is not None and self.abs_min > self.abs_max ): raise InvalidArgumentError("Range.abs_min must be <= Range.abs_max") if self.abs_min is not None: if self.min is not None and self.min < self.abs_min: raise InvalidArgumentError("Range.min must be >= Range.abs_min") if self.max is not None and self.max < self.abs_min: raise InvalidArgumentError("Range.max must be >= Range.abs_min") if self.abs_max is not None: if self.min is not None and self.min > self.abs_max: raise InvalidArgumentError("Range.min must be <= Range.abs_max") if self.max is not None and self.max > self.abs_max: raise InvalidArgumentError("Range.max must be <= Range.abs_max") def __repr__(self) -> str: return ( f"Range(min={self.min!r}, max={self.max!r}, " f"abs_min={self.abs_min!r}, abs_max={self.abs_max!r})" ) def __eq__(self, other: object) -> bool: if not isinstance(other, Range): return NotImplemented return ( self.min == other.min and self.max == other.max and self.abs_min == other.abs_min and self.abs_max == other.abs_max )
[docs] class RangedValue: """Range plus selected value.""" _range: Range _value: NumberOrNone def __init__(self, range: Range | None = None, value: NumberOrNone = None) -> None: self._range = Range() if range is None else range self._value = value self._validate() def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) @property def range(self) -> Range: return self._range @range.setter def range(self, value: Range) -> None: previous = self._range self._range = value try: self._validate() except Exception: self._range = previous raise @property def value(self) -> NumberOrNone: return self._value @value.setter def value(self, value: NumberOrNone) -> None: previous = self._value self._value = value try: self._validate() except Exception: self._value = previous raise def _validate(self) -> None: if not isinstance(self.range, Range): raise InvalidArgumentError("RangedValue.range must be a Range") if self.value is None: return if isinstance(self.value, bool) or not isinstance(self.value, (int, float)): raise InvalidArgumentError("RangedValue.value must be a number") if self.range.min is not None and self.value < self.range.min: raise InvalidArgumentError( "RangedValue.value must be >= RangedValue.range.min" ) if self.range.max is not None and self.value > self.range.max: raise InvalidArgumentError( "RangedValue.value must be <= RangedValue.range.max" ) if self.range.abs_min is not None and self.value < self.range.abs_min: raise InvalidArgumentError( "RangedValue.value must be >= RangedValue.range.abs_min" ) if self.range.abs_max is not None and self.value > self.range.abs_max: raise InvalidArgumentError( "RangedValue.value must be <= RangedValue.range.abs_max" ) def to_payload(self) -> PayloadDict: """Return range and current value payload.""" payload: PayloadDict = {} if self.range.min is not None or self.range.max is not None: payload.update(self.range.to_payload()) if self.value is not None: payload["current"] = float(self.value) if not payload: raise InvalidArgumentError( "RangedValue must define at least one of range.min/range.max/value" ) return payload def __repr__(self) -> str: return f"RangedValue(range={self.range!r}, value={self.value!r})" def __eq__(self, other: object) -> bool: if not isinstance(other, RangedValue): return NotImplemented return self.range == other.range and self.value == other.value
def _normalize_range_from_values( *, min_value: Number, max_value: Number, abs_min: NumberOrNone = None, abs_max: NumberOrNone = None, ) -> Range: """Build a validated Range from potentially inconsistent raw values. This is intended for values read from core state where min/max or abs_min/abs_max may be swapped or partially missing. """ min_f = float(min_value) max_f = float(max_value) if min_f > max_f: min_f, max_f = max_f, min_f if abs_min is None and abs_max is None: abs_min_f = min_f abs_max_f = max_f elif abs_min is None: abs_max_f = _coerce_float(abs_max) abs_min_f = min(min_f, max_f, abs_max_f) elif abs_max is None: abs_min_f = _coerce_float(abs_min) abs_max_f = max(min_f, max_f, abs_min_f) else: abs_min_f = _coerce_float(abs_min) abs_max_f = _coerce_float(abs_max) if abs_min_f > abs_max_f: abs_min_f, abs_max_f = abs_max_f, abs_min_f min_f = max(abs_min_f, min(min_f, abs_max_f)) max_f = max(abs_min_f, min(max_f, abs_max_f)) if min_f > max_f: min_f, max_f = max_f, min_f return Range(min=min_f, max=max_f, abs_min=abs_min_f, abs_max=abs_max_f) def _normalize_ranged_value_from_values( *, min_value: Number, max_value: Number, abs_min: NumberOrNone = None, abs_max: NumberOrNone = None, current: NumberOrNone = None, ) -> RangedValue: """Build a validated RangedValue from potentially inconsistent raw values.""" normalized_range = _normalize_range_from_values( min_value=min_value, max_value=max_value, abs_min=abs_min, abs_max=abs_max, ) value_f: float | None = None if current is not None: value_f = float(current) if normalized_range.min is not None and value_f < normalized_range.min: value_f = float(normalized_range.min) if normalized_range.max is not None and value_f > normalized_range.max: value_f = float(normalized_range.max) return RangedValue(range=normalized_range, value=value_f) class _BoundRange(Range): """Range controller bound to core get/set callbacks.""" __slots__ = ("_get_state", "_set_state", "_cast") def __init__( # pyright: ignore[reportMissingSuperCall] self, get_state: Callable[[], StateDict], set_state: Callable[..., None], cast: Callable[[object], Number], ) -> None: self._get_state = get_state self._set_state = set_state self._cast = cast def _state(self) -> StateDict: return self._get_state() @property def abs_min(self) -> NumberOrNone: return self._cast(self._state()["abs_min"]) @abs_min.setter def abs_min(self, value: NumberOrNone) -> None: raise InvalidArgumentError("Range.abs_min is read-only for bound ranges") @property def abs_max(self) -> NumberOrNone: return self._cast(self._state()["abs_max"]) @abs_max.setter def abs_max(self, value: NumberOrNone) -> None: raise InvalidArgumentError("Range.abs_max is read-only for bound ranges") @property def min(self) -> NumberOrNone: return self._cast(self._state()["min"]) @min.setter def min(self, value: NumberOrNone) -> None: if value is None: raise InvalidArgumentError("Range.min cannot be None") self._set_state(min=self._cast(value)) @property def max(self) -> NumberOrNone: return self._cast(self._state()["max"]) @max.setter def max(self, value: NumberOrNone) -> None: if value is None: raise InvalidArgumentError("Range.max cannot be None") self._set_state(max=self._cast(value)) def to_payload(self) -> PayloadDict: state = self._state() return { "min": float(self._cast(state["min"])), "max": float(self._cast(state["max"])), } class _BoundRangedValue(RangedValue): """RangedValue controller bound to core get/set callbacks.""" __slots__ = ("_range", "_get_state", "_set_state", "_cast") def __init__( # pyright: ignore[reportMissingSuperCall] self, get_state: Callable[[], StateDict], set_state: Callable[..., None], cast: Callable[[object], Number], ) -> None: self._get_state = get_state self._set_state = set_state self._cast = cast self._range = _BoundRange(get_state=get_state, set_state=set_state, cast=cast) @property def range(self) -> Range: return self._range @range.setter def range(self, value: Range) -> None: if not isinstance(value, Range): raise InvalidArgumentError("RangedValue.range must be a Range") self._set_state(**value.to_payload()) @property def value(self) -> NumberOrNone: return self._cast(self._get_state()["current"]) @value.setter def value(self, value: NumberOrNone) -> None: if value is None: raise InvalidArgumentError("RangedValue.value cannot be None") self._set_state(current=self._cast(value)) def to_payload(self) -> PayloadDict: payload = self.range.to_payload() if self.value is None: raise InvalidArgumentError("RangedValue.value cannot be None") payload["current"] = float(self.value) return payload def _bind_range( *, get_state: Callable[[], StateDict], set_state: Callable[..., None], cast: Callable[[object], Number], ) -> Range: """Return a Range bound to core callbacks.""" return _BoundRange(get_state=get_state, set_state=set_state, cast=cast) def _bind_ranged_value( *, get_state: Callable[[], StateDict], set_state: Callable[..., None], cast: Callable[[object], Number], ) -> RangedValue: """Return a RangedValue bound to core callbacks.""" return _BoundRangedValue(get_state=get_state, set_state=set_state, cast=cast) def _to_point3(value: object, label: str) -> tuple[float, float, float]: return _coerce_xyz_triplet(value, label) def _to_point2(value: object, label: str) -> tuple[float, float]: return _coerce_xy_pair(value, label) def _ensure_known_attribute(instance: object, name: str) -> None: if name.startswith("_"): return annotations = getattr(type(instance), "__annotations__", {}) if name in annotations: return class_attr = getattr(type(instance), name, None) if isinstance(class_attr, property): return raise AttributeError(f"{type(instance).__name__} has no attribute '{name}'")
[docs] @dataclass(frozen=True) class CuttingPlane: """Cutting-plane definition used by iso surfaces.""" mode: str pt1: tuple[float, float, float] pt2: tuple[float, float, float] pt3: tuple[float, float, float] = (0.0, 0.0, 0.0)
[docs] @staticmethod def point_and_normal( point: TripletLike, normal: TripletLike, ) -> "CuttingPlane": """Create a cutting plane from one point and a normal vector. Args: point: XYZ point lying on the plane. normal: XYZ normal vector defining the plane orientation. """ return CuttingPlane( mode="point_and_normal", pt1=_to_point3(point, "point"), pt2=_to_point3(normal, "normal"), pt3=(0.0, 0.0, 0.0), )
[docs] @staticmethod def points_in_plane( pt1: TripletLike, pt2: TripletLike, pt3: TripletLike, ) -> "CuttingPlane": """Create a cutting plane from three points lying in the plane. Args: pt1: First XYZ point in the plane. pt2: Second XYZ point in the plane. pt3: Third XYZ point in the plane. """ return CuttingPlane( mode="points_in_plane", pt1=_to_point3(pt1, "pt1"), pt2=_to_point3(pt2, "pt2"), pt3=_to_point3(pt3, "pt3"), )
def to_payload(self) -> StateDict: payload: StateDict = { "mode": self.mode, "pt1": [self.pt1[0], self.pt1[1], self.pt1[2]], "pt2": [self.pt2[0], self.pt2[1], self.pt2[2]], } if self.mode == "points_in_plane": payload["pt3"] = [self.pt3[0], self.pt3[1], self.pt3[2]] return payload @staticmethod def from_payload(payload: StateDict) -> "CuttingPlane": """Create a cutting plane from a host payload dictionary. Args: payload: Serialized cutting-plane payload from the FieldView host. """ mode = str(payload.get("mode", "point_and_normal")) pt1 = _to_point3(payload.get("pt1", (0.0, 0.0, 0.0)), "pt1") pt2 = _to_point3(payload.get("pt2", (0.0, 0.0, 0.0)), "pt2") pt3 = _to_point3(payload.get("pt3", (0.0, 0.0, 0.0)), "pt3") return CuttingPlane(mode=mode, pt1=pt1, pt2=pt2, pt3=pt3)
[docs] @dataclass class VectorOptions: """Vector display options.""" #: Straight or curved shafts. shaft_type: constant.VectorShaftType | str | None = None #: 2D or 3D heads. head_type: constant.VectorHeadType | str | None = None #: Toggle head scaling. head_scaling: bool | None = None #: Head scaling factor. head_scaling_value: float | None = None #: Vector projection type (total/yz/xz/xy/projected). type: constant.VectorProjectionType | str | None = None #: Toggle vector skipping. skip: bool | None = None #: Skip fraction (0.0-1.0). skip_value: float | None = None #: Toggle uniform sampling grid. uniform_sampling: bool | None = None #: Uniform sampling count for X. x_samples: int | None = None #: Uniform sampling count for Y. y_samples: int | None = None #: Uniform sampling count for Z. z_samples: int | None = None #: Uniform sampling count for R (cylindrical). r_samples: int | None = None #: Uniform sampling count for T (cylindrical). t_samples: int | None = None #: Shaft length scale. vector_scale: float | None = None #: Curved vector integration time limit. time_limit: float | None = None #: Curved vector display type (complete/filament/growing). display_type: constant.CurvedVectorDisplayType | str | None = None #: Toggle curved vector animation. animate: bool | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return key = name if name == "r_samples": object.__setattr__(self, "x_samples", value) key = "x_samples" elif name == "t_samples": object.__setattr__(self, "y_samples", value) key = "y_samples" elif name == "x_samples": object.__setattr__(self, "r_samples", value) elif name == "y_samples": object.__setattr__(self, "t_samples", value) callback = getattr(self, "_on_change", None) if callback is None or value is None: return if key in {"shaft_type", "head_type", "type", "display_type"}: if isinstance(value, Enum): callback(key, _enum_payload(value)) else: callback(key, str(value)) elif key in {"head_scaling", "skip", "uniform_sampling", "animate"}: callback(key, bool(value)) elif key in {"head_scaling_value", "skip_value", "vector_scale", "time_limit"}: callback(key, _coerce_float(value)) elif key in {"x_samples", "y_samples", "z_samples"}: callback(key, _coerce_int(value)) def to_payload(self) -> StateDict: """Convert options into a payload dict.""" ignore: set[str] = set() if self.r_samples is not None: ignore.add("x_samples") if self.t_samples is not None: ignore.add("y_samples") payload: StateDict = {} for key, value in cast(dict[str, object], self.__dict__).items(): if key.startswith("_") or key in ignore or value is None: continue if isinstance(value, Enum): payload[key] = _enum_payload(value) else: payload[key] = value return payload @classmethod def from_payload(cls, payload: StateDict) -> "VectorOptions": """Build VectorOptions from a payload dict.""" x_samples = payload.get("x_samples") y_samples = payload.get("y_samples") z_samples = payload.get("z_samples") return cls( shaft_type=_parse_enum_or_str( constant.VectorShaftType, payload.get("shaft_type") ), head_type=_parse_enum_or_str( constant.VectorHeadType, payload.get("head_type") ), head_scaling=_payload_bool(payload.get("head_scaling")), head_scaling_value=_payload_float(payload.get("head_scaling_value")), type=_parse_enum_or_str(constant.VectorProjectionType, payload.get("type")), skip=_payload_bool(payload.get("skip")), skip_value=_payload_float(payload.get("skip_value")), uniform_sampling=_payload_bool(payload.get("uniform_sampling")), x_samples=_payload_int(x_samples), y_samples=_payload_int(y_samples), z_samples=_payload_int(z_samples), r_samples=_payload_int(x_samples), t_samples=_payload_int(y_samples), vector_scale=_payload_float(payload.get("vector_scale")), time_limit=_payload_float(payload.get("time_limit")), display_type=_parse_enum_or_str( constant.CurvedVectorDisplayType, payload.get("display_type") ), animate=_payload_bool(payload.get("animate")), )
[docs] def copy(self) -> "VectorOptions": """Return a detached copy of the options.""" return VectorOptions( shaft_type=self.shaft_type, head_type=self.head_type, head_scaling=self.head_scaling, head_scaling_value=self.head_scaling_value, type=self.type, skip=self.skip, skip_value=self.skip_value, uniform_sampling=self.uniform_sampling, x_samples=self.x_samples, y_samples=self.y_samples, z_samples=self.z_samples, r_samples=self.r_samples, t_samples=self.t_samples, vector_scale=self.vector_scale, time_limit=self.time_limit, display_type=self.display_type, animate=self.animate, )
[docs] @dataclass class Colormap: """Scalar colormap options.""" #: constant.ColormapName or user map filename. name: constant.ColormapName | str | None = None #: Toggle inverted colormap. invert: bool | None = None #: Toggle filled contours. filled_contour: bool | None = None #: Colormap scale (linear/log). scale: constant.Scale | str = constant.Scale.LINEAR #: Toggle local scalar min/max. use_local: bool | None = None #: Scalar minimum. min: float | None = None #: Scalar maximum. max: float | None = None #: Contour count (2-500). num_contours: int | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) _on_unify: Callable[[], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) if callback is None or value is None: return if name == "name": if isinstance(value, Enum): callback(name, _enum_payload(value)) else: callback(name, str(value)) elif name in {"invert", "filled_contour", "use_local"}: callback(name, bool(value)) elif name == "scale": if isinstance(value, Enum): scale_value = str(_enum_payload(value)) else: scale_value = str(value) scale_value = scale_value.lower() if scale_value == constant.Scale.LOG.value: callback("log_scale", True) elif scale_value == constant.Scale.LINEAR.value: callback("log_scale", False) else: raise InvalidArgumentError("scale must be Scale.LINEAR or Scale.LOG") elif name in {"min", "max"}: callback(name, _coerce_float(value)) elif name == "num_contours": callback("number_of_contours", _coerce_int(value)) @property def range(self) -> Range | None: """Get or set the scalar min/max as a Range, if both are set.""" if self.min is None or self.max is None: return None return Range(float(self.min), float(self.max)) @range.setter def range(self, value: "Range") -> None: """Set scalar min/max from a Range.""" if not isinstance(value, Range): raise InvalidArgumentError("range must be a Range") if value.min is None or value.max is None: raise InvalidArgumentError("range must define both min and max") self.min = float(value.min) self.max = float(value.max)
[docs] def unify_map(self) -> None: """Unify the colormap across coordinate surfaces.""" callback = getattr(self, "_on_unify", None) if callback is not None: callback()
[docs] def copy(self) -> "Colormap": """Return a detached copy of the options.""" return Colormap( name=self.name, invert=self.invert, filled_contour=self.filled_contour, scale=self.scale, use_local=self.use_local, min=self.min, max=self.max, num_contours=self.num_contours, )
def to_payload(self) -> StateDict: """Convert options into a payload dict.""" payload: StateDict = {} if self.name is not None: payload["name"] = ( _enum_payload(self.name) if isinstance(self.name, Enum) else str(self.name) ) if self.invert is not None: payload["invert"] = bool(self.invert) if self.filled_contour is not None: payload["filled_contour"] = bool(self.filled_contour) scale_value = ( str(_enum_payload(self.scale)) if isinstance(self.scale, Enum) else str(self.scale) ) scale_value = scale_value.lower() if scale_value == constant.Scale.LOG.value: payload["log_scale"] = True elif scale_value == constant.Scale.LINEAR.value: payload["log_scale"] = False else: raise InvalidArgumentError("scale must be Scale.LINEAR or Scale.LOG") if self.use_local is not None: payload["use_local"] = bool(self.use_local) if self.min is not None: payload["min"] = float(self.min) if self.max is not None: payload["max"] = float(self.max) if self.num_contours is not None: payload["number_of_contours"] = int(self.num_contours) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Colormap": """Create options from a payload dict.""" name_value = payload.get("name") name: constant.ColormapName | str | None = None if name_value: try: name = constant.ColormapName(str(name_value)) except ValueError: name = str(name_value) scale_value = ( constant.Scale.LOG if payload.get("log_scale") else constant.Scale.LINEAR ) return cls( name=name, invert=_payload_bool(payload.get("invert")), filled_contour=_payload_bool(payload.get("filled_contour")), scale=scale_value, use_local=_payload_bool(payload.get("use_local")), min=_payload_float(payload.get("min")), max=_payload_float(payload.get("max")), num_contours=_payload_int(payload.get("number_of_contours")), )
@dataclass class LegendSpectrum: """Legend spectrum options.""" #: Toggle colorbar. colorbar: bool | None = None #: Toggle colorbar border. border: bool | None = None #: Toggle horizontal orientation. horizontal: bool | None = None #: Number of labels (2-52). num_labels: int | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) if callback is None or value is None: return key = f"spectrum_{name}" if name in {"colorbar", "border", "horizontal"}: callback(key, bool(value)) elif name == "num_labels": callback(key, _coerce_int(value)) def to_payload(self) -> StateDict: payload: StateDict = {} if self.colorbar is not None: payload["spectrum_colorbar"] = bool(self.colorbar) if self.border is not None: payload["spectrum_border"] = bool(self.border) if self.horizontal is not None: payload["spectrum_horizontal"] = bool(self.horizontal) if self.num_labels is not None: payload["spectrum_num_labels"] = int(self.num_labels) return payload @classmethod def from_payload(cls, payload: StateDict) -> "LegendSpectrum": """Create spectrum options from a payload dictionary. Args: payload: Serialized legend-spectrum payload from the FieldView host. """ return cls( colorbar=_payload_bool(payload.get("spectrum_colorbar")), border=_payload_bool(payload.get("spectrum_border")), horizontal=_payload_bool(payload.get("spectrum_horizontal")), num_labels=_payload_int(payload.get("spectrum_num_labels")), ) def copy(self) -> "LegendSpectrum": """Return a detached copy of the options.""" return LegendSpectrum( colorbar=self.colorbar, border=self.border, horizontal=self.horizontal, num_labels=self.num_labels, ) @dataclass class LegendContour: """Legend contour options.""" #: constant.LegendLabelsPerLine (single/multi). labels_per_line: constant.LegendLabelsPerLine | str | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return if name != "labels_per_line": return callback = getattr(self, "_on_change", None) if callback is None or value is None: return if isinstance(value, Enum): callback("contour_labels_per_line", _enum_payload(value)) else: callback("contour_labels_per_line", str(value)) def to_payload(self) -> StateDict: payload: StateDict = {} if self.labels_per_line is not None: if isinstance(self.labels_per_line, Enum): payload["contour_labels_per_line"] = _enum_payload(self.labels_per_line) else: payload["contour_labels_per_line"] = str(self.labels_per_line) return payload @classmethod def from_payload(cls, payload: StateDict) -> "LegendContour": """Create contour options from a payload dictionary. Args: payload: Serialized legend-contour payload from the FieldView host. """ value = payload.get("contour_labels_per_line") parsed: constant.LegendLabelsPerLine | str | None = None if value is not None: try: parsed = constant.LegendLabelsPerLine(str(value)) except ValueError: parsed = str(value) return cls(labels_per_line=parsed) def copy(self) -> "LegendContour": """Return a detached copy of the options.""" return LegendContour(labels_per_line=self.labels_per_line) @dataclass class LegendLabels: """Legend labels options.""" #: Toggle legend labels. show: bool | None = None #: constant.LegendLabelsColorMode (geometric/scalar). color_mode: constant.LegendLabelsColorMode | str | None = None #: Geometric color id (1-10). geometric_color: constant.GeometricColor | int | None = None #: constant.NumericalFormat. numerical_format: constant.NumericalFormat | str | None = None #: Decimal places for labels. decimal_places: int | None = None #: constant.Font. font: constant.Font | str | None = None #: Font size. size: int | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) if callback is None or value is None: return if name == "show": callback("labels", bool(value)) elif name == "color_mode": if isinstance(value, Enum): callback("labels_color_mode", _enum_payload(value)) else: callback("labels_color_mode", str(value)) elif name == "geometric_color": callback("labels_color", _coerce_int(value)) elif name == "numerical_format": if isinstance(value, Enum): callback("labels_numerical_format", _enum_payload(value)) else: callback("labels_numerical_format", str(value)) elif name == "decimal_places": callback("labels_decimal_places", _coerce_int(value)) elif name == "font": if isinstance(value, Enum): callback("labels_font", _enum_payload(value)) else: callback("labels_font", str(value)) elif name == "size": callback("labels_size", _coerce_int(value)) def to_payload(self) -> StateDict: payload: StateDict = {} if self.show is not None: payload["labels"] = bool(self.show) if self.color_mode is not None: if isinstance(self.color_mode, Enum): payload["labels_color_mode"] = _enum_payload(self.color_mode) else: payload["labels_color_mode"] = str(self.color_mode) if self.geometric_color is not None: payload["labels_color"] = int(self.geometric_color) if self.numerical_format is not None: if isinstance(self.numerical_format, Enum): payload["labels_numerical_format"] = _enum_payload( self.numerical_format ) else: payload["labels_numerical_format"] = str(self.numerical_format) if self.decimal_places is not None: payload["labels_decimal_places"] = int(self.decimal_places) if self.font is not None: if isinstance(self.font, Enum): payload["labels_font"] = _enum_payload(self.font) else: payload["labels_font"] = str(self.font) if self.size is not None: payload["labels_size"] = int(self.size) return payload @classmethod def from_payload(cls, payload: StateDict) -> "LegendLabels": """Create label options from a payload dictionary. Args: payload: Serialized legend-label payload from the FieldView host. """ return cls( show=_payload_bool(payload.get("labels")), color_mode=_parse_enum_or_str( constant.LegendLabelsColorMode, payload.get("labels_color_mode") ), geometric_color=_payload_geometric_color(payload.get("labels_color")), numerical_format=_parse_enum_or_str( constant.NumericalFormat, payload.get("labels_numerical_format") ), decimal_places=_payload_int(payload.get("labels_decimal_places")), font=_parse_enum_or_str(constant.Font, payload.get("labels_font")), size=_payload_int(payload.get("labels_size")), ) def copy(self) -> "LegendLabels": """Return a detached copy of the options.""" return LegendLabels( show=self.show, color_mode=self.color_mode, geometric_color=self.geometric_color, numerical_format=self.numerical_format, decimal_places=self.decimal_places, font=self.font, size=self.size, ) @dataclass class LegendAnnotationText: """Legend annotation text options.""" #: Text value. text: str | None = None #: constant.Font. font: constant.Font | str | None = None #: Font size. size: int | None = None #: Geometric color id (1-10). color: constant.GeometricColor | int | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) _prefix: str | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) prefix = getattr(self, "_prefix", None) if callback is None or prefix is None or value is None: return if name == "text": callback(prefix, str(value)) elif name == "font": if isinstance(value, Enum): callback(f"{prefix}_font", _enum_payload(value)) else: callback(f"{prefix}_font", str(value)) elif name == "size": callback(f"{prefix}_size", _coerce_int(value)) elif name == "color": callback(f"{prefix}_color", _coerce_int(value)) def to_payload(self, prefix: str) -> StateDict: """Convert annotation text options into a payload dictionary. Args: prefix: Payload key prefix such as ``title`` or ``subtitle``. """ payload: StateDict = {} if self.text is not None: payload[prefix] = str(self.text) if self.font is not None: if isinstance(self.font, Enum): payload[f"{prefix}_font"] = _enum_payload(self.font) else: payload[f"{prefix}_font"] = str(self.font) if self.size is not None: payload[f"{prefix}_size"] = int(self.size) if self.color is not None: payload[f"{prefix}_color"] = int(self.color) return payload @classmethod def from_payload(cls, payload: StateDict, prefix: str) -> "LegendAnnotationText": """Create annotation text options from a payload dictionary. Args: payload: Serialized annotation-text payload from the FieldView host. prefix: Payload key prefix such as ``title`` or ``subtitle``. """ return cls( text=_payload_str(payload.get(prefix)), font=_parse_enum_or_str(constant.Font, payload.get(f"{prefix}_font")), size=_payload_int(payload.get(f"{prefix}_size")), color=_payload_geometric_color(payload.get(f"{prefix}_color")), ) def copy(self) -> "LegendAnnotationText": """Return a detached copy of the options.""" return LegendAnnotationText( text=self.text, font=self.font, size=self.size, color=self.color, ) @dataclass class LegendAnnotation: """Legend annotation options.""" #: Toggle legend annotation. show: bool | None = None #: constant.LegendAnnotationPosition. position: constant.LegendAnnotationPosition | str | None = None #: LegendAnnotationText. title: LegendAnnotationText | None = None #: LegendAnnotationText. subtitle: LegendAnnotationText | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return if name in {"show", "position"}: callback = getattr(self, "_on_change", None) if callback is None or value is None: return if name == "show": callback("annotation", bool(value)) else: if isinstance(value, Enum): callback("annotation_position", _enum_payload(value)) else: callback("annotation_position", str(value)) return if name not in {"title", "subtitle"} or not isinstance( value, LegendAnnotationText ): return callback = getattr(self, "_on_change", None) if callback is None: return prefix = "title" if name == "title" else "subtitle" object.__setattr__(value, "_on_change", callback) object.__setattr__(value, "_prefix", prefix) def to_payload(self) -> StateDict: payload: StateDict = {} if self.show is not None: payload["annotation"] = bool(self.show) if self.position is not None: if isinstance(self.position, Enum): payload["annotation_position"] = _enum_payload(self.position) else: payload["annotation_position"] = str(self.position) if self.title is not None: payload.update(self.title.to_payload("title")) if self.subtitle is not None: payload.update(self.subtitle.to_payload("subtitle")) return payload @classmethod def from_payload(cls, payload: StateDict) -> "LegendAnnotation": """Create annotation options from a payload dictionary. Args: payload: Serialized legend-annotation payload from the FieldView host. """ position = payload.get("annotation_position") parsed_position: constant.LegendAnnotationPosition | str | None = None if position is not None: try: parsed_position = constant.LegendAnnotationPosition(str(position)) except ValueError: parsed_position = str(position) return cls( show=_payload_bool(payload.get("annotation")), position=parsed_position, title=LegendAnnotationText.from_payload(payload, "title"), subtitle=LegendAnnotationText.from_payload(payload, "subtitle"), ) def copy(self) -> "LegendAnnotation": """Return a detached copy of the options.""" return LegendAnnotation( show=self.show, position=self.position, title=self.title.copy() if self.title is not None else None, subtitle=self.subtitle.copy() if self.subtitle is not None else None, )
[docs] @dataclass class Legend: """Legend options.""" #: Toggle legend visibility. show: bool | None = None #: Toggle legend frame. frame: bool | None = None #: Toggle legend background. background: bool | None = None #: constant.LegendType (spectrum/contour). type: constant.LegendType | str | None = None #: Relative legend position as (x, y) in NDC. relative_position: tuple[float, float] | None = None #: Width scale (1.0 is natural width). scale_width: float | None = None #: Height scale (1.0 is natural height). scale_height: float | None = None #: LegendSpectrum. spectrum: LegendSpectrum | None = None #: LegendContour. contour: LegendContour | None = None #: LegendLabels. labels: LegendLabels | None = None #: LegendAnnotation. annotation: LegendAnnotation | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) if name in {"spectrum", "contour", "labels", "annotation"}: if callback is None or value is None: return if name == "spectrum" and isinstance(value, LegendSpectrum): object.__setattr__(value, "_on_change", callback) elif name == "contour" and isinstance(value, LegendContour): object.__setattr__(value, "_on_change", callback) elif name == "labels" and isinstance(value, LegendLabels): object.__setattr__(value, "_on_change", callback) elif name == "annotation" and isinstance(value, LegendAnnotation): object.__setattr__(value, "_on_change", callback) return if callback is None or value is None: return if name in {"show", "frame", "background"}: callback(name, bool(value)) elif name in {"scale_width", "scale_height"}: callback(name, _coerce_float(value)) elif name == "relative_position": xpos, ypos = _to_point2(value, "relative_position") callback("relative_position", [xpos, ypos]) elif name == "type": if isinstance(value, Enum): callback("type", _enum_payload(value)) else: callback("type", str(value)) def to_payload(self) -> StateDict: payload: StateDict = {} if self.show is not None: payload["show"] = bool(self.show) if self.frame is not None: payload["frame"] = bool(self.frame) if self.background is not None: payload["background"] = bool(self.background) if self.type is not None: if isinstance(self.type, Enum): payload["type"] = _enum_payload(self.type) else: payload["type"] = str(self.type) if self.relative_position is not None: xpos, ypos = _to_point2(self.relative_position, "relative_position") payload["relative_position"] = [xpos, ypos] if self.scale_width is not None: payload["scale_width"] = float(self.scale_width) if self.scale_height is not None: payload["scale_height"] = float(self.scale_height) if self.spectrum is not None: payload.update(self.spectrum.to_payload()) if self.contour is not None: payload.update(self.contour.to_payload()) if self.labels is not None: payload.update(self.labels.to_payload()) if self.annotation is not None: payload.update(self.annotation.to_payload()) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Legend": """Create legend options from a payload dictionary. Args: payload: Serialized legend payload from the FieldView host. """ type_value = payload.get("type") parsed_type: constant.LegendType | str | None = None if type_value is not None: try: parsed_type = constant.LegendType(str(type_value)) except ValueError: parsed_type = str(type_value) relative_position = payload.get("relative_position") parsed_relative_position = None if relative_position is not None: parsed_relative_position = _to_point2( relative_position, "relative_position" ) else: xpos = payload.get("xpos") ypos = payload.get("ypos") if xpos is not None and ypos is not None: parsed_relative_position = (_coerce_float(xpos), _coerce_float(ypos)) return cls( show=_payload_bool(payload.get("show")), frame=_payload_bool(payload.get("frame")), background=_payload_bool(payload.get("background")), type=parsed_type, relative_position=parsed_relative_position, scale_width=_payload_float(payload.get("scale_width")), scale_height=_payload_float(payload.get("scale_height")), spectrum=LegendSpectrum.from_payload(payload), contour=LegendContour.from_payload(payload), labels=LegendLabels.from_payload(payload), annotation=LegendAnnotation.from_payload(payload), )
[docs] def copy(self) -> "Legend": """Return a detached copy of the options.""" return Legend( show=self.show, frame=self.frame, background=self.background, type=self.type, relative_position=self.relative_position, scale_width=self.scale_width, scale_height=self.scale_height, spectrum=self.spectrum.copy() if self.spectrum is not None else None, contour=self.contour.copy() if self.contour is not None else None, labels=self.labels.copy() if self.labels is not None else None, annotation=self.annotation.copy() if self.annotation is not None else None, )
@dataclass class Plot2DGridLinesOptions: """Grid-line options for a Plot2D axis.""" enabled: bool | None = None color: constant.GeometricColor | int | None = None style: constant.Plot2DLineStyle | str | None = None def to_payload(self) -> StateDict: payload: StateDict = {} if self.enabled is not None: payload["enabled"] = bool(self.enabled) if self.color is not None: payload["color"] = int(self.color) if self.style is not None: if isinstance(self.style, Enum): payload["style"] = _enum_payload(self.style) else: payload["style"] = str(self.style) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Plot2DGridLinesOptions": style = _parse_enum_or_str(constant.Plot2DLineStyle, payload.get("style")) return cls( enabled=_payload_bool(payload.get("enabled")), color=_payload_geometric_color(payload.get("color")), style=style, ) def copy(self) -> "Plot2DGridLinesOptions": return Plot2DGridLinesOptions( enabled=self.enabled, color=self.color, style=self.style, ) @dataclass class Plot2DTickMarksOptions: """Tick-mark options for a Plot2D axis.""" enabled: bool | None = None unit: float | None = None def to_payload(self) -> StateDict: payload: StateDict = {} if self.enabled is not None: payload["enabled"] = bool(self.enabled) if self.unit is not None: payload["unit"] = float(self.unit) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Plot2DTickMarksOptions": return cls( enabled=_payload_bool(payload.get("enabled")), unit=_payload_float(payload.get("unit")), ) def copy(self) -> "Plot2DTickMarksOptions": return Plot2DTickMarksOptions(enabled=self.enabled, unit=self.unit) @dataclass class Plot2DAxisOptions: """Axis options for Plot2D horizontal/left/right axes.""" label: str | None = None color: constant.GeometricColor | int | None = None min: float | None = None max: float | None = None normalize: float | None = None fix_axis: bool | None = None major_grid_lines: Plot2DGridLinesOptions | None = None minor_grid_lines: Plot2DGridLinesOptions | None = None major_tick_marks: Plot2DTickMarksOptions | None = None minor_tick_marks: Plot2DTickMarksOptions | None = None def to_payload(self) -> StateDict: payload: StateDict = {} if self.label is not None: payload["label"] = str(self.label) if self.color is not None: payload["color"] = int(self.color) if self.min is not None: payload["min"] = float(self.min) if self.max is not None: payload["max"] = float(self.max) if self.normalize is not None: payload["normalize"] = float(self.normalize) if self.fix_axis is not None: payload["fix_axis"] = bool(self.fix_axis) if self.major_grid_lines is not None: payload["major_grid_lines"] = self.major_grid_lines.to_payload() if self.minor_grid_lines is not None: payload["minor_grid_lines"] = self.minor_grid_lines.to_payload() if self.major_tick_marks is not None: payload["major_tick_marks"] = self.major_tick_marks.to_payload() if self.minor_tick_marks is not None: payload["minor_tick_marks"] = self.minor_tick_marks.to_payload() return payload @classmethod def from_payload(cls, payload: StateDict) -> "Plot2DAxisOptions": major_grid_payload = payload.get("major_grid_lines") minor_grid_payload = payload.get("minor_grid_lines") major_tick_payload = payload.get("major_tick_marks") minor_tick_payload = payload.get("minor_tick_marks") return cls( label=_payload_str(payload.get("label")), color=_payload_geometric_color(payload.get("color")), min=_payload_float(payload.get("min")), max=_payload_float(payload.get("max")), normalize=_payload_float(payload.get("normalize")), fix_axis=_payload_bool(payload.get("fix_axis")), major_grid_lines=( Plot2DGridLinesOptions.from_payload(cast(StateDict, major_grid_payload)) if isinstance(major_grid_payload, Mapping) else None ), minor_grid_lines=( Plot2DGridLinesOptions.from_payload(cast(StateDict, minor_grid_payload)) if isinstance(minor_grid_payload, Mapping) else None ), major_tick_marks=( Plot2DTickMarksOptions.from_payload(cast(StateDict, major_tick_payload)) if isinstance(major_tick_payload, Mapping) else None ), minor_tick_marks=( Plot2DTickMarksOptions.from_payload(cast(StateDict, minor_tick_payload)) if isinstance(minor_tick_payload, Mapping) else None ), ) def copy(self) -> "Plot2DAxisOptions": return Plot2DAxisOptions( label=self.label, color=self.color, min=self.min, max=self.max, normalize=self.normalize, fix_axis=self.fix_axis, major_grid_lines=( self.major_grid_lines.copy() if self.major_grid_lines else None ), minor_grid_lines=( self.minor_grid_lines.copy() if self.minor_grid_lines else None ), major_tick_marks=( self.major_tick_marks.copy() if self.major_tick_marks else None ), minor_tick_marks=( self.minor_tick_marks.copy() if self.minor_tick_marks else None ), ) @dataclass class Plot2DSamplingOptions: """Sampling options for Plot2D.""" mode: constant.Plot2DSamplingMode | str | None = None number_samples: int | None = None def to_payload(self) -> StateDict: payload: StateDict = {} if self.mode is not None: if isinstance(self.mode, Enum): payload["mode"] = _enum_payload(self.mode) else: payload["mode"] = str(self.mode) if self.number_samples is not None: payload["number_samples"] = int(self.number_samples) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Plot2DSamplingOptions": return cls( mode=_parse_enum_or_str(constant.Plot2DSamplingMode, payload.get("mode")), number_samples=_payload_int(payload.get("number_samples")), ) def copy(self) -> "Plot2DSamplingOptions": return Plot2DSamplingOptions( mode=self.mode, number_samples=self.number_samples, ) @dataclass class Plot2DAnnotationOptions: """Annotation options for Plot2D.""" show_date: bool | None = None show_legend: bool | None = None title: str | None = None title_color: constant.GeometricColor | int | None = None subtitle: str | None = None subtitle_color: constant.GeometricColor | int | None = None def to_payload(self) -> StateDict: payload: StateDict = {} if self.show_date is not None: payload["show_date"] = bool(self.show_date) if self.show_legend is not None: payload["show_legend"] = bool(self.show_legend) if self.title is not None: payload["title"] = str(self.title) if self.title_color is not None: payload["title_color"] = int(self.title_color) if self.subtitle is not None: payload["subtitle"] = str(self.subtitle) if self.subtitle_color is not None: payload["subtitle_color"] = int(self.subtitle_color) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Plot2DAnnotationOptions": return cls( show_date=_payload_bool(payload.get("show_date")), show_legend=_payload_bool(payload.get("show_legend")), title=_payload_str(payload.get("title")), title_color=_payload_geometric_color(payload.get("title_color")), subtitle=_payload_str(payload.get("subtitle")), subtitle_color=_payload_geometric_color(payload.get("subtitle_color")), ) def copy(self) -> "Plot2DAnnotationOptions": return Plot2DAnnotationOptions( show_date=self.show_date, show_legend=self.show_legend, title=self.title, title_color=self.title_color, subtitle=self.subtitle, subtitle_color=self.subtitle_color, ) @dataclass class Plot2DOptions: """Border/frame options for Plot2D.""" border_color: constant.GeometricColor | int | None = None border_layout: constant.Plot2DBorderLayout | str | None = None border_style: constant.Plot2DLineStyle | str | None = None def to_payload(self) -> StateDict: payload: StateDict = {} if self.border_color is not None: payload["border_color"] = int(self.border_color) if self.border_layout is not None: if isinstance(self.border_layout, Enum): payload["border_layout"] = _enum_payload(self.border_layout) else: payload["border_layout"] = str(self.border_layout) if self.border_style is not None: if isinstance(self.border_style, Enum): payload["border_style"] = _enum_payload(self.border_style) else: payload["border_style"] = str(self.border_style) return payload @classmethod def from_payload(cls, payload: StateDict) -> "Plot2DOptions": return cls( border_color=_payload_geometric_color(payload.get("border_color")), border_layout=_parse_enum_or_str( constant.Plot2DBorderLayout, payload.get("border_layout"), ), border_style=_parse_enum_or_str( constant.Plot2DLineStyle, payload.get("border_style"), ), ) def copy(self) -> "Plot2DOptions": return Plot2DOptions( border_color=self.border_color, border_layout=self.border_layout, border_style=self.border_style, )
[docs] @dataclass class RuledGridAxisOptions: """Ruled grid axis options.""" #: constant.Plane label (x/y/z/r/t). label: constant.Plane | str | None = None #: Toggle grid lines. grid_lines: bool | None = None #: Toggle tick marks. tick_marks: bool | None = None #: Numeric grid interval (0 disables). interval: float | None = None #: Toggle labels. labels: bool | None = None #: Numerical format for labels. numerical_format: constant.NumericalFormat | str | None = None #: Decimal places for labels. decimal_places: int | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) _prefix: str | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return if name not in { "grid_lines", "tick_marks", "interval", "labels", "numerical_format", "decimal_places", }: return callback = getattr(self, "_on_change", None) prefix = getattr(self, "_prefix", None) if callback is None or prefix is None or value is None: return key = f"{prefix}_{name}" if name in {"grid_lines", "tick_marks", "labels"}: callback(key, bool(value)) elif name == "interval": callback(key, _coerce_float(value)) elif name == "decimal_places": callback(key, _coerce_int(value)) else: if isinstance(value, Enum): callback(key, _enum_payload(value)) else: callback(key, str(value)) def to_payload(self, prefix: str) -> StateDict: """Convert ruled-grid axis options into a payload dictionary. Args: prefix: Payload key prefix such as ``horiz`` or ``vert``. """ payload: StateDict = {} if self.grid_lines is not None: payload[f"{prefix}_grid_lines"] = bool(self.grid_lines) if self.tick_marks is not None: payload[f"{prefix}_tick_marks"] = bool(self.tick_marks) if self.interval is not None: payload[f"{prefix}_interval"] = float(self.interval) if self.labels is not None: payload[f"{prefix}_labels"] = bool(self.labels) if self.numerical_format is not None: if isinstance(self.numerical_format, Enum): payload[f"{prefix}_numerical_format"] = _enum_payload( self.numerical_format ) else: payload[f"{prefix}_numerical_format"] = str(self.numerical_format) if self.decimal_places is not None: payload[f"{prefix}_decimal_places"] = int(self.decimal_places) return payload @classmethod def from_payload(cls, payload: StateDict, prefix: str) -> "RuledGridAxisOptions": """Create ruled-grid axis options from a payload dictionary. Args: payload: Serialized ruled-grid-axis payload from the FieldView host. prefix: Payload key prefix such as ``horiz`` or ``vert``. """ label_key = f"{prefix}_label" label_value = payload.get(label_key) label: constant.Plane | str | None = None if label_value is not None: try: label = constant.Plane(str(label_value)) except ValueError: label = str(label_value) fmt_key = f"{prefix}_numerical_format" fmt_value = payload.get(fmt_key) fmt: constant.NumericalFormat | str | None = None if fmt_value is not None: try: fmt = constant.NumericalFormat(str(fmt_value)) except ValueError: fmt = str(fmt_value) return cls( label=label, grid_lines=_payload_bool(payload.get(f"{prefix}_grid_lines")), tick_marks=_payload_bool(payload.get(f"{prefix}_tick_marks")), interval=_payload_float(payload.get(f"{prefix}_interval")), labels=_payload_bool(payload.get(f"{prefix}_labels")), numerical_format=fmt, decimal_places=_payload_int(payload.get(f"{prefix}_decimal_places")), )
[docs] def copy(self) -> "RuledGridAxisOptions": """Return a detached copy of the options.""" return RuledGridAxisOptions( label=self.label, grid_lines=self.grid_lines, tick_marks=self.tick_marks, interval=self.interval, labels=self.labels, numerical_format=self.numerical_format, decimal_places=self.decimal_places, )
[docs] @dataclass class RuledGridOptions: """Ruled grid options.""" show: bool = False #: Geometric color (1-10). color: constant.GeometricColor | int | None = None #: Text font identifier. font: constant.Font | str | None = None #: Font size. size: int | None = None #: Horizontal axis options. horizontal_axis: RuledGridAxisOptions | None = None #: Vertical axis options. vertical_axis: RuledGridAxisOptions | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) if name in {"horizontal_axis", "vertical_axis"} and isinstance( value, RuledGridAxisOptions ): prefix = "horiz" if name == "horizontal_axis" else "vert" object.__setattr__(value, "_on_change", callback) object.__setattr__(value, "_prefix", prefix) return if callback is None or value is None: return if name == "show": callback(name, bool(value)) elif name == "color": callback(name, _coerce_int(value)) elif name == "font": if isinstance(value, Enum): callback(name, _enum_payload(value)) else: callback(name, str(value)) elif name == "size": callback(name, _coerce_int(value)) def to_payload(self) -> StateDict: payload: StateDict = {} if self.color is not None: payload["color"] = int(self.color) if self.font is not None: if isinstance(self.font, Enum): payload["font"] = _enum_payload(self.font) else: payload["font"] = str(self.font) if self.size is not None: payload["size"] = int(self.size) if self.horizontal_axis is not None: payload.update(self.horizontal_axis.to_payload("horiz")) if self.vertical_axis is not None: payload.update(self.vertical_axis.to_payload("vert")) return payload @classmethod def from_payload( cls, payload: StateDict, *, show: bool = False ) -> "RuledGridOptions": """Create ruled-grid options from a payload dictionary. Args: payload: Serialized ruled-grid payload from the FieldView host. show: Initial ruled-grid visibility state to attach to the object. """ font_value = payload.get("font") font: constant.Font | str | None = None if font_value is not None: try: font = constant.Font(str(font_value)) except ValueError: font = str(font_value) return cls( show=bool(show), color=_payload_geometric_color(payload.get("color")), font=font, size=_payload_int(payload.get("size")), horizontal_axis=RuledGridAxisOptions.from_payload(payload, "horiz"), vertical_axis=RuledGridAxisOptions.from_payload(payload, "vert"), )
[docs] def copy(self) -> "RuledGridOptions": """Return a detached copy of the options.""" horiz = ( self.horizontal_axis.copy() if self.horizontal_axis is not None else None ) vert = self.vertical_axis.copy() if self.vertical_axis is not None else None return RuledGridOptions( show=self.show, color=self.color, font=self.font, size=self.size, horizontal_axis=horiz, vertical_axis=vert, )
@dataclass class ScalarAnnotation: """Scalar annotation settings.""" show_location: bool | None = None show_text: bool | None = None color: constant.GeometricColor | int | None = None text: str | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) _prefix: str | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return if name not in {"show_location", "show_text", "color", "text"}: return callback = getattr(self, "_on_change", None) prefix = getattr(self, "_prefix", None) if callback is None or prefix is None or value is None: return key = f"{prefix}_{name}" if name in {"show_location", "show_text"}: callback(key, bool(value)) elif name == "color": callback(key, _coerce_int(value)) else: callback(key, str(value)) def to_payload(self, prefix: str) -> StateDict: """Convert scalar-annotation settings into a payload dictionary. Args: prefix: Payload key prefix such as ``min`` or ``max``. """ payload: StateDict = {} if self.show_location is not None: payload[f"{prefix}_show_location"] = bool(self.show_location) if self.show_text is not None: payload[f"{prefix}_show_text"] = bool(self.show_text) if self.color is not None: payload[f"{prefix}_color"] = int(self.color) if self.text is not None: payload[f"{prefix}_text"] = str(self.text) return payload @classmethod def from_payload(cls, payload: StateDict, prefix: str) -> "ScalarAnnotation": """Create scalar-annotation settings from a payload dictionary. Args: payload: Serialized scalar-annotation payload from the FieldView host. prefix: Payload key prefix such as ``min`` or ``max``. """ return cls( show_location=_payload_bool(payload.get(f"{prefix}_show_location")), show_text=_payload_bool(payload.get(f"{prefix}_show_text")), color=_payload_geometric_color(payload.get(f"{prefix}_color")), text=_payload_str(payload.get(f"{prefix}_text")), ) def copy(self) -> "ScalarAnnotation": """Return a detached copy of the annotation.""" return ScalarAnnotation( show_location=self.show_location, show_text=self.show_text, color=self.color, text=self.text, )
[docs] @dataclass class ScalarMinMax: """Scalar min/max annotation options.""" show: bool = False font: constant.Font | str | None = None size: int | None = None min: ScalarAnnotation | None = None max: ScalarAnnotation | None = None _on_change: Callable[[str, object], None] | None = field( default=None, init=False, repr=False, compare=False, ) _get_data: Callable[[], StateDict] | None = field( default=None, init=False, repr=False, compare=False, ) def __setattr__(self, name: str, value: object) -> None: _ensure_known_attribute(self, name) object.__setattr__(self, name, value) if name.startswith("_"): return callback = getattr(self, "_on_change", None) if callback is None or value is None: return if name == "show": callback(name, bool(value)) elif name == "font": if isinstance(value, Enum): callback(name, _enum_payload(value)) else: callback(name, str(value)) elif name == "size": callback(name, _coerce_int(value)) elif name in {"min", "max"} and isinstance(value, ScalarAnnotation): object.__setattr__(value, "_on_change", callback) object.__setattr__(value, "_prefix", name) def to_payload(self) -> StateDict: payload: StateDict = {} if self.font is not None: if isinstance(self.font, Enum): payload["font"] = _enum_payload(self.font) else: payload["font"] = str(self.font) if self.size is not None: payload["size"] = int(self.size) if self.min is not None: payload.update(self.min.to_payload("min")) if self.max is not None: payload.update(self.max.to_payload("max")) return payload @classmethod def from_payload(cls, payload: StateDict, *, show: bool = False) -> "ScalarMinMax": """Create scalar min/max annotation options from a payload dictionary. Args: payload: Serialized scalar-min/max payload from the FieldView host. show: Initial scalar-min/max visibility state to attach to the object. """ font_value = payload.get("font") font: constant.Font | str | None = None if font_value is not None: try: font = constant.Font(str(font_value)) except ValueError: font = str(font_value) return cls( show=bool(show), font=font, size=_payload_int(payload.get("size")), min=ScalarAnnotation.from_payload(payload, "min"), max=ScalarAnnotation.from_payload(payload, "max"), )
[docs] def copy(self) -> "ScalarMinMax": """Return a detached copy of the options.""" min_annotation = self.min.copy() if self.min is not None else None max_annotation = self.max.copy() if self.max is not None else None return ScalarMinMax( show=self.show, font=self.font, size=self.size, min=min_annotation, max=max_annotation, )
def _require_data(self) -> StateDict: callback = self._get_data if callback is None: raise InvalidArgumentError( "ScalarMinMax min/max values and positions are only available for bound surface objects" ) payload = callback() if not isinstance(payload, dict): raise InvalidArgumentError("ScalarMinMax data payload is invalid") return payload
[docs] def get_minmax_values(self) -> tuple[float, float]: """Return the current scalar min/max values.""" payload = self._require_data() return _required_float(payload, "min"), _required_float(payload, "max")
[docs] def get_minmax_positions( self, ) -> tuple[tuple[float, float, float], tuple[float, float, float]]: """Return XYZ positions for scalar min/max values.""" payload = self._require_data() min_pos = ( _required_float(payload, "min_x"), _required_float(payload, "min_y"), _required_float(payload, "min_z"), ) max_pos = ( _required_float(payload, "max_x"), _required_float(payload, "max_y"), _required_float(payload, "max_z"), ) return min_pos, max_pos
[docs] def get_min_value(self) -> float: """Return the current scalar minimum value.""" min_value, _ = self.get_minmax_values() return min_value
[docs] def get_max_value(self) -> float: """Return the current scalar maximum value.""" _, max_value = self.get_minmax_values() return max_value
[docs] def get_min_position(self) -> tuple[float, float, float]: """Return XYZ position for the current scalar minimum.""" min_pos, _ = self.get_minmax_positions() return min_pos
[docs] def get_max_position(self) -> tuple[float, float, float]: """Return XYZ position for the current scalar maximum.""" _, max_pos = self.get_minmax_positions() return max_pos