diff --git a/doc/api/index.rst b/doc/api/index.rst index 09e822bc87a..0598fd103f8 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -26,6 +26,7 @@ Plotting data and laying out the map: Figure.coast Figure.colorbar Figure.plot + Figure.plot3d Figure.contour Figure.grdcontour Figure.grdimage diff --git a/examples/gallery/plot/scatter3d.py b/examples/gallery/plot/scatter3d.py new file mode 100644 index 00000000000..047b2225b09 --- /dev/null +++ b/examples/gallery/plot/scatter3d.py @@ -0,0 +1,53 @@ +""" +3D Scatter plots +---------------- + +The :meth:`pygmt.Figure.plot3d` method can be used to plot symbols in 3D. +In the example below, we show how the +`Iris flower dataset `__ +can be visualized using a perspective 3-dimensional plot. The ``region`` +argument has to include the :math:`x`, :math:`y`, :math:`z` axis limits in the +form of (xmin, xmax, ymin, ymax, zmin, zmax), which can be done automatically +using :meth:`pygmt.info`. To include the z-axis stick, set ``frame`` as a +minimum to something like ``frame=["WsNeZ", "zaf"]``. Use ``perspective`` to +control the azimuth and elevation angle of the view, and ``zscale`` to adjust +the vertical exaggeration factor. +""" + +import pandas as pd +import pygmt + +# Load sample iris data, and convert 'species' column to categorical dtype +df = pd.read_csv("https://github.com/mwaskom/seaborn-data/raw/master/iris.csv") +df["species"] = df.species.astype(dtype="category") + +# Use pygmt.info to get region bounds (xmin, xmax, ymin, ymax, zmin, zmax) +# The below example will return a numpy array like [0., 3., 4., 8., 1., 7.] +region = pygmt.info( + table=df[["petal_width", "sepal_length", "petal_length"]], # x, y, z columns + per_column=True, # report output as a numpy array + spacing="1/2/0.5", # rounds x, y and z intervals by 1, 2 and 0.5 respectively +) + +# Make our 3D scatter plot, coloring each of the 3 species differently +fig = pygmt.Figure() +pygmt.makecpt(cmap="cubhelix", color_model="+c", series=(0, 3, 1)) +fig.plot3d( + x=df.petal_width, + y=df.sepal_length, + z=df.petal_length, + sizes=0.1 * df.sepal_width, # Vary each symbol size according to a data column + color=df.species.cat.codes.astype(int), # Points colored by categorical number code + cmap=True, # Use colormap created by makecpt + region=region, # (xmin, xmax, ymin, ymax, zmin, zmax) + frame=[ + "WsNeZ3", # z axis label positioned on 3rd corner + 'xafg+l"Petal Width"', + 'yafg+l"Sepal Length"', + 'zafg+l"Petal Length"', + ], + style="uc", # 3D cUbe, with size in centimeter units + perspective=[315, 25], # Azimuth NorthWest (315°), at elevation 25° + zscale=1.5, # Vertical exaggeration factor +) +fig.show() diff --git a/pygmt/base_plotting.py b/pygmt/base_plotting.py index 37b938c7a79..f75f69da075 100644 --- a/pygmt/base_plotting.py +++ b/pygmt/base_plotting.py @@ -816,6 +816,189 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs): arg_str = " ".join([fname, build_arg_string(kwargs)]) lib.call_module("plot", arg_str) + @fmt_docstring + @use_alias( + A="straight_line", + B="frame", + C="cmap", + D="offset", + G="color", + I="intensity", + J="projection", + Jz="zscale", + JZ="zsize", + L="close", + N="no_clip", + Q="no_sort", + R="region", + S="style", + V="verbose", + W="pen", + X="xshift", + Y="yshift", + Z="zvalue", + i="columns", + l="label", + p="perspective", + t="transparency", + ) + @kwargs_to_strings(R="sequence", i="sequence_comma", p="sequence") + def plot3d( + self, x=None, y=None, z=None, data=None, sizes=None, direction=None, **kwargs + ): + """ + Plot lines, polygons, and symbols in 3-D + + Takes a matrix, (x,y,z) triplets, or a file name as input and plots + lines, polygons, or symbols at those locations in 3-D. + + Must provide either *data* or *x*, *y* and *z*. + + If providing data through *x*, *y* and *z*, *color* can be a 1d array + that will be mapped to a colormap. + + If a symbol is selected and no symbol size given, then plot3d will + interpret the fourth column of the input data as symbol size. Symbols + whose size is <= 0 are skipped. If no symbols are specified then the + symbol code (see *style* below) must be present as last column in the + input. If *style* is not used, a line connecting the data points will + be drawn instead. To explicitly close polygons, use *close*. Select a + fill with *color*. If *color* is set, *pen* will control whether the + polygon outline is drawn or not. If a symbol is selected, *color* and + *pen* determines the fill and outline/no outline, respectively. + + Full option list at :gmt-docs:`plot3d.html` + + {aliases} + + Parameters + ---------- + x/y/z : float or 1d arrays + The x, y, and z coordinates, or arrays of x, y and z coordinates of + the data points + data : str or 2d array + Either a data file name or a 2d numpy array with the tabular data. + Use option *columns* (i) to choose which columns are x, y, z, + color, and size, respectively. + sizes : 1d array + The sizes of the data points in units specified in *style* (S). + Only valid if using *x*, *y* and *z*. + direction : list of two 1d arrays + If plotting vectors (using ``style='V'`` or ``style='v'``), then + should be a list of two 1d arrays with the vector directions. These + can be angle and length, azimuth and length, or x and y components, + depending on the style options chosen. + {J} + zscale/zsize : float or str + Set z-axis scaling or z-axis size. + {R} + straight_line : bool or str + ``[m|p|x|y]``. + By default, geographic line segments are drawn as great circle + arcs. To draw them as straight lines, use *straight_line*. + Alternatively, add **m** to draw the line by first following a + meridian, then a parallel. Or append **p** to start following a + parallel, then a meridian. (This can be practical to draw a line + along parallels, for example). For Cartesian data, points are + simply connected, unless you append **x** or **y** to draw + stair-case curves that whose first move is along *x* or *y*, + respectively. **Note**: The **straight_line** option requires + constant *z*-coordinates. + {B} + {CPT} + offset : str + ``dx/dy[/dz]``. + Offset the plot symbol or line locations by the given amounts + *dx/dy*[*dz*] [Default is no offset]. + {G} + intensity : float or bool + Provide an *intens* value (nominally in the -1 to +1 range) to + modulate the fill color by simulating illumination [None]. If + using ``intensity=True``, we will instead read *intens* from the + first data column after the symbol parameters (if given). + close : str + ``[+b|d|D][+xl|r|x0][+yl|r|y0][+ppen]``. + Force closed polygons. Full documentation is at + :gmt-docs:`plot3d.html#l`. + no_clip : bool or str + ``[c|r]``. + Do NOT clip symbols that fall outside map border [Default plots + points whose coordinates are strictly inside the map border only]. + The option does not apply to lines and polygons which are always + clipped to the map region. For periodic (360-longitude) maps we + must plot all symbols twice in case they are clipped by the + repeating boundary. ``no_clip=True`` will turn off clipping and not + plot repeating symbols. Use ``no_clip="r"`` to turn off clipping + but retain the plotting of such repeating symbols, or use + ``no_clip="c"`` to retain clipping but turn off plotting of + repeating symbols. + no_sort : bool + Turn off the automatic sorting of items based on their distance + from the viewer. The default is to sort the items so that items in + the foreground are plotted after items in the background. + style : str + Plot symbols. Full documentation is at :gmt-docs:`plot3d.html#s`. + {U} + {V} + {W} + {XY} + zvalue : str + ``value|file``. + Instead of specifying a symbol or polygon fill and outline color + via **color** and **pen**, give both a *value* via **zvalue** and a + color lookup table via **cmap**. Alternatively, give the name of a + *file* with one z-value (read from the last column) for each + polygon in the input data. To apply it to the fill color, use + ``color='+z'``. To apply it to the pen color, append **+z** to + **pen**. + label : str + Add a legend entry for the symbol or line being plotted. + {p} + {t} + *transparency* can also be a 1d array to set varying transparency + for symbols. + + """ + kwargs = self._preprocess(**kwargs) + + kind = data_kind(data, x, y, z) + + extra_arrays = [] + if "S" in kwargs and kwargs["S"][0] in "vV" and direction is not None: + extra_arrays.extend(direction) + if "G" in kwargs and not isinstance(kwargs["G"], str): + if kind != "vectors": + raise GMTInvalidInput( + "Can't use arrays for color if data is matrix or file." + ) + extra_arrays.append(kwargs["G"]) + del kwargs["G"] + if sizes is not None: + if kind != "vectors": + raise GMTInvalidInput( + "Can't use arrays for sizes if data is matrix or file." + ) + extra_arrays.append(sizes) + + if "t" in kwargs and is_nonstr_iter(kwargs["t"]): + extra_arrays.append(kwargs["t"]) + kwargs["t"] = "" + + with Session() as lib: + # Choose how data will be passed in to the module + if kind == "file": + file_context = dummy_context(data) + elif kind == "matrix": + file_context = lib.virtualfile_from_matrix(data) + elif kind == "vectors": + file_context = lib.virtualfile_from_vectors( + np.atleast_1d(x), np.atleast_1d(y), np.atleast_1d(z), *extra_arrays + ) + + with file_context as fname: + arg_str = " ".join([fname, build_arg_string(kwargs)]) + lib.call_module("plot3d", arg_str) + @fmt_docstring @use_alias( R="region", @@ -921,6 +1104,8 @@ def contour(self, x=None, y=None, z=None, data=None, **kwargs): @use_alias( R="region", J="projection", + Jz="zscale", + JZ="zsize", B="frame", L="map_scale", Td="rose", @@ -935,12 +1120,12 @@ def contour(self, x=None, y=None, z=None, data=None, **kwargs): @kwargs_to_strings(R="sequence", p="sequence") def basemap(self, **kwargs): """ - Produce a basemap for the figure. + Plot base maps and frames for the figure. - Several map projections are available, and the user may specify - separate tick-mark intervals for boundary annotation, ticking, and - [optionally] gridlines. A simple map scale or directional rose may also - be plotted. + Creates a basic or fancy basemap with axes, fill, and titles. Several + map projections are available, and the user may specify separate + tick-mark intervals for boundary annotation, ticking, and [optionally] + gridlines. A simple map scale or directional rose may also be plotted. At least one of the options *frame*, *map_scale*, *rose* or *compass* must be specified. @@ -952,6 +1137,8 @@ def basemap(self, **kwargs): Parameters ---------- {J} + zscale/zsize : float or str + Set z-axis scaling or z-axis size. {R} {B} map_scale : str diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 196cf46037f..cef4dae5448 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -269,7 +269,7 @@ def test_plot_varying_transparency(): @check_figures_equal() def test_plot_sizes_colors_transparencies(): - "Plot the data using z as transparency" + "Plot the data with varying sizes and colors using z as transparency" x = np.arange(1.0, 10.0) y = np.arange(1.0, 10.0) color = np.arange(1, 10) * 0.15 diff --git a/pygmt/tests/test_plot3d.py b/pygmt/tests/test_plot3d.py new file mode 100644 index 00000000000..ba0590b1a1c --- /dev/null +++ b/pygmt/tests/test_plot3d.py @@ -0,0 +1,588 @@ +""" +Tests plot3d. +""" +import os + +import numpy as np +import pytest + +from .. import Figure +from ..exceptions import GMTInvalidInput +from ..helpers import GMTTempFile +from ..helpers.testing import check_figures_equal + + +TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data") +POINTS_DATA = os.path.join(TEST_DATA_DIR, "points.txt") + + +@pytest.fixture(scope="module", name="data") +def fixture_data(): + "Load the point data from the test file" + return np.loadtxt(POINTS_DATA) + + +@pytest.fixture(scope="module", name="region") +def fixture_region(): + "The data region" + return [10, 70, -5, 10, 0, 1] + + +@check_figures_equal() +def test_plot3d_red_circles_zscale(data, region): + "Plot the 3D data in red circles passing in vectors and setting zscale = 5" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + R="/".join(map(str, region)), + J="X4i", + S="c0.2c", + G="red", + B=["afg", "zafg"], + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zscale=5, + perspective=[225, 30], + region=region, + projection="X4i", + style="c0.2c", + color="red", + frame=["afg", "zafg"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_red_circles_zsize(data, region): + "Plot the 3D data in red circles passing in vectors and setting zsize = 3i" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + JZ="3i", + p="225/30", + R="/".join(map(str, region)), + J="X4i", + S="c0.2c", + G="red", + B=["afg", "zafg"], + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zsize="3i", + perspective=[225, 30], + region=region, + projection="X4i", + style="c0.2c", + color="red", + frame=["afg", "zafg"], + ) + return fig_ref, fig_test + + +def test_plot3d_fail_no_data(data, region): + "Plot should raise an exception if no data is given" + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.plot3d( + region=region, projection="X4i", style="c0.2c", color="red", frame="afg" + ) + with pytest.raises(GMTInvalidInput): + fig.plot3d( + x=data[:, 0], + region=region, + projection="X4i", + style="c0.2c", + color="red", + frame="afg", + ) + with pytest.raises(GMTInvalidInput): + fig.plot3d( + y=data[:, 0], + region=region, + projection="X4i", + style="c0.2c", + color="red", + frame="afg", + ) + # Should also fail if given too much data + with pytest.raises(GMTInvalidInput): + fig.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + data=data, + region=region, + projection="X4i", + style="c0.2c", + color="red", + frame="afg", + ) + + +def test_plot3d_fail_size_color(data, region): + "Should raise an exception if array sizes and color are used with matrix" + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.plot3d( + data=data, + region=region, + projection="X4i", + style="c0.2c", + color=data[:, 2], + frame="afg", + ) + with pytest.raises(GMTInvalidInput): + fig.plot3d( + data=data, + region=region, + projection="X4i", + style="cc", + sizes=data[:, 2], + color="red", + frame="afg", + ) + + +@check_figures_equal() +def test_plot3d_projection(data, region): + "Plot the data in green squares with a projection" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + R="/".join(map(str, region)), + J="R270/4i", + S="s1c", + G="green", + B=["ag", "zag"], + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zscale=5, + perspective=[225, 30], + region=region, + projection="R270/4i", + style="s1c", + color="green", + frame=["ag", "zag"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_colors(data, region): + "Plot the data using z as colors" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + G="+z", + R="/".join(map(str, region)), + J="X3i", + S="c0.5c", + C="cubhelix", + B=["afg", "zafg"], + i="0,1,2,2", + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zscale=5, + perspective=[225, 30], + color=data[:, 2], + region=region, + projection="X3i", + style="c0.5c", + cmap="cubhelix", + frame=["afg", "zafg"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_sizes(data, region): + "Plot the data using z as sizes" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + i="0,1,2,2+s0.5", + R="/".join(map(str, region)), + J="X4i", + S="ui", + G="blue", + B=["af", "zaf"], + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zscale=5, + perspective=[225, 30], + sizes=0.5 * data[:, 2], + region=region, + projection="X4i", + # Using inches instead of cm because of upstream bug at + # https://github.com/GenericMappingTools/gmt/issues/4386 + style="ui", + color="blue", + frame=["af", "zaf"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_colors_sizes(data, region): + "Plot the data using z as sizes and colors" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + i="0,1,2,2,2+s0.5", + R="/".join(map(str, region)), + J="X3i", + S="ui", + C="copper", + B=["af", "zaf"], + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zscale=5, + perspective=[225, 30], + color=data[:, 2], + sizes=0.5 * data[:, 2], + region=region, + projection="X3i", + # Using inches instead of cm because of upstream bug at + # https://github.com/GenericMappingTools/gmt/issues/4386 + style="ui", + cmap="copper", + frame=["af", "zaf"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_colors_sizes_proj(data, region): + "Plot the data using z as sizes and colors with a projection" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + R="/".join(map(str, region)), + J="M10i", + B=["af", "zaf"], + G="+z", + i="0,1,2,2,2+s1", + S="ui", + C="copper", + ) + fig_test.plot3d( + x=data[:, 0], + y=data[:, 1], + z=data[:, 2], + zscale=5, + perspective=[225, 30], + region=region, + projection="M10i", + frame=["af", "zaf"], + color=data[:, 2], + sizes=data[:, 2], + # Using inches instead of cm because of upstream bug at + # https://github.com/GenericMappingTools/gmt/issues/4386 + style="ui", + cmap="copper", + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_transparency(): + "Plot the data with a constant transparency" + x = np.arange(1, 10) + y = np.arange(1, 10) + z = np.arange(1, 10) * 10 + + fig_ref, fig_test = Figure(), Figure() + # Use single-character arguments for the reference image + with GMTTempFile() as tmpfile: + np.savetxt(tmpfile.name, np.c_[x, y, z], fmt="%d") + fig_ref.plot3d( + data=tmpfile.name, + S="u0.2c", + G="blue", + R="0/10/0/10/10/90", + J="X4i", + Jz=0.1, + B="", + p="135/30", + t=80.0, + ) + + fig_test.plot3d( + x=x, + y=y, + z=z, + style="u0.2c", + color="blue", + region=[0, 10, 0, 10, 10, 90], + projection="X4i", + zscale=0.1, + frame=True, + perspective=[135, 30], + transparency=80.0, + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_varying_transparency(): + "Plot the data using z as transparency using 3-D column symbols" + x = np.arange(1, 10) + y = np.arange(1, 10) + z = np.arange(1, 10) * 10 + + fig_ref, fig_test = Figure(), Figure() + # Use single-character arguments for the reference image + with GMTTempFile() as tmpfile: + np.savetxt(tmpfile.name, np.c_[x, y, z, z, z], fmt="%d") + fig_ref.plot3d( + data=tmpfile.name, + S="o0.2c+B5", + G="blue", + R="0/10/0/10/10/90", + J="X4i", + Jz=0.1, + B="", + p="135/30", + t="", + ) + fig_test.plot3d( + x=x, + y=y, + z=z, + style="o0.2c+B5", + color="blue", + region=[0, 10, 0, 10, 10, 90], + projection="X4i", + zscale=0.1, + frame=True, + perspective=[135, 30], + transparency=z, + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_sizes_colors_transparencies(): + "Plot the data with varying sizes and colors using z as transparency" + x = np.arange(1.0, 10.0) + y = np.arange(1.0, 10.0) + z = np.arange(1, 10) * 10 + color = np.arange(1, 10) * 0.15 + size = np.arange(1, 10) * 0.2 + transparency = np.arange(1, 10) * 10 + + fig_ref, fig_test = Figure(), Figure() + # Use single-character arguments for the reference image + with GMTTempFile() as tmpfile: + np.savetxt(tmpfile.name, np.c_[x, y, z, color, size, transparency]) + fig_ref.plot3d( + data=tmpfile.name, + R="0/10/0/10/10/90", + J="X4i", + Jz=0.1, + p="135/30", + B="", + S="uc", + C="gray", + t="", + ) + fig_test.plot3d( + x=x, + y=y, + z=z, + region=[0, 10, 0, 10, 10, 90], + projection="X4i", + zscale=0.1, + perspective=[135, 30], + frame=True, + style="uc", + color=color, + sizes=size, + cmap="gray", + transparency=transparency, + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_matrix(data, region): + "Plot the data passing in a matrix and specifying columns" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + R="/".join(map(str, region)), + J="M10i", + S="c1c", + G="#aaaaaa", + B=["a", "za"], + i="0,1,2", + ) + fig_test.plot3d( + data=data, + zscale=5, + perspective=[225, 30], + region=region, + projection="M10i", + style="c1c", + color="#aaaaaa", + frame=["a", "za"], + columns="0,1,2", + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_matrix_color(data, region): + "Plot the data passing in a matrix and using a colormap" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + R="/".join(map(str, region)), + J="X5i", + S="c0.5cc", + C="rainbow", + i="0,1,2,2", + B=["a", "za"], + ) + fig_test.plot3d( + data=data, + zscale=5, + perspective=[225, 30], + region=region, + projection="X5i", + style="c0.5c", + cmap="rainbow", + columns=[0, 1, 2, 2], + frame=["a", "za"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_from_file(region): + "Plot using the data file name instead of loaded data" + fig_ref, fig_test = Figure(), Figure() + fig_ref.plot3d( + data=POINTS_DATA, + Jz=5, + p="225/30", + R="/".join(map(str, region)), + J="X10i", + S="d1c", + G="yellow", + B=["af", "zaf"], + i="0,1,2", + ) + fig_test.plot3d( + data=POINTS_DATA, + zscale=5, + perspective=[225, 30], + region=region, + projection="X10i", + style="d1c", + color="yellow", + frame=["af", "zaf"], + columns=[0, 1, 2], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_vectors(): + "Plot vectors" + azimuth = np.array([0, 45, 90, 135, 180, 225, 270, 310]) + lengths = np.linspace(0.1, 1, len(azimuth)) + lon = np.sin(np.deg2rad(azimuth)) + lat = np.cos(np.deg2rad(azimuth)) + elev = np.tan(np.deg2rad(azimuth)) + fig_ref, fig_test = Figure(), Figure() + with GMTTempFile() as tmpfile: + np.savetxt(tmpfile.name, np.c_[lon, lat, elev, azimuth, lengths]) + fig_ref.plot3d( + data=tmpfile.name, + Jz=2, + p="225/30", + R="-2/2/-2/2/-2/2", + J="X4i", + S="V1c+e", + G="black", + B=["af", "zaf"], + ) + fig_test.plot3d( + x=lon, + y=lat, + z=elev, + zscale=2, + perspective=[225, 30], + direction=(azimuth, lengths), + region=[-2, 2, -2, 2, -2, 2], + projection="X4i", + style="V1c+e", + color="black", + frame=["af", "zaf"], + ) + return fig_ref, fig_test + + +@check_figures_equal() +def test_plot3d_scalar_xyz(): + "Plot symbols given scalar x, y, z coordinates" + fig_ref, fig_test = Figure(), Figure() + with GMTTempFile() as tmpfile: + np.savetxt(tmpfile.name, np.c_[[-1.5, 0, 1.5], [1.5, 0, -1.5], [-1.5, 0, 1.5]]) + fig_ref.basemap( + R="-2/2/-2/2/-2/2", B=["xaf+lx", "yaf+ly", "zaf+lz"], Jz=2, p="225/30" + ) + fig_ref.plot3d(data=tmpfile.name, S="c1c", G="red", Jz="", p="", qi=0) + fig_ref.plot3d(data=tmpfile.name, S="t1c", G="green", Jz="", p="", qi=1) + fig_ref.plot3d(data=tmpfile.name, S="s1c", G="blue", Jz="", p="", qi=2) + + fig_test.basemap( + region=[-2, 2, -2, 2, -2, 2], + frame=["xaf+lx", "yaf+ly", "zaf+lz"], + zscale=2, + perspective=[225, 30], + ) + fig_test.plot3d( + x=-1.5, y=1.5, z=-1.5, style="c1c", color="red", zscale=True, perspective=True + ) + fig_test.plot3d( + x=0, y=0, z=0, style="t1c", color="green", zscale=True, perspective=True + ) + fig_test.plot3d( + x=1.5, y=-1.5, z=1.5, style="s1c", color="blue", zscale=True, perspective=True + ) + return fig_ref, fig_test