diff --git a/pygmt/src/_common.py b/pygmt/src/_common.py index 32a01bc0a80..0dba544fe67 100644 --- a/pygmt/src/_common.py +++ b/pygmt/src/_common.py @@ -345,7 +345,7 @@ def _parse_position( case str() if position in _valid_anchors: # Anchor code position = Position(position, cstype="inside") case str(): # Raw GMT command string. - if any(v is not None for v in kwdict.values()): + if any(v is not None and v is not False for v in kwdict.values()): msg = ( "Parameter 'position' is given with a raw GMT command string, and " f"conflicts with parameters {', '.join(repr(c) for c in kwdict)}." diff --git a/pygmt/src/colorbar.py b/pygmt/src/colorbar.py index 0e29d53cb66..decc3dbdadc 100644 --- a/pygmt/src/colorbar.py +++ b/pygmt/src/colorbar.py @@ -5,31 +5,118 @@ 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 GMTValueError from pygmt.helpers import build_arg_list, fmt_docstring, use_alias -from pygmt.params import Box +from pygmt.helpers.utils import is_nonstr_iter +from pygmt.params import Box, Position +from pygmt.src._common import _parse_position __doctest_skip__ = ["colorbar"] +def _alias_option_D( # noqa: N802, PLR0913 + position=None, + length=None, + width=None, + orientation=None, + reverse=None, + nan_rectangle=None, + nan_rectangle_position=None, + sidebar_triangles=None, + sidebar_triangles_height=None, + move_text=None, + label_as_column=None, +): + """ + Return a list of Alias objects for the -D option. + """ + # Parse the 'move_text' and 'label_as_column' parameters for the +m modifier. + if move_text or label_as_column: + modifier_m = "" + _valids = {"annotations", "label", "unit"} + + match move_text: + case None: + pass + case str() if move_text in _valids: + modifier_m = move_text[0] + case Sequence() if is_nonstr_iter(move_text) and all( + v in _valids for v in move_text + ): + modifier_m = "".join(item[0] for item in move_text) + case _: + raise GMTValueError( + move_text, + description="move_text", + choices=_valids, + ) + if label_as_column: + modifier_m += "c" + else: + modifier_m = None + + return [ + Alias(position, name="position"), + Alias(length, name="length", prefix="+w"), # +wlength/width + Alias(width, name="width", prefix="/"), + Alias( + orientation, + name="orientation", + mapping={"horizontal": "+h", "vertical": "+v"}, + ), + Alias(reverse, name="reverse", prefix="+r"), + Alias( + nan_rectangle, + name="nan_rectangle", + prefix="+n" if nan_rectangle_position in {"start", None} else "+N", + ), + Alias( + sidebar_triangles, + name="sidebar_triangles", + prefix="+e", + mapping={ + True: True, + False: False, + "foreground": "f", + "background": "b", + }, + ), + Alias(sidebar_triangles_height, name="sidebar_triangles_height"), + Alias(modifier_m, name="move_text/label_as_column", prefix="+m"), + ] + + @fmt_docstring -@use_alias(C="cmap", D="position", L="equalsize", Z="zfile") +@use_alias(C="cmap", L="equalsize", Z="zfile") def colorbar( # noqa: PLR0913 self, + position: Position | Sequence[float | str] | AnchorCode | None = None, + length: float | str | None = None, + width: float | str | None = None, + orientation: Literal["horizontal", "vertical"] | None = None, + reverse: bool = False, + nan_rectangle: bool | str = False, + nan_rectangle_position: Literal["start", "end"] | None = None, + sidebar_triangles: bool | Literal["foreground", "background"] = False, + sidebar_triangles_height: float | None = None, + move_text: Sequence[str] | None = None, + label_as_column: bool = False, + box: Box | bool = False, truncate: Sequence[float] | None = None, shading: float | Sequence[float] | bool = False, log: bool = False, scale: float | None = None, projection: str | None = None, - box: Box | bool = False, - frame: str | Sequence[str] | bool = False, region: Sequence[float | str] | str | None = None, + frame: str | Sequence[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, + transparency: float | None = None, **kwargs, ): r""" @@ -70,33 +157,66 @@ def colorbar( # noqa: PLR0913 - p = perspective - t = transparency + .. hlist:: + :columns: 1 + + - D = position, **+w**: length/width, **+h**/**+v**: orientation, + **+r**: reverse, **+n**: nan_rectangle/nan_rectangle_position, + **+e**: sidebar_triangles/scalebar_triangles_height, + **+m**: move_text/label_as_column + Parameters ---------- - frame : str or list - Set colorbar boundary frame, labels, and axes attributes. $cmap - position : str - [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ - [**+w**\ *length*\ [/\ *width*]]\ [**+e**\ [**b**\|\ **f**][*length*]]\ - [**+h**\|\ **v**][**+j**\ *justify*]\ - [**+m**\ [**a**\|\ **c**\|\ **l**\|\ **u**]]\ - [**+n**\ [*txt*]][**+o**\ *dx*\ [/*dy*]]. - Define the reference point on the map for the color scale using one of - four coordinate systems: (1) Use **g** for map (user) coordinates, (2) - use **j** or **J** for setting *refpoint* via a - :doc:`2-character justification code ` - that refers to the (invisible) map domain rectangle, - (3) use **n** for normalized (0-1) coordinates, or (4) use **x** for - plot coordinates (inches, cm, etc.). All but **x** requires both - ``region`` and ``projection`` to be specified. Append **+w** followed - by the length and width of the colorbar. If width is not specified - then it is set to 4% of the given length. Give a negative length to - reverse the scale bar. Append **+h** to get a horizontal scale - [Default is vertical (**+v**)]. By default, the anchor point on the - scale is assumed to be the bottom left corner (**BL**), but this can - be changed by appending **+j** followed by a - :doc:`2-character justification code ` - *justify*. + position + Position of the colorbar on the plot. It can be specified in multiple ways: + + - A :class:`pygmt.params.Position` object to fully control the reference point, + anchor point, and offset. + - A sequence of two values representing the x- and y-coordinates in plot + coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``. + - A :doc:`2-character justification code ` for a + position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot. + + If not specified, defaults to the Bottom Center outside of the plot. + length + width + Length and width of the colorbar. If length is given with a unit ``%`` then it + is in percentage of the corresponding plot side dimension (i.e., plot width for + a horizontal colorbar, or plot height for a vertical colorbar). If width is + given with unit ``%`` then it is in percentage of the bar length. [Length + default to 80% of the corresponding plot side dimension, and width default to + 4% of the bar length]. + orientation + Set the colorbar orientation to either ``"horizontal"`` or ``"vertical"``. + [Default is vertical, unless position is set to bottom-center or top-center with + ``cstype="outside"`` or ``cstype="inside"``, then horizontal is the default]. + reverse + Reverse the positive direction of the bar. + nan_rectangle + Draw a rectangle filled with the NaN color (via the **N** entry in the CPT or + :gmt-term:`COLOR_NAN` if no such entry) at the start of the colorbar. If a + string is given, use that string as the label for the NaN color. + nan_rectangle_position + Set the position of the NaN rectangle. Choose from ``"start"`` or ``"end"``. + [Default is ``"start"``]. + sidebar_triangles + Draw sidebar triangles for back- and/or foreground colors. If set to ``True``, + both triangles are drawn. Alternatively, set it to ``"foreground"`` or + ``"background"`` to draw only one triangle. The back- and/or foreground colors + are taken from the **B** and **F** entries in the CPT. If no such entries exist, + then the system default colors for **B** and **F** are used instead ( + :gmt-term:`COLOR_BACKGROUND` and :gmt-term:`COLOR_FOREGROUND`). + sidebar_triangles_height + Height of the sidebar triangles [Default is half the bar width]. + move_text + Move text (annotations, label, and unit) to opposite side. Accept a sequence of + strings containing one or more of ``"annotations"``, ``"label"``, and + ``"unit"``. The default placement of these texts depends on the colorbar + orientation and position. + label_as_column + Print a vertical label as a column of characters (does not work with special + characters). box Draw a background box behind the colorbar. If set to ``True``, a simple rectangular box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box @@ -138,6 +258,10 @@ def colorbar( # noqa: PLR0913 may be in plot distance units or given as relative fractions and will be automatically scaled so that the sum of the widths equals the requested colorbar length. + $projection + $region + frame : str or list + Set colorbar boundary frame, labels, and axes attributes. $verbose $panel $perspective @@ -162,7 +286,37 @@ def colorbar( # noqa: PLR0913 """ self._activate_figure() + position = _parse_position( + position, + kwdict={ + "length": length, + "width": width, + "orientation": orientation, + "reverse": reverse, + "nan_rectangle": nan_rectangle, + "nan_rectangle_position": nan_rectangle_position, + "sidebar_triangles": sidebar_triangles, + "sidebar_triangles_height": sidebar_triangles_height, + "move_text": move_text, + "label_as_column": label_as_column, + }, + default=None, # Use GMT's default behavior if position is not provided. + ) + aliasdict = AliasSystem( + D=_alias_option_D( + position=position, + length=length, + width=width, + orientation=orientation, + reverse=None, + nan_rectangle=None, + nan_rectangle_position=None, + sidebar_triangles=None, + sidebar_triangles_height=None, + move_text=None, + label_as_column=None, + ), F=Alias(box, name="box"), G=Alias(truncate, name="truncate", sep="/", size=2), I=Alias(shading, name="shading", sep="/", size=2), diff --git a/pygmt/tests/test_colorbar.py b/pygmt/tests/test_colorbar.py index 85925ae944a..3ef937ecc27 100644 --- a/pygmt/tests/test_colorbar.py +++ b/pygmt/tests/test_colorbar.py @@ -4,6 +4,10 @@ import pytest from pygmt import Figure +from pygmt.alias import AliasSystem +from pygmt.exceptions import GMTInvalidInput +from pygmt.params.position import Position +from pygmt.src.colorbar import _alias_option_D @pytest.mark.benchmark @@ -13,7 +17,12 @@ def test_colorbar(): Create a simple colorbar. """ fig = Figure() - fig.colorbar(cmap="gmt/rainbow", position="x0c/0c+w4c", frame=True) + fig.colorbar( + cmap="gmt/rainbow", + position=Position((0, 0), cstype="plotcoords"), + length=4, + frame=True, + ) return fig @@ -26,3 +35,70 @@ def test_colorbar_shading_list(): fig.basemap(region=[0, 10, 0, 2], projection="X10c/2c", frame="a") fig.colorbar(cmap="gmt/geo", shading=[-0.7, 0.2], frame=True) return fig + + +def test_colorbar_alias_D(): # noqa: N802 + """ + Test the parameters for the -D option. + """ + + def alias_wrapper(**kwargs): + """ + A wrapper function for testing the parameters of -D option. + """ + aliasdict = AliasSystem(D=_alias_option_D(**kwargs)) + return aliasdict.get("D") + + argstr = alias_wrapper(position=Position("TL", offset=0.2), length=4, width=0.5) + assert argstr == "jTL+o0.2+w4/0.5" + + assert alias_wrapper(orientation="horizontal") == "+h" + assert alias_wrapper(orientation="vertical") == "+v" + + assert alias_wrapper(reverse=True) == "+r" + + assert alias_wrapper(nan_rectangle=True) == "+n" + assert alias_wrapper(nan_rectangle=True, nan_rectangle_position="end") == "+N" + + assert alias_wrapper(sidebar_triangles=True) == "+e" + assert alias_wrapper(sidebar_triangles="foreground") == "+ef" + assert alias_wrapper(sidebar_triangles="background") == "+eb" + assert ( + alias_wrapper(sidebar_triangles=True, sidebar_triangles_height=0.3) == "+e0.3" + ) + + assert alias_wrapper(move_text=["annotations", "label", "unit"]) == "+malu" + assert alias_wrapper(label_as_column=True) == "+mc" + + +@pytest.mark.mpl_image_compare(filename="test_colorbar.png") +def test_colorbar_position_deprecated_syntax(): + """ + Check that passing the deprecated GMT CLI syntax string to 'position' works. + """ + fig = Figure() + fig.colorbar(cmap="gmt/rainbow", position="x0/0+w4c", frame=True) + return fig + + +def test_image_position_mixed_syntax(): + """ + Test that mixing deprecated GMT CLI syntax string with new parameters. + """ + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", length="4c") + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", width="0.5c") + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", orientation="horizontal") + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", reverse=True) + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", nan_rectangle=True) + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", sidebar_triangles=True) + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", move_text=["label"]) + with pytest.raises(GMTInvalidInput): + fig.colorbar(cmap="gmt/rainbow", position="x0/0", label_as_column=True)