diff --git a/doc/api/index.rst b/doc/api/index.rst index 618217468c2..ac33b5dc5e2 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -194,6 +194,17 @@ Getting metadata from tabular or grid data: info grdinfo +Common Parameters +----------------- + +.. currentmodule:: pygmt.params + +.. autosummary:: + :toctree: generated + + Box + Frame + Xarray Integration ------------------ diff --git a/pygmt/alias.py b/pygmt/alias.py new file mode 100644 index 00000000000..aa8e6536125 --- /dev/null +++ b/pygmt/alias.py @@ -0,0 +1,236 @@ +""" +PyGMT's alias system for converting PyGMT parameters to GMT short-form options. +""" + +import dataclasses +from collections import defaultdict +from collections.abc import Mapping +from typing import Any, Literal + +from pygmt.helpers.utils import is_nonstr_iter + + +def to_string( + value: Any, + prefix: str = "", # Default to an empty string to simplify the code logic. + separator: Literal["/", ","] | None = None, + mapping: bool | Mapping = False, +) -> str | list[str] | None: + """ + Convert any value to a string, a sequence of strings or None. + + The general rules are: + + - ``None``/``False`` will be converted to ``None``. + - ``True`` will be converted to an empty string. + - A sequence will be joined by the separator if a separator is provided. Otherwise, + each item in the sequence will be converted to a string and a sequence of strings + will be returned. + - Any other type of values will be converted to a string if possible. + + If a mapping dictionary is provided, the value will be converted to the short-form + string that GMT accepts (e.g., mapping PyGMT long-form argument ``"high"`` to GMT's + short-form argument ``"h"``). If the value is not in the mapping dictionary, the + original value will be returned. If ``mapping`` is set to ``True``, the first letter + of the long-form argument will be used as the short-form argument. + + An optional prefix (e.g., `"+o"`) can be added to the beginning of the converted + string. + + To avoid extra overhead, this function does not validate parameter combinations. For + example, if ``value`` is a sequence but ``separator`` is not specified, the function + will return a sequence of strings. In this case, ``prefix`` has no effect, but the + function does not check for such inconsistencies. + + Parameters + ---------- + value + The value to convert. + prefix + The string to add as a prefix to the returned value. + separator + The separator to use if the value is a sequence. + mapping + A mapping dictionary or ``True`` to map long-form arguments to GMT's short-form + arguments. If ``True``, will use the first letter of the long-form arguments. + + Returns + ------- + ret + The converted value. + + Examples + -------- + >>> to_string("text") + 'text' + >>> to_string(12) + '12' + >>> to_string((12, 34), separator="/") + '12/34' + >>> to_string(("12p", "34p"), separator=",") + '12p,34p' + >>> to_string(("12p", "34p"), prefix="+o", separator="/") + '+o12p/34p' + >>> to_string(True) + '' + >>> to_string(True, prefix="+a") + '+a' + >>> to_string(False) + >>> to_string(None) + >>> to_string(["xaf", "yaf", "WSen"]) + ['xaf', 'yaf', 'WSen'] + >>> to_string("high", mapping=True) + 'h' + >>> to_string("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) + 'a' + >>> to_string("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) + 'invalid' + """ + if value is None or value is False: # None and False are converted to None. + return None + if value is True: # True is converted to an empty string with the optional prefix. + return f"{prefix}" + + # Convert a non-sequence value to a string. + if not is_nonstr_iter(value): + if mapping: # Mapping long-form arguments to short-form arguments. + value = value[0] if mapping is True else mapping.get(value, value) + return f"{prefix}{value}" + + # Convert a sequence of values to a sequence of strings. + # In some cases, "prefix" and "mapping" are ignored. We can enable them when needed. + _values = [str(item) for item in value] + # When separator is not specified, return a sequence of strings for repeatable GMT + # options like '-B'. Otherwise, join the sequence of strings with the separator. + return _values if separator is None else f"{prefix}{separator.join(_values)}" + + +@dataclasses.dataclass +class Alias: + """ + Class for aliasing a PyGMT parameter to a GMT option or a modifier. + + Attributes + ---------- + value + Value of the parameter. + prefix + String to add at the beginning of the value. + separator + Separator to use if the value is a sequence. + mapping + Map long-form arguments to GMT's short-form arguments. If ``True``, will use the + first letter of the long-form arguments. + + Examples + -------- + >>> par = Alias((3.0, 3.0), prefix="+o", separator="/") + >>> par._value + '+o3.0/3.0' + + >>> par = Alias(["xaf", "yaf", "WSen"]) + >>> par._value + ['xaf', 'yaf', 'WSen'] + + >>> par = Alias("high", mapping=True) + >>> par._value + 'h' + + >>> par = Alias("mean", mapping={"mean": "a", "mad": "d", "full": "g"}) + >>> par._value + 'a' + + >>> par = Alias("invalid", mapping={"mean": "a", "mad": "d", "full": "g"}) + >>> par._value + 'invalid' + """ + + value: Any + prefix: str = "" + separator: Literal["/", ","] | None = None + mapping: bool | Mapping = False + + @property + def _value(self) -> str | list[str] | None: + """ + The value of the alias as a string, a sequence of strings or None. + """ + return to_string( + value=self.value, + prefix=self.prefix, + separator=self.separator, + mapping=self.mapping, + ) + + +class AliasSystem: + """ + Alias system for converting PyGMT parameters to GMT options. + + The AliasSystem class is initialized with keyword arguments, where each key is a GMT + option flag, and the corresponding value is an ``Alias`` object or a list of + ``Alias`` objects. + + The class provides the ``kwdict`` attribute, which is a dictionary mapping each GMT + option flag to its current value. The value can be a string or a list of strings. + This keyword dictionary can then be passed to the ``build_arg_list`` function. + + Examples + -------- + >>> from pygmt.alias import Alias, AliasSystem + >>> from pygmt.helpers import build_arg_list + >>> + >>> def func( + ... par0, + ... par1=None, + ... par2=None, + ... par3=None, + ... par4=None, + ... frame=False, + ... panel=None, + ... **kwargs, + ... ): + ... alias = AliasSystem( + ... A=[ + ... Alias(par1), + ... Alias(par2, prefix="+j"), + ... Alias(par3, prefix="+o", separator="/"), + ... ], + ... B=Alias(frame), + ... c=Alias(panel, separator=","), + ... ) + ... return build_arg_list(alias.kwdict | kwargs) + >>> func( + ... "infile", + ... par1="mytext", + ... par3=(12, 12), + ... frame=True, + ... panel=(1, 2), + ... J="X10c/10c", + ... ) + ['-Amytext+o12/12', '-B', '-JX10c/10c', '-c1,2'] + """ + + def __init__(self, **kwargs): + """ + Initialize the alias system and create the keyword dictionary that stores the + current parameter values. + """ + # Keyword dictionary with an empty string as default value. + self.kwdict = defaultdict(str) + + for option, aliases in kwargs.items(): + if not is_nonstr_iter(aliases): # Single alias. + self.kwdict[option] = aliases._value + continue + + for alias in aliases: # List of aliases. + match alias._value: + case None: + continue + case str(): + self.kwdict[option] += alias._value + case list(): + # A repeatable option should have only one alias, so break. + self.kwdict[option] = alias._value + break diff --git a/pygmt/figure.py b/pygmt/figure.py index f9c8478747d..1f9f89199db 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -430,6 +430,7 @@ def _repr_html_(self) -> str: plot3d, psconvert, rose, + scalebar, set_panel, shift_origin, solar, diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py new file mode 100644 index 00000000000..14326517cde --- /dev/null +++ b/pygmt/params/__init__.py @@ -0,0 +1,6 @@ +""" +Classes for PyGMT common parameters. +""" + +from pygmt.params.box import Box +from pygmt.params.frame import Axes, Axis, Frame diff --git a/pygmt/params/base.py b/pygmt/params/base.py new file mode 100644 index 00000000000..0bb8252aefa --- /dev/null +++ b/pygmt/params/base.py @@ -0,0 +1,50 @@ +""" +Base class for PyGMT common parameters. +""" + + +class BaseParam: + """ + Base class for PyGMT common parameters. + + Examples + -------- + >>> from typing import Any + >>> import dataclasses + >>> from pygmt.params.base import BaseParam + >>> from pygmt.alias import Alias + >>> + >>> @dataclasses.dataclass(repr=False) + ... class Test(BaseParam): + ... par1: Any = None + ... par2: Any = None + ... par3: Any = None + ... + ... @property + ... def _aliases(self): + ... return [ + ... Alias(self.par1), + ... Alias(self.par2, prefix="+a"), + ... Alias(self.par3, prefix="+b", separator="/"), + ... ] + >>> var = Test(par1="val1") + >>> str(var) + 'val1' + >>> repr(var) + "Test(par1='val1')" + """ + + def __str__(self): + """ + String representation of the object that can be passed to GMT directly. + """ + return "".join( + [alias._value for alias in self._aliases if alias._value is not None] + ) + + def __repr__(self): + """ + String representation of the object. + """ + params = ", ".join(f"{k}={v!r}" for k, v in vars(self).items() if v is not None) + return f"{self.__class__.__name__}({params})" diff --git a/pygmt/params/box.py b/pygmt/params/box.py new file mode 100644 index 00000000000..554f3cd0d14 --- /dev/null +++ b/pygmt/params/box.py @@ -0,0 +1,103 @@ +""" +The box parameter. +""" + +from collections.abc import Sequence +from dataclasses import dataclass + +from pygmt.alias import Alias +from pygmt.params.base import BaseParam + + +@dataclass(repr=False) +class Box(BaseParam): + """ + Class for the box around GMT embellishments. + + Attributes + ---------- + clearance + Set clearances between the embellishment and the box border. Can be either a + scalar value or a list of two/four values. + + - a scalar value means a uniform clearance in all four directions. + - a list of two values means separate clearances in x- and y- directions. + - a list of four values means separate clearances for left/right/bottom/top. + fill + Fill for the box. None means no fill. + + Examples + -------- + >>> from pygmt.params import Box + >>> str(Box(fill="red@20")) + '+gred@20' + >>> str(Box(clearance=(0.2, 0.2), fill="red@20", pen="blue")) + '+c0.2/0.2+gred@20+pblue' + >>> str(Box(clearance=(0.2, 0.2), pen="blue", radius=True)) + '+c0.2/0.2+pblue+r' + >>> str(Box(clearance=(0.1, 0.2, 0.3, 0.4), pen="blue", radius="10p")) + '+c0.1/0.2/0.3/0.4+pblue+r10p' + >>> str( + ... Box( + ... clearance=0.2, + ... pen="blue", + ... radius="10p", + ... shading_offset=("5p", "5p"), + ... shading_fill="lightred", + ... ) + ... ) + '+c0.2+pblue+r10p+s5p/5p/lightred' + >>> str(Box(clearance=0.2, inner_gap="2p", inner_pen="1p,red", pen="blue")) + '+c0.2+i2p/1p,red+pblue' + >>> str(Box(clearance=0.2, shading_offset=("5p", "5p"), shading_fill="lightred")) + '+c0.2+s5p/5p/lightred' + """ + + """ + The GMT syntax: + + [+c] + [+g] + [+i[[/]]] + [+p[]] + [+r[]] + [+s[//][]] + """ + clearance: float | str | Sequence[float | str] | None = None + fill: str | None = None + inner_gap: float | str | None = None + inner_pen: str | None = None + pen: str | None = None + radius: float | bool | None = False + shading_offset: Sequence[float | str] | None = None + shading_fill: str | None = None + + def _innerborder(self) -> list[str | float] | None: + """ + innerborder="{inner_gap}/{inner_pen}" + """ + return [v for v in (self.inner_gap, self.inner_pen) if v is not None] or None + + def _shading(self) -> list[str | float] | None: + """ + shading="{shading_offset}/{shading_fill}" + """ + args = ( + [*self.shading_offset, self.shading_fill] + if self.shading_offset + else [self.shading_fill] + ) + return [v for v in args if v is not None] or None + + @property + def _aliases(self): + self.innerborder = self._innerborder() + self.shading = self._shading() + return [ + Alias(self.clearance, prefix="+c", separator="/"), + Alias(self.fill, prefix="+g"), + Alias(self.innerborder, prefix="+i", separator="/"), + Alias(self.pen, prefix="+p"), + Alias(self.radius, prefix="+r"), + Alias(self.shading, prefix="+s", separator="/"), + ] diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py new file mode 100644 index 00000000000..79d54a5ccc2 --- /dev/null +++ b/pygmt/params/frame.py @@ -0,0 +1,104 @@ +""" +The box parameter. +""" + +from dataclasses import dataclass +from typing import Any + +from pygmt.alias import Alias +from pygmt.params.base import BaseParam + + +@dataclass(repr=False) +class Axes(BaseParam): + """ + Class for setting up the axes, title, and fill of a plot. + + Examples + -------- + >>> from pygmt.params import Axes + >>> str(Axes("WSen", title="My Plot Title", fill="lightred")) + 'WSen+glightred+tMy Plot Title' + """ + + axes: Any = None + fill: Any = None + title: Any = None + + @property + def _aliases(self): + return [ + Alias(self.axes), + Alias(self.fill, prefix="+g"), + Alias(self.title, prefix="+t"), + ] + + +@dataclass(repr=False) +class Axis(BaseParam): + """ + Class for setting up one axis of a plot. + + Examples + -------- + >>> from pygmt.params import Axis + >>> str(Axis(10, angle=30, label="X axis", unit="km")) + '10+a30+lX axis+ukm' + """ + + interval: float | str + angle: float | str | None = None + label: str | None = None + unit: str | None = None + + @property + def _aliases(self): + return [ + Alias(self.interval), + Alias(self.angle, prefix="+a"), + Alias(self.label, prefix="+l"), + Alias(self.unit, prefix="+u"), + ] + + +@dataclass(repr=False) +class Frame(BaseParam): + """ + Class for setting up the frame of a plot. + + >>> from pygmt.alias import AliasSystem, Alias + >>> from pygmt.params import Frame, Axes, Axis + >>> frame = Frame( + ... axes=Axes("WSen", title="My Plot Title", fill="lightred"), + ... xaxis=Axis(10, angle=30, label="X axis", unit="km"), + ... ) + >>> def func(frame): + ... alias = AliasSystem(B=Alias(frame)) + ... return alias.kwdict + >>> dict(func(frame)) + {'B': ['WSen+glightred+tMy Plot Title', 'x10+a30+lX axis+ukm']} + """ + + axes: Any = None + xaxis: Any = None + yaxis: Any = None + zaxis: Any = None + + @property + def _aliases(self): + return [ + Alias(self.axes), + Alias(self.xaxis, prefix="x"), + Alias(self.yaxis, prefix="y"), + Alias(self.zaxis, prefix="z"), + ] + + def __iter__(self): + """ + Iterate over the aliases of the class. + + Yields + ------ + The value of each alias in the class. None are excluded. + """ + yield from (alias._value for alias in self._aliases if alias._value is not None) diff --git a/pygmt/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/basemap.py b/pygmt/src/basemap.py index f9acc949042..173905d8e17 100644 --- a/pygmt/src/basemap.py +++ b/pygmt/src/basemap.py @@ -2,6 +2,7 @@ basemap - Plot base maps and frames. """ +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @@ -12,7 +13,6 @@ J="projection", Jz="zscale", JZ="zsize", - B="frame", L="map_scale", F="box", Td="rose", @@ -24,7 +24,7 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def basemap(self, **kwargs): +def basemap(self, frame=None, **kwargs): r""" Plot base maps and frames. @@ -83,5 +83,11 @@ def basemap(self, **kwargs): {transparency} """ kwargs = self._preprocess(**kwargs) + + alias = AliasSystem( + B=Alias(frame), + ) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="basemap", args=build_arg_list(kwargs)) + lib.call_module(module="basemap", args=build_arg_list(kwdict)) diff --git a/pygmt/src/binstats.py b/pygmt/src/binstats.py index 130aeffcd58..e5f1c193207 100644 --- a/pygmt/src/binstats.py +++ b/pygmt/src/binstats.py @@ -4,13 +4,13 @@ import xarray as xr from pygmt._typing import PathLike, TableLike +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @fmt_docstring @use_alias( - C="statistic", E="empty", I="spacing", N="normalize", @@ -26,7 +26,11 @@ ) @kwargs_to_strings(I="sequence", R="sequence", i="sequence_comma") def binstats( - data: PathLike | TableLike, outgrid: PathLike | None = None, **kwargs + data: PathLike | TableLike, + outgrid: PathLike | None = None, + statistic=None, + quantile_value=50, + **kwargs, ) -> xr.DataArray | None: r""" Bin spatial data and determine statistics per bin. @@ -71,6 +75,8 @@ def binstats( - **u**: maximum (upper) - **U**: maximum of negative values only - **z**: sum + quantile_value : float + The quantile value if ``statistic="quantile". empty : float Set the value assigned to empty nodes [Default is NaN]. normalize : bool @@ -104,13 +110,41 @@ def binstats( - ``None`` if ``outgrid`` is set (grid output will be stored in the file set by ``outgrid``) """ + alias = AliasSystem( + C=Alias( + statistic, + mapping={ + "mean": "a", + "mad": "d", + "full": "g", + "interquartile": "i", + "min": "l", + "minpos": "L", + "median": "m", + "number": "n", + "lms": "o", + "mode": "p", + "quantile": "q", + "rms": "r", + "stddev": "s", + "max": "u", + "maxneg": "U", + "sum": "z", + }, + ), + G=Alias(outgrid), + ) + if statistic == "quantile": + statistic += str(quantile_value) + kwdict = alias.kwdict | kwargs + with Session() as lib: with ( lib.virtualfile_in(check_kind="vector", data=data) as vintbl, lib.virtualfile_out(kind="grid", fname=outgrid) as voutgrd, ): - kwargs["G"] = voutgrd + kwdict["G"] = voutgrd lib.call_module( - module="binstats", args=build_arg_list(kwargs, infile=vintbl) + module="binstats", args=build_arg_list(kwdict, infile=vintbl) ) return lib.virtualfile_to_raster(vfname=voutgrd, outgrid=outgrid) diff --git a/pygmt/src/coast.py b/pygmt/src/coast.py index 22ecd018854..b47bac674ce 100644 --- a/pygmt/src/coast.py +++ b/pygmt/src/coast.py @@ -2,6 +2,9 @@ coast - Plot continents, countries, shorelines, rivers, and borders. """ +from typing import Literal + +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -20,7 +23,6 @@ A="area_thresh", B="frame", C="lakes", - D="resolution", E="dcw", F="box", G="land", @@ -37,7 +39,13 @@ t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def coast(self, **kwargs): +def coast( + self, + resolution: Literal[ + "auto", "full", "high", "intermediate", "low", "crude" + ] = "auto", + **kwargs, +): r""" Plot continents, countries, shorelines, rivers, and borders. @@ -73,10 +81,19 @@ def coast(self, **kwargs): parameter. Optionally, specify separate fills by appending **+l** for lakes or **+r** for river-lakes, and passing multiple strings in a list. - resolution : str - **f**\|\ **h**\|\ **i**\|\ **l**\|\ **c**. - Select the resolution of the data set to: (**f**\ )ull, (**h**\ )igh, - (**i**\ )ntermediate, (**l**\ )ow, and (**c**\ )rude. + resolution + Select the resolution of the GSHHG coastline data set to use. The available + resolutions from highest to lowest are: + + - ``"full"`` - Full resolution (may be very slow for large regions). + - ``"high"`` - High resolution (may be slow for large regions). + - ``"intermediate"`` - Intermediate resolution. + - ``"low"`` - Low resolution. + - ``"crude"`` - Crude resolution, for tasks that need crude continent outlines + only. + + The default is ``"auto"`` to automatically select the best resolution given the + chosen map scale. land : str Select filling of "dry" areas. rivers : int, str, or list @@ -200,5 +217,11 @@ def coast(self, **kwargs): "lakes, land, water, rivers, borders, dcw, Q, or shorelines." ) raise GMTInvalidInput(msg) + + alias = AliasSystem( + D=Alias(resolution, mapping=True), + ) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="coast", args=build_arg_list(kwargs)) + lib.call_module(module="coast", args=build_arg_list(kwdict)) diff --git a/pygmt/src/dimfilter.py b/pygmt/src/dimfilter.py index 3eebd84e09c..d9958aa076a 100644 --- a/pygmt/src/dimfilter.py +++ b/pygmt/src/dimfilter.py @@ -4,25 +4,25 @@ import xarray as xr from pygmt._typing import PathLike +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.helpers import build_arg_list, fmt_docstring __doctest_skip__ = ["dimfilter"] @fmt_docstring -@use_alias( - D="distance", - F="filter", - I="spacing", - N="sectors", - R="region", - V="verbose", -) -@kwargs_to_strings(I="sequence", R="sequence") def dimfilter( - grid: PathLike | xr.DataArray, outgrid: PathLike | None = None, **kwargs + grid: PathLike | xr.DataArray, + outgrid: PathLike | None = None, + distance: int | str | None = None, + filter: str | None = None, # noqa: A002 + sectors: str | None = None, + spacing: str | list | None = None, + region: str | list | None = None, + verbose: bool | None = None, + **kwargs, ) -> xr.DataArray | None: r""" Directional filtering of grids in the space domain. @@ -45,8 +45,6 @@ def dimfilter( Full option list at :gmt-docs:`dimfilter.html` - {aliases} - Parameters ---------- {grid} @@ -137,19 +135,35 @@ def dimfilter( ... region=[-55, -51, -24, -19], ... ) """ - if not all(arg in kwargs for arg in ["D", "F", "N"]) and "Q" not in kwargs: + if ( + not all(v is not None for v in [distance, filter, sectors]) + and "Q" not in kwargs + ): msg = ( "At least one of the following parameters must be specified: " "distance, filters, or sectors." ) raise GMTInvalidInput(msg) + + alias = AliasSystem( + D=Alias(distance), + G=Alias(outgrid), + F=Alias(filter), + I=Alias(spacing, separator="/"), + N=Alias(sectors), + R=Alias(region, separator="/"), + V=Alias(verbose), + ) + kwdict = alias.kwdict | kwargs + with Session() as lib: with ( lib.virtualfile_in(check_kind="raster", data=grid) as vingrd, lib.virtualfile_out(kind="grid", fname=outgrid) as voutgrd, ): - kwargs["G"] = voutgrd + kwdict["G"] = voutgrd lib.call_module( - module="dimfilter", args=build_arg_list(kwargs, infile=vingrd) + module="dimfilter", + args=build_arg_list(kwdict, infile=vingrd), ) return lib.virtualfile_to_raster(vfname=voutgrd, outgrid=outgrid) diff --git a/pygmt/src/image.py b/pygmt/src/image.py index a915db5f97f..d568cb4e1cc 100644 --- a/pygmt/src/image.py +++ b/pygmt/src/image.py @@ -3,25 +3,32 @@ """ from pygmt._typing import PathLike +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.helpers import build_arg_list, fmt_docstring @fmt_docstring -@use_alias( - D="position", - F="box", - G="bitcolor", - J="projection", - M="monochrome", - R="region", - V="verbose", - c="panel", - p="perspective", - t="transparency", -) -@kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def image(self, imagefile: PathLike, **kwargs): +def image( # noqa: PLR0913 + self, + imagefile: PathLike, + region=None, + projection=None, + position=None, + position_type=None, + dimension=None, + repeat=None, + offset=None, + dpi=None, + box=None, + bitcolor=None, + monochrome=None, + verbose=None, + panel=None, + perspective=None, + transparency=None, + **kwargs, +): r""" Plot raster or EPS images. @@ -30,8 +37,6 @@ def image(self, imagefile: PathLike, **kwargs): Full option list at :gmt-docs:`image.html` - {aliases} - Parameters ---------- imagefile : str @@ -69,5 +74,26 @@ def image(self, imagefile: PathLike, **kwargs): {transparency} """ kwargs = self._preprocess(**kwargs) + + alias = AliasSystem( + R=Alias(region, separator="/"), + J=Alias(projection), + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(dimension, prefix="+w", separator="/"), + Alias(repeat, prefix="+n", separator="/"), + Alias(offset, prefix="+o", separator="/"), + Alias(dpi, prefix="+r"), + ], + F=Alias(box), + G=Alias(bitcolor), + M=Alias(monochrome), + V=Alias(verbose), + c=Alias(panel, separator=","), + p=Alias(perspective, separator="/"), + t=Alias(transparency), + ) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="image", args=build_arg_list(kwargs, infile=imagefile)) + lib.call_module(module="image", args=build_arg_list(kwdict, infile=imagefile)) diff --git a/pygmt/src/logo.py b/pygmt/src/logo.py index 1174f227604..8f0bba848f3 100644 --- a/pygmt/src/logo.py +++ b/pygmt/src/logo.py @@ -2,8 +2,10 @@ logo - Plot the GMT logo. """ +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.params import Box @fmt_docstring @@ -11,14 +13,22 @@ R="region", J="projection", D="position", - F="box", S="style", V="verbose", c="panel", t="transparency", ) @kwargs_to_strings(R="sequence", c="sequence_comma", p="sequence") -def logo(self, **kwargs): +def logo( + self, + position=None, + position_type=None, + length=None, + height=None, + offset=None, + box: Box | str | None = None, + **kwargs, +): r""" Plot the GMT logo. @@ -39,7 +49,7 @@ def logo(self, **kwargs): [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ **+w**\ *width*\ [**+j**\ *justify*]\ [**+o**\ *dx*\ [/*dy*]]. Set reference point on the map for the image. - box : bool or str + box If set to ``True``, draw a rectangular border around the GMT logo. style : str @@ -55,5 +65,17 @@ def logo(self, **kwargs): {transparency} """ kwargs = self._preprocess(**kwargs) + + alias = AliasSystem( + D=[ + Alias(position, separator="/", prefix=position_type), + Alias(length, prefix="+w"), + Alias(height, prefix="+h"), + Alias(offset, prefix="+o", separator="/"), + ], + F=Alias(box), + ) + kwdict = alias.kwdict | kwargs + with Session() as lib: - lib.call_module(module="logo", args=build_arg_list(kwargs)) + lib.call_module(module="logo", args=build_arg_list(kwdict)) diff --git a/pygmt/src/scalebar.py b/pygmt/src/scalebar.py new file mode 100644 index 00000000000..56e2c5b16b1 --- /dev/null +++ b/pygmt/src/scalebar.py @@ -0,0 +1,70 @@ +""" +scalebar - Add a scale bar. +""" + +from typing import Literal + +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["g", "j", "J", "n", "x"] = "g", + label_alignment=None, + scale_position=None, + fancy=None, + justify=None, + label=None, + offset=None, + unit=None, + vertical=None, + 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="g", + ... length=1000, + ... fancy=True, + ... label="Scale", + ... unit=True, + ... box=Box(pen=0.5, fill="lightblue"), + ... ) + >>> fig.show() + """ + self._preprocess() + + kwdict = AliasSystem( + L=[ + Alias(position, separator="/", prefix=position_type), + Alias(length, prefix="+w"), + Alias(label_alignment, prefix="+a"), + Alias(scale_position, prefix="+c", separator="/"), + Alias(fancy, prefix="+f"), + Alias(justify, prefix="+j"), + Alias(label, prefix="+l"), + Alias(offset, prefix="+o", separator="/"), + Alias(unit, prefix="+u"), + Alias(vertical, prefix="+v"), + ], + F=Alias(box), + ).kwdict + + with Session() as lib: + lib.call_module(module="basemap", args=build_arg_list(kwdict)) diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index 13f278b2816..14d291a976e 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -7,13 +7,13 @@ from packaging.version import Version from pygmt._typing import AnchorCode +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session, __gmt_version__ -from pygmt.helpers import build_arg_list, kwargs_to_strings +from pygmt.helpers import build_arg_list, is_nonstr_iter __doctest_skip__ = ["timestamp"] -@kwargs_to_strings(offset="sequence") def timestamp( self, text: str | None = None, @@ -78,34 +78,34 @@ def timestamp( """ self._preprocess() - # Build the options passed to the "plot" module - kwdict: dict = {"T": True, "U": ""} - if label is not None: - kwdict["U"] += f"{label}" - kwdict["U"] += f"+j{justify}" + if text is not None and len(text) > 64: + msg = ( + "Argument of 'text' must be no longer than 64 characters. " + "The given text string will be truncated to 64 characters." + ) + warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) + text = text[:64] # TODO(GMT>=6.5.0): Remove the patch for upstream bug fixed in GMT 6.5.0. - if Version(__gmt_version__) < Version("6.5.0") and "/" not in str(offset): - # Giving a single offset doesn't work in GMT < 6.5.0. + if Version(__gmt_version__) < Version("6.5.0"): + # Giving a single offset doesn't work. # See https://github.com/GenericMappingTools/gmt/issues/7107. - offset = f"{offset}/{offset}" - kwdict["U"] += f"+o{offset}" + if (is_nonstr_iter(offset) and len(offset) == 1) or "/" not in str(offset): # type: ignore[arg-type] + offset = f"{offset}/{offset}" + # The +t modifier was added in GMT 6.5.0. + # See https://github.com/GenericMappingTools/gmt/pull/7127. + if text is not None: + # Overriding the 'timefmt' parameter and set 'text' to None + timefmt, text = text, None - # The +t modifier was added in GMT 6.5.0. - # See https://github.com/GenericMappingTools/gmt/pull/7127. - if text is not None: - if len(str(text)) > 64: - msg = ( - "Argument of 'text' must be no longer than 64 characters. " - "The given text string will be truncated to 64 characters." - ) - warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) - # TODO(GMT>=6.5.0): Remove the workaround for the new '+t' modifier. - if Version(__gmt_version__) < Version("6.5.0"): - # Workaround for GMT<6.5.0 by overriding the 'timefmt' parameter - timefmt = text[:64] - else: - kwdict["U"] += f"+t{text}" + kwdict = AliasSystem( + U=[ + Alias(label), + Alias(justify, prefix="+j"), + Alias(offset, prefix="+o", separator="/"), + Alias(text, prefix="+t"), + ] + ).kwdict | {"T": True} with Session() as lib: lib.call_module( diff --git a/pygmt/tests/test_binstats.py b/pygmt/tests/test_binstats.py index 6b77c82f861..7e25f069da9 100644 --- a/pygmt/tests/test_binstats.py +++ b/pygmt/tests/test_binstats.py @@ -20,7 +20,7 @@ def test_binstats_outgrid(): data="@capitals.gmt", outgrid=tmpfile.name, spacing=5, - statistic="z", + statistic="sum", search_radius="1000k", aspatial="2=population", region="g", @@ -37,7 +37,7 @@ def test_binstats_no_outgrid(): temp_grid = binstats( data="@capitals.gmt", spacing=5, - statistic="z", + statistic="sum", search_radius="1000k", aspatial="2=population", region="g", diff --git a/pygmt/tests/test_coast.py b/pygmt/tests/test_coast.py index 780ec63cace..64153e8e36e 100644 --- a/pygmt/tests/test_coast.py +++ b/pygmt/tests/test_coast.py @@ -29,7 +29,7 @@ def test_coast_world_mercator(): projection="M15c", frame="af", land="#aaaaaa", - resolution="c", + resolution="crude", water="white", ) return fig diff --git a/pygmt/tests/test_image.py b/pygmt/tests/test_image.py index 5bcc8c28145..184a60224c3 100644 --- a/pygmt/tests/test_image.py +++ b/pygmt/tests/test_image.py @@ -4,6 +4,7 @@ import pytest from pygmt import Figure +from pygmt.params import Box @pytest.mark.mpl_image_compare @@ -12,5 +13,11 @@ def test_image(): Place images on map. """ fig = Figure() - fig.image(imagefile="@circuit.png", position="x0/0+w2c", box="+pthin,blue") + fig.image( + imagefile="@circuit.png", + position=(0, 0), + position_type="x", + dimension="2c", + box=Box(pen="thin,blue"), + ) return fig