Source code for fieldview.vis.plot2d

"""2D plot helpers.

This module exposes the Plot2D API used to create and edit FieldView 2D plots,
including volume line paths and path import/export in FieldView's native text
format.
"""

from __future__ import annotations

import os
from collections.abc import Mapping
from enum import Enum
from typing import cast

from .. import _core
from .. import constant
from .. import utils as fv_utils
from .._core_utils import _core_call
from ..data import Dataset, _resolve_dataset_or_current
from ..exceptions import InvalidArgumentError, InvalidDatasetError
from ..utils import TripletLike, _coerce_pathlike_str, _coerce_xyz_triplet

__all__: list[str] = [
    "Plot2D",
    "Plot2DPath",
    "Plot2DAxis",
    "Plot2DSampling",
    "Plot2DAnnotation",
    "Plot2DOptions",
    "create_plot2d",
]

_UNSET = object()


def _enum_value(value: object, enum_type: type[Enum], label: str) -> str:
    if isinstance(value, enum_type):
        return str(cast(object, value.value))
    text = str(value).strip().lower()
    allowed = {str(cast(object, item.value)) for item in enum_type}
    if text not in allowed:
        raise InvalidArgumentError(
            f"{label} must be one of: {', '.join(sorted(allowed))}"
        )
    return text


def _lookup_scalar_func_id(dataset: Dataset, name: str, label: str) -> int:
    if not isinstance(name, str):
        raise InvalidArgumentError(f"{label} must be a string")
    needle = name.strip()
    if not needle:
        raise InvalidArgumentError(f"{label} cannot be empty")
    for func_name, func_id in dataset._scalar_function_ids.items():
        if func_name.lower() == needle.lower():
            return int(func_id)
    raise InvalidArgumentError(f"{label} not found: {needle}")


def _mapping_dict(value: object) -> dict[str, object]:
    return dict(cast(Mapping[str, object], value)) if isinstance(value, Mapping) else {}


def _number_to_int(value: object, label: str) -> int:
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise InvalidArgumentError(f"{label} must be numeric")
    return int(value)


def _number_to_float(value: object, label: str) -> float:
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise InvalidArgumentError(f"{label} must be numeric")
    return float(value)


def _plot2d_sampling_payload(
    value: fv_utils.Plot2DSamplingOptions | Mapping[str, object],
) -> dict[str, object]:
    if isinstance(value, fv_utils.Plot2DSamplingOptions):
        return value.to_payload()
    if isinstance(value, Mapping):
        return dict(value)
    raise InvalidArgumentError("sampling must be Plot2DSamplingOptions or a mapping")


def _plot2d_annotation_payload(
    value: fv_utils.Plot2DAnnotationOptions | Mapping[str, object],
) -> dict[str, object]:
    if isinstance(value, fv_utils.Plot2DAnnotationOptions):
        return value.to_payload()
    if isinstance(value, Mapping):
        return dict(value)
    raise InvalidArgumentError(
        "annotation must be Plot2DAnnotationOptions or a mapping"
    )


def _plot2d_options_payload(
    value: fv_utils.Plot2DOptions | Mapping[str, object],
) -> dict[str, object]:
    if isinstance(value, fv_utils.Plot2DOptions):
        return value.to_payload()
    if isinstance(value, Mapping):
        return dict(value)
    raise InvalidArgumentError("options must be Plot2DOptions or a mapping")


def _plot2d_axis_payload(
    value: fv_utils.Plot2DAxisOptions | Mapping[str, object],
    *,
    label: str,
) -> dict[str, object]:
    if isinstance(value, fv_utils.Plot2DAxisOptions):
        return value.to_payload()
    if isinstance(value, Mapping):
        return dict(value)
    raise InvalidArgumentError(f"{label} must be Plot2DAxisOptions or a mapping")


class _Plot2DSection:
    __slots__ = ("_plot", "_key")
    _plot: "Plot2D"
    _key: str

    def __init__(self, plot: "Plot2D", key: str) -> None:
        self._plot = plot
        self._key = key

    def _state(self) -> dict[str, object]:
        value = self._plot._state().get(self._key, {})
        return _mapping_dict(value)

    def _modify(self, payload: dict[str, object]) -> None:
        self._plot.modify(**{self._key: payload})


