diff --git a/.github/ISSUE_TEMPLATE/release_checklist.md b/.github/ISSUE_TEMPLATE/release_checklist.md index d9c9b2de597..0043fa7006c 100644 --- a/.github/ISSUE_TEMPLATE/release_checklist.md +++ b/.github/ISSUE_TEMPLATE/release_checklist.md @@ -13,6 +13,7 @@ assignees: '' **Before release**: - [ ] Reserve a DOI on [Zenodo](https://zenodo.org) by clicking on "New Version" - [ ] Update Changelog +- [ ] Add a new entry in `doc/_static/version_switch.js` for documentation switcher **Release**: - [ ] Go to [GitHub Release](https://github.com/GenericMappingTools/pygmt/releases) and make a release, this will automatically create a tag too diff --git a/.github/workflows/ci_tests_dev.yaml b/.github/workflows/ci_tests_dev.yaml index 8e08e5cf836..e73021b00bd 100644 --- a/.github/workflows/ci_tests_dev.yaml +++ b/.github/workflows/ci_tests_dev.yaml @@ -21,7 +21,7 @@ jobs: matrix: python-version: [3.8] os: [ubuntu-20.04, macOS-10.15] - gmt_git_ref: [6.1, master] + gmt_git_ref: [master] env: # LD_LIBRARY_PATH: ${{ github.workspace }}/gmt/lib:$LD_LIBRARY_PATH GMT_INSTALL_DIR: ${{ github.workspace }}/gmt-install-dir diff --git a/Makefile b/Makefile index 82dd1d560aa..d6672c19f5e 100644 --- a/Makefile +++ b/Makefile @@ -6,8 +6,8 @@ PYTEST_ARGS=--cov=$(PROJECT) --cov-config=../.coveragerc \ --doctest-modules -v --mpl --mpl-results-path=results \ --pyargs ${PYTEST_EXTRA} BLACK_FILES=$(PROJECT) setup.py doc/conf.py examples -FLAKE8_FILES=$(PROJECT) setup.py -LINT_FILES=$(PROJECT) setup.py +FLAKE8_FILES=$(PROJECT) setup.py doc/conf.py +LINT_FILES=$(PROJECT) setup.py doc/conf.py help: @echo "Commands:" @@ -47,7 +47,8 @@ lint: clean: find . -name "*.pyc" -exec rm -v {} \; find . -name "*~" -exec rm -v {} \; - rm -rvf build dist MANIFEST *.egg-info __pycache__ .coverage .cache htmlcov coverage.xml + find . -type d -name "__pycache__" -exec rm -rv {} + + rm -rvf build dist MANIFEST *.egg-info .coverage .cache htmlcov coverage.xml rm -rvf $(TESTDIR) rm -rvf baseline rm -rvf result_images diff --git a/README.rst b/README.rst index 4d12c74e9f7..d0c04e6e224 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ Project goals * Build a Pythonic API for GMT. * Interface with the GMT C API directly using ctypes (no system calls). * Support for rich display in the Jupyter notebook. -* Integration with the Scipy stack: numpy.ndarray or pandas.DataFrame for data tables +* Integration with the PyData Ecosystem: numpy.ndarray or pandas.DataFrame for data tables and xarray.DataArray for grids. diff --git a/doc/Makefile b/doc/Makefile index d310fc02cbb..ce8dc86f764 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -21,7 +21,7 @@ help: @echo " doctest run all doctests embedded in the documentation (if enabled)" clean: - rm -rf $(BUILDDIR)/html/* + rm -rf $(BUILDDIR)/html rm -rf $(BUILDDIR)/doctrees rm -rf $(BUILDDIR)/linkcheck rm -rf modules diff --git a/doc/_static/version_switch.js b/doc/_static/version_switch.js new file mode 100644 index 00000000000..b61756ae60f --- /dev/null +++ b/doc/_static/version_switch.js @@ -0,0 +1,79 @@ +// Copyright 2013 PSF. Licensed under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +// File originates from the cpython source found in Doc/tools/sphinxext/static/version_switch.js + +(function() { + 'use strict'; + + var doc_url = "www.pygmt.org"; + //var doc_url = "0.0.0.0:8000"; // for local testing only + var url_re = new RegExp(doc_url + "\\/(dev|latest|(v\\d+\\.\\d+\\.\\d+))\\/"); + // List all versions. + // Add one entry "version: title" for any minor releases + var all_versions = { + 'latest': 'latest', + 'dev': 'dev', + 'v0.2.0': 'v0.2.0', + 'v0.1.2': 'v0.1.2', + 'v0.1.1': 'v0.1.1', + 'v0.1.0': 'v0.1.0', + '0.0.1a0': 'v0.0.1a0', + }; + + function build_select(current_version, current_release) { + var buf = [''); + return buf.join(''); + } + + function patch_url(url, new_version) { + return url.replace(url_re, doc_url + '/' + new_version + '/'); + } + + function on_switch() { + var selected = $(this).children('option:selected').attr('value'); + + var url = window.location.href, + new_url = patch_url(url, selected); + + if (new_url != url) { + // check beforehand if url exists, else redirect to version's start page + $.ajax({ + url: new_url, + success: function() { + window.location.href = new_url; + }, + error: function() { + window.location.href = 'http://' + doc_url + '/' + selected; + } + }); + } + } + + $(document).ready(function() { + var match = url_re.exec(window.location.href); + if (match) { + var release = DOCUMENTATION_OPTIONS.VERSION; + var version = match[1]; + var select = build_select(version, release); + $('.version_switch_note').html('Or, select a version from the drop-down menu above.'); + $('.version').html(select); + $('.version select').bind('change', on_switch); + } + }); +})(); diff --git a/doc/_templates/layout.html b/doc/_templates/layout.html index 3de7b40dee3..eb0656de46a 100644 --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -15,6 +15,10 @@ ga('set', 'anonymizeIp', true); ga('send', 'pageview'); + + + + {% endblock %} diff --git a/doc/conf.py b/doc/conf.py index c3a9e6ef388..6b61ac02008 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- -import sys -import os -import glob -import shutil +""" +Sphinx documentation configuration file. +""" +# pylint: disable=invalid-name + import datetime -import sphinx_rtd_theme -import sphinx_gallery -from sphinx_gallery.sorting import FileNameSortKey, ExplicitOrder +from sphinx_gallery.sorting import ( # pylint: disable=no-name-in-module + FileNameSortKey, + ExplicitOrder, +) from pygmt import __version__, __commit__ from pygmt.sphinx_gallery import PyGMTScraper @@ -60,6 +62,7 @@ "gallery_dirs": ["gallery", "tutorials", "projections"], "subsection_order": ExplicitOrder( [ + "../examples/gallery/line", "../examples/gallery/coast", "../examples/gallery/plot", "../examples/gallery/grid", @@ -90,6 +93,7 @@ templates_path = ["_templates"] exclude_patterns = ["_build", "**.ipynb_checkpoints"] source_suffix = ".rst" +needs_sphinx = "1.8" # The encoding of source files. source_encoding = "utf-8-sig" master_doc = "index" @@ -97,11 +101,12 @@ # General information about the project year = datetime.date.today().year project = "PyGMT" -copyright = "2017-{}, The PyGMT Developers.".format(year) +copyright = f"2017-{year}, The PyGMT Developers." # pylint: disable=redefined-builtin if len(__version__.split("+")) > 1 or __version__ == "unknown": version = "dev" else: version = __version__ +release = __version__ # These enable substitutions using |variable| in the rst files rst_epilog = """ @@ -116,6 +121,7 @@ html_logo = "" html_favicon = "_static/favicon.png" html_static_path = ["_static"] +html_css_files = ["style.css"] html_extra_path = [] pygments_style = "default" add_function_parentheses = False @@ -127,20 +133,21 @@ html_theme = "sphinx_rtd_theme" html_theme_options = {} repository = "GenericMappingTools/pygmt" -commit_link = f'{ __commit__[:7] }' +repository_url = "https://github.com/GenericMappingTools/pygmt" +commit_link = f'{ __commit__[:7] }' html_context = { "menu_links": [ ( ' Contributing', - "https://github.com/GenericMappingTools/pygmt/blob/master/CONTRIBUTING.md", + f"{repository_url}/blob/master/CONTRIBUTING.md", ), ( ' Code of Conduct', - "https://github.com/GenericMappingTools/pygmt/blob/master/CODE_OF_CONDUCT.md", + f"{repository_url}/blob/master/CODE_OF_CONDUCT.md", ), ( ' License', - "https://github.com/GenericMappingTools/pygmt/blob/master/LICENSE.txt", + f"{repository_url}/blob/master/LICENSE.txt", ), ( ' Contact', @@ -148,7 +155,7 @@ ), ( ' Source Code', - "https://github.com/GenericMappingTools/pygmt", + repository_url, ), ], # Custom variables to enable "Improve this page"" and "Download notebook" @@ -162,8 +169,3 @@ "github_version": "master", "commit": commit_link, } - - -# Load the custom CSS files (needs sphinx >= 1.6 for this to work) -def setup(app): - app.add_stylesheet("style.css") diff --git a/doc/overview.rst b/doc/overview.rst index 53ffd321749..7296bb8a19d 100644 --- a/doc/overview.rst +++ b/doc/overview.rst @@ -49,13 +49,13 @@ These are conference presentations about the development of PyGMT (previously * "Building an object-oriented Python interface for the Generic Mapping Tools". 2018. Leonardo Uieda and Paul Wessel. - Presented at *Scipy 2018*. + Presented at *SciPy 2018*. doi:`10.6084/m9.figshare.6814052 `__ .. figure:: _static/scipy2018-youtube-thumbnail.png :target: https://www.youtube.com/watch?v=6wMtfZXfTRM :align: center - :alt: Scipy youtube video + :alt: SciPy youtube video * "Integrating the Generic Mapping Tools with the Scientific Python Ecosystem". 2018. @@ -71,13 +71,13 @@ These are conference presentations about the development of PyGMT (previously * "Bringing the Generic Mapping Tools to Python". 2017. Leonardo Uieda and Paul Wessel. - Presented at *Scipy 2017*. + Presented at *SciPy 2017*. doi:`10.6084/m9.figshare.7635833 `__ .. figure:: _static/scipy2017-youtube-thumbnail.png :target: https://www.youtube.com/watch?v=93M4How7R24 :align: center - :alt: Scipy youtube video + :alt: SciPy youtube video * "A modern Python interface for the Generic Mapping Tools". 2017. diff --git a/examples/gallery/line/README.txt b/examples/gallery/line/README.txt new file mode 100644 index 00000000000..043f6ca6a79 --- /dev/null +++ b/examples/gallery/line/README.txt @@ -0,0 +1,2 @@ +Lines +----- diff --git a/examples/gallery/line/linestyles.py b/examples/gallery/line/linestyles.py new file mode 100644 index 00000000000..d0a1f1daa52 --- /dev/null +++ b/examples/gallery/line/linestyles.py @@ -0,0 +1,42 @@ +""" +Line styles +----------- + +The :meth:`pygmt.Figure.plot` method can plot lines in different styles. +The default line style is a 0.25-point wide, black, solid line, and can be +customized via the ``pen`` argument. + +A *pen* in GMT has three attributes: *width*, *color*, and *style*. +The *style* attribute controls the appearance of the line. +Giving “dotted” or “.” yields a dotted line, whereas a dashed pen is requested +with “dashed” or “-”. Also combinations of dots and dashes, like “.-” for a +dot-dashed line, are allowed. + +For more advanced *pen* attributes, see the GMT cookbook +:gmt-docs:`cookbook/features.html#wpen-attrib`. + +""" + +import numpy as np +import pygmt + +# Generate a sample line for plotting +x = np.linspace(0, 10, 500) +y = np.sin(x) + +fig = pygmt.Figure() +fig.basemap(region=[0, 10, -3, 3], projection="X15c/8c", frame=["xaf", "yaf", "WSrt"]) + +# Plot the line using the default line style +fig.plot(x=x, y=y) + +# Plot the lines using different line styles +fig.plot(x=x, y=y + 1.5, pen="1p,red,-") +fig.plot(x=x, y=y + 1.0, pen="2p,blue,.") +fig.plot(x=x, y=y + 0.5, pen="1p,red,-.") + +fig.plot(x=x, y=y - 0.5, pen="2p,blue,..-") +fig.plot(x=x, y=y - 1.0, pen="3p,tomato,--.") +fig.plot(x=x, y=y - 1.5, pen="3p,tomato,4_2:2p") + +fig.show() diff --git a/examples/gallery/plot/colorbar.py b/examples/gallery/plot/colorbar.py index 195198cfdac..beb4a160362 100644 --- a/examples/gallery/plot/colorbar.py +++ b/examples/gallery/plot/colorbar.py @@ -2,50 +2,57 @@ Colorbar -------- -The :meth:`pygmt.Figure.colorbar` method creates a color scalebar. We must specify the -colormap via the ``cmap`` argument, and set the placement via the ``position`` argument. -The full list of color paletted tables can be found at :gmt-docs:`cookbook/cpts.html`. -You can set the `position` of the colorbar using the following options: - -- j/J: justified inside/outside the mapframe using any 2 character combination of - vertical (**T** op, **M** iddle, **B** ottom) and horizontal (**L** eft, **C** enter, - **R** ight) alignment codes, e.g. `position="jTR"` for top right. -- g: using map coordinates, e.g. `position="g170/-45"` for longitude 170E, latitude 45S. -- x: using paper coordinates, e.g. `position="x5c/7c"` for 5cm,7cm from anchor point. +The :meth:`pygmt.Figure.colorbar` method creates a color scalebar. We must +specify the colormap via the ``cmap`` argument, and optionally set the +placement via the ``position`` argument. The full list of color palette tables +can be found at :gmt-docs:`cookbook/cpts.html`. You can set the `position` of +the colorbar using the following options: + +- j/J: justified inside/outside the map frame using any 2 character combination + of vertical (**T** op, **M** iddle, **B** ottom) and horizontal (**L** eft, + **C** enter, **R** ight) alignment codes, e.g. `position="jTR"` for top + right. +- g: using map coordinates, e.g. `position="g170/-45"` for longitude 170E, + latitude 45S. +- x: using paper coordinates, e.g. `position="x5c/7c"` for 5cm,7cm from anchor + point. - n: using normalized (0-1) coordinates, e.g. `position="n0.4/0.8"`. -Note that the anchor point defaults to the bottom left (BL). Append +h to ``position`` -to get a horizontal colorbar instead of a vertical one. For more advanced styling -options, see the full option list at :gmt-docs:`colorbar.html`. +Note that the anchor point defaults to the bottom left (BL). Append +h to +``position`` to get a horizontal colorbar instead of a vertical one. For more +advanced styling options, see the full option list at +:gmt-docs:`colorbar.html`. """ import pygmt fig = pygmt.Figure() -fig.basemap(region=[0, 3, 6, 9], projection="t0/3c", frame=True) +fig.basemap(region=[0, 3, 6, 9], projection="x3c", frame=["af", 'WSne+t"Colorbars"']) -# Create a colorbar suitable for surface topography- oleron +## Create a colorbar designed for seismic tomography - roma +# Colorbar is placed at bottom center (BC) by default if no position is given +fig.colorbar(cmap="roma", frame=["x+lVelocity", "y+lm/s"]) +## Create a colorbar showing the scientific rainbow - batlow fig.colorbar( - cmap="oleron", - position="jTC+w6c/1c+h", # justified inside map frame (j) at Top Center (TC) - box=True, - frame=["+Loleron", "xaf", "y+lm"], - scale=10, -) -# Create a colorbar designed for seismic tomography- roma -fig.colorbar( - cmap="roma", - position="x1.2c/4.75c+w6c/1c+h", # plot using paper coordinates (x) at 1.2cm,4.75cm + cmap="batlow", + # Colorbar positioned at map coordinates (g) longitude/latitude 0.3/8.7, + # with a length/width (+w) of 4cm by 0.5cm, and plotted horizontally (+h) + position="g0.3/8.7+w4c/0.5c+h", box=True, - frame=["+Lroma", "xaf", "y+lm/s"], - scale=10, + frame=["x+lTemperature", r"y+l\260C"], + scale=100, ) -# Create a colorbar showing the scientific rainbow - batlow + +## Create a colorbar suitable for surface topography - oleron fig.colorbar( - cmap="batlow", - position="g0.45/6.6+w6c/1c+h", # plot using map coordinates (g) at lon/lat 0.45/6.6 - box=True, - frame=["+Lbatlow", "xaf", r"y+l\260C"], + cmap="oleron", + # Colorbar position justified outside map frame (J) at Middle Right (MR), + # offset (+o) by 1cm horizontally and 0cm vertically from anchor point, + # with a length/width (+w) of 7cm by 0.5cm and a box for NaN values (+n) + position="JMR+o1c/0c+w7c/0.5c+n+mc", + # Note that the label 'Elevation' is moved to the opposite side and plotted + # vertically as a column of text using '+mc' in the position argument above + frame=["x+lElevation", "y+lm"], scale=10, ) diff --git a/examples/gallery/plot/scatter.py b/examples/gallery/plot/scatter.py new file mode 100644 index 00000000000..2d3199b35c8 --- /dev/null +++ b/examples/gallery/plot/scatter.py @@ -0,0 +1,41 @@ +""" +Scatter plots with a legend +--------------------------- + +To create a scatter plot with a legend one may use a loop and create one scatter +plot per item to appear in the legend and set the label accordingly. + +Modified from the matplotlib example: +https://matplotlib.org/gallery/lines_bars_and_markers/scatter_with_legend.html +""" + +import numpy as np +import pygmt + +np.random.seed(19680801) +n = 200 # number of random data points + +fig = pygmt.Figure() +fig.basemap( + region=[-0.1, 1.1, -0.1, 1.1], + projection="X10c/10c", + frame=["xa0.2fg", "ya0.2fg", "WSrt"], +) +for color in ["blue", "orange", "green"]: + x, y = np.random.rand(2, n) # random X and Y data in [0,1] + sizes = np.random.rand(n) * 0.5 # random size [0,0.5], in cm + # plot data points as circles (style="c"), with different sizes + fig.plot( + x=x, + y=y, + style="c", + sizes=sizes, + color=color, + # Set the legend label, + # and set the circle size to be 0.25 cm (+S0.25c) in legend + label=f"{color}+S0.25c", + transparency=70, # set transparency level for all symbols + ) + +fig.legend(transparency=30) # set transparency level for legends +fig.show() diff --git a/examples/tutorials/first-figure.py b/examples/tutorials/first-figure.py index 752f010ab5e..40dce53a050 100644 --- a/examples/tutorials/first-figure.py +++ b/examples/tutorials/first-figure.py @@ -27,7 +27,7 @@ ######################################################################################## # Add elements to the figure using its methods. For example, let's start a map with an # automatic frame and ticks around a given longitude and latitude bound, set the -# projection to Mercator (``M``), and the figure width to 8 inches: +# projection to Mercator (``M``), and the map width to 8 inches: fig.basemap(region=[-90, -70, 0, 20], projection="M8i", frame=True) diff --git a/pygmt/base_plotting.py b/pygmt/base_plotting.py index a3f682bebf9..e02eb03d310 100644 --- a/pygmt/base_plotting.py +++ b/pygmt/base_plotting.py @@ -68,8 +68,12 @@ def _preprocess(self, **kwargs): # pylint: disable=no-self-use G="land", S="water", U="timestamp", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) - @kwargs_to_strings(R="sequence") + @kwargs_to_strings(R="sequence", p="sequence") def coast(self, **kwargs): """ Plot continents, shorelines, rivers, and borders on maps @@ -128,6 +132,9 @@ def coast(self, **kwargs): shorelines : str ``'[level/]pen'`` Draw shorelines [Default is no shorelines]. Append pen attributes. + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -144,8 +151,12 @@ def coast(self, **kwargs): F="box", G="truncate", W="scale", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) - @kwargs_to_strings(R="sequence", G="sequence") + @kwargs_to_strings(R="sequence", G="sequence", p="sequence") def colorbar(self, **kwargs): """ Plot a gray or color scale-bar on maps. @@ -205,6 +216,9 @@ def colorbar(self, **kwargs): scale : float Multiply all z-values in the CPT by the provided scale. By default the CPT is used as is. + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -225,8 +239,12 @@ def colorbar(self, **kwargs): U="timestamp", W="pen", l="label", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) - @kwargs_to_strings(R="sequence", L="sequence", A="sequence_plus") + @kwargs_to_strings(R="sequence", L="sequence", A="sequence_plus", p="sequence") def grdcontour(self, grid, **kwargs): """ Convert grids or images to contours and plot them on maps @@ -274,6 +292,7 @@ def grdcontour(self, grid, **kwargs): {G} {U} {W} + {XY} label : str Add a legend entry for the contour being plotted. Normally, the annotated contour is selected for the legend. You can select the @@ -281,6 +300,8 @@ def grdcontour(self, grid, **kwargs): to be of the format [*annotcontlabel*][/*contlabel*]. If either label contains a slash (/) character then use ``|`` as the separator for the two labels instead. + {p} + {t} """ kwargs = self._preprocess(**kwargs) kind = data_kind(grid, None, None) @@ -296,13 +317,60 @@ def grdcontour(self, grid, **kwargs): lib.call_module("grdcontour", arg_str) @fmt_docstring - @use_alias(R="region", J="projection", W="pen", B="frame", I="shading", C="cmap") - @kwargs_to_strings(R="sequence") + @use_alias( + A="img_out", + B="frame", + C="cmap", + D="img_in", + E="dpi", + G="bit_color", + I="shading", + J="projection", + M="monochrome", + N="no_clip", + Q="nan_transparent", + R="region", + U="timestamp", + V="verbose", + X="xshift", + Y="yshift", + n="interpolation", + p="perspective", + t="transparency", + x="cores", + ) + @kwargs_to_strings(R="sequence", p="sequence") def grdimage(self, grid, **kwargs): """ - Project grids or images and plot them on maps. - - Takes a grid file name or an xarray.DataArray object as input. + Project and plot grids or images. + + Reads a 2-D grid file and produces a gray-shaded (or colored) map by + building a rectangular image and assigning pixels a gray-shade (or + color) based on the z-value and the CPT file. Optionally, illumination + may be added by providing a file with intensities in the (-1,+1) range + or instructions to derive intensities from the input data grid. Values + outside this range will be clipped. Such intensity files can be created + from the grid using `grdgradient` and, optionally, modified by + `grdmath` or `grdhisteq`. If GMT is built with GDAL support, *grid* can + be an image file (geo-referenced or not). In this case the image can + optionally be illuminated with the file provided via the *shading* + option. Here, if image has no coordinates then those of the intensity + file will be used. + + When using map projections, the grid is first resampled on a new + rectangular grid with the same dimensions. Higher resolution images can + be obtained by using the *dpi* option. To obtain the resampled value + (and hence shade or color) of each map pixel, its location is inversely + projected back onto the input grid after which a value is interpolated + between the surrounding input grid values. By default bi-cubic + interpolation is used. Aliasing is avoided by also forward projecting + the input grid nodes. If two or more nodes are projected onto the same + pixel, their average will dominate in the calculation of the pixel + value. Interpolation and aliasing is controlled with the + *interpolation* option. + + The *region* option can be used to select a map region larger or + smaller than that implied by the extent of the grid. Full option list at :gmt-docs:`grdimage.html` @@ -311,7 +379,85 @@ def grdimage(self, grid, **kwargs): Parameters ---------- grid : str or xarray.DataArray - The file name of the input grid or the grid loaded as a DataArray. + The file name or a DataArray containing the input 2-D gridded data + set or image to be plotted (See GRID FILE FORMATS at + :gmt-docs:`grdimage.html#grid-file-formats`). + img_out : str + ``out_img[=driver]``. + Save an image in a raster format instead of PostScript. Use + extension .ppm for a Portable Pixel Map format which is the only + raster format GMT can natively write. For GMT installations + configured with GDAL support there are more choices: Append + *out_img* to select the image file name and extension. If the + extension is one of .bmp, .gif, .jpg, .png, or .tif then no driver + information is required. For other output formats you must append + the required GDAL driver. The *driver* is the driver code name used + by GDAL; see your GDAL installation's documentation for available + drivers. Append a **+c**\\ *options* string where options is a list + of one or more concatenated number of GDAL **-co** options. For + example, to write a GeoPDF with the TerraGo format use + ``=PDF+cGEO_ENCODING=OGC_BP``. Notes: (1) If a tiff file (.tif) is + selected then we will write a GeoTiff image if the GMT projection + syntax translates into a PROJ syntax, otherwise a plain tiff file + is produced. (2) Any vector elements will be lost. + {B} + {CPT} + img_in : str + ``[r]`` + GMT will automatically detect standard image files (Geotiff, TIFF, + JPG, PNG, GIF, etc.) and will read those via GDAL. For very obscure + image formats you may need to explicitly set *img_in*, which + specifies that the grid is in fact an image file to be read via + GDAL. Append **r** to assign the region specified by *region* + to the image. For example, if you have used ``region='d'`` then the + image will be assigned a global domain. This mode allows you to + project a raw image (an image without referencing coordinates). + dpi : int + ``[i|dpi]``. + Sets the resolution of the projected grid that will be created if a + map projection other than Linear or Mercator was selected [100]. By + default, the projected grid will be of the same size (rows and + columns) as the input file. Specify **i** to use the PostScript + image operator to interpolate the image at the device resolution. + bit_color : str + ``color[+b|f]``. + This option only applies when a resulting 1-bit image otherwise + would consist of only two colors: black (0) and white (255). If so, + this option will instead use the image as a transparent mask and + paint the mask with the given color. Append **+b** to paint the + background pixels (1) or **+f** for the foreground pixels + [Default]. + shading : str + ``[intensfile|intensity|modifiers]``. + Give the name of a grid file with intensities in the (-1,+1) range, + or a constant intensity to apply everywhere (affects the ambient + light). Alternatively, derive an intensity grid from the input data + grid via a call to `grdgradient`; append **+a**\\ *azimuth*, + **+n**\\ *args*, and **+m**\\ *ambient* to specify azimuth, + intensity, and ambient arguments for that module, or just give + **+d** to select the default arguments (``+a-45+nt1+m0``). If you + want a more specific intensity scenario then run `grdgradient` + separately first. If we should derive intensities from another file + than grid, specify the file with suitable modifiers [Default is no + illumination]. + {J} + monochrome : bool + Force conversion to monochrome image using the (television) YIQ + transformation. Cannot be used with *nan_transparent*. + no_clip : bool + Do not clip the image at the map boundary (only relevant for + non-rectangular maps). + nan_transparent : bool + Make grid nodes with z = NaN transparent, using the color-masking + feature in PostScript Level 3 (the PS device must support PS Level + 3). + {R} + {V} + {XY} + {n} + {p} + {t} + {x} """ kwargs = self._preprocess(**kwargs) @@ -341,8 +487,11 @@ def grdimage(self, grid, **kwargs): Wc="contourpen", Wm="meshpen", Wf="facadepen", - p="perspective", I="shading", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) @kwargs_to_strings(R="sequence", p="sequence") def grdview(self, grid, **kwargs): @@ -406,10 +555,6 @@ def grdview(self, grid, **kwargs): 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. - shading : str Provide the name of a grid file with intensities in the (-1,+1) range, or a constant intensity to apply everywhere (affects the @@ -419,6 +564,10 @@ def grdview(self, grid, **kwargs): intensity, and ambient arguments for that module, or just give ``+d`` to select the default arguments (``+a-45+nt1+m0``). + {XY} + {p} + {t} + """ kwargs = self._preprocess(**kwargs) kind = data_kind(grid, None, None) @@ -458,8 +607,12 @@ def grdview(self, grid, **kwargs): l="label", C="cmap", U="timestamp", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) - @kwargs_to_strings(R="sequence", i="sequence_comma") + @kwargs_to_strings(R="sequence", i="sequence_comma", p="sequence") def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs): """ Plot lines, polygons, and symbols on maps. @@ -525,8 +678,12 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs): quoted lines). {W} {U} + {XY} label : str Add a legend entry for the symbol or line being plotted. + + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -576,8 +733,12 @@ def plot(self, x=None, y=None, data=None, sizes=None, direction=None, **kwargs): i="columns", l="label", C="levels", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) - @kwargs_to_strings(R="sequence", i="sequence_comma") + @kwargs_to_strings(R="sequence", i="sequence_comma", p="sequence") def contour(self, x=None, y=None, z=None, data=None, **kwargs): """ Contour table data by direct triangulation. @@ -633,6 +794,9 @@ def contour(self, x=None, y=None, z=None, data=None, **kwargs): to be of the format [*annotcontlabel*][/*contlabel*]. If either label contains a slash (/) character then use ``|`` as the separator for the two labels instead. + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -663,8 +827,12 @@ def contour(self, x=None, y=None, z=None, data=None, **kwargs): Td="rose", Tm="compass", U="timestamp", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) - @kwargs_to_strings(R="sequence") + @kwargs_to_strings(R="sequence", p="sequence") def basemap(self, **kwargs): """ Produce a basemap for the figure. @@ -696,6 +864,9 @@ def basemap(self, **kwargs): Draws a map magnetic rose on the map at the location defined by the reference and anchor points {U} + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -705,8 +876,18 @@ def basemap(self, **kwargs): lib.call_module("basemap", build_arg_string(kwargs)) @fmt_docstring - @use_alias(R="region", J="projection", U="timestamp", D="position", F="box") - @kwargs_to_strings(R="sequence") + @use_alias( + R="region", + J="projection", + U="timestamp", + D="position", + F="box", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", + ) + @kwargs_to_strings(R="sequence", p="sequence") def logo(self, **kwargs): """ Place the GMT graphics logo on a map. @@ -731,6 +912,9 @@ def logo(self, **kwargs): Without further options, draws a rectangular border around the GMT logo. {U} + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -740,8 +924,18 @@ def logo(self, **kwargs): lib.call_module("logo", build_arg_string(kwargs)) @fmt_docstring - @use_alias(R="region", J="projection", D="position", F="box", M="monochrome") - @kwargs_to_strings(R="sequence") + @use_alias( + R="region", + J="projection", + D="position", + F="box", + M="monochrome", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", + ) + @kwargs_to_strings(R="sequence", p="sequence") def image(self, imagefile, **kwargs): """ Place images or EPS files on maps. @@ -774,6 +968,9 @@ def image(self, imagefile, **kwargs): monochrome : bool Convert color image to monochrome grayshades using the (television) YIQ-transformation. + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) with Session() as lib: @@ -781,8 +978,17 @@ def image(self, imagefile, **kwargs): lib.call_module("image", arg_str) @fmt_docstring - @use_alias(R="region", J="projection", D="position", F="box") - @kwargs_to_strings(R="sequence") + @use_alias( + R="region", + J="projection", + D="position", + F="box", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", + ) + @kwargs_to_strings(R="sequence", p="sequence") def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwargs): """ Plot legends on maps. @@ -816,6 +1022,9 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg rectangular border around the legend using **MAP_FRAME_PEN**. By default, uses '+gwhite+p1p' which draws a box around the legend using a 1 point black pen and adds a white background. + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -844,6 +1053,10 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg D="offset", G="fill", W="pen", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", ) @kwargs_to_strings( R="sequence", @@ -851,6 +1064,7 @@ def legend(self, spec=None, position="JTR+jTR+o0.2c", box="+gwhite+p1p", **kwarg angle="sequence_comma", font="sequence_comma", justify="sequence_comma", + p="sequence", ) def text( self, @@ -949,6 +1163,9 @@ def text( Sets the pen used to draw a rectangle around the text string (see *clearance*) [Default is width = default, color = black, style = solid]. + {XY} + {p} + {t} """ kwargs = self._preprocess(**kwargs) @@ -998,8 +1215,17 @@ def text( lib.call_module("text", arg_str) @fmt_docstring - @use_alias(R="region", J="projection", B="frame", C="offset") - @kwargs_to_strings(R="sequence") + @use_alias( + R="region", + J="projection", + B="frame", + C="offset", + X="xshift", + Y="yshift", + p="perspective", + t="transparency", + ) + @kwargs_to_strings(R="sequence", p="sequence") def meca( self, spec, @@ -1094,6 +1320,9 @@ def meca( {J} {R} {B} + {XY} + {p} + {t} """ # pylint warnings that need to be fixed diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index 0a11c37277c..376952891b4 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -53,6 +53,15 @@ "W": """\ pen : str Set pen attributes for lines or the outline of symbols.""", + "XY": """\ + xshift : str + ``[a|c|f|r][xshift]``. + Shift plot origin in x-direction. + yshift : str + ``[a|c|f|r][yshift]``. + Shift plot origin in y-direction. Full documentation is at + :gmt-docs:`gmt.html#xy-full`. + """, "j": """\ distcalc : str ``e|f|g``. @@ -77,11 +86,35 @@ - 'c' for bicubic [Default] - 'l' for bilinear - 'n' for nearest-neighbor""", + "p": """\ + perspective : list or str + ``'[x|y|z]azim[/elev[/zlevel]][+wlon0/lat0[/z0]][+vx0/y0]'``. + Select perspective view and set the azimuth and elevation angle of + the viewpoint. Default is [180, 90]. Full documentation is at + :gmt-docs:`gmt.html#perspective-full`. + """, "registration": """\ registration : str ``[g|p]`` Force output grid to be gridline (g) or pixel (p) node registered. Default is gridline (g).""", + "t": """\ + transparency : float + Set transparency level, in [0-100] percent range. + Default is 0, i.e., opaque. + Only visible when PDF or raster format output is selected. + Only the PNG format selection adds a transparency layer + in the image (for further processing). """, + "x": """\ + cores : int + ``[[-]n]``. + Limit the number of cores to be used in any OpenMP-enabled + multi-threaded algorithms. By default we try to use all available + cores. Set a number *n* to only use n cores (if too large it will + be truncated to the maximum cores available). Finally, give a + negative number *-n* to select (all - n) cores (or at least 1 if + n equals or exceeds all). + """, } diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 5b4bb731baa..5004e1b24cf 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -172,6 +172,11 @@ def is_nonstr_iter(value): True >>> is_nonstr_iter((1, 2, 3)) True + >>> import numpy as np + >>> is_nonstr_iter(np.array([1.0, 2.0, 3.0])) + True + >>> is_nonstr_iter(np.array(["abc", "def", "ghi"])) + True """ return isinstance(value, Iterable) and not isinstance(value, str) diff --git a/pygmt/modules.py b/pygmt/modules.py index 1cacf67d6df..477bc99221b 100644 --- a/pygmt/modules.py +++ b/pygmt/modules.py @@ -78,9 +78,10 @@ def info(table, **kwargs): Parameters ---------- - table : pandas.DataFrame or np.ndarray or str - Either a pandas dataframe, a 1D/2D numpy.ndarray or a file name to an - ASCII data table. + table : str or np.ndarray or pandas.DataFrame or xarray.Dataset + Pass in either a file name to an ASCII data table, a 1D/2D numpy array, + a pandas dataframe, or an xarray dataset made up of 1D xarray.DataArray + data variables. per_column : bool Report the min/max values per column in separate columns. spacing : str @@ -107,10 +108,13 @@ def info(table, **kwargs): if kind == "file": file_context = dummy_context(table) elif kind == "matrix": - _table = np.asanyarray(table) - if table.ndim == 1: # 1D arrays need to be 2D and transposed - _table = np.transpose(np.atleast_2d(_table)) - file_context = lib.virtualfile_from_matrix(_table) + try: + # pandas.DataFrame and xarray.Dataset types + arrays = [array for _, array in table.items()] + except AttributeError: + # Python lists, tuples, and numpy ndarray types + arrays = np.atleast_2d(np.asanyarray(table).T) + file_context = lib.virtualfile_from_vectors(*arrays) else: raise GMTInvalidInput(f"Unrecognized data type: {type(table)}") diff --git a/pygmt/tests/test_grdcontour.py b/pygmt/tests/test_grdcontour.py index 2b45f2622d6..1ff3b3a2cb3 100644 --- a/pygmt/tests/test_grdcontour.py +++ b/pygmt/tests/test_grdcontour.py @@ -9,6 +9,7 @@ from .. import Figure from ..exceptions import GMTInvalidInput from ..datasets import load_earth_relief +from ..helpers.testing import check_figures_equal TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data") @@ -21,49 +22,49 @@ def fixture_grid(): return load_earth_relief(registration="gridline") -@pytest.mark.xfail( - reason="Baseline image not updated to use earth relief grid in GMT 6.1.0", -) -@pytest.mark.mpl_image_compare +@check_figures_equal() def test_grdcontour(grid): """Plot a contour image using an xarray grid with fixed contour interval """ - fig = Figure() - fig.grdcontour(grid, interval="1000", projection="W0/6i") - return fig + fig_ref, fig_test = Figure(), Figure() + kwargs = dict(interval="1000", projection="W0/6i") + fig_ref.grdcontour("@earth_relief_01d_g", **kwargs) + fig_test.grdcontour(grid, **kwargs) + return fig_ref, fig_test -@pytest.mark.xfail( - reason="Baseline image not updated to use earth relief grid in GMT 6.1.0", -) -@pytest.mark.mpl_image_compare +@check_figures_equal() def test_grdcontour_labels(grid): """Plot a contour image using a xarray grid with contour labels and alternate colors """ - fig = Figure() - fig.grdcontour( - grid, + fig_ref, fig_test = Figure(), Figure() + kwargs = dict( interval="1000", annotation="5000", projection="W0/6i", pen=["a1p,red", "c0.5p,black"], label_placement="d3i", ) - return fig + fig_ref.grdcontour("@earth_relief_01d_g", **kwargs) + fig_test.grdcontour(grid, **kwargs) + return fig_ref, fig_test -@pytest.mark.xfail( - reason="Baseline image not updated to use earth relief grid in GMT 6.1.0", -) -@pytest.mark.mpl_image_compare +@check_figures_equal() def test_grdcontour_slice(grid): "Plot an contour image using an xarray grid that has been sliced" + + fig_ref, fig_test = Figure(), Figure() + grid_ = grid.sel(lat=slice(-30, 30)) - fig = Figure() - fig.grdcontour(grid_, interval="1000", projection="M6i") - return fig + kwargs = dict(interval="1000", projection="M6i") + fig_ref.grdcontour( + grid="@earth_relief_01d_g", region=[-180, 180, -30, 30], **kwargs + ) + fig_test.grdcontour(grid=grid_, **kwargs) + return fig_ref, fig_test @pytest.mark.mpl_image_compare diff --git a/pygmt/tests/test_grdimage.py b/pygmt/tests/test_grdimage.py index d86798178f3..8b9fac9acd8 100644 --- a/pygmt/tests/test_grdimage.py +++ b/pygmt/tests/test_grdimage.py @@ -71,7 +71,7 @@ def test_grdimage_file(): @pytest.mark.xfail(reason="Upstream bug in GMT 6.1.1") @check_figures_equal() -def test_grdimage_xarray_shading(grid, fig_ref, fig_test): +def test_grdimage_xarray_shading(grid): """ Test that shading works well for xarray. See https://github.com/GenericMappingTools/pygmt/issues/364 diff --git a/pygmt/tests/test_info.py b/pygmt/tests/test_info.py index 142b56dce8e..92e7616adc6 100644 --- a/pygmt/tests/test_info.py +++ b/pygmt/tests/test_info.py @@ -8,13 +8,17 @@ import pandas as pd import pytest import xarray as xr +from packaging.version import Version -from .. import info +from .. import clib, info from ..exceptions import GMTInvalidInput TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), "data") POINTS_DATA = os.path.join(TEST_DATA_DIR, "points.txt") +with clib.Session() as _lib: + gmt_version = Version(_lib.info["version"]) + def test_info(): "Make sure info works on file name inputs" @@ -33,7 +37,48 @@ def test_info_dataframe(): table = pd.read_csv(POINTS_DATA, sep=" ", header=None) output = info(table=table) expected_output = ( - ": N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" + ": N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" + ) + assert output == expected_output + + +@pytest.mark.xfail( + condition=gmt_version <= Version("6.1.1"), + reason="UNIX timestamps returned instead of ISO datetime, should work on GMT 6.2.0 " + "after https://github.com/GenericMappingTools/gmt/issues/4241 is resolved", +) +def test_info_pandas_dataframe_time_column(): + "Make sure info works on pandas.DataFrame inputs with a time column" + table = pd.DataFrame( + data={ + "z": [10, 13, 12, 15, 14], + "time": pd.date_range(start="2020-01-01", periods=5), + } + ) + output = info(table=table) + expected_output = ( + ": N = 5 <10/15> <2020-01-01T00:00:00/2020-01-05T00:00:00>\n" + ) + assert output == expected_output + + +@pytest.mark.xfail( + condition=gmt_version <= Version("6.1.1"), + reason="UNIX timestamp returned instead of ISO datetime, should work on GMT 6.2.0 " + "after https://github.com/GenericMappingTools/gmt/issues/4241 is resolved", +) +def test_info_xarray_dataset_time_column(): + "Make sure info works on xarray.Dataset 1D inputs with a time column" + table = xr.Dataset( + coords={"index": [0, 1, 2, 3, 4]}, + data_vars={ + "z": ("index", [10, 13, 12, 15, 14]), + "time": ("index", pd.date_range(start="2020-01-01", periods=5)), + }, + ) + output = info(table=table) + expected_output = ( + ": N = 5 <10/15> <2020-01-01T00:00:00/2020-01-05T00:00:00>\n" ) assert output == expected_output @@ -43,7 +88,7 @@ def test_info_2d_array(): table = np.loadtxt(POINTS_DATA) output = info(table=table) expected_output = ( - ": N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" + ": N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" ) assert output == expected_output @@ -51,7 +96,7 @@ def test_info_2d_array(): def test_info_1d_array(): "Make sure info works on 1D numpy.ndarray inputs" output = info(table=np.arange(20)) - expected_output = ": N = 20 <0/19>\n" + expected_output = ": N = 20 <0/19>\n" assert output == expected_output diff --git a/pygmt/x2sys.py b/pygmt/x2sys.py index 22294c3e791..e3803ba7806 100644 --- a/pygmt/x2sys.py +++ b/pygmt/x2sys.py @@ -206,8 +206,8 @@ def x2sys_cross(tracks=None, outfile=None, **kwargs): in the current directory and second in all directories listed in $X2SYS_HOME/TAG/TAG_paths.txt (if it exists). [If $X2SYS_HOME is not set it will default to $GMT_SHAREDIR/x2sys]. (Note: MGD77 files will - also be looked for via $MGD77_HOME/mgd77_paths.txt and *.gmt files will - be searched for via $GMT_SHAREDIR/mgg/gmtfile_paths). + also be looked for via $MGD77_HOME/mgd77_paths.txt and \\*.gmt files + will be searched for via $GMT_SHAREDIR/mgg/gmtfile_paths). outfile : str Optional. The file name for the output ASCII txt file to store the @@ -271,7 +271,7 @@ def x2sys_cross(tracks=None, outfile=None, **kwargs): headings will not be computed (i.e., set to NaN) [Default calculates \ headings regardless of speed]. - For example, you can use ``speed=["l0", "u10", "h5"] to set a lower + For example, you can use ``speed=["l0", "u10", "h5"]`` to set a lower speed of 0, upper speed of 10, and disable heading calculations for speeds below 5.