From 0851a2e0342fbbff7ae701a3b2b93c7ef2b1d63e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 8 Sep 2025 14:29:13 +0800 Subject: [PATCH] Add Figure.directional_rose for adding directional rose on maps --- doc/api/index.rst | 1 + pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/directional_rose.py | 112 ++++++++++++++++++ .../baseline/test_directional_rose.png.dvc | 5 + .../test_directional_rose_complex.png.dvc | 5 + .../test_directional_rose_fancy.png.dvc | 5 + pygmt/tests/test_directional_rose.py | 45 +++++++ pygmt/tests/test_inset.py | 3 +- 9 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 pygmt/src/directional_rose.py create mode 100644 pygmt/tests/baseline/test_directional_rose.png.dvc create mode 100644 pygmt/tests/baseline/test_directional_rose_complex.png.dvc create mode 100644 pygmt/tests/baseline/test_directional_rose_fancy.png.dvc create mode 100644 pygmt/tests/test_directional_rose.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 264f5a9175a..317f7368095 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -27,6 +27,7 @@ Plotting map elements Figure.basemap Figure.coast Figure.colorbar + Figure.directional_rose Figure.hlines Figure.inset Figure.legend diff --git a/pygmt/figure.py b/pygmt/figure.py index 56ad2c3d5cf..b7e49b6132f 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -413,6 +413,7 @@ def _repr_html_(self) -> str: coast, colorbar, contour, + directional_rose, grdcontour, grdimage, grdview, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..1e34c3ba669 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -10,6 +10,7 @@ from pygmt.src.config import config from pygmt.src.contour import contour from pygmt.src.dimfilter import dimfilter +from pygmt.src.directional_rose import directional_rose from pygmt.src.filter1d import filter1d from pygmt.src.grd2cpt import grd2cpt from pygmt.src.grd2xyz import grd2xyz diff --git a/pygmt/src/directional_rose.py b/pygmt/src/directional_rose.py new file mode 100644 index 00000000000..96fc879c71c --- /dev/null +++ b/pygmt/src/directional_rose.py @@ -0,0 +1,112 @@ +""" +directional_rose - Add a map directional 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.helpers import build_arg_list, fmt_docstring +from pygmt.params import Box, Position +from pygmt.src._common import _parse_position + +__doctest_skip__ = ["directional_rose"] + + +@fmt_docstring +def directional_rose( + self, + position: Position | Sequence[float | str] | AnchorCode | None = None, + width: float | str | None = None, + labels: Sequence[str] | bool = False, + fancy: Literal[1, 2, 3] | bool = False, + 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 directional rose on the map. + + Parameters + ---------- + position + Position of the directional 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. + width + Width of the rose in plot coordinates, or append % for a size in percentage of + map width [Default is 10%]. + 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 set to ``True``, the default labels + ``["W", "E", "S", "N"]`` are used. + fancy + Get a fancy rose. The fanciness level can be set to 1, 2, or 3: + + - Level 1 draws the two principal E-W, N-S orientations + - Level 2 adds the two intermediate NW-SE and NE-SW orientations + - Level 3 adds the four minor orientations WNW-ESE, NNW-SSE, NNE-SSW, and + ENE-WSW + + If set to ``True``, defaults to level 1. + box + Draw a background box behind the directional 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. + $verbose + $panel + $perspective + $transparency + + Examples + -------- + >>> import pygmt + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 80, 0, 30], projection="M10c", frame=True) + >>> fig.directional_rose() + >>> fig.show() + """ + self._activate_figure() + + position = _parse_position( + position, + kwdict={"width": width, "fancy": fancy, "labels": labels}, + default=Position("BL", cstype="inside"), # Default to BL. + ) + + # The GMT documentation says the default width is 10%, but in PyGMT, it crashes + # if width is not provided. So we set it to 10% here. + if not isinstance(position, str) and width is None: + width = "10%" + + aliasdict = AliasSystem( + F=Alias(box, name="box"), + Td=[ + Alias(position, name="position"), + Alias(width, name="width", prefix="+w"), + Alias(fancy, name="fancy", prefix="+f"), # +F is not supported yet. + Alias(labels, name="labels", prefix="+l", sep=",", size=4), + ], + ).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_directional_rose.png.dvc b/pygmt/tests/baseline/test_directional_rose.png.dvc new file mode 100644 index 00000000000..c60eab2fb4c --- /dev/null +++ b/pygmt/tests/baseline/test_directional_rose.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: c1c167a826132bcdabc3b032438db01e + size: 8319 + hash: md5 + path: test_directional_rose.png diff --git a/pygmt/tests/baseline/test_directional_rose_complex.png.dvc b/pygmt/tests/baseline/test_directional_rose_complex.png.dvc new file mode 100644 index 00000000000..89b7676a220 --- /dev/null +++ b/pygmt/tests/baseline/test_directional_rose_complex.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 7a7eabd0036dc9b744e54dfbc398fc6e + size: 9772 + hash: md5 + path: test_directional_rose_complex.png diff --git a/pygmt/tests/baseline/test_directional_rose_fancy.png.dvc b/pygmt/tests/baseline/test_directional_rose_fancy.png.dvc new file mode 100644 index 00000000000..806ea7b9131 --- /dev/null +++ b/pygmt/tests/baseline/test_directional_rose_fancy.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 7498ed12efd8b956a0fe13d18e0e3ea9 + size: 8648 + hash: md5 + path: test_directional_rose_fancy.png diff --git a/pygmt/tests/test_directional_rose.py b/pygmt/tests/test_directional_rose.py new file mode 100644 index 00000000000..9dd9569a904 --- /dev/null +++ b/pygmt/tests/test_directional_rose.py @@ -0,0 +1,45 @@ +""" +Test Figure.directional_rose. +""" + +import pytest +from pygmt import Figure +from pygmt.params import Position + + +@pytest.mark.mpl_image_compare +def test_directional_rose(): + """ + Test the Figure.directional_rose method with default position and width. + """ + fig = Figure() + fig.basemap(region=[0, 80, 0, 30], projection="M10c", frame=True) + fig.directional_rose() + return fig + + +@pytest.mark.mpl_image_compare +def test_directional_rose_fancy(): + """ + Test the Figure.directional_rose method with default position and width. + """ + fig = Figure() + fig.basemap(region=[0, 80, 0, 30], projection="M10c", frame=True) + fig.directional_rose(fancy=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_directional_rose_complex(): + """ + Test the Figure.directional_rose method with more parameters. + """ + fig = Figure() + fig.basemap(region=[0, 80, 0, 30], projection="M10c", frame=True) + fig.directional_rose( + position=Position((50, 0), cstype="mapcoords", anchor="MC", offset=(1, 1)), + width="1c", + labels=["", "", "", "N"], + fancy=2, + ) + return fig diff --git a/pygmt/tests/test_inset.py b/pygmt/tests/test_inset.py index abb60b85d18..dcf1d869b13 100644 --- a/pygmt/tests/test_inset.py +++ b/pygmt/tests/test_inset.py @@ -30,5 +30,6 @@ def test_inset_context_manager(): fig.basemap(region=[-74, -69.5, 41, 43], projection="M9c", frame=True) with fig.inset(position="jBL+w3c+o0.2c", clearance=0.2, box=True): fig.basemap(region="g", projection="G47/-20/?", frame="afg") - fig.basemap(rose="jTR+w3c") # Pass rose argument with basemap after the inset + # Plot an rose after the inset + fig.directional_rose(position="TR", width="3c") return fig