diff --git a/doc/api/index.rst b/doc/api/index.rst index ede258136e4..301aac99e0c 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -29,6 +29,7 @@ Plotting data and laying out the map: Figure.contour Figure.grdcontour Figure.grdimage + Figure.grdview Figure.legend Figure.logo Figure.image diff --git a/pygmt/base_plotting.py b/pygmt/base_plotting.py index a56f13868f0..0a1581f258d 100644 --- a/pygmt/base_plotting.py +++ b/pygmt/base_plotting.py @@ -2,6 +2,7 @@ Base class with plot generating commands. Does not define any special non-GMT methods (savefig, show, etc). """ +import contextlib import csv import os import numpy as np @@ -318,6 +319,114 @@ def grdimage(self, grid, **kwargs): arg_str = " ".join([fname, build_arg_string(kwargs)]) lib.call_module("grdimage", arg_str) + @fmt_docstring + @use_alias( + R="region", + J="projection", + Jz="zscale", + JZ="zsize", + B="frame", + C="cmap", + G="drapegrid", + N="plane", + Q="surftype", + Wc="contourpen", + Wm="meshpen", + Wf="facadepen", + p="perspective", + ) + @kwargs_to_strings(R="sequence", p="sequence") + def grdview(self, grid, **kwargs): + """ + Create 3-D perspective image or surface mesh from a grid. + + Reads a 2-D grid file and produces a 3-D perspective plot by drawing a + mesh, painting a colored/gray-shaded surface made up of polygons, or by + scanline conversion of these polygons to a raster image. Options + include draping a data set on top of a surface, plotting of contours on + top of the surface, and apply artificial illumination based on + intensities provided in a separate grid file. + + Full option list at :gmt-docs:`grdview.html` + + Parameters + ---------- + grid : str or xarray.DataArray + The file name of the input relief grid or the grid loaded as a + DataArray. + + zscale/zsize : float or str + Set z-axis scaling or z-axis size. + + cmap : str + The name of the color palette table to use. + + drapegrid : str or xarray.DataArray + The file name or a DataArray of the image grid to be draped on top + of the relief provided by grid. [Default determines colors from + grid]. Note that -Jz and -N always refers to the grid. The + drapegrid only provides the information pertaining to colors, which + (if drapegrid is a grid) will be looked-up via the CPT (see -C). + + plane : float or str + ``level[+gfill]``. + Draws a plane at this z-level. If the optional color is provided + via the +g modifier, and the projection is not oblique, the frontal + facade between the plane and the data perimeter is colored. + + surftype : str + Specifies cover type of the grid. + Select one of following settings: + 1. 'm' for mesh plot [Default]. + 2. 'mx' or 'my' for waterfall plots (row or column profiles). + 3. 's' for surface plot. + 4. 'i' for image plot. + 5. 'c'. Same as 'i' but will make nodes with z = NaN transparent. + For any of these choices, you may force a monochrome image by + appending the modifier +m. + + contourpen : str + Draw contour lines on top of surface or mesh (not image). Append + pen attributes used for the contours. + meshpen : str + Sets the pen attributes used for the mesh. You must also select -Qm + or -Qsm for meshlines to be drawn. + facadepen :str + Sets the pen attributes used for the facade. You must also select + -N for the facade outline to be drawn. + + perspective : list or str + ``'[x|y|z]azim[/elev[/zlevel]][+wlon0/lat0[/z0]][+vx0/y0]'``. + Select perspective view. + + {aliases} + """ + kwargs = self._preprocess(**kwargs) + kind = data_kind(grid, None, None) + with Session() as lib: + if kind == "file": + file_context = dummy_context(grid) + elif kind == "grid": + file_context = lib.virtualfile_from_grid(grid) + else: + raise GMTInvalidInput(f"Unrecognized data type for grid: {type(grid)}") + + with contextlib.ExitStack() as stack: + fname = stack.enter_context(file_context) + if "G" in kwargs: + drapegrid = kwargs["G"] + if data_kind(drapegrid) in ("file", "grid"): + if data_kind(drapegrid) == "grid": + drape_context = lib.virtualfile_from_grid(drapegrid) + drapefile = stack.enter_context(drape_context) + kwargs["G"] = drapefile + else: + raise GMTInvalidInput( + f"Unrecognized data type for drapegrid: {type(drapegrid)}" + ) + arg_str = " ".join([fname, build_arg_string(kwargs)]) + lib.call_module("grdview", arg_str) + @fmt_docstring @use_alias( R="region", diff --git a/pygmt/tests/baseline/test_grdview_drapegrid_dataarray.png b/pygmt/tests/baseline/test_grdview_drapegrid_dataarray.png new file mode 100644 index 00000000000..3fa54b3ecfb Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_drapegrid_dataarray.png differ diff --git a/pygmt/tests/baseline/test_grdview_grid_dataarray.png b/pygmt/tests/baseline/test_grdview_grid_dataarray.png new file mode 100644 index 00000000000..bef1240fcd9 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_grid_dataarray.png differ diff --git a/pygmt/tests/baseline/test_grdview_grid_file_with_region_subset.png b/pygmt/tests/baseline/test_grdview_grid_file_with_region_subset.png new file mode 100644 index 00000000000..dc9798fc1d4 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_grid_file_with_region_subset.png differ diff --git a/pygmt/tests/baseline/test_grdview_on_a_plane.png b/pygmt/tests/baseline/test_grdview_on_a_plane.png new file mode 100644 index 00000000000..ddd5634cac8 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_on_a_plane.png differ diff --git a/pygmt/tests/baseline/test_grdview_on_a_plane_styled_with_facadepen.png b/pygmt/tests/baseline/test_grdview_on_a_plane_styled_with_facadepen.png new file mode 100644 index 00000000000..64a7127921c Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_on_a_plane_styled_with_facadepen.png differ diff --git a/pygmt/tests/baseline/test_grdview_on_a_plane_with_colored_frontal_facade.png b/pygmt/tests/baseline/test_grdview_on_a_plane_with_colored_frontal_facade.png new file mode 100644 index 00000000000..d6debca7957 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_on_a_plane_with_colored_frontal_facade.png differ diff --git a/pygmt/tests/baseline/test_grdview_surface_mesh_plot_styled_with_meshpen.png b/pygmt/tests/baseline/test_grdview_surface_mesh_plot_styled_with_meshpen.png new file mode 100644 index 00000000000..5a92da03769 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_surface_mesh_plot_styled_with_meshpen.png differ diff --git a/pygmt/tests/baseline/test_grdview_surface_plot_styled_with_contourpen.png b/pygmt/tests/baseline/test_grdview_surface_plot_styled_with_contourpen.png new file mode 100644 index 00000000000..0d0e3611a6f Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_surface_plot_styled_with_contourpen.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_cmap_for_image_plot.png b/pygmt/tests/baseline/test_grdview_with_cmap_for_image_plot.png new file mode 100644 index 00000000000..2d1137d025d Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_cmap_for_image_plot.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_cmap_for_perspective_surface_plot.png b/pygmt/tests/baseline/test_grdview_with_cmap_for_perspective_surface_plot.png new file mode 100644 index 00000000000..1249d8f4343 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_cmap_for_perspective_surface_plot.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_cmap_for_surface_monochrome_plot.png b/pygmt/tests/baseline/test_grdview_with_cmap_for_surface_monochrome_plot.png new file mode 100644 index 00000000000..db799b8251e Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_cmap_for_surface_monochrome_plot.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_perspective.png b/pygmt/tests/baseline/test_grdview_with_perspective.png new file mode 100644 index 00000000000..06dd8959833 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_perspective.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_perspective_and_zaxis_frame.png b/pygmt/tests/baseline/test_grdview_with_perspective_and_zaxis_frame.png new file mode 100644 index 00000000000..a6156cc4e5c Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_perspective_and_zaxis_frame.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_perspective_and_zscale.png b/pygmt/tests/baseline/test_grdview_with_perspective_and_zscale.png new file mode 100644 index 00000000000..c9dd5da2e85 Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_perspective_and_zscale.png differ diff --git a/pygmt/tests/baseline/test_grdview_with_perspective_and_zsize.png b/pygmt/tests/baseline/test_grdview_with_perspective_and_zsize.png new file mode 100644 index 00000000000..2633773840d Binary files /dev/null and b/pygmt/tests/baseline/test_grdview_with_perspective_and_zsize.png differ diff --git a/pygmt/tests/test_grdview.py b/pygmt/tests/test_grdview.py new file mode 100644 index 00000000000..53df40a452b --- /dev/null +++ b/pygmt/tests/test_grdview.py @@ -0,0 +1,218 @@ +# pylint: disable=redefined-outer-name +""" +Tests grdview +""" +import pytest + +from .. import Figure, which +from ..datasets import load_earth_relief +from ..exceptions import GMTInvalidInput +from ..helpers import data_kind + + +@pytest.fixture(scope="module") +def grid(): + "Load the grid data from the sample earth_relief file" + return load_earth_relief().sel(lat=slice(-49, -42), lon=slice(-118, -107)) + + +@pytest.mark.mpl_image_compare +def test_grdview_grid_dataarray(grid): + """ + Run grdview by passing in a grid as an xarray.DataArray. + """ + fig = Figure() + fig.grdview(grid=grid) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_grid_file_with_region_subset(): + """ + Run grdview by passing in a grid filename, and cropping it to a region. + """ + gridfile = which("@earth_relief_60m", download="c") + + fig = Figure() + fig.grdview(grid=gridfile, region=[-116, -109, -47, -44]) + return fig + + +def test_grdview_wrong_kind_of_grid(grid): + """ + Run grdview using grid input that is not an xarray.DataArray or file. + """ + dataset = grid.to_dataset() # convert xarray.DataArray to xarray.Dataset + assert data_kind(dataset) == "matrix" + + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.grdview(grid=dataset) + + +@pytest.mark.mpl_image_compare +def test_grdview_with_perspective(grid): + """ + Run grdview by passing in a grid and setting a perspective viewpoint with + an azimuth from the SouthEast and an elevation angle 15 degrees from the + z-plane. + """ + fig = Figure() + fig.grdview(grid=grid, perspective=[135, 15]) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_with_perspective_and_zscale(grid): + """ + Run grdview by passing in a grid and setting a perspective viewpoint with + an azimuth from the SouthWest and an elevation angle 30 degrees from the + z-plane, plus a z-axis scaling factor of 0.005. + """ + fig = Figure() + fig.grdview(grid=grid, perspective=[225, 30], zscale=0.005) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_with_perspective_and_zsize(grid): + """ + Run grdview by passing in a grid and setting a perspective viewpoint with + an azimuth from the SouthWest and an elevation angle 30 degrees from the + z-plane, plus a z-axis size of 10cm. + """ + fig = Figure() + fig.grdview(grid=grid, perspective=[225, 30], zsize="10c") + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_with_cmap_for_image_plot(grid): + """ + Run grdview by passing in a grid and setting a colormap for producing an + image plot. + """ + fig = Figure() + fig.grdview(grid=grid, cmap="oleron", surftype="i") + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_with_cmap_for_surface_monochrome_plot(grid): + """ + Run grdview by passing in a grid and setting a colormap for producing a + surface monochrome plot. + """ + fig = Figure() + fig.grdview(grid=grid, cmap="oleron", surftype="s+m") + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_with_cmap_for_perspective_surface_plot(grid): + """ + Run grdview by passing in a grid and setting a colormap for producing a + surface plot with a 3D perspective viewpoint. + """ + fig = Figure() + fig.grdview( + grid=grid, cmap="oleron", surftype="s", perspective=[225, 30], zscale=0.005, + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_on_a_plane(grid): + """ + Run grdview by passing in a grid and plotting it on a z-plane, while + setting a 3D perspective viewpoint. + """ + fig = Figure() + fig.grdview(grid=grid, plane=-4000, perspective=[225, 30], zscale=0.005) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_on_a_plane_with_colored_frontal_facade(grid): + """ + Run grdview by passing in a grid and plotting it on a z-plane whose frontal + facade is colored gray, while setting a 3D perspective viewpoint. + """ + fig = Figure() + fig.grdview(grid=grid, plane="-4000+ggray", perspective=[225, 30], zscale=0.005) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_with_perspective_and_zaxis_frame(grid): + """ + Run grdview by passing in a grid and plotting an annotated vertical + z-axis frame. + """ + fig = Figure() + fig.grdview(grid=grid, perspective=[225, 30], zscale=0.005, frame="zaf") + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_surface_plot_styled_with_contourpen(grid): + """ + Run grdview by passing in a grid with styled contour lines plotted on top + of a surface plot. + """ + fig = Figure() + fig.grdview(grid=grid, cmap="relief", surftype="s", contourpen="0.5p,black,dash") + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_surface_mesh_plot_styled_with_meshpen(grid): + """ + Run grdview by passing in a grid with styled mesh lines plotted on top of a + surface mesh plot. + """ + fig = Figure() + fig.grdview(grid=grid, cmap="relief", surftype="sm", meshpen="0.5p,black,dash") + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_on_a_plane_styled_with_facadepen(grid): + """ + Run grdview by passing in a grid and plotting it on a z-plane with styled + lines for the frontal facade. + """ + fig = Figure() + fig.grdview( + grid=grid, + plane=-4000, + perspective=[225, 30], + zscale=0.005, + facadepen="0.5p,blue,dash", + ) + return fig + + +@pytest.mark.mpl_image_compare +def test_grdview_drapegrid_dataarray(grid): + """ + Run grdview by passing in both a grid and drapegrid as an xarray.DataArray, + setting a colormap for producing an image plot. + """ + drapegrid = 1.1 * grid + + fig = Figure() + fig.grdview(grid=grid, drapegrid=drapegrid, cmap="oleron", surftype="c") + return fig + + +def test_grdview_wrong_kind_of_drapegrid(grid): + """ + Run grdview using drapegrid input that is not an xarray.DataArray or file. + """ + dataset = grid.to_dataset() # convert xarray.DataArray to xarray.Dataset + assert data_kind(dataset) == "matrix" + + fig = Figure() + with pytest.raises(GMTInvalidInput): + fig.grdview(grid=grid, drapegrid=dataset)