From 98d0a0df3646d54e91f575911065ef9312ba701b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 11 Aug 2025 02:01:43 +0800 Subject: [PATCH 1/7] Add Figure.magnetic_rose to plot a magnetic rose on map --- doc/api/index.rst | 1 + pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/magnetic_rose.py | 147 ++++++++++++++++++ .../tests/baseline/test_magnetic_rose.png.dvc | 5 + .../test_magnetic_rose_complete.png.dvc | 5 + pygmt/tests/test_magnetic_rose.py | 50 ++++++ 7 files changed, 210 insertions(+) create mode 100644 pygmt/src/magnetic_rose.py create mode 100644 pygmt/tests/baseline/test_magnetic_rose.png.dvc create mode 100644 pygmt/tests/baseline/test_magnetic_rose_complete.png.dvc create mode 100644 pygmt/tests/test_magnetic_rose.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 264f5a9175a..119f204f13d 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -31,6 +31,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.magnetic_rose Figure.solar Figure.text Figure.timestamp diff --git a/pygmt/figure.py b/pygmt/figure.py index 56ad2c3d5cf..348614b1e27 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -422,6 +422,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 8905124f917..61eebccb6a4 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -35,6 +35,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/magnetic_rose.py b/pygmt/src/magnetic_rose.py new file mode 100644 index 00000000000..46a5461f500 --- /dev/null +++ b/pygmt/src/magnetic_rose.py @@ -0,0 +1,147 @@ +""" +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 + + +@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, + kwdict={ + "width": width, + "labels": labels, + "outer_pen": outer_pen, + "inner_pen": inner_pen, + "declination": declination, + "declination_label": declination_label, + "intervals": intervals, + }, + default=Position("BL", cstype="inside"), # Default to BL. + ) + + 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") From 4ef70f2efb9464421c58b2ac37862831523bfbce Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 20 Dec 2025 09:10:38 +0800 Subject: [PATCH 2/7] Add doctest-skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/src/magnetic_rose.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py index 46a5461f500..de24565da65 100644 --- a/pygmt/src/magnetic_rose.py +++ b/pygmt/src/magnetic_rose.py @@ -13,6 +13,8 @@ from pygmt.params import Box, Position from pygmt.src._common import _parse_position +__doctest_skip__ = ["magnetic_rose"] + @fmt_docstring def magnetic_rose( # noqa: PLR0913 From 344b5ee6ea57b3e7e8412549e9c76ef8fc20b0b0 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 20 Dec 2025 09:11:13 +0800 Subject: [PATCH 3/7] Fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/src/magnetic_rose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py index de24565da65..ff4c4100996 100644 --- a/pygmt/src/magnetic_rose.py +++ b/pygmt/src/magnetic_rose.py @@ -44,7 +44,7 @@ def magnetic_rose( # noqa: PLR0913 - 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 + - 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. From 877be6807d9a4af5e7aa0a272f59ad9d7485e06b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 20 Dec 2025 09:11:36 +0800 Subject: [PATCH 4/7] Fix typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/src/magnetic_rose.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py index ff4c4100996..0e6c2dd60fb 100644 --- a/pygmt/src/magnetic_rose.py +++ b/pygmt/src/magnetic_rose.py @@ -49,7 +49,7 @@ def magnetic_rose( # noqa: PLR0913 - 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 + 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 From 309f2718e98430bf11a7f464c29cb42a11e23151 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 18 Jan 2026 15:24:23 +0800 Subject: [PATCH 5/7] Pass an empty dict to kwdict --- pygmt/src/magnetic_rose.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py index 0e6c2dd60fb..3449968edf4 100644 --- a/pygmt/src/magnetic_rose.py +++ b/pygmt/src/magnetic_rose.py @@ -110,15 +110,7 @@ def magnetic_rose( # noqa: PLR0913 position = _parse_position( position, - kwdict={ - "width": width, - "labels": labels, - "outer_pen": outer_pen, - "inner_pen": inner_pen, - "declination": declination, - "declination_label": declination_label, - "intervals": intervals, - }, + kwdict={}, # No need to check conflicts since it's a new function. default=Position("BL", cstype="inside"), # Default to BL. ) From b2fe80dfe24b0c74450b95992ae48ef5bd89972d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 19 Jan 2026 10:38:38 +0800 Subject: [PATCH 6/7] Simplify _parse_position --- pygmt/src/magnetic_rose.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pygmt/src/magnetic_rose.py b/pygmt/src/magnetic_rose.py index 3449968edf4..e91ef24c1af 100644 --- a/pygmt/src/magnetic_rose.py +++ b/pygmt/src/magnetic_rose.py @@ -108,11 +108,7 @@ def magnetic_rose( # noqa: PLR0913 """ self._activate_figure() - position = _parse_position( - position, - kwdict={}, # No need to check conflicts since it's a new function. - default=Position("BL", cstype="inside"), # Default to BL. - ) + 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." From 02a858585b7a667eae5793501c209299a67f81cc Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 21 Jan 2026 23:28:36 +0800 Subject: [PATCH 7/7] Document that Figure.basemap's compass parmater is deprecated --- pygmt/src/basemap.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pygmt/src/basemap.py b/pygmt/src/basemap.py index a629c0e207a..4bffa9907ce 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -11,7 +11,7 @@ @fmt_docstring -@use_alias(F="box", Td="rose", Tm="compass", f="coltypes") +@use_alias(F="box", Td="rose", 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, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, @@ -73,6 +74,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. box : bool or str [**+c**\ *clearances*][**+g**\ *fill*][**+i**\ [[*gap*/]\ *pen*]]\ [**+p**\ [*pen*]][**+r**\ [*radius*]][**+s**\ [[*dx*/*dy*/][*shade*]]]. @@ -109,6 +119,7 @@ def basemap( # noqa: PLR0913 Jz=Alias(zscale, name="zscale"), JZ=Alias(zsize, name="zsize"), L=Alias(map_scale, name="map_scale"), # Deprecated. + Tm=Alias(compass, name="compass"), # Deprecated. ).add_common( B=frame, J=projection,