diff --git a/doc/api/index.rst b/doc/api/index.rst index 75dedbf737d..c0e5bcf472c 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -217,6 +217,7 @@ Class-style Parameters Box Frame Pattern + Perspective Position Enums diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index 62d38b23356..d379644b0ee 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -274,24 +274,13 @@ Set pen attributes for lines or the outline of symbols.""", "perspective": r""" perspective - Select perspective view and set the azimuth and elevation of the viewpoint. - - Accepts a single value or a sequence of two or three values: *azimuth*, - (*azimuth*, *elevation*), or (*azimuth*, *elevation*, *zlevel*). - - - *azimuth*: Azimuth angle of the viewpoint in degrees [Default is 180, - i.e., looking from south to north]. - - *elevation*: Elevation angle of the viewpoint above the horizon [Default - is 90, i.e., looking straight down at nadir]. - - *zlevel*: Z-level at which 2-D elements (e.g., the plot frame) are drawn. - Only applied when used together with ``zsize`` or ``zscale``. [Default is - at the bottom of the z-axis]. - + Select perspective view and set the viewpoint. + Pass a :class:`pygmt.params.Perspective` object to control the viewpoint. Alternatively, set ``perspective=True`` to reuse the perspective setting - from the previous plotting method, or pass a string following the full - GMT syntax for finer control (e.g., adding ``+w`` or ``+v`` modifiers to - select an axis location other than the plot origin). See - :gmt-docs:`gmt.html#perspective-full` for details.""", + from the previous plotting method. It also accepts following shortcuts: a + single value *azimuth*, a sequence of two values (*azimuth*, *elevation*), + or a sequence of three values (*azimuth*, *elevation*, *level*). Refer to + :class:`pygmt.params.Perspective` for meaning of the parameters.""", "projection": r""" projection *projcode*\[*projparams*/]\ *width*\|\ *scale*. diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py index 43b83ab0131..3d115745aa3 100644 --- a/pygmt/params/__init__.py +++ b/pygmt/params/__init__.py @@ -5,4 +5,5 @@ from pygmt.params.box import Box from pygmt.params.frame import Axis, Frame from pygmt.params.pattern import Pattern +from pygmt.params.perspective import Perspective from pygmt.params.position import Position diff --git a/pygmt/params/perspective.py b/pygmt/params/perspective.py new file mode 100644 index 00000000000..874c803a88b --- /dev/null +++ b/pygmt/params/perspective.py @@ -0,0 +1,110 @@ +""" +The Perspective class for setting perspective view. +""" + +import dataclasses +from collections.abc import Sequence +from typing import Literal + +from pygmt.alias import Alias +from pygmt.exceptions import GMTValueError +from pygmt.params.base import BaseParam + +__doctest_skip__ = ["Perspective"] + + +@dataclasses.dataclass(repr=False) +class Perspective(BaseParam): + """ + Class for setting perspective view. + + Examples + -------- + >>> import pygmt + >>> from pygmt.params import Axis, Frame, Perspective + >>> fig = pygmt.Figure() + >>> fig.basemap( + ... region=[0, 10, 0, 10, 0, 20], + ... projection="X3c", + ... zsize="3c", + ... frame=Frame(axes="WSenZ", title="Perspective View", axis=Axis(grid=True)), + ... perspective=Perspective(azimuth=135, elevation=40, level=10), + ... ) + >>> fig.show() + """ + + #: Azimuth angle of the viewpoint in degrees. Default is 180.0, i.e., looking from + #: south to north. + azimuth: float | None = None + + #: Elevation angle of the viewpoint in degrees above the horizon. Default is 90.0, + #: i.e., looking straight down at nadir. + elevation: float | None = None + + #: The level at which all 2-D elements, (e.g., the plot frame), are drawn. Only + #: valid when used together with parameters ``zsize`` or ``zscale``. Default is at + #: the bottom of the selected axis. + level: float | None = None + + #: Set which constant-coordinate plane is used as the plotting plane. Use ``"x"``, + #: ``"y"``, or ``"z"`` for the x-plane, y-plane, or horizontal z-plane, + #: respectively [Default is ``"z"``]. + plane: Literal["x", "y", "z"] | None = None + + #: Reference point for the perspective view. By default, the view rotates about the + #: plotting origin. Use ``refpoint`` and ``cstype`` to rotate about a different + #: point instead. The format of ``refpoint`` depends on the value of ``cstype``: + #: + #: - ``cstype="mapcoords"``: (*longitude*, *latitude*) or + #: (*longitude*, *latitude*, *z*) + #: - ``cstype="plotcoords"``: (*x*, *y*) + refpoint: Sequence[float | str] | None = None + + #: Coordinate system type of ``refpoint``. Valid values are: + #: + #: - ``"mapcoords"``: Map/data coordinates + #: - ``"plotcoords"``: Plot coordinates + #: + #: Defaults to ``"mapcoords"``. + cstype: Literal["mapcoords", "plotcoords"] = "mapcoords" + + def _validate(self): + """ + Post-initialization processing to validate parameters. + """ + # azimuth is required, so it must be set to the default if not specified. + if self.azimuth is None: + self.azimuth = 180.0 # Default azimuth is 180.0 + + # Set default elevation if level is set but elevation is not. + if self.level is not None and self.elevation is None: + self.elevation = 90.0 # Default elevation is 90.0 + + if self.plane is not None and self.plane not in {"x", "y", "z"}: + raise GMTValueError( + self.plane, description="plane", choices={"x", "y", "z"} + ) + + if self.cstype not in {"mapcoords", "plotcoords"}: + raise GMTValueError( + self.cstype, description="cstype", choices={"mapcoords", "plotcoords"} + ) + + @property + def _aliases(self): + """ + Aliases for the parameters. + """ + return [ + Alias(self.plane, name="plane"), + Alias(self.azimuth, name="azimuth"), + Alias(self.elevation, name="elevation", prefix="/"), + Alias(self.level, name="level", prefix="/"), + Alias( + self.refpoint, + name="refpoint", + sep="/", + prefix={"mapcoords": "+w", "plotcoords": "+v"}[self.cstype], + size={"mapcoords": {2, 3}, "plotcoords": 2}[self.cstype], + ), + ] diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index ddd736033a1..b064391c095 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -9,7 +9,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, is_given -from pygmt.params import Axis, Box, Frame +from pygmt.params import Axis, Box, Frame, Perspective @fmt_docstring @@ -28,7 +28,7 @@ def basemap( box: Box | str | bool = False, panel: int | Sequence[int] | bool = False, coltypes: str | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index 4190cb5f707..8625bf8675b 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -15,7 +15,7 @@ is_nonstr_iter, use_alias, ) -from pygmt.params import Axis, Box, Frame +from pygmt.params import Axis, Box, Frame, Perspective __doctest_skip__ = ["coast"] @@ -90,7 +90,7 @@ def coast( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/colorbar.py b/pygmt/src/colorbar.py index 9d264e42787..bb56d81a1ab 100644 --- a/pygmt/src/colorbar.py +++ b/pygmt/src/colorbar.py @@ -11,7 +11,7 @@ from pygmt.exceptions import GMTValueError from pygmt.helpers import build_arg_list, fmt_docstring, is_given, use_alias from pygmt.helpers.utils import is_nonstr_iter -from pygmt.params import Axis, Box, Frame, Position +from pygmt.params import Axis, Box, Frame, Perspective, Position from pygmt.src._common import _parse_position __doctest_skip__ = ["colorbar"] @@ -278,7 +278,7 @@ def colorbar( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/contour.py b/pygmt/src/contour.py index 13a4450e3dd..14396b6897b 100644 --- a/pygmt/src/contour.py +++ b/pygmt/src/contour.py @@ -14,7 +14,7 @@ is_nonstr_iter, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective @fmt_docstring @@ -45,7 +45,7 @@ def contour( | bool = False, panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/src/directional_rose.py b/pygmt/src/directional_rose.py index 8a964c10a44..5c29f80a8e3 100644 --- a/pygmt/src/directional_rose.py +++ b/pygmt/src/directional_rose.py @@ -9,7 +9,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring -from pygmt.params import Box, Position +from pygmt.params import Box, Perspective, Position from pygmt.src._common import _parse_position __doctest_skip__ = ["directional_rose"] @@ -26,7 +26,7 @@ def directional_rose( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, ): """ diff --git a/pygmt/src/fill_between.py b/pygmt/src/fill_between.py index 6a4a23b6b2b..80283af23d0 100644 --- a/pygmt/src/fill_between.py +++ b/pygmt/src/fill_between.py @@ -10,7 +10,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTValueError from pygmt.helpers import build_arg_list, fmt_docstring -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective __doctest_skip__ = ["fill_between"] @@ -36,7 +36,7 @@ def fill_between( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, ): """ diff --git a/pygmt/src/grdcontour.py b/pygmt/src/grdcontour.py index 954f75b658c..583cc660acd 100644 --- a/pygmt/src/grdcontour.py +++ b/pygmt/src/grdcontour.py @@ -16,7 +16,7 @@ kwargs_to_strings, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective __doctest_skip__ = ["grdcontour"] @@ -42,7 +42,7 @@ def grdcontour( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/src/grdimage.py b/pygmt/src/grdimage.py index 4ae502ef157..ec470be5f43 100644 --- a/pygmt/src/grdimage.py +++ b/pygmt/src/grdimage.py @@ -10,7 +10,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective __doctest_skip__ = ["grdimage"] @@ -36,7 +36,7 @@ def grdimage( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, cores: int | bool = False, coltypes: str | None = None, diff --git a/pygmt/src/grdview.py b/pygmt/src/grdview.py index cfe715384da..124aaee56a8 100644 --- a/pygmt/src/grdview.py +++ b/pygmt/src/grdview.py @@ -17,7 +17,7 @@ is_given, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective from pygmt.src.grdinfo import grdinfo __doctest_skip__ = ["grdview"] @@ -141,7 +141,7 @@ def grdview( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/src/histogram.py b/pygmt/src/histogram.py index 67d9b6d1009..b60a66dc133 100644 --- a/pygmt/src/histogram.py +++ b/pygmt/src/histogram.py @@ -15,7 +15,7 @@ kwargs_to_strings, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective @fmt_docstring @@ -52,7 +52,7 @@ def histogram( | bool = False, panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/hlines.py b/pygmt/src/hlines.py index c5ae316c1e5..39b76dacc38 100644 --- a/pygmt/src/hlines.py +++ b/pygmt/src/hlines.py @@ -6,6 +6,7 @@ import numpy as np from pygmt.exceptions import GMTValueError +from pygmt.params import Perspective __doctest_skip__ = ["hlines"] @@ -18,7 +19,7 @@ def hlines( pen: str | None = None, label: str | None = None, no_clip: bool = False, - perspective: str | bool | None = None, + perspective: Perspective | float | Sequence[float] | bool | None = None, ): """ Plot one or multiple horizontal line(s). diff --git a/pygmt/src/image.py b/pygmt/src/image.py index 1c0ceb9b7f2..6ca7fb5478d 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -9,7 +9,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Box, Frame, Position +from pygmt.params import Axis, Box, Frame, Perspective, Position from pygmt.src._common import _parse_position @@ -32,7 +32,7 @@ def image( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/legend.py b/pygmt/src/legend.py index 488ca009a17..145ef096c7f 100644 --- a/pygmt/src/legend.py +++ b/pygmt/src/legend.py @@ -11,7 +11,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTTypeError from pygmt.helpers import build_arg_list, data_kind, fmt_docstring, is_nonstr_iter -from pygmt.params import Axis, Box, Frame, Position +from pygmt.params import Axis, Box, Frame, Perspective, Position from pygmt.src._common import _parse_position @@ -31,7 +31,7 @@ def legend( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 9733c185fbc..26f7d40e376 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -10,7 +10,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring -from pygmt.params import Box, Position +from pygmt.params import Box, Perspective, Position from pygmt.src._common import _parse_position __doctest_skip__ = ["logo"] @@ -29,7 +29,7 @@ def logo( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py index b670d6d013a..aed779aa6d2 100644 --- a/pygmt/src/magnetic_rose.py +++ b/pygmt/src/magnetic_rose.py @@ -10,7 +10,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTParameterError from pygmt.helpers import build_arg_list, fmt_docstring -from pygmt.params import Box, Position +from pygmt.params import Box, Perspective, Position from pygmt.src._common import _parse_position __doctest_skip__ = ["magnetic_rose"] @@ -31,7 +31,7 @@ def magnetic_rose( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, ): """ diff --git a/pygmt/src/meca.py b/pygmt/src/meca.py index a2c84cd641a..9b9e8880a70 100644 --- a/pygmt/src/meca.py +++ b/pygmt/src/meca.py @@ -17,7 +17,7 @@ fmt_docstring, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective from pygmt.src._common import _FocalMechanismConvention @@ -140,7 +140,7 @@ def meca( | bool = False, panel: int | Sequence[int] | bool = False, transparency: float | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, **kwargs, ): r""" diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index 37cf2b07989..dfee6fb4ca0 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -16,7 +16,7 @@ is_nonstr_iter, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective from pygmt.src._common import _data_geometry_is_point @@ -58,7 +58,7 @@ def plot( # ruff: ignore[too-many-branches] | bool = False, panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | Sequence[float] | bool | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index ca8e8c474f1..b0a306a6d52 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -16,7 +16,7 @@ is_nonstr_iter, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective from pygmt.src._common import _data_geometry_is_point @@ -60,7 +60,7 @@ def plot3d( # ruff: ignore[too-many-branches] | bool = False, panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | Sequence[float] | bool | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/src/pygmtlogo.py b/pygmt/src/pygmtlogo.py index ea4338999d5..caaedb63215 100644 --- a/pygmt/src/pygmtlogo.py +++ b/pygmt/src/pygmtlogo.py @@ -12,7 +12,7 @@ from pygmt._typing import AnchorCode, PathLike from pygmt.exceptions import GMTValueError from pygmt.helpers import GMTTempFile, fmt_docstring -from pygmt.params import Box, Position +from pygmt.params import Box, Perspective, Position __doctest_skip__ = ["pygmtlogo"] @@ -41,7 +41,7 @@ def _create_logo( # ruff: ignore[too-many-statements] }[wordmark] # Rotation around z-axis by 30 degrees counter-clockwise placed in the center. - perspective = "30+w0/0" + perspective = Perspective(azimuth=30, refpoint=(0, 0)) # Radii (make sure that r4-r5 == r2-r3) r0, r1, r2, r3, r4, r5 = size * np.array([128, 112, 75, 61, 53, 39]) / 128 diff --git a/pygmt/src/rose.py b/pygmt/src/rose.py index 1c59d87fc68..93059702e77 100644 --- a/pygmt/src/rose.py +++ b/pygmt/src/rose.py @@ -9,7 +9,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective @fmt_docstring @@ -46,7 +46,7 @@ def rose( | bool = False, panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index fe3b6961138..79ab25a0290 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -9,7 +9,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring -from pygmt.params import Box, Position +from pygmt.params import Box, Perspective, Position from pygmt.src._common import _parse_position __doctest_skip__ = ["scalebar"] @@ -31,7 +31,7 @@ def scalebar( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, ): """ diff --git a/pygmt/src/solar.py b/pygmt/src/solar.py index 7b51dd02a2b..23f2a516ad2 100644 --- a/pygmt/src/solar.py +++ b/pygmt/src/solar.py @@ -10,7 +10,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTValueError from pygmt.helpers import build_arg_list, fmt_docstring -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective __doctest_skip__ = ["solar"] @@ -28,7 +28,7 @@ def solar( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/ternary.py b/pygmt/src/ternary.py index e828fbda854..a6ae390fe89 100644 --- a/pygmt/src/ternary.py +++ b/pygmt/src/ternary.py @@ -10,7 +10,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTValueError from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective from pygmt.params.frame import _Axes @@ -125,7 +125,7 @@ def ternary( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 3b5e8c90274..9cfc6035951 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -19,7 +19,7 @@ non_ascii_to_octal, use_alias, ) -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective @fmt_docstring @@ -51,7 +51,7 @@ def text( # ruff: ignore[too-many-branches, too-many-statements] verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | Sequence[float] | bool | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/src/tilemap.py b/pygmt/src/tilemap.py index c01ab26ac54..660bae97bd3 100644 --- a/pygmt/src/tilemap.py +++ b/pygmt/src/tilemap.py @@ -10,7 +10,7 @@ from pygmt.datasets.tile_map import load_tile_map from pygmt.enums import GridType from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective try: from xyzservices import TileProvider @@ -37,7 +37,7 @@ def tilemap( verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/velo.py b/pygmt/src/velo.py index 71d09dee444..9646845e805 100644 --- a/pygmt/src/velo.py +++ b/pygmt/src/velo.py @@ -12,7 +12,7 @@ from pygmt.clib import Session from pygmt.exceptions import GMTParameterError, GMTTypeError from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Frame +from pygmt.params import Axis, Frame, Perspective @fmt_docstring @@ -43,7 +43,7 @@ def velo( | bool = False, panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, **kwargs, ): diff --git a/pygmt/src/vlines.py b/pygmt/src/vlines.py index 1e99de0b535..377064eb923 100644 --- a/pygmt/src/vlines.py +++ b/pygmt/src/vlines.py @@ -6,6 +6,7 @@ import numpy as np from pygmt.exceptions import GMTValueError +from pygmt.params import Perspective __doctest_skip__ = ["vlines"] @@ -18,7 +19,7 @@ def vlines( pen: str | None = None, label: str | None = None, no_clip: bool = False, - perspective: str | bool | None = None, + perspective: Perspective | float | Sequence[float] | bool | None = None, ): """ Plot one or multiple vertical line(s). diff --git a/pygmt/src/wiggle.py b/pygmt/src/wiggle.py index cb4eb24a399..8c55ba29665 100644 --- a/pygmt/src/wiggle.py +++ b/pygmt/src/wiggle.py @@ -9,7 +9,7 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Axis, Frame, Position +from pygmt.params import Axis, Frame, Perspective, Position from pygmt.src._common import _parse_position @@ -44,7 +44,7 @@ def wiggle( panel: int | Sequence[int] | bool = False, incols: int | str | Sequence[int | str] | None = None, label: str | None = None, - perspective: float | Sequence[float] | str | bool = False, + perspective: Perspective | float | Sequence[float] | bool = False, transparency: float | None = None, coltypes: str | None = None, **kwargs, diff --git a/pygmt/tests/test_params_perspective.py b/pygmt/tests/test_params_perspective.py new file mode 100644 index 00000000000..c68b944bd0c --- /dev/null +++ b/pygmt/tests/test_params_perspective.py @@ -0,0 +1,65 @@ +""" +Test the Perspective class. +""" + +import pytest +from pygmt.exceptions import GMTInvalidInput, GMTValueError +from pygmt.params import Perspective + + +def test_params_perspective(): + """ + Test the Perspective class with various parameters. + """ + # Test azimuth, elevation, and level separately + assert str(Perspective(azimuth=120)) == "120" + assert str(Perspective(elevation=30)) == "180.0/30" + assert str(Perspective(level=1000)) == "180.0/90.0/1000" + + # Test combinations of azimuth, elevation, and level + assert str(Perspective(azimuth=120, elevation=30)) == "120/30" + assert str(Perspective(azimuth=120, elevation=30, level=1000)) == "120/30/1000" + assert str(Perspective(elevation=30, level=1000)) == "180.0/30/1000" + + # Test plane parameter + assert str(Perspective(azimuth=120, elevation=30, plane="x")) == "x120/30" + assert str(Perspective(azimuth=120, elevation=30, plane="y")) == "y120/30" + assert str(Perspective(azimuth=120, elevation=30, plane="z")) == "z120/30" + assert str(Perspective(plane="y")) == "y180.0" + + +def test_params_perspective_refpoint_cstype(): + """ + Test the Perspective class with the refpoint/cstype parameters. + """ + # Default cstype is "mapcoords". + assert str(Perspective(azimuth=120, refpoint=(4, 4))) == "120+w4/4" + assert str(Perspective(azimuth=120, refpoint=(4, 4, 10))) == "120+w4/4/10" + + # Different cstype values. + view = Perspective(azimuth=120, refpoint=(4, 4), cstype="mapcoords") + assert str(view) == "120+w4/4" + view = Perspective(azimuth=120, refpoint=(4, 4, 10), cstype="mapcoords") + assert str(view) == "120+w4/4/10" + view = Perspective(azimuth=120, refpoint=(4, 4), cstype="plotcoords") + assert str(view) == "120+v4/4" + + +def test_params_perspective_refpoint_invalid(): + """ + Test that invalid refpoint/cstype combinations raise errors. + """ + # Invalid cstype. + with pytest.raises(GMTValueError): + str(Perspective(refpoint=(4, 4), cstype="bad")) + # plotcoords ("+v") only accepts 2 values, not 3. + with pytest.raises(GMTInvalidInput): + str(Perspective(refpoint=(4, 4, 4), cstype="plotcoords")) + + +def test_params_perspective_invalid_plane(): + """ + Test that an invalid plane raises an error. + """ + with pytest.raises(GMTValueError): + str(Perspective(plane="bad"))