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 a36306210f6..087e9f6754d 100644 --- a/pygmt/src/inset.py +++ b/pygmt/src/inset.py @@ -121,9 +121,8 @@ def inset( ... dcw="MG+gred", ... ) ... - >>> # Map elements outside the "with" statement are plotted in the main - >>> # figure - >>> fig.logo(position="jBR+o0.2c+w3c") + >>> # Map elements outside the "with" statement are plotted in the main figure + >>> fig.logo(position="BR", position_type="inside", offset=0.2, width="3c") >>> fig.show() """ self._activate_figure() diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index d6bbdff2d09..bdc87bee9ca 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -2,19 +2,30 @@ logo - Plot the GMT logo. """ +from collections.abc import Sequence from typing import Literal +from pygmt._typing import AnchorCode from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session +from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias from pygmt.params import Box @fmt_docstring -@use_alias(R="region", D="position") +@use_alias(R="region") @kwargs_to_strings(R="sequence", p="sequence") -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=None, style: Literal["standard", "url", "no_label"] = "standard", box: Box | bool = False, @@ -27,14 +38,21 @@ 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.5/_images/GMT_coverlogo.png + :alt: GMT logo + :align: center + :width: 300px + + By default, the GMT logo is 2 inches wide and 1 inch high and will be positioned + relative to the current plot origin. The position can be changed by specifying the + reference point (via ``position_type`` and ``position``) and anchor point (via + ``anchor`` and ``anchor_offset``). Refer to :doc:`/techref/reference_anchor_points` + for details about the positioning. Full GMT docs at :gmt-docs:`gmtlogo.html`. {aliases} + - D = position/position_type/anchor/anchor_offset/width/height - F = box - J = projection - S = style @@ -44,17 +62,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/position_type + Specify the reference point on the plot for the GMT logo. The method of + defining the the reference point is controlled by **position_type**, and the + exact location is set by **position**. + + The **position_type** parameter can take one of the following values: + + - ``"mapcoords"``: **position** is specified as (*longitude*, *latitude*) in map + coordinates. Example: (120, -45) places the reference point at 120°E, 45°S. + - ``"boxcoords"``: **position** is specified as (*nx*, *ny*) in normalized + coordinates, i.e., fractional values between 0 and 1 along the x- and y-axes. + Example: (0, 0) corresponds to the lower-left corner, and (1, 1) to the + upper-right corner of the plot bounding box. + - ``"plotcoords"``: **position** is specified as (*x*, *y*) in plot coordinates, + i.e., distances from the lower-left plot origin given in inches, centimeters, + or points. Example: ("1c", "2c") places the reference point 1 cm to the right + and 2 cm above the plot origin. + - ``"inside"`` or ``"outside"``: **position** is one of the nine + :doc:two-character justification codes , + indicating a specific location relative to the plot bounding box. Example: + ``"TL"`` places the reference point at the top-left corner, either inside or + outside the bounding box. + anchor + Specify the anchor point of the GMT logo, using one of the + :doc:`2-character justification codes `. + 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. @@ -67,7 +119,42 @@ def logo( """ self._activate_figure() + # width and height are mutually exclusive. + if width is not None and height is not None: + msg = "Cannot specify both width and height." + raise GMTInvalidInput(msg) + + # Mapping position_type to GMT single-letter code. + _position_type = { + "mapcoords": "g", + "boxcoords": "n", + "plotcoords": "x", + "inside": "j", + "outside": "J", + }[position_type] + + # Prior PyGMT v0.17.0, 'position' was aliased to the -D option. + # For backward compatibility, we need to check if users pass a string with the GMT + # CLI syntax to 'position', i.e., a string starting with one of the leading + # single-letter or contains modifiers with "+". + if isinstance(position, str) and (position[0] in "gnxjJ" or "+" in position): + if any(v is not None for v in (anchor, anchor_offset, height, width)): + msg = ( + "Parameter 'position' is given with a raw GMT CLI syntax, and conflicts " + "with other parameters (anchor, anchor_offset, height, width). " + "Please refer to the documentation for the recommended usage." + ) + raise GMTInvalidInput(msg) + _position_type = "" # Unset _position_type to an empty string. + aliasdict = AliasSystem( + D=[ + Alias(position, name="position", sep="/", size=2, prefix=_position_type), + 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..2d39c52c534 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 the new group of parameters works 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")