diff --git a/pygmt/src/fill_between.py b/pygmt/src/fill_between.py index 268ed85a933..2949e72253e 100644 --- a/pygmt/src/fill_between.py +++ b/pygmt/src/fill_between.py @@ -12,6 +12,8 @@ from pygmt.helpers import build_arg_list, fmt_docstring from pygmt.params import Axis, Frame +__doctest_skip__ = ["fill_between"] + @fmt_docstring def fill_between( # noqa: PLR0913 @@ -19,6 +21,7 @@ def fill_between( # noqa: PLR0913 x: Sequence[float], y: Sequence[float], y2: float | Sequence[float] = 0, + x2: Sequence[float] | None = None, fill: str | None = None, pen: str | None = None, label: str | None = None, @@ -39,19 +42,23 @@ def fill_between( # noqa: PLR0913 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``. + parameter can be either a single value [Default is 0] or a sequence. It can share + the same ``x`` coordinates as ``y`` or use a separate ``x2`` coordinate sequence. Parameters ---------- x - X-coordinates shared by the two curves. + X-coordinates of the primary curve. 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. + reference line, or a sequence with the same length as ``x`` and ``y`` when + ``x2`` is not used. Default is 0. + x2 + X-coordinates of the secondary curve. Use this parameter only when ``y2`` is a + sequence sampled at different x-coordinates from ``y``. In that case, ``y2`` + must have the same length as ``x2``. fill Fill for areas where the primary curve is greater than the secondary curve. fill2 @@ -111,19 +118,30 @@ def fill_between( # noqa: PLR0913 description="size for 'y'", reason=f"'y' is expected to have length {npoints!r}.", ) - if not y2_is_scalar and _y2.size != npoints: + if y2_is_scalar and x2 is not None: + raise GMTValueError( + x2, + description="value for 'x2'", + reason="'x2' can only be used when 'y2' is a sequence.", + ) + if not y2_is_scalar and x2 is None 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} + _x2 = None if x2 is None else np.atleast_1d(x2) + if _x2 is not None and _y2.size != _x2.size: + raise GMTValueError( + _y2.size, + description="size for 'y2'", + reason=f"'y2' is expected to have length {_x2.size!r} when 'x2' is used.", + ) aliasdict = AliasSystem( G=Alias(fill, name="fill"), M=[ - Alias("c"), + Alias("s" if _x2 is not None else "c"), Alias(fill2, name="fill2", prefix="+g"), Alias(pen2, name="pen2", prefix="+p"), Alias(label2, name="label2", prefix="+l"), @@ -142,7 +160,18 @@ def fill_between( # noqa: PLR0913 ) with Session() as lib: - with lib.virtualfile_in(data=data) as vintbl: - lib.call_module( - module="plot", args=build_arg_list(aliasdict, infile=vintbl) - ) + if _x2 is not None: + with ( + lib.virtualfile_in(data={"x": _x, "y": _y}) as vintbl1, + lib.virtualfile_in(data={"x": _x2, "y": _y2}) as vintbl2, + ): + lib.call_module( + module="plot", + args=build_arg_list(aliasdict, infile=[vintbl1, vintbl2]), + ) + else: + data = {"x": _x, "y": _y} if y2_is_scalar else {"x": _x, "y": _y, "y2": _y2} + 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 index 74fbe1db7d2..d3a573f5e55 100644 --- a/pygmt/tests/baseline/test_fill_between_coregistered.png.dvc +++ b/pygmt/tests/baseline/test_fill_between_coregistered.png.dvc @@ -1,5 +1,5 @@ outs: -- md5: 27d288932c7094da06cd2532bfcb707c - size: 29497 +- md5: 390a2f2403f84cda79e9248c866db0ee + size: 39880 hash: md5 path: test_fill_between_coregistered.png diff --git a/pygmt/tests/baseline/test_fill_between_noncoregistered.png.dvc b/pygmt/tests/baseline/test_fill_between_noncoregistered.png.dvc new file mode 100644 index 00000000000..68ac046505e --- /dev/null +++ b/pygmt/tests/baseline/test_fill_between_noncoregistered.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: d0a262ef052241b331975e471f777785 + size: 33972 + hash: md5 + path: test_fill_between_noncoregistered.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 index a1a0a0abc99..9a72bc4ee6c 100644 --- a/pygmt/tests/baseline/test_fill_between_y2_scalar.png.dvc +++ b/pygmt/tests/baseline/test_fill_between_y2_scalar.png.dvc @@ -1,5 +1,5 @@ outs: -- md5: 43a8d89f104ef27788da4ce04a31f941 - size: 20863 +- md5: 50d5b464fcff97f1f42f62d83e98c6de + size: 25471 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 index 83bc77baab0..93964e4e903 100644 --- a/pygmt/tests/test_fill_between.py +++ b/pygmt/tests/test_fill_between.py @@ -13,7 +13,7 @@ def fixture_x(): """ X-coordinates of the primary curve. """ - return np.linspace(0, 4, 200) + return np.linspace(0, 4, 100) @pytest.fixture(scope="module", name="y") @@ -32,6 +32,24 @@ def fixture_y2(x): return 0.5 * np.cos(3 * x) +@pytest.fixture(scope="module", name="x3") +def fixture_x3(): + """ + X-coordinates of the secondary curve with non-co-registered sampling. + """ + return np.array( + [0, 0.21, 0.4, 0.63, 0.89, 1.18, 1.45, 1.69, 1.96, 2.26, 2.61, 3.23, 3.49, 4.0] + ) + + +@pytest.fixture(scope="module", name="y3") +def fixture_y3(x3): + """ + Y-coordinates of the secondary curve with non-co-registered sampling. + """ + return 0.5 * np.cos(3 * x3) + + @pytest.mark.mpl_image_compare def test_fill_between_y2_scalar(x, y): """ @@ -50,6 +68,7 @@ def test_fill_between_y2_scalar(x, y): label="y=sin(5x)", label2="y=0", ) + fig.plot(x=x, y=y, style="c0.06c", fill="blue", pen="blue") fig.legend() return fig @@ -72,6 +91,33 @@ def test_fill_between_coregistered(x, y, y2): label="y=sin(5x)", label2="y=0.5*cos(3x)", ) + fig.plot(x=x, y=y, style="c0.06c", fill="green", pen="green") + fig.plot(x=x, y=y2, style="c0.06c", fill="brown", pen="brown") + fig.legend() + return fig + + +@pytest.mark.mpl_image_compare +def test_fill_between_noncoregistered(x, y, x3, y3): + """ + Fill between two curves that do not share the same x-coordinates. + """ + fig = Figure() + fig.basemap(region=[0, 4, -1.2, 1.2], projection="X10c/5c", frame=True) + fig.fill_between( + x=x, + y=y, + x2=x3, + y2=y3, + fill="lightgreen", + fill2="lightbrown", + pen="1p,green", + pen2="1p,brown", + label="y=sin(5x)", + label2="y=0.5*cos(3x)", + ) + fig.plot(x=x, y=y, style="c0.06c", fill="green", pen="green") + fig.plot(x=x3, y=y3, style="c0.06c", fill="brown", pen="brown") fig.legend() return fig @@ -93,3 +139,7 @@ def test_fill_between_invalid_input(): 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]) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1, 2], y2=0, x2=[0, 1]) + with pytest.raises(GMTValueError): + fig.fill_between(x=[0, 1], y=[1, 2], y2=[0, 1], x2=[0])