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/examples/gallery/lines/fill_areas_between_curves.py b/examples/gallery/lines/fill_areas_between_curves.py index 0b270aac158..ac25d25eca2 100644 --- a/examples/gallery/lines/fill_areas_between_curves.py +++ b/examples/gallery/lines/fill_areas_between_curves.py @@ -1,81 +1,69 @@ """ 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, - fill="orange", - label="short > long", - fill_between="c+gsteelblue+lshort < long", +fig.fill_between( + x=x, y=y1, y2=y2, fill="orange", fill2="steelblue", label="y=y1", label2="y=y2" ) - 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 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..d22464876cc --- /dev/null +++ b/pygmt/src/fill_between.py @@ -0,0 +1,148 @@ +""" +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: Sequence[float], + y: 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: 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 | 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 shared by the two 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.basemap(region=[0, 4 * np.pi, -1.2, 1.2], projection="X10c/4c", frame=True) + >>> fig.fill_between( + ... x=x, + ... y=np.sin(2 * x), + ... y2=np.sin(3 * x), + ... 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 + + # 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, + 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_coregistered.png.dvc b/pygmt/tests/baseline/test_fill_between_coregistered.png.dvc new file mode 100644 index 00000000000..74fbe1db7d2 --- /dev/null +++ b/pygmt/tests/baseline/test_fill_between_coregistered.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 27d288932c7094da06cd2532bfcb707c + size: 29497 + hash: md5 + path: test_fill_between_coregistered.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..83bc77baab0 --- /dev/null +++ b/pygmt/tests/test_fill_between.py @@ -0,0 +1,95 @@ +""" +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_coregistered(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="lightbrown", + pen="1p,green", + pen2="1p,brown", + 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, y=[1, 2]) + with pytest.raises(GMTValueError): + 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])