diff --git a/doc/api/index.rst b/doc/api/index.rst index 25de6d44adf..e376e4172ae 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -47,6 +47,7 @@ Plotting tabular data Figure.contour Figure.histogram Figure.meca + Figure.clip Figure.plot Figure.plot3d Figure.rose diff --git a/pygmt/figure.py b/pygmt/figure.py index 5c5d4734ce6..8af1b4f2404 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -411,6 +411,7 @@ def _repr_html_(self) -> str: from pygmt.src import ( # type: ignore[misc] basemap, + clip, coast, colorbar, contour, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..0389d4dfe5e 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -5,6 +5,7 @@ from pygmt.src.basemap import basemap from pygmt.src.binstats import binstats from pygmt.src.blockm import blockmean, blockmedian, blockmode +from pygmt.src.clip import clip from pygmt.src.coast import coast from pygmt.src.colorbar import colorbar from pygmt.src.config import config diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py new file mode 100644 index 00000000000..3ff2b1ff720 --- /dev/null +++ b/pygmt/src/clip.py @@ -0,0 +1,107 @@ +""" +clip - Create a polygonal clip path. +""" + +import contextlib + +from pygmt.clib import Session +from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias + + +@fmt_docstring +@contextlib.contextmanager +@use_alias( + A="straight_line", + B="frame", + J="projection", + N="invert", + R="region", + W="pen", + V="verbose", +) +@kwargs_to_strings(R="sequence") +def clip(self, data=None, x=None, y=None, **kwargs): + r""" + Create a polygonal clip path. + + This function sets a clip path for the figure. The clip path is applied + to plotting functions that are called within the context manager. + + Full option list at :gmt-docs:`clip.html` + + {aliases} + + Parameters + ---------- + data : str or {table-like} + Pass in either a file name to an ASCII data table, a 2D {table-classes}. + x/y : 1d arrays + The x and y coordinates of the clip path. + {frame} + {projection} + {region} + straight_line + By default, line segments are drawn as straight lines in the Cartesian and polar + coordinate systems, and as great circle arcs (by resampling coarse input data + along such arcs) in the geographic coordinate system. The ``straight_line`` + parameter can control the drawing of line segments. Valid values are: + + - ``True``: Draw line segments as straight lines in geographic coordinate + systems. + - ``"x"``: Draw line segments by first along *x*, then along *y*. + - ``"y"``: Draw line segments by first along *y*, then along *x*. + + Here, *x* and *y* have different meanings depending on the coordinate system: + + - **Cartesian** coordinate system: *x* and *y* are the X- and Y-axes. + - **Polar** coordinate system: *x* and *y* are theta and radius. + - **Geographic** coordinate system: *x* and *y* are parallels and meridians. + + .. attention:: + + There exits a bug in GMT<=6.5.0 that, in geographic coordinate systems, the + meaning of *x* and *y* is reversed, i.e., *x* means meridians and *y* means + parallels. The bug is fixed by upstream + `PR #8648 `__. + invert : bool + Invert the sense of what is inside and outside. For example, when using a single + path, ``invert=True`` means only plot points outside that path will be shown. + Cannot be used together with ``frame``. + {verbose} + pen : str + Draw outline of clip path using given pen attributes before clipping is + initiated [Default is no outline]. + + Examples + -------- + >>> import pygmt + >>> + >>> # Create x,y data for the clip path + >>> x = [-60, 60, 60, -60] + >>> y = [-30, -30, 30, 30] + >>> + >>> # Load the 1 degree global earth relief + >>> grid = pygmt.datasets.load_earth_relief() + >>> + >>> # Create a figure and draw the map frame + >>> fig = pygmt.Figure() + >>> fig.basemap(region="d", projection="W15c", frame=True) + >>> + >>> # Use a "with" statement to initialize the clip context manager + >>> with fig.clip(x=x, y=y): + ... # Map elements under the "with" statement are clipped + ... fig.grdimage(grid=grid) + >>> fig.show() # doctest: +SKIP + + """ + kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access + with Session() as lib: + try: + with lib.virtualfile_in(check_kind="vector", data=data, x=x, y=y) as vintbl: + lib.call_module( + module="clip", args=build_arg_list(kwargs, infile=vintbl) + ) + yield + finally: + # End the top most clipping path + lib.call_module(module="clip", args="-C1") diff --git a/pygmt/tests/baseline/test_clip.png.dvc b/pygmt/tests/baseline/test_clip.png.dvc new file mode 100644 index 00000000000..be6e9a34749 --- /dev/null +++ b/pygmt/tests/baseline/test_clip.png.dvc @@ -0,0 +1,4 @@ +outs: +- md5: 449fec4d58132742abb615931e6e240a + size: 7968 + path: test_clip.png diff --git a/pygmt/tests/test_clip.py b/pygmt/tests/test_clip.py new file mode 100644 index 00000000000..d79cc112d1d --- /dev/null +++ b/pygmt/tests/test_clip.py @@ -0,0 +1,80 @@ +""" +Tests for fig.clip. +""" + +import numpy as np +import pandas as pd +import pytest +import xarray as xr +from pygmt import Figure +from pygmt.helpers.testing import load_static_earth_relief + + +@pytest.fixture(scope="module", name="grid") +def fixture_grid(): + """ + Load the grid data from the static_earth_relief file. + """ + return load_static_earth_relief() + + +@pytest.fixture(scope="module", name="dataframe") +def fixture_dataframe(): + """ + Load the table data from the sample bathymetry dataset. + """ + return pd.DataFrame(data={"x": [-52, -50, -50, -52], "y": [-20, -20, -16, -16]}) + + +@pytest.fixture(scope="module", name="region") +def fixture_region(): + """ + Load the table data from the sample bathymetry dataset. + """ + return [-55, -47, -24, -10] + + +@pytest.fixture(scope="module", name="projection") +def fixture_projection(): + """ + Load the table data from the sample bathymetry dataset. + """ + return "M4c" + + +@pytest.mark.mpl_image_compare(filename="test_clip.png") +def test_clip_xy(grid, dataframe, region, projection): + """ + Test clip with x,y input. + """ + fig = Figure() + fig.basemap(region=region, frame=True, projection=projection) + with fig.clip(x=dataframe["x"], y=dataframe["y"]): + fig.grdimage(grid=grid) + return fig + + +@pytest.mark.parametrize("array_func", [np.array, xr.Dataset]) +@pytest.mark.mpl_image_compare(filename="test_clip.png") +def test_clip_matrix(array_func, dataframe, grid, region, projection): + """ + Test clip with matrix input for the clip path. + """ + table = array_func(dataframe) + fig = Figure() + fig.basemap(region=region, frame=True, projection=projection) + with fig.clip(data=table): + fig.grdimage(grid=grid, region=region) + return fig + + +@pytest.mark.mpl_image_compare(filename="test_clip.png") +def test_clip_dataframe(grid, dataframe, region, projection): + """ + Test clip with dataframe input for the clip path. + """ + fig = Figure() + fig.basemap(region=region, frame=True, projection=projection) + with fig.clip(data=dataframe): + fig.grdimage(grid=grid, region=region) + return fig