"""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,
)