From 753be829b458f8f51ef549a435684bddb32a418b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 8 Sep 2025 14:29:13 +0800 Subject: [PATCH 1/3] 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 | 145 ++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 pygmt/src/directional_rose.py diff --git a/doc/api/index.rst b/doc/api/index.rst index ccb049f996a..d4d59ec7b90 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 474cd91179b..c9d7fd4f664 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -435,6 +435,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..f6a5574e7b1 --- /dev/null +++ b/pygmt/src/directional_rose.py @@ -0,0 +1,145 @@ +""" +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.exceptions import GMTInvalidInput +from pygmt.helpers import build_arg_list +from pygmt.params import Box + + +def directional_rose( # noqa: PLR0913 + self, + position: Sequence[float | str] | AnchorCode | None = None, + position_type: Literal[ + "mapcoords", "boxcoords", "plotcoords", "inside", "outside" + ] = "plotcoords", + anchor: AnchorCode | None = None, + anchor_offset: Sequence[float | str] | None = None, + width: float | str | None = None, + label: Sequence[str] | bool = False, + fancy: Literal[1, 2, 3] | bool = False, + box: Box | bool = False, + perspective: str | bool = False, + verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] + | bool = False, + transparency: float | None = None, +): + r""" + Add a directional rose on the map. + + The directional rose is plotted at the location defined by the reference point + (specified by the **position** and *position_type** parameters) and anchor point + (specified by the **anchor** and **anchor_offset** parameters). Refer to + :doc:`/techref/reference_anchor_points` for details about the positioning. + + Parameters + ---------- + position/position_type + Specify the reference point on the map for the directional rose. The reference + point can be specified in five different ways, which is selected by the + **position_type** parameter. The actual reference point is then given by the + coordinates or code specified by the **position** parameter. + + The **position_type** parameter can be one of the following: + + - ``"mapcoords"``: **position** is given as (*longitude*, *latitude*) in map + coordinates. + - ``"boxcoords"``: **position** is given as (*nx*, *ny*) in normalized + coordinates, i.e., fractional coordinates between 0 and 1 in both the x and y + directions. For example, (0, 0) is the lower-left corner and (1, 1) is the + upper-right corner of the plot bounding box. + - ``"plotcoords"``: **position** is given as (x, y) in plot coordinates, i.e., + the distances in inches, centimeters, or points from the lower left plot + origin. + - ``"inside"`` or ``"outside"``: **position** is one of the nine + :doc:`2-character justification codes `, meaning + placing the reference point at specific locations, either inside or outside + the plot bounding box. + anchor + Anchor point of the directional rose, specified by one of the + :doc:`2-character justification codes `. + The default value depends on the **position_type** parameter. + + - ``position_type="inside"``: **anchor** defaults to the same as **position**. + - ``position_type="outside"``: **anchor** defaults to the mirror opposite of + **position**. + - Otherwise, **anchor** defaults to ``"MC"`` (middle center). + anchor_offset + *offset* or (*offset_x*, *offset_y*). + Offset the anchor point by *offset_x* and *offset_y*. If a single value *offset* + is given, *offset_y* = *offset_x* = *offset*. + width + Width of the rose in plot coordinates (append **i** (inch), **cm** + (centimeters), or **p** (points)), or append % for a size in percentage of map + width [Default is 10 %]. + label + 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``, use the default labels + ``["W", "E", "S", "N"]``. + 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. + {perspective} + {verbose} + {transparency} + + Examples + -------- + >>> import pygmt + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) + >>> fig.directional_rose(position=(10, 10), position_type="mapcoords") + >>> fig.show() + """ + self._activate_figure() + + if position is None: + msg = "Parameter 'position' is required." + raise GMTInvalidInput(msg) + + aliasdict = AliasSystem( + F=Alias(box, name="box"), + Td=[ + Alias( + position_type, + name="position_type", + mapping={ + "mapcoords": "g", + "boxcoords": "n", + "plotcoords": "x", + "inside": "j", + "outside": "J", + }, + ), + Alias(position, name="position", sep="/", size=2), + Alias(anchor, name="anchor", prefix="+j"), + Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), + Alias(fancy, name="fancy", prefix="+f"), # +F is not supported yet. + Alias(label, name="label", prefix="+l", sep=",", size=4), + Alias(width, name="width", prefix="+w"), + ], + p=Alias(perspective, name="perspective"), + ).add_common( + V=verbose, + t=transparency, + ) + + with Session() as lib: + lib.call_module(module="basemap", args=build_arg_list(aliasdict)) From 839851c3b0fd630ffbfeaf38335ffe79d2b52bbd Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 8 Sep 2025 14:29:38 +0800 Subject: [PATCH 2/3] Add tests for Figure.directional_rose --- .../test_directional_rose_complex.png.dvc | 5 +++ pygmt/tests/test_directional_rose.py | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 pygmt/tests/baseline/test_directional_rose_complex.png.dvc create mode 100644 pygmt/tests/test_directional_rose.py 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..6cf64af3cf6 --- /dev/null +++ b/pygmt/tests/baseline/test_directional_rose_complex.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4bf9dc9a74af3df5fe03ba4cdfaf27ca + size: 14424 + hash: md5 + path: test_directional_rose_complex.png diff --git a/pygmt/tests/test_directional_rose.py b/pygmt/tests/test_directional_rose.py new file mode 100644 index 00000000000..15d12da66a7 --- /dev/null +++ b/pygmt/tests/test_directional_rose.py @@ -0,0 +1,36 @@ +""" +Test Figure.directional_rose. +""" + +import pytest +from pygmt import Figure + + +@pytest.mark.mpl_image_compare(filename="test_basemap_rose.png") +def test_directional_rose(): + """ + Test the Figure.directional_rose method. + """ + fig = Figure() + fig.basemap(region=[127.5, 128.5, 26, 27], projection="H15c", frame=True) + fig.directional_rose(position="MC", position_type="inside", width="5c") + 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, -30, 30], projection="M10c", frame=True) + fig.directional_rose( + position=(50, 0), + position_type="mapcoords", + width="1c", + label=["", "", "", "N"], + fancy=2, + anchor="MC", + anchor_offset=(1, 1), + ) + return fig From 4b7b7993a4770d9438d3066986eb86e212e32444 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 8 Sep 2025 14:44:54 +0800 Subject: [PATCH 3/3] Use Figure.directional_rose in the inset test --- pygmt/tests/test_inset.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pygmt/tests/test_inset.py b/pygmt/tests/test_inset.py index ca5b411fe68..7a77ecc3818 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", margin=0, box=Box(pen="black")): fig.basemap(region=[-80, -65, 35, 50], projection="M3c", 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", position_type="inside", width="3c") return fig