diff --git a/doc/api/index.rst b/doc/api/index.rst index 07f76aff217..06bc87c3317 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -48,6 +48,7 @@ Plotting tabular data Figure.plot Figure.plot3d Figure.rose + Figure.scatter Figure.ternary Figure.velo Figure.wiggle diff --git a/pygmt/figure.py b/pygmt/figure.py index 4163ab52eb1..93ec4c5385f 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -418,6 +418,7 @@ def _repr_html_(self): plot3d, psconvert, rose, + scatter, set_panel, shift_origin, solar, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index e4db7321963..aad5f67cd83 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -42,6 +42,7 @@ from pygmt.src.project import project from pygmt.src.psconvert import psconvert from pygmt.src.rose import rose +from pygmt.src.scatter import scatter from pygmt.src.select import select from pygmt.src.shift_origin import shift_origin from pygmt.src.solar import solar diff --git a/pygmt/src/scatter.py b/pygmt/src/scatter.py new file mode 100644 index 00000000000..967ebd2a8dd --- /dev/null +++ b/pygmt/src/scatter.py @@ -0,0 +1,145 @@ +""" +scatter - Plot scatter points. +""" + +from collections.abc import Sequence + +from pygmt.helpers import fmt_docstring, is_nonstr_iter + + +def _parse_symbol_size( + symbol: str | Sequence[str], size: float | str | Sequence[float | str] +) -> str: + """ + Parse the 'symbol' and 'size' parameter into GMT's style string. + + Examples + -------- + >>> _parse_symbol_size("c", 0.2) + 'c0.2' + >>> _parse_symbol_size("c", "0.2c") + 'c0.2c' + >>> _parse_symbol_size("c", [0.2, 0.3]) + 'c' + >>> _parse_symbol_size(["c", "t"], "0.2c") + '0.2c' + >>> _parse_symbol_size(["c", "t"], [0.2, 0.3]) + '' + """ + return "".join(f"{arg}" for arg in [symbol, size] if not is_nonstr_iter(arg)) + + +@fmt_docstring +def scatter( # noqa: PLR0913 + self, + x, + y, + symbol: str | Sequence[str], + size: float | str | Sequence[float | str], + fill: str | Sequence[float] | None = None, + intensity: float | Sequence[float] | None = None, + transparency: float | Sequence[float] | None = None, + cmap: str | bool | None = None, + pen: str | float | None = None, + no_clip: bool = False, + perspective=None, +): + """ + Plot scatter points. + + It can plot data points with constant or varying symbols, sizes, colors, + transparencies, and intensities. The parameters ``symbol``, ``size``, ``fill``, + ``intensity``, and ``transparency`` can be a single scalar value or a sequence of + values with the same length as the number of data points. If a single value is + given, it is used for all data points. If a sequence is given, different values are + used for different data points. + + Parameters + ---------- + x, y + The data coordinates. + symbol + The symbol(s) to use. Valid symbols are: + + - ``-``: X-dash (-) + - ``+``: Plus + - ``a``: Star + - ``c``: Circle + - ``d``: Diamond + - ``g``: Octagon + - ``h``: Hexagon + - ``i``: Inverted triangle + - ``n``: Pentagon + - ``s``: Square + - ``t``: Triangle + - ``x``: Cross + - ``y``: Y-dash (|) + size + The size(s) of the points. + fill + Set color or pattern for filling symbols [Default is no fill]. If a sequence of + values is given, ``cmap`` must be specified. + intensity + The intensity(ies) of the points. + transparency + The transparency(ies) of the points. + cmap + The colormap to map scalar values in ``fill`` to colors. In this case, ``fill`` + must be a sequence of values. + pen + The pen property of the symbol outline. + no_clip + If True, do not clip the points that fall outside the frame boundaries. + {perspective} + + Examples + -------- + + Plot three points with the same symbol and size. + + >>> import pygmt + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 3, 0, 3], projection="X10c/5c", frame=True) + >>> fig.scatter(x=[0, 1, 2], y=[0, 1, 2], symbol="c", size=0.3, fill="red") + >>> fig.show() + + Plot three points with different sizes and transparencies. + + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 3, 0, 3], projection="X10c/5c", frame=True) + >>> fig.scatter( + ... x=[0, 1, 2], + ... y=[0, 1, 2], + ... symbol="c", + ... size=[0.5, 0.3, 0.2], + ... fill="blue", + ... transparency=[50, 70, 90], + ... ) + >>> fig.show() + """ + self._preprocess() + + # Create GMT plot's "style" from "symbol" and "size". + _style = _parse_symbol_size(symbol, size) + # Set "symbol" and "size" to None if they're not sequences. + _symbol = symbol if is_nonstr_iter(symbol) else None + _size = size if is_nonstr_iter(size) else None + + # Set "cmap" to True if "fill" is a sequence of values. + if is_nonstr_iter(fill) and cmap is None: + cmap = True + + self.plot( + x=x, + y=y, + style=_style, + symbol=_symbol, + size=_size, + fill=fill, + intensity=intensity, + transparency=transparency, + cmap=cmap, + pen=pen, + no_clip=no_clip, + perspective=perspective, + ) diff --git a/pygmt/tests/baseline/test_scatter.png.dvc b/pygmt/tests/baseline/test_scatter.png.dvc new file mode 100644 index 00000000000..84acf08cf75 --- /dev/null +++ b/pygmt/tests/baseline/test_scatter.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 5705688b5f45ec842980b94bec239b02 + size: 20703 + hash: md5 + path: test_scatter.png diff --git a/pygmt/tests/baseline/test_scatter_fills.png.dvc b/pygmt/tests/baseline/test_scatter_fills.png.dvc new file mode 100644 index 00000000000..edc28e369db --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_fills.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: f4bc6e6818d9889f6c729993dbfa8cfe + size: 22012 + hash: md5 + path: test_scatter_fills.png diff --git a/pygmt/tests/baseline/test_scatter_intensity.png.dvc b/pygmt/tests/baseline/test_scatter_intensity.png.dvc new file mode 100644 index 00000000000..e28d11f0119 --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_intensity.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: ff2b09d8983b015ea7229c6def8ef7de + size: 21670 + hash: md5 + path: test_scatter_intensity.png diff --git a/pygmt/tests/baseline/test_scatter_sizes.png.dvc b/pygmt/tests/baseline/test_scatter_sizes.png.dvc new file mode 100644 index 00000000000..68b4f0c9a75 --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_sizes.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4b85b972171fa9a5cfcc20fab7e81fb5 + size: 22315 + hash: md5 + path: test_scatter_sizes.png diff --git a/pygmt/tests/baseline/test_scatter_sizes_fills.png.dvc b/pygmt/tests/baseline/test_scatter_sizes_fills.png.dvc new file mode 100644 index 00000000000..d759a6c5aeb --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_sizes_fills.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 457480111a3d08e16aca82e8464809db + size: 22049 + hash: md5 + path: test_scatter_sizes_fills.png diff --git a/pygmt/tests/baseline/test_scatter_sizes_fills_transparencies.png.dvc b/pygmt/tests/baseline/test_scatter_sizes_fills_transparencies.png.dvc new file mode 100644 index 00000000000..1c704cc029a --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_sizes_fills_transparencies.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 2b85221c5c451cefeb0cd81e033724b2 + size: 21152 + hash: md5 + path: test_scatter_sizes_fills_transparencies.png diff --git a/pygmt/tests/baseline/test_scatter_sizes_fills_transparencies_intensity.png.dvc b/pygmt/tests/baseline/test_scatter_sizes_fills_transparencies_intensity.png.dvc new file mode 100644 index 00000000000..086ed902e84 --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_sizes_fills_transparencies_intensity.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: f0282431c7ef8d95c870e1c3b6bcdfbd + size: 21231 + hash: md5 + path: test_scatter_sizes_fills_transparencies_intensity.png diff --git a/pygmt/tests/baseline/test_scatter_symbols.png.dvc b/pygmt/tests/baseline/test_scatter_symbols.png.dvc new file mode 100644 index 00000000000..18ad6449807 --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_symbols.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4f5f9fc44bc8f5f32de388bc75a2d474 + size: 20479 + hash: md5 + path: test_scatter_symbols.png diff --git a/pygmt/tests/baseline/test_scatter_symbols_sizes.png.dvc b/pygmt/tests/baseline/test_scatter_symbols_sizes.png.dvc new file mode 100644 index 00000000000..eafe78a736a --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_symbols_sizes.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 8d23bb9dd39f1b1151412f8a43f6c12c + size: 21815 + hash: md5 + path: test_scatter_symbols_sizes.png diff --git a/pygmt/tests/baseline/test_scatter_symbols_sizes_fills_transparencies_intensity.png.dvc b/pygmt/tests/baseline/test_scatter_symbols_sizes_fills_transparencies_intensity.png.dvc new file mode 100644 index 00000000000..3afffde12ac --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_symbols_sizes_fills_transparencies_intensity.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 488eec9d3ef156993a82cc2a66125a67 + size: 20879 + hash: md5 + path: test_scatter_symbols_sizes_fills_transparencies_intensity.png diff --git a/pygmt/tests/baseline/test_scatter_transparencies.png.dvc b/pygmt/tests/baseline/test_scatter_transparencies.png.dvc new file mode 100644 index 00000000000..8f1dc493dcf --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_transparencies.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4943894ddcff5d0f486246e106839158 + size: 20679 + hash: md5 + path: test_scatter_transparencies.png diff --git a/pygmt/tests/baseline/test_scatter_valid_symbols.png.dvc b/pygmt/tests/baseline/test_scatter_valid_symbols.png.dvc new file mode 100644 index 00000000000..e7fb0a73dc0 --- /dev/null +++ b/pygmt/tests/baseline/test_scatter_valid_symbols.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 6d4e6acdd44006be6e384f425c302932 + size: 24570 + hash: md5 + path: test_scatter_valid_symbols.png diff --git a/pygmt/tests/test_scatter.py b/pygmt/tests/test_scatter.py new file mode 100644 index 00000000000..7305b09d532 --- /dev/null +++ b/pygmt/tests/test_scatter.py @@ -0,0 +1,247 @@ +""" +Test Figure.scatter. +""" + +import numpy as np +import pytest +from pygmt import Figure, makecpt + + +# +# Fixtures that are used in tests. +# +@pytest.fixture(scope="module", name="region") +def fixture_region(): + """ + The region of sample data. + """ + return [0, 5, 5, 10] + + +@pytest.fixture(scope="module", name="x") +def fixture_x(): + """ + The x coordinates of sample data. + """ + return [1, 2, 3, 4] + + +@pytest.fixture(scope="module", name="y") +def fixture_y(): + """ + The y coordinates of sample data. + """ + return [6, 7, 8, 9] + + +@pytest.fixture(scope="module", name="size") +def fixture_size(): + """ + The size of sample data. + """ + return [0.2, 0.4, 0.6, 0.8] + + +@pytest.fixture(scope="module", name="symbol") +def fixture_symbol(): + """ + The symbol of sample data. + """ + return ["a", "c", "i", "t"] + + +@pytest.fixture(scope="module", name="fill") +def fixture_fill(): + """ + The z value of sample data for fill. + """ + return [1, 2, 3, 4] + + +@pytest.fixture(scope="module", name="transparency") +def fixture_transparency(): + """ + The transparency of sample data. + """ + return [20, 40, 60, 80] + + +@pytest.fixture(scope="module", name="intensity") +def fixture_intensity(): + """ + The intensity of sample data. + """ + return [-0.8, -0.3, 0.3, 0.8] + + +# +# Tests for the simplest scatter plot with constant size and symbol. +# +@pytest.mark.mpl_image_compare +def test_scatter(region, x, y): + """ + Test the simplest scatter plot with constant size and symbol. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + fig.scatter(x=x, y=y, symbol="c", size=0.2) + return fig + + +# +# Tests for scatter plots with one parameter given as a sequence. +# +@pytest.mark.mpl_image_compare +def test_scatter_sizes(region, x, y, size): + """ + Test the scatter plot with different sizes. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + fig.scatter(x=x, y=y, symbol="c", size=size) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_symbols(region, x, y, symbol): + """ + Test the scatter plot with different symbols. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + fig.scatter(x=x, y=y, symbol=symbol, size=0.2) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_fills(region, x, y, fill): + """ + Test the scatter plot with different colors. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + makecpt(cmap="viridis", series=[0, 5]) + fig.scatter(x=x, y=y, symbol="c", size=0.5, fill=fill) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_transparencies(region, x, y, transparency): + """ + Test the scatter plot with different transparency. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + makecpt(cmap="viridis", series=[1, 4]) + fig.scatter(x=x, y=y, symbol="c", size=0.5, fill="red", transparency=transparency) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_intensity(region, x, y, intensity): + """ + Test the scatter plot with different intensity. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + fig.scatter(x=x, y=y, symbol="c", size=0.5, fill="red", intensity=intensity) + return fig + + +# +# Tests for scatter plots with multiple parameters given as sequences. +# +@pytest.mark.mpl_image_compare +def test_scatter_symbols_sizes(region, x, y, symbol, size): + """ + Test the scatter plot with different symbols and sizes. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + fig.scatter(x=x, y=y, symbol=symbol, size=size) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_sizes_fills(region, x, y, size, fill): + """ + Test the scatter plot with different sizes and colors. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + makecpt(cmap="viridis", series=[0, 5]) + fig.scatter(x=x, y=y, symbol="c", size=size, fill=fill) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_sizes_fills_transparencies(region, x, y, size, fill, transparency): + """ + Test the scatter plot with different sizes and colors. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + makecpt(cmap="viridis", series=[0, 5]) + fig.scatter(x=x, y=y, symbol="c", size=size, fill=fill, transparency=transparency) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_sizes_fills_transparencies_intensity( + region, x, y, size, fill, transparency, intensity +): + """ + Test the scatter plot with different sizes and colors. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + makecpt(cmap="viridis", series=[0, 5]) + fig.scatter( + x=x, + y=y, + symbol="c", + size=size, + fill=fill, + transparency=transparency, + intensity=intensity, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_scatter_symbols_sizes_fills_transparencies_intensity( + region, x, y, symbol, size, fill, transparency, intensity +): + """ + Test the scatter plot with different sizes and colors. + """ + fig = Figure() + fig.basemap(region=region, frame=True) + makecpt(cmap="viridis", series=[0, 5]) + fig.scatter( + x=x, + y=y, + symbol=symbol, + size=size, + fill=fill, + transparency=transparency, + intensity=intensity, + ) + return fig + + +# +# Other tests for scatter plots. +# +@pytest.mark.mpl_image_compare +def test_scatter_valid_symbols(): + """ + Test the scatter plot with data. + """ + symbols = ["-", "+", "a", "c", "d", "g", "h", "i", "n", "s", "t", "x", "y"] + x = np.arange(len(symbols)) + y = [1.0] * len(symbols) + fig = Figure() + fig.basemap(region=[-1, len(symbols), 0, 2], frame=True) + fig.scatter(x=x, y=y, symbol=symbols, size=0.5, pen="1p,black") + return fig