class Plot2DGridLines(_Plot2DSection):
    __slots__ = ("_plot", "_key", "_line_key")
    _line_key: str

    def __init__(self, plot: "Plot2D", axis_key: str, line_key: str) -> None:
        """Create a grid-line helper bound to one plot axis.

        Args:
            plot: Owning 2D plot instance.
            axis_key: Axis payload key such as ``left_axis`` or
                ``horizontal_axis``.
            line_key: Grid-line payload key such as ``major_grid_lines`` or
                ``minor_grid_lines``.
        """
        super().__init__(plot, axis_key)
        self._line_key = line_key

    def _line_state(self) -> dict[str, object]:
        return _mapping_dict(self._state().get(self._line_key, {}))

    def _line_modify(self, payload: dict[str, object]) -> None:
        self._modify({self._line_key: payload})

    @property
    def enabled(self) -> bool:
        return bool(self._line_state().get("enabled", False))

    @enabled.setter
    def enabled(self, value: bool) -> None:
        self._line_modify({"enabled": bool(value)})

    @property
    def color(self) -> int:
        return _number_to_int(self._line_state().get("color", 0), "grid line color")

    @color.setter
    def color(self, value: int) -> None:
        self._line_modify({"color": int(value)})

    @property
    def style(self) -> constant.Plot2DLineStyle:
        return constant.Plot2DLineStyle(
            str(self._line_state().get("style", constant.Plot2DLineStyle.SOLID.value))
        )

    @style.setter
    def style(self, value: constant.Plot2DLineStyle | str) -> None:
        self._line_modify(
            {"style": _enum_value(value, constant.Plot2DLineStyle, "grid line style")}
        )


class Plot2DTickMarks(_Plot2DSection):
    __slots__ = ("_plot", "_key", "_tick_key")
    _tick_key: str

    def __init__(self, plot: "Plot2D", axis_key: str, tick_key: str) -> None:
        """Create a tick-mark helper bound to one plot axis.

        Args:
            plot: Owning 2D plot instance.
            axis_key: Axis payload key such as ``left_axis`` or
                ``horizontal_axis``.
            tick_key: Tick-mark payload key such as ``major_tick_marks`` or
                ``minor_tick_marks``.
        """
        super().__init__(plot, axis_key)
        self._tick_key = tick_key

    def _tick_state(self) -> dict[str, object]:
        return _mapping_dict(self._state().get(self._tick_key, {}))

    def _tick_modify(self, payload: dict[str, object]) -> None:
        self._modify({self._tick_key: payload})

    @property
    def enabled(self) -> bool:
        return bool(self._tick_state().get("enabled", False))

    @enabled.setter
    def enabled(self, value: bool) -> None:
        self._tick_modify({"enabled": bool(value)})

    @property
    def unit(self) -> float:
        return _number_to_float(self._tick_state().get("unit", 0.0), "tick unit")

    @unit.setter
    def unit(self, value: float) -> None:
        self._tick_modify({"unit": float(value)})


