diff --git a/examples/gallery/embellishments/gmt_logo.py b/examples/gallery/embellishments/gmt_logo.py index b0311881577..1993dafa325 100644 --- a/examples/gallery/embellishments/gmt_logo.py +++ b/examples/gallery/embellishments/gmt_logo.py @@ -13,6 +13,5 @@ # Add the GMT logo in the Top Right (TR) corner of the current plot, scaled up to be 3 # centimeters wide and offset by 0.3 cm in x-direction and 0.6 cm in y-direction. -fig.logo(position="jTR+o0.3c/0.6c+w3c") - +fig.logo(position="TR", position_type="inside", anchor_offset=(0.3, 0.6), width="3c") fig.show() diff --git a/pygmt/src/inset.py b/pygmt/src/inset.py index 30b5d316e0b..48cc56084b6 100644 --- a/pygmt/src/inset.py +++ b/pygmt/src/inset.py @@ -126,9 +126,8 @@ def inset( ... dcw="MG+gred", ... ) ... - >>> # Map elements outside the "with" statement are plotted in the main - >>> # figure - >>> fig.logo(position="jBR+o0.2c+w3c") + >>> # Map elements outside the "with" statement are plotted in the main figure + >>> fig.logo(position="BR", position_type="inside", anchor_offset=0.2, width="3c") >>> fig.show() """ self._activate_figure() diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index defdc065eb3..b0b303dda92 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -5,16 +5,25 @@ from collections.abc import Sequence from typing import Literal +from pygmt._typing import AnchorCode from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.helpers import build_arg_list, fmt_docstring, use_alias +from pygmt.exceptions import GMTInvalidInput +from pygmt.helpers import build_arg_list, fmt_docstring from pygmt.params import Box @fmt_docstring -@use_alias(D="position") -def logo( +def logo( # noqa: PLR0913 self, + position: Sequence[str | float] | AnchorCode | None = None, + position_type: Literal[ + "mapcoords", "boxcoords", "plotcoords", "inside", "outside" + ] = "plotcoords", + anchor: AnchorCode | None = None, + anchor_offset: Sequence[float | str] | None = None, + height: float | str | None = None, + width: float | str | None = None, projection: str | None = None, region: Sequence[float | str] | str | None = None, style: Literal["standard", "url", "no_label"] = "standard", @@ -29,14 +38,29 @@ def logo( r""" Plot the GMT logo. - By default, the GMT logo is 2 inches wide and 1 inch high and - will be positioned relative to the current plot origin. - Use various options to change this and to place a transparent or - opaque rectangular map panel behind the GMT logo. + .. figure:: https://docs.generic-mapping-tools.org/6.6/_images/GMT_coverlogo.png + :alt: GMT logo + :align: center + :width: 300px + + By default, the GMT logo is 2 inches wide and 1 inch high and will be positioned + relative to the current plot origin. The position can be changed by specifying the + reference point (via ``position_type`` and ``position``) and anchor point (via + ``anchor`` and ``anchor_offset``). Refer to :doc:`/techref/reference_anchor_points` + for details about the positioning. Full GMT docs at :gmt-docs:`gmtlogo.html`. - {aliases} + **Aliases:** + + .. hlist:: + :columns: 1 + + - D = position/position_type, **+j**: anchor, **+o**: anchor_offset, + **+w**: width, **+h**: height + .. hlist:: + :columns: 3 + - F = box - J = projection - R = region @@ -48,17 +72,51 @@ def logo( Parameters ---------- - {projection} - {region} - position : str - [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ - **+w**\ *width*\ [**+j**\ *justify*]\ [**+o**\ *dx*\ [/*dy*]]. - Set reference point on the map for the image. + position + Specify the reference point on the plot for the GMT logo. The method of defining + the reference point is controlled by ``position_type``, and the exact location + is set by ``position``. + position_type + Specify the type of coordinates used to define the reference point. It can be + one of the following values: + + - ``"mapcoords"``: ``position`` is specified as (*longitude*, *latitude*) in map + coordinates. + - ``"boxcoords"``: ``position`` is specified as (*nx*, *ny*) in normalized + coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. + - ``"plotcoords"``: ``position`` is specified as (*x*, *y*) in plot coordinates, + i.e., distances from the lower-left plot origin given in inches, centimeters, + or points. + - ``"inside"`` or ``"outside"``: ``position`` is one of the nine + :doc:`two-character justification codes `, + indicating a specific location relative to the plot bounding box. + + Refer to :doc:`/techref/reference_anchor_points` for details about the + positioning. + anchor + Specify the anchor point of the GMT logo, using one of the + :doc:`2-character justification codes `. The + default value depends on ``position_type``. + + - ``position_type="inside"``: ``anchor`` defaults to the same as ``position``. + - ``position_type="outside"``: ``anchor`` defaults to the mirror opposite of + ``position``. + - Otherwise, ``anchor`` defaults to ``"MC"`` (middle center). + anchor_offset + Specifies an offset for the anchor point as *offset* or + (*offset_x*, *offset_y*). If a single value *offset* is given, both *offset_x* + and *offset_y* are set to *offset*. + width + height + Width or height of the GMT logo. Since the aspect ratio is fixed, only one of + the two can be specified. box Draw a background box behind the logo. 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. + {projection} + {region} style Control what is written beneath the map portion of the logo. @@ -72,7 +130,50 @@ def logo( """ self._activate_figure() + # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. For backward + # compatibility, need to check if users pass a string with the GMT CLI syntax to + # 'position', i.e., a string starting with one of the codes "g", "n", "x", "j", "J", + # or contains modifiers with "+". + _is_deprecated_position = isinstance(position, str) and ( + position.startswith(("g", "n", "x", "j", "J")) or "+" in position + ) + + if _is_deprecated_position and any( + v is not None for v in (anchor, anchor_offset, height, width) + ): + msg = ( + "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " + "with parameters 'anchor', 'anchor_offset', 'height', and 'width'. " + "Please refer to the documentation for the recommended usage." + ) + raise GMTInvalidInput(msg) + + # width and height are mutually exclusive. + if width is not None and height is not None: + msg = "Cannot specify both width and height." + raise GMTInvalidInput(msg) + aliasdict = AliasSystem( + D=Alias(position, name="position") + if _is_deprecated_position + else [ + Alias( + position_type, + name="position_type", + mapping={ + "mapcoords": "g", + "boxcoords": "n", + "plotcoords": "x", + "inside": "j", + "outside": "J", + }, + ), + Alias(position, name="position", sep="/", size=2), + Alias(anchor, name="anchor", prefix="+j"), + Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), + Alias(height, name="height", prefix="+h"), + Alias(width, name="width", prefix="+w"), + ], F=Alias(box, name="box"), S=Alias( style, name="style", mapping={"standard": "l", "url": "u", "no_label": "n"} diff --git a/pygmt/tests/baseline/test_logo_position_type.png.dvc b/pygmt/tests/baseline/test_logo_position_type.png.dvc new file mode 100644 index 00000000000..a1ea8c1a191 --- /dev/null +++ b/pygmt/tests/baseline/test_logo_position_type.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 9b9d740219df9edb298dbc49e7ef1351 + size: 232635 + hash: md5 + path: test_logo_position_type.png diff --git a/pygmt/tests/test_logo.py b/pygmt/tests/test_logo.py index 62bddf4eb24..6ebf7943655 100644 --- a/pygmt/tests/test_logo.py +++ b/pygmt/tests/test_logo.py @@ -4,6 +4,7 @@ import pytest from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput @pytest.mark.benchmark @@ -24,5 +25,63 @@ def test_logo_on_a_map(): """ fig = Figure() fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) - fig.logo(position="jTR+o0.25c/0.25c+w7.5c", box=True) + fig.logo( + position_type="inside", + position="TR", + anchor_offset=(0.25, 0.25), + width="7.5c", + box=True, + ) return fig + + +@pytest.mark.mpl_image_compare +def test_logo_position_type(): + """ + Test that different position types work as expected. + """ + fig = Figure() + fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) + fig.logo(position_type="inside", position="TL") + fig.logo(position_type="outside", position="TR") + fig.logo(position_type="mapcoords", position=(-80, 15)) + fig.logo(position_type="boxcoords", position=(0, 0.5)) + fig.logo(position_type="plotcoords", position=("2c", "0c"), width="5c") + fig.logo(position=("8c", "0c")) # Default position_type is "plotcoords". + return fig + + +@pytest.mark.mpl_image_compare(filename="test_logo_position_type.png") +def test_logo_position_deprecated_syntax(): + """ + Test that passing the deprecated GMT CLI syntax string to 'position' works. + """ + fig = Figure() + fig.basemap(region=[-90, -70, 0, 20], projection="M15c", frame=True) + fig.logo(position="jTL") + fig.logo(position="JTR") + fig.logo(position="g-80/15") + fig.logo(position="n0/0.5") + fig.logo(position="x2c/0c+w5c") + fig.logo(position="8c/0c") + return fig + + +def test_logo_width_and_height(): + """ + Test that an error is raised when both width and height are specified. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.logo(width="5c", height="5c") + + +def test_logo_position_mixed_syntax(): + """ + Test that an error is raised when mixing new and deprecated syntax in 'position'. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.logo(position="jTL", width="5c") + with pytest.raises(GMTInvalidInput): + fig.logo(position="jTL", anchor="BR")