From eec0fb74bd7e16dad6ec769ef64e50f532a74f97 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 16 Nov 2025 11:56:45 +0800 Subject: [PATCH 01/49] 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 02/49] 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 03/49] 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 04/49] 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 05/49] 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 06/49] 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 07/49] 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 08/49] 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 09/49] 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 10/49] 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 11/49] 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 12/49] 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 13/49] 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 14/49] 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 15/49] 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 16/49] 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 17/49] 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 18/49] 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 19/49] 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 20/49] 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 18b90b3e5f412cbbc8902577306f6a74d6d7b09a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 25 Nov 2025 20:15:14 +0800 Subject: [PATCH 21/49] 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 22/49] 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 23/49] 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 24/49] 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 25/49] 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 26/49] 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 c9c422261787e1ac36e03229528e8ae85748eb80 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 7 Dec 2025 14:01:53 +0800 Subject: [PATCH 31/49] 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 32/49] 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 ff6392dde3216e7d57cfcae6db779aa06be3c91f Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 9 Dec 2025 10:38:10 +0800 Subject: [PATCH 33/49] 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 34/49] 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 35/49] 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 36/49] 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 bd6a33fb41b16b43a11be121f4b7a9e77ff6d2a2 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Jul 2025 15:22:43 +0800 Subject: [PATCH 37/49] Add Figure.scalebar to plot a scale bar on maps --- pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/scalebar.py | 88 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 pygmt/src/scalebar.py diff --git a/pygmt/figure.py b/pygmt/figure.py index 56ad2c3d5cf..3c48080c482 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -427,6 +427,7 @@ def _repr_html_(self) -> str: plot3d, psconvert, rose, + scalebar, set_panel, shift_origin, solar, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..2aa4e6b5587 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -43,6 +43,7 @@ from pygmt.src.project import project from pygmt.src.psconvert import psconvert from pygmt.src.rose import rose +from pygmt.src.scalebar import scalebar from pygmt.src.select import select from pygmt.src.shift_origin import shift_origin from pygmt.src.solar import solar diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py new file mode 100644 index 00000000000..f24e8cada63 --- /dev/null +++ b/pygmt/src/scalebar.py @@ -0,0 +1,88 @@ +""" +scalebar - Add a scale bar. +""" + +from collections.abc import Sequence +from typing import Literal + +from pygmt._typing import AnchorCode +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.helpers import build_arg_list + + +def scalebar( # noqa: PLR0913 + self, + position, + length, + position_type: Literal["user", "justify", "mirror", "normalize", "plot"] + | None = None, + label_alignment: Literal["left", "right", "top", "bottom"] | None = None, + scale_position=None, + justify: AnchorCode | None = None, + offset: Sequence[float | str] | None = None, + label: str | bool = False, + fancy: bool = False, + unit: bool = False, + vertical: bool = False, + box=None, +): + """ + Add a scale bar. + + Parameters + ---------- + TODO + + Examples + -------- + >>> import pygmt + >>> from pygmt.params import Box + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) + >>> fig.scalebar( + ... position=(10, 10), + ... position_type="user", + ... length=1000, + ... fancy=True, + ... label="Scale", + ... unit=True, + ... ) + >>> fig.show() + """ + self._preprocess() + + aliasdict = AliasSystem( + L=[ + Alias( + position_type, + name="position_type", + mapping={ + "user": "g", + "justify": "j", + "mirror": "J", + "normalize": "n", + "plot": "x", + }, + ), + Alias(position, name="position", separator="/"), + Alias(length, name="length", prefix="+w"), + Alias( + label_alignment, + name="label_alignment", + prefix="+a", + mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"}, + ), + Alias(scale_position, name="scale_position", prefix="+c", separator="/"), + Alias(fancy, name="fancy", prefix="+f"), + Alias(justify, name="justify", prefix="+j"), + Alias(label, name="label", prefix="+l"), + Alias(offset, name="offset", prefix="+o", separator="/", size=[1, 2]), + Alias(unit, name="unit", prefix="+u"), + Alias(vertical, name="vertical", prefix="+v"), + ], + F=Alias(box, name="box"), + ) + + with Session() as lib: + lib.call_module(module="basemap", args=build_arg_list(aliasdict)) From 4356536706befcfe710d23fd5002e90975174d4e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 7 Aug 2025 16:27:13 +0800 Subject: [PATCH 38/49] Use new values for position_type --- pygmt/src/scalebar.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index f24e8cada63..805e2295295 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -15,8 +15,9 @@ def scalebar( # noqa: PLR0913 self, position, length, - position_type: Literal["user", "justify", "mirror", "normalize", "plot"] - | None = None, + position_type: Literal[ + "mapcoords", "inside", "outside", "boxcoords", "plotcoords" + ] = "mapcoords", label_alignment: Literal["left", "right", "top", "bottom"] | None = None, scale_position=None, justify: AnchorCode | None = None, @@ -30,6 +31,24 @@ def scalebar( # noqa: PLR0913 """ Add a scale bar. + Parameters + ---------- + position/position_type + Location of the rose. 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. + Parameters ---------- TODO @@ -58,14 +77,14 @@ def scalebar( # noqa: PLR0913 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", separator="/", size=2), Alias(length, name="length", prefix="+w"), Alias( label_alignment, From 02a2e77e720298c78e80bb2b1245407172f95cb5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 7 Aug 2025 16:38:21 +0800 Subject: [PATCH 39/49] Use new values for position_type --- pygmt/src/scalebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 805e2295295..8249a5d7123 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -61,7 +61,7 @@ def scalebar( # noqa: PLR0913 >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) >>> fig.scalebar( ... position=(10, 10), - ... position_type="user", + ... position_type="mapcoords", ... length=1000, ... fancy=True, ... label="Scale", From 8aaef2f74d34aa10e305c86a130b467a1d2e7718 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 10 Aug 2025 20:37:31 +0800 Subject: [PATCH 40/49] Updates --- doc/api/index.rst | 1 + pygmt/src/scalebar.py | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/doc/api/index.rst b/doc/api/index.rst index 264f5a9175a..9b312ca2f9f 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -31,6 +31,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.scalebar Figure.solar Figure.text Figure.timestamp diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 8249a5d7123..a5980d932f2 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -16,12 +16,16 @@ def scalebar( # noqa: PLR0913 position, length, position_type: Literal[ - "mapcoords", "inside", "outside", "boxcoords", "plotcoords" + "mapcoords", + "boxcoords", + "plotcoords", + "inside", + "outside", ] = "mapcoords", label_alignment: Literal["left", "right", "top", "bottom"] | None = None, scale_position=None, justify: AnchorCode | None = None, - offset: Sequence[float | str] | None = None, + anchor_offset: Sequence[float | str] | None = None, label: str | bool = False, fancy: bool = False, unit: bool = False, @@ -34,20 +38,21 @@ def scalebar( # noqa: PLR0913 Parameters ---------- position/position_type - Location of the rose. The actual meaning of this parameter depends on the - ``position_type`` parameter. + Location of the map scale bar. 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. + 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. Parameters ---------- @@ -78,13 +83,13 @@ def scalebar( # noqa: PLR0913 name="position_type", mapping={ "mapcoords": "g", - "inside": "j", - "outside": "J", "boxcoords": "n", "plotcoords": "x", + "inside": "j", + "outside": "J", }, ), - Alias(position, name="position", separator="/", size=2), + Alias(position, name="position", sep="/", size=2), Alias(length, name="length", prefix="+w"), Alias( label_alignment, @@ -92,11 +97,13 @@ def scalebar( # noqa: PLR0913 prefix="+a", mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"}, ), - Alias(scale_position, name="scale_position", prefix="+c", separator="/"), + Alias(scale_position, name="scale_position", prefix="+c", sep="/"), Alias(fancy, name="fancy", prefix="+f"), Alias(justify, name="justify", prefix="+j"), Alias(label, name="label", prefix="+l"), - Alias(offset, name="offset", prefix="+o", separator="/", size=[1, 2]), + Alias( + anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=[1, 2] + ), Alias(unit, name="unit", prefix="+u"), Alias(vertical, name="vertical", prefix="+v"), ], From 1dfff80205ca2868ea1dcd7f4b62ccae1aead5ca Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 6 Sep 2025 23:30:44 +0800 Subject: [PATCH 41/49] Add docstrings --- pygmt/src/scalebar.py | 144 +++++++++++++++++++++++++++++++----------- 1 file changed, 108 insertions(+), 36 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index a5980d932f2..6607cbf7652 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -8,55 +8,118 @@ from pygmt._typing import AnchorCode from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInputError from pygmt.helpers import build_arg_list +from pygmt.params import Box def scalebar( # noqa: PLR0913 self, - position, - length, + position: Sequence[float | str] | AnchorCode | None = None, position_type: Literal[ - "mapcoords", - "boxcoords", - "plotcoords", - "inside", - "outside", - ] = "mapcoords", - label_alignment: Literal["left", "right", "top", "bottom"] | None = None, - scale_position=None, - justify: AnchorCode | None = None, + "mapcoords", "boxcoords", "plotcoords", "inside", "outside" + ] = "plotcoords", + anchor: AnchorCode | None = None, anchor_offset: Sequence[float | str] | None = None, + length: float | str | None = None, + label_alignment: Literal["left", "right", "top", "bottom"] | None = None, + scale_position: float | tuple[float, float] | bool = False, label: str | bool = False, fancy: bool = False, unit: bool = False, vertical: bool = False, - box=None, + box: Box | bool = False, + perspective: str | bool = False, + verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] + | bool = False, + transparency: float | None = None, ): """ - Add a scale bar. + Add a scale bar on the map. + + The scale bar is plotted at the location defined by the reference point (specified + by the **position** and *position_type** parameters) and anchor point (specified by + the **anchor** and **anchor_offset** parameters). Refer to + :doc:`/techref/reference_anchor_points` for details about the positioning. Parameters ---------- position/position_type - Location of the map scale bar. 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 map for the directional rose. The reference + point can be specified in five different ways, which is selected by the + **position_type** parameter. The actual reference point is then given by the + coordinates or code specified by the **position** parameter. + + The **position_type** parameter can be one of the following: + + - ``"mapcoords"``: **position** is given as (*longitude*, *latitude*) in map coordinates. - - ``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 directional rose, specified by one of the + :doc:`2-character justification codes `. + The default value depends on the **position_type** parameter. - Parameters - ---------- - TODO + - ``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*. + length + Length of the scale bar in km. You can append different units to the length, + which are: + - **e**: meters + - **f**: feet + - **k**: kilometers + - **M**: statute mile + - **n**: nautical miles + - **u**: US Survey foot + scale_position + Specify the location where on a geographic map the scale applies. It can be: + + - *slat*: Map scale is calculated for latitude *slat* + - (*slon*, *slat*): Map scale is calculated for latitude *slat* and longitude + *slon*, which is useful for oblique projections. + - ``True``: Map scale is calculated for the middle of the map. + - ``False``: Default to the location of the reference point. + label + Text string to use as the scale bar label. If ``False``, no label is drawn. If + ``True``, the distance unit provided in the **length** parameter (default is km) + is used as the label. The parameter requires ``fancy=True``. + label_alignment + Alignment of the scale bar label. Choose from "left", "right", "top", or + "bottom". [Default is "top"]. + fancy + If ``True``, draw a “fancy” scale bar. A fancy scale bar is a segmented bar with + alternating black and white rectangles. If ``False``, draw a plain scale bar. + unit + If ``True``, append the unit to all distance annotations along the scale. For a + plain scale, this will instead select the unit to be appended to the distance + length. The unit is determined from the suffix in the **length** or defaults to + km. + vertical + If ``True``, plot a vertical rather than a horizontal Cartesian scale. + box + Draw a background box behind the directional rose. If set to ``True``, a simple + rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box + appearance, pass a :class:`pygmt.params.Box` object to control style, fill, pen, + and other box properties. + {perspective} + {verbose} + {transparency} Examples -------- @@ -76,7 +139,15 @@ def scalebar( # noqa: PLR0913 """ self._preprocess() + if position is None: + msg = "Parameter 'position' must be specified." + raise GMTInvalidInputError(msg) + if length is None: + msg = "Parameter 'length' must be specified." + raise GMTInvalidInputError(msg) + aliasdict = AliasSystem( + F=Alias(box, name="box"), L=[ Alias( position_type, @@ -90,6 +161,8 @@ def scalebar( # noqa: PLR0913 }, ), Alias(position, name="position", sep="/", size=2), + Alias(anchor, name="justify", prefix="+j"), + Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), Alias(length, name="length", prefix="+w"), Alias( label_alignment, @@ -97,17 +170,16 @@ def scalebar( # noqa: PLR0913 prefix="+a", mapping={"left": "l", "right": "r", "top": "t", "bottom": "b"}, ), - Alias(scale_position, name="scale_position", prefix="+c", sep="/"), + Alias(scale_position, name="scale_position", prefix="+c", sep="/", size=2), Alias(fancy, name="fancy", prefix="+f"), - Alias(justify, name="justify", prefix="+j"), Alias(label, name="label", prefix="+l"), - Alias( - anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=[1, 2] - ), Alias(unit, name="unit", prefix="+u"), Alias(vertical, name="vertical", prefix="+v"), ], - F=Alias(box, name="box"), + p=Alias(perspective, name="perspective"), + ).add_common( + V=verbose, + t=transparency, ) with Session() as lib: From 37d02bcdbc7adce0eb2b09a70d14eb818a2c0520 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 26 Nov 2025 15:33:53 +0800 Subject: [PATCH 42/49] fix --- pygmt/src/scalebar.py | 74 +++++++------------------------------------ 1 file changed, 11 insertions(+), 63 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 6607cbf7652..92d0ae6c709 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -2,25 +2,18 @@ scalebar - Add a scale bar. """ -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 GMTInvalidInputError from pygmt.helpers import build_arg_list -from pygmt.params import Box +from pygmt.params import Box, Position def scalebar( # noqa: PLR0913 self, - position: Sequence[float | str] | AnchorCode | None = None, - position_type: Literal[ - "mapcoords", "boxcoords", "plotcoords", "inside", "outside" - ] = "plotcoords", - anchor: AnchorCode | None = None, - anchor_offset: Sequence[float | str] | None = None, + position: Position, length: float | str | None = None, label_alignment: Literal["left", "right", "top", "bottom"] | None = None, scale_position: float | tuple[float, float] | bool = False, @@ -44,40 +37,9 @@ def scalebar( # noqa: PLR0913 Parameters ---------- - position/position_type - Specify the reference point on the map for the directional rose. The reference - point can be specified in five different ways, which is selected by the - **position_type** parameter. The actual reference point is then given by the - coordinates or code specified by the **position** parameter. - - The **position_type** parameter can be one of the following: - - - ``"mapcoords"``: **position** is given as (*longitude*, *latitude*) in map - coordinates. - - ``"boxcoords"``: **position** is given as (*nx*, *ny*) in normalized - coordinates, i.e., fractional coordinates between 0 and 1 in both the x and y - directions. For example, (0, 0) is the lower-left corner and (1, 1) is the - upper-right corner of the plot bounding box. - - ``"plotcoords"``: **position** is given as (x, y) in plot coordinates, i.e., - the distances in inches, centimeters, or points from the lower left plot - origin. - - ``"inside"`` or ``"outside"``: **position** is one of the nine - :doc:`2-character justification codes `, meaning - placing the reference point at specific locations, either inside or outside - the plot bounding box. - anchor - Anchor point of the directional rose, specified by one of the - :doc:`2-character justification codes `. - The default value depends on the **position_type** parameter. - - - ``position_type="inside"``: **anchor** defaults to the same as **position**. - - ``position_type="outside"``: **anchor** defaults to the mirror opposite of - **position**. - - Otherwise, **anchor** defaults to ``"MC"`` (middle center). - anchor_offset - *offset* or (*offset_x*, *offset_y*). - Offset the anchor point by *offset_x* and *offset_y*. If a single value *offset* - is given, *offset_y* = *offset_x* = *offset*. + position + Specify the location of the scale bar. See :class:`pygmt.params.Position` for + more details. length Length of the scale bar in km. You can append different units to the length, which are: @@ -117,19 +79,18 @@ def scalebar( # noqa: PLR0913 rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box appearance, pass a :class:`pygmt.params.Box` object to control style, fill, pen, and other box properties. - {perspective} - {verbose} - {transparency} + $perspective + $verbose + $transparency Examples -------- >>> import pygmt - >>> from pygmt.params import Box + >>> from pygmt.params import Box, Position >>> fig = pygmt.Figure() >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) >>> fig.scalebar( - ... position=(10, 10), - ... position_type="mapcoords", + ... position=Position((10, 10)), ... length=1000, ... fancy=True, ... label="Scale", @@ -149,20 +110,7 @@ def scalebar( # noqa: PLR0913 aliasdict = AliasSystem( F=Alias(box, name="box"), L=[ - 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="justify", prefix="+j"), - Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), + Alias(position, name="position"), Alias(length, name="length", prefix="+w"), Alias( label_alignment, From 17d842729c01c294dcc231953edbbc0bf5942e53 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 10 Dec 2025 00:10:46 +0800 Subject: [PATCH 43/49] Update Figure.scalebar and add tests --- pygmt/src/scalebar.py | 44 +++++++------- pygmt/tests/baseline/test_scalebar.png.dvc | 5 ++ .../baseline/test_scalebar_complete.png.dvc | 5 ++ pygmt/tests/test_scalebar.py | 59 +++++++++++++++++++ 4 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 pygmt/tests/baseline/test_scalebar.png.dvc create mode 100644 pygmt/tests/baseline/test_scalebar_complete.png.dvc create mode 100644 pygmt/tests/test_scalebar.py diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 92d0ae6c709..977f2e992d7 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -2,39 +2,36 @@ scalebar - Add a scale bar. """ +from collections.abc import Sequence from typing import Literal from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTInvalidInputError +from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import build_arg_list from pygmt.params import Box, Position def scalebar( # noqa: PLR0913 self, - position: Position, + position: Position | None = None, length: float | str | None = None, - label_alignment: Literal["left", "right", "top", "bottom"] | None = None, - scale_position: float | tuple[float, float] | bool = False, + scale_position: float | Sequence[float] | bool = False, label: str | bool = False, - fancy: bool = False, + label_alignment: Literal["left", "right", "top", "bottom"] | None = None, unit: bool = False, + fancy: bool = False, vertical: bool = False, box: Box | bool = False, - perspective: str | bool = False, verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, + panel: int | Sequence[int] | bool = False, transparency: float | None = None, + perspective: float | Sequence[float] | str | bool = False, ): """ Add a scale bar on the map. - The scale bar is plotted at the location defined by the reference point (specified - by the **position** and *position_type** parameters) and anchor point (specified by - the **anchor** and **anchor_offset** parameters). Refer to - :doc:`/techref/reference_anchor_points` for details about the positioning. - Parameters ---------- position @@ -59,19 +56,19 @@ def scalebar( # noqa: PLR0913 - ``False``: Default to the location of the reference point. label Text string to use as the scale bar label. If ``False``, no label is drawn. If - ``True``, the distance unit provided in the **length** parameter (default is km) - is used as the label. The parameter requires ``fancy=True``. + ``True``, the distance unit provided in the ``length` parameter (default is km) + is used as the label. This parameter requires ``fancy=True``. label_alignment - Alignment of the scale bar label. Choose from "left", "right", "top", or - "bottom". [Default is "top"]. + Alignment of the scale bar label. Choose from ``"left"``, ``"right"``, + ``"top"``, or ``"bottom"``. [Default is ``"top"``]. fancy - If ``True``, draw a “fancy” scale bar. A fancy scale bar is a segmented bar with - alternating black and white rectangles. If ``False``, draw a plain scale bar. + If ``True``, draw a "fancy" scale bar, which is a segmented bar with alternating + black and white rectangles. If ``False``, draw a plain scale bar. unit If ``True``, append the unit to all distance annotations along the scale. For a plain scale, this will instead select the unit to be appended to the distance - length. The unit is determined from the suffix in the **length** or defaults to - km. + length. The unit is determined from the suffix in the ``length`` or defaults to + ``"km"``. vertical If ``True``, plot a vertical rather than a horizontal Cartesian scale. box @@ -98,14 +95,14 @@ def scalebar( # noqa: PLR0913 ... ) >>> fig.show() """ - self._preprocess() + self._activate_figure() if position is None: msg = "Parameter 'position' must be specified." - raise GMTInvalidInputError(msg) + raise GMTInvalidInput(msg) if length is None: msg = "Parameter 'length' must be specified." - raise GMTInvalidInputError(msg) + raise GMTInvalidInput(msg) aliasdict = AliasSystem( F=Alias(box, name="box"), @@ -124,9 +121,10 @@ def scalebar( # noqa: PLR0913 Alias(unit, name="unit", prefix="+u"), Alias(vertical, name="vertical", prefix="+v"), ], - p=Alias(perspective, name="perspective"), ).add_common( V=verbose, + c=panel, + p=perspective, t=transparency, ) diff --git a/pygmt/tests/baseline/test_scalebar.png.dvc b/pygmt/tests/baseline/test_scalebar.png.dvc new file mode 100644 index 00000000000..cd1b775689d --- /dev/null +++ b/pygmt/tests/baseline/test_scalebar.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 0bc967a510306086752fae3b556cc7e2 + size: 10201 + hash: md5 + path: test_scalebar.png diff --git a/pygmt/tests/baseline/test_scalebar_complete.png.dvc b/pygmt/tests/baseline/test_scalebar_complete.png.dvc new file mode 100644 index 00000000000..e04eb34756e --- /dev/null +++ b/pygmt/tests/baseline/test_scalebar_complete.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 5a339623e1c78d80484f99321b56ddf2 + size: 11712 + hash: md5 + path: test_scalebar_complete.png diff --git a/pygmt/tests/test_scalebar.py b/pygmt/tests/test_scalebar.py new file mode 100644 index 00000000000..e60f5228d21 --- /dev/null +++ b/pygmt/tests/test_scalebar.py @@ -0,0 +1,59 @@ +""" +Test Figure.scalebar. +""" + +import pytest +from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput +from pygmt.params import Position + + +@pytest.mark.mpl_image_compare +def test_scalebar(): + """ + Create a map with a scale bar. + """ + fig = Figure() + fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True) + fig.scalebar(position=Position((118, 22), cstype="mapcoords"), length=200) + return fig + + +@pytest.mark.mpl_image_compare +def test_scalebar_complete(): + """ + Test all parameters of scalebar. + """ + fig = Figure() + fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True) + fig.scalebar( + position=Position((110, 22), cstype="mapcoords"), + length=1000, + fancy=True, + label="Scale", + label_alignment="left", + scale_position=(110, 25), + unit=True, + box=True, + ) + return fig + + +def test_scalebar_no_position(): + """ + Test that an error is raised when position is not provided. + """ + fig = Figure() + fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True) + with pytest.raises(GMTInvalidInput): + fig.scalebar(length=200) + + +def test_scalebar_no_length(): + """ + Test that an error is raised when length is not provided. + """ + fig = Figure() + fig.basemap(region=[100, 120, 20, 30], projection="M10c", frame=True) + with pytest.raises(GMTInvalidInput): + fig.scalebar(position=Position((118, 22), cstype="mapcoords")) From f5133e25f29ddb8111f99a5600ef1d25d7da0802 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 10 Dec 2025 00:26:55 +0800 Subject: [PATCH 44/49] Add a test for Cartesian scale --- pygmt/tests/baseline/test_scalebar_cartesian.png.dvc | 5 +++++ pygmt/tests/test_scalebar.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 pygmt/tests/baseline/test_scalebar_cartesian.png.dvc diff --git a/pygmt/tests/baseline/test_scalebar_cartesian.png.dvc b/pygmt/tests/baseline/test_scalebar_cartesian.png.dvc new file mode 100644 index 00000000000..0d01ea6cca5 --- /dev/null +++ b/pygmt/tests/baseline/test_scalebar_cartesian.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: e09a7c67f6146530ea594694853b6f98 + size: 6508 + hash: md5 + path: test_scalebar_cartesian.png diff --git a/pygmt/tests/test_scalebar.py b/pygmt/tests/test_scalebar.py index e60f5228d21..6e1e1ff726a 100644 --- a/pygmt/tests/test_scalebar.py +++ b/pygmt/tests/test_scalebar.py @@ -39,6 +39,18 @@ def test_scalebar_complete(): return fig +@pytest.mark.mpl_image_compare +def test_scalebar_cartesian(): + """ + Test scale bar in Cartesian coordinates. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 5], projection="X10c/5c", frame=True) + fig.scalebar(position=Position((2, 1), cstype="mapcoords"), length=1) + fig.scalebar(position=Position((4, 1), cstype="mapcoords"), length=1, vertical=True) + return fig + + def test_scalebar_no_position(): """ Test that an error is raised when position is not provided. From cb23c570f48aa5643fc3aec6d9225edaf28c9734 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 11 Dec 2025 15:09:39 +0800 Subject: [PATCH 45/49] Fix docstrings --- pygmt/src/scalebar.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 977f2e992d7..9457a5de16b 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -8,10 +8,11 @@ from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput -from pygmt.helpers import build_arg_list +from pygmt.helpers import build_arg_list, fmt_docstring from pygmt.params import Box, Position +@fmt_docstring def scalebar( # noqa: PLR0913 self, position: Position | None = None, @@ -40,6 +41,7 @@ def scalebar( # noqa: PLR0913 length Length of the scale bar in km. You can append different units to the length, which are: + - **e**: meters - **f**: feet - **k**: kilometers @@ -56,7 +58,7 @@ def scalebar( # noqa: PLR0913 - ``False``: Default to the location of the reference point. label Text string to use as the scale bar label. If ``False``, no label is drawn. If - ``True``, the distance unit provided in the ``length` parameter (default is km) + ``True``, the distance unit provided in the ``length`` parameter (default is km) is used as the label. This parameter requires ``fancy=True``. label_alignment Alignment of the scale bar label. Choose from ``"left"``, ``"right"``, From 4d0486800d75dc2e7776e8be8e829eaa56436480 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 11 Dec 2025 15:39:15 +0800 Subject: [PATCH 46/49] Improve docstrings --- pygmt/src/scalebar.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 9457a5de16b..247502f5c7a 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -39,15 +39,9 @@ def scalebar( # noqa: PLR0913 Specify the location of the scale bar. See :class:`pygmt.params.Position` for more details. length - Length of the scale bar in km. You can append different units to the length, - which are: - - - **e**: meters - - **f**: feet - - **k**: kilometers - - **M**: statute mile - - **n**: nautical miles - - **u**: US Survey foot + Length of the scale bar in km. Append a suffix to specify different units. Valid + units are: **e**: meters; **f**: feet; **k**: kilometers; **M**: statute mile; + **n**: nautical miles; **u**: US Survey foot. scale_position Specify the location where on a geographic map the scale applies. It can be: From de4e14201f683f63a4e8113bb2b65def44743ab5 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 11 Dec 2025 20:01:59 +0800 Subject: [PATCH 47/49] Add a parameter height for MAP_SCALE_HEIGHT --- pygmt/src/scalebar.py | 11 ++++++++++- pygmt/tests/baseline/test_scalebar_complete.png.dvc | 4 ++-- pygmt/tests/test_scalebar.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 247502f5c7a..a33bb454509 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -17,6 +17,7 @@ def scalebar( # noqa: PLR0913 self, position: Position | None = None, length: float | str | None = None, + height: float | str | None = None, scale_position: float | Sequence[float] | bool = False, label: str | bool = False, label_alignment: Literal["left", "right", "top", "bottom"] | None = None, @@ -42,6 +43,8 @@ def scalebar( # noqa: PLR0913 Length of the scale bar in km. Append a suffix to specify different units. Valid units are: **e**: meters; **f**: feet; **k**: kilometers; **M**: statute mile; **n**: nautical miles; **u**: US Survey foot. + height + Height of the scale bar. Only works when ``fancy=True``. [Default is ``"5p"``]. scale_position Specify the location where on a geographic map the scale applies. It can be: @@ -124,5 +127,11 @@ def scalebar( # noqa: PLR0913 t=transparency, ) + confdict = {} + if height is not None: + confdict["MAP_SCALE_HEIGHT"] = height + with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(aliasdict)) + lib.call_module( + module="basemap", args=build_arg_list(aliasdict, confdict=confdict) + ) diff --git a/pygmt/tests/baseline/test_scalebar_complete.png.dvc b/pygmt/tests/baseline/test_scalebar_complete.png.dvc index e04eb34756e..4ac6b71bb99 100644 --- a/pygmt/tests/baseline/test_scalebar_complete.png.dvc +++ b/pygmt/tests/baseline/test_scalebar_complete.png.dvc @@ -1,5 +1,5 @@ outs: -- md5: 5a339623e1c78d80484f99321b56ddf2 - size: 11712 +- md5: c018b219d3ebc719fb1b1686e074dcd9 + size: 11749 hash: md5 path: test_scalebar_complete.png diff --git a/pygmt/tests/test_scalebar.py b/pygmt/tests/test_scalebar.py index 6e1e1ff726a..827d0f4330d 100644 --- a/pygmt/tests/test_scalebar.py +++ b/pygmt/tests/test_scalebar.py @@ -29,6 +29,7 @@ def test_scalebar_complete(): fig.scalebar( position=Position((110, 22), cstype="mapcoords"), length=1000, + height="10p", fancy=True, label="Scale", label_alignment="left", From e8595939a43110af0a581c7110130a67451ef004 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 12 Dec 2025 13:41:57 +0800 Subject: [PATCH 48/49] Fix the inline test --- pygmt/src/scalebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index a33bb454509..9af0b27b1c7 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -86,7 +86,7 @@ def scalebar( # noqa: PLR0913 >>> fig = pygmt.Figure() >>> fig.basemap(region=[0, 80, -30, 30], projection="M10c", frame=True) >>> fig.scalebar( - ... position=Position((10, 10)), + ... position=Position((10, 10), cstype="mapcoords"), ... length=1000, ... fancy=True, ... label="Scale", From 71880a3399d4261bc221a2d9d9209da64b424f0d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 12 Dec 2025 13:42:21 +0800 Subject: [PATCH 49/49] Skip the doctest --- pygmt/src/scalebar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py index 9af0b27b1c7..f6e54f646ee 100644 --- a/pygmt/src/scalebar.py +++ b/pygmt/src/scalebar.py @@ -11,6 +11,8 @@ from pygmt.helpers import build_arg_list, fmt_docstring from pygmt.params import Box, Position +__doctest_skip__ = ["scalebar"] + @fmt_docstring def scalebar( # noqa: PLR0913