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 618217468c2..ec5b10df3b5 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 @@ -226,6 +227,7 @@ Miscellaneous which show_versions + src.ClipAccessor Datasets -------- diff --git a/pygmt/figure.py b/pygmt/figure.py index 591c30b5c85..9ec0b1efe02 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -10,6 +10,7 @@ from typing import Literal, overload from pygmt._typing import PathLike +from pygmt.src import ClipAccessor try: import IPython @@ -145,6 +146,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 it. + + 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..8f42373a1b8 --- /dev/null +++ b/pygmt/src/clip.py @@ -0,0 +1,373 @@ +""" +clip - Clip a path and only plot data inside or outside. +""" + +from collections.abc import Sequence + +from pygmt.clib import Session +from pygmt.helpers import ( + build_arg_list, + fmt_docstring, + is_nonstr_iter, + kwargs_to_strings, + use_alias, +) + + +class _ClipContext: + """ + Base class for the clip context manager. + """ + + def __init__(self, figure, data=None, x=None, y=None, **kwargs): + self._figure = figure # The parent Figure object. + self._data = data + self._x = x + self._y = y + self._kwargs = kwargs + + def __enter__(self): + self._figure._activate_figure() + self._activate() + + def __exit__(self, exc_type, exc_val, exc_tb): + self._figure._activate_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, x=self._x, y=self._y) 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 different clip methods. + """ + + def __init__(self, figure): + """ + Initialize the ClipAccessor. + """ + self._figure = figure # The parent Figure object. + + def land(self, **kwargs): + """ + 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. + + Parameters + ---------- + kwargs + Additional keyword arguments passed to :meth:`pygmt.Figure.coast`. Not all + parameters make sense in this context. + + 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() + """ + return _ClipLand(self._figure, **kwargs) + + def water(self, **kwargs): + """ + 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. + + Parameters + ---------- + kwargs + Additional keyword arguments passed to :meth:`pygmt.Figure.coast`. Not all + parameters make sense in this context. + + 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() + """ + return _ClipWater(self._figure, **kwargs) + + def dcw(self, code: str | Sequence[str], **kwargs): + """ + Clip based on the Digital Chart of the World. + + Must be used as a context manager. Any plotting operations within the context + manager will be clipped to the region defined by the codes. + + Parameters + ---------- + code + The codes of the region to clip to. + kwargs + Additional keyword arguments passed to :meth:`pygmt.Figure.coast`. Not all + parameters make sense in this context. + + 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 + return _ClipDcw(self._figure, dcw=f"{_code}+c", **kwargs) + + def solar(self, **kwargs): + """ + Clip the data to the solar terminator. + + Must be used as a context manager. Any plotting operations within the context + manager will be clipped 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 + >>> 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(terminator="civil"): + ... fig.grdimage(grid, cmap="geo") + >>> fig.show() + """ + return _ClipSolar(self._figure, **kwargs) + + @fmt_docstring + @use_alias( + A="straight_line", + B="frame", + J="projection", + N="invert", + R="region", + V="verbose", + W="pen", + ) + @kwargs_to_strings(R="sequence") + def polygon(self, data=None, x=None, y=None, **kwargs): + """ + Clip polygonal paths. + + Must be used as a context manager. Any plotting operations within the context + manager will be clipped to the polygons. + + {aliases} + + Parameters + ---------- + data + Either a file name to an ASCII data table, a 2-D {table-classes}. + x/y + X and Y coordinates of the polygon. + {frame} + {region} + {projection} + {verbose} + straight_line + By default, line segments are connected 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 connection 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 + Invert the sense of what is inside and outside. For example, when using a + single path, this means that only points outside that path will be shown. + Cannot be used together with ``frame``. + pen + Draw outline of clip path using given pen attributes before clipping is + initiated [Default is no outline]. + + 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() + """ + return _ClipPolygon(self._figure, data=data, x=x, y=y, **kwargs) + + @fmt_docstring + @use_alias( + I="spacing", + N="invert", + S="radius", + ) + @kwargs_to_strings(R="sequence") + def mask(self, data=None, x=None, y=None, **kwargs): + """ + Clip the data to a mask. + + Must be used as a context manager. Any plotting operations within the context + manager will be clipped to the mask. + + Parameters + ---------- + data + Either a file name to an ASCII data table, a 2-D {table-classes}. + x/y + X and Y coordinates of the mask. + {spacing} + invert + Invert the sense of what is inside and outside. + + Examples + -------- + >>> import numpy as np + >>> 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() + """ + return _ClipMask(self._figure, data=data, x=x, y=y, **kwargs)