From 32cf9a845e456ca8d8dd1318eb9b03387f062391 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 14 Jun 2026 16:15:59 +0800 Subject: [PATCH 1/6] Add Figure.fill_between to fill between two curves --- doc/api/index.rst | 1 + pygmt/figure.py | 2 + pygmt/src/fill_between.py | 142 ++++++++++++++++++ ...ll_between_two_coregistered_curves.png.dvc | 5 + .../test_fill_between_y2_scalar.png.dvc | 5 + pygmt/tests/test_fill_between.py | 89 +++++++++++ 6 files changed, 244 insertions(+) create mode 100644 pygmt/src/fill_between.py create mode 100644 pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc create mode 100644 pygmt/tests/baseline/test_fill_between_y2_scalar.png.dvc create mode 100644 pygmt/tests/test_fill_between.py diff --git a/doc/api/index.rst b/doc/api/index.rst index a760615228b..49b8166abb3 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -48,6 +48,7 @@ Plotting tabular data :toctree: generated Figure.contour + Figure.fill_between Figure.histogram Figure.meca Figure.plot diff --git a/pygmt/figure.py b/pygmt/figure.py index 0979fe07158..664798a9226 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -15,6 +15,7 @@ from pygmt.src.colorbar import colorbar as _colorbar from pygmt.src.contour import contour as _contour from pygmt.src.directional_rose import directional_rose as _directional_rose +from pygmt.src.fill_between import fill_between as _fill_between from pygmt.src.grdcontour import grdcontour as _grdcontour from pygmt.src.grdimage import grdimage as _grdimage from pygmt.src.grdview import grdview as _grdview @@ -449,6 +450,7 @@ def _repr_html_(self) -> str: colorbar = _colorbar contour = _contour directional_rose = _directional_rose + fill_between = _fill_between grdcontour = _grdcontour grdimage = _grdimage grdview = _grdview diff --git a/pygmt/src/fill_between.py b/pygmt/src/fill_between.py new file mode 100644 index 00000000000..8b204751a58 --- /dev/null +++ b/pygmt/src/fill_between.py @@ -0,0 +1,142 @@ +""" +fill_between - Fill the area between two horizontal curves. +""" + +from collections.abc import Sequence +from typing import Literal + +import numpy as np +from pygmt.alias import Alias, AliasSystem +from pygmt.clib import Session +from pygmt.exceptions import GMTValueError +from pygmt.helpers import build_arg_list, fmt_docstring +from pygmt.params import Axis, Frame + + +@fmt_docstring +def fill_between( # noqa: PLR0913 + self, + x: float | Sequence[float], + y: float | Sequence[float], + y2: float | Sequence[float] = 0, + fill: str | None = None, + pen: str | None = None, + label: str | None = None, + fill2: str | None = None, + pen2: str | None = None, + label2: str | None = None, + projection: str | None = None, + region: Sequence[float | str] | str | None = None, + frame: Frame | Axis | Literal["none"] | str | Sequence[str] | bool = False, + verbose: bool = False, + panel: int | Sequence[int] | bool = False, + perspective: float | Sequence[float] | str | bool = False, + transparency: float | Sequence[float] | bool | None = None, +): + """ + Fill the area between two horizontal curves. + + This method is a high-level wrapper around :meth:`pygmt.Figure.plot` to fill the + area between a primary curve ``y(x)`` and a secondary curve ``y2(x)``. The ``y2`` + parameter can be either a single value [Default is 0] or a sequence with the same + length as ``x`` and ``y``. + + Parameters + ---------- + x + X-coordinates of the curves. + y + Y-coordinates of the primary curve. + y2 + Y-coordinates of the secondary curve. It can be a scalar value for a horizontal + reference line, or a sequence with the same length as ``x`` and ``y``. Default + is 0. + fill + Fill for areas where the primary curve is greater than the secondary curve. + fill2 + Fill for areas where the secondary curve is greater than the primary curve. + pen + Pen attributes for the primary curve. + pen2 + Pen attributes for the secondary curve. + label + Label for the primary curve, to be displayed in the legend. + label2 + Label for the secondary curve, to be displayed in the legend. + $projection + $region + $frame + $verbose + $panel + $perspective + $transparency + + Examples + -------- + >>> import numpy as np + >>> import pygmt + >>> x = np.linspace(0, 2 * np.pi, 200) + >>> fig = pygmt.Figure() + >>> fig.fill_between( + ... x=x, + ... y=np.sin(2 * x), + ... y2=np.sin(3 * x), + ... region=[0, 4 * np.pi, -1.2, 1.2], + ... projection="X10c/4c", + ... frame=True, + ... fill="lightblue", + ... pen="1p,blue", + ... fill2="lightred", + ... pen2="1p,red", + ... ) + >>> fig.show() + """ + self._activate_figure() + _x = np.atleast_1d(x) + _y = np.atleast_1d(y) + _y2 = np.atleast_1d(y2) + + y2_is_scalar = np.ndim(y2) == 0 + + npoints = _x.size + if _y.size != npoints: + raise GMTValueError( + _y.size, + description="size for 'y'", + reason=f"'y' is expected to have length {npoints!r}.", + ) + if not y2_is_scalar and _y2.size != npoints: + raise GMTValueError( + _y2.size, + description="size for 'y2'", + reason=f"'y2' is expected to be a scalar or have length {npoints!r}.", + ) + + data = {"x": _x, "y": _y} if y2_is_scalar else {"x": _x, "y": _y, "y2": _y2} + + aliasdict = AliasSystem( + G=Alias(fill, name="fill"), + M=[ + Alias("c"), + Alias(fill2, name="fill2", prefix="+g"), + Alias(pen2, name="pen2", prefix="+p"), + Alias(label2, name="label2", prefix="+l"), + Alias(y2 if y2_is_scalar else None, name="y2", prefix="+y"), + ], + W=Alias(pen, name="pen"), + l=Alias(label, name="label"), + ).add_common( + B=frame, + J=projection, + R=region, + V=verbose, + c=panel, + p=perspective, + t=transparency, + ) + + with Session() as lib: + with lib.virtualfile_in(data=data) as vintbl: + lib.call_module( + module="plot", args=build_arg_list(aliasdict, infile=vintbl) + ) diff --git a/pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc b/pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc new file mode 100644 index 00000000000..94ee73bbbd9 --- /dev/null +++ b/pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: a4e06de28387321846b5dc8308224bde + size: 28624 + hash: md5 + path: test_fill_between_two_coregistered_curves.png diff --git a/pygmt/tests/baseline/test_fill_between_y2_scalar.png.dvc b/pygmt/tests/baseline/test_fill_between_y2_scalar.png.dvc new file mode 100644 index 00000000000..a1a0a0abc99 --- /dev/null +++ b/pygmt/tests/baseline/test_fill_between_y2_scalar.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 43a8d89f104ef27788da4ce04a31f941 + size: 20863 + hash: md5 + path: test_fill_between_y2_scalar.png diff --git a/pygmt/tests/test_fill_between.py b/pygmt/tests/test_fill_between.py new file mode 100644 index 00000000000..77155348acb --- /dev/null +++ b/pygmt/tests/test_fill_between.py @@ -0,0 +1,89 @@ +""" +Tests for Figure.fill_between. +""" + +import numpy as np +import pytest +from pygmt import Figure +from pygmt.exceptions import GMTValueError + + +@pytest.fixture(scope="module", name="x") +def fixture_x(): + """ + X-coordinates of the primary curve. + """ + return np.linspace(0, 4, 200) + + +@pytest.fixture(scope="module", name="y") +def fixture_y(x): + """ + Y-coordinates of the primary curve. + """ + return np.sin(5 * x) + + +@pytest.fixture(scope="module", name="y2") +def fixture_y2(x): + """ + Y-coordinates of the secondary curve. + """ + return 0.5 * np.cos(3 * x) + + +@pytest.mark.mpl_image_compare +def test_fill_between_y2_scalar(x, y): + """ + Fill between a curve and a horizontal reference level. + """ + fig = Figure() + fig.basemap(region=[0, 4, -1.2, 1.2], projection="X10c/5c", frame=True) + fig.fill_between( + x=x, + y=y, + y2=0, + fill="lightblue", + fill2="lightred", + pen="1p,blue", + pen2="1p,red", + label="y=sin(5x)", + label2="y=0", + ) + fig.legend() + return fig + + +@pytest.mark.mpl_image_compare +def test_fill_between_two_coregistered_curves(x, y, y2): + """ + Fill between two co-registered curves. + """ + fig = Figure() + fig.basemap(region=[0, 4, -1.2, 1.2], projection="X10c/5c", frame=True) + fig.fill_between( + x=x, + y=y, + y2=y2, + fill="lightgreen", + fill2="lightred", + pen="1p,green", + pen2="1p,red", + label="y=sin(5x)", + label2="y=0.5*cos(3x)", + ) + fig.legend() + return fig + + +def test_fill_between_invalid_input(): + """ + Test invalid input for fill_between. + """ + fig = Figure() + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1]) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1, 2], y2=[0, 1, 2]) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1, 2], y2=[0]) From f55530884b4e2e587f737887b7a205b54954fe2c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 14 Jun 2026 18:01:40 +0800 Subject: [PATCH 2/6] Update the gallery example for fill_between --- .../lines/fill_areas_between_curves.py | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/examples/gallery/lines/fill_areas_between_curves.py b/examples/gallery/lines/fill_areas_between_curves.py index 0b270aac158..b0c78e93d17 100644 --- a/examples/gallery/lines/fill_areas_between_curves.py +++ b/examples/gallery/lines/fill_areas_between_curves.py @@ -1,81 +1,75 @@ """ Fill area between curves ======================== -Using the ``fill_between`` parameter of the :meth:`pygmt.Figure.plot` method it is -possible to fill the area between two curves y1 and y2. Different fills (colors or -patterns) can be used for the areas y1 > y2 and y1 < y2. Optionally, the curves can be -drawn. -To plot an anomaly along a track use :meth:`pygmt.Figure.wiggle` and see the gallery -example :doc:`Wiggle along tracks `. +The :meth:`pygmt.Figure.fill_between` method fills the area between two curves y1 and +y2. Different fills (colors or patterns) can be used for the areas y1 > y2 and +y1 < y2. Optionally, the curves can be drawn. """ # %% import numpy as np -import pandas as pd import pygmt -# Generate some test data and create a pandas DataFrame +# Generate some test data x = np.arange(-10, 10.2, 0.1) y1 = np.sin(x * 3) y2 = np.sin(x / 2) -data_df = pd.DataFrame({"x": x, "y1": y1, "y2": y2}) - # %% -# Fill the areas between the two curves using the ``fill_between`` parameter. Use the -# ``fill`` parameter and the modifier **+g** for ``fill_between`` to set different fills -# for areas with y1 > y2 and y1 < y2, respectively. Use the ``label`` parameter and the -# modifier **+l** for ``fill_between`` to set the corresponding legend entries. +# Fill the areas between the two curves. Use the ``fill`` and ``fill2`` parameters to +# set different fills for areas with y1 > y2 and y1 < y2, respectively. Use the +# ``label`` and ``label2`` parameters to set the corresponding legend entries. fig = pygmt.Figure() fig.basemap(region=[-10, 10, -5, 5], projection="X15c/5c", frame=True) -fig.plot( - data=data_df, +fig.fill_between( + x=x, + y=y1, + y2=y2, fill="orange", + fill2="steelblue", label="short > long", - fill_between="c+gsteelblue+lshort < long", + label2="short < long", ) - fig.legend() - fig.show() # %% -# In addition to filling the areas, we can draw the curves. Use the ``pen`` parameter -# and the modifier **+p** for ``fill_between`` to set different lines for the two -# curves y1 and y2, respectively. +# In addition to filling the areas, we can draw the curves. Use the ``pen`` and +# ``pen2`` parameters to set different lines for the two curves y1 and y2, respectively. fig = pygmt.Figure() fig.basemap(region=[-10, 10, -5, 5], projection="X15c/5c", frame=True) - -fig.plot( - data=data_df, +fig.fill_between( + x=x, + y=y1, + y2=y2, fill="p8", + fill2="p17", pen="1p,black,solid", - fill_between="c+gp17+p1p,black,dashed", + pen2="1p,black,dashed", ) - fig.show() # %% -# To compare a curve y1 to a horizontal line, append **+y** to ``fill_between`` and give -# the desired y-level. +# To compare a curve y1 to a horizontal line, pass the desired y-level to ``y2``. fig = pygmt.Figure() fig.basemap(region=[-10, 10, -5, 5], projection="X15c/5c", frame=True) -fig.plot( - data=data_df[["x", "y1"]], +fig.fill_between( + x=x, + y=y1, + y2=0.42, fill="p8", + fill2="p17", pen="1p,black,solid", - # Define a horizontal line at y=0.42 - fill_between="c+gp17+p1p,black,dashed+y0.42", + pen2="1p,black,dashed", ) - fig.show() # sphinx_gallery_thumbnail_number = 1 From 5f767c88deb2b053d5ab8bf33d5852034fd97d9c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 14 Jun 2026 22:22:54 +0800 Subject: [PATCH 3/6] Check if x/y have at least two data point --- pygmt/src/fill_between.py | 11 +++++++++-- pygmt/tests/test_fill_between.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pygmt/src/fill_between.py b/pygmt/src/fill_between.py index 8b204751a58..d76320ca21b 100644 --- a/pygmt/src/fill_between.py +++ b/pygmt/src/fill_between.py @@ -16,8 +16,8 @@ @fmt_docstring def fill_between( # noqa: PLR0913 self, - x: float | Sequence[float], - y: float | Sequence[float], + x: Sequence[float], + y: Sequence[float], y2: float | Sequence[float] = 0, fill: str | None = None, pen: str | None = None, @@ -98,7 +98,14 @@ def fill_between( # noqa: PLR0913 y2_is_scalar = np.ndim(y2) == 0 + # Validate the lengths of the input arrays npoints = _x.size + if npoints <= 1: + raise GMTValueError( + npoints, + description="size for 'x'/'y'", + reason="'x' and 'y' must be arrays with lengths greater than 1.", + ) if _y.size != npoints: raise GMTValueError( _y.size, diff --git a/pygmt/tests/test_fill_between.py b/pygmt/tests/test_fill_between.py index 77155348acb..191ffbf1ea7 100644 --- a/pygmt/tests/test_fill_between.py +++ b/pygmt/tests/test_fill_between.py @@ -82,8 +82,14 @@ def test_fill_between_invalid_input(): """ fig = Figure() with pytest.raises(GMTValueError): - fig.fill_between(x=[0, 1], y=[1]) + fig.fill_between(x=0, y=[1, 2]) with pytest.raises(GMTValueError): - fig.fill_between(x=[0, 1], y=[1, 2], y2=[0, 1, 2]) + fig.fill_between(x=[0, 1], y=1) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0], y=[1]) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1]) with pytest.raises(GMTValueError): fig.fill_between(x=[0, 1], y=[1, 2], y2=[0]) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1, 2], y2=[0, 1, 2]) From 23342d1917a0f8f11fe5ade19cf4a896d379287e Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 14 Jun 2026 22:24:15 +0800 Subject: [PATCH 4/6] Improve the labeling of curves --- examples/gallery/lines/fill_areas_between_curves.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/gallery/lines/fill_areas_between_curves.py b/examples/gallery/lines/fill_areas_between_curves.py index b0c78e93d17..ac25d25eca2 100644 --- a/examples/gallery/lines/fill_areas_between_curves.py +++ b/examples/gallery/lines/fill_areas_between_curves.py @@ -25,13 +25,7 @@ fig.basemap(region=[-10, 10, -5, 5], projection="X15c/5c", frame=True) fig.fill_between( - x=x, - y=y1, - y2=y2, - fill="orange", - fill2="steelblue", - label="short > long", - label2="short < long", + x=x, y=y1, y2=y2, fill="orange", fill2="steelblue", label="y=y1", label2="y=y2" ) fig.legend() fig.show() From c809466f0fa448cf566102e420d1c6b7e0d61e09 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Sun, 14 Jun 2026 23:08:42 +0800 Subject: [PATCH 5/6] Rename test test_fill_between_coregistered --- ...ed_curves.png.dvc => test_fill_between_coregistered.png.dvc} | 2 +- pygmt/tests/test_fill_between.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pygmt/tests/baseline/{test_fill_between_two_coregistered_curves.png.dvc => test_fill_between_coregistered.png.dvc} (57%) diff --git a/pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc b/pygmt/tests/baseline/test_fill_between_coregistered.png.dvc similarity index 57% rename from pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc rename to pygmt/tests/baseline/test_fill_between_coregistered.png.dvc index 94ee73bbbd9..e34c0eacc09 100644 --- a/pygmt/tests/baseline/test_fill_between_two_coregistered_curves.png.dvc +++ b/pygmt/tests/baseline/test_fill_between_coregistered.png.dvc @@ -2,4 +2,4 @@ outs: - md5: a4e06de28387321846b5dc8308224bde size: 28624 hash: md5 - path: test_fill_between_two_coregistered_curves.png + path: test_fill_between_coregistered.png diff --git a/pygmt/tests/test_fill_between.py b/pygmt/tests/test_fill_between.py index 191ffbf1ea7..e0314ba5f95 100644 --- a/pygmt/tests/test_fill_between.py +++ b/pygmt/tests/test_fill_between.py @@ -55,7 +55,7 @@ def test_fill_between_y2_scalar(x, y): @pytest.mark.mpl_image_compare -def test_fill_between_two_coregistered_curves(x, y, y2): +def test_fill_between_coregistered(x, y, y2): """ Fill between two co-registered curves. """ From da643a0cdfafa3ac2763b06d954b0cb7a4a73cd8 Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Mon, 15 Jun 2026 00:02:26 +0800 Subject: [PATCH 6/6] Fix ype hints --- pygmt/src/fill_between.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pygmt/src/fill_between.py b/pygmt/src/fill_between.py index d76320ca21b..86f9c6ad2e7 100644 --- a/pygmt/src/fill_between.py +++ b/pygmt/src/fill_between.py @@ -28,10 +28,11 @@ def fill_between( # noqa: PLR0913 projection: str | None = None, region: Sequence[float | str] | str | None = None, frame: Frame | Axis | Literal["none"] | str | Sequence[str] | bool = False, - verbose: bool = False, + verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] + | bool = False, panel: int | Sequence[int] | bool = False, perspective: float | Sequence[float] | str | bool = False, - transparency: float | Sequence[float] | bool | None = None, + transparency: float | None = None, ): """ Fill the area between two horizontal curves.