[docs] class Plot2DAxis(_Plot2DSection): """Axis settings for a :class:`Plot2D`. Instances are available from :attr:`Plot2D.horizontal_axis`, :attr:`Plot2D.left_axis`, and :attr:`Plot2D.right_axis`. Example usage: .. code-block:: python >>> plot = fv.vis.create_plot2d(left_axis_func="Pressure", show_plot=True) >>> plot.left_axis.label = "Pressure" >>> plot.left_axis.major_grid_lines.enabled = True """ __slots__ = ( "_plot", "_key", "major_grid_lines", "minor_grid_lines", "major_tick_marks", "minor_tick_marks", ) def __init__(self, plot: "Plot2D", key: str) -> None: """Create an axis helper for a 2D plot. Args: plot: Owning 2D plot instance. key: Axis payload key such as ``horizontal_axis``, ``left_axis``, or ``right_axis``. """ super().__init__(plot, key) self.major_grid_lines = Plot2DGridLines(plot, key, "major_grid_lines") self.minor_grid_lines = Plot2DGridLines(plot, key, "minor_grid_lines") self.major_tick_marks = Plot2DTickMarks(plot, key, "major_tick_marks") self.minor_tick_marks = Plot2DTickMarks(plot, key, "minor_tick_marks") @property def label(self) -> str: """`str`: Axis label text.""" return str(self._state().get("label", "")) @label.setter def label(self, value: str) -> None: self._modify({"label": str(value)}) @property def color(self) -> int: """`int`: FieldView color index for the axis.""" return _number_to_int(self._state().get("color", 0), "axis color") @color.setter def color(self, value: int) -> None: self._modify({"color": int(value)}) @property def min(self) -> float | None: """`Optional[float]`: Explicit minimum axis value, or `None` for host defaults.""" value = self._state().get("min") return None if value is None else _number_to_float(value, "axis min") @min.setter def min(self, value: float | None) -> None: self._modify({"min": None if value is None else float(value)}) @property def max(self) -> float | None: """`Optional[float]`: Explicit maximum axis value, or `None` for host defaults.""" value = self._state().get("max") return None if value is None else _number_to_float(value, "axis max") @max.setter def max(self, value: float | None) -> None: self._modify({"max": None if value is None else float(value)}) @property def normalize(self) -> float | None: """`Optional[float]`: Axis normalization factor, or `None` for host defaults.""" value = self._state().get("normalize") return None if value is None else _number_to_float(value, "axis normalize") @normalize.setter def normalize(self, value: float | None) -> None: self._modify({"normalize": None if value is None else float(value)})
[docs] class Plot2DSampling(_Plot2DSection): """Sampling controls for a :class:`Plot2D`. Example usage: .. code-block:: python >>> plot = fv.vis.create_plot2d(left_axis_func="Pressure") >>> plot.sampling.mode = fv.constant.Plot2DSamplingMode.UNIFORM >>> plot.sampling.number_samples = 40 """ __slots__ = () @property def mode(self) -> constant.Plot2DSamplingMode: """`Plot2DSamplingMode`: Sampling mode used for plot evaluation.""" return constant.Plot2DSamplingMode( str( self._state().get( "mode", constant.Plot2DSamplingMode.CELL_CROSSINGS.value ) ) ) @mode.setter def mode(self, value: constant.Plot2DSamplingMode | str) -> None: self._modify( {"mode": _enum_value(value, constant.Plot2DSamplingMode, "sampling mode")} ) @property def number_samples(self) -> int: """`int`: Number of samples when uniform sampling is enabled.""" return _number_to_int(self._state().get("number_samples", 20), "number_samples") @number_samples.setter def number_samples(self, value: int) -> None: ivalue = int(value) if ivalue <= 0: raise InvalidArgumentError("number_samples must be positive") self._modify({"number_samples": ivalue})
[docs] class Plot2DAnnotation(_Plot2DSection): """Annotation settings for a :class:`Plot2D`. Supported attributes are `show_date`, `show_legend`, `title`, `title_color`, `subtitle`, and `subtitle_color`. Example usage: .. code-block:: python >>> plot = fv.vis.create_plot2d(left_axis_func="Pressure") >>> plot.annotation.title = "Pressure Along Path" >>> plot.annotation.show_legend = True """ __slots__ = () def __getattr__(self, name: str) -> object: if name in { "show_date", "show_legend", "title", "title_color", "subtitle", "subtitle_color", }: return self._state().get(name) raise AttributeError(name) def __setattr__(self, name: str, value: object) -> None: if name in {"_plot", "_key"}: object.__setattr__(self, name, value) elif name in { "show_date", "show_legend", "title", "title_color", "subtitle", "subtitle_color", }: self._modify({name: value}) else: raise AttributeError(name)
[docs] class Plot2DOptions(_Plot2DSection): """Border and framing options for a :class:`Plot2D`. Example usage: .. code-block:: python >>> plot = fv.vis.create_plot2d(left_axis_func="Pressure") >>> plot.options.border_layout = fv.constant.Plot2DBorderLayout.ACTIVE_AXES >>> plot.options.border_style = fv.constant.Plot2DLineStyle.DASHED """ __slots__ = () def __getattr__(self, name: str) -> object: if name == "border_layout": return constant.Plot2DBorderLayout( str( self._state().get(name, constant.Plot2DBorderLayout.ALL_SIDES.value) ) ) if name == "border_style": return constant.Plot2DLineStyle( str(self._state().get(name, constant.Plot2DLineStyle.SOLID.value)) ) if name == "border_color": return _number_to_int(self._state().get(name, 0), "border_color") raise AttributeError(name) def __setattr__(self, name: str, value: object) -> None: if name in {"_plot", "_key"}: object.__setattr__(self, name, value) elif name == "border_layout": self._modify( {name: _enum_value(value, constant.Plot2DBorderLayout, "border_layout")} ) elif name == "border_style": self._modify( {name: _enum_value(value, constant.Plot2DLineStyle, "border_style")} ) elif name == "border_color": self._modify({name: _number_to_int(value, "border_color")}) else: raise AttributeError(name)
[docs] class Plot2DPath: """Path handle owned by a :class:`Plot2D`. A path may be either a native volume line created from two XYZ points or an imported path loaded from a FieldView path text file. Example usage: .. code-block:: python >>> plot = fv.vis.create_plot2d(left_axis_func="Pressure") >>> path = plot.create_line_path_volume((0.0, 0.0, 0.0), (1.0, 0.0, 0.0)) >>> path.name = "centerline" >>> path.export_txt("centerline.txt") """ __slots__ = ("_plot", "_path_id", "_deleted") def __init__(self, plot: "Plot2D", path_id: int) -> None: """Create a path handle owned by a 2D plot. Args: plot: Owning 2D plot instance. path_id: Host-assigned path identifier within that plot. """ self._plot = plot self._path_id = int(path_id) self._deleted = False def _ensure_valid(self) -> None: if self._deleted or self._plot._deleted: raise InvalidArgumentError("2D plot path has been deleted") def _state(self) -> dict[str, object]: self._ensure_valid() items = self._plot._state().get("paths", []) if not isinstance(items, list): raise InvalidArgumentError("2D plot path list is invalid") for item in cast(list[object], items): if not isinstance(item, Mapping): continue item_map = cast(Mapping[str, object], item) if _number_to_int(item_map.get("path_id", -1), "path_id") == self._path_id: return dict(item_map) raise InvalidArgumentError("2D plot path is no longer valid") @property def plot(self) -> "Plot2D": """`Plot2D`: Owning plot object.""" return self._plot @property def path_id(self) -> int: """`int`: Host-assigned path identifier within the owning plot.""" return self._path_id @property def kind(self) -> constant.Plot2DPathKind: """`Plot2DPathKind`: Path type reported by the host.""" return constant.Plot2DPathKind( str(self._state().get("kind", constant.Plot2DPathKind.VOLUME_LINE.value)) ) @property def name(self) -> str | None: """`Optional[str]`: User-visible path name, or `None` when unset.""" value = self._state().get("name") return None if value is None or str(value) == "" else str(value) @name.setter def name(self, value: str | None) -> None: self._ensure_valid() _core_call( _core.plot2d_path_modify, self._plot.dataset_id, self._plot.plot_id, self._path_id, {"name": value}, ) @property def start(self) -> tuple[float, float, float] | None: """`Optional[tuple[float, float, float]]`: Start point in dataset coordinates. For Cartesian datasets this is ``(x, y, z)``. For cylindrical datasets this is the displayed cylindrical triplet, such as ``(r, theta, z)`` or ``(r, theta, x)``. """ value = self._state().get("start") return None if value is None else _coerce_xyz_triplet(value, "start") @property def end(self) -> tuple[float, float, float] | None: """`Optional[tuple[float, float, float]]`: End point in dataset coordinates. For Cartesian datasets this is ``(x, y, z)``. For cylindrical datasets this is the displayed cylindrical triplet, such as ``(r, theta, z)`` or ``(r, theta, x)``. """ value = self._state().get("end") return None if value is None else _coerce_xyz_triplet(value, "end")
[docs] def export_txt(self, filename: str | os.PathLike[str]) -> None: """Export this path to a FieldView path text file. Parameters ---------- filename: Destination text file path. """ self._ensure_valid() filename = _coerce_pathlike_str(filename, "filename") if not filename.strip(): raise InvalidArgumentError("filename must be a non-empty string") _core_call( _core.plot2d_path_export_txt, self._plot.dataset_id, self._plot.plot_id, self._path_id, filename, )
def _invalidate(self) -> None: self._deleted = True
[docs] class Plot2D: """2D plot wrapper. Use :func:`create_plot2d` to create instances. A plot owns zero or more :class:`Plot2DPath` objects and exposes nested axis, sampling, annotation, and border settings through helper objects. Example usage: .. code-block:: python >>> plot = fv.vis.create_plot2d( ... left_axis_func="Pressure", ... right_axis_func="Density", ... show_plot=True, ... ) >>> path = plot.create_line_path_volume((0.0, 0.0, 0.0), (1.0, 0.0, 0.0)) >>> plot.left_axis.label = "Pressure" >>> plot.right_axis_func = None """ __slots__ = ( "_dataset_id", "_plot_id", "_dataset", "_deleted", "_known_paths", "horizontal_axis", "left_axis", "right_axis", "sampling", "annotation", "options", ) def __init__( self, dataset_id: int, plot_id: int, dataset: Dataset | None = None ) -> None: """Create a 2D plot wrapper around an existing host plot. Args: dataset_id: Host dataset identifier backing the plot. plot_id: Host-assigned 2D plot identifier. dataset: Optional live :class:`Dataset` wrapper associated with the plot. """ self._dataset_id = int(dataset_id) self._plot_id = int(plot_id) self._dataset = dataset self._deleted = False self._known_paths: list[Plot2DPath] = [] self.horizontal_axis = Plot2DAxis(self, "horizontal_axis") self.left_axis = Plot2DAxis(self, "left_axis") self.right_axis = Plot2DAxis(self, "right_axis") self.sampling = Plot2DSampling(self, "sampling") self.annotation = Plot2DAnnotation(self, "annotation") self.options = Plot2DOptions(self, "options") @property def dataset_id(self) -> int: """`int`: Dataset identifier backing this plot.""" return self._dataset_id @property def plot_id(self) -> int: """`int`: Host-assigned 2D plot identifier.""" return self._plot_id def _ensure_valid(self) -> None: if self._deleted: raise InvalidArgumentError("2D plot has been deleted") def _require_dataset(self) -> Dataset: dataset = self._dataset if dataset is None: raise InvalidDatasetError("2D plot is missing a dataset reference") dataset._ensure_valid() return dataset def _state(self) -> dict[str, object]: self._ensure_valid() return _core_call(_core.plot2d_get_state, self._dataset_id, self._plot_id) def _find_known_path(self, path_id: int) -> Plot2DPath | None: self._known_paths = [path for path in self._known_paths if not path._deleted] for path in self._known_paths: if path._path_id == path_id: return path return None def _wrap_path(self, path_id: int) -> Plot2DPath: existing = self._find_known_path(path_id) if existing is not None: return existing path = Plot2DPath(self, path_id) self._known_paths.append(path) return path @property def paths(self) -> tuple[Plot2DPath, ...]: """`tuple[Plot2DPath, ...]`: Read-only view of the plot's current paths.""" items = self._state().get("paths", []) if not isinstance(items, list): return () path_ids: list[Plot2DPath] = [] for item in cast(list[object], items): if not isinstance(item, Mapping): continue item_map = cast(Mapping[str, object], item) path_ids.append( self._wrap_path(_number_to_int(item_map["path_id"], "path_id")) ) return tuple(path_ids) @property def left_axis_func(self) -> str: """`str`: Scalar function used on the left axis.""" return str(self._state().get("left_axis_func", "")) @left_axis_func.setter def left_axis_func(self, value: str) -> None: self.modify(left_axis_func=value) @property def right_axis_func(self) -> str | None: """`Optional[str]`: Scalar function used on the right axis, or `None` when disabled.""" value = self._state().get("right_axis_func") return None if value is None or str(value) == "" else str(value) @right_axis_func.setter def right_axis_func(self, value: str | None) -> None: self.modify(right_axis_func=value) @property def show_right_axis(self) -> bool: """`bool`: Whether the right axis is visible.""" return bool(self._state().get("show_right_axis", False)) @show_right_axis.setter def show_right_axis(self, value: bool) -> None: self.modify(show_right_axis=bool(value)) @property def show_path(self) -> bool: """`bool`: Whether path geometry is shown in the host plot view.""" return bool(self._state().get("show_path", False)) @show_path.setter def show_path(self, value: bool) -> None: self.modify(show_path=bool(value)) @property def show_plot(self) -> bool: """`bool`: Whether the plot is displayed in the host.""" return bool(self._state().get("show_plot", False)) @show_plot.setter def show_plot(self, value: bool) -> None: self.modify(show_plot=bool(value)) @property def background(self) -> bool: """`bool`: Whether the plot uses background drawing mode.""" return bool(self._state().get("background", False)) @background.setter def background(self, value: bool) -> None: self.modify(background=bool(value)) @property def horizontal_axis_mode(self) -> constant.Plot2DHorizontalAxis: """`Plot2DHorizontalAxis`: Quantity shown on the horizontal axis.""" return constant.Plot2DHorizontalAxis( str( self._state().get( "horizontal_axis_mode", constant.Plot2DHorizontalAxis.DISTANCE.value ) ) ) @horizontal_axis_mode.setter def horizontal_axis_mode(self, value: constant.Plot2DHorizontalAxis | str) -> None: self.modify(horizontal_axis_mode=value) @property def path_style(self) -> constant.Plot2DPathStyle: """`Plot2DPathStyle`: Drawing style applied to plotted path data.""" return constant.Plot2DPathStyle( str(self._state().get("path_style", constant.Plot2DPathStyle.LINES.value)) ) @path_style.setter def path_style(self, value: constant.Plot2DPathStyle | str) -> None: self.modify(path_style=value)
[docs] def modify( self, *, left_axis_func: str | None | object = _UNSET, right_axis_func: str | None | object = _UNSET, show_right_axis: bool | None | object = _UNSET, show_path: bool | None | object = _UNSET, show_plot: bool | None | object = _UNSET, background: bool | None | object = _UNSET, horizontal_axis_mode: constant.Plot2DHorizontalAxis | str | None | object = _UNSET, path_style: constant.Plot2DPathStyle | str | None | object = _UNSET, sampling: fv_utils.Plot2DSamplingOptions | Mapping[str, object] | None | object = _UNSET, annotation: fv_utils.Plot2DAnnotationOptions | Mapping[str, object] | None | object = _UNSET, options: fv_utils.Plot2DOptions | Mapping[str, object] | None | object = _UNSET, horizontal_axis: fv_utils.Plot2DAxisOptions | Mapping[str, object] | None | object = _UNSET, left_axis: fv_utils.Plot2DAxisOptions | Mapping[str, object] | None | object = _UNSET, right_axis: fv_utils.Plot2DAxisOptions | Mapping[str, object] | None | object = _UNSET, ) -> None: """Modify multiple plot settings in one host round trip. Supported keyword arguments mirror :func:`create_plot2d`, plus `horizontal_axis`, `left_axis`, and `right_axis` nested payloads. Args: left_axis_func: Left-axis scalar function name. right_axis_func: Right-axis scalar function name, or `None` to clear. show_right_axis: Show/hide the right axis. show_path: Show/hide path geometry in the host plot. show_plot: Show/hide the plot in the host. background: Toggle background drawing mode. horizontal_axis_mode: Horizontal axis quantity selector. path_style: Path draw style. sampling: Nested sampling payload as a mapping or :class:`fieldview.utils.Plot2DSamplingOptions`. annotation: Nested annotation payload as a mapping or :class:`fieldview.utils.Plot2DAnnotationOptions`. options: Nested border/layout payload as a mapping or :class:`fieldview.utils.Plot2DOptions`. horizontal_axis: Nested axis payload as a mapping or :class:`fieldview.utils.Plot2DAxisOptions`. left_axis: Nested axis payload as a mapping or :class:`fieldview.utils.Plot2DAxisOptions`. right_axis: Nested axis payload as a mapping or :class:`fieldview.utils.Plot2DAxisOptions`. """ self._ensure_valid() payload: dict[str, object] = {} dataset = self._dataset if left_axis_func is not _UNSET and left_axis_func is not None: if dataset is None: raise InvalidDatasetError( "left_axis_func changes require a dataset reference" ) if not isinstance(left_axis_func, str): raise InvalidArgumentError("left_axis_func must be a string") payload["left_axis_func_id"] = _lookup_scalar_func_id( dataset, left_axis_func, "left_axis_func" ) if right_axis_func is not _UNSET: if right_axis_func is None: payload["right_axis_func_id"] = None payload["show_right_axis"] = False else: if dataset is None: raise InvalidDatasetError( "right_axis_func changes require a dataset reference" ) if not isinstance(right_axis_func, str): raise InvalidArgumentError("right_axis_func must be a string") payload["right_axis_func_id"] = _lookup_scalar_func_id( dataset, right_axis_func, "right_axis_func" ) for key, value in { "show_right_axis": show_right_axis, "show_path": show_path, "show_plot": show_plot, "background": background, }.items(): if value is not _UNSET and value is not None: if key == "show_right_axis" and bool(value): if ( "right_axis_func_id" in payload and payload["right_axis_func_id"] is None ): raise InvalidArgumentError( "show_right_axis=True requires right_axis_func" ) if ( "right_axis_func_id" not in payload and self.right_axis_func is None ): raise InvalidArgumentError( "show_right_axis=True requires right_axis_func" ) payload[key] = bool(value) if horizontal_axis_mode is not _UNSET and horizontal_axis_mode is not None: payload["horizontal_axis_mode"] = _enum_value( horizontal_axis_mode, constant.Plot2DHorizontalAxis, "horizontal_axis_mode", ) if path_style is not _UNSET and path_style is not None: payload["path_style"] = _enum_value( path_style, constant.Plot2DPathStyle, "path_style" ) for key, nested in { "sampling": sampling, "annotation": annotation, "options": options, "horizontal_axis": horizontal_axis, "left_axis": left_axis, "right_axis": right_axis, }.items(): if nested is not _UNSET and nested is not None: if key == "sampling": payload[key] = _plot2d_sampling_payload( cast( fv_utils.Plot2DSamplingOptions | Mapping[str, object], nested, ) ) elif key == "annotation": payload[key] = _plot2d_annotation_payload( cast( fv_utils.Plot2DAnnotationOptions | Mapping[str, object], nested, ) ) elif key == "options": payload[key] = _plot2d_options_payload( cast(fv_utils.Plot2DOptions | Mapping[str, object], nested) ) else: payload[key] = _plot2d_axis_payload( cast(fv_utils.Plot2DAxisOptions | Mapping[str, object], nested), label=key, ) if payload: _core_call(_core.plot2d_modify, self._dataset_id, self._plot_id, payload)
[docs] def create_line_path_volume( self, start: TripletLike, end: TripletLike, **_options: object, ) -> Plot2DPath: """Create a volume line path from two dataset-coordinate points. Args: start: Three-number start point in dataset coordinates. For Cartesian datasets this is ``(x, y, z)``. For cylindrical datasets this is the displayed cylindrical triplet, such as ``(r, theta, z)`` or ``(r, theta, x)``. end: Three-number end point in dataset coordinates. For Cartesian datasets this is ``(x, y, z)``. For cylindrical datasets this is the displayed cylindrical triplet, such as ``(r, theta, z)`` or ``(r, theta, x)``. **_options: Reserved for future compatibility. Currently ignored. Notes ----- Some non-fatal host diagnostics are returned as Python warnings rather than exceptions. In interactive FieldView sessions those warnings may appear in the Python console. """ self._ensure_valid() start_triplet = _coerce_xyz_triplet(start, "start") end_triplet = _coerce_xyz_triplet(end, "end") state = _core_call( _core.plot2d_path_create_volume_line, self._dataset_id, self._plot_id, start_triplet, end_triplet, ) state_map = state path_map = cast(dict[str, object], state_map["path"]) return self._wrap_path(_number_to_int(path_map["path_id"], "path_id"))
[docs] def import_path_txt(self, filename: str | os.PathLike[str]) -> Plot2DPath: """Import one path from a FieldView path text file. Args: filename: FieldView path text file to import. """ self._ensure_valid() filename = _coerce_pathlike_str(filename, "filename") if not filename.strip(): raise InvalidArgumentError("filename must be a non-empty string") state = _core_call( _core.plot2d_path_import_txt, self._dataset_id, self._plot_id, filename ) state_map = state path_map = cast(dict[str, object], state_map["path"]) return self._wrap_path(_number_to_int(path_map["path_id"], "path_id"))
[docs] def delete_path(self, path: Plot2DPath) -> None: """Delete one path owned by this plot. Args: path: Path handle owned by this :class:`Plot2D`. """ self._ensure_valid() if not isinstance(path, Plot2DPath) or path.plot is not self: raise InvalidArgumentError("path must be owned by this Plot2D") path._ensure_valid() _core_call( _core.plot2d_path_delete, self._dataset_id, self._plot_id, path.path_id ) path._invalidate()
[docs] def delete_all_paths(self) -> None: """Delete all paths owned by this plot.""" self._ensure_valid() _core_call(_core.plot2d_paths_delete_all, self._dataset_id, self._plot_id) for path in self._known_paths: path._invalidate() self._known_paths.clear()
[docs] def delete(self) -> None: """Delete this plot and invalidate all known path handles.""" self._ensure_valid() _core_call(_core.plot2d_delete, self._dataset_id, self._plot_id) self._deleted = True for path in self._known_paths: path._invalidate() self._known_paths.clear()
[docs] def create_plot2d( dataset: Dataset | None = None, *, left_axis_func: str | None = None, right_axis_func: str | None = None, show_right_axis: bool | None = None, show_path: bool | None = None, show_plot: bool | None = None, background: bool | None = None, horizontal_axis_mode: constant.Plot2DHorizontalAxis | str | None = None, path_style: constant.Plot2DPathStyle | str | None = None, sampling: fv_utils.Plot2DSamplingOptions | Mapping[str, object] | None = None, annotation: fv_utils.Plot2DAnnotationOptions | Mapping[str, object] | None = None, options: fv_utils.Plot2DOptions | Mapping[str, object] | None = None, ) -> Plot2D: """Create a 2D plot for the current or supplied dataset. Parameters ---------- dataset: Dataset to attach to the new plot. When omitted, the current dataset is used. left_axis_func: Scalar function name for the left axis. When omitted, FieldView keeps its default left-axis function. right_axis_func: Scalar function name for the right axis. Use `None` to leave the right axis disabled. show_right_axis, show_path, show_plot, background: Initial display flags for the new plot. horizontal_axis_mode: Horizontal axis quantity such as `distance`, `x`, `y`, or `z`. path_style: Plot style for rendered path data. sampling, annotation, options: Nested dictionaries or typed payload objects from :mod:`fieldview.utils` matching the corresponding helper objects on :class:`Plot2D`. Example usage: .. code-block:: python >>> import os >>> import fieldview as fv >>> data_dir = os.path.join(fv.home, "examples", "f18") >>> ds = fv.data.load_plot3d( ... os.path.join(data_dir, "f18i9b_g_bin"), ... os.path.join(data_dir, "f18i9b_q_bin"), ... ) >>> left_func = ds.scalar_functions[0] >>> plot = fv.vis.create_plot2d( ... ds, ... left_axis_func=left_func, ... show_plot=True, ... show_path=True, ... ) """ dataset = _resolve_dataset_or_current(dataset) if show_right_axis and right_axis_func is None: raise InvalidArgumentError("show_right_axis=True requires right_axis_func") left_id = ( None if left_axis_func is None else _lookup_scalar_func_id(dataset, left_axis_func, "left_axis_func") ) right_id = ( None if right_axis_func is None else _lookup_scalar_func_id(dataset, right_axis_func, "right_axis_func") ) payload: dict[str, object] = {} for key, value in { "show_right_axis": show_right_axis, "show_path": show_path, "show_plot": show_plot, "background": background, }.items(): if value is not None: payload[key] = bool(value) if horizontal_axis_mode is not None: payload["horizontal_axis_mode"] = _enum_value( horizontal_axis_mode, constant.Plot2DHorizontalAxis, "horizontal_axis_mode" ) if path_style is not None: payload["path_style"] = _enum_value( path_style, constant.Plot2DPathStyle, "path_style" ) if sampling is not None: payload["sampling"] = _plot2d_sampling_payload(sampling) if annotation is not None: payload["annotation"] = _plot2d_annotation_payload(annotation) if options is not None: payload["options"] = _plot2d_options_payload(options) state = _core_call( _core.plot2d_create, dataset.dataset_id, left_id, right_id, payload ) state_map = state return Plot2D( _number_to_int(state_map["dataset_id"], "dataset_id"), _number_to_int(state_map["plot_id"], "plot_id"), dataset, )