From fb64cbf18f6f8cf870c9cd8a0892b3874e3b1664 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 15:14:02 +0800 Subject: [PATCH 01/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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/79] 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() From 18b90b3e5f412cbbc8902577306f6a74d6d7b09a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 20:15:14 +0800 Subject: [PATCH 49/79] Improve docstrings --- pygmt/params/position.py | 81 ++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 96f6371a1ea..c445d6bd3d2 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -21,7 +21,8 @@ class Position(BaseParam): :width: 600 px :align: center - Positioning of GMT embellishment. + The placement of a GMT embellishment (represented by a green rectangle) in + relation to the underlying plot (represented by a bisque rectangle). 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 @@ -36,73 +37,72 @@ class Position(BaseParam): *reference point* 3. The embellishment is "dropped" at that position - **Reference Point Types** + **Reference Point** - The reference point can be specified in five different ways using the ``type`` and + 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. + ``type="mapcoords"`` Map Coordinates + Use data/geographic coordinates. Specify ``location`` as + (*longitude*, *latitude*). Useful when tying the embellishment to a specific + geographic location. - Example: ``location=(135, 20), type="mapcoords"``. + **Example:** ``location=(135, 20), type="mapcoords"``. - **type="plotcoords"** (Plot Coordinates) + ``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"`` + **Example:** ``location=("2c", "2.5c"), type="plotcoords"`` - **type="boxcoords"** (Normalized Coordinates) + ``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. + the upper-right corner of the bounding box of the current plot. Specify + ``location`` as (*nx*, *ny*). Useful for positioning relative to plot dimensions + without units. - Example: ``location=(0.2, 0.1), type="boxcoords"`` + **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. + ``type="inside"`` Inside Plot + Select one of the nine :doc:`justification codes ` + as the *reference point*. The *anchor point* defaults to be the same as the + *reference point*, so the embellishment is placed inside the plot. - Example: ``location="TL", type="inside"`` + **Example:** ``location="TL", type="inside"`` [anchor point defaults to "TL"] - **type="outside"** (Outside Plot) - Similar to ``type="inside"``, but the anchor point defaults to the mirror + ``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"`` + **Example:** ``location="TL", type="outside"`` [anchor point defaults to "BR"] **Anchor Point** - The anchor point determines which part of the embellishment aligns with the - reference point. It uses one of nine + 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: + *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"`` + - ``type="inside"``: Same as the *reference point* justification code + - ``type="outside"``: Mirror opposite of the *reference point* justification code + - 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*). + 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. It should be + 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): + Position the GMT 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 @@ -114,7 +114,7 @@ class Position(BaseParam): ... ) >>> fig.show() - Position an embellishment at the top-left corner inside the plot: + Position the GMT logo at the top-left corner inside the plot: >>> fig = pygmt.Figure() >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) @@ -125,9 +125,10 @@ class Position(BaseParam): #: 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 + #: - ``type="plotcoords"``: (*x*, *y*) with plot units (e.g., ``"2c"``) + #: - ``type="boxcoords"``: (*nx*, *ny*) + #: - ``type="inside"`` or ``"outside"``: + #: :doc:`2-character justification codes ` location: Sequence[float | str] | AnchorCode #: Coordinate system for the reference point. Valid values are: From 6b1b5bc3a89605edf34394b36ff13656cf133dfc Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 20:18:48 +0800 Subject: [PATCH 50/79] Remove unneeded blank lines --- pygmt/tests/test_params_position.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 1ce981ec1f8..161c571c7a3 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -27,9 +27,7 @@ def test_params_position_anchor_offset(): Test the Position class with anchor and offset parameters. """ assert str(Position((10, 20), type="mapcoords", anchor="TL")) == "g10/20+jTL" - assert str(Position((10, 20), type="mapcoords", offset=(1, 2))) == "g10/20+o1/2" - pos = Position("TL", type="inside", anchor="MC", offset=("1c", "2c")) assert str(pos) == "jTL+jMC+o1c/2c" From 1eae742469349587ad80b63b2299b637977e8dd5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 22:52:13 +0800 Subject: [PATCH 51/79] Improve docstrings --- pygmt/params/position.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index c445d6bd3d2..433731b8e40 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -73,7 +73,7 @@ class Position(BaseParam): ``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 + opposite of the *reference point*. Useful for placing embellishments outside the plot boundaries (e.g., color bars). **Example:** ``location="TL", type="outside"`` [anchor point defaults to "BR"] @@ -125,13 +125,13 @@ class Position(BaseParam): #: Location of the reference point on the plot. The format depends on ``type``: #: #: - ``type="mapcoords"``: (*longitude*, *latitude*) - #: - ``type="plotcoords"``: (*x*, *y*) with plot units (e.g., ``"2c"``) + #: - ``type="plotcoords"``: (*x*, *y*) with plot units #: - ``type="boxcoords"``: (*nx*, *ny*) #: - ``type="inside"`` or ``"outside"``: #: :doc:`2-character justification codes ` location: Sequence[float | str] | AnchorCode - #: Coordinate system for the reference point. Valid values are: + #: Types of the reference point. Valid values are: #: #: - ``"mapcoords"``: Map/Data coordinates #: - ``"plotcoords"``: Plot coordinates From 721b46fa4f366fe8ea0e350479174611bb18c2cf Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 22:58:08 +0800 Subject: [PATCH 52/79] Validate anchor code --- pygmt/params/position.py | 7 +++++++ pygmt/tests/test_params_position.py | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 433731b8e40..b955b36fbe6 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -181,6 +181,13 @@ def _validate(self): description="reference point", reason="Expect a valid 2-character justification code.", ) + # Validate the anchor if specified. + if self.anchor is not None and self.anchor not in _valid_anchors: + raise GMTValueError( + self.anchor, + description="anchor point", + reason="Expect a valid 2-character justification code.", + ) @property def _aliases(self): diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 161c571c7a3..91305da1752 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -38,6 +38,8 @@ def test_params_position_invalid_location(): """ with pytest.raises(GMTValueError): Position("invalid", type="mapcoords") + with pytest.raises(GMTValueError): + Position((1, 2, 3), type="mapcoords") with pytest.raises(GMTValueError): Position(5, type="plotcoords") with pytest.raises(GMTValueError): @@ -46,3 +48,11 @@ def test_params_position_invalid_location(): Position((10, 20), type="inside") with pytest.raises(GMTValueError): Position("TT", type="outside") + + +def test_params_position_invalid_anchor(): + """ + Test that invalid anchor inputs raise GMTValueError. + """ + with pytest.raises(GMTValueError): + Position((10, 20), type="mapcoords", anchor="XX") From 0f9ed6ca2f74aaf3d9161d9229834aa6ae1b7f01 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 26 Nov 2025 16:16:28 +0800 Subject: [PATCH 53/79] offset can be a single value --- 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 b955b36fbe6..1b44b0f45a0 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -151,7 +151,7 @@ class Position(BaseParam): #: 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 + offset: float | str | Sequence[float | str] | None = None def _validate(self): """ From 10a0dfb4acb013492170b7a2acdbc6928a03a684 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 4 Dec 2025 14:29:37 +0800 Subject: [PATCH 54/79] Use is_nonstr_iter to check the location parameter --- 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 1b44b0f45a0..3eaca277af1 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -9,6 +9,7 @@ from pygmt._typing import AnchorCode from pygmt.alias import Alias from pygmt.exceptions import GMTValueError +from pygmt.helpers import is_nonstr_iter from pygmt.params.base import BaseParam @@ -168,7 +169,7 @@ def _validate(self): # Validate the location based on type. match self.type: case "mapcoords" | "plotcoords" | "boxcoords": - if not isinstance(self.location, Sequence) or len(self.location) != 2: + if is_nonstr_iter(self.location) or len(self.location) != 2: raise GMTValueError( self.location, description="reference point", From c27213f77281363559c022cc464242dee13acd25 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 4 Dec 2025 14:32:42 +0800 Subject: [PATCH 55/79] Fix a typo [skip ci] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 3eaca277af1..5b4c1454c7f 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -132,7 +132,7 @@ class Position(BaseParam): #: :doc:`2-character justification codes ` location: Sequence[float | str] | AnchorCode - #: Types of the reference point. Valid values are: + #: Type of the reference point. Valid values are: #: #: - ``"mapcoords"``: Map/Data coordinates #: - ``"plotcoords"``: Plot coordinates From d47aaeb44fd8b84951017fc9d6814bed396370fe Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 4 Dec 2025 14:33:05 +0800 Subject: [PATCH 56/79] Fix a typo [skip ci] Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 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 5b4c1454c7f..2b0fcfbd53b 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -146,7 +146,7 @@ class Position(BaseParam): ) = None #: Anchor point on the embellishment using a - #: :doc:`2-character justification codes `. + #: :doc:`2-character justification code `. #: If ``None``, defaults are applied based on ``type`` (see above). anchor: AnchorCode | None = None From 7fc6ffcbd6e202129f6fb3f2d2a338c805643b55 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 4 Dec 2025 14:36:28 +0800 Subject: [PATCH 57/79] Fix the wrong logic in checking location --- 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 2b0fcfbd53b..6276bacabd6 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -169,7 +169,7 @@ def _validate(self): # Validate the location based on type. match self.type: case "mapcoords" | "plotcoords" | "boxcoords": - if is_nonstr_iter(self.location) or len(self.location) != 2: + if not is_nonstr_iter(self.location) or len(self.location) != 2: raise GMTValueError( self.location, description="reference point", From d82f4ba53a66d3a83527e9300c307f7e46d8dc97 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 4 Dec 2025 14:36:42 +0800 Subject: [PATCH 58/79] Add a tests for passing a single value to offset --- pygmt/tests/test_params_position.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 91305da1752..484ec1b63d0 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -30,6 +30,7 @@ def test_params_position_anchor_offset(): assert str(Position((10, 20), type="mapcoords", offset=(1, 2))) == "g10/20+o1/2" pos = Position("TL", type="inside", anchor="MC", offset=("1c", "2c")) assert str(pos) == "jTL+jMC+o1c/2c" + assert str(Position("TL", anchor="BR", offset=0.5)) == "jTL+jBR+o0.5" def test_params_position_invalid_location(): From 02d87139ea165f2bd6f6237a7341564913bc02e2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 15:27:00 +0800 Subject: [PATCH 59/79] Add the Axes, Axis, Frame classes for the frame parameter --- pygmt/params/__init__.py | 1 + pygmt/params/frame.py | 139 +++++++++++++++++++++++++++++++ pygmt/tests/test_params_frame.py | 30 +++++++ 3 files changed, 170 insertions(+) create mode 100644 pygmt/params/frame.py create mode 100644 pygmt/tests/test_params_frame.py diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py index b80b921407a..12a788e3352 100644 --- a/pygmt/params/__init__.py +++ b/pygmt/params/__init__.py @@ -3,4 +3,5 @@ """ from pygmt.params.box import Box +from pygmt.params.frame import Axes, Axis, Frame from pygmt.params.pattern import Pattern diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py new file mode 100644 index 00000000000..6c4c2969cfa --- /dev/null +++ b/pygmt/params/frame.py @@ -0,0 +1,139 @@ +""" +The Axes, Axis, and Frame classes for specifying the frame. +""" + +import dataclasses +from typing import Any, Literal + +from pygmt.alias import Alias +from pygmt.params.base import BaseParam + + +@dataclasses.dataclass(repr=False) +class Axis(BaseParam): + """ + Class for setting up one axis of a plot. + """ + + #: Intervals for annotations and major tick spacing, minor tick spacing, and/or + #: grid line spacing. + interval: float | str + + #: Plot slanted annotations (for Cartesian plots only), where *angle* is measured + #: with respect to the horizontal and must be in the -90 <= *angle* <= 90 range. + #: Default is normal (i.e., ``angle=90``) for y-axis and parallel (i.e., + #: ``angle=0``) for x-axis annotations. These defaults can be changed via + #: :gmt-term:`MAP_ANNOT_ORTHO`. + angle: float | None = None + + #: Skip annotations that fall exactly at the ends of the axis. Choose from ``left`` + #: or ``right`` to skip only the lower or upper annotation, respectively, or + #: ``True`` to skip both. + skip_edge: Literal["left", "right"] | bool = False + + #: Give fancy annotations with W|E|S|N suffixes encoding the sign (for geographic + #: axes only). + fancy: bool = False + + #: Add a label to the axis (for Cartesian plots only). The label is placed parallel + #: to the axis by default; use **hlabel** to force a horizontal label for y-axis, + #: which is useful for very short labels. + label: str | None = None + hlabel: str | None = None + + #: Add an alternate label for the right or upper axes. The label is placed parallel + #: to the axis by default; use **alt_hlabel** to force a horizontal label for + #: y-axis, which is useful for very short labels. [For Cartesian plots only]. + alt_label: str | None = None + alt_hlabel: str | None = None + + #: Add a leading text prefix for axis annotation (e.g., dollar sign for plots + #: related to money) (for Cartesian plots only). For geographic maps the addition + #: of degree symbols, etc. is automatic and controlled by + #: :gmt-term:`FORMAT_GEO_MAP`. + prefix: str | None = None + + #: Append a unit to the annotations (for Cartesian plots only). For geographic maps + #: the addition of degree symbols, etc. is automatic and controlled by + #: :gmt-term:`FORMAT_GEO_MAP`. + unit: str | None = None + + @property + def _aliases(self): + return [ + Alias(self.interval, name="interval"), + Alias(self.angle, name="angle", prefix="+a"), + Alias( + self.skip_edge, + name="skip_edge", + prefix="+e", + mapping={True: True, "left": "l", "right": "r"}, + ), + Alias(self.fancy, name="fancy", prefix="+f"), + Alias(self.label, name="label", prefix="+l"), + Alias(self.hlabel, name="hlabel", prefix="+L"), + Alias(self.alt_label, name="alt_label", prefix="+s"), + Alias(self.alt_hlabel, name="alt_hlabel", prefix="+S"), + Alias(self.unit, name="unit", prefix="+u"), + ] + + +@dataclasses.dataclass(repr=False) +class Axes(BaseParam): + """ + Class for specifying the frame of a plot. + """ + + #: Specify which axes to draw and their attributes. + axes: str | None = None + + #: Fill for the interior of the canvas [Default is no fill]. This also sets fill + #: for the two back-walls in 3-D plots. + fill: str | None = None + + #: The title string centered above the plot frame [Default is no title]. + title: str | None = None + + #: The subtitle string beneath the title [Default is no subtitle]. This requires + #: ``title`` to be set. + subtitle: str | None = None + + @property + def _aliases(self): + return [ + Alias(self.axes, name="axes"), + Alias(self.fill, name="fill", prefix="+g"), + Alias(self.title, name="title", prefix="+t"), + Alias(self.subtitle, name="subtitle", prefix="+s"), + ] + + +@dataclasses.dataclass(repr=False) +class Frame(BaseParam): + """ + Class for setting up the frame of a plot. + """ + + axes: Any = None + xaxis: Any = None + yaxis: Any = None + zaxis: Any = None + + @property + def _aliases(self): + return [ + Alias(self.axes), + Alias(self.xaxis, prefix="x"), + Alias(self.yaxis, prefix="y"), + Alias(self.zaxis, prefix="z"), + ] + + def __iter__(self): + """ + Iterate over the aliases of the class. + + Yields + ------ + The value of each alias in the class. None are excluded. + """ + yield from (alias._value for alias in self._aliases if alias._value is not None) diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py new file mode 100644 index 00000000000..85de1bb406b --- /dev/null +++ b/pygmt/tests/test_params_frame.py @@ -0,0 +1,30 @@ +""" +Test the Frame/Axes/Axis classes. +""" + +from pygmt.params import Axes, Axis + + +def test_params_axis(): + """ + Test the Axis class. + """ + assert str(Axis(interval="a1f0.5")) == "a1f0.5" + assert str(Axis(interval="a1f0.5", angle=30)) == "a1f0.5+a30" + assert str(Axis(interval="a1f0.5", angle=30, skip_edge="left")) == "a1f0.5+a30+el" + assert str(Axis(interval="a1f0.5", fancy=True)) == "a1f0.5+f" + assert str(Axis(interval="a1f0.5", label="My Label")) == "a1f0.5+lMy Label" + assert str(Axis(interval="a1f0.5", hlabel="My HLabel")) == "a1f0.5+LMy HLabel" + assert str(Axis(interval="a1f0.5", alt_label="Alt Label")) == "a1f0.5+sAlt Label" + assert str(Axis(interval="a1f0.5", alt_hlabel="Alt HLabel")) == "a1f0.5+SAlt HLabel" + assert str(Axis(interval="a1f0.5", unit="km")) == "a1f0.5+ukm" + + +def test_params_axes(): + """ + Test the Axes class. + """ + assert ( + str(Axes("WSen", title="My Plot Title", fill="lightred")) + == "WSen+glightred+tMy Plot Title" + ) From 6f97c57ad48b3e5f1bfe9466002357ce834db272 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 15 Nov 2025 13:50:20 +0800 Subject: [PATCH 60/79] Add to API documentation --- doc/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 3656bba286e..5c4c4cc0ed6 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -213,6 +213,7 @@ Class-style Parameters :template: autosummary/params.rst Box + Frame Pattern Enums From b2c98771175cdb1176ef68bb0f4c6c054175f327 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 01:06:02 +0800 Subject: [PATCH 61/79] Improve Axis/Axes/Frame and add more tests --- pygmt/params/frame.py | 56 +++++++++++++++++++++---- pygmt/tests/test_params_frame.py | 70 +++++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 24 deletions(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 6c4c2969cfa..5999f8a6bf1 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -6,6 +6,7 @@ from typing import Any, Literal from pygmt.alias import Alias +from pygmt.exceptions import GMTInvalidInput from pygmt.params.base import BaseParam @@ -15,9 +16,17 @@ class Axis(BaseParam): Class for setting up one axis of a plot. """ - #: Intervals for annotations and major tick spacing, minor tick spacing, and/or - #: grid line spacing. - interval: float | str + #: Specify annotation for the axis. Provide a specific interval with an optional + #: unit. Set to ``True`` to use default interval. + annotation: float | str | bool = False + + #: Specify ticks for the axis. Provide a specific interval with an optional unit. + #: Set to ``True`` to use default interval. + tick: float | str | bool = False + + #: Specify grid lines for the axis. Provide a specific interval with an optional + #: unit. Set to ``True`` to use default interval. + grid: float | str | bool = False #: Plot slanted annotations (for Cartesian plots only), where *angle* is measured #: with respect to the horizontal and must be in the -90 <= *angle* <= 90 range. @@ -26,10 +35,10 @@ class Axis(BaseParam): #: :gmt-term:`MAP_ANNOT_ORTHO`. angle: float | None = None - #: Skip annotations that fall exactly at the ends of the axis. Choose from ``left`` - #: or ``right`` to skip only the lower or upper annotation, respectively, or + #: Skip annotations that fall exactly at the ends of the axis. Choose from ``lower`` + #: or ``upper`` to skip only the lower or upper annotation, respectively, or #: ``True`` to skip both. - skip_edge: Literal["left", "right"] | bool = False + skip_edge: Literal["lower", "upper"] | bool = False #: Give fancy annotations with W|E|S|N suffixes encoding the sign (for geographic #: axes only). @@ -58,22 +67,43 @@ class Axis(BaseParam): #: :gmt-term:`FORMAT_GEO_MAP`. unit: str | None = None + def _validate(self): + """ + Validate the parameters. + """ + if self.label is not None and self.hlabel is not None: + msg = "Parameters 'label' and 'hlabel' cannot be both set." + raise GMTInvalidInput(msg) + if self.alt_label is not None and self.alt_hlabel is not None: + msg = "Parameters 'alt_label' and 'alt_hlabel' cannot be both set." + raise GMTInvalidInput(msg) + @property def _aliases(self): return [ - Alias(self.interval, name="interval"), - Alias(self.angle, name="angle", prefix="+a"), + Alias(self.annotation, name="annotation", prefix="a"), + Alias(self.tick, name="tick", prefix="f"), + Alias(self.grid, name="grid", prefix="g"), + Alias( + self.angle, + name="angle", + prefix="+a", + mapping={"normal": "n", "parallel": "p"} + if isinstance(self.angle, str) + else None, + ), Alias( self.skip_edge, name="skip_edge", prefix="+e", - mapping={True: True, "left": "l", "right": "r"}, + mapping={True: True, "lower": "l", "upper": "u"}, ), Alias(self.fancy, name="fancy", prefix="+f"), Alias(self.label, name="label", prefix="+l"), Alias(self.hlabel, name="hlabel", prefix="+L"), Alias(self.alt_label, name="alt_label", prefix="+s"), Alias(self.alt_hlabel, name="alt_hlabel", prefix="+S"), + Alias(self.prefix, name="prefix", prefix="+p"), Alias(self.unit, name="unit", prefix="+u"), ] @@ -115,17 +145,25 @@ class Frame(BaseParam): """ axes: Any = None + axis: Any = None xaxis: Any = None yaxis: Any = None zaxis: Any = None + xaxis_secondary: Any = None + yaxis_secondary: Any = None + zaxis_secondary: Any = None @property def _aliases(self): return [ Alias(self.axes), + Alias(self.axis), Alias(self.xaxis, prefix="x"), Alias(self.yaxis, prefix="y"), Alias(self.zaxis, prefix="z"), + Alias(self.xaxis_secondary, prefix="sx"), + Alias(self.yaxis_secondary, prefix="sy"), + Alias(self.zaxis_secondary, prefix="sz"), ] def __iter__(self): diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py index 85de1bb406b..ed6a0d97657 100644 --- a/pygmt/tests/test_params_frame.py +++ b/pygmt/tests/test_params_frame.py @@ -5,26 +5,66 @@ from pygmt.params import Axes, Axis -def test_params_axis(): +def test_params_axis_intervals(): """ - Test the Axis class. + Test the annotation, tick, and grid parameters of the Axis class. """ - assert str(Axis(interval="a1f0.5")) == "a1f0.5" - assert str(Axis(interval="a1f0.5", angle=30)) == "a1f0.5+a30" - assert str(Axis(interval="a1f0.5", angle=30, skip_edge="left")) == "a1f0.5+a30+el" - assert str(Axis(interval="a1f0.5", fancy=True)) == "a1f0.5+f" - assert str(Axis(interval="a1f0.5", label="My Label")) == "a1f0.5+lMy Label" - assert str(Axis(interval="a1f0.5", hlabel="My HLabel")) == "a1f0.5+LMy HLabel" - assert str(Axis(interval="a1f0.5", alt_label="Alt Label")) == "a1f0.5+sAlt Label" - assert str(Axis(interval="a1f0.5", alt_hlabel="Alt HLabel")) == "a1f0.5+SAlt HLabel" - assert str(Axis(interval="a1f0.5", unit="km")) == "a1f0.5+ukm" + assert str(Axis(annotation=1)) == "a1" + assert str(Axis(tick=2)) == "f2" + assert str(Axis(grid=3)) == "g3" + assert str(Axis(annotation=1, tick=2, grid=3)) == "a1f2g3" + + assert str(Axis(annotation=True)) == "a" + assert str(Axis(tick=True)) == "f" + assert str(Axis(grid=True)) == "g" + assert str(Axis(annotation=True, tick=True)) == "af" + assert str(Axis(annotation=True, grid=True)) == "ag" + assert str(Axis(tick=True, grid=True)) == "fg" + assert str(Axis(annotation=True, tick=True, grid=True)) == "afg" + + +def test_params_axis_modifiers(): + """ + Test the modifiers of the Axis class. + """ + assert str(Axis(annotation=True, angle=30)) == "a+a30" + assert str(Axis(annotation=True, angle="normal")) == "a+an" + assert str(Axis(annotation=True, angle="parallel")) == "a+ap" + + assert str(Axis(annotation=True, skip_edge=True)) == "a+e" + assert str(Axis(annotation=True, skip_edge="lower")) == "a+el" + assert str(Axis(annotation=True, skip_edge="upper")) == "a+eu" + + assert str(Axis(annotation=True, fancy=True)) == "a+f" + + assert str(Axis(annotation=True, label="My Label")) == "a+lMy Label" + assert str(Axis(annotation=True, hlabel="My HLabel")) == "a+LMy HLabel" + assert str(Axis(annotation=True, alt_label="Alt Label")) == "a+sAlt Label" + assert str(Axis(annotation=True, alt_hlabel="Alt HLabel")) == "a+SAlt HLabel" + + axis = Axis(annotation=True, label="My Label", alt_label="My HLabel") + assert str(axis) == "a+lMy Label+sMy HLabel" + + assert str(Axis(annotation=True, prefix="$")) == "a+p$" + + assert str(Axis(annotation=True, unit="km")) == "a+ukm" def test_params_axes(): """ Test the Axes class. """ - assert ( - str(Axes("WSen", title="My Plot Title", fill="lightred")) - == "WSen+glightred+tMy Plot Title" - ) + + assert str(Axes(axes="WSen")) == "WSen" + assert str(Axes(fill="lightred")) == "+glightred" + assert str(Axes(title="My Plot Title")) == "+tMy Plot Title" + assert str(Axes(subtitle="My Subtitle")) == "+sMy Subtitle" + + axes = Axes(axes="WSen", fill="lightred", title="My Plot Title") + assert str(axes) == "WSen+glightred+tMy Plot Title" + + +def test_params_frame(): + """ + Test the Frame class. + """ From 708eeacedb083c1a22de68b39cee471cb641e20e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 10:47:28 +0800 Subject: [PATCH 62/79] Revert "Improve Axis/Axes/Frame and add more tests" This reverts commit b2c98771175cdb1176ef68bb0f4c6c054175f327. --- pygmt/params/frame.py | 56 ++++--------------------- pygmt/tests/test_params_frame.py | 70 +++++++------------------------- 2 files changed, 24 insertions(+), 102 deletions(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 5999f8a6bf1..6c4c2969cfa 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -6,7 +6,6 @@ from typing import Any, Literal from pygmt.alias import Alias -from pygmt.exceptions import GMTInvalidInput from pygmt.params.base import BaseParam @@ -16,17 +15,9 @@ class Axis(BaseParam): Class for setting up one axis of a plot. """ - #: Specify annotation for the axis. Provide a specific interval with an optional - #: unit. Set to ``True`` to use default interval. - annotation: float | str | bool = False - - #: Specify ticks for the axis. Provide a specific interval with an optional unit. - #: Set to ``True`` to use default interval. - tick: float | str | bool = False - - #: Specify grid lines for the axis. Provide a specific interval with an optional - #: unit. Set to ``True`` to use default interval. - grid: float | str | bool = False + #: Intervals for annotations and major tick spacing, minor tick spacing, and/or + #: grid line spacing. + interval: float | str #: Plot slanted annotations (for Cartesian plots only), where *angle* is measured #: with respect to the horizontal and must be in the -90 <= *angle* <= 90 range. @@ -35,10 +26,10 @@ class Axis(BaseParam): #: :gmt-term:`MAP_ANNOT_ORTHO`. angle: float | None = None - #: Skip annotations that fall exactly at the ends of the axis. Choose from ``lower`` - #: or ``upper`` to skip only the lower or upper annotation, respectively, or + #: Skip annotations that fall exactly at the ends of the axis. Choose from ``left`` + #: or ``right`` to skip only the lower or upper annotation, respectively, or #: ``True`` to skip both. - skip_edge: Literal["lower", "upper"] | bool = False + skip_edge: Literal["left", "right"] | bool = False #: Give fancy annotations with W|E|S|N suffixes encoding the sign (for geographic #: axes only). @@ -67,43 +58,22 @@ class Axis(BaseParam): #: :gmt-term:`FORMAT_GEO_MAP`. unit: str | None = None - def _validate(self): - """ - Validate the parameters. - """ - if self.label is not None and self.hlabel is not None: - msg = "Parameters 'label' and 'hlabel' cannot be both set." - raise GMTInvalidInput(msg) - if self.alt_label is not None and self.alt_hlabel is not None: - msg = "Parameters 'alt_label' and 'alt_hlabel' cannot be both set." - raise GMTInvalidInput(msg) - @property def _aliases(self): return [ - Alias(self.annotation, name="annotation", prefix="a"), - Alias(self.tick, name="tick", prefix="f"), - Alias(self.grid, name="grid", prefix="g"), - Alias( - self.angle, - name="angle", - prefix="+a", - mapping={"normal": "n", "parallel": "p"} - if isinstance(self.angle, str) - else None, - ), + Alias(self.interval, name="interval"), + Alias(self.angle, name="angle", prefix="+a"), Alias( self.skip_edge, name="skip_edge", prefix="+e", - mapping={True: True, "lower": "l", "upper": "u"}, + mapping={True: True, "left": "l", "right": "r"}, ), Alias(self.fancy, name="fancy", prefix="+f"), Alias(self.label, name="label", prefix="+l"), Alias(self.hlabel, name="hlabel", prefix="+L"), Alias(self.alt_label, name="alt_label", prefix="+s"), Alias(self.alt_hlabel, name="alt_hlabel", prefix="+S"), - Alias(self.prefix, name="prefix", prefix="+p"), Alias(self.unit, name="unit", prefix="+u"), ] @@ -145,25 +115,17 @@ class Frame(BaseParam): """ axes: Any = None - axis: Any = None xaxis: Any = None yaxis: Any = None zaxis: Any = None - xaxis_secondary: Any = None - yaxis_secondary: Any = None - zaxis_secondary: Any = None @property def _aliases(self): return [ Alias(self.axes), - Alias(self.axis), Alias(self.xaxis, prefix="x"), Alias(self.yaxis, prefix="y"), Alias(self.zaxis, prefix="z"), - Alias(self.xaxis_secondary, prefix="sx"), - Alias(self.yaxis_secondary, prefix="sy"), - Alias(self.zaxis_secondary, prefix="sz"), ] def __iter__(self): diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py index ed6a0d97657..85de1bb406b 100644 --- a/pygmt/tests/test_params_frame.py +++ b/pygmt/tests/test_params_frame.py @@ -5,66 +5,26 @@ from pygmt.params import Axes, Axis -def test_params_axis_intervals(): +def test_params_axis(): """ - Test the annotation, tick, and grid parameters of the Axis class. + Test the Axis class. """ - assert str(Axis(annotation=1)) == "a1" - assert str(Axis(tick=2)) == "f2" - assert str(Axis(grid=3)) == "g3" - assert str(Axis(annotation=1, tick=2, grid=3)) == "a1f2g3" - - assert str(Axis(annotation=True)) == "a" - assert str(Axis(tick=True)) == "f" - assert str(Axis(grid=True)) == "g" - assert str(Axis(annotation=True, tick=True)) == "af" - assert str(Axis(annotation=True, grid=True)) == "ag" - assert str(Axis(tick=True, grid=True)) == "fg" - assert str(Axis(annotation=True, tick=True, grid=True)) == "afg" - - -def test_params_axis_modifiers(): - """ - Test the modifiers of the Axis class. - """ - assert str(Axis(annotation=True, angle=30)) == "a+a30" - assert str(Axis(annotation=True, angle="normal")) == "a+an" - assert str(Axis(annotation=True, angle="parallel")) == "a+ap" - - assert str(Axis(annotation=True, skip_edge=True)) == "a+e" - assert str(Axis(annotation=True, skip_edge="lower")) == "a+el" - assert str(Axis(annotation=True, skip_edge="upper")) == "a+eu" - - assert str(Axis(annotation=True, fancy=True)) == "a+f" - - assert str(Axis(annotation=True, label="My Label")) == "a+lMy Label" - assert str(Axis(annotation=True, hlabel="My HLabel")) == "a+LMy HLabel" - assert str(Axis(annotation=True, alt_label="Alt Label")) == "a+sAlt Label" - assert str(Axis(annotation=True, alt_hlabel="Alt HLabel")) == "a+SAlt HLabel" - - axis = Axis(annotation=True, label="My Label", alt_label="My HLabel") - assert str(axis) == "a+lMy Label+sMy HLabel" - - assert str(Axis(annotation=True, prefix="$")) == "a+p$" - - assert str(Axis(annotation=True, unit="km")) == "a+ukm" + assert str(Axis(interval="a1f0.5")) == "a1f0.5" + assert str(Axis(interval="a1f0.5", angle=30)) == "a1f0.5+a30" + assert str(Axis(interval="a1f0.5", angle=30, skip_edge="left")) == "a1f0.5+a30+el" + assert str(Axis(interval="a1f0.5", fancy=True)) == "a1f0.5+f" + assert str(Axis(interval="a1f0.5", label="My Label")) == "a1f0.5+lMy Label" + assert str(Axis(interval="a1f0.5", hlabel="My HLabel")) == "a1f0.5+LMy HLabel" + assert str(Axis(interval="a1f0.5", alt_label="Alt Label")) == "a1f0.5+sAlt Label" + assert str(Axis(interval="a1f0.5", alt_hlabel="Alt HLabel")) == "a1f0.5+SAlt HLabel" + assert str(Axis(interval="a1f0.5", unit="km")) == "a1f0.5+ukm" def test_params_axes(): """ Test the Axes class. """ - - assert str(Axes(axes="WSen")) == "WSen" - assert str(Axes(fill="lightred")) == "+glightred" - assert str(Axes(title="My Plot Title")) == "+tMy Plot Title" - assert str(Axes(subtitle="My Subtitle")) == "+sMy Subtitle" - - axes = Axes(axes="WSen", fill="lightred", title="My Plot Title") - assert str(axes) == "WSen+glightred+tMy Plot Title" - - -def test_params_frame(): - """ - Test the Frame class. - """ + assert ( + str(Axes("WSen", title="My Plot Title", fill="lightred")) + == "WSen+glightred+tMy Plot Title" + ) From e30969e84380f39a2d70d6a9b879deb9d108ce35 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 10:47:36 +0800 Subject: [PATCH 63/79] Revert "Add to API documentation" This reverts commit 6f97c57ad48b3e5f1bfe9466002357ce834db272. --- doc/api/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 0376ef29b5f..264f5a9175a 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -213,7 +213,6 @@ Class-style Parameters :template: autosummary/params.rst Box - Frame Pattern Position From 353a73da8d414a9a9aee73739b129cf747a48878 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 10:47:43 +0800 Subject: [PATCH 64/79] Revert "Add the Axes, Axis, Frame classes for the frame parameter" This reverts commit 02d87139ea165f2bd6f6237a7341564913bc02e2. --- pygmt/params/__init__.py | 1 - pygmt/params/frame.py | 139 ------------------------------- pygmt/tests/test_params_frame.py | 30 ------- 3 files changed, 170 deletions(-) delete mode 100644 pygmt/params/frame.py delete mode 100644 pygmt/tests/test_params_frame.py diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py index 46e61fe964f..d1a00a7f5f2 100644 --- a/pygmt/params/__init__.py +++ b/pygmt/params/__init__.py @@ -3,6 +3,5 @@ """ from pygmt.params.box import Box -from pygmt.params.frame import Axes, Axis, Frame from pygmt.params.pattern import Pattern from pygmt.params.position import Position diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py deleted file mode 100644 index 6c4c2969cfa..00000000000 --- a/pygmt/params/frame.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -The Axes, Axis, and Frame classes for specifying the frame. -""" - -import dataclasses -from typing import Any, Literal - -from pygmt.alias import Alias -from pygmt.params.base import BaseParam - - -@dataclasses.dataclass(repr=False) -class Axis(BaseParam): - """ - Class for setting up one axis of a plot. - """ - - #: Intervals for annotations and major tick spacing, minor tick spacing, and/or - #: grid line spacing. - interval: float | str - - #: Plot slanted annotations (for Cartesian plots only), where *angle* is measured - #: with respect to the horizontal and must be in the -90 <= *angle* <= 90 range. - #: Default is normal (i.e., ``angle=90``) for y-axis and parallel (i.e., - #: ``angle=0``) for x-axis annotations. These defaults can be changed via - #: :gmt-term:`MAP_ANNOT_ORTHO`. - angle: float | None = None - - #: Skip annotations that fall exactly at the ends of the axis. Choose from ``left`` - #: or ``right`` to skip only the lower or upper annotation, respectively, or - #: ``True`` to skip both. - skip_edge: Literal["left", "right"] | bool = False - - #: Give fancy annotations with W|E|S|N suffixes encoding the sign (for geographic - #: axes only). - fancy: bool = False - - #: Add a label to the axis (for Cartesian plots only). The label is placed parallel - #: to the axis by default; use **hlabel** to force a horizontal label for y-axis, - #: which is useful for very short labels. - label: str | None = None - hlabel: str | None = None - - #: Add an alternate label for the right or upper axes. The label is placed parallel - #: to the axis by default; use **alt_hlabel** to force a horizontal label for - #: y-axis, which is useful for very short labels. [For Cartesian plots only]. - alt_label: str | None = None - alt_hlabel: str | None = None - - #: Add a leading text prefix for axis annotation (e.g., dollar sign for plots - #: related to money) (for Cartesian plots only). For geographic maps the addition - #: of degree symbols, etc. is automatic and controlled by - #: :gmt-term:`FORMAT_GEO_MAP`. - prefix: str | None = None - - #: Append a unit to the annotations (for Cartesian plots only). For geographic maps - #: the addition of degree symbols, etc. is automatic and controlled by - #: :gmt-term:`FORMAT_GEO_MAP`. - unit: str | None = None - - @property - def _aliases(self): - return [ - Alias(self.interval, name="interval"), - Alias(self.angle, name="angle", prefix="+a"), - Alias( - self.skip_edge, - name="skip_edge", - prefix="+e", - mapping={True: True, "left": "l", "right": "r"}, - ), - Alias(self.fancy, name="fancy", prefix="+f"), - Alias(self.label, name="label", prefix="+l"), - Alias(self.hlabel, name="hlabel", prefix="+L"), - Alias(self.alt_label, name="alt_label", prefix="+s"), - Alias(self.alt_hlabel, name="alt_hlabel", prefix="+S"), - Alias(self.unit, name="unit", prefix="+u"), - ] - - -@dataclasses.dataclass(repr=False) -class Axes(BaseParam): - """ - Class for specifying the frame of a plot. - """ - - #: Specify which axes to draw and their attributes. - axes: str | None = None - - #: Fill for the interior of the canvas [Default is no fill]. This also sets fill - #: for the two back-walls in 3-D plots. - fill: str | None = None - - #: The title string centered above the plot frame [Default is no title]. - title: str | None = None - - #: The subtitle string beneath the title [Default is no subtitle]. This requires - #: ``title`` to be set. - subtitle: str | None = None - - @property - def _aliases(self): - return [ - Alias(self.axes, name="axes"), - Alias(self.fill, name="fill", prefix="+g"), - Alias(self.title, name="title", prefix="+t"), - Alias(self.subtitle, name="subtitle", prefix="+s"), - ] - - -@dataclasses.dataclass(repr=False) -class Frame(BaseParam): - """ - Class for setting up the frame of a plot. - """ - - axes: Any = None - xaxis: Any = None - yaxis: Any = None - zaxis: Any = None - - @property - def _aliases(self): - return [ - Alias(self.axes), - Alias(self.xaxis, prefix="x"), - Alias(self.yaxis, prefix="y"), - Alias(self.zaxis, prefix="z"), - ] - - def __iter__(self): - """ - Iterate over the aliases of the class. - - Yields - ------ - The value of each alias in the class. None are excluded. - """ - yield from (alias._value for alias in self._aliases if alias._value is not None) diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py deleted file mode 100644 index 85de1bb406b..00000000000 --- a/pygmt/tests/test_params_frame.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Test the Frame/Axes/Axis classes. -""" - -from pygmt.params import Axes, Axis - - -def test_params_axis(): - """ - Test the Axis class. - """ - assert str(Axis(interval="a1f0.5")) == "a1f0.5" - assert str(Axis(interval="a1f0.5", angle=30)) == "a1f0.5+a30" - assert str(Axis(interval="a1f0.5", angle=30, skip_edge="left")) == "a1f0.5+a30+el" - assert str(Axis(interval="a1f0.5", fancy=True)) == "a1f0.5+f" - assert str(Axis(interval="a1f0.5", label="My Label")) == "a1f0.5+lMy Label" - assert str(Axis(interval="a1f0.5", hlabel="My HLabel")) == "a1f0.5+LMy HLabel" - assert str(Axis(interval="a1f0.5", alt_label="Alt Label")) == "a1f0.5+sAlt Label" - assert str(Axis(interval="a1f0.5", alt_hlabel="Alt HLabel")) == "a1f0.5+SAlt HLabel" - assert str(Axis(interval="a1f0.5", unit="km")) == "a1f0.5+ukm" - - -def test_params_axes(): - """ - Test the Axes class. - """ - assert ( - str(Axes("WSen", title="My Plot Title", fill="lightred")) - == "WSen+glightred+tMy Plot Title" - ) From 238eb2ee0992387c8f785c90873a432619dbebde Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 10:51:08 +0800 Subject: [PATCH 65/79] Fix an inline doctest --- pygmt/src/inset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/inset.py b/pygmt/src/inset.py index ac1ac79947a..3a363e54aa7 100644 --- a/pygmt/src/inset.py +++ b/pygmt/src/inset.py @@ -109,7 +109,7 @@ def inset( Examples -------- >>> import pygmt - >>> from pygmt.params import Box + >>> from pygmt.params import Box, Position >>> >>> # Create the larger figure >>> fig = pygmt.Figure() @@ -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", anchor_offset=0.2, width="3c") + >>> fig.logo(Position("BR", type="inside", offset=0.2), width="3c") >>> fig.show() """ self._activate_figure() From d2ed7b7cb9d90749c4c7d4e4b6d3726e9e654462 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 10:51:49 +0800 Subject: [PATCH 66/79] Improve the import in examples/gallery/embellishments/gmt_logo.py --- examples/gallery/embellishments/gmt_logo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/gallery/embellishments/gmt_logo.py b/examples/gallery/embellishments/gmt_logo.py index d8f0d14470d..28e55bd89ab 100644 --- a/examples/gallery/embellishments/gmt_logo.py +++ b/examples/gallery/embellishments/gmt_logo.py @@ -7,11 +7,12 @@ # %% import pygmt +from pygmt.params import Position fig = pygmt.Figure() fig.basemap(region=[0, 10, 0, 2], projection="X6c", frame=True) # 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=pygmt.params.Position("TR", offset=(0.3, 0.6)), width="3c") +fig.logo(position=Position("TR", offset=(0.3, 0.6)), width="3c") fig.show() From 9e9c0d686bb6873509661335fb69f0ca4fcba17b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 10:52:44 +0800 Subject: [PATCH 67/79] Fix the inline doctest in inset --- 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 3a363e54aa7..935007023d8 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", type="inside", offset=0.2), width="3c") + >>> fig.logo(position=Position("BR", type="inside", offset=0.2), width="3c") >>> fig.show() """ self._activate_figure() From 271af6817229fee9fa4ab9f1b4c4035a6c31e3b2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Dec 2025 11:08:58 +0800 Subject: [PATCH 68/79] Reorder parameters --- pygmt/src/logo.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index cf0a573b1b4..f6538d2674e 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -16,12 +16,12 @@ def logo( # noqa: PLR0913 self, position: Position | None = None, - height: float | str | None = None, width: float | str | None = None, + height: float | str | None = None, + box: Box | bool = False, + style: Literal["standard", "url", "no_label"] = "standard", projection: str | None = None, region: Sequence[float | str] | str | None = None, - style: Literal["standard", "url", "no_label"] = "standard", - box: Box | bool = False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, panel: int | Sequence[int] | bool = False, @@ -38,10 +38,7 @@ def logo( # noqa: PLR0913 :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 - 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. + relative to the current plot origin. Full GMT docs at :gmt-docs:`gmtlogo.html`. @@ -63,8 +60,8 @@ def logo( # noqa: PLR0913 Parameters ---------- position - Specify the position of the GMT logo. See the :class:`pygmt.params.Position` - class for details. + Specify the position of the GMT logo. See :class:`pygmt.params.Position` for + details. width height Width or height of the GMT logo. Since the aspect ratio is fixed, only one of From c9c422261787e1ac36e03229528e8ae85748eb80 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 7 Dec 2025 14:01:53 +0800 Subject: [PATCH 69/79] Rename position to refpoint --- pygmt/params/position.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 6276bacabd6..ef6072a9f75 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -41,43 +41,42 @@ class Position(BaseParam): **Reference Point** The *reference point* can be specified in five different ways using the ``type`` and - ``location`` attributes: + ``refpoint`` attributes: ``type="mapcoords"`` Map Coordinates - Use data/geographic coordinates. Specify ``location`` as + Use data/geographic coordinates. Specify ``refpoint`` as (*longitude*, *latitude*). Useful when tying the embellishment to a specific geographic location. - **Example:** ``location=(135, 20), type="mapcoords"``. - + **Example:** ``refpoint=(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). + ``refpoint`` as (*x*, *y*) with units (e.g., inches, centimeters, points). Useful for precise layout control. - **Example:** ``location=("2c", "2.5c"), type="plotcoords"`` + **Example:** ``refpoint=("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 of the bounding box of the current plot. Specify - ``location`` as (*nx*, *ny*). Useful for positioning relative to plot dimensions + ``refpoint`` as (*nx*, *ny*). Useful for positioning relative to plot dimensions without units. - **Example:** ``location=(0.2, 0.1), type="boxcoords"`` + **Example:** ``refpoint=(0.2, 0.1), type="boxcoords"`` ``type="inside"`` Inside Plot Select one of the nine :doc:`justification codes ` as the *reference point*. The *anchor point* defaults to be the same as the *reference point*, so the embellishment is placed inside the plot. - **Example:** ``location="TL", type="inside"`` [anchor point defaults to "TL"] + **Example:** ``refpoint="TL", type="inside"`` [anchor point defaults to "TL"] ``type="outside"`` Outside Plot Similar to ``type="inside"``, but the *anchor point* defaults to the mirror opposite of the *reference point*. Useful for placing embellishments outside the plot boundaries (e.g., color bars). - **Example:** ``location="TL", type="outside"`` [anchor point defaults to "BR"] + **Example:** ``refpoint="TL", type="outside"`` [anchor point defaults to "BR"] **Anchor Point** @@ -130,7 +129,7 @@ class Position(BaseParam): #: - ``type="boxcoords"``: (*nx*, *ny*) #: - ``type="inside"`` or ``"outside"``: #: :doc:`2-character justification codes ` - location: Sequence[float | str] | AnchorCode + refpoint: Sequence[float | str] | AnchorCode #: Type of the reference point. Valid values are: #: @@ -139,7 +138,7 @@ class Position(BaseParam): #: - ``"boxcoords"``: Normalized coordinates #: - ``"inside"`` or ``"outside"``: Justification codes #: - #: If not specified, defaults to ``"inside"`` if ``location`` is a justification + #: If not specified, defaults to ``"inside"`` if ``refpoint`` is a justification #: code; otherwise defaults to ``"plotcoords"``. type: ( Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] | None @@ -164,21 +163,21 @@ def _validate(self): # 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" + self.type = "inside" if isinstance(self.refpoint, str) else "plotcoords" # Validate the location based on type. match self.type: case "mapcoords" | "plotcoords" | "boxcoords": - if not is_nonstr_iter(self.location) or len(self.location) != 2: + if not is_nonstr_iter(self.refpoint) or len(self.refpoint) != 2: raise GMTValueError( - self.location, + self.refpoint, description="reference point", reason="Expect a sequence of two values.", ) case "inside" | "outside": - if self.location not in _valid_anchors: + if self.refpoint not in _valid_anchors: raise GMTValueError( - self.location, + self.refpoint, description="reference point", reason="Expect a valid 2-character justification code.", ) @@ -204,7 +203,7 @@ def _aliases(self): "outside": "J", }, ), - Alias(self.location, name="location", sep="/", size=2), + Alias(self.refpoint, name="refpoint", sep="/", size=2), Alias(self.anchor, name="anchor", prefix="+j"), Alias(self.offset, name="offset", prefix="+o", sep="/", size=2), ] From 0064cde007612e6ac8720c4547dcc4d61a070e34 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 7 Dec 2025 14:08:29 +0800 Subject: [PATCH 70/79] Fix formatting --- pygmt/params/position.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index ef6072a9f75..9b0d8ab440b 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -69,14 +69,16 @@ class Position(BaseParam): as the *reference point*. The *anchor point* defaults to be the same as the *reference point*, so the embellishment is placed inside the plot. - **Example:** ``refpoint="TL", type="inside"`` [anchor point defaults to "TL"] + **Example:** ``refpoint="TL", type="inside"`` [anchor point defaults to + ``"TL"``] ``type="outside"`` Outside Plot Similar to ``type="inside"``, but the *anchor point* defaults to the mirror opposite of the *reference point*. Useful for placing embellishments outside the plot boundaries (e.g., color bars). - **Example:** ``refpoint="TL", type="outside"`` [anchor point defaults to "BR"] + **Example:** ``refpoint="TL", type="outside"`` [anchor point defaults to + ``"BR"``] **Anchor Point** From bd223e2964cee4fc52ee0fc36343501eed51d810 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 7 Dec 2025 23:59:54 +0800 Subject: [PATCH 71/79] Use any() to simplify codes --- 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 f6538d2674e..d9a3ceeed1c 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -88,7 +88,7 @@ def logo( # noqa: PLR0913 # 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): + if isinstance(position, str) and any(v is not None for v in (height, width)): msg = ( "Parameter 'position' is given with a raw GMT command string, and conflicts " "with parameters 'height', and 'width'. " From 97a13f1d7d86cc4e57fe4a0596ba16f63b5398fb Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 8 Dec 2025 00:01:01 +0800 Subject: [PATCH 72/79] Improve error message --- 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 d9a3ceeed1c..56e0396b33b 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -88,10 +88,10 @@ def logo( # noqa: PLR0913 # Prior PyGMT v0.17.0, 'position' can accept a raw GMT CLI string. Check for # conflicts with other parameters. - if isinstance(position, str) and any(v is not None for v in (height, width)): + if isinstance(position, str) and any(v is not None for v in (width, height)): msg = ( "Parameter 'position' is given with a raw GMT command string, and conflicts " - "with parameters 'height', and 'width'. " + "with parameters 'width' and 'height'. " ) raise GMTInvalidInput(msg) From 91b56305185d7db7ea1e29790598e5a6fa57e017 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 8 Dec 2025 10:53:24 +0800 Subject: [PATCH 73/79] r-string 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 56e0396b33b..8772b2d039f 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -29,7 +29,7 @@ def logo( # noqa: PLR0913 perspective: float | Sequence[float] | str | bool = False, **kwargs, ): - r""" + """ Plot the GMT logo. .. figure:: https://docs.generic-mapping-tools.org/6.6/_images/GMT_coverlogo.png From ff6392dde3216e7d57cfcae6db779aa06be3c91f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Dec 2025 10:38:10 +0800 Subject: [PATCH 74/79] Update pygmt/params/position.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/params/position.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 9b0d8ab440b..2c95c43d835 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -69,8 +69,7 @@ class Position(BaseParam): as the *reference point*. The *anchor point* defaults to be the same as the *reference point*, so the embellishment is placed inside the plot. - **Example:** ``refpoint="TL", type="inside"`` [anchor point defaults to - ``"TL"``] + **Example:** ``refpoint="TL", type="inside"`` ``type="outside"`` Outside Plot Similar to ``type="inside"``, but the *anchor point* defaults to the mirror From 2310b22f636661de9e490cc82d765602b5dbdc8d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Dec 2025 10:38:26 +0800 Subject: [PATCH 75/79] Update pygmt/params/position.py [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/params/position.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 2c95c43d835..d192c0190ef 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -76,8 +76,7 @@ class Position(BaseParam): opposite of the *reference point*. Useful for placing embellishments outside the plot boundaries (e.g., color bars). - **Example:** ``refpoint="TL", type="outside"`` [anchor point defaults to - ``"BR"``] + **Example:** ``refpoint="TL", type="outside"`` **Anchor Point** From a3185e8729eb2e5f7a945e09404b383f3f301e5b Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Dec 2025 10:39:51 +0800 Subject: [PATCH 76/79] Fix styling --- 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 d192c0190ef..58991bbc7dd 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -48,7 +48,8 @@ class Position(BaseParam): (*longitude*, *latitude*). Useful when tying the embellishment to a specific geographic location. - **Example:** ``refpoint=(135, 20), type="mapcoords"``. + **Example:** ``refpoint=(135, 20), type="mapcoords"`` + ``type="plotcoords"`` Plot Coordinates Use plot coordinates as distances from the lower-left plot origin. Specify ``refpoint`` as (*x*, *y*) with units (e.g., inches, centimeters, points). From e153ebf12936541a3ddd36b92aebc6e76ebc3bf0 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Dec 2025 11:26:20 +0800 Subject: [PATCH 77/79] Rename type to cstype --- pygmt/params/position.py | 68 +++++++++++++++-------------- pygmt/tests/test_params_position.py | 36 +++++++-------- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/pygmt/params/position.py b/pygmt/params/position.py index 58991bbc7dd..bb8df3c1ae9 100644 --- a/pygmt/params/position.py +++ b/pygmt/params/position.py @@ -40,44 +40,44 @@ class Position(BaseParam): **Reference Point** - The *reference point* can be specified in five different ways using the ``type`` and - ``refpoint`` attributes: + The *reference point* can be specified in five different ways using the ``cstype`` + and ``refpoint`` attributes: - ``type="mapcoords"`` Map Coordinates + ``cstype="mapcoords"`` Map Coordinates Use data/geographic coordinates. Specify ``refpoint`` as (*longitude*, *latitude*). Useful when tying the embellishment to a specific geographic location. - **Example:** ``refpoint=(135, 20), type="mapcoords"`` + **Example:** ``refpoint=(135, 20), cstype="mapcoords"`` - ``type="plotcoords"`` Plot Coordinates + ``cstype="plotcoords"`` Plot Coordinates Use plot coordinates as distances from the lower-left plot origin. Specify ``refpoint`` as (*x*, *y*) with units (e.g., inches, centimeters, points). Useful for precise layout control. - **Example:** ``refpoint=("2c", "2.5c"), type="plotcoords"`` + **Example:** ``refpoint=("2c", "2.5c"), cstype="plotcoords"`` - ``type="boxcoords"`` Normalized Coordinates + ``cstype="boxcoords"`` Normalized Coordinates Use normalized coordinates where (0, 0) is the lower-left corner and (1, 1) is the upper-right corner of the bounding box of the current plot. Specify ``refpoint`` as (*nx*, *ny*). Useful for positioning relative to plot dimensions without units. - **Example:** ``refpoint=(0.2, 0.1), type="boxcoords"`` + **Example:** ``refpoint=(0.2, 0.1), cstype="boxcoords"`` - ``type="inside"`` Inside Plot + ``cstype="inside"`` Inside Plot Select one of the nine :doc:`justification codes ` as the *reference point*. The *anchor point* defaults to be the same as the *reference point*, so the embellishment is placed inside the plot. - **Example:** ``refpoint="TL", type="inside"`` + **Example:** ``refpoint="TL", cstype="inside"`` - ``type="outside"`` Outside Plot - Similar to ``type="inside"``, but the *anchor point* defaults to the mirror + ``cstype="outside"`` Outside Plot + Similar to ``cstype="inside"``, but the *anchor point* defaults to the mirror opposite of the *reference point*. Useful for placing embellishments outside the plot boundaries (e.g., color bars). - **Example:** ``refpoint="TL", type="outside"`` + **Example:** ``refpoint="TL", cstype="outside"`` **Anchor Point** @@ -88,9 +88,9 @@ class Position(BaseParam): Set ``anchor`` explicitly to override these defaults. If not set, the default *anchor* behaviors are: - - ``type="inside"``: Same as the *reference point* justification code - - ``type="outside"``: Mirror opposite of the *reference point* justification code - - Other types: ``"MC"`` (middle center) for map rose and scale, ``"BL"`` + - ``cstype="inside"``: Same as the *reference point* justification code + - ``cstype="outside"``: Mirror opposite of the *reference point* justification code + - Other cstypes: ``"MC"`` (middle center) for map rose and scale, ``"BL"`` (bottom-left) for other embellishments **Offset** @@ -110,7 +110,9 @@ class Position(BaseParam): >>> fig = pygmt.Figure() >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c", frame=True) >>> fig.logo( - ... position=Position((3, 3), type="mapcoords", anchor="ML", offset=(0.2, 0.2)), + ... position=Position( + ... (3, 3), cstype="mapcoords", anchor="ML", offset=(0.2, 0.2) + ... ), ... box=True, ... ) >>> fig.show() @@ -119,20 +121,20 @@ class Position(BaseParam): >>> 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.logo(position=Position("TL", cstype="inside", offset="0.2c"), box=True) >>> fig.show() """ - #: Location of the reference point on the plot. The format depends on ``type``: + #: Location of the reference point on the plot. The format depends on ``cstype``: #: - #: - ``type="mapcoords"``: (*longitude*, *latitude*) - #: - ``type="plotcoords"``: (*x*, *y*) with plot units - #: - ``type="boxcoords"``: (*nx*, *ny*) - #: - ``type="inside"`` or ``"outside"``: + #: - ``cstype="mapcoords"``: (*longitude*, *latitude*) + #: - ``cstype="plotcoords"``: (*x*, *y*) with plot units + #: - ``cstype="boxcoords"``: (*nx*, *ny*) + #: - ``cstype="inside"`` or ``"outside"``: #: :doc:`2-character justification codes ` refpoint: Sequence[float | str] | AnchorCode - #: Type of the reference point. Valid values are: + #: cstype of the reference point. Valid values are: #: #: - ``"mapcoords"``: Map/Data coordinates #: - ``"plotcoords"``: Plot coordinates @@ -141,13 +143,13 @@ class Position(BaseParam): #: #: If not specified, defaults to ``"inside"`` if ``refpoint`` is a justification #: code; otherwise defaults to ``"plotcoords"``. - type: ( + cstype: ( Literal["mapcoords", "inside", "outside", "boxcoords", "plotcoords"] | None ) = None #: Anchor point on the embellishment using a #: :doc:`2-character justification code `. - #: If ``None``, defaults are applied based on ``type`` (see above). + #: If ``None``, defaults are applied based on ``cstype`` (see above). anchor: AnchorCode | None = None #: Offset for the anchor point as a single value or (*offset_x*, *offset_y*). @@ -162,12 +164,12 @@ def _validate(self): 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.refpoint, str) else "plotcoords" + # Default to "inside" if cstype is not specified and location is an anchor code. + if self.cstype is None: + self.cstype = "inside" if isinstance(self.refpoint, str) else "plotcoords" - # Validate the location based on type. - match self.type: + # Validate the location based on cstype. + match self.cstype: case "mapcoords" | "plotcoords" | "boxcoords": if not is_nonstr_iter(self.refpoint) or len(self.refpoint) != 2: raise GMTValueError( @@ -194,8 +196,8 @@ def _validate(self): def _aliases(self): return [ Alias( - self.type, - name="type", + self.cstype, + name="cstype", mapping={ "mapcoords": "g", "boxcoords": "n", diff --git a/pygmt/tests/test_params_position.py b/pygmt/tests/test_params_position.py index 484ec1b63d0..a9d67de36c5 100644 --- a/pygmt/tests/test_params_position.py +++ b/pygmt/tests/test_params_position.py @@ -7,28 +7,28 @@ from pygmt.params import Position -def test_params_position_types(): +def test_params_position_cstypes(): """ - Test the Position class with different types of coordinate systems. + Test the Position class with different cstypes of coordinate systems. """ - # Default type is "plotcoords" for (x,y) and "inside" for anchor codes. + # Default cstype 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("MR", type="inside")) == "jMR" - assert str(Position("BR", type="outside")) == "JBR" + assert str(Position((10, 20), cstype="mapcoords")) == "g10/20" + assert str(Position((0.1, 0.2), cstype="boxcoords")) == "n0.1/0.2" + assert str(Position(("5c", "3c"), cstype="plotcoords")) == "x5c/3c" + assert str(Position("MR", cstype="inside")) == "jMR" + assert str(Position("BR", cstype="outside")) == "JBR" def test_params_position_anchor_offset(): """ Test the Position class with anchor and offset parameters. """ - assert str(Position((10, 20), type="mapcoords", anchor="TL")) == "g10/20+jTL" - assert str(Position((10, 20), type="mapcoords", offset=(1, 2))) == "g10/20+o1/2" - pos = Position("TL", type="inside", anchor="MC", offset=("1c", "2c")) + assert str(Position((10, 20), cstype="mapcoords", anchor="TL")) == "g10/20+jTL" + assert str(Position((10, 20), cstype="mapcoords", offset=(1, 2))) == "g10/20+o1/2" + pos = Position("TL", cstype="inside", anchor="MC", offset=("1c", "2c")) assert str(pos) == "jTL+jMC+o1c/2c" assert str(Position("TL", anchor="BR", offset=0.5)) == "jTL+jBR+o0.5" @@ -38,17 +38,17 @@ def test_params_position_invalid_location(): Test that invalid location inputs raise GMTValueError. """ with pytest.raises(GMTValueError): - Position("invalid", type="mapcoords") + Position("invalid", cstype="mapcoords") with pytest.raises(GMTValueError): - Position((1, 2, 3), type="mapcoords") + Position((1, 2, 3), cstype="mapcoords") with pytest.raises(GMTValueError): - Position(5, type="plotcoords") + Position(5, cstype="plotcoords") with pytest.raises(GMTValueError): - Position((0.5,), type="boxcoords") + Position((0.5,), cstype="boxcoords") with pytest.raises(GMTValueError): - Position((10, 20), type="inside") + Position((10, 20), cstype="inside") with pytest.raises(GMTValueError): - Position("TT", type="outside") + Position("TT", cstype="outside") def test_params_position_invalid_anchor(): @@ -56,4 +56,4 @@ def test_params_position_invalid_anchor(): Test that invalid anchor inputs raise GMTValueError. """ with pytest.raises(GMTValueError): - Position((10, 20), type="mapcoords", anchor="XX") + Position((10, 20), cstype="mapcoords", anchor="XX") From fe5c47a5d981f6e900d88e1ddafe5b5c4e905991 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Dec 2025 22:05:35 +0800 Subject: [PATCH 78/79] Fix type to cstype --- pygmt/src/inset.py | 2 +- pygmt/src/logo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/inset.py b/pygmt/src/inset.py index 935007023d8..2a787244a7f 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=Position("BR", type="inside", offset=0.2), width="3c") + >>> fig.logo(position=Position("BR", cstype="inside", offset=0.2), width="3c") >>> fig.show() """ self._activate_figure() diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 8772b2d039f..1636f34e6a4 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -91,7 +91,7 @@ def logo( # noqa: PLR0913 if isinstance(position, str) and any(v is not None for v in (width, height)): msg = ( "Parameter 'position' is given with a raw GMT command string, and conflicts " - "with parameters 'width' and 'height'. " + "with parameters 'width' and 'height'." ) raise GMTInvalidInput(msg) From 3a48d4d0a0c64401cbcdb3dbbd91aed009485353 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 11 Dec 2025 09:59:47 +0800 Subject: [PATCH 79/79] cstype="inside" is the default --- 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 2a787244a7f..36a3778369f 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=Position("BR", cstype="inside", offset=0.2), width="3c") + >>> fig.logo(position=Position("BR", offset=0.2), width="3c") >>> fig.show() """ self._activate_figure()