From 291e24bcad8244d98c68133984b7bc76f591d7c3 Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 16:03:15 -0500 Subject: [PATCH 1/9] Draft an implementation of gmt clip --- pygmt/figure.py | 1 + pygmt/src/__init__.py | 1 + pygmt/src/clip.py | 101 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 pygmt/src/clip.py diff --git a/pygmt/figure.py b/pygmt/figure.py index f6111fff54f..84ef06e18ee 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -467,6 +467,7 @@ def _repr_html_(self): from pygmt.src import ( # pylint: disable=import-outside-toplevel basemap, + clip, coast, colorbar, contour, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 0acb118bf43..1fab86d6b50 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -5,6 +5,7 @@ from pygmt.src.basemap import basemap 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..1a44afec0df --- /dev/null +++ b/pygmt/src/clip.py @@ -0,0 +1,101 @@ +""" +clip - Create a polygonal clip path. +""" +import contextlib + +from pygmt.clib import Session +from pygmt.helpers import build_arg_string, 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. + {B} + {J} + {R} + straight_line : bool or str + [**m**\|\ **p**\|\ **x**\|\ **y**\|\ **r**\|\ **t**]. + By default, geographic line segments are connected as great circle + arcs. To connect them as straight lines, use ``straight_line``. + Alternatively, add **m** to connect the line by first following a + meridian, then a parallel. Or append **p** to start following a + parallel, then a meridian. (This can be practical to connect a line + along parallels, for example). For Cartesian data, points are + simply connected, unless you append **x** or **y** to draw + stair-case curves that whose first move is along *x* or *y*, + respectively. For polar projection, append **r** or **t** to connect + staircase curves whose first move is along *r* or *theta*, + respectively. + invert : bool + Invert the sense of what is inside and outside. For example, when + using a single clip path, use ``invert=True`` to only plot points + outside to path. Cannot be used with ``frame``. + {V} + pen : str + Draw the output of the clip path using the 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) + ... + + """ + kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access + with Session() as lib: + try: + file_context = lib.virtualfile_from_data( + check_kind="vector", data=data, x=x, y=y + ) + + with file_context as fname: + arg_str = " ".join([fname, build_arg_string(kwargs)]) + lib.call_module("clip", arg_str) + yield + finally: + # End the top most clipping path + lib.call_module("clip", "-C1") From 8d8b2078e4aa192350831f2e8c06539826d9a46c Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 16:33:22 -0500 Subject: [PATCH 2/9] Add fig.clip to the API docs --- doc/api/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api/index.rst b/doc/api/index.rst index 959f9ba3d50..ec55ab7094c 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -44,6 +44,7 @@ Plotting tabular data Figure.contour Figure.histogram Figure.meca + Figure.clip Figure.plot Figure.plot3d Figure.rose From a6bf176ed0d5f35a7048756f81d1f12135eb5ad1 Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 16:34:27 -0500 Subject: [PATCH 3/9] Add basic tests for fig.clip --- pygmt/tests/test_clip.py | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 pygmt/tests/test_clip.py diff --git a/pygmt/tests/test_clip.py b/pygmt/tests/test_clip.py new file mode 100644 index 00000000000..3495ba3d16b --- /dev/null +++ b/pygmt/tests/test_clip.py @@ -0,0 +1,71 @@ +""" +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.mark.mpl_image_compare(filename="test_clip.png") +def test_clip_xy(grid, dataframe, region): + """ + Test clip with x,y input. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + 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): + """ + Test clip with matrix input for the clip path. + """ + table = array_func(dataframe) + fig = Figure() + fig.basemap(region=region, frame=True) + 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): + """ + Test clip with dataframe input for the clip path. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + with fig.clip(data=dataframe): + fig.grdimage(grid=grid, region=region) + return fig From 7e8ed2d539849ce84c30a4accc72d50587ce4d7f Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 16:39:52 -0500 Subject: [PATCH 4/9] Set projection for fig.clip tests --- pygmt/tests/test_clip.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pygmt/tests/test_clip.py b/pygmt/tests/test_clip.py index 3495ba3d16b..fa530c52180 100644 --- a/pygmt/tests/test_clip.py +++ b/pygmt/tests/test_clip.py @@ -32,14 +32,21 @@ def fixture_region(): """ 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): +def test_clip_xy(grid, dataframe, region, projection): """ Test clip with x,y input. """ fig = Figure() - fig.basemap(region=region, frame=True) + fig.basemap(region=region, frame=True, projection=projection) with fig.clip(x=dataframe["x"], y=dataframe["y"]): fig.grdimage(grid=grid) return fig @@ -47,25 +54,25 @@ def test_clip_xy(grid, dataframe, region): @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): +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) + 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): +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) + fig.basemap(region=region, frame=True, projection=projection) with fig.clip(data=dataframe): fig.grdimage(grid=grid, region=region) return fig From 77a83a8e14bf207bcf007fcd02f213731cc79b27 Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 16:40:54 -0500 Subject: [PATCH 5/9] Format --- pygmt/tests/test_clip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygmt/tests/test_clip.py b/pygmt/tests/test_clip.py index fa530c52180..d541a9d9e1c 100644 --- a/pygmt/tests/test_clip.py +++ b/pygmt/tests/test_clip.py @@ -32,6 +32,7 @@ def fixture_region(): """ return [-55, -47, -24, -10] + @pytest.fixture(scope="module", name="projection") def fixture_projection(): """ From b6afd6879b2cc2ba89a1a89478032523d4526cff Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 16:53:18 -0500 Subject: [PATCH 6/9] Fix inline example --- pygmt/src/clip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 1a44afec0df..83e08269bbc 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -83,6 +83,7 @@ def clip(self, data=None, x=None, y=None, **kwargs): ... # Map elements under the "with" statement are clipped ... fig.grdimage(grid=grid) ... + >>> fig.show() # doctest: +SKIP """ kwargs = self._preprocess(**kwargs) # pylint: disable=protected-access From 57dc35217a6c60e30059af72e5065129d3321d3a Mon Sep 17 00:00:00 2001 From: Meghan Jones Date: Tue, 1 Mar 2022 17:32:12 -0500 Subject: [PATCH 7/9] Add baseline image for clip tests --- pygmt/tests/baseline/test_clip.png.dvc | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pygmt/tests/baseline/test_clip.png.dvc 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 From c9bdfe0d72e8742663bcc8ae5dbfa36dabc7e5e9 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 7 Mar 2025 20:09:26 +0800 Subject: [PATCH 8/9] Adapt to latest syntax --- pygmt/src/clip.py | 20 ++++++++------------ pygmt/tests/test_clip.py | 1 + 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 83e08269bbc..1d85c3a7746 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -1,10 +1,11 @@ """ clip - Create a polygonal clip path. """ + import contextlib from pygmt.clib import Session -from pygmt.helpers import build_arg_string, fmt_docstring, kwargs_to_strings, use_alias +from pygmt.helpers import build_arg_list, fmt_docstring, kwargs_to_strings, use_alias @fmt_docstring @@ -33,8 +34,7 @@ def clip(self, data=None, x=None, y=None, **kwargs): Parameters ---------- data : str or {table-like} - Pass in either a file name to an ASCII data table, a 2D - {table-classes}. + 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. {B} @@ -82,21 +82,17 @@ def clip(self, data=None, x=None, y=None, **kwargs): >>> 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: - file_context = lib.virtualfile_from_data( - check_kind="vector", data=data, x=x, y=y - ) - - with file_context as fname: - arg_str = " ".join([fname, build_arg_string(kwargs)]) - lib.call_module("clip", arg_str) + 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("clip", "-C1") + lib.call_module(module="clip", args="-C1") diff --git a/pygmt/tests/test_clip.py b/pygmt/tests/test_clip.py index d541a9d9e1c..d79cc112d1d 100644 --- a/pygmt/tests/test_clip.py +++ b/pygmt/tests/test_clip.py @@ -1,6 +1,7 @@ """ Tests for fig.clip. """ + import numpy as np import pandas as pd import pytest From d8f247e8bf1316bd3c04b2176bf8abdfe79697ea Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Fri, 7 Mar 2025 20:24:32 +0800 Subject: [PATCH 9/9] Update docstrings --- pygmt/src/clip.py | 55 +++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 1d85c3a7746..3ff2b1ff720 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -37,31 +37,40 @@ def clip(self, data=None, x=None, y=None, **kwargs): 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. - {B} - {J} - {R} - straight_line : bool or str - [**m**\|\ **p**\|\ **x**\|\ **y**\|\ **r**\|\ **t**]. - By default, geographic line segments are connected as great circle - arcs. To connect them as straight lines, use ``straight_line``. - Alternatively, add **m** to connect the line by first following a - meridian, then a parallel. Or append **p** to start following a - parallel, then a meridian. (This can be practical to connect a line - along parallels, for example). For Cartesian data, points are - simply connected, unless you append **x** or **y** to draw - stair-case curves that whose first move is along *x* or *y*, - respectively. For polar projection, append **r** or **t** to connect - staircase curves whose first move is along *r* or *theta*, - respectively. + {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 clip path, use ``invert=True`` to only plot points - outside to path. Cannot be used with ``frame``. - {V} + 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 the output of the clip path using the pen attributes before - clipping is initiated [Default is no outline]. - + Draw outline of clip path using given pen attributes before clipping is + initiated [Default is no outline]. Examples --------