diff --git a/doc/api/index.rst b/doc/api/index.rst index 6d41862b6c5..7dd3d35afd3 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -32,6 +32,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.magnetic_rose Figure.scalebar Figure.solar Figure.text diff --git a/pygmt/figure.py b/pygmt/figure.py index 41cadbb0bb6..c516c3d7f75 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -423,6 +423,7 @@ def _repr_html_(self) -> str: inset, legend, logo, + magnetic_rose, meca, plot, plot3d, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index e11c3b04680..be06c09b335 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -36,6 +36,7 @@ from pygmt.src.inset import inset from pygmt.src.legend import legend from pygmt.src.logo import logo +from pygmt.src.magnetic_rose import magnetic_rose from pygmt.src.makecpt import makecpt from pygmt.src.meca import meca from pygmt.src.nearneighbor import nearneighbor diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index 2f30a702a2c..ee941de1e3c 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -11,7 +11,7 @@ @fmt_docstring -@use_alias(F="box", Tm="compass", f="coltypes") +@use_alias(F="box", f="coltypes") def basemap( # noqa: PLR0913 self, projection: str | None = None, @@ -20,6 +20,7 @@ def basemap( # noqa: PLR0913 frame: str | Sequence[str] | bool = False, region: Sequence[float | str] | str | None = None, map_scale: str | None = None, + compass: str | None = None, rose: str | None = None, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, @@ -43,6 +44,7 @@ def basemap( # noqa: PLR0913 - :meth:`pygmt.Figure.scalebar`: Add a scale bar on the plot. - :meth:``pygmt.Figure.directional_rose`: Add a directional rose on the plot. + - :meth:`pygmt.Figure.magnetic_rose`: Add a magnetic rose on the plot. Full GMT docs at :gmt-docs:`basemap.html`. @@ -76,6 +78,15 @@ def basemap( # noqa: PLR0913 which provides a more comprehensive and flexible API for adding scale bars to plots. This parameter still accepts raw GMT CLI strings for the ``-L`` option of the ``basemap`` module for backward compatibility. + compass + Draw a map magnetic rose on the map. + + .. deprecated:: v0.19.0 + + This parameter is deprecated. Use :meth:`pygmt.Figure.magnetic_rose` + instead, which provides a more comprehensive and flexible API for adding + magnetic roses to plots. This parameter still accepts raw GMT CLI strings + for the ``-Tm`` option of the ``basemap`` module for backward compatibility. rose Draw a map directional rose on the map. @@ -103,9 +114,6 @@ def basemap( # noqa: PLR0913 Here, *dx/dy* indicates the shift relative to the foreground frame [Default is ``"4p/-4p"``] and shade sets the fill style to use for shading [Default is ``"gray50"``]. - compass : str - Draw a map magnetic rose on the map at the location defined by the - reference and anchor points. $verbose $panel $coltypes @@ -119,6 +127,7 @@ def basemap( # noqa: PLR0913 JZ=Alias(zsize, name="zsize"), L=Alias(map_scale, name="map_scale"), # Deprecated. Td=Alias(rose, name="rose"), # Deprecated. + Tm=Alias(compass, name="compass"), # Deprecated. ).add_common( B=frame, J=projection, diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py new file mode 100644 index 00000000000..e91ef24c1af --- /dev/null +++ b/pygmt/src/magnetic_rose.py @@ -0,0 +1,137 @@ +""" +magnetic_rose - Add a map magnetic rose. +""" + +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import build_arg_list, fmt_docstring +from pygmt.params import Box, Position +from pygmt.src._common import _parse_position + +__doctest_skip__ = ["magnetic_rose"] + + +@fmt_docstring +def magnetic_rose( # noqa: PLR0913 + self, + position: Position | Sequence[float | str] | AnchorCode | None = None, + width: float | str | None = None, + labels: Sequence[str] | bool = False, + outer_pen: str | bool = False, + inner_pen: str | bool = False, + declination: float | None = None, + declination_label: str | None = None, + intervals: Sequence[float] | None = None, + box: Box | bool = False, + verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] + | bool = False, + panel: int | Sequence[int] | bool = False, + perspective: str | bool = False, + transparency: float | None = None, +): + """ + Add a magnetic rose to the map. + + Parameters + ---------- + position + Position of the magnetic rose on the plot. It can be specified in multiple ways: + + - A :class:`pygmt.params.Position` object to fully control the reference point, + anchor point, and offset. + - A sequence of two values representing the x- and y-coordinates in plot + coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``. + - A :doc:`2-character justification code ` for a + position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot. + + If not specified, defaults to the Bottom Left corner of the plot (position + ``(0, 0)`` with anchor ``"BL"``). + width + Width of the rose in plot coordinates, or append unit ``%`` for a size in + percentage of plot width [Default is 15%]. + labels + A sequence of four strings to label the cardinal points W, E, S, N. Use an empty + string to skip a specific label. If the north label is ``"*"``, then a north + star is plotted instead of the north label. If set to ``True``, use the default + labels ``["W", "E", "S", "N"]``. + outer_pen + Draw the outer circle of the magnetic rose, using the given pen attributes. + inner_pen + Draw the inner circle of the magnetic rose, using the given pen attributes. + declination + Magnetic declination in degrees. By default, only a geographic north is plotted. + With this parameter set, a magnetic north is also plotted. A magnetic compass + needle is drawn inside the rose to indicate the direction to magnetic north. + declination_label + Label for the magnetic compass needle. Default is to format a label based on + ``declination``. To bypass the label, set to ``"-"``. + intervals + Specify the annotation and tick intervals for the geographic and magnetic + directions. It can be a sequence of three or six values. If three values are + given, they are used for both geographic and magnetic directions. If six values + are given, the first three are used for geographic directions and the last three + for magnetic directions. [Default is ``(30, 5, 1)``]. + **Note**: If :gmt-term:`MAP_EMBELLISHMENT_MODE` is ``"auto"`` and the compass + size is smaller than 2.5 cm then the interval defaults are reset to + ``(90,30, 3, 45, 15, 3)``. + box + Draw a background box behind the magnetic rose. If set to ``True``, a simple + rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box + appearance, pass a :class:`pygmt.params.Box` object to control style, fill, pen, + and other box properties. + $perspective + $verbose + $transparency + + Examples + -------- + >>> import pygmt + >>> from pygmt.params import Position + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[-10, 10, -10, 10], projection="M15c", frame=True) + >>> fig.magnetic_rose( + ... position=Position((-5, -5), cstype="mapcoords"), + ... width="4c", + ... labels=["W", "E", "S", "*"], + ... intervals=(45, 15, 3, 60, 20, 4), + ... outer_pen="1p,red", + ... inner_pen="1p,blue", + ... declination=11.5, + ... declination_label="11.5°E", + ... ) + >>> fig.show() + """ + self._activate_figure() + + position = _parse_position(position, default=Position("BL", cstype="inside")) + + if declination_label is not None and declination is None: + msg = "Parameter 'declination' must be set when 'declination_label' is set." + raise GMTInvalidInput(msg) + + aliasdict = AliasSystem( + F=Alias(box, name="box"), + Tm=[ + Alias(position, name="position"), + Alias(width, name="width", prefix="+w"), + Alias(labels, name="labels", prefix="+l", sep=",", size=4), + Alias(outer_pen, name="outer_pen", prefix="+p"), + Alias(inner_pen, name="inner_pen", prefix="+i"), + Alias(declination, name="declination", prefix="+d"), + Alias(declination_label, name="declination_label", prefix="/"), + Alias(intervals, name="intervals", prefix="+t", sep="/", size=(3, 6)), + ], + ).add_common( + V=verbose, + c=panel, + p=perspective, + t=transparency, + ) + + with Session() as lib: + lib.call_module(module="basemap", args=build_arg_list(aliasdict)) diff --git a/pygmt/tests/baseline/test_magnetic_rose.png.dvc b/pygmt/tests/baseline/test_magnetic_rose.png.dvc new file mode 100644 index 00000000000..41df3d03622 --- /dev/null +++ b/pygmt/tests/baseline/test_magnetic_rose.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 5af8ffee19917b1fbc3371cd3aed53cd + size: 27013 + hash: md5 + path: test_magnetic_rose.png diff --git a/pygmt/tests/baseline/test_magnetic_rose_complete.png.dvc b/pygmt/tests/baseline/test_magnetic_rose_complete.png.dvc new file mode 100644 index 00000000000..6491e1250e4 --- /dev/null +++ b/pygmt/tests/baseline/test_magnetic_rose_complete.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 78c53322b2514fbcd56483b793d47ebe + size: 28831 + hash: md5 + path: test_magnetic_rose_complete.png diff --git a/pygmt/tests/test_magnetic_rose.py b/pygmt/tests/test_magnetic_rose.py new file mode 100644 index 00000000000..6102c1b0054 --- /dev/null +++ b/pygmt/tests/test_magnetic_rose.py @@ -0,0 +1,50 @@ +""" +Test Figure.magnetic_rose. +""" + +import pytest +from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput +from pygmt.params import Position + + +@pytest.mark.mpl_image_compare +def test_magnetic_rose(): + """ + Create a map with a compass. Modified from the test_basemap_compass test. + """ + fig = Figure() + fig.basemap(region=[127.5, 128.5, 26, 27], projection="M10c", frame=True) + fig.magnetic_rose() + return fig + + +@pytest.mark.mpl_image_compare +def test_magnetic_rose_complete(): + """ + Test all parameters of Figure.magnetic_rose. + """ + fig = Figure() + fig.basemap(region=[-10, 10, -10, 10], projection="M10c", frame=True) + fig.magnetic_rose( + position=Position("BL"), + width="2c", + labels=["W", "E", "S", "*"], + intervals=(45, 15, 3, 60, 20, 4), + outer_pen="1p,red", + inner_pen="1p,blue", + declination=11.5, + declination_label="11.5°E", + ) + return fig + + +def test_magnetic_rose_invalid_declination_label(): + """ + Test that an exception is raised when declination_label is set but declination is + not set. + """ + fig = Figure() + fig.basemap(region=[-10, 10, -10, 10], projection="M10c", frame=True) + with pytest.raises(GMTInvalidInput): + fig.magnetic_rose(declination_label="11.5°E")