From 931bfc1bf1bfe5bc33245b41cace200c9c7e4cc9 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Wed, 26 Mar 2025 19:38:27 +0800 Subject: [PATCH 1/5] Figure.clip: Initial implementation --- doc/_templates/autosummary/class.rst | 4 +- doc/api/index.rst | 2 + pygmt/figure.py | 10 ++ pygmt/src/__init__.py | 1 + pygmt/src/clip.py | 207 +++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 pygmt/src/clip.py diff --git a/doc/_templates/autosummary/class.rst b/doc/_templates/autosummary/class.rst index 57a35f189a2..c3d36b90a43 100644 --- a/doc/_templates/autosummary/class.rst +++ b/doc/_templates/autosummary/class.rst @@ -8,8 +8,8 @@ .. rubric:: Attributes {% for item in attributes %} -.. autoproperty:: - {{ objname }}.{{ item }} +.. autoproperty:: {{ objname }}.{{ item }} + :no-index: {% endfor %} {% endif %} diff --git a/doc/api/index.rst b/doc/api/index.rst index 3d299cce71f..9916a9c9c80 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -25,6 +25,7 @@ Plotting map elements :toctree: generated Figure.basemap + Figure.clip Figure.coast Figure.colorbar Figure.hlines @@ -218,6 +219,7 @@ Miscellaneous which show_versions + src.ClipAccessor Datasets -------- diff --git a/pygmt/figure.py b/pygmt/figure.py index f9c8478747d..31eb6f1206c 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -9,6 +9,7 @@ from typing import Literal, overload from pygmt._typing import PathLike +from pygmt.src import ClipAccessor try: import IPython @@ -137,6 +138,15 @@ def region(self) -> np.ndarray: wesn = lib.extract_region() return wesn + @property + def clip(self) -> ClipAccessor: + """ + Set up a clipping path and only plot data inside/outside the clipped path. + + See :class:`pygmt.src.clip.ClipAccessor ` for the usage. + """ + return ClipAccessor(self) + def savefig( self, fname: PathLike, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..49a569a94ac 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 ClipAccessor 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..08fb0d0d8e2 --- /dev/null +++ b/pygmt/src/clip.py @@ -0,0 +1,207 @@ +""" +Clip. +""" + +from collections.abc import Sequence + +from pygmt.clib import Session +from pygmt.helpers import build_arg_list, is_nonstr_iter + + +class ClipAccessor: + """ + Accessor for the clip methods. + """ + + def __init__(self, fig): + self._fig = fig # The parent Figure object. + + def land(self, **kwargs): + """ + Clip the land area (i.e., "dry" areas). + + Must be used as a context manager. Any plotting operations within the context + manager will be clipped to the land areas. + + Parameters + ---------- + kwargs + Additional arguments passed to :meth:`pygmt.Figure.coast`. + + Examples + -------- + >>> from pygmt import Figure + >>> from pygmt.datasets import load_earth_relief + >>> + >>> grid = load_earth_relief() + >>> fig = Figure() + >>> fig.basemap(region="g", projection="W15c", frame=True) + >>> with fig.clip.land(): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + self.data = None + self.module_enter = self.module_exit = "coast" + self.kwargs_enter = {"G": True} | kwargs + self.kwargs_exit = {"Q": True} + return self + + def water(self, **kwargs): + """ + Clip the water areas (i.e., "wet" areas such as oceans and lakes). + + Must be used as a context manager. Any plotting operations within the context + manager will be clipped to the water areas. + + Parameters + ---------- + kwargs + Additional arguments passed to :meth:`pygmt.Figure.coast`. + + Examples + -------- + >>> from pygmt import Figure + >>> from pygmt.datasets import load_earth_relief + >>> + >>> grid = load_earth_relief() + >>> fig = Figure() + >>> fig.basemap(region="g", projection="W15c", frame=True) + >>> with fig.clip.water(): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + self.data = None + self.module_enter = self.module_exit = "coast" + self.kwargs_enter = {"S": True} | kwargs + self.kwargs_exit = {"Q": True} + return self + + def polygon(self, x, y, **kwargs): + """ + Clip polygonal paths. + + Parameters + ---------- + x/y + Coordinates of polygon. + kwargs + Additional arguments passed to GMT's ``clip`` module. + + Examples + -------- + >>> from pygmt import Figure + >>> from pygmt.datasets import load_earth_relief + >>> + >>> grid = load_earth_relief() + >>> fig = Figure() + >>> fig.basemap(region="g", projection="W15c", frame=True) + >>> with fig.clip.polygon(x=[-10, 10, 10, -10], y=[-10, -10, 10, 10]): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + self.data = (x, y) + self.module_enter = self.module_exit = "clip" + self.kwargs_enter = kwargs + self.kwargs_exit = {"C": True} + + return self + + def dcw(self, code: str | Sequence[str]): + """ + Clip based on the Digital Chart of the World. + + Examples + -------- + >>> from pygmt import Figure + >>> from pygmt.datasets import load_earth_relief + >>> + >>> grid = load_earth_relief() + >>> fig = Figure() + >>> fig.basemap(region="g", projection="W15c", frame=True) + >>> with fig.clip.dcw(code="JP"): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + _code = ",".join(code) if is_nonstr_iter(code) else code + self.data = None + self.module_enter = "coast" + self.kwargs_enter = {"E": _code + "+c"} + self.module_exit = "coast" + self.kwargs_exit = {"Q": True} + return self + + def solar(self, **kwargs): + """ + Clip the data to the solar terminator. + + Examples + -------- + >>> from pygmt import Figure + >>> from pygmt.datasets import load_earth_relief + >>> + >>> grid = load_earth_relief() + >>> fig = Figure() + >>> fig.basemap(region="g", projection="W15c", frame=True) + >>> with fig.clip.solar(T="c"): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + self.data = None + self.module_enter = "solar" + self.kwargs_enter = {"G": True} | kwargs + self.module_exit = "clip" + self.kwargs_exit = {"C": True} + + return self + + def mask(self, x, y, spacing, radius=None): + """ + Clip the data to a mask. + + Examples + -------- + >>> from pygmt import Figure + >>> from pygmt.datasets import load_earth_relief + >>> + >>> grid = load_earth_relief() + >>> fig = Figure() + >>> fig.basemap(region="g", projection="Q15c", frame=True) + >>> with fig.clip.mask( + ... x=[180] * 16, y=np.arange(-80, 80, 10), spacing="30m", radius="5d" + ... ): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + self.data = (x, y) + self.module_enter = self.module_exit = "mask" + self.kwargs_enter = {"I": spacing, "S": radius} + self.kwargs_exit = {"C": True} + return self + + def __enter__(self): + """ + Enter the context manager. + """ + self._fig._preprocess() # Activate the current figure. + with Session() as lib: + if self.data: + with lib.virtualfile_in(x=self.data[0], y=self.data[1]) as vintbl: + lib.call_module( + module=self.module_enter, + args=build_arg_list(self.kwargs_enter, infile=vintbl), + ) + else: + lib.call_module( + module=self.module_enter, args=build_arg_list(self.kwargs_enter) + ) + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ + Exit the context manager. + """ + self._fig._preprocess() # Activate the current figure. + with Session() as lib: + lib.call_module( + module=self.module_exit, args=build_arg_list(self.kwargs_exit) + ) From bb32777c73a0c4f49adf1727fa150fc367a3deca Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sat, 26 Apr 2025 14:44:40 +0800 Subject: [PATCH 2/5] Define more classes --- pygmt/src/clip.py | 188 ++++++++++++++++++++++++++++++---------------- 1 file changed, 124 insertions(+), 64 deletions(-) diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 08fb0d0d8e2..3a7fb900633 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -8,6 +8,122 @@ from pygmt.helpers import build_arg_list, is_nonstr_iter +class _ClipContext: + """ + Base class for the clip context manager. + """ + + def __init__(self, figure, data=None, **kwargs): + self._figure = figure # The parent Figure object. + self._data = data + self._kwargs = kwargs + + def __enter__(self): + self._figure._preprocess() # Activate the current figure. + self._activate() + + def __exit__(self, exc_type, exc_val, exc_tb): + self._figure._preprocess() # Activate the current figure. + self._deactivate() + + def _activate(self): + """ + Activate clipping. + """ + raise NotImplementedError + + def _deactivate(self): + """ + Deactivate clipping. + """ + raise NotImplementedError + + +class _ClipLand(_ClipContext): + """ + Clip the land area (i.e., "dry" areas). + """ + + def _activate(self): + self._figure.coast(land=True, **self._kwargs) + + def _deactivate(self): + self._figure.coast(Q=True) + + +class _ClipWater(_ClipContext): + """ + Clip the water areas (i.e., "wet" areas such as oceans and lakes). + """ + + def _activate(self): + self._figure.coast(water=True, **self._kwargs) + + def _deactivate(self): + self._figure.coast(Q=True) + + +class _ClipDcw(_ClipContext): + """ + Clip based on the Digital Chart of the World. + """ + + def _activate(self): + self._figure.coast(**self._kwargs) + + def _deactivate(self): + self._figure.coast(Q=True) + + +class _ClipSolar(_ClipContext): + """ + Clip the data to the solar terminator. + """ + + def _activate(self): + self._figure.solar(fill=True, **self._kwargs) + + def _deactivate(self): + with Session() as lib: + lib.call_module(module="clip", args=build_arg_list({"C": True})) + + +class _ClipPolygon(_ClipContext): + """ + Clip polygonal paths. + """ + + def _activate(self): + with Session() as lib: + with lib.virtualfile_in(data=self._data) as vintbl: + lib.call_module( + module="clip", + args=build_arg_list(self._kwargs, infile=vintbl), + ) + + def _deactivate(self): + with Session() as lib: + lib.call_module(module="clip", args=build_arg_list({"C": True})) + + +class _ClipMask(_ClipContext): + """ + Clip the data to a mask. + """ + + def _activate(self): + with Session() as lib: + with lib.virtualfile_in(data=self._data) as vintbl: + lib.call_module( + module="mask", + args=build_arg_list(self._kwargs, infile=vintbl), + ) + + def _deactivate(self): + with Session() as lib: + lib.call_module(module="mask", args=build_arg_list({"C": True})) + + class ClipAccessor: """ Accessor for the clip methods. @@ -40,11 +156,7 @@ def land(self, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - self.data = None - self.module_enter = self.module_exit = "coast" - self.kwargs_enter = {"G": True} | kwargs - self.kwargs_exit = {"Q": True} - return self + return _ClipLand(self._fig, **kwargs) def water(self, **kwargs): """ @@ -70,11 +182,7 @@ def water(self, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - self.data = None - self.module_enter = self.module_exit = "coast" - self.kwargs_enter = {"S": True} | kwargs - self.kwargs_exit = {"Q": True} - return self + return _ClipWater(self._fig, **kwargs) def polygon(self, x, y, **kwargs): """ @@ -99,14 +207,9 @@ def polygon(self, x, y, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - self.data = (x, y) - self.module_enter = self.module_exit = "clip" - self.kwargs_enter = kwargs - self.kwargs_exit = {"C": True} + return _ClipPolygon(self._fig, data={"x": x, "y": y}, **kwargs) - return self - - def dcw(self, code: str | Sequence[str]): + def dcw(self, code: str | Sequence[str], **kwargs): """ Clip based on the Digital Chart of the World. @@ -123,12 +226,7 @@ def dcw(self, code: str | Sequence[str]): >>> fig.show() """ _code = ",".join(code) if is_nonstr_iter(code) else code - self.data = None - self.module_enter = "coast" - self.kwargs_enter = {"E": _code + "+c"} - self.module_exit = "coast" - self.kwargs_exit = {"Q": True} - return self + return _ClipDcw(self._fig, dcw=f"{_code}+c", **kwargs) def solar(self, **kwargs): """ @@ -142,17 +240,11 @@ def solar(self, **kwargs): >>> grid = load_earth_relief() >>> fig = Figure() >>> fig.basemap(region="g", projection="W15c", frame=True) - >>> with fig.clip.solar(T="c"): + >>> with fig.clip.solar(terminator="civil"): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - self.data = None - self.module_enter = "solar" - self.kwargs_enter = {"G": True} | kwargs - self.module_exit = "clip" - self.kwargs_exit = {"C": True} - - return self + return _ClipSolar(self._fig, **kwargs) def mask(self, x, y, spacing, radius=None): """ @@ -172,36 +264,4 @@ def mask(self, x, y, spacing, radius=None): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - self.data = (x, y) - self.module_enter = self.module_exit = "mask" - self.kwargs_enter = {"I": spacing, "S": radius} - self.kwargs_exit = {"C": True} - return self - - def __enter__(self): - """ - Enter the context manager. - """ - self._fig._preprocess() # Activate the current figure. - with Session() as lib: - if self.data: - with lib.virtualfile_in(x=self.data[0], y=self.data[1]) as vintbl: - lib.call_module( - module=self.module_enter, - args=build_arg_list(self.kwargs_enter, infile=vintbl), - ) - else: - lib.call_module( - module=self.module_enter, args=build_arg_list(self.kwargs_enter) - ) - return self - - def __exit__(self, exc_type, exc_value, traceback): - """ - Exit the context manager. - """ - self._fig._preprocess() # Activate the current figure. - with Session() as lib: - lib.call_module( - module=self.module_exit, args=build_arg_list(self.kwargs_exit) - ) + return _ClipMask(self._fig, data={"x": x, "y": y}, I=spacing, S=radius) From 83f04fa501ede9a8ad0429cce18db11d86229e6c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 12 May 2025 08:22:17 +0800 Subject: [PATCH 3/5] Improve docstrings --- pygmt/src/clip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 3a7fb900633..340a285b08a 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -1,5 +1,5 @@ """ -Clip. +clip - Clip a path and only plot data inside or outside. """ from collections.abc import Sequence @@ -134,7 +134,7 @@ def __init__(self, fig): def land(self, **kwargs): """ - Clip the land area (i.e., "dry" areas). + Clip the land area (i.e., "dry" areas) and only plot data inside. Must be used as a context manager. Any plotting operations within the context manager will be clipped to the land areas. From ccf4d92741dbfa32b67cac329e20b130b349e575 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 12 May 2025 17:37:43 +0800 Subject: [PATCH 4/5] Update clip.land and clip.water --- pygmt/figure.py | 2 +- pygmt/src/clip.py | 30 ++++++++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/pygmt/figure.py b/pygmt/figure.py index 31eb6f1206c..adcb9bf1301 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -141,7 +141,7 @@ def region(self) -> np.ndarray: @property def clip(self) -> ClipAccessor: """ - Set up a clipping path and only plot data inside/outside the clipped path. + Set up a clipping path and only plot data inside/outside it. See :class:`pygmt.src.clip.ClipAccessor ` for the usage. """ diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 340a285b08a..26b3c5023b4 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -126,11 +126,14 @@ def _deactivate(self): class ClipAccessor: """ - Accessor for the clip methods. + Accessor for different clip methods. """ - def __init__(self, fig): - self._fig = fig # The parent Figure object. + def __init__(self, figure): + """ + Initialize the ClipAccessor. + """ + self._figure = figure # The parent Figure object. def land(self, **kwargs): """ @@ -142,7 +145,8 @@ def land(self, **kwargs): Parameters ---------- kwargs - Additional arguments passed to :meth:`pygmt.Figure.coast`. + Additional keyword arguments passed to :meth:`pygmt.Figure.coast`. Not all + parameters make sense in this context. Examples -------- @@ -156,11 +160,12 @@ def land(self, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - return _ClipLand(self._fig, **kwargs) + return _ClipLand(self._figure, **kwargs) def water(self, **kwargs): """ - Clip the water areas (i.e., "wet" areas such as oceans and lakes). + Clip the water areas (i.e., "wet" areas such as oceans and lakes) and only plot + data inside. Must be used as a context manager. Any plotting operations within the context manager will be clipped to the water areas. @@ -168,7 +173,8 @@ def water(self, **kwargs): Parameters ---------- kwargs - Additional arguments passed to :meth:`pygmt.Figure.coast`. + Additional keyword arguments passed to :meth:`pygmt.Figure.coast`. Not all + parameters make sense in this context. Examples -------- @@ -182,7 +188,7 @@ def water(self, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - return _ClipWater(self._fig, **kwargs) + return _ClipWater(self._figure, **kwargs) def polygon(self, x, y, **kwargs): """ @@ -207,7 +213,7 @@ def polygon(self, x, y, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - return _ClipPolygon(self._fig, data={"x": x, "y": y}, **kwargs) + return _ClipPolygon(self._figure, data={"x": x, "y": y}, **kwargs) def dcw(self, code: str | Sequence[str], **kwargs): """ @@ -226,7 +232,7 @@ def dcw(self, code: str | Sequence[str], **kwargs): >>> fig.show() """ _code = ",".join(code) if is_nonstr_iter(code) else code - return _ClipDcw(self._fig, dcw=f"{_code}+c", **kwargs) + return _ClipDcw(self._figure, dcw=f"{_code}+c", **kwargs) def solar(self, **kwargs): """ @@ -244,7 +250,7 @@ def solar(self, **kwargs): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - return _ClipSolar(self._fig, **kwargs) + return _ClipSolar(self._figure, **kwargs) def mask(self, x, y, spacing, radius=None): """ @@ -264,4 +270,4 @@ def mask(self, x, y, spacing, radius=None): ... fig.grdimage(grid, cmap="geo") >>> fig.show() """ - return _ClipMask(self._fig, data={"x": x, "y": y}, I=spacing, S=radius) + return _ClipMask(self._figure, data={"x": x, "y": y}, I=spacing, S=radius) From 1784795456e4fb931838ab0bb77d0a92933eda12 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Tue, 13 May 2025 08:41:52 +0800 Subject: [PATCH 5/5] Update clip.solar --- pygmt/src/clip.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pygmt/src/clip.py b/pygmt/src/clip.py index 26b3c5023b4..50fbb75802a 100644 --- a/pygmt/src/clip.py +++ b/pygmt/src/clip.py @@ -238,6 +238,12 @@ def solar(self, **kwargs): """ Clip the data to the solar terminator. + Parameters + ---------- + kwargs + Additional keyword arguments passed to :meth:`pygmt.Figure.solar`. Not all + parameters make sense in this context. + Examples -------- >>> from pygmt import Figure