From fb64cbf18f6f8cf870c9cd8a0892b3874e3b1664 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 15:14:02 +0800 Subject: [PATCH 01/48] Figure.logo: Refactor using the AliasSystem class --- pygmt/src/logo.py | 48 +++++++++++++++++++++++++++++++++++++--- pygmt/tests/test_logo.py | 8 ++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index d65bf3bc775..b302d806883 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -2,7 +2,11 @@ logo - Plot the GMT logo. """ +from typing import Literal + +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, kwargs_to_strings, use_alias @@ -10,7 +14,6 @@ @use_alias( R="region", J="projection", - D="position", F="box", S="style", V="verbose", @@ -18,7 +21,18 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def logo(self, **kwargs): +def logo( + self, + position_type: Literal[ + "user", "justify", "mirror", "normalize", "plot", None + ] = None, + position=None, + height=None, + width=None, + justify=None, + offset=None, + **kwargs, +): r""" Plot the GMT logo. @@ -39,6 +53,9 @@ def logo(self, **kwargs): [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ **+w**\ *width*\ [**+j**\ *justify*]\ [**+o**\ *dx*\ [/*dy*]]. Set reference point on the map for the image. + positon_type + width/height + Width or height of the GMT logo. box : bool or str If set to ``True``, draw a rectangular border around the GMT logo. @@ -55,5 +72,30 @@ def logo(self, **kwargs): {transparency} """ self._activate_figure() + if width is not None and height is not None: + msg = "Cannot specify both width and height." + raise GMTInvalidInput(msg) + + aliasdict = AliasSystem( + D=[ + Alias( + position_type, + name="position_type", + mapping={ + "user": "g", + "justify": "j", + "mirror": "J", + "normalize": "n", + "plot": "x", + }, + ), + Alias(position, name="position", separator="/"), + Alias(height, name="height", prefix="+h"), + Alias(width, name="width", prefix="+w"), + Alias(justify, name="justify", prefix="+j"), + Alias(offset, name="offset", prefix="+o", separator="/", size=2), + ] + ).merge(kwargs) + with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(kwargs)) + lib.call_module(module="logo", args=build_arg_list(aliasdict)) diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 62bddf4eb24..bcd803d8252 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -24,5 +24,11 @@ def test_logo_on_a_map(): """ fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) - fig.logo(position="jTR+o0.25c/0.25c+w7.5c", box=True) + fig.logo( + position_type="justify", + position="TR", + offset=(0.25, 0.25), + width="7.5c", + box=True, + ) return fig From 1eb6587657ffc55ed33b48c0a677a91e3165819c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 10 Aug 2025 20:12:00 +0800 Subject: [PATCH 02/48] Update position_type --- pygmt/src/logo.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index b302d806883..f02cf44b8bc 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -2,8 +2,10 @@ logo - Plot the GMT logo. """ +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 @@ -23,14 +25,14 @@ @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") def logo( self, + position: Sequence[str | float] | AnchorCode, position_type: Literal[ - "user", "justify", "mirror", "normalize", "plot", None - ] = None, - position=None, + "mapcoords", "inside", "outside", "boxcoords", "plotcoords" + ] = "mapcoords", height=None, width=None, justify=None, - offset=None, + anchor_offset=None, **kwargs, ): r""" @@ -49,11 +51,21 @@ def logo( ---------- {projection} {region} - position : str - [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ - **+w**\ *width*\ [**+j**\ *justify*]\ [**+o**\ *dx*\ [/*dy*]]. - Set reference point on the map for the image. - positon_type + position/position_type + Location of the GMT logo. The actual meaning of this parameter depends on the + ``position_type`` parameter. + - ``position_type="mapcoords"``: *position* is given as (x, y) in user + coordinates. + - ``position_type="boxcoords"``: *position* is given as (nx, ny) in normalized + coordinates, where (0, 0) is the lower-left corner and (1, 1) is the + upper-right corner of the map. + - ``position_type="plotcoords"``: *position* is given as (x, y) in plot + coordinates. + - ``position_type="inside"``: *position* is given as a two-character + justification code, meaning the anchor point of the rose is inside the map + bounding box. + - ``position_type="outside"``: *position* is given as a two-character + justification code, but the rose is outside the map bounding box. width/height Width or height of the GMT logo. box : bool or str @@ -82,18 +94,18 @@ def logo( position_type, name="position_type", mapping={ - "user": "g", - "justify": "j", - "mirror": "J", - "normalize": "n", - "plot": "x", + "mapcoords": "g", + "inside": "j", + "outside": "J", + "boxcoords": "n", + "plotcoords": "x", }, ), - Alias(position, name="position", separator="/"), + Alias(position, name="position", sep="/"), Alias(height, name="height", prefix="+h"), Alias(width, name="width", prefix="+w"), Alias(justify, name="justify", prefix="+j"), - Alias(offset, name="offset", prefix="+o", separator="/", size=2), + Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), ] ).merge(kwargs) From 6fa17ac3cbe843d75268ce8464781adbc835d231 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 10 Aug 2025 20:39:36 +0800 Subject: [PATCH 03/48] Updates --- pygmt/src/logo.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index f02cf44b8bc..230a23555a8 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -27,7 +27,7 @@ def logo( self, position: Sequence[str | float] | AnchorCode, position_type: Literal[ - "mapcoords", "inside", "outside", "boxcoords", "plotcoords" + "mapcoords", "boxcoords", "plotcoords", "inside", "outside" ] = "mapcoords", height=None, width=None, @@ -58,14 +58,15 @@ def logo( coordinates. - ``position_type="boxcoords"``: *position* is given as (nx, ny) in normalized coordinates, where (0, 0) is the lower-left corner and (1, 1) is the - upper-right corner of the map. + upper-right corner of the plot. - ``position_type="plotcoords"``: *position* is given as (x, y) in plot - coordinates. + coordinates, i.e., the distances in inches, centimeters, or points from the + lower left plot origin. - ``position_type="inside"``: *position* is given as a two-character - justification code, meaning the anchor point of the rose is inside the map + justification code, meaning the anchor point of the rose is inside the plot bounding box. - ``position_type="outside"``: *position* is given as a two-character - justification code, but the rose is outside the map bounding box. + justification code, but the rose is outside the plot bounding box. width/height Width or height of the GMT logo. box : bool or str @@ -95,10 +96,10 @@ def logo( name="position_type", mapping={ "mapcoords": "g", - "inside": "j", - "outside": "J", "boxcoords": "n", "plotcoords": "x", + "inside": "j", + "outside": "J", }, ), Alias(position, name="position", sep="/"), From cd1165fcc744853f4962e1ad8623290fd5bf0d7b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Sep 2025 22:59:20 +0800 Subject: [PATCH 04/48] fix --- examples/gallery/embellishments/gmt_logo.py | 3 +- pygmt/src/inset.py | 5 +- pygmt/src/logo.py | 94 +++++++++++++-------- pygmt/tests/test_logo.py | 2 +- 4 files changed, 61 insertions(+), 43 deletions(-) diff --git a/examples/gallery/embellishments/gmt_logo.py b/examples/gallery/embellishments/gmt_logo.py index b0311881577..1993dafa325 100644 --- a/examples/gallery/embellishments/gmt_logo.py +++ b/examples/gallery/embellishments/gmt_logo.py @@ -13,6 +13,5 @@ # Add the GMT logo in the Top Right (TR) corner of the current plot, scaled up to be 3 # centimeters wide and offset by 0.3 cm in x-direction and 0.6 cm in y-direction. -fig.logo(position="jTR+o0.3c/0.6c+w3c") - +fig.logo(position="TR", position_type="inside", anchor_offset=(0.3, 0.6), width="3c") fig.show() diff --git a/pygmt/src/inset.py b/pygmt/src/inset.py index ad0195875e8..fee3289c528 100644 --- a/pygmt/src/inset.py +++ b/pygmt/src/inset.py @@ -139,9 +139,8 @@ def inset( ... dcw="MG+gred", ... ) ... - >>> # Map elements outside the "with" statement are plotted in the main - >>> # figure - >>> fig.logo(position="jBR+o0.2c+w3c") + >>> # Map elements outside the "with" statement are plotted in the main figure + >>> fig.logo(position="BR", position_type="inside", offset=0.2, width="3c") >>> fig.show() """ self._activate_figure() diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 71d257e8433..a30c034a9ea 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -17,7 +17,7 @@ @kwargs_to_strings(R="sequence", p="sequence") def logo( # noqa: PLR0913 self, - position: Sequence[str | float] | AnchorCode, + position: Sequence[str | float] | AnchorCode | None = None, position_type: Literal[ "mapcoords", "boxcoords", "plotcoords", "inside", "outside" ] = "plotcoords", @@ -26,7 +26,6 @@ def logo( # noqa: PLR0913 height: float | str | None = None, width: float | str | None = None, projection=None, - box=False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | tuple[int, int] | bool = False, @@ -36,14 +35,20 @@ def logo( # noqa: PLR0913 r""" Plot the GMT logo. - By default, the GMT logo is 2 inches wide and 1 inch high and - will be positioned relative to the current plot origin. - Use various options to change this and to place a transparent or - opaque rectangular map panel behind the GMT logo. + .. figure:: https://docs.generic-mapping-tools.org/6.5/_images/GMT_coverlogo.png + :alt: GMT logo + :align: center + + By default, the GMT logo is 2 inches wide and 1 inch high and will be positioned + relative to the current plot origin. The position can be changed by specifying the + reference point (via ``position_type`` and ``position``) and anchor point (via + ``anchor`` and ``anchor_offset``). Refer to :doc:`/techref/reference_anchor_points` + for details about the positioning. Full GMT docs at :gmt-docs:`gmtlogo.html`. {aliases} + - D = position/position_type/anchor/anchor_offset/width/height - J = projection - V = verbose - c = panel @@ -54,23 +59,42 @@ def logo( # noqa: PLR0913 {projection} {region} position/position_type - Location of the GMT logo. The actual meaning of this parameter depends on the - ``position_type`` parameter. - - ``position_type="mapcoords"``: *position* is given as (x, y) in user + Specify the reference point on the plot for the GMT logo. 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. - - ``position_type="boxcoords"``: *position* is given as (nx, ny) in normalized - coordinates, where (0, 0) is the lower-left corner and (1, 1) is the - upper-right corner of the plot. - - ``position_type="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. - - ``position_type="inside"``: *position* is given as a two-character - justification code, meaning the anchor point of the rose is inside the plot - bounding box. - - ``position_type="outside"``: *position* is given as a two-character - justification code, but the rose is outside the plot bounding box. + - ``"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 magnetic 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/height - Width or height of the GMT logo. + Width or height of the GMT logo. Since the aspect ratio is fixed, only one of + the two can be specified. box : bool or str If set to ``True``, draw a rectangular border around the GMT logo. @@ -87,33 +111,29 @@ def logo( # noqa: PLR0913 {transparency} """ self._activate_figure() + + # width and height are mutually exclusive. if width is not None and height is not None: msg = "Cannot specify both width and height." raise GMTInvalidInput(msg) + # Mapping position_type to GMT shorthand. + _position_type = { + "mapcoords": "g", + "boxcoords": "n", + "plotcoords": "x", + "inside": "j", + "outside": "J", + }.get(position_type) + aliasdict = AliasSystem( D=[ - Alias( - position_type, - name="position_type", - mapping={ - "mapcoords": "g", - "boxcoords": "n", - "plotcoords": "x", - "inside": "j", - "outside": "J", - }, - ), - Alias(position, name="position", sep="/"), + Alias(position, name="position", sep="/", size=2, prefix=_position_type), Alias(anchor, name="justify", prefix="+j"), Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), Alias(height, name="height", prefix="+h"), Alias(width, name="width", prefix="+w"), ] - ).merge(kwargs) - - aliasdict = AliasSystem( - F=Alias(box, name="box"), ).add_common( J=projection, V=verbose, diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index bcd803d8252..10bf8608456 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -25,7 +25,7 @@ def test_logo_on_a_map(): fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) fig.logo( - position_type="justify", + position_type="inside", position="TR", offset=(0.25, 0.25), width="7.5c", From b17ecfeb93b94c933fcf06a4bab72bbc63007f61 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Sep 2025 23:19:12 +0800 Subject: [PATCH 05/48] Remove D from use_alias --- pygmt/src/logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index a30c034a9ea..76d9d5e0381 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -13,7 +13,7 @@ @fmt_docstring -@use_alias(R="region", D="position", F="box", S="style") +@use_alias(R="region", F="box", S="style") @kwargs_to_strings(R="sequence", p="sequence") def logo( # noqa: PLR0913 self, From 6ec9c6601d5c45ed763ae555b174998c87862e95 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Sep 2025 23:31:08 +0800 Subject: [PATCH 06/48] Fix typos --- pygmt/src/logo.py | 2 +- pygmt/tests/test_logo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 76d9d5e0381..035864a3f31 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -124,7 +124,7 @@ def logo( # noqa: PLR0913 "plotcoords": "x", "inside": "j", "outside": "J", - }.get(position_type) + }[position_type] aliasdict = AliasSystem( D=[ diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 10bf8608456..b75c6c05641 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -27,7 +27,7 @@ def test_logo_on_a_map(): fig.logo( position_type="inside", position="TR", - offset=(0.25, 0.25), + anchor_offset=(0.25, 0.25), width="7.5c", box=True, ) From 9a0575d37c232158f962b0b477ec487a284e284d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 12 Sep 2025 00:43:00 +0800 Subject: [PATCH 07/48] Improve docstrings --- pygmt/src/logo.py | 14 +++++++++----- pygmt/tests/test_logo.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 035864a3f31..dfca512973b 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -38,6 +38,7 @@ def logo( # noqa: PLR0913 .. figure:: https://docs.generic-mapping-tools.org/6.5/_images/GMT_coverlogo.png :alt: GMT logo :align: center + :width: 300px By default, the GMT logo is 2 inches wide and 1 inch high and will be positioned relative to the current plot origin. The position can be changed by specifying the @@ -67,20 +68,23 @@ def logo( # noqa: PLR0913 The **position_type** parameter can be one of the following: - ``"mapcoords"``: **position** is given as (*longitude*, *latitude*) in map - coordinates. + coordinates. For example, (120, -45) means placing the reference point at + 120°E and 45°S 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. + origin. For example, ("1c", "2c") means placing the reference point 1 cm to + the right and 2 cm above 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. + the plot bounding box. E.g., ``"TL"`` means placing the reference point at the + top left corner of the plot bounding box, either inside or outside the box. anchor - Anchor point of the magnetic rose, specified by one of the + Anchor point of the GMT logo, specified by one of the :doc:`2-character justification codes `. The default value depends on the **position_type** parameter. @@ -129,7 +133,7 @@ def logo( # noqa: PLR0913 aliasdict = AliasSystem( D=[ Alias(position, name="position", sep="/", size=2, prefix=_position_type), - Alias(anchor, name="justify", prefix="+j"), + Alias(anchor, name="anchor", prefix="+j"), Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), Alias(height, name="height", prefix="+h"), Alias(width, name="width", prefix="+w"), diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index b75c6c05641..c7691095e53 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -4,6 +4,7 @@ import pytest from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput @pytest.mark.benchmark @@ -32,3 +33,12 @@ def test_logo_on_a_map(): box=True, ) return fig + + +def test_logo_width_and_height(): + """ + Test that an error is raised when both width and height are specified. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.logo(width="5c", height="5c") From d19959ec383c6d640c28a5e4de9317f905889a0c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 12 Sep 2025 12:43:36 +0800 Subject: [PATCH 08/48] Improve docstrings --- pygmt/src/logo.py | 56 +++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index dfca512973b..8487391bcab 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -57,45 +57,41 @@ def logo( # noqa: PLR0913 Parameters ---------- - {projection} - {region} position/position_type - Specify the reference point on the plot for the GMT logo. 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. For example, (120, -45) means placing the reference point at - 120°E and 45°S 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 + Specify the reference point on the plot for the GMT logo. The method of + defining the the reference point is controlled by **position_type**, and the + exact location is set by **position**. + + The **position_type** parameter can take one of the following values: + + - ``"mapcoords"``: **position** is specified as (*longitude*, *latitude*) in map + coordinates. Example: (120, -45) places the reference point at 120°E, 45°S. + - ``"boxcoords"``: **position** is specified as (*nx*, *ny*) in normalized + coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. + Example: (0, 0) corresponds to the lower-left corner, and (1, 1) to 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. For example, ("1c", "2c") means placing the reference point 1 cm to - the right and 2 cm above the lower left plot origin. + - ``"plotcoords"``: **position** is specified as (*x*, *y*) in plot coordinates, + i.e., distances from the lower-left plot origin given in inches, centimeters, + or points. Example: ("1c", "2c") places the reference point 1 cm to the right + and 2 cm above the 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. E.g., ``"TL"`` means placing the reference point at the - top left corner of the plot bounding box, either inside or outside the box. + :doc:two-character justification codes , + indicating a specific location relative to the plot bounding box. Example: + ``"TL"`` places the reference point at the top-left corner, either inside or + outside the bounding box. anchor - Anchor point of the GMT logo, specified by one of the + Specify the anchor point of the GMT logo, using one of the :doc:`2-character justification codes `. - The default value depends on the **position_type** parameter. + The default value depends on **position_type**. - ``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*. + Specifies an offset for the anchor point as *offset* or + (*offset_x*, *offset_y*). If a single value *offset* is given, both *offset_x* + and *offset_y* are set to *offset*. width/height Width or height of the GMT logo. Since the aspect ratio is fixed, only one of the two can be specified. @@ -110,6 +106,8 @@ def logo( # noqa: PLR0913 [Default] - **n** to skip the label placement - **u** to place the URL to the GMT site + {projection} + {region} {verbose} {panel} {transparency} @@ -121,7 +119,7 @@ def logo( # noqa: PLR0913 msg = "Cannot specify both width and height." raise GMTInvalidInput(msg) - # Mapping position_type to GMT shorthand. + # Mapping position_type to GMT single-letter code. _position_type = { "mapcoords": "g", "boxcoords": "n", From f8f418b2710207ff8cc84f666b4e99f449936a8f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 14 Sep 2025 21:00:47 +0800 Subject: [PATCH 09/48] Backward compatibility --- pygmt/src/logo.py | 14 ++++++++++++++ pygmt/tests/test_logo.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 8487391bcab..0d7c6d3de94 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -128,6 +128,20 @@ def logo( # noqa: PLR0913 "outside": "J", }[position_type] + # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. + # For backward compatibility, we need to check if users pass a string with the GMT + # CLI syntax to 'position', i.e., a string starting with one of the leading + # single-letter or contains modifiers with "+". + if isinstance(position, str) and (position[0] in "gnxjJ" or "+" in position): + if any(v is not None for v in (anchor, anchor_offset, height, width)): + msg = ( + "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " + "with other parameters (anchor, anchor_offset, height, width). " + "Please refer to the documentation for the recommended usage." + ) + raise GMTInvalidInput(msg) + _position_type = "" # Unset _position_type to an empty string. + aliasdict = AliasSystem( D=[ Alias(position, name="position", sep="/", size=2, prefix=_position_type), diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index c7691095e53..457304f96a3 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -35,6 +35,34 @@ def test_logo_on_a_map(): return fig +@pytest.mark.mpl_image_compare(filename="test_logo_position_deprecated_syntax.png") +def test_logo_position(): + """ + Test that the new group of parameters works as expected. + """ + fig = Figure() + fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) + fig.logo(position="TL", position_type="inside") + fig.logo(position="TR", position_type="inside", width="7.5c") + fig.logo(position=(6, 0), width="7.5c") + fig.logo(position=(0, 0)) + return fig + + +@pytest.mark.mpl_image_compare +def test_logo_position_deprecated_syntax(): + """ + Test that passing the deprecated GMT CLI syntax string to 'position' works. + """ + fig = Figure() + fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) + fig.logo(position="jTL") + fig.logo(position="jTR+w7.5c") + fig.logo(position="6/0+w7.5c") + fig.logo(position="0/0") # This is actually the new syntax. + return fig + + def test_logo_width_and_height(): """ Test that an error is raised when both width and height are specified. From df50342bd899a211fab51dcc43c5e632661e9b29 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 15 Sep 2025 14:40:42 +0800 Subject: [PATCH 10/48] Improve Figure.logo tests --- .../baseline/test_logo_position_type.png.dvc | 5 ++++ pygmt/tests/test_logo.py | 24 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 pygmt/tests/baseline/test_logo_position_type.png.dvc diff --git a/pygmt/tests/baseline/test_logo_position_type.png.dvc b/pygmt/tests/baseline/test_logo_position_type.png.dvc new file mode 100644 index 00000000000..a1ea8c1a191 --- /dev/null +++ b/pygmt/tests/baseline/test_logo_position_type.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 9b9d740219df9edb298dbc49e7ef1351 + size: 232635 + hash: md5 + path: test_logo_position_type.png diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 457304f96a3..c9bcacfd17d 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -35,21 +35,23 @@ def test_logo_on_a_map(): return fig -@pytest.mark.mpl_image_compare(filename="test_logo_position_deprecated_syntax.png") -def test_logo_position(): +@pytest.mark.mpl_image_compare +def test_logo_position_type(): """ Test that the new group of parameters works as expected. """ fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) - fig.logo(position="TL", position_type="inside") - fig.logo(position="TR", position_type="inside", width="7.5c") - fig.logo(position=(6, 0), width="7.5c") - fig.logo(position=(0, 0)) + fig.logo(position_type="inside", position="TL") + fig.logo(position_type="outside", position="TR") + fig.logo(position_type="mapcoords", position=(-80, 15)) + fig.logo(position_type="boxcoords", position=(0, 0.5)) + fig.logo(position_type="plotcoords", position=("2c", "0c"), width="5c") + fig.logo(position=("8c", "0c")) # Default position_type is "plotcoords". return fig -@pytest.mark.mpl_image_compare +@pytest.mark.mpl_image_compare(filename="test_logo_position_type.png") def test_logo_position_deprecated_syntax(): """ Test that passing the deprecated GMT CLI syntax string to 'position' works. @@ -57,9 +59,11 @@ def test_logo_position_deprecated_syntax(): fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) fig.logo(position="jTL") - fig.logo(position="jTR+w7.5c") - fig.logo(position="6/0+w7.5c") - fig.logo(position="0/0") # This is actually the new syntax. + fig.logo(position="JTR") + fig.logo(position="g-80/15") + fig.logo(position="n0/0.5") + fig.logo(position="x2c/0c+w5c") + fig.logo(position="8c/0c") return fig From 8699d9aceca9ab49358f2394c3ddd0c328f3cf86 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 15 Sep 2025 15:07:32 +0800 Subject: [PATCH 11/48] Add a test for mixed syntax --- pygmt/tests/test_logo.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index c9bcacfd17d..2d39c52c534 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -74,3 +74,14 @@ def test_logo_width_and_height(): fig = Figure() with pytest.raises(GMTInvalidInput): fig.logo(width="5c", height="5c") + + +def test_logo_position_mixed_syntax(): + """ + Test that an error is raised when mixing new and deprecated syntax in 'position'. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.logo(position="jTL", width="5c") + with pytest.raises(GMTInvalidInput): + fig.logo(position="jTL", anchor="BR") From 5e02d22c3c5d82b91b07592d5fe71730e87b875b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 15 Oct 2025 12:26:12 +0800 Subject: [PATCH 12/48] Update docstrings --- pygmt/src/logo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 9425c61ee95..0c47751c4d2 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -63,11 +63,11 @@ def logo( # noqa: PLR0913 Parameters ---------- - position/position_type - Specify the reference point on the plot for the GMT logo. The method of - defining the the reference point is controlled by **position_type**, and the - exact location is set by **position**. - + position + Specify the reference point on the plot for the GMT logo. The method of defining + the reference point is controlled by **position_type**, and the exact location + is set by **position**. + position_type The **position_type** parameter can take one of the following values: - ``"mapcoords"``: **position** is specified as (*longitude*, *latitude*) in map From 75682649ec356a922a84eea4762111ffe0652e41 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 15 Oct 2025 13:10:14 +0800 Subject: [PATCH 13/48] Fix formatting --- pygmt/src/logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 0c47751c4d2..3e5f4191e84 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -65,7 +65,7 @@ def logo( # noqa: PLR0913 ---------- position Specify the reference point on the plot for the GMT logo. The method of defining - the reference point is controlled by **position_type**, and the exact location + the reference point is controlled by **position_type**, and the exact location is set by **position**. position_type The **position_type** parameter can take one of the following values: From 43d08a0960e29f1db1b6876b1d0bf7bd23404848 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 15 Oct 2025 13:16:08 +0800 Subject: [PATCH 14/48] Fix docstrings --- pygmt/src/logo.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 3e5f4191e84..e97dd06bd45 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -51,8 +51,12 @@ def logo( # noqa: PLR0913 Full GMT docs at :gmt-docs:`gmtlogo.html`. - {aliases} - - D = position/position_type/anchor/anchor_offset/width/height + **Aliases:** + + .. hlist:: + :columns: 3 + + - D = position/position_type, **+j**: anchor, **+o**: anchor_offset, **+w**: width, **+h**:height - F = box - J = projection - R = region From 91b6b258fba7a1cd04b6ff97a145cda1530ba7b0 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 15 Oct 2025 14:00:11 +0800 Subject: [PATCH 15/48] Fix long docstrings --- pygmt/src/logo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index e97dd06bd45..9ef29b08507 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -53,10 +53,15 @@ def logo( # noqa: PLR0913 **Aliases:** + .. hlist:: + :columns: 1 + + - D = position/position_type, **+j**: anchor, **+o**: anchor_offset, + **+w**: width, **+h**:height + .. hlist:: :columns: 3 - - D = position/position_type, **+j**: anchor, **+o**: anchor_offset, **+w**: width, **+h**:height - F = box - J = projection - R = region From 6d3dc9bb78bc31fb6fb3e30b0bccde6273c6ff32 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Oct 2025 16:33:27 +0800 Subject: [PATCH 16/48] Fix docstrings --- pygmt/src/logo.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 9ef29b08507..cb26be33dec 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -38,7 +38,7 @@ def logo( # noqa: PLR0913 r""" Plot the GMT logo. - .. figure:: https://docs.generic-mapping-tools.org/6.5/_images/GMT_coverlogo.png + .. figure:: https://docs.generic-mapping-tools.org/6.6/_images/GMT_coverlogo.png :alt: GMT logo :align: center :width: 300px @@ -57,8 +57,7 @@ def logo( # noqa: PLR0913 :columns: 1 - D = position/position_type, **+j**: anchor, **+o**: anchor_offset, - **+w**: width, **+h**:height - + **+w**: width, **+h**: height .. hlist:: :columns: 3 From 62e62747af39e20105281225e7c97729798da8ed Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Oct 2025 16:35:14 +0800 Subject: [PATCH 17/48] Fix docstrings --- pygmt/src/logo.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index cb26be33dec..8ff6f7d5d58 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -73,22 +73,23 @@ def logo( # noqa: PLR0913 ---------- position Specify the reference point on the plot for the GMT logo. The method of defining - the reference point is controlled by **position_type**, and the exact location - is set by **position**. + the reference point is controlled by ``position_type``, and the exact location + is set by ``position``. position_type - The **position_type** parameter can take one of the following values: + Specify the type of coordinates used to define the reference point. It can be + one of the following values: - - ``"mapcoords"``: **position** is specified as (*longitude*, *latitude*) in map + - ``"mapcoords"``: ``position`` is specified as (*longitude*, *latitude*) in map coordinates. Example: (120, -45) places the reference point at 120°E, 45°S. - - ``"boxcoords"``: **position** is specified as (*nx*, *ny*) in normalized + - ``"boxcoords"``: ``position`` is specified as (*nx*, *ny*) in normalized coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. Example: (0, 0) corresponds to the lower-left corner, and (1, 1) to the upper-right corner of the plot bounding box. - - ``"plotcoords"``: **position** is specified as (*x*, *y*) in plot coordinates, + - ``"plotcoords"``: ``position`` is specified as (*x*, *y*) in plot coordinates, i.e., distances from the lower-left plot origin given in inches, centimeters, or points. Example: ("1c", "2c") places the reference point 1 cm to the right and 2 cm above the plot origin. - - ``"inside"`` or ``"outside"``: **position** is one of the nine + - ``"inside"`` or ``"outside"``: ``position`` is one of the nine :doc:two-character justification codes , indicating a specific location relative to the plot bounding box. Example: ``"TL"`` places the reference point at the top-left corner, either inside or @@ -106,7 +107,8 @@ def logo( # noqa: PLR0913 Specifies an offset for the anchor point as *offset* or (*offset_x*, *offset_y*). If a single value *offset* is given, both *offset_x* and *offset_y* are set to *offset*. - width/height + width + height Width or height of the GMT logo. Since the aspect ratio is fixed, only one of the two can be specified. box From e3d14d509b00c43569b0d43af4e3b6551a6c56d7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Oct 2025 16:38:54 +0800 Subject: [PATCH 18/48] Fix docstrings --- pygmt/src/logo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 8ff6f7d5d58..ef57964b641 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -97,12 +97,12 @@ def logo( # noqa: PLR0913 anchor Specify the anchor point of the GMT logo, using one of the :doc:`2-character justification codes `. - The default value depends on **position_type**. + The default value depends on ``position_type``. - - ``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). + - ``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 Specifies an offset for the anchor point as *offset* or (*offset_x*, *offset_y*). If a single value *offset* is given, both *offset_x* From e73dc9ed2bcd4dc3656cfda43e277f68927dffbf Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Oct 2025 16:40:22 +0800 Subject: [PATCH 19/48] Fix comments --- pygmt/src/logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index ef57964b641..f0119ec176c 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -147,7 +147,7 @@ def logo( # noqa: PLR0913 # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. # For backward compatibility, we need to check if users pass a string with the GMT # CLI syntax to 'position', i.e., a string starting with one of the leading - # single-letter or contains modifiers with "+". + # single-letter codes or contains modifiers with "+". if isinstance(position, str) and (position[0] in "gnxjJ" or "+" in position): if any(v is not None for v in (anchor, anchor_offset, height, width)): msg = ( From e77993b91e7ae4825a75acd5bca4684320ebf833 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Oct 2025 18:19:21 +0800 Subject: [PATCH 20/48] Fix docstrings --- pygmt/src/logo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index f0119ec176c..379f7507456 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -90,13 +90,13 @@ def logo( # noqa: PLR0913 or points. Example: ("1c", "2c") places the reference point 1 cm to the right and 2 cm above the plot origin. - ``"inside"`` or ``"outside"``: ``position`` is one of the nine - :doc:two-character justification codes , + :doc:two-character justification codes , indicating a specific location relative to the plot bounding box. Example: ``"TL"`` places the reference point at the top-left corner, either inside or outside the bounding box. anchor Specify the anchor point of the GMT logo, using one of the - :doc:`2-character justification codes `. + :doc:`2-character justification codes `. The default value depends on ``position_type``. - ``position_type="inside"``: ``anchor`` defaults to the same as ``position``. From 66f9c4d9ab31475987b3dd99fc31c1376c660053 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Oct 2025 18:37:55 +0800 Subject: [PATCH 21/48] Fix docstrings --- pygmt/src/logo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 379f7507456..c10e21bba15 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -90,13 +90,13 @@ def logo( # noqa: PLR0913 or points. Example: ("1c", "2c") places the reference point 1 cm to the right and 2 cm above the plot origin. - ``"inside"`` or ``"outside"``: ``position`` is one of the nine - :doc:two-character justification codes , + :doc:`two-character justification codes `, indicating a specific location relative to the plot bounding box. Example: ``"TL"`` places the reference point at the top-left corner, either inside or outside the bounding box. anchor Specify the anchor point of the GMT logo, using one of the - :doc:`2-character justification codes `. + :doc:`2-character justification codes `. The default value depends on ``position_type``. - ``position_type="inside"``: ``anchor`` defaults to the same as ``position``. From fb9547881b00f3c475963f8451d857bd2796fa45 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 18 Oct 2025 16:10:45 +0800 Subject: [PATCH 22/48] Fix offset to anchor_offset in the Figure.inset doctest --- pygmt/src/inset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/inset.py b/pygmt/src/inset.py index 749875c7728..48cc56084b6 100644 --- a/pygmt/src/inset.py +++ b/pygmt/src/inset.py @@ -127,7 +127,7 @@ def inset( ... ) ... >>> # Map elements outside the "with" statement are plotted in the main figure - >>> fig.logo(position="BR", position_type="inside", offset=0.2, width="3c") + >>> fig.logo(position="BR", position_type="inside", anchor_offset=0.2, width="3c") >>> fig.show() """ self._activate_figure() From 51fe97657053a464ba330728db83c973e68dd7cc Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 21 Oct 2025 17:00:12 +0800 Subject: [PATCH 23/48] Improve the handling of deprecation position parameter --- pygmt/src/logo.py | 49 +++++++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index c10e21bba15..b6f146f0c48 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -130,37 +130,48 @@ def logo( # noqa: PLR0913 """ self._activate_figure() + # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. For backward + # compatibility, need to check if users pass a string with the GMT CLI syntax to + # 'position', i.e., a string starting with one of the codes "g", "n", "x", "j", "J", + # or contains modifiers with "+". + _is_deprecated_position = isinstance(position, str) and ( + position.startswith(("g", "n", "x", "j", "J")) or "+" in position + ) + + if _is_deprecated_position and any( + v is not None for v in (anchor, anchor_offset, height, width) + ): + msg = ( + "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " + "with parameters 'anchor', 'anchor_offset', 'height', and 'width'. " + "Please refer to the documentation for the recommended usage." + ) + raise GMTInvalidInput(msg) + # width and height are mutually exclusive. if width is not None and height is not None: msg = "Cannot specify both width and height." raise GMTInvalidInput(msg) - # Mapping position_type to GMT single-letter code. - _position_type = { + _position_types = { "mapcoords": "g", "boxcoords": "n", "plotcoords": "x", "inside": "j", "outside": "J", - }[position_type] - - # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. - # For backward compatibility, we need to check if users pass a string with the GMT - # CLI syntax to 'position', i.e., a string starting with one of the leading - # single-letter codes or contains modifiers with "+". - if isinstance(position, str) and (position[0] in "gnxjJ" or "+" in position): - if any(v is not None for v in (anchor, anchor_offset, height, width)): - msg = ( - "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " - "with other parameters (anchor, anchor_offset, height, width). " - "Please refer to the documentation for the recommended usage." - ) - raise GMTInvalidInput(msg) - _position_type = "" # Unset _position_type to an empty string. + } aliasdict = AliasSystem( - D=[ - Alias(position, name="position", sep="/", size=2, prefix=_position_type), + D=Alias(position, name="position") + if _is_deprecated_position + else [ + Alias( + position, + name="position", + sep="/", + size=2, + prefix=_position_types[position_type], + ), Alias(anchor, name="anchor", prefix="+j"), Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), Alias(height, name="height", prefix="+h"), From 573e950c96aadf9056130cc01a6439319b7def63 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 12 Nov 2025 13:25:43 +0800 Subject: [PATCH 24/48] Fix docstrings in tests --- pygmt/tests/test_logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 2d39c52c534..6ebf7943655 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -38,7 +38,7 @@ def test_logo_on_a_map(): @pytest.mark.mpl_image_compare def test_logo_position_type(): """ - Test that the new group of parameters works as expected. + Test that different position types work as expected. """ fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) From 173e7a4ad74ead3de1f4ff9a5b105ec592a97e1d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 12 Nov 2025 13:29:53 +0800 Subject: [PATCH 25/48] Shorten the docstrings --- pygmt/src/logo.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 5d3f5ebf154..08dfd6430f3 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -80,24 +80,22 @@ def logo( # noqa: PLR0913 one of the following values: - ``"mapcoords"``: ``position`` is specified as (*longitude*, *latitude*) in map - coordinates. Example: (120, -45) places the reference point at 120°E, 45°S. + coordinates. - ``"boxcoords"``: ``position`` is specified as (*nx*, *ny*) in normalized coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. - Example: (0, 0) corresponds to the lower-left corner, and (1, 1) to the - upper-right corner of the plot bounding box. - ``"plotcoords"``: ``position`` is specified as (*x*, *y*) in plot coordinates, i.e., distances from the lower-left plot origin given in inches, centimeters, - or points. Example: ("1c", "2c") places the reference point 1 cm to the right - and 2 cm above the plot origin. + or points. - ``"inside"`` or ``"outside"``: ``position`` is one of the nine :doc:`two-character justification codes `, - indicating a specific location relative to the plot bounding box. Example: - ``"TL"`` places the reference point at the top-left corner, either inside or - outside the bounding box. + indicating a specific location relative to the plot bounding box. + + Refer to :doc:`/techref/reference_anchor_points` for details about the + positioning. anchor Specify the anchor point of the GMT logo, using one of the - :doc:`2-character justification codes `. - The default value depends on ``position_type``. + :doc:`2-character justification codes `. The + default value depends on ``position_type``. - ``position_type="inside"``: ``anchor`` defaults to the same as ``position``. - ``position_type="outside"``: ``anchor`` defaults to the mirror opposite of From eec0fb74bd7e16dad6ec769ef64e50f532a74f97 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 16 Nov 2025 11:56:45 +0800 Subject: [PATCH 26/48] Initial implemention of the Position class --- pygmt/params/__init__.py | 1 + pygmt/params/position.py | 42 ++++++++++++++++++++++++++++++++++++++++ pygmt/src/logo.py | 20 +++++++++++++++---- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 pygmt/params/position.py diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py index b80b921407a..d1a00a7f5f2 100644 --- a/pygmt/params/__init__.py +++ b/pygmt/params/__init__.py @@ -4,3 +4,4 @@ from pygmt.params.box import Box from pygmt.params.pattern import Pattern +from pygmt.params.position import Position diff --git a/pygmt/params/position.py b/pygmt/params/position.py new file mode 100644 index 00000000000..060c2bed2a4 --- /dev/null +++ b/pygmt/params/position.py @@ -0,0 +1,42 @@ +""" +The Position class for positioning GMT embellishments. +""" + +import dataclasses +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.alias import Alias +from pygmt.params.base import BaseParam + + +@dataclasses.dataclass(repr=False) +class Position(BaseParam): + """ + The class for positioning GMT embellishments. + """ + + location: str | tuple[float | str, float | str] + type: Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] + anchor: AnchorCode + offset: Sequence[float | str] + + @property + def _aliases(self): + return [ + Alias( + self.type, + name="type", + mapping={ + "mapcoords": "g", + "boxcoords": "n", + "plotcoords": "x", + "inside": "j", + "outside": "J", + }, + ), + Alias(self.location, name="location", sep="/", size=2), + Alias(self.anchor, name="anchor"), + Alias(self.offset, name="offset", sep="/", size=2), + ] diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index defdc065eb3..668dc0a7ca2 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -7,14 +7,16 @@ 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 Box +from pygmt.helpers import build_arg_list, fmt_docstring +from pygmt.params import Box, Position @fmt_docstring -@use_alias(D="position") def logo( self, + position: Position, + width: float | str | None = None, + height: float | str | None = None, projection: str | None = None, region: Sequence[float | str] | str | None = None, style: Literal["standard", "url", "no_label"] = "standard", @@ -36,7 +38,12 @@ def logo( Full GMT docs at :gmt-docs:`gmtlogo.html`. - {aliases} + **Aliases:** + + .. hlist:: + :columns: 3 + + - D = position, **+w**: width, **+h**: height - F = box - J = projection - R = region @@ -73,6 +80,11 @@ def logo( self._activate_figure() aliasdict = AliasSystem( + D=[ + Alias(position, name="position"), + Alias(width, name="width", prefix="+w"), + Alias(height, name="height", prefix="+h"), + ], F=Alias(box, name="box"), S=Alias( style, name="style", mapping={"standard": "l", "url": "u", "no_label": "n"} From 539f66f25f97c4a4d3e3418a9bb9173d1047484b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 20 Nov 2025 16:14:34 +0800 Subject: [PATCH 27/48] Fix styling --- pygmt/src/logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 668dc0a7ca2..ab66ff3366f 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -12,7 +12,7 @@ @fmt_docstring -def logo( +def logo( # noqa: PLR0913 self, position: Position, width: float | str | None = None, From 97f015f040a0aab7bee4f58beb104557b98d7365 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 23 Nov 2025 16:45:43 +0800 Subject: [PATCH 28/48] Add tests and improve docstrings --- pygmt/params/position.py | 40 +++++++++++++++++++++++++---- pygmt/tests/test_params_position.py | 30 ++++++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 pygmt/tests/test_params_position.py diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 060c2bed2a4..4353841424f 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -17,10 +17,40 @@ class Position(BaseParam): The class for positioning GMT embellishments. """ - location: str | tuple[float | str, float | str] + #: Specify the reference point on the plot. The method of defining the reference + #: point is controlled by ``type``, and the exact location is set by ``position``. + location: Sequence[float | str] | AnchorCode + + #: Specify the type of coordinates used to define the reference point. It can be + #: one of the following values: + #: + #: - ``"mapcoords"``: ``position`` is specified as (*longitude*, *latitude*) in map + #: coordinates. + #: - ``"boxcoords"``: ``position`` is specified as (*nx*, *ny*) in normalized + #: coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. + #: - ``"plotcoords"``: ``position`` is specified as (*x*, *y*) in plot coordinates, + #: i.e., distances from the lower-left plot origin given in inches, centimeters, + #: or points. + #: - ``"inside"`` or ``"outside"``: ``position`` is one of the nine + #: :doc:`two-character justification codes `, + #: indicating a specific location relative to the plot bounding box. + #: type: Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] - anchor: AnchorCode - offset: Sequence[float | str] + + #: Specify the anchor point of the GMT logo, using one of the + #: :doc:`2-character justification codes `. The + #: default value depends on ``position_type``. + #: + #: - ``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: AnchorCode | None = None + + #: Specifies an offset for the anchor point as *offset* or (*offset_x*, *offset_y*). + #: If a single value *offset* is given, both *offset_x* and *offset_y* are set to + #: *offset*. + offset: Sequence[float | str] | None = None @property def _aliases(self): @@ -37,6 +67,6 @@ def _aliases(self): }, ), Alias(self.location, name="location", sep="/", size=2), - Alias(self.anchor, name="anchor"), - Alias(self.offset, name="offset", sep="/", size=2), + Alias(self.anchor, name="anchor", prefix="+j"), + Alias(self.offset, name="offset", prefix="+o", sep="/", size=2), ] diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py new file mode 100644 index 00000000000..8999f2822c4 --- /dev/null +++ b/pygmt/tests/test_params_position.py @@ -0,0 +1,30 @@ +""" +Test the Position class. +""" + +from pygmt.params import Position + + +def test_params_position_types(): + """ + Test the Position class with different types of coordinate systems. + """ + assert str(Position(location=(10, 20), type="mapcoords")) == "g10/20" + assert str(Position(location=(0.1, 0.2), type="boxcoords")) == "n0.1/0.2" + assert str(Position(location=("5c", "3c"), type="plotcoords")) == "x5c/3c" + assert str(Position(location="TL", type="inside")) == "jTL" + assert str(Position(location="BR", type="outside")) == "JBR" + + +def test_params_position_anchor_offset(): + """ + Test the Position class with anchor and offset parameters. + """ + pos = Position(location=(10, 20), type="mapcoords", anchor="TL") + assert str(pos) == "g10/20+jTL" + + pos = Position(location=(10, 20), type="mapcoords", offset=(1, 2)) + assert str(pos) == "g10/20+o1/2" + + pos = Position(location="TL", type="inside", anchor="MC", offset=("1c", "2c")) + assert str(pos) == "jTL+jMC+o1c/2c" From 854804ef0f992ee6e291e9a4a5647a53d0fdac2f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 23 Nov 2025 16:46:28 +0800 Subject: [PATCH 29/48] Add to API doc --- doc/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 3656bba286e..264f5a9175a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -214,6 +214,7 @@ Class-style Parameters Box Pattern + Position Enums ----- From 6b55dde70d63ab6d865bb3565adddea41c4e7bf4 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 23 Nov 2025 16:48:38 +0800 Subject: [PATCH 30/48] Add an inline doctest --- pygmt/params/position.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 4353841424f..5858dd9103a 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -15,6 +15,20 @@ class Position(BaseParam): """ The class for positioning GMT embellishments. + + Example + ------- + >>> import pygmt + >>> from pygmt.params import Position + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) + >>> fig.logo( + ... position=Position( + ... location=(3, 3), type="mapcoords", anchor="ML", offset=(0.2, 0.2) + ... ), + ... box=True, + ... ) + >>> fig.show() """ #: Specify the reference point on the plot. The method of defining the reference From 3d629cb52a0e7bb05968a8e0f7f2411289172a52 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 23 Nov 2025 19:27:50 +0800 Subject: [PATCH 31/48] position is not required --- pygmt/src/logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index ab66ff3366f..d98d4f3fb2f 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -14,7 +14,7 @@ @fmt_docstring def logo( # noqa: PLR0913 self, - position: Position, + position: Position | None = None, width: float | str | None = None, height: float | str | None = None, projection: str | None = None, From 576b822e51518096e8009cb2b0c20f99d45481a4 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 23 Nov 2025 19:33:58 +0800 Subject: [PATCH 32/48] Default to plotcoords --- pygmt/params/position.py | 5 ++++- pygmt/tests/test_params_position.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 5858dd9103a..6669a5dcb0a 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -49,7 +49,10 @@ class Position(BaseParam): #: :doc:`two-character justification codes `, #: indicating a specific location relative to the plot bounding box. #: - type: Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] + #: The default value is ``"plotcoords"``. + type: Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] = ( + "plotcoords" + ) #: Specify the anchor point of the GMT logo, using one of the #: :doc:`2-character justification codes `. The diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 8999f2822c4..dbcf76b501e 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -9,6 +9,7 @@ def test_params_position_types(): """ Test the Position class with different types of coordinate systems. """ + assert str(Position(location=(10, 20))) == "x10/20" assert str(Position(location=(10, 20), type="mapcoords")) == "g10/20" assert str(Position(location=(0.1, 0.2), type="boxcoords")) == "n0.1/0.2" assert str(Position(location=("5c", "3c"), type="plotcoords")) == "x5c/3c" From f54bec989be2f53a938388947984c842c917813a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 23 Nov 2025 19:35:53 +0800 Subject: [PATCH 33/48] Updates --- pygmt/params/position.py | 4 +--- pygmt/tests/test_params_position.py | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 6669a5dcb0a..2f12d5fdb60 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -23,9 +23,7 @@ class Position(BaseParam): >>> fig = pygmt.Figure() >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) >>> fig.logo( - ... position=Position( - ... location=(3, 3), type="mapcoords", anchor="ML", offset=(0.2, 0.2) - ... ), + ... position=Position((3, 3), type="mapcoords", anchor="ML", offset=(0.2, 0.2)), ... box=True, ... ) >>> fig.show() diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index dbcf76b501e..2ca05cf2150 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -9,7 +9,8 @@ def test_params_position_types(): """ Test the Position class with different types of coordinate systems. """ - assert str(Position(location=(10, 20))) == "x10/20" + assert str(Position((1, 2))) == "x1/2" + assert str(Position(location=(1, 2))) == "x1/2" assert str(Position(location=(10, 20), type="mapcoords")) == "g10/20" assert str(Position(location=(0.1, 0.2), type="boxcoords")) == "n0.1/0.2" assert str(Position(location=("5c", "3c"), type="plotcoords")) == "x5c/3c" From 2c59b7f1fd0c024c21bd95a1b0ccf379c1fd2dc5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 24 Nov 2025 12:59:23 +0800 Subject: [PATCH 34/48] Improve the checking in Figure.logo --- pygmt/src/logo.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index d98d4f3fb2f..8d0e98699d4 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -7,6 +7,7 @@ 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 @@ -79,6 +80,19 @@ def logo( # noqa: PLR0913 """ self._activate_figure() + if isinstance(position, str) and any(v is not None for v in (width, height)): + msg = ( + "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " + "with parameters 'height', and 'width'. Please refer to the documentation " + "for the recommended usage." + ) + raise GMTInvalidInput(msg) + + # width and height are mutually exclusive. + if width is not None and height is not None: + msg = "Cannot specify both width and height." + raise GMTInvalidInput(msg) + aliasdict = AliasSystem( D=[ Alias(position, name="position"), From fe18c87297892681543c460e0fb395a2e4612667 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 24 Nov 2025 18:50:13 +0800 Subject: [PATCH 35/48] Improve docstrings --- pygmt/params/position.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 2f12d5fdb60..626baea27fe 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -29,21 +29,21 @@ class Position(BaseParam): >>> fig.show() """ - #: Specify the reference point on the plot. The method of defining the reference - #: point is controlled by ``type``, and the exact location is set by ``position``. + #: Location of the reference point on the plot. Its meaning depends on the value of + #: ``type``. location: Sequence[float | str] | AnchorCode - #: Specify the type of coordinates used to define the reference point. It can be - #: one of the following values: + #: The coordinates used to define the reference point. Valid values and meanings for + #: corresponding ``location`` are: #: - #: - ``"mapcoords"``: ``position`` is specified as (*longitude*, *latitude*) in map + #: - ``"mapcoords"``: ``location`` is specified as (*longitude*, *latitude*) in map #: coordinates. - #: - ``"boxcoords"``: ``position`` is specified as (*nx*, *ny*) in normalized + #: - ``"boxcoords"``: ``location`` is specified as (*nx*, *ny*) in normalized #: coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. - #: - ``"plotcoords"``: ``position`` is specified as (*x*, *y*) in plot coordinates, + #: - ``"plotcoords"``: ``location`` is specified as (*x*, *y*) in plot coordinates, #: i.e., distances from the lower-left plot origin given in inches, centimeters, #: or points. - #: - ``"inside"`` or ``"outside"``: ``position`` is one of the nine + #: - ``"inside"`` or ``"outside"``: ``location`` is one of the nine #: :doc:`two-character justification codes `, #: indicating a specific location relative to the plot bounding box. #: @@ -52,19 +52,19 @@ class Position(BaseParam): "plotcoords" ) - #: Specify the anchor point of the GMT logo, using one of the + #: Anchor point of the embellishment, using one of the #: :doc:`2-character justification codes `. The - #: default value depends on ``position_type``. + #: default value depends on ``type``. #: - #: - ``position_type="inside"``: ``anchor`` defaults to the same as ``position``. - #: - ``position_type="outside"``: ``anchor`` defaults to the mirror opposite of - #: ``position``. + #: - ``type="inside"``: ``anchor`` defaults to the same as ``location``. + #: - ``type="outside"``: ``anchor`` defaults to the mirror opposite of ``location``. #: - Otherwise, ``anchor`` defaults to ``"MC"`` (middle center). anchor: AnchorCode | None = None - #: Specifies an offset for the anchor point as *offset* or (*offset_x*, *offset_y*). - #: If a single value *offset* is given, both *offset_x* and *offset_y* are set to - #: *offset*. + #: Offset for the anchor point. It can be either a single value *offset* or a pair + #: (*offset_x*, *offset_y*), where *offset_x* and *offset_y* are the offsets in the + #: x- and y-directions, respectively. If a single value *offset* is given, both + #: *offset_x* and *offset_y* are set to *offset*. offset: Sequence[float | str] | None = None @property From 038161b0bce59484629fc6d92b4df9cdc25e8ddc Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 24 Nov 2025 20:14:56 +0800 Subject: [PATCH 36/48] Improve docstrings --- pygmt/params/position.py | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 626baea27fe..218f83bd6e6 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -16,6 +16,53 @@ class Position(BaseParam): """ The class for positioning GMT embellishments. + .. figure:: https://github.com/user-attachments/assets/0f3e9b39-7d64-4628-8acb-58fe74ff6fa5 + :width: 400 px + + Positioning of GMT embellishment using the :class:`pygmt.params.Position` class. + + Placing an embellishment on the plot means selecting a *reference point* somewhere + on the plot, an *anchor point* somewhere on the embellishment, and then positioning + the embellishment so that the two points overlap. It may be helpful to consider the + analog of a boat dropping an anchor: The boat navigates to the *reference point* and + then, depending on where on the boat the *anchor* is located, moves so that the + *anchor* connection point overlies the *reference point*, then drops the *anchor*. + + There are five different ways to specify the *reference point* on a map, controlled + by the ``type`` and ``location`` attributes of this class, for complete freedom to + select any location inside or outside the map. + + ``type="mapcoords"`` + Specify the *reference point* using data coordinates. ``location`` is given as + (*longitude*, *latitude*). This mechanism is useful when you want to tie the + location of the embellishment to an actual point best described by data + coordinates. Example: ``location=(135, 20), type="mapcoords"``. + ``type="plotcoords"`` + Specify the *reference point* using plot coordinates, i.e., the distances in + inches, centimeters, or points from the lower left plot origin. This mechanism + is preferred when you wish to lay out an embellishment using familiar + measurements of distance from origins. Example: + ``location=("2c", "2.5c"), type="plotcoords"``. + ``type="boxcoords"`` + Specify the *reference point* using normalized coordinates, i.e., fractional + coordinates between 0 and 1 in both the x and y directions. This mechanism + avoids units and is useful if you want to always place embellishments at + locations best referenced as fractions of the plot dimensions. Example: + ``location=(0.2, 0.1), type="boxcoords"``. + ``type="inside"`` + Specify the *reference point* using one of the nine justification codes. This + mechanism is preferred when you just want to place the embellishment inside the + basemap at one of the corners or centered at one of the sides (or even smack in + the middle). Example: ``location="TL", type="inside"``. When used, the anchor + point on the map embellishments will default to the same justification, i.e., + ``"TL"`` in this example. + ``type="outside"`` + Same ``type="inside"`` except it implies that the default anchor point is the + mirror opposite of the justification code. Thus, when using + ``location="TL", type="outside"``, the anchor point on the map embellishment + will default to ``"BR"``. This is practical for embellishments that are drawn + outside of the basemap (like color bars often are). + Example ------- >>> import pygmt From a6e75bcb1512a06eca66cc93fe45a41ecb4bb075 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 08:26:15 +0800 Subject: [PATCH 37/48] Improve docstrings --- pygmt/params/position.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 218f83bd6e6..3789293a51f 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -63,6 +63,26 @@ class Position(BaseParam): will default to ``"BR"``. This is practical for embellishments that are drawn outside of the basemap (like color bars often are). + While the reference point selection gives unlimited flexibility to pick any point + inside or outside the map region, the anchor point selection is limited to the nine + justification points. Set ``anchor`` to indicate which justification point on the + map feature should be co-registered with the chosen reference point. If an anchor + point is not specified then it defaults to the justification point set for the + reference point (for ``type="inside"``), or to the mirror + opposite of the reference point (for ``type="outside"``); with all other + specifications of the reference point, the anchor point takes on the default value + of ``MC`` (for map rose and map scale) or ``BL`` (all other map features). Setting + ``anchor`` overrules those defaults. For instance, ``anchor="TR"`` would select the + top right point on the map feature as the anchor. + + It is likely that you will wish to offset the anchor point away from your selection + by some arbitrary amount, particularly if the reference point is specified with + ``type="inside"`` or ``type="outside"``. This can be done by setting ``offset``. + These increments are added to the projected plot coordinates of the anchor point, + with positive values moving the reference point in the same direction as the + 2-character code of the anchor point implies. Finally, the adjusted anchor point is + matched with the reference point. + Example ------- >>> import pygmt From 3ec8c0686f7251f6dce05679f3cc614c3a6d026f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 13:22:23 +0800 Subject: [PATCH 38/48] Improve docstrings --- pygmt/params/position.py | 41 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 3789293a51f..8ba585a261c 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -28,9 +28,9 @@ class Position(BaseParam): then, depending on where on the boat the *anchor* is located, moves so that the *anchor* connection point overlies the *reference point*, then drops the *anchor*. - There are five different ways to specify the *reference point* on a map, controlled + There are five different ways to specify the *reference point* on a plot, controlled by the ``type`` and ``location`` attributes of this class, for complete freedom to - select any location inside or outside the map. + select any location inside or outside the plot. ``type="mapcoords"`` Specify the *reference point* using data coordinates. ``location`` is given as @@ -50,30 +50,31 @@ class Position(BaseParam): locations best referenced as fractions of the plot dimensions. Example: ``location=(0.2, 0.1), type="boxcoords"``. ``type="inside"`` - Specify the *reference point* using one of the nine justification codes. This - mechanism is preferred when you just want to place the embellishment inside the - basemap at one of the corners or centered at one of the sides (or even smack in - the middle). Example: ``location="TL", type="inside"``. When used, the anchor - point on the map embellishments will default to the same justification, i.e., - ``"TL"`` in this example. + Specify the *reference point* using one of the nine + :doc:`justification codes `. This mechanism is + preferred when you just want to place the embellishment inside the basemap at + one of the corners or centered at one of the sides (or even smack in the + middle). Example: ``location="TL", type="inside"``. When used, the anchor point + on the embellishments will default to the same justification, i.e., ``"TL"`` in + this example. ``type="outside"`` Same ``type="inside"`` except it implies that the default anchor point is the mirror opposite of the justification code. Thus, when using - ``location="TL", type="outside"``, the anchor point on the map embellishment - will default to ``"BR"``. This is practical for embellishments that are drawn - outside of the basemap (like color bars often are). + ``location="TL", type="outside"``, the anchor point on the embellishment will + default to ``"BR"``. This is practical for embellishments that are drawn outside + of the basemap (like color bars often are). - While the reference point selection gives unlimited flexibility to pick any point + While the *reference point* selection gives unlimited flexibility to pick any point inside or outside the map region, the anchor point selection is limited to the nine justification points. Set ``anchor`` to indicate which justification point on the - map feature should be co-registered with the chosen reference point. If an anchor - point is not specified then it defaults to the justification point set for the - reference point (for ``type="inside"``), or to the mirror - opposite of the reference point (for ``type="outside"``); with all other - specifications of the reference point, the anchor point takes on the default value - of ``MC`` (for map rose and map scale) or ``BL`` (all other map features). Setting - ``anchor`` overrules those defaults. For instance, ``anchor="TR"`` would select the - top right point on the map feature as the anchor. + map embellishment should be co-registered with the chosen reference point. If an + anchor point is not specified then it defaults to the justification point set for + the reference point (for ``type="inside"``), or to the mirror opposite of the + reference point (for ``type="outside"``); with all other specifications of the + reference point, the anchor point takes on the default value of ``MC`` (for map + rose and map scale) or ``BL`` (all other map embellishments). Setting ``anchor`` + overrules those defaults. For instance, ``anchor="TR"`` would select the top right + point on the map embellishment as the anchor. It is likely that you will wish to offset the anchor point away from your selection by some arbitrary amount, particularly if the reference point is specified with From 339ce004c9daab8208d445dcfe9fbd24278e26fa Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 15:38:25 +0800 Subject: [PATCH 39/48] Improve docstrings --- pygmt/params/position.py | 202 +++++++++++++++++++++------------------ 1 file changed, 107 insertions(+), 95 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 8ba585a261c..6dea68311e0 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -14,78 +14,94 @@ @dataclasses.dataclass(repr=False) class Position(BaseParam): """ - The class for positioning GMT embellishments. + Class for positioning embellishments on a plot. .. figure:: https://github.com/user-attachments/assets/0f3e9b39-7d64-4628-8acb-58fe74ff6fa5 :width: 400 px - Positioning of GMT embellishment using the :class:`pygmt.params.Position` class. - - Placing an embellishment on the plot means selecting a *reference point* somewhere - on the plot, an *anchor point* somewhere on the embellishment, and then positioning - the embellishment so that the two points overlap. It may be helpful to consider the - analog of a boat dropping an anchor: The boat navigates to the *reference point* and - then, depending on where on the boat the *anchor* is located, moves so that the - *anchor* connection point overlies the *reference point*, then drops the *anchor*. - - There are five different ways to specify the *reference point* on a plot, controlled - by the ``type`` and ``location`` attributes of this class, for complete freedom to - select any location inside or outside the plot. - - ``type="mapcoords"`` - Specify the *reference point* using data coordinates. ``location`` is given as - (*longitude*, *latitude*). This mechanism is useful when you want to tie the - location of the embellishment to an actual point best described by data - coordinates. Example: ``location=(135, 20), type="mapcoords"``. - ``type="plotcoords"`` - Specify the *reference point* using plot coordinates, i.e., the distances in - inches, centimeters, or points from the lower left plot origin. This mechanism - is preferred when you wish to lay out an embellishment using familiar - measurements of distance from origins. Example: - ``location=("2c", "2.5c"), type="plotcoords"``. - ``type="boxcoords"`` - Specify the *reference point* using normalized coordinates, i.e., fractional - coordinates between 0 and 1 in both the x and y directions. This mechanism - avoids units and is useful if you want to always place embellishments at - locations best referenced as fractions of the plot dimensions. Example: - ``location=(0.2, 0.1), type="boxcoords"``. - ``type="inside"`` - Specify the *reference point* using one of the nine - :doc:`justification codes `. This mechanism is - preferred when you just want to place the embellishment inside the basemap at - one of the corners or centered at one of the sides (or even smack in the - middle). Example: ``location="TL", type="inside"``. When used, the anchor point - on the embellishments will default to the same justification, i.e., ``"TL"`` in - this example. - ``type="outside"`` - Same ``type="inside"`` except it implies that the default anchor point is the - mirror opposite of the justification code. Thus, when using - ``location="TL", type="outside"``, the anchor point on the embellishment will - default to ``"BR"``. This is practical for embellishments that are drawn outside - of the basemap (like color bars often are). - - While the *reference point* selection gives unlimited flexibility to pick any point - inside or outside the map region, the anchor point selection is limited to the nine - justification points. Set ``anchor`` to indicate which justification point on the - map embellishment should be co-registered with the chosen reference point. If an - anchor point is not specified then it defaults to the justification point set for - the reference point (for ``type="inside"``), or to the mirror opposite of the - reference point (for ``type="outside"``); with all other specifications of the - reference point, the anchor point takes on the default value of ``MC`` (for map - rose and map scale) or ``BL`` (all other map embellishments). Setting ``anchor`` - overrules those defaults. For instance, ``anchor="TR"`` would select the top right - point on the map embellishment as the anchor. - - It is likely that you will wish to offset the anchor point away from your selection - by some arbitrary amount, particularly if the reference point is specified with - ``type="inside"`` or ``type="outside"``. This can be done by setting ``offset``. - These increments are added to the projected plot coordinates of the anchor point, - with positive values moving the reference point in the same direction as the - 2-character code of the anchor point implies. Finally, the adjusted anchor point is - matched with the reference point. - - Example - ------- + Positioning of GMT embellishment. + + This class provides flexible positioning for GMT embellishments (e.g., logo, scale, + rose) by defining a *reference point* on the plot and an *anchor point* on the + embellishment. The embellishment is positioned so these two points overlap. + + **Conceptual Model** + + Think of it like dropping an anchor from a boat: + + 1. The boat navigates to the *reference point* (a location on the plot) + 2. The *anchor point* (a specific point on the embellishment) is aligned with the + *reference point* + 3. The embellishment is "dropped" at that position + + **Reference Point Types** + + The reference point can be specified in five different ways using the ``type`` and + ``location`` attributes: + + **type="mapcoords"** (Map Coordinates) + Use data/geographic coordinates. Set ``location`` as (*longitude*, *latitude*). + Useful when tying the embellishment to a specific geographic location. + + Example: ``location=(135, 20), type="mapcoords"``. + + **type="plotcoords"** (Plot Coordinates) + Use plot coordinates as distances from the lower-left plot origin. Specify + ``location`` as (*x*, *y*) with units (e.g., inches, centimeters, points). + Useful for precise layout control. + + Example: ``location=("2c", "2.5c"), type="plotcoords"`` + + **type="boxcoords"** (Normalized Coordinates) + Use normalized coordinates where (0, 0) is the lower-left corner and (1, 1) is + the upper-right corner. Set ``location`` as (*nx*, *ny*) with values between + 0 and 1. Useful for positioning relative to plot dimensions without units. + + Example: ``location=(0.2, 0.1), type="boxcoords"`` + + **type="inside"** (Inside Plot) + Use a :doc:`justification code ` (e.g., ``"TL"``) + to place the embellishment inside the plot. Set ``location`` to one of the nine + 2-character codes. + + Example: ``location="TL", type="inside"`` + + **type="outside"** (Outside Plot) + Similar to ``type="inside"``, but the anchor point defaults to the mirror + opposite of the justification code. Useful for placing embellishments outside + the plot boundaries (e.g., color bars). + + Example: ``location="TL", type="outside"`` + + **Anchor Point** + + The anchor point determines which part of the embellishment aligns with the + reference point. It uses one of nine + :doc:`justification codes `. + + Set ``anchor`` explicitly to override these defaults. If not set, the default + anchor behaviors are: + + - For ``type="inside"``: Same as the reference point justification + - For ``type="outside"``: Mirror opposite of the reference point justification + - For other types: ``"MC"`` (middle center) for map rose and scale, ``"BL"`` + (bottom-left) for other embellishments + + Example: ``anchor="TR"`` selects the top-right point of the embellishment. + + **Offset** + + The ``offset`` parameter shifts the anchor point from its default position. Offsets + are applied to the projected plot coordinates, with positive values moving in the + direction indicated by the anchor point's justification code. + + Specify as a single value (applied to both x and y) or as (*offset_x*, *offset_y*). + + Examples + -------- + Position a logo at map coordinates (3, 3) with the logo's middle-left point as the + anchor, offset by (0.2, 0.2): + >>> import pygmt >>> from pygmt.params import Position >>> fig = pygmt.Figure() @@ -95,44 +111,40 @@ class Position(BaseParam): ... box=True, ... ) >>> fig.show() + + Position an embellishment at the top-left corner inside the plot: + + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) + >>> fig.logo(position=Position("TL", type="inside", offset="0.2c"), box=True) + >>> fig.show() """ - #: Location of the reference point on the plot. Its meaning depends on the value of - #: ``type``. + #: Location of the reference point on the plot. The format depends on ``type``: + #: + #: - ``type="mapcoords"``: (*longitude*, *latitude*) + #: - ``type="plotcoords"``: (*x*, *y*) with units (e.g., ``"2c"``) + #: - ``type="boxcoords"``: (*nx*, *ny*) with values between 0 and 1 + #: - ``type="inside"`` or ``"outside"``: 2-character justification code location: Sequence[float | str] | AnchorCode - #: The coordinates used to define the reference point. Valid values and meanings for - #: corresponding ``location`` are: + #: Coordinate system for the reference point. Valid values are: #: - #: - ``"mapcoords"``: ``location`` is specified as (*longitude*, *latitude*) in map - #: coordinates. - #: - ``"boxcoords"``: ``location`` is specified as (*nx*, *ny*) in normalized - #: coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. - #: - ``"plotcoords"``: ``location`` is specified as (*x*, *y*) in plot coordinates, - #: i.e., distances from the lower-left plot origin given in inches, centimeters, - #: or points. - #: - ``"inside"`` or ``"outside"``: ``location`` is one of the nine - #: :doc:`two-character justification codes `, - #: indicating a specific location relative to the plot bounding box. - #: - #: The default value is ``"plotcoords"``. + #: - ``"mapcoords"``: Map/Data coordinates + #: - ``"plotcoords"``: Plot coordinates + #: - ``"boxcoords"``: Normalized coordinates + #: - ``"inside"`` or ``"outside"``: Justification codes type: Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] = ( "plotcoords" ) - #: Anchor point of the embellishment, using one of the - #: :doc:`2-character justification codes `. The - #: default value depends on ``type``. - #: - #: - ``type="inside"``: ``anchor`` defaults to the same as ``location``. - #: - ``type="outside"``: ``anchor`` defaults to the mirror opposite of ``location``. - #: - Otherwise, ``anchor`` defaults to ``"MC"`` (middle center). + #: Anchor point on the embellishment using a + #: :doc:`2-character justification codes `. + #: If ``None``, defaults are applied based on ``type`` (see above). anchor: AnchorCode | None = None - #: Offset for the anchor point. It can be either a single value *offset* or a pair - #: (*offset_x*, *offset_y*), where *offset_x* and *offset_y* are the offsets in the - #: x- and y-directions, respectively. If a single value *offset* is given, both - #: *offset_x* and *offset_y* are set to *offset*. + #: Offset for the anchor point as a single value or (*offset_x*, *offset_y*). + #: If a single value is given, the offset is applied to both x and y directions. offset: Sequence[float | str] | None = None @property From 4d616de507fdd4e9f0b1d331f1b626bac3981960 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 16:19:24 +0800 Subject: [PATCH 40/48] Revert changes in logo.py --- pygmt/src/logo.py | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 8d0e98699d4..defdc065eb3 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -7,17 +7,14 @@ 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.helpers import build_arg_list, fmt_docstring, use_alias +from pygmt.params import Box @fmt_docstring -def logo( # noqa: PLR0913 +@use_alias(D="position") +def logo( self, - position: Position | None = None, - width: float | str | None = None, - height: float | str | None = None, projection: str | None = None, region: Sequence[float | str] | str | None = None, style: Literal["standard", "url", "no_label"] = "standard", @@ -39,12 +36,7 @@ def logo( # noqa: PLR0913 Full GMT docs at :gmt-docs:`gmtlogo.html`. - **Aliases:** - - .. hlist:: - :columns: 3 - - - D = position, **+w**: width, **+h**: height + {aliases} - F = box - J = projection - R = region @@ -80,25 +72,7 @@ def logo( # noqa: PLR0913 """ self._activate_figure() - if isinstance(position, str) and any(v is not None for v in (width, height)): - msg = ( - "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " - "with parameters 'height', and 'width'. Please refer to the documentation " - "for the recommended usage." - ) - raise GMTInvalidInput(msg) - - # width and height are mutually exclusive. - if width is not None and height is not None: - msg = "Cannot specify both width and height." - raise GMTInvalidInput(msg) - aliasdict = AliasSystem( - D=[ - Alias(position, name="position"), - Alias(width, name="width", prefix="+w"), - Alias(height, name="height", prefix="+h"), - ], F=Alias(box, name="box"), S=Alias( style, name="style", mapping={"standard": "l", "url": "u", "no_label": "n"} From ad9e0aa9153addba8307b15be0ec98e387731d2b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 16:21:21 +0800 Subject: [PATCH 41/48] Simplify tests --- pygmt/tests/test_params_position.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 2ca05cf2150..346f4d3b03b 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -10,23 +10,20 @@ def test_params_position_types(): Test the Position class with different types of coordinate systems. """ assert str(Position((1, 2))) == "x1/2" - assert str(Position(location=(1, 2))) == "x1/2" - assert str(Position(location=(10, 20), type="mapcoords")) == "g10/20" - assert str(Position(location=(0.1, 0.2), type="boxcoords")) == "n0.1/0.2" - assert str(Position(location=("5c", "3c"), type="plotcoords")) == "x5c/3c" - assert str(Position(location="TL", type="inside")) == "jTL" - assert str(Position(location="BR", type="outside")) == "JBR" + assert str(Position((10, 20), type="mapcoords")) == "g10/20" + assert str(Position((0.1, 0.2), type="boxcoords")) == "n0.1/0.2" + assert str(Position(("5c", "3c"), type="plotcoords")) == "x5c/3c" + assert str(Position("TL", type="inside")) == "jTL" + assert str(Position("BR", type="outside")) == "JBR" def test_params_position_anchor_offset(): """ Test the Position class with anchor and offset parameters. """ - pos = Position(location=(10, 20), type="mapcoords", anchor="TL") - assert str(pos) == "g10/20+jTL" + assert str(Position((10, 20), type="mapcoords", anchor="TL")) == "g10/20+jTL" - pos = Position(location=(10, 20), type="mapcoords", offset=(1, 2)) - assert str(pos) == "g10/20+o1/2" + assert str(Position((10, 20), type="mapcoords", offset=(1, 2))) == "g10/20+o1/2" - pos = Position(location="TL", type="inside", anchor="MC", offset=("1c", "2c")) + pos = Position("TL", type="inside", anchor="MC", offset=("1c", "2c")) assert str(pos) == "jTL+jMC+o1c/2c" From b084e5f98af6e64639ff913e796a962108845702 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 16:48:21 +0800 Subject: [PATCH 42/48] Validate values --- pygmt/params/position.py | 41 ++++++++++++++++++++++++++--- pygmt/tests/test_params_position.py | 23 +++++++++++++++- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 6dea68311e0..06204f6d6b9 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -8,6 +8,7 @@ from pygmt._typing import AnchorCode from pygmt.alias import Alias +from pygmt.exceptions import GMTValueError from pygmt.params.base import BaseParam @@ -134,9 +135,12 @@ class Position(BaseParam): #: - ``"plotcoords"``: Plot coordinates #: - ``"boxcoords"``: Normalized coordinates #: - ``"inside"`` or ``"outside"``: Justification codes - type: Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] = ( - "plotcoords" - ) + #: + #: If not specified, defaults to ``"inside"`` if ``location`` is a justification + #: code; otherwise defaults to ``"plotcoords"``. + type: ( + Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] | None + ) = None #: Anchor point on the embellishment using a #: :doc:`2-character justification codes `. @@ -147,6 +151,37 @@ class Position(BaseParam): #: If a single value is given, the offset is applied to both x and y directions. offset: Sequence[float | str] | None = None + def _validate(self): + """ + Validate the parameters. + """ + _valid_anchors = {f"{h}{v}" for v in "TMB" for h in "LCR"} | { + f"{v}{h}" for v in "TMB" for h in "LCR" + } + + # Default to "inside" if type is not specified and location is an anchor code. + if self.type is None: + self.type = "inside" if isinstance(self.location, str) else "plotcoords" + + # Validate the location based on type. + match self.type: + case "mapcoords" | "plotcoords" | "boxcoords": + if not isinstance(self.location, Sequence) or len(self.location) != 2: + raise GMTValueError( + self.location, + description="reference point", + reason="Expect a sequence of two values.", + ) + case "inside" | "outside": + if self.location not in _valid_anchors: + raise GMTValueError( + self.location, + description="reference point", + reason="Expect a valid 2-character justification code.", + ) + case _: + pass # Will check type in the Alias system. + @property def _aliases(self): return [ diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 346f4d3b03b..1ce981ec1f8 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -2,6 +2,8 @@ Test the Position class. """ +import pytest +from pygmt.exceptions import GMTValueError from pygmt.params import Position @@ -9,11 +11,14 @@ def test_params_position_types(): """ Test the Position class with different types of coordinate systems. """ + # Default type is "plotcoords" for (x,y) and "inside" for anchor codes. assert str(Position((1, 2))) == "x1/2" + assert str(Position("TL")) == "jTL" + assert str(Position((10, 20), type="mapcoords")) == "g10/20" assert str(Position((0.1, 0.2), type="boxcoords")) == "n0.1/0.2" assert str(Position(("5c", "3c"), type="plotcoords")) == "x5c/3c" - assert str(Position("TL", type="inside")) == "jTL" + assert str(Position("MR", type="inside")) == "jMR" assert str(Position("BR", type="outside")) == "JBR" @@ -27,3 +32,19 @@ def test_params_position_anchor_offset(): pos = Position("TL", type="inside", anchor="MC", offset=("1c", "2c")) assert str(pos) == "jTL+jMC+o1c/2c" + + +def test_params_position_invalid_location(): + """ + Test that invalid location inputs raise GMTValueError. + """ + with pytest.raises(GMTValueError): + Position("invalid", type="mapcoords") + with pytest.raises(GMTValueError): + Position(5, type="plotcoords") + with pytest.raises(GMTValueError): + Position((0.5,), type="boxcoords") + with pytest.raises(GMTValueError): + Position((10, 20), type="inside") + with pytest.raises(GMTValueError): + Position("TT", type="outside") From d4ad6e0b955ea030d813eb94591846328c957c5b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 16:54:15 +0800 Subject: [PATCH 43/48] type will be validated in the Alias System --- pygmt/params/position.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 06204f6d6b9..05406a47037 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -179,8 +179,6 @@ def _validate(self): description="reference point", reason="Expect a valid 2-character justification code.", ) - case _: - pass # Will check type in the Alias system. @property def _aliases(self): From 7dc37bd96c728b818e9465561b00db1fc04b2117 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 18:14:09 +0800 Subject: [PATCH 44/48] Use the image from the GMT docs --- pygmt/params/position.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 05406a47037..59d8afd0f7c 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -17,7 +17,7 @@ class Position(BaseParam): """ Class for positioning embellishments on a plot. - .. figure:: https://github.com/user-attachments/assets/0f3e9b39-7d64-4628-8acb-58fe74ff6fa5 + .. figure:: https://docs.generic-mapping-tools.org/dev/_images/GMT_anchor.png :width: 400 px Positioning of GMT embellishment. From bfecb2deca103acda01b77727234a9c47db9feae Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 18:25:24 +0800 Subject: [PATCH 45/48] Fix width and alignment --- pygmt/params/position.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 59d8afd0f7c..96f6371a1ea 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -18,7 +18,8 @@ class Position(BaseParam): Class for positioning embellishments on a plot. .. figure:: https://docs.generic-mapping-tools.org/dev/_images/GMT_anchor.png - :width: 400 px + :width: 600 px + :align: center Positioning of GMT embellishment. From 9f2cafdbdb480cdeca805e2497b79a5d9634d2b7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 19:37:25 +0800 Subject: [PATCH 46/48] Refactor Figure.logo using the Position class --- pygmt/src/logo.py | 91 +++++++---------------------------------------- 1 file changed, 13 insertions(+), 78 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index b0b303dda92..eb33dbd51fc 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -5,23 +5,17 @@ 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 +from pygmt.params import Box, Position @fmt_docstring def logo( # noqa: PLR0913 self, - position: Sequence[str | float] | AnchorCode | None = None, - position_type: Literal[ - "mapcoords", "boxcoords", "plotcoords", "inside", "outside" - ] = "plotcoords", - anchor: AnchorCode | None = None, - anchor_offset: Sequence[float | str] | None = None, + position: Position | None = None, height: float | str | None = None, width: float | str | None = None, projection: str | None = None, @@ -53,14 +47,10 @@ def logo( # noqa: PLR0913 **Aliases:** - .. hlist:: - :columns: 1 - - - D = position/position_type, **+j**: anchor, **+o**: anchor_offset, - **+w**: width, **+h**: height .. hlist:: :columns: 3 + - D = position, **+w**: width, **+h**: height - F = box - J = projection - R = region @@ -73,39 +63,8 @@ def logo( # noqa: PLR0913 Parameters ---------- position - Specify the reference point on the plot for the GMT logo. The method of defining - the reference point is controlled by ``position_type``, and the exact location - is set by ``position``. - position_type - Specify the type of coordinates used to define the reference point. It can be - one of the following values: - - - ``"mapcoords"``: ``position`` is specified as (*longitude*, *latitude*) in map - coordinates. - - ``"boxcoords"``: ``position`` is specified as (*nx*, *ny*) in normalized - coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. - - ``"plotcoords"``: ``position`` is specified as (*x*, *y*) in plot coordinates, - i.e., distances from the lower-left plot origin given in inches, centimeters, - or points. - - ``"inside"`` or ``"outside"``: ``position`` is one of the nine - :doc:`two-character justification codes `, - indicating a specific location relative to the plot bounding box. - - Refer to :doc:`/techref/reference_anchor_points` for details about the - positioning. - anchor - Specify the anchor point of the GMT logo, using one of the - :doc:`2-character justification codes `. The - default value depends on ``position_type``. - - - ``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 - Specifies an offset for the anchor point as *offset* or - (*offset_x*, *offset_y*). If a single value *offset* is given, both *offset_x* - and *offset_y* are set to *offset*. + Specify the position of the GMT logo. See the :class:`pygmt.params.Position` + class for details. width height Width or height of the GMT logo. Since the aspect ratio is fixed, only one of @@ -130,47 +89,23 @@ def logo( # noqa: PLR0913 """ self._activate_figure() - # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. For backward - # compatibility, need to check if users pass a string with the GMT CLI syntax to - # 'position', i.e., a string starting with one of the codes "g", "n", "x", "j", "J", - # or contains modifiers with "+". - _is_deprecated_position = isinstance(position, str) and ( - position.startswith(("g", "n", "x", "j", "J")) or "+" in position - ) - - if _is_deprecated_position and any( - v is not None for v in (anchor, anchor_offset, height, width) - ): + # Prior PyGMT v0.17.0, 'position' can accept a raw GMT CLI string. Check for + # conflicts with other parameters. + if isinstance(position, str) and (height is not None or width is not None): msg = ( - "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " - "with parameters 'anchor', 'anchor_offset', 'height', and 'width'. " - "Please refer to the documentation for the recommended usage." + "Parameter 'position' is given with a raw GMT command string, and conflicts " + "with parameters 'height', and 'width'. " ) raise GMTInvalidInput(msg) # width and height are mutually exclusive. if width is not None and height is not None: - msg = "Cannot specify both width and height." + msg = "Cannot specify both 'width' and 'height'." raise GMTInvalidInput(msg) aliasdict = AliasSystem( - D=Alias(position, name="position") - if _is_deprecated_position - else [ - 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), + D=[ + Alias(position, name="position"), Alias(height, name="height", prefix="+h"), Alias(width, name="width", prefix="+w"), ], From 026e58325b97604bf3eca23c37fa3bd437ebd2a6 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 19:41:10 +0800 Subject: [PATCH 47/48] Remove some tests --- .../baseline/test_logo_position_type.png.dvc | 5 --- pygmt/tests/test_logo.py | 36 +++---------------- 2 files changed, 5 insertions(+), 36 deletions(-) delete mode 100644 pygmt/tests/baseline/test_logo_position_type.png.dvc diff --git a/pygmt/tests/baseline/test_logo_position_type.png.dvc b/pygmt/tests/baseline/test_logo_position_type.png.dvc deleted file mode 100644 index a1ea8c1a191..00000000000 --- a/pygmt/tests/baseline/test_logo_position_type.png.dvc +++ /dev/null @@ -1,5 +0,0 @@ -outs: -- md5: 9b9d740219df9edb298dbc49e7ef1351 - size: 232635 - hash: md5 - path: test_logo_position_type.png diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 6ebf7943655..3eea9033be5 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -5,6 +5,7 @@ import pytest from pygmt import Figure from pygmt.exceptions import GMTInvalidInput +from pygmt.params import Position @pytest.mark.benchmark @@ -25,45 +26,18 @@ def test_logo_on_a_map(): """ fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) - fig.logo( - position_type="inside", - position="TR", - anchor_offset=(0.25, 0.25), - width="7.5c", - box=True, - ) + fig.logo(position=Position("TR", offset=(0.25, 0.25)), width="7.5c", box=True) return fig -@pytest.mark.mpl_image_compare -def test_logo_position_type(): - """ - Test that different position types work as expected. - """ - fig = Figure() - fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) - fig.logo(position_type="inside", position="TL") - fig.logo(position_type="outside", position="TR") - fig.logo(position_type="mapcoords", position=(-80, 15)) - fig.logo(position_type="boxcoords", position=(0, 0.5)) - fig.logo(position_type="plotcoords", position=("2c", "0c"), width="5c") - fig.logo(position=("8c", "0c")) # Default position_type is "plotcoords". - return fig - - -@pytest.mark.mpl_image_compare(filename="test_logo_position_type.png") +@pytest.mark.mpl_image_compare(filename="test_logo_on_a_map.png") def test_logo_position_deprecated_syntax(): """ Test that passing the deprecated GMT CLI syntax string to 'position' works. """ fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) - fig.logo(position="jTL") - fig.logo(position="JTR") - fig.logo(position="g-80/15") - fig.logo(position="n0/0.5") - fig.logo(position="x2c/0c+w5c") - fig.logo(position="8c/0c") + fig.logo(position="jTR+o0.25/0.25+w7.5c", box=True) return fig @@ -84,4 +58,4 @@ def test_logo_position_mixed_syntax(): with pytest.raises(GMTInvalidInput): fig.logo(position="jTL", width="5c") with pytest.raises(GMTInvalidInput): - fig.logo(position="jTL", anchor="BR") + fig.logo(position="jTL", height="6c") From e9dd1e000bf92d69893f0295a61079b10236dbe7 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 19:48:15 +0800 Subject: [PATCH 48/48] Update examples/gallery/embellishments/gmt_logo.py --- examples/gallery/embellishments/gmt_logo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gallery/embellishments/gmt_logo.py b/examples/gallery/embellishments/gmt_logo.py index 1993dafa325..d8f0d14470d 100644 --- a/examples/gallery/embellishments/gmt_logo.py +++ b/examples/gallery/embellishments/gmt_logo.py @@ -13,5 +13,5 @@ # Add the GMT logo in the Top Right (TR) corner of the current plot, scaled up to be 3 # centimeters wide and offset by 0.3 cm in x-direction and 0.6 cm in y-direction. -fig.logo(position="TR", position_type="inside", anchor_offset=(0.3, 0.6), width="3c") +fig.logo(position=pygmt.params.Position("TR", offset=(0.3, 0.6)), width="3c") fig.show()