diff --git a/.github/ISSUE_TEMPLATE/4-release_checklist.md b/.github/ISSUE_TEMPLATE/4-release_checklist.md index 59919b8fb25..87a164b15c8 100644 --- a/.github/ISSUE_TEMPLATE/4-release_checklist.md +++ b/.github/ISSUE_TEMPLATE/4-release_checklist.md @@ -1,6 +1,6 @@ --- name: PyGMT release checklist -about: Checklist for a new PyGMT release. +about: Checklist for a new PyGMT release. [For project maintainers only!] title: Release PyGMT vX.Y.Z labels: maintenance assignees: '' @@ -19,17 +19,15 @@ assignees: '' **Before release**: -- [ ] Check [SPEC 0](https://scientific-python.org/specs/spec-0000/) to see if we need to bump the minimum supported versions of GMT, Python and - core package dependencies (NumPy, pandas, Xarray) +- [ ] Check [SPEC 0](https://scientific-python.org/specs/spec-0000/) to see if we need to bump the minimum supported versions of GMT, Python and core package dependencies (NumPy, pandas, Xarray) - [ ] Review the ["PyGMT Team" page](https://www.pygmt.org/dev/team.html) +- [ ] README looks good on TestPyPI. Visit [TestPyPI](https://test.pypi.org/project/pygmt/#history), click the latest pre-release, and check the homepage. - [ ] Check to ensure that: - - [ ] Deprecations and related tests are removed for this version by running `grep --include="*.py" -r vX.Y.Z` from the base of the repository + - [ ] Deprecated workarounds/codes/tests are removed. Run `grep "# TODO" **/*.py` to find all potential TODOs. - [ ] All tests pass in the ["GMT Legacy Tests" workflow](https://github.com/GenericMappingTools/pygmt/actions/workflows/ci_tests_legacy.yaml) - [ ] All tests pass in the ["GMT Dev Tests" workflow](https://github.com/GenericMappingTools/pygmt/actions/workflows/ci_tests_dev.yaml) - [ ] All tests pass in the ["Doctests" workflow](https://github.com/GenericMappingTools/pygmt/actions/workflows/ci_doctests.yaml) -- [ ] Update warnings in `pygmt/_show_versions.py` as well as notes in - [Not working transparency](https://www.pygmt.org/dev/install.html#not-working-transparency) - regarding GMT-Ghostscript incompatibility +- [ ] Update warnings in `pygmt/_show_versions.py` as well as notes in [Not working transparency](https://www.pygmt.org/dev/install.html#not-working-transparency) regarding GMT-Ghostscript incompatibility - [ ] Reserve a DOI on [Zenodo](https://zenodo.org) by clicking on "New Version" - [ ] Finish up the "Changelog entry for v0.x.x" Pull Request (Use the previous changelog PR as a reference) - [ ] Run `make codespell` to check common misspellings. If there are any, either fix them or add them to `ignore-words-list` in `pyproject.toml` @@ -41,18 +39,16 @@ assignees: '' - [ ] Edit the draft release notes with the finalized changelog - [ ] Set the tag version and release title to vX.Y.Z - [ ] Make a release by clicking the 'Publish Release' button, this will automatically create a tag too -- [ ] Download pygmt-X.Y.Z.zip (rename to pygmt-vX.Y.Z.zip) and baseline-images.zip from - the release page, and upload the two zip files to https://zenodo.org/deposit, - ensure that they are filed under the correct reserved DOI +- [ ] Download pygmt-X.Y.Z.zip (rename to pygmt-vX.Y.Z.zip) and baseline-images.zip from the release page, and upload the two zip files to https://zenodo.org/deposit, ensure that they are filed under the correct reserved DOI **After release**: -- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) - [Done automatically by conda-forge's bot. Remember to pin Python and SPEC0 versions] +- [ ] Update conda-forge [pygmt-feedstock](https://github.com/conda-forge/pygmt-feedstock) [Done automatically by conda-forge's bot. Remember to pin GMT, Python and SPEC0 versions] - [ ] Bump PyGMT version on https://github.com/GenericMappingTools/try-gmt (after conda-forge update) - [ ] Announce the release on: - [ ] GMT [forum](https://forum.generic-mapping-tools.org/c/news/) (do this announcement first! Requires moderator status) - - [ ] [ResearchGate](https://www.researchgate.net) (after forum announcement, add new version as research item via the **code** category, be sure to include the corresponding new Zenodo DOI) + - [ ] [ResearchGate](https://www.researchgate.net) (after forum announcement; download the ZIP file of the new release from the release page and add it as research item via the **code** category, be sure to include the corresponding new Zenodo DOI) +- [ ] Update release checklist template with any additional bullet points that may have arisen during the release --- diff --git a/.github/ISSUE_TEMPLATE/5-bump_gmt_checklist.md b/.github/ISSUE_TEMPLATE/5-bump_gmt_checklist.md index 9652f2150ee..1ca5117ff5f 100644 --- a/.github/ISSUE_TEMPLATE/5-bump_gmt_checklist.md +++ b/.github/ISSUE_TEMPLATE/5-bump_gmt_checklist.md @@ -1,6 +1,6 @@ --- name: Bump GMT version checklist -about: Checklist for bumping the minimum required GMT version. +about: Checklist for bumping the minimum required GMT version. [For project maintainers only!] title: Bump to GMT X.Y.Z labels: maintenance assignees: '' diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 14b75d86f5a..0c259cf5672 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,6 @@ Fixes # - [ ] Write detailed docstrings for all functions/methods. - [ ] If wrapping a new module, open a 'Wrap new GMT module' issue and submit reasonably-sized PRs. - [ ] If adding new functionality, add an example to docstrings or tutorials. -- [ ] Use underscores (not hyphens) in names of Python files and directories. **Slash Commands** diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 2fcc724ddae..f219ae76bdc 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -45,7 +45,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | diff --git a/.github/workflows/cache_data.yaml b/.github/workflows/cache_data.yaml index 7e12513b0e9..46c37211424 100644 --- a/.github/workflows/cache_data.yaml +++ b/.github/workflows/cache_data.yaml @@ -43,7 +43,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | @@ -76,7 +76,7 @@ jobs: # Upload the downloaded files as artifacts to GitHub - name: Upload artifacts to GitHub - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.0 with: name: gmt-cache include-hidden-files: true diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml index b1f45b73df8..0ed425da8d6 100644 --- a/.github/workflows/check-links.yml +++ b/.github/workflows/check-links.yml @@ -35,7 +35,7 @@ jobs: - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@v2.1.0 + uses: lycheeverse/lychee-action@v2.2.0 with: fail: false # Don't fail action on broken links output: /tmp/lychee-out.md diff --git a/.github/workflows/ci_docs.yml b/.github/workflows/ci_docs.yml index 749d5b42b00..b02d953c0f7 100644 --- a/.github/workflows/ci_docs.yml +++ b/.github/workflows/ci_docs.yml @@ -80,7 +80,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | @@ -108,6 +108,7 @@ jobs: make pip python-build + geodatasets myst-nb panel sphinx>=6.2 @@ -162,9 +163,14 @@ jobs: # to get the right commit hash. message="Deploy $version from $(git rev-parse --short HEAD)" cd deploy - # Need to have this file so that GitHub doesn't try to run Jekyll + # Create some files in the root directory. + # .nojekyll: Need to have this file so that GitHub doesn't try to run Jekyll touch .nojekyll - # Delete all the files and replace with our new set + # CNAME: Set the custom domain name + echo "www.pygmt.org" > CNAME + # index.html: Redirect to the latest version + echo '' > index.html + # Delete all the files and replace with our new set echo -e "\nRemoving old files from previous builds of ${version}:" rm -rvf ${version} echo -e "\nCopying HTML files to ${version}:" diff --git a/.github/workflows/ci_doctests.yaml b/.github/workflows/ci_doctests.yaml index 1d0cdac4f98..af28e6ac710 100644 --- a/.github/workflows/ci_doctests.yaml +++ b/.github/workflows/ci_doctests.yaml @@ -42,7 +42,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 8ed7e728c02..44416d210e0 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -113,7 +113,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | @@ -154,20 +154,18 @@ jobs: GH_TOKEN: ${{ github.token }} - name: Install uv - uses: astral-sh/setup-uv@v4.2.0 + uses: astral-sh/setup-uv@v5.1.0 + with: + python-version: ${{ matrix.python-version }} - name: Install dvc run: | - uv venv - source .venv/bin/activate uv pip install dvc uv pip list # Pull baseline image data from dvc remote (DAGsHub) - name: Pull baseline image data from dvc remote - run: | - source .venv/bin/activate - uv run dvc pull --no-run-cache --verbose && ls -lhR pygmt/tests/baseline/ + run: uv run dvc pull --no-run-cache --verbose && ls -lhR pygmt/tests/baseline/ # Install the package that we want to test - name: Install the package @@ -179,7 +177,7 @@ jobs: # Upload diff images on test failure - name: Upload diff images if any test fails - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.0 if: failure() with: name: artifact-${{ runner.os }}-${{ matrix.python-version }} @@ -187,7 +185,7 @@ jobs: # Upload coverage to Codecov - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.1.2 if: success() || failure() with: use_oidc: true diff --git a/.github/workflows/ci_tests_dev.yaml b/.github/workflows/ci_tests_dev.yaml index d8259827f1d..6932a0d520b 100644 --- a/.github/workflows/ci_tests_dev.yaml +++ b/.github/workflows/ci_tests_dev.yaml @@ -57,7 +57,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | @@ -187,7 +187,7 @@ jobs: # Upload diff images on test failure - name: Upload diff images if any test fails - uses: actions/upload-artifact@v4.4.3 + uses: actions/upload-artifact@v4.6.0 if: ${{ failure() }} with: name: artifact-GMT-${{ matrix.gmt_git_ref }}-${{ runner.os }} diff --git a/.github/workflows/ci_tests_legacy.yaml b/.github/workflows/ci_tests_legacy.yaml index 1af5e00cd38..0c4ae574235 100644 --- a/.github/workflows/ci_tests_legacy.yaml +++ b/.github/workflows/ci_tests_legacy.yaml @@ -51,7 +51,7 @@ jobs: # Install Micromamba with conda-forge dependencies - name: Setup Micromamba - uses: mamba-org/setup-micromamba@v2.0.3 + uses: mamba-org/setup-micromamba@v2.0.4 with: environment-name: pygmt condarc: | diff --git a/.github/workflows/format-command.yml b/.github/workflows/format-command.yml index ceb2604ba2b..3ca7d72d71f 100644 --- a/.github/workflows/format-command.yml +++ b/.github/workflows/format-command.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: # Generate token from GenericMappingTools bot - - uses: actions/create-github-app-token@v1.11.0 + - uses: actions/create-github-app-token@v1.11.1 id: generate-token with: app-id: ${{ secrets.APP_ID }} diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 2dbc12cbef1..917353ee85a 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -35,13 +35,9 @@ on: # - main jobs: - publish-pypi: - name: Publish to PyPI + build: + name: Build distribution 📦 runs-on: ubuntu-latest - permissions: - # This permission is mandatory for OIDC publishing - id-token: write - if: github.repository == 'GenericMappingTools/pygmt' steps: - name: Checkout @@ -49,6 +45,7 @@ jobs: with: # fetch all history so that setuptools-scm works fetch-depth: 0 + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5.3.0 @@ -74,11 +71,54 @@ jobs: echo "Generated files:" ls -lh dist/ - - name: Publish to Test PyPI + - name: Store the distribution packages + uses: actions/upload-artifact@v4.6.0 + with: + name: python-package-distributions + path: dist/ + + publish-to-testpypi: + name: Publish Python 🐍 distribution 📦 to TestPyPI + if: github.repository == 'GenericMappingTools/pygmt' + needs: + - build + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/project/pygmt + permissions: + id-token: write # IMPORTANT: mandatory for trusted OIDC publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4.1.8 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution 📦 to TestPyPI uses: pypa/gh-action-pypi-publish@v1.12.3 with: repository-url: https://test.pypi.org/legacy/ - - name: Publish to PyPI - if: startsWith(github.ref, 'refs/tags') + publish-pypi: + name: Publish Python 🐍 distribution 📦 to PyPI + if: github.repository == 'GenericMappingTools/pygmt' && startsWith(github.ref, 'refs/tags/') + needs: + - build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/pygmt/ + permissions: + id-token: write # IMPORTANT: mandatory for trusted OIDC publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@v4.1.8 + with: + name: python-package-distributions + path: dist/ + + - name: Publish distribution 📦 to PyPI uses: pypa/gh-action-pypi-publish@v1.12.3 diff --git a/.github/workflows/style_checks.yaml b/.github/workflows/style_checks.yaml index f15bcde0012..43fa5acd6c7 100644 --- a/.github/workflows/style_checks.yaml +++ b/.github/workflows/style_checks.yaml @@ -52,3 +52,15 @@ jobs: rm output.txt exit $nfiles fi + + - name: Ensure hyphens are not used in names of directories and Python files + run: | + git ls-files '*.py' | grep '-' > output.txt || true + git ls-tree -rd --name-only HEAD | grep '-' >> output.txt || true + nfiles=$(wc --lines output.txt | awk '{print $1}') + if [[ $nfiles > 0 ]]; then + echo "Following directories/files use hyphens in file names:" + cat output.txt + rm output.txt + exit $nfiles + fi diff --git a/CITATION.cff b/CITATION.cff index e6d79e2530d..a18999e8f2d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -40,14 +40,14 @@ authors: family-names: Yao affiliation: Nanyang Technological University, Singapore orcid: https://orcid.org/0000-0001-7036-4238 +- given-names: Jing-Hui + family-names: Tong + affiliation: National Taiwan Normal University, Taiwan + orcid: https://orcid.org/0009-0002-7195-3071 - given-names: Yohai family-names: Magen affiliation: Tel Aviv University, Israel orcid: https://orcid.org/0000-0002-4892-4013 -- given-names: Tong - family-names: Jing-Hui - affiliation: National Taiwan Normal University, Taiwan - orcid: https://orcid.org/0009-0002-7195-3071 - given-names: Kathryn family-names: Materna affiliation: US Geological Survey, USA @@ -76,9 +76,9 @@ authors: family-names: Wessel affiliation: University of Hawaiʻi at Mānoa, USA orcid: https://orcid.org/0000-0001-5708-7336 -date-released: 2024-09-05 -doi: 10.5281/zenodo.13679420 +date-released: 2024-12-31 +doi: 10.5281/zenodo.14535921 license: BSD-3-Clause repository-code: https://github.com/GenericMappingTools/pygmt type: software -version: 0.13.0 +version: 0.14.0 diff --git a/LICENSE.txt b/LICENSE.txt index 6411f912cea..c6c569c4bc6 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2017-2024 The PyGMT Developers +Copyright (c) 2017-2025 The PyGMT Developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/README.md b/README.md index 44f4065aeb7..ba4e47d8fdb 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,22 @@ ## Why PyGMT? -A beautiful map is worth a thousand words. To truly understand how powerful PyGMT is, play with it online on -[Binder](https://github.com/GenericMappingTools/try-gmt)! For a quicker introduction, check out our -[3 minute overview](https://youtu.be/4iPnITXrxVU)! +A beautiful map is worth a thousand words. To truly understand how powerful PyGMT is, +play with it online on [Binder](https://github.com/GenericMappingTools/try-gmt)! For a +quicker introduction, check out our [3 minute overview](https://youtu.be/4iPnITXrxVU)! -Afterwards, feel free to look at our [Tutorials](https://www.pygmt.org/latest/tutorials), visit the -[Gallery](https://www.pygmt.org/latest/gallery), and check out some +Afterwards, feel free to look at our [Tutorials](https://www.pygmt.org/latest/tutorials), +visit the [Gallery](https://www.pygmt.org/latest/gallery), and check out some [external PyGMT examples](https://www.pygmt.org/latest/external_resources.html)! -![Quick Introduction to PyGMT YouTube Video](doc/_static/scipy2022-youtube-thumbnail.jpg) +![Quick Introduction to PyGMT YouTube Video](https://raw.githubusercontent.com/GenericMappingTools/pygmt/refs/heads/main/doc/_static/scipy2022-youtube-thumbnail.jpg) ## About -PyGMT is a library for processing geospatial and geophysical data and making publication-quality -maps and figures. It provides a Pythonic interface for the -[Generic Mapping Tools (GMT)](https://github.com/GenericMappingTools/gmt), a command-line program -widely used across the Earth, Ocean, and Planetary sciences and beyond. +PyGMT is a library for processing geospatial and geophysical data and making +publication-quality maps and figures. It provides a Pythonic interface for the +[Generic Mapping Tools (GMT)](https://github.com/GenericMappingTools/gmt), a command-line +program widely used across the Earth, Ocean, and Planetary sciences and beyond. ## Project goals @@ -45,8 +45,9 @@ widely used across the Earth, Ocean, and Planetary sciences and beyond. - 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 [scientific Python ecosystem](https://scientific-python.org/): `numpy.ndarray` or - `pandas.DataFrame` for data tables, `xarray.DataArray` for grids, and `geopandas.GeoDataFrame` for geographical data. +- Integration with the [scientific Python ecosystem](https://scientific-python.org/): + `numpy.ndarray` or `pandas.DataFrame` for data tables, `xarray.DataArray` for grids, + and `geopandas.GeoDataFrame` for geographical data. ## Quickstart @@ -69,7 +70,8 @@ For other ways to install `pygmt`, see the [full installation instructions](http ### Getting started As a starting point, you can open a [Python interpreter](https://docs.python.org/3/tutorial/interpreter.html) -or a [Jupyter notebook](https://docs.jupyter.org/en/latest/running.html), and try the following example: +or a [Jupyter notebook](https://docs.jupyter.org/en/latest/running.html), and try the +following example: ``` python import pygmt @@ -79,18 +81,18 @@ fig.text(position="MC", text="PyGMT", font="80p,Helvetica-Bold,red@75") fig.show() ``` -You should see a global map with land and water masses colored in tan and lightblue, respectively. On top, -there should be the semi-transparent text "PyGMT". For more examples, please have a look at the -[Gallery](https://www.pygmt.org/latest/gallery/index.html) and +You should see a global map with land and water masses colored in tan and lightblue, +respectively. On top, there should be the semi-transparent text "PyGMT". For more examples, +please have a look at the [Gallery](https://www.pygmt.org/latest/gallery/index.html) and [Tutorials](https://www.pygmt.org/latest/tutorials/index.html). ## Contacting us - Most discussion happens [on GitHub](https://github.com/GenericMappingTools/pygmt). - Feel free to [open an issue](https://github.com/GenericMappingTools/pygmt/issues/new) or comment on any open - issue or pull request. -- We have a [Discourse forum](https://forum.generic-mapping-tools.org/c/questions/pygmt-q-a) where you can ask - questions and leave comments. + Feel free to [open an issue](https://github.com/GenericMappingTools/pygmt/issues/new) + or comment on any open issue or pull request. +- We have a [Discourse forum](https://forum.generic-mapping-tools.org/c/questions/pygmt-q-a) + where you can ask questions and leave comments. ## Contributing @@ -109,30 +111,33 @@ to see how you can help and give feedback. **We want your help.** No, really. -There may be a little voice inside your head that is telling you that you're not ready to be an open source -contributor; that your skills aren't nearly good enough to contribute. What could you possibly offer? +There may be a little voice inside your head that is telling you that you're not ready +to be an open source contributor; that your skills aren't nearly good enough to +contribute. What could you possibly offer? We assure you that the little voice in your head is wrong. -**Being a contributor doesn't just mean writing code.** Equally important contributions include: writing or -proof-reading documentation, suggesting or implementing tests, or even giving feedback about the project -(including giving feedback about the contribution process). If you're coming to the project with fresh eyes, -you might see the errors and assumptions that seasoned contributors have glossed over. If you can write any -code at all, you can contribute code to open source. We are constantly trying out new skills, making mistakes, -and learning from those mistakes. That's how we all improve and we are happy to help others learn. +**Being a contributor doesn't just mean writing code.** Equally important contributions +include: writing or proof-reading documentation, suggesting or implementing tests, or +even giving feedback about the project (including giving feedback about the contribution +process). If you're coming to the project with fresh eyes, you might see the errors and +assumptions that seasoned contributors have glossed over. If you can write any code at +all, you can contribute code to open source. We are constantly trying out new skills, +making mistakes, and learning from those mistakes. That's how we all improve and we are +happy to help others learn. *This disclaimer was adapted from the* [MetPy project](https://github.com/Unidata/MetPy). ## Citing PyGMT PyGMT is a community developed project. See the -[AUTHORS.md](https://github.com/GenericMappingTools/pygmt/blob/main/AUTHORS.md) file on GitHub for a list of -the people involved and a definition of the term "PyGMT Developers". Feel free to cite our work in your -research using the following BibTeX: +[AUTHORS.md](https://github.com/GenericMappingTools/pygmt/blob/main/AUTHORS.md) file +on GitHub for a list of the people involved and a definition of the term "PyGMT Developers". +Feel free to cite our work in your research using the following BibTeX: ``` @software{ - pygmt_2024_13679420, + pygmt_2024_14535921, author = {Tian, Dongdong and Uieda, Leonardo and Leong, Wei Ji and @@ -142,8 +147,8 @@ research using the following BibTeX: Jones, Max and Toney, Liam and Yao, Jiayuan and - Magen, Yohai and Tong, Jing-Hui and + Magen, Yohai and Materna, Kathryn and Belem, Andre and Newton, Tyler and @@ -152,20 +157,20 @@ research using the following BibTeX: Quinn, Jamie and Wessel, Paul}, title = {{PyGMT: A Python interface for the Generic Mapping Tools}}, - month = sep, + month = dec, year = 2024, publisher = {Zenodo}, - version = {0.13.0}, - doi = {10.5281/zenodo.13679420}, - url = {https://doi.org/10.5281/zenodo.13679420} + version = {0.14.0}, + doi = {10.5281/zenodo.14535921}, + url = {https://doi.org/10.5281/zenodo.14535921} } ``` To cite a specific version of PyGMT, go to our Zenodo page at -and use the "Export to BibTeX" function there. It is also strongly recommended to cite the -[GMT 6 paper](https://doi.org/10.1029/2019GC008515) (which PyGMT wraps around). Note that some modules -like `dimfilter`, `surface`, and `x2sys` also have their dedicated citations. Further information for -all these can be found at . +and use the "Export to BibTeX" function there. It is also strongly recommended to cite +the [GMT 6 paper](https://doi.org/10.1029/2019GC008515) (which PyGMT wraps around). Note +that some modules like `dimfilter`, `surface`, and `x2sys` also have their dedicated +citations. Further information for all these can be found at . ## License diff --git a/ci/requirements/docs.yml b/ci/requirements/docs.yml index 84be526f47f..2a6fa860961 100644 --- a/ci/requirements/docs.yml +++ b/ci/requirements/docs.yml @@ -23,6 +23,7 @@ dependencies: - pip - python-build # Dev dependencies (building documentation) + - geodatasets - myst-nb - panel - sphinx>=6.2 diff --git a/doc/Makefile b/doc/Makefile index b82215c44c4..04b1c1ab549 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,14 +1,12 @@ # Makefile for Sphinx documentation # You can set these variables from the command line. -SPHINXOPTS = -j auto -SPHINXBUILD = sphinx-build +SPHINXOPTS ?= -j auto +SPHINXBUILD ?= sphinx-build SPHINXAUTOGEN = sphinx-autogen +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . - .PHONY: help all api html server clean help: @@ -28,20 +26,20 @@ api: @echo $(SPHINXAUTOGEN) -i -t _templates -o api/generated api/*.rst -html: api +html latex: api @echo - @echo "Building HTML files." + @echo "Building "$@" files." @echo # Set PYGMT_USE_EXTERNAL_DISPLAY to "false" to disable external display - PYGMT_USE_EXTERNAL_DISPLAY="false" $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + PYGMT_USE_EXTERNAL_DISPLAY="false" $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The files are in $(BUILDDIR)/$@." html-noplot: api @echo @echo "Building HTML files without example plots." @echo - $(SPHINXBUILD) -D plot_gallery=0 -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) -D plot_gallery=0 @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." diff --git a/doc/_static/version_switch.js b/doc/_static/version_switch.js index acedd8c1d4c..5450cb9d6af 100644 --- a/doc/_static/version_switch.js +++ b/doc/_static/version_switch.js @@ -12,6 +12,7 @@ var all_versions = { 'latest': 'latest', 'dev': 'dev', + 'v0.14.0': 'v0.14.0', 'v0.13.0': 'v0.13.0', 'v0.12.0': 'v0.12.0', 'v0.11.0': 'v0.11.0', diff --git a/doc/api/index.rst b/doc/api/index.rst index 07f76aff217..25de6d44adf 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -29,12 +29,14 @@ Plotting map elements Figure.basemap Figure.coast Figure.colorbar + Figure.hlines Figure.inset Figure.legend Figure.logo Figure.solar Figure.text Figure.timestamp + Figure.vlines Plotting tabular data ~~~~~~~~~~~~~~~~~~~~~ @@ -233,10 +235,14 @@ and store them in GMT's user data directory. datasets.load_black_marble datasets.load_blue_marble datasets.load_earth_age + datasets.load_earth_deflection + datasets.load_earth_dist datasets.load_earth_free_air_anomaly datasets.load_earth_geoid datasets.load_earth_magnetic_anomaly datasets.load_earth_mask + datasets.load_earth_mean_dynamic_topography + datasets.load_earth_mean_sea_surface datasets.load_earth_relief datasets.load_earth_vertical_gravity_gradient datasets.load_mars_relief @@ -329,7 +335,6 @@ Low level access (these are mostly used by the :mod:`pygmt.clib` package): clib.Session.read_virtualfile clib.Session.extract_region clib.Session.get_libgmt_func - clib.Session.virtualfile_from_data clib.Session.virtualfile_from_grid clib.Session.virtualfile_from_stringio clib.Session.virtualfile_from_matrix diff --git a/doc/changes.md b/doc/changes.md index 2efe67a7f1d..8e7142d2fd8 100644 --- a/doc/changes.md +++ b/doc/changes.md @@ -1,5 +1,109 @@ # Changelog +## Release v0.14.0 (2024/12/31) + +[![Digital Object Identifier for PyGMT v0.14.0](https://zenodo.org/badge/DOI/10.5281/zenodo.14535921.svg)](https://doi.org/10.5281/zenodo.14535921) + +### Highlights + +* 🎉 **Fourteenth minor release of PyGMT** 🎉 +* Bump minimum supported version to GMT>=6.4.0 ([#3450](https://github.com/GenericMappingTools/pygmt/pull/3450)) +* Two new plotting methods and six new functions to access more GMT remote datasets +* PyArrow as an optional dependency and improved support of PyArrow data types ([#3592](https://github.com/GenericMappingTools/pygmt/pull/3592)) + +### New Features + +* Add Figure.hlines for plotting horizontal lines ([#923](https://github.com/GenericMappingTools/pygmt/pull/923)) +* Add Figure.vlines for plotting vertical lines ([#3726](https://github.com/GenericMappingTools/pygmt/pull/3726)) +* Add load_black_marble to load "Black Marble" dataset ([#3469](https://github.com/GenericMappingTools/pygmt/pull/3469)) +* Add load_blue_marble to load "Blue Marble" dataset ([#2235](https://github.com/GenericMappingTools/pygmt/pull/2235)) +* Add load_earth_deflection to load "IGPP Earth east-west and north-south deflection" datasets ([#3728](https://github.com/GenericMappingTools/pygmt/pull/3728)) +* Add load_earth_dist to load "GSHHG Earth distance to shoreline" dataset ([#3706](https://github.com/GenericMappingTools/pygmt/pull/3706)) +* Add load_earth_mean_dynamic_topography to load "CNES Earth Mean Dynamic Topography" dataset ([#3718](https://github.com/GenericMappingTools/pygmt/pull/3718)) +* Add load_earth_mean_sea_surface to load "CNES Earth Mean Sea Surface" dataset ([#3717](https://github.com/GenericMappingTools/pygmt/pull/3717)) +* load_earth_free_air_anomaly: Add "uncertainty" parameter to load the "IGPP Earth free-air anomaly uncertainty" dataset ([#3727](https://github.com/GenericMappingTools/pygmt/pull/3727)) + + +### Enhancements + +* Figure.plot: Add the "symbol" parameter to support plotting data points with varying symbols ([#1117](https://github.com/GenericMappingTools/pygmt/pull/1117)) +* Figure.plot3d: Add the "symbol" parameter to support plotting data points with varying symbols ([#3559](https://github.com/GenericMappingTools/pygmt/pull/3559)) +* Figure.legend: Support passing a StringIO object as the legend specification ([#3438](https://github.com/GenericMappingTools/pygmt/pull/3438)) +* load_tile_map: Add parameter "crs" to set the CRS of the returned dataarray ([#3554](https://github.com/GenericMappingTools/pygmt/pull/3554)) +* PyArrow: Support pyarrow arrays with string/large_string/string_view types ([#3619](https://github.com/GenericMappingTools/pygmt/pull/3619)) +* Support 1-D/2-D numpy arrays with longlong and ulonglong dtype ([#3566](https://github.com/GenericMappingTools/pygmt/pull/3566)) +* GMT_IMAGE: Implement the to_dataarray method for 3-band images ([#3128](https://github.com/GenericMappingTools/pygmt/pull/3128)) +* Ensure non-ASCII characters are typeset correctly even if PS_CHAR_ENCODING is not "ISOLatin1+" ([#3611](https://github.com/GenericMappingTools/pygmt/pull/3611)) +* Add enums GridRegistration and GridType for grid registration and type ([#3693](https://github.com/GenericMappingTools/pygmt/pull/3693)) + +### Deprecations + +* SPEC 0: Bump minimum supported versions to Python 3.11, NumPy 1.25, pandas>=2.0 and xarray>=2023.04 ([#3460](https://github.com/GenericMappingTools/pygmt/pull/3460), [#3606](https://github.com/GenericMappingTools/pygmt/pull/3606), [#3697](https://github.com/GenericMappingTools/pygmt/pull/3697)) +* clib.Session.virtualfile_from_vectors: Now takes a sequence of vectors as its single argument (Passing multiple arguments will be unsupported in v0.16.0) ([#3522](https://github.com/GenericMappingTools/pygmt/pull/3522)) +* Remove the deprecated build_arg_string function (deprecated since v0.12.0) ([#3427](https://github.com/GenericMappingTools/pygmt/pull/3427)) +* Figure.grdcontour: Remove the deprecated syntax for the 'annotation' parameter (deprecated since v0.12.0) ([#3428](https://github.com/GenericMappingTools/pygmt/pull/3428)) + +### Bug Fixes + +* launch_external_viewer: Use full path when opening the file in a web browser ([#3647](https://github.com/GenericMappingTools/pygmt/pull/3647)) +* PyArrow: Map date32[day]/date64[ms] dtypes in pandas objects to np.datetime64 with correct date/time units ([#3617](https://github.com/GenericMappingTools/pygmt/pull/3617)) +* clib.session: Add the GMT_SESSION_NOGDALCLOSE flag to keep GDAL open ([#3672](https://github.com/GenericMappingTools/pygmt/pull/3672)) +* Set the "Conventions" attribute to "CF-1.7" for netCDF grids only ([#3463](https://github.com/GenericMappingTools/pygmt/pull/3463)) +* Fix the conversion error for pandas.Series with missing values in pandas<=2.1 ([#3505](https://github.com/GenericMappingTools/pygmt/pull/3505), [#3596](https://github.com/GenericMappingTools/pygmt/pull/3596)) +* GeoPandas: Explicitly convert columns with overflow integers to avoid OverflowError with fiona 1.10 ([#3455](https://github.com/GenericMappingTools/pygmt/pull/3455)) +* Figure.plot/Figure.plot3d: Improve the check of the "style" parameter for "v" or "V" ([#3603](https://github.com/GenericMappingTools/pygmt/pull/3603)) +* Correctly reserve the grid data dtype by converting ctypes array to numpy array with np.ctypeslib.as_array ([#3446](https://github.com/GenericMappingTools/pygmt/pull/3446)) +* **Breaking**: Figure.text: Fix typesetting of integers when mixed with floating-point values ([#3493](https://github.com/GenericMappingTools/pygmt/pull/3493)) + +### Documentation + +* Add basic tutorial "Plotting polygons" ([#3593](https://github.com/GenericMappingTools/pygmt/pull/3593)) +* Update the gallery example for plotting lines with LineString/MultiLineString geometry ([#3711](https://github.com/GenericMappingTools/pygmt/pull/3711)) +* Add the PyGMT ecosystem page ([#3475](https://github.com/GenericMappingTools/pygmt/pull/3475)) +* Document the support policy for optional packages ([#3616](https://github.com/GenericMappingTools/pygmt/pull/3616)) +* Document the environment variables that can affect the behavior of PyGMT ([#3432](https://github.com/GenericMappingTools/pygmt/pull/3432)) +* Document the built-in patterns in the Technical Reference section ([#3466](https://github.com/GenericMappingTools/pygmt/pull/3466)) +* Document Continuous Benchmarking in Maintainers Guides ([#3631](https://github.com/GenericMappingTools/pygmt/pull/3631)) +* Add instructions for installing optional dependencies ([#3506](https://github.com/GenericMappingTools/pygmt/pull/3506)) +* Update "PyData Ecosystem" to "Scientific Python Ecosystem" ([#3447](https://github.com/GenericMappingTools/pygmt/pull/3447)) +* Figure.savefig: Clarify that the "transparent" parameter also works for the PNG file associated with the KML format ([#3579](https://github.com/GenericMappingTools/pygmt/pull/3579)) +* Add the PyGMT talk at AGU24 to the "Overview" section ([#3685](https://github.com/GenericMappingTools/pygmt/pull/3685)) +* Add the GMT/PyGMT pre-conference workshop at AGU24 to the "External resources" section ([#3689](https://github.com/GenericMappingTools/pygmt/pull/3689)) +* Add TODO comments in the maintainers guides and update the release checklist ([#3724](https://github.com/GenericMappingTools/pygmt/pull/3724)) + +### Maintenance + +* **Breaking**: data_kind: data is None and required now returns the "empty" kind ([#3482](https://github.com/GenericMappingTools/pygmt/pull/3482)) +* **Breaking**: data_kind: Now "matrix" represents a 2-D numpy array and unrecognized data types fall back to "vectors" ([#3351](https://github.com/GenericMappingTools/pygmt/pull/3351)) +* Add Support for Python 3.13 ([#3490](https://github.com/GenericMappingTools/pygmt/pull/3490)) +* Add the Session.virtualfile_from_stringio method to allow StringIO input for certain functions/methods ([#3326](https://github.com/GenericMappingTools/pygmt/pull/3326)) +* Add "geodatasets" as a dependency for docs and update the choropleth example ([#3719](https://github.com/GenericMappingTools/pygmt/pull/3719)) +* PyArrow: Check compatibility of pyarrow.array with string type ([#2933](https://github.com/GenericMappingTools/pygmt/pull/2933)) +* Rename sphinx-gallery's README.txt to GALLERY_HEADER.rst and require Sphinx-Gallery>=0.17.0 ([#3348](https://github.com/GenericMappingTools/pygmt/pull/3348)) +* clib.conversion: Remove the as_c_contiguous function and use np.ascontiguousarray instead ([#3492](https://github.com/GenericMappingTools/pygmt/pull/3492)) +* Use TODO comments to track deprecations and workarounds ([#3722](https://github.com/GenericMappingTools/pygmt/pull/3722)) +* Move Figure.psconvert into a separate file ([#3553](https://github.com/GenericMappingTools/pygmt/pull/3553)) +* Improve the data type checking for 2-D arrays passed to the GMT C API ([#3563](https://github.com/GenericMappingTools/pygmt/pull/3563)) +* Enable ruff's TD (flake8-todos), COM (flake8-commas), TRY (tryceratops), and EM (flake8-errmsg) rules ([#3723](https://github.com/GenericMappingTools/pygmt/pull/3723), [#3531](https://github.com/GenericMappingTools/pygmt/pull/3531), [#3665](https://github.com/GenericMappingTools/pygmt/pull/3665), [#3661](https://github.com/GenericMappingTools/pygmt/pull/3661)) +* CI: Install pyarrow-core instead of pyarrow from conda-forge ([#3698](https://github.com/GenericMappingTools/pygmt/pull/3698)) +* CI: Ensure no hyphens in Python file and directory names in the "Style Checks" workflow ([#3703](https://github.com/GenericMappingTools/pygmt/pull/3703)) +* Bump to ruff>=0.8.0 and rename rule TCH to TC ([#3662](https://github.com/GenericMappingTools/pygmt/pull/3662)) +* Bump to Ghostscript 10.04.0 ([#3443](https://github.com/GenericMappingTools/pygmt/pull/3443)) +* Add enums GridFormat for GMT grid format ID ([#3449](https://github.com/GenericMappingTools/pygmt/pull/3449)) + +**Full Changelog**: + +### Contributors + +* [Dongdong Tian](https://github.com/seisman) +* [Yvonne Fröhlich](https://github.com/yvonnefroehlich) +* [Wei Ji Leong](https://github.com/weiji14) +* [Michael Grund](https://github.com/michaelgrund) +* [Will Schlitzer](https://github.com/willschlitzer) +* [Jiayuan Yao](https://github.com/core-man) + +--- + ## Release v0.13.0 (2024/09/05) [![Digital Object Identifier for PyGMT v0.13.0](https://zenodo.org/badge/DOI/10.5281/zenodo.13679420.svg)](https://doi.org/10.5281/zenodo.13679420) @@ -178,6 +282,8 @@ * [Michael Grund](https://github.com/michaelgrund) * [Wei Ji Leong](https://github.com/weiji14) +--- + ## Release v0.11.0 (2024/02/01) [![Digital Object Identifier for PyGMT v0.11.0](https://zenodo.org/badge/DOI/10.5281/zenodo.10578540.svg)](https://doi.org/10.5281/zenodo.10578540) diff --git a/doc/conf.py b/doc/conf.py index 2b41acd3f61..f3cb59228c2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -210,12 +210,10 @@ repository = "GenericMappingTools/pygmt" repository_url = "https://github.com/GenericMappingTools/pygmt" if __commit__: - commit_link = ( - f'{ __commit__[:8] }' - ) + commit_link = f'{__commit__[:8]}' else: commit_link = ( - f'{ __version__ }' + f'{__version__}' ) html_context = { "menu_links": [ diff --git a/doc/contributing.md b/doc/contributing.md index 5dd93418f53..36b0d76e984 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -130,9 +130,9 @@ our tests. This way, the *main* branch is always stable. integrated separately. - Bug fixes should be submitted in separate PRs. * How to write and submit a PR - - Use underscores for all Python (*.py) files as per - [PEP8](https://www.python.org/dev/peps/pep-0008/), not hyphens. Directory - names should also use underscores instead of hyphens. + - Use underscores for all Python (\*.py) files as per + [PEP8](https://www.python.org/dev/peps/pep-0008/), not hyphens. Directory names + should also use underscores instead of hyphens. - Describe what your PR changes and *why* this is a good thing. Be as specific as you can. The PR description is how we keep track of the changes made to the project over time. diff --git a/doc/external_resources.md b/doc/external_resources.md index b0e8ad10e34..30c04764e50 100644 --- a/doc/external_resources.md +++ b/doc/external_resources.md @@ -13,7 +13,7 @@ to submit a pull request with your recommended addition to the :::::{grid} 1 2 2 3 ::::{grid-item-card} 2024 AGU PREWS9: Mastering Geospatial Visualizations with GMT/PyGMT -:link: https://github.com/GenericMappingTools/agu24workshop +:link: https://www.generic-mapping-tools.org/agu24workshop/ :text-align: center :margin: 0 3 0 0 diff --git a/doc/maintenance.md b/doc/maintenance.md index 41c64769f98..0664a2850eb 100644 --- a/doc/maintenance.md +++ b/doc/maintenance.md @@ -148,34 +148,35 @@ patch release. ## Backwards Compatibility and Deprecation Policy -PyGMT is still undergoing rapid development. All of the API is subject to change -until the v1.0.0 release. Versioning in PyGMT is based on the +PyGMT is still undergoing rapid development. All of the API is subject to change until +the v1.0.0 release. Versioning in PyGMT is based on the [semantic versioning specification](https://semver.org/spec/v2.0.0.html) -(i.e., v*MAJOR*.*MINOR*.*PATCH*). -Basic policy for backwards compatibility: +(i.e., v*MAJOR*.*MINOR*.*PATCH*). Basic policy for backwards compatibility: - Any incompatible changes should go through the deprecation process below. -- Incompatible changes are only allowed in major and minor releases, not in - patch releases. +- Incompatible changes are only allowed in major and minor releases, not in patch releases. - Incompatible changes should be documented in the release notes. When making incompatible changes, we should follow the process: - Discuss whether the incompatible changes are necessary on GitHub. -- Make the changes in a backwards compatible way, and raise a `FutureWarning` - warning for the old usage. At least one test using the old usage should be added. -- The warning message should clearly explain the changes and include the versions - in which the old usage is deprecated and is expected to be removed. -- The `FutureWarning` warning should appear for 2-4 minor versions, depending on - the impact of the changes. It means the deprecation period usually lasts - 3-12 months. +- Make the changes in a backwards compatible way, and raise a `FutureWarning` warning + for the old usage. At least one test using the old usage should be added. +- The warning message should clearly explain the changes and include the versions in + which the old usage is deprecated and is expected to be removed. +- The `FutureWarning` warning should appear for 2-4 minor versions, depending on the + impact of the changes. It means the deprecation period usually lasts 3-12 months. - Remove the old usage and warning when reaching the declared version. -To rename a function parameter, add the `@deprecate_parameter` decorator near -the top after the `@fmt_docstring` decorator but before the `@use_alias` -decorator (if those two exist). Here is an example: +### Deprecating a function parameter -``` +To rename a function parameter, add the `@deprecate_parameter` decorator near the top +after the `@fmt_docstring` decorator but before the `@use_alias` decorator (if those two +exist). A `TODO` comment should also be added to indicate the deprecation period (see below). +Here is an example: + +```python +# TODO(PyGMT>=0.6.0): Remove the deprecated "columns" parameter. @fmt_docstring @deprecate_parameter("columns", "incols", "v0.4.0", remove_version="v0.6.0") @use_alias(J="projection", R="region", V="verbose", i="incols") @@ -184,8 +185,30 @@ def plot(self, x=None, y=None, data=None, size=None, direction=None, **kwargs): pass ``` -In this case, the old parameter name `columns` is deprecated since v0.4.0, and -will be fully removed in v0.6.0. The new parameter name is `incols`. +In this case, the old parameter name `columns` is deprecated since v0.4.0, and will be +fully removed in v0.6.0. The new parameter name is `incols`. + +### TODO comments + +Occasionally, we need to implement temporary code that should be removed in the future. +This can occur in situations such as: + +- When a parameter, function, or method is deprecated and scheduled for removal. +- When workarounds are necessary to address issues in older or upcoming versions of GMT + or other dependencies. + +To track these temporary codes or workarounds, we use TODO comments. These comments +should adhere to the following format: + +```python +# TODO(package>=X.Y.Z): A brief description of the TODO item. +# Additional details if necessary. +``` +The TODO comment indicates that we should address the item when *package* version +*X.Y.Z* or later is required. + +It's important not to overuse TODO comments for tracking unimplemented features. +Instead, open issues to monitor these features. ## Making a Release diff --git a/doc/minversions.md b/doc/minversions.md index 1d348ec6922..b10150b5ce1 100644 --- a/doc/minversions.md +++ b/doc/minversions.md @@ -41,6 +41,7 @@ compatibility reasons. | PyGMT Version | GMT | Python | NumPy | pandas | Xarray | |---|---|---|---|---|---| | [Dev][]* [] | {{ requires.gmt }} | {{ requires.python }} | {{ requires.numpy }} | {{ requires.pandas }} | {{ requires.xarray }} | +| [] | >=6.4.0 | >=3.11 | >=1.25 | >=2.0 | >=2023.04 | | [] | >=6.3.0 | >=3.10 | >=1.24 | >=1.5 | >=2022.09 | | [] | >=6.3.0 | >=3.10 | >=1.23 | >=1.5 | >=2022.06 | | [] | >=6.3.0 | >=3.9 | >=1.23 | | | diff --git a/doc/techref/patterns.md b/doc/techref/patterns.md index 17deb045aa3..de7df02d047 100644 --- a/doc/techref/patterns.md +++ b/doc/techref/patterns.md @@ -11,7 +11,7 @@ image raster file. The former will result in one of the 90 predefined 64x64 bit- provided by GMT (see the figure below). The latter allows the user to create customized, repeating images using image raster files. -By specifying upper case **P** instead of **p** the image will be bit-reversed, i.e., +By specifying uppercase **P** instead of **p** the image will be bit-reversed, i.e., white and black areas will be interchanged (only applies to 1-bit images or predefined bit-image patterns). For these patterns and other 1-bit images one may specify alternative **b**ackground and **f**oreground colors (by appending **+b**_color_ and/or diff --git a/environment.yml b/environment.yml index 620b1bad91b..c51b2967fc2 100644 --- a/environment.yml +++ b/environment.yml @@ -27,7 +27,7 @@ dependencies: # Dev dependencies (style checks) - codespell - pre-commit - - ruff>=0.8.2 + - ruff>=0.9.0 # Dev dependencies (unit testing) - matplotlib-base - pytest>=6.0 @@ -35,6 +35,7 @@ dependencies: - pytest-doctestplus - pytest-mpl # Dev dependencies (building documentation) + - geodatasets - myst-nb - panel - sphinx>=6.2 diff --git a/examples/gallery/basemaps/double_y_axes.py b/examples/gallery/basemaps/double_y_axes.py index aa8ba8a6815..c6f970e59ca 100644 --- a/examples/gallery/basemaps/double_y_axes.py +++ b/examples/gallery/basemaps/double_y_axes.py @@ -5,7 +5,7 @@ The ``frame`` parameter of the plotting methods of the :class:`pygmt.Figure` class can control which axes should be plotted and optionally show annotations, tick marks, and gridlines. By default, all 4 axes are plotted, along with -annotations and tick marks (denoted **W**, **S**, **E**, **N**). Lower case +annotations and tick marks (denoted **W**, **S**, **E**, **N**). Lowercase versions (**w**, **s**, **e**, **n**) can be used to denote to only plot the axes with tick marks. We can also only plot the axes without annotations and tick marks using **l** (left axis), **r** (right axis), **t** (top axis), diff --git a/examples/gallery/embellishments/scalebar.py b/examples/gallery/embellishments/scalebar.py index bca7d0b9703..4b829165fe5 100644 --- a/examples/gallery/embellishments/scalebar.py +++ b/examples/gallery/embellishments/scalebar.py @@ -12,8 +12,9 @@ - **g**: Give map coordinates as *longitude*\/\ *latitude*. - **j**\|\ **J**: Specify a two-character (order independent) code. Choose from vertical **T**\(op), **M**\(iddle), or **B**\(ottom) and - horizontal **L**\(eft), **C**\(entre), or **R**\(ight). Lower / upper - case **j** / **J** mean inside / outside of the map bounding box. + horizontal **L**\(eft), **C**\(entre), or **R**\(ight). Lower / + uppercase **j** / **J** mean inside / outside of the map bounding + box. - **n**: Give normalized bounding box coordinates as *nx*\/\ *ny*. - **x**: Give plot coordinates as *x*\/\ *y*. diff --git a/examples/gallery/images/cross_section.py b/examples/gallery/images/cross_section.py index 0f0cd8e352e..d451c8c3ee8 100644 --- a/examples/gallery/images/cross_section.py +++ b/examples/gallery/images/cross_section.py @@ -37,7 +37,7 @@ # Add a colorbar for the elevation fig.colorbar( - # Place the colorbar inside the plot (lower-case "j") in the Bottom Right (BR) + # Place the colorbar inside the plot (lowercase "j") in the Bottom Right (BR) # corner with an offset ("+o") of 0.7 centimeters and 0.3 centimeters in x or y # directions, respectively; move the x label above the horizontal colorbar ("+ml") position="jBR+o0.7c/0.8c+h+w5c/0.3c+ml", diff --git a/examples/gallery/images/rgb_image.py b/examples/gallery/images/rgb_image.py index 56d7b9d2f70..8afb2c93e25 100644 --- a/examples/gallery/images/rgb_image.py +++ b/examples/gallery/images/rgb_image.py @@ -28,7 +28,7 @@ # Subset to area of Lāhainā in EPSG:32604 coordinates image = img.rio.clip_box(minx=738000, maxx=755000, miny=2300000, maxy=2318000) image = image.load() # Force loading the DataArray into memory -image # noqa: B018 +image # %% # Plot the RGB imagery: diff --git a/examples/gallery/lines/decorated_lines.py b/examples/gallery/lines/decorated_lines.py index 42ad5dc7be9..cbe6b6b510d 100644 --- a/examples/gallery/lines/decorated_lines.py +++ b/examples/gallery/lines/decorated_lines.py @@ -51,7 +51,7 @@ "~d1c:+sd0.5c+gtan+p1p,black+n-0.2c/0.1c", # Give the number of equally spaced symbols by using "n" instead of "d" "~n6:+sn0.5c+gtan+p1p,black", - # Use upper-case "N" to have symbols at the start and end of the line + # Use uppercase "N" to have symbols at the start and end of the line "~N6:+sh0.5c+gtan+p1p,black", # Suppress the main decorated line by appending "+i" "~d1c:+sg0.5c+gtan+p1p,black+i", diff --git a/examples/gallery/lines/linestrings.py b/examples/gallery/lines/linestrings.py new file mode 100644 index 00000000000..18f94502f16 --- /dev/null +++ b/examples/gallery/lines/linestrings.py @@ -0,0 +1,46 @@ +""" +GeoPandas: Plotting lines with LineString or MultiLineString geometry +===================================================================== + +The :meth:`pygmt.Figure.plot` method allows us to plot geographical data such as lines +with LineString or MultiLineString geometry types stored in a +:class:`geopandas.GeoDataFrame` object or any object that implements the +`__geo_interface__ `__ property. + +Use :func:`geopandas.read_file` to load data from any supported OGR format such as a +shapefile (.shp), GeoJSON (.geojson), geopackage (.gpkg), etc. Then, pass the +:class:`geopandas.GeoDataFrame` object as an argument to the ``data`` parameter of +:meth:`pygmt.Figure.plot`, and style the lines using the ``pen`` parameter. +""" + +# %% +import geodatasets +import geopandas as gpd +import pygmt + +# Read a sample dataset provided by the geodatasets package. +# The dataset contains large rivers in Europe, stored as LineString/MultiLineString +# geometry types. +gdf = gpd.read_file(geodatasets.get_path("eea large_rivers")) + +# Convert object to EPSG 4326 coordinate system +gdf = gdf.to_crs("EPSG:4326") +gdf.head() + +# %% +fig = pygmt.Figure() + +fig.coast( + projection="M10c", + region=[-10, 30, 35, 57], + resolution="l", + land="gray95", + shorelines="1/0.1p,gray50", + borders="1/0.1,gray30", + frame=True, +) + +# Add rivers to map +fig.plot(data=gdf, pen="1p,steelblue") + +fig.show() diff --git a/examples/gallery/lines/quoted_lines.py b/examples/gallery/lines/quoted_lines.py index 9e70ec15c4a..2ccfb1309a6 100644 --- a/examples/gallery/lines/quoted_lines.py +++ b/examples/gallery/lines/quoted_lines.py @@ -33,7 +33,7 @@ "qd1c:+ltext+i", # Give the number of equally spaced labels by using "n" instead of "d" "qn5:+ltext", - # Use upper-case "N" to have labels at the start and end of the line + # Use uppercase "N" to have labels at the start and end of the line "qN5:+ltext", # To only plot a label at the start of the line use "N-1" "qN-1:+ltext", diff --git a/examples/gallery/lines/roads.py b/examples/gallery/lines/roads.py deleted file mode 100644 index c2a5f69980a..00000000000 --- a/examples/gallery/lines/roads.py +++ /dev/null @@ -1,46 +0,0 @@ -# ruff: noqa: RUF003 -""" -Roads -===== - -The :meth:`pygmt.Figure.plot` method allows us to plot geographical data such -as lines which are stored in a :class:`geopandas.GeoDataFrame` object. Use -:func:`geopandas.read_file` to load data from any supported OGR format such as -a shapefile (.shp), GeoJSON (.geojson), geopackage (.gpkg), etc. Then, pass the -:class:`geopandas.GeoDataFrame` as an argument to the ``data`` parameter of -:meth:`pygmt.Figure.plot`, and style the geometry using the ``pen`` parameter. -""" - -# %% -import geopandas as gpd -import pygmt - -# Read shapefile data using geopandas -gdf = gpd.read_file( - "https://www2.census.gov/geo/tiger/TIGER2015/PRISECROADS/tl_2015_15_prisecroads.zip" -) -# The dataset contains different road types listed in the RTTYP column, -# here we select the following ones to plot: -roads_common = gdf[gdf.RTTYP == "M"] # Common name roads -roads_state = gdf[gdf.RTTYP == "S"] # State recognized roads -roads_interstate = gdf[gdf.RTTYP == "I"] # Interstate roads - -fig = pygmt.Figure() - -# Define target region around Oʻahu (Hawaiʻi) -region = [-158.3, -157.6, 21.2, 21.75] # xmin, xmax, ymin, ymax - -title = "Main roads of O`ahu (Hawai`i)" # Approximating the Okina letter ʻ with ` -fig.basemap(region=region, projection="M12c", frame=["af", f"WSne+t{title}"]) -fig.coast(land="gray", water="dodgerblue4", shorelines="1p,black") - -# Plot the individual road types with different pen settings and assign labels -# which are displayed in the legend -fig.plot(data=roads_common, pen="5p,dodgerblue", label="CommonName") -fig.plot(data=roads_state, pen="2p,gold", label="StateRecognized") -fig.plot(data=roads_interstate, pen="2p,red", label="Interstate") - -# Add legend -fig.legend() - -fig.show() diff --git a/examples/gallery/maps/choropleth_map.py b/examples/gallery/maps/choropleth_map.py index 6c43d24d3dd..f1cce8c3014 100644 --- a/examples/gallery/maps/choropleth_map.py +++ b/examples/gallery/maps/choropleth_map.py @@ -2,25 +2,27 @@ Choropleth map ============== -The :meth:`pygmt.Figure.plot` method allows us to plot geographical data such -as polygons which are stored in a :class:`geopandas.GeoDataFrame` object. Use -:func:`geopandas.read_file` to load data from any supported OGR format such as -a shapefile (.shp), GeoJSON (.geojson), geopackage (.gpkg), etc. You can also -use a full URL pointing to your desired data source. Then, pass the -:class:`geopandas.GeoDataFrame` as an argument to the ``data`` parameter of -:meth:`pygmt.Figure.plot`, and style the geometry using the ``pen`` parameter. -To fill the polygons based on a corresponding column you need to set -``fill="+z"`` as well as select the appropriate column using the ``aspatial`` -parameter as shown in the example below. +The :meth:`pygmt.Figure.plot` method allows us to plot geographical data such as +polygons which are stored in a :class:`geopandas.GeoDataFrame` object. Use +:func:`geopandas.read_file` to load data from any supported OGR format such as a +shapefile (.shp), GeoJSON (.geojson), geopackage (.gpkg), etc. You can also use a full +URL pointing to your desired data source. Then, pass the :class:`geopandas.GeoDataFrame` +as an argument to the ``data`` parameter of :meth:`pygmt.Figure.plot`, and style the +geometry using the ``pen`` parameter. To fill the polygons based on a corresponding +column you need to set ``fill="+z"`` as well as select the appropriate column using the +``aspatial`` parameter as shown in the example below. """ # %% +import geodatasets import geopandas as gpd import pygmt -# Read polygon data using geopandas -gdf = gpd.read_file("https://geodacenter.github.io/data-and-lab/data/airbnb.zip") +# Read the example dataset provided by geodatasets. +gdf = gpd.read_file(geodatasets.get_path("geoda airbnb")) +print(gdf.head()) +# %% fig = pygmt.Figure() fig.basemap( @@ -29,11 +31,10 @@ frame="+tPopulation of Chicago", ) -# The dataset contains different attributes, here we select -# the "population" column to plot. +# The dataset contains different attributes, here we select the "population" column to +# plot. -# First, we define the colormap to fill the polygons based on -# the "population" column. +# First, we define the colormap to fill the polygons based on the "population" column. pygmt.makecpt( cmap="acton", series=[gdf["population"].min(), gdf["population"].max(), 10], @@ -41,8 +42,8 @@ reverse=True, ) -# Next, we plot the polygons and fill them using the defined colormap. -# The target column is defined by the aspatial parameter. +# Next, we plot the polygons and fill them using the defined colormap. The target column +# is defined by the aspatial parameter. fig.plot( data=gdf, pen="0.3p,gray10", @@ -51,7 +52,7 @@ aspatial="Z=population", ) -# Add colorbar legend +# Add colorbar legend. fig.colorbar(frame="x+lPopulation", position="jML+o-0.5c+w3.5c/0.2c") fig.show() diff --git a/examples/gallery/symbols/multi_parameter_symbols.py b/examples/gallery/symbols/multi_parameter_symbols.py index 6baa607f1c5..72480aba063 100644 --- a/examples/gallery/symbols/multi_parameter_symbols.py +++ b/examples/gallery/symbols/multi_parameter_symbols.py @@ -33,7 +33,7 @@ # directions given in degrees counter-clockwise from horizontal. Append **+i** and the # desired value to apply an inner diameter. # -# Upper-case versions **E**, **J**, and **W** are similar to **e**, **j**, and **w** +# Uppercase versions **E**, **J**, and **W** are similar to **e**, **j**, and **w** # but expect geographic azimuths and distances. fig = pygmt.Figure() diff --git a/examples/projections/cyl/cyl_oblique_mercator.py b/examples/projections/cyl/cyl_oblique_mercator.py index 17bf589d48b..22db4f973da 100644 --- a/examples/projections/cyl/cyl_oblique_mercator.py +++ b/examples/projections/cyl/cyl_oblique_mercator.py @@ -9,7 +9,7 @@ The projection is set with **o** or **O**. There are three different specification ways (**a**\|\ **A**, **b**\|\ **B**, **c**\|\ **C**) available. For all three -definitions, the upper case letter mean the projection pole is set in the southern +definitions, the uppercase letter mean the projection pole is set in the southern hemisphere [Default is northern hemisphere]. Align the y-axis with the optional modifier **+v**. The figure size is set with *scale* or *width*. """ diff --git a/examples/projections/nongeo/cartesian_linear.py b/examples/projections/nongeo/cartesian_linear.py index 021d080ae94..83e986f6739 100644 --- a/examples/projections/nongeo/cartesian_linear.py +++ b/examples/projections/nongeo/cartesian_linear.py @@ -4,7 +4,7 @@ **X**\ *width*\ [/*height*] or **x**\ *x-scale*\ [/*y-scale*] -Give the *width* of the figure and the optional *height*. The lower-case version +Give the *width* of the figure and the optional *height*. The lowercase version **x** is similar to **X** but expects an *x-scale* and an optional *y-scale*. The Cartesian linear projection is primarily designed for regular floating point diff --git a/examples/projections/nongeo/cartesian_logarithmic.py b/examples/projections/nongeo/cartesian_logarithmic.py index ef354dba73f..0e4619075ea 100644 --- a/examples/projections/nongeo/cartesian_logarithmic.py +++ b/examples/projections/nongeo/cartesian_logarithmic.py @@ -6,7 +6,7 @@ **x**\ *x-scale*\ [**l**][/*y-scale*\ [**l**]] Give the *width* of the figure and the optional *height*. -The lower-case version **x** is similar to **X** but expects +The lowercase version **x** is similar to **X** but expects an *x-scale* and an optional *y-scale*. Each axis with a logarithmic transformation requires **l** after its size argument. diff --git a/examples/projections/nongeo/cartesian_power.py b/examples/projections/nongeo/cartesian_power.py index 862ceba8595..b5f856713ed 100644 --- a/examples/projections/nongeo/cartesian_power.py +++ b/examples/projections/nongeo/cartesian_power.py @@ -6,7 +6,7 @@ **x**\ *x-scale*\ [**p**\ *pvalue*][/*y-scale*\ [**p**\ *pvalue*]] Give the *width* of the figure and the optional argument *height*. -The lower-case version **x** is similar to **X** but expects +The lowercase version **x** is similar to **X** but expects an *x-scale* and an optional *y-scale*. Each axis with a power transformation requires **p** and the exponent for that axis after its size argument. diff --git a/examples/projections/nongeo/polar.py b/examples/projections/nongeo/polar.py index 5c71e517c17..760838b34ac 100644 --- a/examples/projections/nongeo/polar.py +++ b/examples/projections/nongeo/polar.py @@ -12,7 +12,7 @@ Limits are set via the ``region`` parameter ([*theta_min*, *theta_max*, *radius_min*, *radius_max*]). When using **P**\ *width* you -have to give the *width* of the figure. The lower-case version **p** is similar to **P** +have to give the *width* of the figure. The lowercase version **p** is similar to **P** but expects a *scale* instead of a width (**p**\ *scale*). The following customizing modifiers are available: diff --git a/examples/tutorials/advanced/cartesian_histograms.py b/examples/tutorials/advanced/cartesian_histograms.py index 7191d18cbfd..3a547fd92f5 100644 --- a/examples/tutorials/advanced/cartesian_histograms.py +++ b/examples/tutorials/advanced/cartesian_histograms.py @@ -348,7 +348,7 @@ # of the bin width # Offset ("+o") the bars to align each bar with the left limit of the corresponding # bin - barwidth=f"{binwidth/2}+o-{binwidth/4}", + barwidth=f"{binwidth / 2}+o-{binwidth / 4}", label="data01", ) @@ -359,7 +359,7 @@ fill="orange", pen="1p,darkgray,solid", histtype=0, - barwidth=f"{binwidth/2}+o{binwidth/4}", + barwidth=f"{binwidth / 2}+o{binwidth / 4}", label="data02", ) diff --git a/examples/tutorials/basics/frames.py b/examples/tutorials/basics/frames.py index 56b4e9ac130..b3fb466fa23 100644 --- a/examples/tutorials/basics/frames.py +++ b/examples/tutorials/basics/frames.py @@ -90,9 +90,9 @@ # :meth:`pygmt.Figure.basemap`. The map boundaries (or plot axes) are named as # West/west/left (**W**, **w**, **l**), South/south/bottom # (**S**, **s**, **b**), North/north/top (**N**, **n**, **t**), and -# East/east/right (**E**, **e**, **r**) sides of a figure. If an upper-case +# East/east/right (**E**, **e**, **r**) sides of a figure. If an uppercase # letter (**W**, **S**, **N**, **E**) is passed, the axis is plotted with -# tick marks and annotations. The lower-case version +# tick marks and annotations. The lowercase version # (**w**, **s**, **n**, **e**) plots the axis only with tick marks. # To only plot the axis pass **l**, **b**, **t**, **r**. By default # (``frame=True`` or ``frame="af"``), the West and the South axes are diff --git a/examples/tutorials/basics/plot.py b/examples/tutorials/basics/plot.py index babfb60d771..42cb8aad5ae 100644 --- a/examples/tutorials/basics/plot.py +++ b/examples/tutorials/basics/plot.py @@ -18,7 +18,9 @@ # The data are loaded as a :class:`pandas.DataFrame`. data = pygmt.datasets.load_sample_data(name="japan_quakes") +data.head() +# %% # Set the region for the plot to be slightly larger than the data bounds. region = [ data.longitude.min() - 1, @@ -26,9 +28,7 @@ data.latitude.min() - 1, data.latitude.max() + 1, ] - -print(region) -print(data.head()) +region # %% # We'll use the :meth:`pygmt.Figure.plot` method to plot circles on the diff --git a/pygmt/_show_versions.py b/pygmt/_show_versions.py index e529f053e46..f0d4b4e3c2f 100644 --- a/pygmt/_show_versions.py +++ b/pygmt/_show_versions.py @@ -16,7 +16,7 @@ from pygmt.clib import Session, __gmt_version__ # Get semantic version through setuptools-scm -__version__ = f'v{version("pygmt")}' # e.g. v0.1.2.dev3+g0ab3cd78 +__version__ = f"v{version('pygmt')}" # e.g. v0.1.2.dev3+g0ab3cd78 __commit__ = __version__.split("+g")[-1] if "+g" in __version__ else "" # 0ab3cd78 @@ -85,7 +85,7 @@ def _check_ghostscript_version(gs_version: str | None) -> str | None: return None -def show_versions(file: TextIO | None = sys.stdout): +def show_versions(file: TextIO | None = sys.stdout) -> None: """ Print various dependency versions which are useful when submitting bug reports. diff --git a/pygmt/clib/conversion.py b/pygmt/clib/conversion.py index 5a1d1cf51b9..7823aa32103 100644 --- a/pygmt/clib/conversion.py +++ b/pygmt/clib/conversion.py @@ -173,12 +173,11 @@ def _to_numpy(data: Any) -> np.ndarray: # The numpy dtype for the result numpy array, but can be None. numpy_dtype = dtypes.get(str(dtype)) + # TODO(pandas>=2.2): Remove the workaround for pandas<2.2. + # # pandas numeric dtypes were converted to np.object_ dtype prior pandas 2.2, and are # converted to suitable NumPy dtypes since pandas 2.2. Refer to the following link # for details: https://pandas.pydata.org/docs/whatsnew/v2.2.0.html#to-numpy-for-numpy-nullable-and-arrow-types-converts-to-suitable-numpy-dtype - # - # Workarounds for pandas < 2.2. Following SPEC 0, pandas 2.1 should be dropped in - # 2025 Q3, so it's likely we can remove the workaround in PyGMT v0.17.0. if ( Version(pd.__version__) < Version("2.2") # pandas < 2.2 only. and hasattr(data, "dtype") # NumPy array or pandas objects only. @@ -193,8 +192,24 @@ def _to_numpy(data: Any) -> np.ndarray: numpy_dtype = np.float64 data = data.to_numpy(na_value=np.nan) + # Deal with timezone-aware datetime dtypes. + if isinstance(dtype, pd.DatetimeTZDtype): # pandas.DatetimeTZDtype + numpy_dtype = getattr(dtype, "base", None) + elif isinstance(dtype, pd.ArrowDtype) and hasattr(dtype.pyarrow_dtype, "tz"): + # pd.ArrowDtype[pa.Timestamp] + numpy_dtype = getattr(dtype, "numpy_dtype", None) + # TODO(pandas>=2.1): Remove the workaround for pandas<2.1. + if Version(pd.__version__) < Version("2.1"): + # In pandas 2.0, dtype.numpy_type is dtype("O"). + numpy_dtype = np.dtype(f"M8[{dtype.pyarrow_dtype.unit}]") # type: ignore[assignment, attr-defined] + array = np.ascontiguousarray(data, dtype=numpy_dtype) + # Check if a np.object_ or np.str_ array can be converted to np.datetime64. + if array.dtype.type in {np.object_, np.str_}: + with contextlib.suppress(TypeError, ValueError): + return np.ascontiguousarray(array, dtype=np.datetime64) + # Check if a np.object_ array can be converted to np.str_. if array.dtype == np.object_: with contextlib.suppress(TypeError, ValueError): diff --git a/pygmt/clib/session.py b/pygmt/clib/session.py index ee37c55d59f..179737d35f9 100644 --- a/pygmt/clib/session.py +++ b/pygmt/clib/session.py @@ -327,7 +327,7 @@ def get_libgmt_func( function.restype = restype return function - def create(self, name: str): + def create(self, name: str) -> None: """ Create a new GMT C API session. @@ -384,7 +384,8 @@ def print_func(file_pointer, message): # noqa: ARG001 We'll capture the messages and print them to stderr so that they will show up on the Jupyter notebook. """ - # Have to use try..except due to upstream GMT bug in GMT <= 6.5.0. + # TODO(GMT>6.5.0): Remove the workaround for upstream bug in GMT<=6.5.0. + # Have to use try..except due to upstream GMT bug in GMT<=6.5.0. # See https://github.com/GenericMappingTools/pygmt/issues/3205. try: message = message.decode().strip() @@ -593,7 +594,7 @@ def get_common(self, option: str) -> bool | int | float | np.ndarray: case _: # 'status' is the option value (in integer type). return status - def call_module(self, module: str, args: str | list[str]): + def call_module(self, module: str, args: str | list[str]) -> None: """ Call a GMT module with the given arguments. @@ -945,7 +946,9 @@ def _check_dtype_and_dim(self, array: np.ndarray, ndim: int) -> int: raise GMTInvalidInput(msg) return self[DTYPES[dtype]] - def put_vector(self, dataset: ctp.c_void_p, column: int, vector: np.ndarray): + def put_vector( + self, dataset: ctp.c_void_p, column: int, vector: np.ndarray + ) -> None: r""" Attach a 1-D numpy array as a column on a GMT dataset. @@ -1004,7 +1007,9 @@ def put_vector(self, dataset: ctp.c_void_p, column: int, vector: np.ndarray): ) raise GMTCLibError(msg) - def put_strings(self, dataset: ctp.c_void_p, family: str, strings: np.ndarray): + def put_strings( + self, dataset: ctp.c_void_p, family: str, strings: np.ndarray + ) -> None: """ Attach a 1-D numpy array of dtype str as a column on a GMT dataset. @@ -1058,7 +1063,9 @@ def put_strings(self, dataset: ctp.c_void_p, family: str, strings: np.ndarray): msg = f"Failed to put strings of type {strings.dtype} into dataset." raise GMTCLibError(msg) - def put_matrix(self, dataset: ctp.c_void_p, matrix: np.ndarray, pad: int = 0): + def put_matrix( + self, dataset: ctp.c_void_p, matrix: np.ndarray, pad: int = 0 + ) -> None: """ Attach a 2-D numpy array to a GMT dataset. @@ -1203,7 +1210,7 @@ def read_data( raise GMTCLibError(msg) return ctp.cast(data_ptr, ctp.POINTER(dtype)) - def write_data(self, family, geometry, mode, wesn, output, data): + def write_data(self, family, geometry, mode, wesn, output, data) -> None: """ Write a GMT data container to a file. @@ -1388,23 +1395,6 @@ def open_virtualfile( msg = f"Failed to close virtual file '{vfname}'." raise GMTCLibError(msg) - def open_virtual_file(self, family, geometry, direction, data): - """ - Open a GMT virtual file associated with a data object for reading or writing. - - .. deprecated: 0.11.0 - - Will be removed in v0.15.0. Use :meth:`pygmt.clib.Session.open_virtualfile` - instead. - """ - msg = ( - "API function `Session.open_virtual_file()' has been deprecated " - "since v0.11.0 and will be removed in v0.15.0. " - "Use `Session.open_virtualfile()' instead." - ) - warnings.warn(msg, category=FutureWarning, stacklevel=2) - return self.open_virtualfile(family, geometry, direction, data) - @contextlib.contextmanager def virtualfile_from_vectors( self, vectors: Sequence, *args @@ -1454,9 +1444,9 @@ def virtualfile_from_vectors( ... print(fout.read().strip()) : N = 3 <1/3> <4/6> <7/9> """ + # TODO(PyGMT>=0.16.0): Remove the "*args" parameter and related codes. # "*args" is added in v0.14.0 for backward-compatibility with the deprecated # syntax of passing multiple vectors as positional arguments. - # Remove it in v0.16.0. if len(args) > 0: msg = ( "Passing multiple arguments to Session.virtualfile_from_vectors is " @@ -1918,42 +1908,6 @@ def virtualfile_in( file_context = _virtualfile_from(_data) return file_context - def virtualfile_from_data( - self, - check_kind=None, - data=None, - x=None, - y=None, - z=None, - extra_arrays=None, - required_z=False, - required_data=True, - ): - """ - Store any data inside a virtual file. - - .. deprecated: 0.13.0 - - Will be removed in v0.15.0. Use :meth:`pygmt.clib.Session.virtualfile_in` - instead. - """ - msg = ( - "API function 'Session.virtualfile_from_data()' has been deprecated since " - "v0.13.0 and will be removed in v0.15.0. Use 'Session.virtualfile_in()' " - "instead." - ) - warnings.warn(msg, category=FutureWarning, stacklevel=2) - return self.virtualfile_in( - check_kind=check_kind, - data=data, - x=x, - y=y, - z=z, - extra_arrays=extra_arrays, - required_z=required_z, - required_data=required_data, - ) - @contextlib.contextmanager def virtualfile_out( self, diff --git a/pygmt/conftest.py b/pygmt/conftest.py index bc896d44732..ee491b7f8bc 100644 --- a/pygmt/conftest.py +++ b/pygmt/conftest.py @@ -5,7 +5,7 @@ import numpy as np from packaging.version import Version -# Keep this until we require numpy to be >=2.0 +# TODO(NumPy>=2.0): Remove the conftest.py file. # Address https://github.com/GenericMappingTools/pygmt/issues/2628. if Version(np.__version__) >= Version("2.0.0.dev0+git20230726"): np.set_printoptions(legacy="1.25") # type: ignore[arg-type] diff --git a/pygmt/datasets/__init__.py b/pygmt/datasets/__init__.py index d70eec5a1de..3d44a8fe676 100644 --- a/pygmt/datasets/__init__.py +++ b/pygmt/datasets/__init__.py @@ -6,10 +6,16 @@ from pygmt.datasets.earth_age import load_earth_age from pygmt.datasets.earth_day import load_blue_marble +from pygmt.datasets.earth_deflection import load_earth_deflection +from pygmt.datasets.earth_dist import load_earth_dist from pygmt.datasets.earth_free_air_anomaly import load_earth_free_air_anomaly from pygmt.datasets.earth_geoid import load_earth_geoid from pygmt.datasets.earth_magnetic_anomaly import load_earth_magnetic_anomaly from pygmt.datasets.earth_mask import load_earth_mask +from pygmt.datasets.earth_mean_dynamic_topography import ( + load_earth_mean_dynamic_topography, +) +from pygmt.datasets.earth_mean_sea_surface import load_earth_mean_sea_surface from pygmt.datasets.earth_night import load_black_marble from pygmt.datasets.earth_relief import load_earth_relief from pygmt.datasets.earth_vertical_gravity_gradient import ( diff --git a/pygmt/datasets/earth_deflection.py b/pygmt/datasets/earth_deflection.py new file mode 100644 index 00000000000..c0a9cbf406f --- /dev/null +++ b/pygmt/datasets/earth_deflection.py @@ -0,0 +1,119 @@ +""" +Function to download the IGPP Earth east-west and north-south deflection datasets from +the GMT data server, and load as :class:`xarray.DataArray`. + +The grids are available in various resolutions. +""" + +from collections.abc import Sequence +from typing import Literal + +import xarray as xr +from pygmt.datasets.load_remote_dataset import _load_remote_dataset + +__doctest_skip__ = ["load_earth_deflection"] + + +def load_earth_deflection( + resolution: Literal[ + "01d", "30m", "20m", "15m", "10m", "06m", "05m", "04m", "03m", "02m", "01m" + ] = "01d", + region: Sequence[float] | str | None = None, + registration: Literal["gridline", "pixel", None] = None, + component: Literal["east", "north"] = "east", +) -> xr.DataArray: + r""" + Load the IGPP Earth east-west and north-south deflection datasets in various + resolutions. + + .. list-table:: + :widths: 50 50 + :header-rows: 1 + + * - IGPP Earth east-west deflection + - IGPP Earth north-south deflection + * - .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_edefl.jpg + - .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_ndefl.jpg + + The grids are downloaded to a user data directory (usually + ``~/.gmt/server/earth/earth_edefl/`` and ``~/.gmt/server/earth/earth_ndefl/`` the + first time you invoke this function. Afterwards, it will load the grid from the + data directory. So you'll need an internet connection the first time around. + + These grids can also be accessed by passing in the file name + **@**\ *earth_defl_type*\_\ *res*\[_\ *reg*] to any grid processing function or + plotting method. *earth_defl_type* is the GMT name for the dataset. The available + options are **earth_edefl** and **earth_ndefl**. *res* is the grid resolution (see + below), and *reg* is the grid registration type (**p** for pixel registration or + **g** for gridline registration). + + The default color palette table (CPTs) for this dataset is *@earth_defl.cpt*. It's + implicitly used when passing in the file name of the dataset to any grid plotting + method if no CPT is explicitly specified. When the dataset is loaded and plotted as + an :class:`xarray.DataArray` object, the default CPT is ignored, and GMT's default + CPT (*turbo*) is used. To use the dataset-specific CPT, you need to explicitly set + ``cmap="@earth_defl.cpt"``. + + Refer to :gmt-datasets:`earth-edefl.html` and :gmt-datasets:`earth-ndefl.html` for + more details about available datasets, including version information and references. + + Parameters + ---------- + resolution + The grid resolution. The suffix ``d`` and ``m`` stand for arc-degrees and + arc-minutes. + region + The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, + *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions + higher than 5 arc-minutes (i.e., ``"05m"``). + registration + Grid registration type. Either ``"pixel"`` for pixel registration or + ``"gridline"`` for gridline registration. Default is ``None``, which means + ``"gridline"`` for all resolutions except ``"01m"`` which is ``"pixel"`` only. + component + By default, the east-west deflection (``component="east"``) is returned, + set ``component="north"`` to return the north-south deflection. + + Returns + ------- + grid + The Earth east-west or north-south deflection grid. Coordinates are latitude + and longitude in degrees. Deflection values are in micro-radians, where + positive (negative) values indicate a deflection to the east or north (west + or south). + + Note + ---- + The registration and coordinate system type of the returned + :class:`xarray.DataArray` grid can be accessed via the GMT accessors (i.e., + ``grid.gmt.registration`` and ``grid.gmt.gtype`` respectively). However, these + properties may be lost after specific grid operations (such as slicing) and will + need to be manually set before passing the grid to any PyGMT data processing or + plotting functions. Refer to :class:`pygmt.GMTDataArrayAccessor` for detailed + explanations and workarounds. + + Examples + -------- + + >>> from pygmt.datasets import load_earth_deflection + >>> # load the default grid for east-west deflection (gridline-registered + >>> # 1 arc-degree grid) + >>> grid = load_earth_deflection() + >>> # load the default grid for north-south deflection + >>> grid = load_earth_deflection(component="north") + >>> # load the 30 arc-minutes grid with "gridline" registration + >>> grid = load_earth_deflection(resolution="30m", registration="gridline") + >>> # load high-resolution (5 arc-minutes) grid for a specific region + >>> grid = load_earth_deflection( + ... resolution="05m", region=[120, 160, 30, 60], registration="gridline" + ... ) + """ + prefix = "earth_ndefl" if component == "north" else "earth_edefl" + grid = _load_remote_dataset( + name=prefix, + prefix=prefix, + resolution=resolution, + region=region, + registration=registration, + ) + return grid diff --git a/pygmt/datasets/earth_dist.py b/pygmt/datasets/earth_dist.py new file mode 100644 index 00000000000..4897c475b43 --- /dev/null +++ b/pygmt/datasets/earth_dist.py @@ -0,0 +1,105 @@ +""" +Function to download the GSHHG Earth distance to shoreline dataset from the GMT data +server, and load as :class:`xarray.DataArray`. + +The grids are available in various resolutions. +""" + +from collections.abc import Sequence +from typing import Literal + +import xarray as xr +from pygmt.datasets.load_remote_dataset import _load_remote_dataset + +__doctest_skip__ = ["load_earth_dist"] + + +def load_earth_dist( + resolution: Literal[ + "01d", "30m", "20m", "15m", "10m", "06m", "05m", "04m", "03m", "02m", "01m" + ] = "01d", + region: Sequence[float] | str | None = None, + registration: Literal["gridline", "pixel"] = "gridline", +) -> xr.DataArray: + r""" + Load the GSHHG Earth distance to shoreline dataset in various resolutions. + + .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_dist.jpg + :width: 80 % + :align: center + + GSHHG Earth distance to shoreline dataset. + + The grids are downloaded to a user data directory (usually + ``~/.gmt/server/earth/earth_dist/``) the first time you invoke this function. + Afterwards, it will load the grid from the data directory. So you'll need an + internet connection the first time around. + + These grids can also be accessed by passing in the file name + **@earth_dist**\_\ *res*\[_\ *reg*] to any grid processing function or plotting + method. *res* is the grid resolution (see below), and *reg* is the grid registration + type (**p** for pixel registration or **g** for gridline registration). + + The default color palette table (CPT) for this dataset is *@earth_dist.cpt*. It's + implicitly used when passing in the file name of the dataset to any grid plotting + method if no CPT is explicitly specified. When the dataset is loaded and plotted + as an :class:`xarray.DataArray` object, the default CPT is ignored, and GMT's + default CPT (*turbo*) is used. To use the dataset-specific CPT, you need to + explicitly set ``cmap="@earth_dist.cpt"``. + + Refer to :gmt-datasets:`earth-dist.html` for more details about available datasets, + including version information and references. + + Parameters + ---------- + resolution + The grid resolution. The suffix ``d`` and ``m`` stand for arc-degrees and + arc-minutes. + region + The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, + *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions + higher than 5 arc-minutes (i.e., ``"05m"``). + registration + Grid registration type. Either ``"pixel"`` for pixel registration or + ``"gridline"`` for gridline registration. + + Returns + ------- + grid + The GSHHG Earth distance to shoreline grid. Coordinates are latitude and + longitude in degrees. Distances are in kilometers, where positive (negative) + values mean land to coastline (ocean to coastline). + + Note + ---- + The registration and coordinate system type of the returned + :class:`xarray.DataArray` grid can be accessed via the GMT accessors (i.e., + ``grid.gmt.registration`` and ``grid.gmt.gtype`` respectively). However, these + properties may be lost after specific grid operations (such as slicing) and will + need to be manually set before passing the grid to any PyGMT data processing or + plotting functions. Refer to :class:`pygmt.GMTDataArrayAccessor` for detailed + explanations and workarounds. + + Examples + -------- + + >>> from pygmt.datasets import load_earth_dist + >>> # load the default grid (gridline-registered 1 arc-degree grid) + >>> grid = load_earth_dist() + >>> # load the 30 arc-minutes grid with "gridline" registration + >>> grid = load_earth_dist(resolution="30m", registration="gridline") + >>> # load high-resolution (5 arc-minutes) grid for a specific region + >>> grid = load_earth_dist( + ... resolution="05m", + ... region=[120, 160, 30, 60], + ... registration="gridline", + ... ) + """ + grid = _load_remote_dataset( + name="earth_dist", + prefix="earth_dist", + resolution=resolution, + region=region, + registration=registration, + ) + return grid diff --git a/pygmt/datasets/earth_free_air_anomaly.py b/pygmt/datasets/earth_free_air_anomaly.py index da48977d688..d85911496d6 100644 --- a/pygmt/datasets/earth_free_air_anomaly.py +++ b/pygmt/datasets/earth_free_air_anomaly.py @@ -1,6 +1,6 @@ """ -Function to download the IGPP Earth free-air anomaly dataset from the GMT data server, -and load as :class:`xarray.DataArray`. +Function to download the IGPP Earth free-air anomaly and uncertainty datasets from +the GMT data server, and load as :class:`xarray.DataArray`. The grids are available in various resolutions. """ @@ -20,36 +20,43 @@ def load_earth_free_air_anomaly( ] = "01d", region: Sequence[float] | str | None = None, registration: Literal["gridline", "pixel", None] = None, + uncertainty: bool = False, ) -> xr.DataArray: r""" - Load the IGPP Earth free-air anomaly dataset in various resolutions. + Load the IGPP Earth free-air anomaly and uncertainty datasets in various + resolutions. - .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_faa.jpg - :width: 80 % - :align: center + .. list-table:: + :widths: 50 50 + :header-rows: 1 - IGPP Earth free-air anomaly dataset. + * - IGPP Earth free-air anomaly + - IGPP Earth free-air anomaly uncertainty + * - .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_faa.jpg + - .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_faaerror.jpg - The grids are downloaded to a user data directory - (usually ``~/.gmt/server/earth/earth_faa/``) the first time you invoke - this function. Afterwards, it will load the grid from the data directory. - So you'll need an internet connection the first time around. + The grids are downloaded to a user data directory (usually + ``~/.gmt/server/earth/earth_faa/`` or ``~/.gmt/server/earth/earth_faaerror/``) the + first time you invoke this function. Afterwards, it will load the grid from data + directory. So you'll need an internet connection the first time around. These grids can also be accessed by passing in the file name - **@earth_faa**\_\ *res*\[_\ *reg*] to any grid processing function or - plotting method. *res* is the grid resolution (see below), and *reg* is - the grid registration type (**p** for pixel registration or **g** for - gridline registration). - - The default color palette table (CPT) for this dataset is *@earth_faa.cpt*. - It's implicitly used when passing in the file name of the dataset to any - grid plotting method if no CPT is explicitly specified. When the dataset - is loaded and plotted as an :class:`xarray.DataArray` object, the default - CPT is ignored, and GMT's default CPT (*turbo*) is used. To use the - dataset-specific CPT, you need to explicitly set ``cmap="@earth_faa.cpt"``. - - Refer to :gmt-datasets:`earth-faa.html` for more details about available - datasets, including version information and references. + **@earth_faa_type**\_\ *res*\[_\ *reg*] to any grid processing function or + plotting method. *earth_faa_type* is the GMT name for the dataset. The available + options are **earth_faa** and **earth_faaerror**. *res* is the grid resolution (see + below), and *reg* is the grid registration type (**p** for pixel registration or + **g** for gridline registration). + + The default color palette tables (CPTs) for these datasets are *@earth_faa.cpt* and + *@earth_faaerror.cpt*. The dataset-specific CPT is implicitly used when passing in + the file name of the dataset to any grid plotting method if no CPT is explicitly + specified. When the dataset is loaded and plotted as an :class:`xarray.DataArray` + object, the default CPT is ignored, and GMT's default CPT (*turbo*) is used. To use + the dataset-specific CPT, you need to explicitly set ``cmap="@earth_faa.cpt"`` or + ``cmap="@earth_faaerror.cpt"``. + + Refer to :gmt-datasets:`earth-faa.html` and :gmt-datasets:`earth-faaerror.html` for + more details about available datasets, including version information and references. Parameters ---------- @@ -62,26 +69,28 @@ def load_earth_free_air_anomaly( higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means - ``"gridline"`` for all resolutions except ``"01m"`` which is - ``"pixel"`` only. + ``"gridline"`` for gridline registration. Default is ``None``, which means + ``"gridline"`` for all resolutions except ``"01m"`` which is ``"pixel"`` + only. + uncertainty + By default, the Earth free-air anomaly values are returned. Set to ``True`` to + return the related uncertainties instead. Returns ------- grid - The Earth free-air anomaly grid. Coordinates are latitude and - longitude in degrees. Units are in mGal. + The Earth free-air anomaly (uncertainty) grid. Coordinates are latitude and + longitude in degrees. Values and uncertainties are in mGal. Note ---- The registration and coordinate system type of the returned - :class:`xarray.DataArray` grid can be accessed via the GMT accessors - (i.e., ``grid.gmt.registration`` and ``grid.gmt.gtype`` respectively). - However, these properties may be lost after specific grid operations (such - as slicing) and will need to be manually set before passing the grid to any - PyGMT data processing or plotting functions. Refer to - :class:`pygmt.GMTDataArrayAccessor` for detailed explanations and - workarounds. + :class:`xarray.DataArray` grid can be accessed via the GMT accessors (i.e., + ``grid.gmt.registration`` and ``grid.gmt.gtype`` respectively). However, these + properties may be lost after specific grid operations (such as slicing) and will + need to be manually set before passing the grid to any PyGMT data processing or + plotting functions. Refer to :class:`pygmt.GMTDataArrayAccessor` for detailed + explanations and workarounds. Examples -------- @@ -89,18 +98,19 @@ def load_earth_free_air_anomaly( >>> from pygmt.datasets import load_earth_free_air_anomaly >>> # load the default grid (gridline-registered 1 arc-degree grid) >>> grid = load_earth_free_air_anomaly() + >>> # load the uncertainties related to the default grid + >>> grid = load_earth_free_air_anomaly(uncertainty=True) >>> # load the 30 arc-minutes grid with "gridline" registration >>> grid = load_earth_free_air_anomaly(resolution="30m", registration="gridline") >>> # load high-resolution (5 arc-minutes) grid for a specific region >>> grid = load_earth_free_air_anomaly( - ... resolution="05m", - ... region=[120, 160, 30, 60], - ... registration="gridline", + ... resolution="05m", region=[120, 160, 30, 60], registration="gridline" ... ) """ + prefix = "earth_faaerror" if uncertainty is True else "earth_faa" grid = _load_remote_dataset( - name="earth_faa", - prefix="earth_faa", + name=prefix, + prefix=prefix, resolution=resolution, region=region, registration=registration, diff --git a/pygmt/datasets/earth_magnetic_anomaly.py b/pygmt/datasets/earth_magnetic_anomaly.py index 70ffcf54248..463f7cf93b9 100644 --- a/pygmt/datasets/earth_magnetic_anomaly.py +++ b/pygmt/datasets/earth_magnetic_anomaly.py @@ -76,10 +76,10 @@ def load_earth_magnetic_anomaly( higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means + ``"gridline"`` for gridline registration. Default is ``None``, which means ``"gridline"`` for all resolutions except ``"02m"`` for - ``data_source="emag2"`` or ``data_source="emag2_4km"``, which are - ``"pixel"`` only. + ``data_source="emag2"`` or ``data_source="emag2_4km"``, which are ``"pixel"`` + only. data_source Select the source of the magnetic anomaly data. Available options are: diff --git a/pygmt/datasets/earth_mean_dynamic_topography.py b/pygmt/datasets/earth_mean_dynamic_topography.py new file mode 100644 index 00000000000..4ca50e476be --- /dev/null +++ b/pygmt/datasets/earth_mean_dynamic_topography.py @@ -0,0 +1,103 @@ +""" +Function to download the CNES Earth mean dynamic topography dataset from the GMT data +server, and load as :class:`xarray.DataArray`. + +The grids are available in various resolutions. +""" + +from collections.abc import Sequence +from typing import Literal + +import xarray as xr +from pygmt.datasets.load_remote_dataset import _load_remote_dataset + +__doctest_skip__ = ["load_earth_mean_dynamic_topography"] + + +def load_earth_mean_dynamic_topography( + resolution: Literal["01d", "30m", "20m", "15m", "10m", "07m"] = "01d", + region: Sequence[float] | str | None = None, + registration: Literal["gridline", "pixel"] = "gridline", +) -> xr.DataArray: + r""" + Load the CNES Earth mean dynamic topography dataset in various resolutions. + + .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_mdt.jpg + :width: 80 % + :align: center + + CNES Earth mean dynamic topography dataset. + + The grids are downloaded to a user data directory (usually + ``~/.gmt/server/earth/earth_mdt/``) the first time you invoke this function. + Afterwards, it will load the grid from the data directory. So you'll need an + internet connection the first time around. + + These grids can also be accessed by passing in the file name + **@earth_mdt**\_\ *res*\[_\ *reg*] to any grid processing function or plotting + method. *res* is the grid resolution (see below), and *reg* is the grid registration + type (**p** for pixel registration or **g** for gridline registration). + + The default color palette table (CPT) for this dataset is *@earth_mdt.cpt*. It's + implicitly used when passing in the file name of the dataset to any grid plotting + method if no CPT is explicitly specified. When the dataset is loaded and plotted + as an :class:`xarray.DataArray` object, the default CPT is ignored, and GMT's + default CPT (*turbo*) is used. To use the dataset-specific CPT, you need to + explicitly set ``cmap="@earth_mdt.cpt"``. + + Refer to :gmt-datasets:`earth-mdt.html` for more details about available datasets, + including version information and references. + + Parameters + ---------- + resolution + The grid resolution. The suffix ``d`` and ``m`` stand for arc-degrees and + arc-minutes. Note that ``"07m"`` refers to a resolution of 7.5 arc-minutes. + region + The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, + *ymin*, *ymax*] or an ISO country code. + registration + Grid registration type. Either ``"pixel"`` for pixel registration or + ``"gridline"`` for gridline registration. + + Returns + ------- + grid + The CNES Earth mean dynamic topography grid. Coordinates are latitude and + longitude in degrees. Values are in meters. + + Note + ---- + The registration and coordinate system type of the returned + :class:`xarray.DataArray` grid can be accessed via the GMT accessors (i.e., + ``grid.gmt.registration`` and ``grid.gmt.gtype`` respectively). However, these + properties may be lost after specific grid operations (such as slicing) and will + need to be manually set before passing the grid to any PyGMT data processing or + plotting functions. Refer to :class:`pygmt.GMTDataArrayAccessor` for detailed + explanations and workarounds. + + Examples + -------- + + >>> from pygmt.datasets import load_earth_mean_dynamic_topography + >>> # load the default grid (gridline-registered 1 arc-degree grid) + >>> grid = load_earth_mean_dynamic_topography() + >>> # load the 30 arc-minutes grid with "gridline" registration + >>> grid = load_earth_mean_dynamic_topography( + ... resolution="30m", registration="gridline" + ... ) + >>> # load high-resolution (7 arc-minutes) grid for a specific region + >>> grid = load_earth_mean_dynamic_topography( + ... resolution="07m", + ... region=[120, 160, 30, 60], + ... registration="gridline", + ... ) + """ + grid = _load_remote_dataset( + name="earth_mdt", + prefix="earth_mdt", + resolution=resolution, + region=region, + registration=registration, + ) + return grid diff --git a/pygmt/datasets/earth_mean_sea_surface.py b/pygmt/datasets/earth_mean_sea_surface.py new file mode 100644 index 00000000000..f4856d98626 --- /dev/null +++ b/pygmt/datasets/earth_mean_sea_surface.py @@ -0,0 +1,104 @@ +""" +Function to download the CNES Earth mean sea surface dataset from the GMT data +server, and load as :class:`xarray.DataArray`. + +The grids are available in various resolutions. +""" + +from collections.abc import Sequence +from typing import Literal + +import xarray as xr +from pygmt.datasets.load_remote_dataset import _load_remote_dataset + +__doctest_skip__ = ["load_earth_mean_sea_surface"] + + +def load_earth_mean_sea_surface( + resolution: Literal[ + "01d", "30m", "20m", "15m", "10m", "06m", "05m", "04m", "03m", "02m", "01m" + ] = "01d", + region: Sequence[float] | str | None = None, + registration: Literal["gridline", "pixel"] = "gridline", +) -> xr.DataArray: + r""" + Load the CNES Earth mean sea surface dataset in various resolutions. + + .. figure:: https://www.generic-mapping-tools.org/remote-datasets/_images/GMT_earth_mss.jpg + :width: 80 % + :align: center + + CNES Earth mean sea surface dataset. + + The grids are downloaded to a user data directory (usually + ``~/.gmt/server/earth/earth_mss/``) the first time you invoke this function. + Afterwards, it will load the grid from the data directory. So you'll need an + internet connection the first time around. + + These grids can also be accessed by passing in the file name + **@earth_mss**\_\ *res*\[_\ *reg*] to any grid processing function or plotting + method. *res* is the grid resolution (see below), and *reg* is the grid registration + type (**p** for pixel registration or **g** for gridline registration). + + The default color palette table (CPT) for this dataset is *@earth_mss.cpt*. It's + implicitly used when passing in the file name of the dataset to any grid plotting + method if no CPT is explicitly specified. When the dataset is loaded and plotted + as an :class:`xarray.DataArray` object, the default CPT is ignored, and GMT's + default CPT (*turbo*) is used. To use the dataset-specific CPT, you need to + explicitly set ``cmap="@earth_mss.cpt"``. + + Refer to :gmt-datasets:`earth-mss.html` for more details about available datasets, + including version information and references. + + Parameters + ---------- + resolution + The grid resolution. The suffix ``d`` and ``m`` stand for arc-degrees and + arc-minutes. + region + The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, + *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions + higher than 5 arc-minutes (i.e., ``"05m"``). + registration + Grid registration type. Either ``"pixel"`` for pixel registration or + ``"gridline"`` for gridline registration. + + Returns + ------- + grid + The CNES Earth mean sea surface grid. Coordinates are latitude and + longitude in degrees. Values are in meters. + + Note + ---- + The registration and coordinate system type of the returned + :class:`xarray.DataArray` grid can be accessed via the GMT accessors (i.e., + ``grid.gmt.registration`` and ``grid.gmt.gtype`` respectively). However, these + properties may be lost after specific grid operations (such as slicing) and will + need to be manually set before passing the grid to any PyGMT data processing or + plotting functions. Refer to :class:`pygmt.GMTDataArrayAccessor` for detailed + explanations and workarounds. + + Examples + -------- + + >>> from pygmt.datasets import load_earth_mean_sea_surface + >>> # load the default grid (gridline-registered 1 arc-degree grid) + >>> grid = load_earth_mean_sea_surface() + >>> # load the 30 arc-minutes grid with "gridline" registration + >>> grid = load_earth_mean_sea_surface(resolution="30m", registration="gridline") + >>> # load high-resolution (5 arc-minutes) grid for a specific region + >>> grid = load_earth_mean_sea_surface( + ... resolution="05m", + ... region=[120, 160, 30, 60], + ... registration="gridline", + ... ) + """ + grid = _load_remote_dataset( + name="earth_mss", + prefix="earth_mss", + resolution=resolution, + region=region, + registration=registration, + ) + return grid diff --git a/pygmt/datasets/earth_relief.py b/pygmt/datasets/earth_relief.py index 3ff03613491..f0090ca3bd2 100644 --- a/pygmt/datasets/earth_relief.py +++ b/pygmt/datasets/earth_relief.py @@ -83,9 +83,9 @@ def load_earth_relief( higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means - ``"gridline"`` for all resolutions except ``"15s"`` which is - ``"pixel"`` only. + ``"gridline"`` for gridline registration. Default is ``None``, which means + ``"gridline"`` for all resolutions except ``"15s"`` which is ``"pixel"`` + only. data_source Select the source for the Earth relief data. Available options are: diff --git a/pygmt/datasets/earth_vertical_gravity_gradient.py b/pygmt/datasets/earth_vertical_gravity_gradient.py index 2ebba4563bc..bcf00099fc2 100644 --- a/pygmt/datasets/earth_vertical_gravity_gradient.py +++ b/pygmt/datasets/earth_vertical_gravity_gradient.py @@ -62,9 +62,9 @@ def load_earth_vertical_gravity_gradient( higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means - ``"gridline"`` for all resolutions except ``"01m"`` which is - ``"pixel"`` only. + ``"gridline"`` for gridline registration. Default is ``None``, which means + ``"gridline"`` for all resolutions except ``"01m"`` which is ``"pixel"`` + only. Returns ------- diff --git a/pygmt/datasets/load_remote_dataset.py b/pygmt/datasets/load_remote_dataset.py index 168a93583b2..41fe729d0e0 100644 --- a/pygmt/datasets/load_remote_dataset.py +++ b/pygmt/datasets/load_remote_dataset.py @@ -39,7 +39,9 @@ class GMTRemoteDataset(NamedTuple): Attributes ---------- description - The name assigned as an attribute to the DataArray. + The name assigned as an attribute to the DataArray. + kind + The kind of the dataset source. Valid values are ``"grid"`` and ``"image"``. units The units of the values in the DataArray. resolutions @@ -49,6 +51,7 @@ class GMTRemoteDataset(NamedTuple): """ description: str + kind: Literal["grid", "image"] units: str | None resolutions: dict[str, Resolution] extra_attributes: dict[str, Any] @@ -57,6 +60,7 @@ class GMTRemoteDataset(NamedTuple): datasets = { "earth_age": GMTRemoteDataset( description="EarthByte Earth seafloor crustal age", + kind="grid", units="Myr", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -75,6 +79,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_day": GMTRemoteDataset( description="NASA Day Images", + kind="image", units=None, extra_attributes={"long_name": "blue_marble", "horizontal_datum": "WGS84"}, resolutions={ @@ -92,8 +97,66 @@ class GMTRemoteDataset(NamedTuple): "30s": Resolution("30s", registrations=["pixel"]), }, ), + "earth_dist": GMTRemoteDataset( + description="GSHHG Earth distance to shoreline", + kind="grid", + units="kilometers", + extra_attributes={"horizontal_datum": "WGS84"}, + resolutions={ + "01d": Resolution("01d"), + "30m": Resolution("30m"), + "20m": Resolution("20m"), + "15m": Resolution("15m"), + "10m": Resolution("10m"), + "06m": Resolution("06m"), + "05m": Resolution("05m", tiled=True), + "04m": Resolution("04m", tiled=True), + "03m": Resolution("03m", tiled=True), + "02m": Resolution("02m", tiled=True), + "01m": Resolution("01m", registrations=["gridline"], tiled=True), + }, + ), + "earth_edefl": GMTRemoteDataset( + description="IGPP Earth east-west deflection", + kind="grid", + units="micro-radians", + extra_attributes={"horizontal_datum": "WGS84"}, + resolutions={ + "01d": Resolution("01d"), + "30m": Resolution("30m"), + "20m": Resolution("20m"), + "15m": Resolution("15m"), + "10m": Resolution("10m"), + "06m": Resolution("06m"), + "05m": Resolution("05m", tiled=True), + "04m": Resolution("04m", tiled=True), + "03m": Resolution("03m", tiled=True), + "02m": Resolution("02m", tiled=True), + "01m": Resolution("01m", registrations=["pixel"], tiled=True), + }, + ), "earth_faa": GMTRemoteDataset( description="IGPP Earth free-air anomaly", + kind="grid", + units="mGal", + extra_attributes={"horizontal_datum": "WGS84"}, + resolutions={ + "01d": Resolution("01d"), + "30m": Resolution("30m"), + "20m": Resolution("20m"), + "15m": Resolution("15m"), + "10m": Resolution("10m"), + "06m": Resolution("06m"), + "05m": Resolution("05m", tiled=True), + "04m": Resolution("04m", tiled=True), + "03m": Resolution("03m", tiled=True), + "02m": Resolution("02m", tiled=True), + "01m": Resolution("01m", registrations=["pixel"], tiled=True), + }, + ), + "earth_faaerror": GMTRemoteDataset( + description="IGPP Earth free-air anomaly errors", + kind="grid", units="mGal", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -112,6 +175,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_gebco": GMTRemoteDataset( description="GEBCO Earth relief", + kind="grid", units="meters", extra_attributes={"vertical_datum": "EGM96", "horizontal_datum": "WGS84"}, resolutions={ @@ -134,7 +198,8 @@ class GMTRemoteDataset(NamedTuple): ), "earth_geoid": GMTRemoteDataset( description="EGM2008 Earth geoid", - units="m", + kind="grid", + units="meters", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ "01d": Resolution("01d"), @@ -152,6 +217,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_igpp": GMTRemoteDataset( description="IGPP Earth relief", + kind="grid", units="meters", extra_attributes={"vertical_datum": "EGM96", "horizontal_datum": "WGS84"}, resolutions={ @@ -174,6 +240,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_mag": GMTRemoteDataset( description="EMAG2 Earth Magnetic Anomaly Model", + kind="grid", units="nT", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -191,6 +258,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_mask": GMTRemoteDataset( description="GSHHG Earth mask", + kind="grid", units=None, extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -209,8 +277,28 @@ class GMTRemoteDataset(NamedTuple): "15s": Resolution("15s"), }, ), + "earth_mss": GMTRemoteDataset( + description="CNES Earth mean sea surface", + kind="grid", + units="meters", + extra_attributes={"horizontal_datum": "WGS84"}, + resolutions={ + "01d": Resolution("01d"), + "30m": Resolution("30m"), + "20m": Resolution("20m"), + "15m": Resolution("15m"), + "10m": Resolution("10m"), + "06m": Resolution("06m"), + "05m": Resolution("05m", tiled=True), + "04m": Resolution("04m", tiled=True), + "03m": Resolution("03m", tiled=True), + "02m": Resolution("02m", tiled=True), + "01m": Resolution("01m", tiled=True, registrations=["gridline"]), + }, + ), "earth_night": GMTRemoteDataset( description="NASA Night Images", + kind="image", units=None, extra_attributes={"long_name": "black_marble", "horizontal_datum": "WGS84"}, resolutions={ @@ -228,8 +316,42 @@ class GMTRemoteDataset(NamedTuple): "30s": Resolution("30s", registrations=["pixel"]), }, ), + "earth_mdt": GMTRemoteDataset( + description="CNES Earth mean dynamic topography", + kind="grid", + units="meters", + extra_attributes={"horizontal_datum": "WGS84"}, + resolutions={ + "01d": Resolution("01d"), + "30m": Resolution("30m"), + "20m": Resolution("20m"), + "15m": Resolution("15m"), + "10m": Resolution("10m"), + "07m": Resolution("07m", registrations=["gridline"]), + }, + ), + "earth_ndefl": GMTRemoteDataset( + description="IGPP Earth north-south deflection", + kind="grid", + units="micro-radians", + extra_attributes={"horizontal_datum": "WGS84"}, + resolutions={ + "01d": Resolution("01d"), + "30m": Resolution("30m"), + "20m": Resolution("20m"), + "15m": Resolution("15m"), + "10m": Resolution("10m"), + "06m": Resolution("06m"), + "05m": Resolution("05m", tiled=True), + "04m": Resolution("04m", tiled=True), + "03m": Resolution("03m", tiled=True), + "02m": Resolution("02m", tiled=True), + "01m": Resolution("01m", registrations=["pixel"], tiled=True), + }, + ), "earth_vgg": GMTRemoteDataset( description="IGPP Earth vertical gravity gradient", + kind="grid", units="Eotvos", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -248,6 +370,7 @@ class GMTRemoteDataset(NamedTuple): ), "earth_wdmam": GMTRemoteDataset( description="WDMAM World Digital Magnetic Anomaly Map", + kind="grid", units="nT", extra_attributes={"horizontal_datum": "WGS84"}, resolutions={ @@ -264,6 +387,7 @@ class GMTRemoteDataset(NamedTuple): ), "mars_relief": GMTRemoteDataset( description="NASA Mars (MOLA) relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -285,6 +409,7 @@ class GMTRemoteDataset(NamedTuple): ), "moon_relief": GMTRemoteDataset( description="USGS Moon (LOLA) relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -306,6 +431,7 @@ class GMTRemoteDataset(NamedTuple): ), "mercury_relief": GMTRemoteDataset( description="USGS Mercury relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -325,6 +451,7 @@ class GMTRemoteDataset(NamedTuple): ), "pluto_relief": GMTRemoteDataset( description="USGS Pluto relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -344,6 +471,7 @@ class GMTRemoteDataset(NamedTuple): ), "venus_relief": GMTRemoteDataset( description="NASA Magellan Venus relief", + kind="grid", units="meters", extra_attributes={}, resolutions={ @@ -442,15 +570,16 @@ def _load_remote_dataset( raise GMTInvalidInput(msg) fname = f"@{prefix}_{resolution}_{reg}" - kind = "image" if name in {"earth_day", "earth_night"} else "grid" - kwdict = {"R": region, "T": {"grid": "g", "image": "i"}[kind]} + kwdict = {"R": region, "T": {"grid": "g", "image": "i"}[dataset.kind]} with Session() as lib: - with lib.virtualfile_out(kind=kind) as voutgrd: + with lib.virtualfile_out(kind=dataset.kind) as voutgrd: lib.call_module( module="read", args=[fname, voutgrd, *build_arg_list(kwdict)], ) - grid = lib.virtualfile_to_raster(kind=kind, outgrid=None, vfname=voutgrd) + grid = lib.virtualfile_to_raster( + kind=dataset.kind, outgrid=None, vfname=voutgrd + ) # Full path to the grid if not tiled grids. source = which(fname, download="a") if not resinfo.tiled else None diff --git a/pygmt/datasets/mars_relief.py b/pygmt/datasets/mars_relief.py index 1d2cb631fd9..e04f048d42f 100644 --- a/pygmt/datasets/mars_relief.py +++ b/pygmt/datasets/mars_relief.py @@ -67,14 +67,15 @@ def load_mars_relief( ---------- resolution The grid resolution. The suffix ``d``, ``m`` and ``s`` stand for arc-degrees, - arc-minutes and arc-seconds. + arc-minutes and arc-seconds. Note that ``"12s"`` refers to a resolution of + 12.1468873601 arc-seconds. region The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means + ``"gridline"`` for gridline registration. Default is ``None``, which means ``"gridline"`` for all resolutions except for ``"12s"`` which is ``"pixel"`` only. diff --git a/pygmt/datasets/mercury_relief.py b/pygmt/datasets/mercury_relief.py index f3c360c356a..06da8194e4a 100644 --- a/pygmt/datasets/mercury_relief.py +++ b/pygmt/datasets/mercury_relief.py @@ -65,14 +65,15 @@ def load_mercury_relief( ---------- resolution The grid resolution. The suffix ``d``, ``m`` and ``s`` stand for arc-degrees, - arc-minutes and arc-seconds. + arc-minutes and arc-seconds. Note that ``"56s"`` refers to a resolution of + 56.25 arc-seconds. region The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means + ``"gridline"`` for gridline registration. Default is ``None``, which means ``"gridline"`` for all resolutions except for ``"56s"`` which is ``"pixel"`` only. diff --git a/pygmt/datasets/moon_relief.py b/pygmt/datasets/moon_relief.py index 9daab0f47a5..66817f42d08 100644 --- a/pygmt/datasets/moon_relief.py +++ b/pygmt/datasets/moon_relief.py @@ -67,14 +67,15 @@ def load_moon_relief( ---------- resolution The grid resolution. The suffix ``d``, ``m`` and ``s`` stand for arc-degrees, - arc-minutes and arc-seconds. + arc-minutes and arc-seconds. Note that ``"14s"`` refers to a resolution of + 14.0625 arc-seconds. region The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means + ``"gridline"`` for gridline registration. Default is ``None``, which means ``"gridline"`` for all resolutions except for ``"14s"`` which is ``"pixel"`` only. diff --git a/pygmt/datasets/pluto_relief.py b/pygmt/datasets/pluto_relief.py index 620545899da..9a9998d228a 100644 --- a/pygmt/datasets/pluto_relief.py +++ b/pygmt/datasets/pluto_relief.py @@ -65,14 +65,15 @@ def load_pluto_relief( ---------- resolution The grid resolution. The suffix ``d``, ``m`` and ``s`` stand for arc-degrees, - arc-minutes and arc-seconds. + arc-minutes and arc-seconds. Note that ``"52s"`` refers to a resolution of + 52.0732883317 arc-seconds. region The subregion of the grid to load, in the form of a sequence [*xmin*, *xmax*, *ymin*, *ymax*] or an ISO country code. Required for grids with resolutions higher than 5 arc-minutes (i.e., ``"05m"``). registration Grid registration type. Either ``"pixel"`` for pixel registration or - ``"gridline"`` for gridline registration. Default is ``None``, means + ``"gridline"`` for gridline registration. Default is ``None``, which means ``"gridline"`` for all resolutions except for ``"52s"`` which is ``"pixel"`` only. diff --git a/pygmt/datasets/tile_map.py b/pygmt/datasets/tile_map.py index d18bce423f8..caa18dacd84 100644 --- a/pygmt/datasets/tile_map.py +++ b/pygmt/datasets/tile_map.py @@ -166,6 +166,7 @@ def load_tile_map( "wait": wait, "max_retries": max_retries, } + # TODO(contextily>=1.5.0): Remove the check for the 'zoom_adjust' parameter. if zoom_adjust is not None: if Version(contextily.__version__) < Version("1.5.0"): msg = ( diff --git a/pygmt/encodings.py b/pygmt/encodings.py index 0c7b7ddc895..09c749c4c82 100644 --- a/pygmt/encodings.py +++ b/pygmt/encodings.py @@ -1,3 +1,4 @@ +# noqa: A005 """ Character encodings supported by GMT. diff --git a/pygmt/figure.py b/pygmt/figure.py index 4163ab52eb1..5c5d4734ce6 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -6,7 +6,7 @@ import os from pathlib import Path, PurePath from tempfile import TemporaryDirectory -from typing import Literal +from typing import Literal, overload try: import IPython @@ -95,19 +95,19 @@ class Figure: 122.94, 145.82, 20.53, 45.52 """ - def __init__(self): + def __init__(self) -> None: self._name = unique_name() self._preview_dir = TemporaryDirectory(prefix=f"{self._name}-preview-") self._activate_figure() - def __del__(self): + def __del__(self) -> None: """ Clean up the temporary directory that stores the previews. """ if hasattr(self, "_preview_dir"): self._preview_dir.cleanup() - def _activate_figure(self): + def _activate_figure(self) -> None: """ Start and/or activate the current figure. @@ -144,7 +144,7 @@ def savefig( show: bool = False, worldfile: bool = False, **kwargs, - ): + ) -> None: """ Save the figure to an image file. @@ -248,6 +248,7 @@ def savefig( kwargs.pop("metadata", None) self.psconvert(prefix=prefix, fmt=fmts[ext], crop=crop, **kwargs) + # TODO(GMT>=6.5.0): Remove the workaround for upstream bug in GMT<6.5.0. # Remove the .pgw world file if exists. Not necessary after GMT 6.5.0. # See upstream fix https://github.com/GenericMappingTools/gmt/pull/7865 if ext == "tiff": @@ -267,7 +268,7 @@ def show( width: int = 500, waiting: float = 0.5, **kwargs, - ): + ) -> None: """ Display a preview of the figure. @@ -353,6 +354,14 @@ def show( ) raise GMTInvalidInput(msg) + @overload + def _preview( + self, fmt: str, dpi: int, as_bytes: Literal[True] = True, **kwargs + ) -> bytes: ... + @overload + def _preview( + self, fmt: str, dpi: int, as_bytes: Literal[False] = False, **kwargs + ) -> str: ... def _preview(self, fmt: str, dpi: int, as_bytes: bool = False, **kwargs): """ Grab a preview of the figure. @@ -380,7 +389,7 @@ def _preview(self, fmt: str, dpi: int, as_bytes: bool = False, **kwargs): return fname.read_bytes() return fname - def _repr_png_(self): + def _repr_png_(self) -> bytes: """ Show a PNG preview if the object is returned in an interactive shell. @@ -389,7 +398,7 @@ def _repr_png_(self): png = self._preview(fmt="png", dpi=70, anti_alias=True, as_bytes=True) return png - def _repr_html_(self): + def _repr_html_(self) -> str: """ Show the PNG image embedded in HTML with a controlled width. @@ -409,6 +418,7 @@ def _repr_html_(self): grdimage, grdview, histogram, + hlines, image, inset, legend, @@ -427,11 +437,12 @@ def _repr_html_(self): tilemap, timestamp, velo, + vlines, wiggle, ) -def set_display(method: Literal["external", "notebook", "none", None] = None): +def set_display(method: Literal["external", "notebook", "none", None] = None) -> None: """ Set the display method when calling :meth:`pygmt.Figure.show`. diff --git a/pygmt/helpers/caching.py b/pygmt/helpers/caching.py index 26648b17060..ea6bed8d4cf 100644 --- a/pygmt/helpers/caching.py +++ b/pygmt/helpers/caching.py @@ -5,7 +5,7 @@ from pygmt.src import which -def cache_data(): +def cache_data() -> None: """ Download GMT remote data files used in PyGMT tests and docs to cache folder. """ @@ -14,7 +14,10 @@ def cache_data(): # List of GMT remote datasets. "@earth_age_01d_g", "@earth_day_01d", + "@earth_dist_01d", + "@earth_edefl_01d", "@earth_faa_01d_g", + "@earth_faaerror_01d_g", "@earth_gebco_01d_g", "@earth_gebcosi_01d_g", "@earth_gebcosi_15m_p", @@ -22,6 +25,10 @@ def cache_data(): "@earth_mag_01d_g", "@earth_mag4km_01d_g", "@earth_mask_01d_g", + "@earth_mdt_01d_g", + "@earth_mdt_07m_g", + "@earth_mss_01d_g", + "@earth_ndefl_01d", "@earth_night_01d", "@earth_relief_01d_g", "@earth_relief_01d_p", @@ -45,10 +52,15 @@ def cache_data(): "@N00W030.earth_age_01m_g.nc", "@N30E060.earth_age_01m_g.nc", "@N30E090.earth_age_01m_g.nc", + "@N00W030.earth_dist_01m_g.nc", + "@N00W030.earth_edefl_01m_p.nc", "@N00W030.earth_faa_01m_p.nc", + "@N00W030.earth_faaerror_01m_p.nc", "@N00W030.earth_geoid_01m_g.nc", "@S30W060.earth_mag_02m_p.nc", "@S30W120.earth_mag4km_02m_p.nc", + "@N30E090.earth_mss_01m_g.nc", + "@N30E090.earth_ndefl_01m_p.nc", "@N00W090.earth_relief_03m_p.nc", "@N00E135.earth_relief_30s_g.nc", "@N00W010.earth_relief_15s_p.nc", diff --git a/pygmt/helpers/decorators.py b/pygmt/helpers/decorators.py index a22d49334af..3c4f9dd5510 100644 --- a/pygmt/helpers/decorators.py +++ b/pygmt/helpers/decorators.py @@ -37,17 +37,17 @@ (using ``binary="o"``), where *ncols* is the number of data columns of *type*, which must be one of: - - **c** - int8_t (1-byte signed char) - - **u** - uint8_t (1-byte unsigned char) - - **h** - int16_t (2-byte signed int) - - **H** - uint16_t (2-byte unsigned int) - - **i** - int32_t (4-byte signed int) - - **I** - uint32_t (4-byte unsigned int) - - **l** - int64_t (8-byte signed int) - - **L** - uint64_t (8-byte unsigned int) - - **f** - 4-byte single-precision float - - **d** - 8-byte double-precision float - - **x** - use to skip *ncols* anywhere in the record + - **c**: int8_t (1-byte signed char) + - **u**: uint8_t (1-byte unsigned char) + - **h**: int16_t (2-byte signed int) + - **H**: uint16_t (2-byte unsigned int) + - **i**: int32_t (4-byte signed int) + - **I**: uint32_t (4-byte unsigned int) + - **l**: int64_t (8-byte signed int) + - **L**: uint64_t (8-byte unsigned int) + - **f**: 4-byte single-precision float + - **d**: 8-byte double-precision float + - **x**: use to skip *ncols* anywhere in the record For records with mixed types, append additional comma-separated combinations of *ncols* *type* (no space). The following modifiers @@ -84,9 +84,9 @@ **e**\|\ **f**\|\ **g**. Determine how spherical distances are calculated. - - **e** - Ellipsoidal (or geodesic) mode - - **f** - Flat Earth mode - - **g** - Great circle distance [Default] + - **e**: Ellipsoidal (or geodesic) mode + - **f**: Flat Earth mode + - **g**: Great circle distance [Default] All spherical distance calculations depend on the current ellipsoid (:gmt-term:`PROJ_ELLIPSOID`), the definition of the mean radius @@ -118,16 +118,16 @@ a list with each item containing a string describing one set of criteria. - - **x**\|\ **X** - define a gap when there is a large enough - change in the x coordinates (upper case to use projected + - **x**\|\ **X**: define a gap when there is a large enough + change in the x coordinates (uppercase to use projected coordinates). - - **y**\|\ **Y** - define a gap when there is a large enough - change in the y coordinates (upper case to use projected + - **y**\|\ **Y**: define a gap when there is a large enough + change in the y coordinates (uppercase to use projected coordinates). - - **d**\|\ **D** - define a gap when there is a large enough - distance between coordinates (upper case to use projected + - **d**\|\ **D**: define a gap when there is a large enough + distance between coordinates (uppercase to use projected coordinates). - - **z** - define a gap when there is a large enough change in + - **z**: define a gap when there is a large enough change in the z data. Use **+c**\ *col* to change the z data column [Default *col* is 2 (i.e., 3rd column)]. @@ -146,9 +146,9 @@ One of the following modifiers can be appended: - - **+n** - specify that the previous value minus the current + - **+n**: specify that the previous value minus the current column value must exceed *gap* for a break to be imposed. - - **+p** - specify that the current value minus the previous + - **+p**: specify that the current value minus the previous value must exceed *gap* for a break to be imposed.""", "grid": r""" grid : str or xarray.DataArray @@ -367,13 +367,13 @@ Select verbosity level [Default is **w**], which modulates the messages written to stderr. Choose among 7 levels of verbosity: - - **q** - Quiet, not even fatal error messages are produced - - **e** - Error messages only - - **w** - Warnings [Default] - - **t** - Timings (report runtimes for time-intensive algorithms) - - **i** - Informational messages (same as ``verbose=True``) - - **c** - Compatibility warnings - - **d** - Debugging messages""", + - **q**: Quiet, not even fatal error messages are produced + - **e**: Error messages only + - **w**: Warnings [Default] + - **t**: Timings (report runtimes for time-intensive algorithms) + - **i**: Informational messages (same as ``verbose=True``) + - **c**: Compatibility warnings + - **d**: Debugging messages""", "wrap": r""" wrap : str **y**\|\ **a**\|\ **w**\|\ **d**\|\ **h**\|\ **m**\|\ **s**\|\ @@ -382,14 +382,14 @@ different column if selected via **+c**\ *col*. The following cyclical coordinate transformations are supported: - - **y** - yearly cycle (normalized) - - **a** - annual cycle (monthly) - - **w** - weekly cycle (day) - - **d** - daily cycle (hour) - - **h** - hourly cycle (minute) - - **m** - minute cycle (second) - - **s** - second cycle (second) - - **c** - custom cycle (normalized) + - **y**: yearly cycle (normalized) + - **a**: annual cycle (monthly) + - **w**: weekly cycle (day) + - **d**: daily cycle (hour) + - **h**: hourly cycle (minute) + - **m**: minute cycle (second) + - **s**: second cycle (second) + - **c**: custom cycle (normalized) Full documentation is at :gmt-docs:`gmt.html#w-full`.""", } diff --git a/pygmt/helpers/tempfile.py b/pygmt/helpers/tempfile.py index 191d6bc088c..70cc688156a 100644 --- a/pygmt/helpers/tempfile.py +++ b/pygmt/helpers/tempfile.py @@ -1,3 +1,4 @@ +# noqa: A005 """ Utilities for dealing with temporary file management. """ @@ -59,7 +60,7 @@ class GMTTempFile: [0. 0. 0.] [1. 1. 1.] [2. 2. 2.] """ - def __init__(self, prefix: str = "pygmt-", suffix: str = ".txt"): + def __init__(self, prefix: str = "pygmt-", suffix: str = ".txt") -> None: """ Initialize the object. """ @@ -144,6 +145,8 @@ def tempfile_from_geojson(geojson): # https://github.com/geopandas/geopandas/issues/967#issuecomment-842877704 # https://github.com/GenericMappingTools/pygmt/issues/2497 int32_info = np.iinfo(np.int32) + # TODO(GeoPandas>=1.0): Remove the workaround for GeoPandas < 1. + # The default engine is "fiona" in v0.x and "pyogrio" in v1.x. if Version(gpd.__version__).major < 1: # GeoPandas v0.x # The default engine 'fiona' supports the 'schema' parameter. if geojson.index.name is None: diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py index 32fad37e4ff..e32f5bbe03f 100644 --- a/pygmt/helpers/utils.py +++ b/pygmt/helpers/utils.py @@ -43,7 +43,7 @@ def _validate_data_input( data=None, x=None, y=None, z=None, required_z=False, required_data=True, kind=None -): +) -> None: """ Check if the combination of data/x/y/z is valid. @@ -552,7 +552,7 @@ def is_nonstr_iter(value): return isinstance(value, Iterable) and not isinstance(value, str) -def launch_external_viewer(fname: str, waiting: float = 0): +def launch_external_viewer(fname: str, waiting: float = 0) -> None: """ Open a file in an external viewer program. @@ -574,9 +574,8 @@ def launch_external_viewer(fname: str, waiting: float = 0): } match sys.platform: - case name if ( - (name == "linux" or name.startswith("freebsd")) - and (xdgopen := shutil.which("xdg-open")) + case name if (name == "linux" or name.startswith("freebsd")) and ( + xdgopen := shutil.which("xdg-open") ): # Linux/FreeBSD subprocess.run([xdgopen, fname], check=False, **run_args) # type:ignore[call-overload] case "darwin": # macOS diff --git a/pygmt/io.py b/pygmt/io.py index 9451de36c8f..a4ba289c7d9 100644 --- a/pygmt/io.py +++ b/pygmt/io.py @@ -1,3 +1,4 @@ +# noqa: A005 """ PyGMT input/output (I/O) utilities. """ diff --git a/pygmt/session_management.py b/pygmt/session_management.py index be3cff9539c..ac18218c858 100644 --- a/pygmt/session_management.py +++ b/pygmt/session_management.py @@ -9,7 +9,7 @@ from pygmt.helpers import unique_name -def begin(): +def begin() -> None: """ Initiate a new GMT modern mode session. @@ -28,7 +28,7 @@ def begin(): lib.call_module(module="set", args=["GMT_COMPATIBILITY=6"]) -def end(): +def end() -> None: """ Terminate the GMT modern mode session created by :func:`pygmt.begin`. diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index e4db7321963..8905124f917 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -29,6 +29,7 @@ from pygmt.src.grdview import grdview from pygmt.src.grdvolume import grdvolume from pygmt.src.histogram import histogram +from pygmt.src.hlines import hlines from pygmt.src.image import image from pygmt.src.info import info from pygmt.src.inset import inset @@ -56,6 +57,7 @@ from pygmt.src.timestamp import timestamp from pygmt.src.triangulate import triangulate from pygmt.src.velo import velo +from pygmt.src.vlines import vlines from pygmt.src.which import which from pygmt.src.wiggle import wiggle from pygmt.src.x2sys_cross import x2sys_cross diff --git a/pygmt/src/filter1d.py b/pygmt/src/filter1d.py index 206c3a2fd48..6d0e97938cf 100644 --- a/pygmt/src/filter1d.py +++ b/pygmt/src/filter1d.py @@ -75,7 +75,7 @@ def filter1d( - **u**: upper (absolute). Return maximum of all values. - **U**: upper. Return maximum of all negative values only. - Upper case type **B**, **C**, **G**, **M**, **P**, **F** will use + Uppercase type **B**, **C**, **G**, **M**, **P**, **F** will use robust filter versions: i.e., replace outliers (2.5 L1 scale off median, using 1.4826 \* median absolute deviation [MAD]) with median during filtering. diff --git a/pygmt/src/grd2xyz.py b/pygmt/src/grd2xyz.py index a44dc996c6d..b31d0013a25 100644 --- a/pygmt/src/grd2xyz.py +++ b/pygmt/src/grd2xyz.py @@ -145,8 +145,7 @@ def grd2xyz( if kwargs.get("o") is not None and output_type == "pandas": msg = ( - "If 'outcols' is specified, 'output_type' must be either 'numpy' " - "or 'file'." + "If 'outcols' is specified, 'output_type' must be either 'numpy' or 'file'." ) raise GMTInvalidInput(msg) # Set the default column names for the pandas DataFrame header. diff --git a/pygmt/src/grdfilter.py b/pygmt/src/grdfilter.py index 1ad044d0b5e..786e280dd61 100644 --- a/pygmt/src/grdfilter.py +++ b/pygmt/src/grdfilter.py @@ -48,13 +48,13 @@ def grdfilter(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | None [/*width2*\][*modifiers*]. Name of the filter type you wish to apply, followed by the *width*: - - **b** - Box Car - - **c** - Cosine Arch - - **g** - Gaussian - - **o** - Operator - - **m** - Median - - **p** - Maximum Likelihood probability - - **h** - Histogram + - **b**: Box Car + - **c**: Cosine Arch + - **g**: Gaussian + - **o**: Operator + - **m**: Median + - **p**: Maximum Likelihood probability + - **h**: Histogram distance : str State how the grid (x,y) relates to the filter *width*: diff --git a/pygmt/src/grdgradient.py b/pygmt/src/grdgradient.py index a793be04df0..96c4c61937f 100644 --- a/pygmt/src/grdgradient.py +++ b/pygmt/src/grdgradient.py @@ -69,11 +69,11 @@ def grdgradient(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | No Find the direction of the positive (up-slope) gradient of the data. The following options are supported: - - **a** - Find the aspect (i.e., the down-slope direction) - - **c** - Use the conventional Cartesian angles measured + - **a**: Find the aspect (i.e., the down-slope direction) + - **c**: Use the conventional Cartesian angles measured counterclockwise from the positive x (east) direction. - - **o** - Report orientations (0-180) rather than directions (0-360). - - **n** - Add 90 degrees to all angles (e.g., to give local strikes of + - **o**: Report orientations (0-180) rather than directions (0-360). + - **n**: Add 90 degrees to all angles (e.g., to give local strikes of the surface). radiance : str or list [**m**\|\ **s**\|\ **p**]\ *azim/elev*\ [**+a**\ *ambient*][**+d**\ @@ -102,14 +102,14 @@ def grdgradient(grid, outgrid: str | None = None, **kwargs) -> xr.DataArray | No given, it is set to the average of :math:`g`. The following forms are supported: - - **True** - Normalize using :math:`g_n = \mbox{{amp}}\ + - **True**: Normalize using :math:`g_n = \mbox{{amp}}\ (\frac{{g - \mbox{{offset}}}}{{max(|g - \mbox{{offset}}|)}})` - - **e** - Normalize using a cumulative Laplace distribution yielding: + - **e**: Normalize using a cumulative Laplace distribution yielding: :math:`g_n = \mbox{{amp}}(1 - \ \exp{{(\sqrt{{2}}\frac{{g - \mbox{{offset}}}}{{\sigma}}))}}`, where :math:`\sigma` is estimated using the L1 norm of :math:`(g - \mbox{{offset}})` if it is not given. - - **t** - Normalize using a cumulative Cauchy distribution yielding: + - **t**: Normalize using a cumulative Cauchy distribution yielding: :math:`g_n = \ \frac{{2(\mbox{{amp}})}}{{\pi}}(\tan^{{-1}}(\frac{{g - \ \mbox{{offset}}}}{{\sigma}}))` where :math:`\sigma` is estimated diff --git a/pygmt/src/grdview.py b/pygmt/src/grdview.py index ba7341046e0..77ace573a30 100644 --- a/pygmt/src/grdview.py +++ b/pygmt/src/grdview.py @@ -76,12 +76,12 @@ def grdview(self, grid, **kwargs): Specify cover type of the grid. Select one of following settings: - - **m** - mesh plot [Default]. - - **mx** or **my** - waterfall plots (row or column profiles). - - **s** - surface plot, and optionally append **m** to have mesh lines + - **m**: mesh plot [Default]. + - **mx** or **my**: waterfall plots (row or column profiles). + - **s**: surface plot, and optionally append **m** to have mesh lines drawn on top of the surface. - - **i** - image plot. - - **c** - Same as **i** but will make nodes with z = NaN transparent. + - **i**: image plot. + - **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**. diff --git a/pygmt/src/hlines.py b/pygmt/src/hlines.py new file mode 100644 index 00000000000..b277358d981 --- /dev/null +++ b/pygmt/src/hlines.py @@ -0,0 +1,137 @@ +""" +hlines - Plot horizontal lines. +""" + +from collections.abc import Sequence + +import numpy as np +from pygmt.exceptions import GMTInvalidInput + +__doctest_skip__ = ["hlines"] + + +def hlines( + self, + y: float | Sequence[float], + xmin: float | Sequence[float] | None = None, + xmax: float | Sequence[float] | None = None, + pen: str | None = None, + label: str | None = None, + no_clip: bool = False, + perspective: str | bool | None = None, +): + """ + Plot one or multiple horizontal line(s). + + This method is a high-level wrapper around :meth:`pygmt.Figure.plot` that focuses on + plotting horizontal lines at Y-coordinates specified by the ``y`` parameter. The + ``y`` parameter can be a single value (for a single horizontal line) or a sequence + of values (for multiple horizontal lines). + + By default, the X-coordinates of the start and end points of the lines are set to + be the X-limits of the current plot, but this can be overridden by specifying the + ``xmin`` and ``xmax`` parameters. ``xmin`` and ``xmax`` can be either a single + value or a sequence of values. If a single value is provided, it is applied to all + lines. If a sequence is provided, the length of ``xmin`` and ``xmax`` must match + the length of ``y``. + + The term "horizontal" lines can be interpreted differently in different coordinate + systems: + + - **Cartesian** coordinate system: lines are plotted as straight lines. + - **Polar** projection: lines are plotted as arcs along a constant radius. + - **Geographic** projection: lines are plotted as parallels along constant latitude. + + Parameters + ---------- + y + Y-coordinates to plot the lines. It can be a single value (for a single line) + or a sequence of values (for multiple lines). + xmin/xmax + X-coordinates of the start/end point(s) of the line(s). If ``None``, defaults to + the X-limits of the current plot. ``xmin`` and ``xmax`` can be either a single + value or a sequence of values. If a single value is provided, it is applied to + all lines. If a sequence is provided, the length of ``xmin`` and ``xmax`` must + match the length of ``y``. + pen + Pen attributes for the line(s), in the format of *width,color,style*. + label + Label for the line(s), to be displayed in the legend. + no_clip + If ``True``, do not clip lines outside the plot region. Only makes sense in the + Cartesian coordinate system. + perspective + Select perspective view and set the azimuth and elevation angle of the + viewpoint. Refer to :meth:`pygmt.Figure.plot` for details. + + Examples + -------- + >>> import pygmt + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + >>> fig.hlines(y=1, pen="1p,black", label="Line at y=1") + >>> fig.hlines(y=2, xmin=2, xmax=8, pen="1p,red,-", label="Line at y=2") + >>> fig.hlines(y=[3, 4], xmin=3, xmax=7, pen="1p,black,.", label="Lines at y=3,4") + >>> fig.hlines(y=[5, 6], xmin=4, xmax=9, pen="1p,red", label="Lines at y=5,6") + >>> fig.hlines( + ... y=[7, 8], xmin=[0, 1], xmax=[7, 8], pen="1p,blue", label="Lines at y=7,8" + ... ) + >>> fig.legend() + >>> fig.show() + """ + self._preprocess() + + # Determine the x limits from the current plot region if not specified. + if xmin is None or xmax is None: + xlimits = self.region[:2] + if xmin is None: + xmin = xlimits[0] + if xmax is None: + xmax = xlimits[1] + + # Ensure y/xmin/xmax are 1-D arrays. + _y = np.atleast_1d(y) + _xmin = np.atleast_1d(xmin) + _xmax = np.atleast_1d(xmax) + + nlines = len(_y) # Number of lines to plot. + + # Check if xmin/xmax are scalars or have the expected length. + if _xmin.size not in {1, nlines} or _xmax.size not in {1, nlines}: + msg = ( + f"'xmin' and 'xmax' are expected to be scalars or have lengths '{nlines}', " + f"but lengths '{_xmin.size}' and '{_xmax.size}' are given." + ) + raise GMTInvalidInput(msg) + + # Repeat xmin/xmax to match the length of y if they are scalars. + if nlines != 1: + if _xmin.size == 1: + _xmin = np.repeat(_xmin, nlines) + if _xmax.size == 1: + _xmax = np.repeat(_xmax, nlines) + + # Call the Figure.plot method to plot the lines. + for i in range(nlines): + # Special handling for label. + # 1. Only specify a label when plotting the first line. + # 2. The -l option can accept comma-separated labels for labeling multiple lines + # with auto-coloring enabled. We don't need this feature here, so we need to + # replace comma with \054 if the label contains commas. + _label = label.replace(",", "\\054") if label and i == 0 else None + + # By default, points are connected as great circle arcs in geographic coordinate + # systems and straight lines in Cartesian coordinate systems (including polar + # projection). To plot "horizontal" lines along constant latitude (in geographic + # coordinate systems) or constant radius (in polar projection), we need to + # resample the line to at least 4 points. + npoints = 4 # 2 for Cartesian, at least 4 for geographic and polar projections. + self.plot( + x=np.linspace(_xmin[i], _xmax[i], npoints), + y=[_y[i]] * npoints, + pen=pen, + label=_label, + no_clip=no_clip, + perspective=perspective, + straight_line="x", + ) diff --git a/pygmt/src/meca.py b/pygmt/src/meca.py index 75b69d2facb..ba6de1f7868 100644 --- a/pygmt/src/meca.py +++ b/pygmt/src/meca.py @@ -227,169 +227,145 @@ def meca( # noqa: PLR0912, PLR0913, PLR0915 Parameters ---------- - spec : str, 1-D array, 2-D array, dict, or :class:`pandas.DataFrame` + spec : str, 1-D numpy array, 2-D numpy array, dict, or pandas.DataFrame Data that contain focal mechanism parameters. ``spec`` can be specified in either of the following types: - - *str*: a file name containing focal mechanism parameters as - columns. The meaning of each column is: + - *str*: a file name containing focal mechanism parameters as columns. The + meaning of each column is: - Columns 1 and 2: event longitude and latitude - - Column 3: event depth (in km) - - Columns 4 to 3+n: focal mechanism parameters. The number of columns - *n* depends on the choice of ``convention``, which will be - described below. - - Columns 4+n and 5+n: longitude, latitude at which to place - beachball. Using ``0 0`` will plot the beachball at the longitude, - latitude given in columns 1 and 2. [optional and requires - ``offset=True`` to take effect]. - - Text string to appear near the beachball [optional]. - - - *1-D array*: focal mechanism parameters of a single event. + - Column 3: event depth (in kilometers) + - Columns 4 to 3+n: focal mechanism parameters. The number of columns *n* + depends on the choice of ``convention``, which is described below. + - Columns 4+n and 5+n: longitude and latitude at which to place the + beachball. ``0 0`` plots the beachball at the longitude and latitude + given in the columns 1 and 2. [optional; requires ``offset=True``]. + - Last Column: text string to appear near the beachball [optional]. + + - *1-D np.array*: focal mechanism parameters of a single event. The meanings of columns are the same as above. - - *2-D array*: focal mechanism parameters of multiple events. + - *2-D np.array*: focal mechanism parameters of multiple events. The meanings of columns are the same as above. - - *dictionary or :class:`pandas.DataFrame`*: The dictionary keys or + - *dict* or :class:`pandas.DataFrame`: The dict keys or :class:`pandas.DataFrame` column names determine the focal mechanism - convention. For different conventions, the following combination of - keys are allowed: - - - ``"aki"``: *strike, dip, rake, magnitude* - - ``"gcmt"``: *strike1, dip1, rake1, strike2, dip2, rake2, mantissa,* - *exponent* - - ``"mt"``: *mrr, mtt, mff, mrt, mrf, mtf, exponent* - - ``"partial"``: *strike1, dip1, strike2, fault_type, magnitude* - - ``"principal_axis"``: *t_value, t_azimuth, t_plunge, n_value, - n_azimuth, n_plunge, p_value, p_azimuth, p_plunge, exponent* - - A dictionary may contain values for a single focal mechanism or - lists of values for multiple focal mechanisms. - - Both dictionary and :class:`pandas.DataFrame` may optionally contain - keys/column names: ``latitude``, ``longitude``, ``depth``, - ``plot_longitude``, ``plot_latitude``, and/or ``event_name``. - - If ``spec`` is either a str, a 1-D array or a 2-D array, the - ``convention`` parameter is required so we know how to interpret the - columns. If ``spec`` is a dictionary or a :class:`pandas.DataFrame`, - ``convention`` is not needed and is ignored if specified. + convention. For the different conventions, the following combination of + keys / column names are required: + + - ``"aki"``: *strike*, *dip*, *rake*, *magnitude* + - ``"gcmt"``: *strike1*, *dip1*, *rake1*, *strike2*, *dip2*, *rake2*, + *mantissa*, *exponent* + - ``"mt"``: *mrr*, *mtt*, *mff*, *mrt*, *mrf*, *mtf*, *exponent* + - ``"partial"``: *strike1*, *dip1*, *strike2*, *fault_type*, *magnitude* + - ``"principal_axis"``: *t_value*, *t_azimuth*, *t_plunge*, *n_value*, + *n_azimuth*, *n_plunge*, *p_value*, *p_azimuth*, *p_plunge*, *exponent* + + A dict may contain values for a single focal mechanism or lists of + values for multiple focal mechanisms. + + Both dict and :class:`pandas.DataFrame` may optionally contain the keys / + column names: ``latitude``, ``longitude``, ``depth``, ``plot_longitude``, + ``plot_latitude``, and/or ``event_name``. + + If ``spec`` is either a str or a 1-D or 2-D numpy array, the ``convention`` + parameter is required to interpret the columns. If ``spec`` is a dict or + a :class:`pandas.DataFrame`, ``convention`` is not needed and ignored if + specified. scale : float or str *scale*\ [**+a**\ *angle*][**+f**\ *font*][**+j**\ *justify*]\ [**+l**][**+m**][**+o**\ *dx*\ [/\ *dy*]][**+s**\ *reference*]. - Adjust scaling of the radius of the beachball, which is - proportional to the magnitude. By default, *scale* defines the - size for magnitude = 5 (i.e., scalar seismic moment - M0 = 4.0E23 dynes-cm). If **+l** is used the radius will be - proportional to the seismic moment instead. Use **+s** and give - a *reference* to change the reference magnitude (or moment), and - use **+m** to plot all beachballs with the same size. A text - string can be specified to appear near the beachball - (corresponding to column or parameter ``event_name``). - Append **+a**\ *angle* to change the angle of the text string; - append **+f**\ *font* to change its font (size,fontname,color); - append **+j**\ *justify* to change the text location relative - to the beachball [Default is ``"TC"``, i.e., Top Center]; - append **+o** to offset the text string by *dx*\ /*dy*. + Adjust scaling of the radius of the beachball, which is proportional to the + magnitude. By default, *scale* defines the size for magnitude = 5 (i.e., scalar + seismic moment M0 = 4.0E23 dynes-cm). If **+l** is used the radius will be + proportional to the seismic moment instead. Use **+s** and give a *reference* + to change the reference magnitude (or moment), and use **+m** to plot all + beachballs with the same size. A text string can be specified to appear near + the beachball (corresponding to column or parameter ``event_name``). Append + **+a**\ *angle* to change the angle of the text string; append **+f**\ *font* + to change its font (size,fontname,color); append **+j**\ *justify* to change + the text location relative to the beachball [Default is ``"TC"``, i.e., Top + Center]; append **+o** to offset the text string by *dx*\ /*dy*. convention : str Focal mechanism convention. Choose from: - - ``"aki"`` (Aki & Richards) + - ``"aki"`` (Aki and Richards) - ``"gcmt"`` (global CMT) - ``"mt"`` (seismic moment tensor) - ``"partial"`` (partial focal mechanism) - ``"principal_axis"`` (principal axis) - Ignored if ``spec`` is a dictionary or :class:`pandas.DataFrame`. + Ignored if ``spec`` is a dict or :class:`pandas.DataFrame`. component : str The component of the seismic moment tensor to plot. - ``"full"``: the full seismic moment tensor - - ``"dc"``: the closest double couple defined from the moment tensor - (zero trace and zero determinant) + - ``"dc"``: the closest double couple defined from the moment tensor (zero + trace and zero determinant) - ``"deviatoric"``: deviatoric part of the moment tensor (zero trace) - longitude : float, list, or 1-D numpy array - Longitude(s) of event location(s). Must be the same length as the - number of events. Will override the ``longitude`` values - in ``spec`` if ``spec`` is a dictionary or :class:`pandas.DataFrame`. - latitude : float, list, or 1-D numpy array - Latitude(s) of event location(s). Must be the same length as the - number of events. Will override the ``latitude`` values - in ``spec`` if ``spec`` is a dictionary or :class:`pandas.DataFrame`. - depth : float, list, or 1-D numpy array - Depth(s) of event location(s) in kilometers. Must be the same length - as the number of events. Will override the ``depth`` values in ``spec`` - if ``spec`` is a dictionary or :class:`pandas.DataFrame`. - plot_longitude : float, str, list, or 1-D numpy array - Longitude(s) at which to place beachball(s). Must be the same length - as the number of events. Will override the ``plot_longitude`` values - in ``spec`` if ``spec`` is a dictionary or :class:`pandas.DataFrame`. - plot_latitude : float, str, list, or 1-D numpy array - Latitude(s) at which to place beachball(s). List must be the same - length as the number of events. Will override the ``plot_latitude`` - values in ``spec`` if ``spec`` is a dictionary or :class:`pandas.DataFrame`. + longitude/latitude/depth : float, list, or 1-D numpy array + Longitude(s) / latitude(s) / depth(s) of the event(s). Length must match the + number of events. Overrides the ``longitude`` / ``latitude`` / ``depth`` values + in ``spec`` if ``spec`` is a dict or :class:`pandas.DataFrame`. + plot_longitude/plot_latitude : float, str, list, or 1-D numpy array + Longitude(s) / Latitude(s) at which to place the beachball(s). Length must match + the number of events. Overrides the ``plot_longitude`` / ``plot_latitude`` + values in ``spec`` if ``spec`` is a dict or :class:`pandas.DataFrame`. event_name : str, list of str, or 1-D numpy array - Text string(s), e.g., event name(s) to appear near the beachball(s). - List must be the same length as the number of events. Will override - the ``event_name`` labels in ``spec`` if ``spec`` is a dictionary - or :class:`pandas.DataFrame`. + Text string(s), e.g., event name(s) to appear near the beachball(s). Length + must match the number of events. Overrides the ``event_name`` labels in ``spec`` + if ``spec`` is a dict or :class:`pandas.DataFrame`. labelbox : bool or str [*fill*]. - Draw a box behind the label if given. Use *fill* to give a fill color - [Default is ``"white"``]. + Draw a box behind the label if given via ``event_name``. Use *fill* to give a + fill color [Default is ``"white"``]. offset : bool or str [**+p**\ *pen*][**+s**\ *size*]. - Offset beachball(s) to longitude(s) and latitude(s) specified in the - the last two columns of the input file or array, or by - ``plot_longitude`` and ``plot_latitude`` if provided. A small circle - is plotted at the initial location and a line connects the beachball - to the circle. Use **+s**\ *size* to set the diameter of the circle - [Default is no circle]. Use **+p**\ *pen* to set the pen attributes - for this feature [Default is set via ``pen``]. The fill of the - circle is set via ``compressionfill`` or ``cmap``, i.e., - corresponds to the fill of the compressive quadrants. + Offset beachball(s) to the longitude(s) and latitude(s) specified in the last + two columns of the input file or array, or by ``plot_longitude`` and + ``plot_latitude`` if provided. A line from the beachball to the initial location + is drawn. Use **+s**\ *size* to plot a small circle at the initial location and + to set the diameter of this circle [Default is no circle]. Use **+p**\ *pen* to + set the pen attributes for this feature [Default is set via ``pen``]. The fill + of the circle is set via ``compressionfill`` or ``cmap``, i.e., corresponds to + the fill of the compressive quadrants. compressionfill : str - Set color or pattern for filling compressive quadrants - [Default is ``"black"``]. This setting also applies to the fill of - the circle defined via ``offset``. + Set color or pattern for filling compressive quadrants [Default is ``"black"``]. + This setting also applies to the fill of the circle defined via ``offset``. extensionfill : str - Set color or pattern for filling extensive quadrants - [Default is ``"white"``]. + Set color or pattern for filling extensive quadrants [Default is ``"white"``]. pen : str - Set pen attributes for all lines related to beachball [Default is - ``"0.25p,black,solid"``]. This setting applies to ``outline``, - ``nodal``, and ``offset``, unless overruled by arguments passed to - those parameters. Draws circumference of beachball. + Set (default) pen attributes for all lines related to the beachball [Default is + ``"0.25p,black,solid"``]. This setting applies to ``outline``, ``nodal``, and + ``offset``, unless overruled by arguments passed to those parameters. Draws the + circumference of the beachball. outline : bool or str [*pen*]. - Draw circumference and nodal planes of beachball. Use *pen* to set - the pen attributes for this feature [Default is set via ``pen``]. + Draw circumference and nodal planes of the beachball. Use *pen* to set the pen + attributes for this feature [Default is set via ``pen``]. nodal : bool, int, or str [*nplane*][/*pen*]. - Plot the nodal planes and outline the bubble which is transparent. - If *nplane* is + Plot the nodal planes and outline the bubble which is transparent. If *nplane* + is - ``0`` or ``True``: both nodal planes are plotted [Default]. - ``1``: only the first nodal plane is plotted. - ``2``: only the second nodal plane is plotted. - Use /*pen* to set the pen attributes for this feature [Default is - set via ``pen``]. - For double couple mechanisms, ``nodal`` renders the beachball - transparent by drawing only the nodal planes and the circumference. - For non-double couple mechanisms, ``nodal=0`` overlays best - double couple transparently. + Use /*pen* to set the pen attributes for this feature [Default is set via + ``pen``]. + For double couple mechanisms, ``nodal`` renders the beachball transparent by + drawing only the nodal planes and the circumference. For non-double couple + mechanisms, ``nodal=0`` overlays best double couple transparently. cmap : str File name of a CPT file or a series of comma-separated colors (e.g., - *color1,color2,color3*) to build a linear continuous CPT from those - colors automatically. The color of the compressive quadrants is - determined by the z-value (i.e., event depth or the third column for - an input file). This setting also applies to the fill of the circle - defined via ``offset``. + *color1,color2,color3*) to build a linear continuous CPT from those colors + automatically. The color of the compressive quadrants is determined by the + z-value (i.e., event depth or the third column for an input file). This setting + also applies to the fill of the circle defined via ``offset``. no_clip : bool - Do **not** skip symbols that fall outside the frame boundaries - [Default is ``False``, i.e., plot symbols inside the frame - boundaries only]. + Do **not** skip symbols that fall outside the frame boundaries [Default is + ``False``, i.e., plot symbols inside the frame boundaries only]. {projection} {region} {frame} diff --git a/pygmt/src/plot.py b/pygmt/src/plot.py index ebfb2f22f3c..23c5bde12fd 100644 --- a/pygmt/src/plot.py +++ b/pygmt/src/plot.py @@ -2,6 +2,8 @@ plot - Plot in two dimensions. """ +from typing import Literal + from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -49,7 +51,15 @@ ) @kwargs_to_strings(R="sequence", c="sequence_comma", i="sequence_comma", p="sequence") def plot( - self, data=None, x=None, y=None, size=None, symbol=None, direction=None, **kwargs + self, + data=None, + x=None, + y=None, + size=None, + symbol=None, + direction=None, + straight_line: bool | Literal["x", "y"] = False, # noqa: ARG001 + **kwargs, ): r""" Plot lines, polygons, and symbols in 2-D. @@ -98,18 +108,29 @@ def plot( depending on the style options chosen. {projection} {region} - 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=True``. - 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. + straight_line + By default, line segments are drawn as straight lines in the Cartesian and polar + coordinate systems, and as great circle arcs (by resampling coarse input data + along such arcs) in the geographic coordinate system. The ``straight_line`` + parameter can control the drawing of line segments. Valid values are: + + - ``True``: Draw line segments as straight lines in geographic coordinate + systems. + - ``"x"``: Draw line segments by first along *x*, then along *y*. + - ``"y"``: Draw line segments by first along *y*, then along *x*. + + Here, *x* and *y* have different meanings depending on the coordinate system: + + - **Cartesian** coordinate system: *x* and *y* are the X- and Y-axes. + - **Polar** coordinate system: *x* and *y* are theta and radius. + - **Geographic** coordinate system: *x* and *y* are parallels and meridians. + + .. attention:: + + There exits a bug in GMT<=6.5.0 that, in geographic coordinate systems, the + meaning of *x* and *y* is reversed, i.e., *x* means meridians and *y* means + parallels. The bug is fixed by upstream + `PR #8648 `__. {frame} {cmap} offset : str @@ -206,6 +227,8 @@ def plot( ``x``/``y``. {wrap} """ + # TODO(GMT>6.5.0): Remove the note for the upstream bug of the "straight_line" + # parameter. kwargs = self._preprocess(**kwargs) kind = data_kind(data) diff --git a/pygmt/src/plot3d.py b/pygmt/src/plot3d.py index f7f2b08290a..e8e75382d74 100644 --- a/pygmt/src/plot3d.py +++ b/pygmt/src/plot3d.py @@ -2,6 +2,8 @@ plot3d - Plot in three dimensions. """ +from typing import Literal + from pygmt.clib import Session from pygmt.exceptions import GMTInvalidInput from pygmt.helpers import ( @@ -58,6 +60,7 @@ def plot3d( size=None, symbol=None, direction=None, + straight_line: bool | Literal["x", "y"] = False, # noqa: ARG001 **kwargs, ): r""" @@ -108,18 +111,31 @@ def plot3d( zscale/zsize : float or str Set z-axis scaling or z-axis size. {region} - 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`` parameter requires - constant *z*-coordinates. + straight_line + By default, line segments are drawn as straight lines in the Cartesian and polar + coordinate systems, and as great circle arcs (by resampling coarse input data + along such arcs) in the geographic coordinate system. The ``straight_line`` + parameter can control the drawing of line segments. Valid values are: + + - ``True``: Draw line segments as straight lines in geographic coordinate + systems. + - ``"x"``: Draw line segments by first along *x*, then along *y*. + - ``"y"``: Draw line segments by first along *y*, then along *x*. + + Here, *x* and *y* have different meanings depending on the coordinate system: + + - **Cartesian** coordinate system: *x* and *y* are the X- and Y-axes. + - **Polar** coordinate system: *x* and *y* are theta and radius. + - **Geographic** coordinate system: *x* and *y* are parallels and meridians. + + **NOTE**: The ``straight_line`` parameter requires constant *z*-coordinates. + + .. attention:: + + There exits a bug in GMT<=6.5.0 that, in geographic coordinate systems, the + meaning of *x* and *y* is reversed, i.e., *x* means meridians and *y* means + parallels. The bug is fixed by upstream + `PR #8648 `__. {frame} {cmap} offset : str @@ -189,6 +205,8 @@ def plot3d( ``x``/``y``/``z``. {wrap} """ + # TODO(GMT>6.5.0): Remove the note for the upstream bug of the "straight_line" + # parameter. kwargs = self._preprocess(**kwargs) kind = data_kind(data) diff --git a/pygmt/src/project.py b/pygmt/src/project.py index f90e517b202..a49d5a1ad1f 100644 --- a/pygmt/src/project.py +++ b/pygmt/src/project.py @@ -136,7 +136,7 @@ def project( convention : str Specify the desired output using any combination of **xyzpqrs**, in any order [Default is **xypqrsz**]. Do not space between the letters. - Use lower case. The output will be columns of values corresponding to + Use lowercase. The output will be columns of values corresponding to your ``convention``. The **z** flag is special and refers to all numerical columns beyond the leading **x** and **y** in your input record. The **z** flag also includes any trailing text (which is diff --git a/pygmt/src/select.py b/pygmt/src/select.py index ecd6d12bfad..a7db421a210 100644 --- a/pygmt/src/select.py +++ b/pygmt/src/select.py @@ -1,3 +1,4 @@ +# noqa: A005 """ select - Select data table subsets based on multiple spatial criteria. """ diff --git a/pygmt/src/sphdistance.py b/pygmt/src/sphdistance.py index ab49f961e03..279db4e3590 100644 --- a/pygmt/src/sphdistance.py +++ b/pygmt/src/sphdistance.py @@ -65,10 +65,10 @@ def sphdistance( Specify the quantity that should be assigned to the grid nodes [Default is **d**]: - - **d** - compute distances to the nearest data point - - **n** - assign the ID numbers of the Voronoi polygons that each + - **d**: compute distances to the nearest data point + - **n**: assign the ID numbers of the Voronoi polygons that each grid node is inside - - **z** - assign all nodes inside the polygon the z-value of the center + - **z**: assign all nodes inside the polygon the z-value of the center node for a natural nearest-neighbor grid. Optionally, append the resampling interval along Voronoi arcs in diff --git a/pygmt/src/ternary.py b/pygmt/src/ternary.py index 5bc477fab99..633707cf427 100644 --- a/pygmt/src/ternary.py +++ b/pygmt/src/ternary.py @@ -87,7 +87,7 @@ def ternary( if any(v is not None for v in labels): kwargs["L"] = "/".join(str(v) if v is not None else "-" for v in labels) - # Patch for GMT < 6.5.0. + # TODO(GMT>=6.5.0): Remove the patch for upstream bug fixed in GMT 6.5.0. # See https://github.com/GenericMappingTools/pygmt/pull/2138 if Version(__gmt_version__) < Version("6.5.0") and isinstance(data, pd.DataFrame): data = data.to_numpy() diff --git a/pygmt/src/text.py b/pygmt/src/text.py index 75b2653043c..b507510f620 100644 --- a/pygmt/src/text.py +++ b/pygmt/src/text.py @@ -138,11 +138,11 @@ def text_( # noqa: PLR0912 **i** for inches, or **p** for points; if not given we consult :gmt-term:`PROJ_LENGTH_UNIT`) or *%* for a percentage of the font size. Optionally, use modifier **+t** to set the shape of the text - box when using ``fill`` and/or ``pen``. Append lower case **o** - to get a straight rectangle [Default is **o**]. Append upper case + box when using ``fill`` and/or ``pen``. Append lowercase **o** + to get a straight rectangle [Default is **o**]. Append uppercase **O** to get a rounded rectangle. In paragraph mode (*paragraph*) - you can also append lower case **c** to get a concave rectangle or - append upper case **C** to get a convex rectangle. + you can also append lowercase **c** to get a concave rectangle or + append uppercase **C** to get a convex rectangle. fill : str Set color for filling text boxes [Default is no fill]. offset : str diff --git a/pygmt/src/timestamp.py b/pygmt/src/timestamp.py index 3db9ff694d9..13f278b2816 100644 --- a/pygmt/src/timestamp.py +++ b/pygmt/src/timestamp.py @@ -84,6 +84,7 @@ def timestamp( kwdict["U"] += f"{label}" kwdict["U"] += f"+j{justify}" + # TODO(GMT>=6.5.0): Remove the patch for upstream bug fixed in GMT 6.5.0. if Version(__gmt_version__) < Version("6.5.0") and "/" not in str(offset): # Giving a single offset doesn't work in GMT < 6.5.0. # See https://github.com/GenericMappingTools/gmt/issues/7107. @@ -99,6 +100,7 @@ def timestamp( "The given text string will be truncated to 64 characters." ) warnings.warn(message=msg, category=RuntimeWarning, stacklevel=2) + # TODO(GMT>=6.5.0): Remove the workaround for the new '+t' modifier. if Version(__gmt_version__) < Version("6.5.0"): # Workaround for GMT<6.5.0 by overriding the 'timefmt' parameter timefmt = text[:64] diff --git a/pygmt/src/vlines.py b/pygmt/src/vlines.py new file mode 100644 index 00000000000..2483df99f27 --- /dev/null +++ b/pygmt/src/vlines.py @@ -0,0 +1,132 @@ +""" +vlines - Plot vertical lines. +""" + +from collections.abc import Sequence + +import numpy as np +from pygmt.exceptions import GMTInvalidInput + +__doctest_skip__ = ["vlines"] + + +def vlines( + self, + x: float | Sequence[float], + ymin: float | Sequence[float] | None = None, + ymax: float | Sequence[float] | None = None, + pen: str | None = None, + label: str | None = None, + no_clip: bool = False, + perspective: str | bool | None = None, +): + """ + Plot one or multiple vertical line(s). + + This method is a high-level wrapper around :meth:`pygmt.Figure.plot` that focuses on + plotting vertical lines at X-coordinates specified by the ``x`` parameter. The ``x`` + parameter can be a single value (for a single vertical line) or a sequence of values + (for multiple vertical lines). + + By default, the Y-coordinates of the start and end points of the lines are set to be + the Y-limits of the current plot, but this can be overridden by specifying the + ``ymin`` and ``ymax`` parameters. ``ymin`` and ``ymax`` can be either a single value + or a sequence of values. If a single value is provided, it is applied to all lines. + If a sequence is provided, the length of ``ymin`` and ``ymax`` must match the length + of ``x``. + + The term "vertical" lines can be interpreted differently in different coordinate + systems: + + - **Cartesian** coordinate system: lines are plotted as straight lines. + - **Polar** projection: lines are plotted as straight lines along radius. + - **Geographic** projection: lines are plotted as meridians along constant + longitude. + + Parameters + ---------- + x + X-coordinates to plot the lines. It can be a single value (for a single line) + or a sequence of values (for multiple lines). + ymin/ymax + Y-coordinates of the start/end point(s) of the line(s). If ``None``, defaults to + the Y-limits of the current plot. ``ymin`` and ``ymax`` can either be a single + value or a sequence of values. If a single value is provided, it is applied to + all lines. If a sequence is provided, the length of ``ymin`` and ``ymax`` must + match the length of ``x``. + pen + Pen attributes for the line(s), in the format of *width,color,style*. + label + Label for the line(s), to be displayed in the legend. + no_clip + If ``True``, do not clip lines outside the plot region. Only makes sense in the + Cartesian coordinate system. + perspective + Select perspective view and set the azimuth and elevation angle of the + viewpoint. Refer to :meth:`pygmt.Figure.plot` for details. + + Examples + -------- + >>> import pygmt + >>> fig = pygmt.Figure() + >>> fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + >>> fig.vlines(x=1, pen="1p,black", label="Line at x=1") + >>> fig.vlines(x=2, ymin=2, ymax=8, pen="1p,red,-", label="Line at x=2") + >>> fig.vlines(x=[3, 4], ymin=3, ymax=7, pen="1p,black,.", label="Lines at x=3,4") + >>> fig.vlines(x=[5, 6], ymin=4, ymax=9, pen="1p,red", label="Lines at x=5,6") + >>> fig.vlines( + ... x=[7, 8], ymin=[0, 1], ymax=[7, 8], pen="1p,blue", label="Lines at x=7,8" + ... ) + >>> fig.legend() + >>> fig.show() + """ + self._preprocess() + + # Determine the y limits from the current plot region if not specified. + if ymin is None or ymax is None: + ylimits = self.region[2:] + if ymin is None: + ymin = ylimits[0] + if ymax is None: + ymax = ylimits[1] + + # Ensure x/ymin/ymax are 1-D arrays. + _x = np.atleast_1d(x) + _ymin = np.atleast_1d(ymin) + _ymax = np.atleast_1d(ymax) + + nlines = len(_x) # Number of lines to plot. + + # Check if ymin/ymax are scalars or have the expected length. + if _ymin.size not in {1, nlines} or _ymax.size not in {1, nlines}: + msg = ( + f"'ymin' and 'ymax' are expected to be scalars or have lengths '{nlines}', " + f"but lengths '{_ymin.size}' and '{_ymax.size}' are given." + ) + raise GMTInvalidInput(msg) + + # Repeat ymin/ymax to match the length of x if they are scalars. + if nlines != 1: + if _ymin.size == 1: + _ymin = np.repeat(_ymin, nlines) + if _ymax.size == 1: + _ymax = np.repeat(_ymax, nlines) + + # Call the Figure.plot method to plot the lines. + for i in range(nlines): + # Special handling for label. + # 1. Only specify a label when plotting the first line. + # 2. The -l option can accept comma-separated labels for labeling multiple lines + # with auto-coloring enabled. We don't need this feature here, so we need to + # replace comma with \054 if the label contains commas. + _label = label.replace(",", "\\054") if label and i == 0 else None + + self.plot( + x=[_x[i], _x[i]], + y=[_ymin[i], _ymax[i]], + pen=pen, + label=_label, + no_clip=no_clip, + perspective=perspective, + straight_line="y", + ) diff --git a/pygmt/src/x2sys_cross.py b/pygmt/src/x2sys_cross.py index f7d9f8e1843..4e209d33d18 100644 --- a/pygmt/src/x2sys_cross.py +++ b/pygmt/src/x2sys_cross.py @@ -145,9 +145,9 @@ def x2sys_cross( Sets the interpolation mode for estimating values at the crossover. Choose among: - - **l** - Linear interpolation [Default]. - - **a** - Akima spline interpolation. - - **c** - Cubic spline interpolation. + - **l**: Linear interpolation [Default]. + - **a**: Akima spline interpolation. + - **c**: Cubic spline interpolation. coe : str Use **e** for external COEs only, and **i** for internal COEs only diff --git a/pygmt/src/x2sys_init.py b/pygmt/src/x2sys_init.py index 99af7211424..6e36263fe0e 100644 --- a/pygmt/src/x2sys_init.py +++ b/pygmt/src/x2sys_init.py @@ -85,13 +85,13 @@ def x2sys_init(tag, **kwargs): programs. Append **d** for distance or **s** for speed, then give the desired *unit* as: - - **c** - Cartesian userdist or userdist/usertime - - **e** - meters or m/s - - **f** - feet or ft/s - - **k** - kilometers or km/hr - - **m** - miles or mi/hr - - **n** - nautical miles or knots - - **u** - survey feet or sft/s + - **c**: Cartesian userdist or userdist/usertime + - **e**: meters or m/s + - **f**: feet or ft/s + - **k**: kilometers or km/hr + - **m**: miles or mi/hr + - **n**: nautical miles or knots + - **u**: survey feet or sft/s [Default is ``units=["dk", "se"]`` (km and m/s) if ``discontinuity`` is set, and ``units=["dc", "sc"]`` otherwise (e.g., for Cartesian units)]. diff --git a/pygmt/tests/baseline/test_hlines_clip.png.dvc b/pygmt/tests/baseline/test_hlines_clip.png.dvc new file mode 100644 index 00000000000..1c24bb1c16d --- /dev/null +++ b/pygmt/tests/baseline/test_hlines_clip.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: e87ea1b80ae5d32d49e9ad94a5c25f96 + size: 7199 + hash: md5 + path: test_hlines_clip.png diff --git a/pygmt/tests/baseline/test_hlines_geographic_global_d.png.dvc b/pygmt/tests/baseline/test_hlines_geographic_global_d.png.dvc new file mode 100644 index 00000000000..960f3a05fdc --- /dev/null +++ b/pygmt/tests/baseline/test_hlines_geographic_global_d.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: b7055f03ff5bc152c0f6b72f2d39f32c + size: 29336 + hash: md5 + path: test_hlines_geographic_global_d.png diff --git a/pygmt/tests/baseline/test_hlines_geographic_global_g.png.dvc b/pygmt/tests/baseline/test_hlines_geographic_global_g.png.dvc new file mode 100644 index 00000000000..29b83d3b44f --- /dev/null +++ b/pygmt/tests/baseline/test_hlines_geographic_global_g.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: ab2e7717cad6ac4132fd3e3af1fefa89 + size: 29798 + hash: md5 + path: test_hlines_geographic_global_g.png diff --git a/pygmt/tests/baseline/test_hlines_multiple_lines.png.dvc b/pygmt/tests/baseline/test_hlines_multiple_lines.png.dvc new file mode 100644 index 00000000000..e915a5a65f0 --- /dev/null +++ b/pygmt/tests/baseline/test_hlines_multiple_lines.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 70c8decbffd37fc48b2eb9ff84442ec0 + size: 14139 + hash: md5 + path: test_hlines_multiple_lines.png diff --git a/pygmt/tests/baseline/test_hlines_one_line.png.dvc b/pygmt/tests/baseline/test_hlines_one_line.png.dvc new file mode 100644 index 00000000000..aa42ce3f492 --- /dev/null +++ b/pygmt/tests/baseline/test_hlines_one_line.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 121970f75d34c552e632cacc692f09e9 + size: 13685 + hash: md5 + path: test_hlines_one_line.png diff --git a/pygmt/tests/baseline/test_hlines_polar_projection.png.dvc b/pygmt/tests/baseline/test_hlines_polar_projection.png.dvc new file mode 100644 index 00000000000..4e5bef96dc6 --- /dev/null +++ b/pygmt/tests/baseline/test_hlines_polar_projection.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 0c0eeb160dd6beb06bb6d3dcc264127a + size: 57789 + hash: md5 + path: test_hlines_polar_projection.png diff --git a/pygmt/tests/baseline/test_plot_datetime.png.dvc b/pygmt/tests/baseline/test_plot_datetime.png.dvc index 714104995ba..1450b29ef82 100644 --- a/pygmt/tests/baseline/test_plot_datetime.png.dvc +++ b/pygmt/tests/baseline/test_plot_datetime.png.dvc @@ -1,5 +1,5 @@ outs: -- md5: 583947facaa873122f0bf18137809cd4 - size: 12695 +- md5: 0a2eae0da1e3d5b71d7392de1c081346 + size: 13124 path: test_plot_datetime.png hash: md5 diff --git a/pygmt/tests/baseline/test_vlines_clip.png.dvc b/pygmt/tests/baseline/test_vlines_clip.png.dvc new file mode 100644 index 00000000000..f20f77ae249 --- /dev/null +++ b/pygmt/tests/baseline/test_vlines_clip.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 4eb9c7fd7e3a803dcc3cde1409ad7fa7 + size: 7361 + hash: md5 + path: test_vlines_clip.png diff --git a/pygmt/tests/baseline/test_vlines_geographic_global.png.dvc b/pygmt/tests/baseline/test_vlines_geographic_global.png.dvc new file mode 100644 index 00000000000..d09fa8f8d82 --- /dev/null +++ b/pygmt/tests/baseline/test_vlines_geographic_global.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 3fb4a271c670e4cbe647838b6fee5a8c + size: 67128 + hash: md5 + path: test_vlines_geographic_global.png diff --git a/pygmt/tests/baseline/test_vlines_multiple_lines.png.dvc b/pygmt/tests/baseline/test_vlines_multiple_lines.png.dvc new file mode 100644 index 00000000000..da9a4bf8aed --- /dev/null +++ b/pygmt/tests/baseline/test_vlines_multiple_lines.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 499b2d08832247673f208b1c0a282c4c + size: 13874 + hash: md5 + path: test_vlines_multiple_lines.png diff --git a/pygmt/tests/baseline/test_vlines_one_line.png.dvc b/pygmt/tests/baseline/test_vlines_one_line.png.dvc new file mode 100644 index 00000000000..efc2df680b3 --- /dev/null +++ b/pygmt/tests/baseline/test_vlines_one_line.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 2cd30ad55fc660123c67e6a684a5ea21 + size: 13589 + hash: md5 + path: test_vlines_one_line.png diff --git a/pygmt/tests/baseline/test_vlines_polar_projection.png.dvc b/pygmt/tests/baseline/test_vlines_polar_projection.png.dvc new file mode 100644 index 00000000000..1252a2d0455 --- /dev/null +++ b/pygmt/tests/baseline/test_vlines_polar_projection.png.dvc @@ -0,0 +1,5 @@ +outs: +- md5: 1981df3bd9c57cd975b6e74946496175 + size: 44621 + hash: md5 + path: test_vlines_polar_projection.png diff --git a/pygmt/tests/test_accessor.py b/pygmt/tests/test_accessor.py index 2701d2a0b3a..07ece609b69 100644 --- a/pygmt/tests/test_accessor.py +++ b/pygmt/tests/test_accessor.py @@ -73,6 +73,7 @@ def test_accessor_set_non_boolean(): grid.gmt.gtype = 2 +# TODO(GMT>=6.5.0): Remove the xfail marker for GMT>=6.5.0. @pytest.mark.xfail( condition=sys.platform == "win32" and Version(__gmt_version__) < Version("6.5.0"), reason="Upstream bug fixed in https://github.com/GenericMappingTools/gmt/pull/7573", diff --git a/pygmt/tests/test_clib_loading.py b/pygmt/tests/test_clib_loading.py index 98d9ae148c0..8b96128c0da 100644 --- a/pygmt/tests/test_clib_loading.py +++ b/pygmt/tests/test_clib_loading.py @@ -9,6 +9,7 @@ import sys import types from pathlib import PurePath +from unittest import mock import pytest from pygmt.clib.loading import ( @@ -70,24 +71,15 @@ def test_load_libgmt(): check_libgmt(load_libgmt()) -def test_load_libgmt_fails(monkeypatch): +def test_load_libgmt_fails(): """ Test that GMTCLibNotFoundError is raised when GMT's shared library cannot be found. """ - with monkeypatch.context() as mpatch: - if sys.platform == "win32": - mpatch.setattr(ctypes.util, "find_library", lambda name: "fakegmt.dll") # noqa: ARG005 - mpatch.setattr( - sys, - "platform", - # Pretend to be on macOS if running on Linux, and vice versa - "darwin" if sys.platform == "linux" else "linux", - ) - mpatch.setattr( - subprocess, - "check_output", - lambda cmd, encoding: "libfakegmt.so", # noqa: ARG005 - ) + with ( + mock.patch("ctypes.util.find_library", return_value="fakegmt.dll"), + mock.patch("sys.platform", "darwin" if sys.platform == "linux" else "linux"), + mock.patch("subprocess.check_output", return_value="libfakegmt.so"), + ): with pytest.raises(GMTCLibNotFoundError): check_libgmt(load_libgmt()) @@ -214,42 +206,25 @@ def test_brokenlib_brokenlib_workinglib(self): assert check_libgmt(load_libgmt(lib_fullnames=lib_fullnames)) is None -class TestLibgmtCount: +def test_libgmt_load_counter(): """ - Test that the GMT library is not repeatedly loaded in every session. + Make sure that the GMT library is not loaded in every session. """ - - loaded_libgmt = load_libgmt() # Load the GMT library and reuse it when necessary - counter = 0 # Global counter for how many times ctypes.CDLL is called - - def _mock_ctypes_cdll_return(self, libname): # noqa: ARG002 - """ - Mock ctypes.CDLL to count how many times the function is called. - - If ctypes.CDLL is called, the counter increases by one. - """ - self.counter += 1 # Increase the counter - return self.loaded_libgmt - - def test_libgmt_load_counter(self, monkeypatch): - """ - Make sure that the GMT library is not loaded in every session. - """ - # Monkeypatch the ctypes.CDLL function - monkeypatch.setattr(ctypes, "CDLL", self._mock_ctypes_cdll_return) - - # Create two sessions and check the global counter + loaded_libgmt = load_libgmt() # Load the GMT library and reuse it when necessary. + with mock.patch("ctypes.CDLL", return_value=loaded_libgmt) as mock_cdll: + # Create two sessions and check the call count with Session() as lib: _ = lib with Session() as lib: _ = lib - assert self.counter == 0 # ctypes.CDLL is not called after two sessions. + # ctypes.CDLL is not called after two sessions. + assert mock_cdll.call_count == 0 - # Explicitly calling load_libgmt to make sure the mock function is correct + # Explicitly calling load_libgmt to make sure the mock function is correct. load_libgmt() - assert self.counter == 1 + assert mock_cdll.call_count == 1 load_libgmt() - assert self.counter == 2 + assert mock_cdll.call_count == 2 ############################################################################### diff --git a/pygmt/tests/test_clib_to_numpy.py b/pygmt/tests/test_clib_to_numpy.py index 29fc50826ab..40b45e466d8 100644 --- a/pygmt/tests/test_clib_to_numpy.py +++ b/pygmt/tests/test_clib_to_numpy.py @@ -2,8 +2,8 @@ Tests for the _to_numpy function in the clib.conversion module. """ +import datetime import sys -from datetime import date, datetime import numpy as np import numpy.testing as npt @@ -54,6 +54,7 @@ def _check_result(result, expected_dtype): @pytest.mark.parametrize( ("data", "expected_dtype"), [ + # TODO(NumPy>=2.0): Remove the if-else statement after NumPy>=2.0. pytest.param( [1, 2, 3], np.int32 @@ -79,6 +80,70 @@ def test_to_numpy_python_types(data, expected_dtype): npt.assert_array_equal(result, data) +@pytest.mark.parametrize( + "data", + [ + pytest.param( + ["2018", "2018-02", "2018-03-01", "2018-04-01T01:02:03"], id="iso8601" + ), + pytest.param( + [ + datetime.date(2018, 1, 1), + datetime.datetime(2018, 2, 1), + datetime.date(2018, 3, 1), + datetime.datetime(2018, 4, 1, 1, 2, 3), + ], + id="datetime", + ), + pytest.param( + [ + np.datetime64("2018"), + np.datetime64("2018-02"), + np.datetime64("2018-03-01"), + np.datetime64("2018-04-01T01:02:03"), + ], + id="np_datetime64", + ), + pytest.param( + [ + pd.Timestamp("2018-01-01"), + pd.Timestamp("2018-02-01"), + pd.Timestamp("2018-03-01"), + pd.Timestamp("2018-04-01T01:02:03"), + ], + id="pd_timestamp", + ), + pytest.param( + [ + "2018-01-01", + np.datetime64("2018-02-01"), + datetime.datetime(2018, 3, 1), + pd.Timestamp("2018-04-01T01:02:03"), + ], + id="mixed", + ), + ], +) +def test_to_numpy_python_datetime(data): + """ + Test the _to_numpy function with Python sequence of datetime types. + """ + result = _to_numpy(data) + assert result.dtype.type == np.datetime64 + npt.assert_array_equal( + result, + np.array( + [ + "2018-01-01T00:00:00", + "2018-02-01T00:00:00", + "2018-03-01T00:00:00", + "2018-04-01T01:02:03", + ], + dtype="datetime64[s]", + ), + ) + + ######################################################################################## # Test the _to_numpy function with NumPy arrays. # @@ -152,6 +217,36 @@ def test_to_numpy_numpy_string(dtype): npt.assert_array_equal(result, array) +@pytest.mark.parametrize( + "dtype", + [ + np.datetime64, # The expected dtype is "datetime64[D]" for this test. + "datetime64[Y]", + "datetime64[M]", + "datetime64[W]", + "datetime64[D]", + "datetime64[h]", + "datetime64[m]", + "datetime64[s]", + "datetime64[ms]", + "datetime64[us]", + "datetime64[ns]", + ], +) +def test_to_numpy_numpy_datetime(dtype): + """ + Test the _to_ndarray function with 1-D NumPy arrays of datetime. + + Time units "fs", "as", "ps" are not tested here because they can only represent a + small range of times in 1969-1970. + """ + array = np.array(["2024-01-01", "2024-01-02", "2024-01-03"], dtype=dtype) + result = _to_numpy(array) + _check_result(result, np.datetime64) + assert result.dtype == (dtype if isinstance(dtype, str) else "datetime64[D]") + npt.assert_array_equal(result, array) + + ######################################################################################## # Test the _to_numpy function with pandas.Series. # @@ -218,9 +313,10 @@ def test_to_numpy_pandas_numeric(dtype, expected_dtype): Test the _to_numpy function with pandas.Series of numeric dtypes. """ data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + # TODO(pandas>=2.2): Remove the workaround for float16 dtype in pandas<2.2. + # float16 needs special handling for pandas < 2.2. + # Example from https://arrow.apache.org/docs/python/generated/pyarrow.float16.html if dtype == "float16[pyarrow]" and Version(pd.__version__) < Version("2.2"): - # float16 needs special handling for pandas < 2.2. - # Example from https://arrow.apache.org/docs/python/generated/pyarrow.float16.html data = np.array(data, dtype=np.float16) series = pd.Series(data, dtype=dtype)[::2] # Not C-contiguous result = _to_numpy(series) @@ -264,9 +360,10 @@ def test_to_numpy_pandas_numeric_with_na(dtype, expected_dtype): dtypes and missing values (NA). """ data = [1.0, 2.0, None, 4.0, 5.0, 6.0] + # TODO(pandas>=2.2): Remove the workaround for float16 dtype in pandas<2.2. + # float16 needs special handling for pandas < 2.2. + # Example from https://arrow.apache.org/docs/python/generated/pyarrow.float16.html if dtype == "float16[pyarrow]" and Version(pd.__version__) < Version("2.2"): - # float16 needs special handling for pandas < 2.2. - # Example from https://arrow.apache.org/docs/python/generated/pyarrow.float16.html data = np.array(data, dtype=np.float16) series = pd.Series(data, dtype=dtype)[::2] # Not C-contiguous assert series.isna().any() @@ -287,6 +384,7 @@ def test_to_numpy_pandas_numeric_with_na(dtype, expected_dtype): "string[pyarrow_numpy]", marks=[ skip_if_no(package="pyarrow"), + # TODO(pandas>=2.1): Remove the skipif marker for pandas<2.1. pytest.mark.skipif( Version(pd.__version__) < Version("2.1"), reason="string[pyarrow_numpy] was added since pandas 2.1", @@ -331,6 +429,113 @@ def test_to_numpy_pandas_date(dtype, expected_dtype): ) +pandas_old_version = pytest.mark.xfail( + condition=Version(pd.__version__) < Version("2.1"), + reason="pandas 2.0 bug reported in https://github.com/pandas-dev/pandas/issues/52705", +) + + +@pytest.mark.parametrize( + ("dtype", "expected_dtype"), + [ + # NumPy datetime64 types. Only unit 's'/'ms'/'us'/'ns' are supported. + pytest.param("datetime64[s]", "datetime64[s]", id="datetime64[s]"), + pytest.param("datetime64[ms]", "datetime64[ms]", id="datetime64[ms]"), + pytest.param("datetime64[us]", "datetime64[us]", id="datetime64[us]"), + pytest.param("datetime64[ns]", "datetime64[ns]", id="datetime64[ns]"), + # pandas.DatetimeTZDtype can be given in two ways [tz is required]: + # 1. pandas.DatetimeTZDtype(unit, tz) + # 2. String aliases: "datetime64[unit, tz]" + pytest.param( + "datetime64[s, UTC]", + "datetime64[s]", + id="datetime64[s, tz=UTC]", + marks=pandas_old_version, + ), + pytest.param( + "datetime64[s, America/New_York]", + "datetime64[s]", + id="datetime64[s, tz=America/New_York]", + marks=pandas_old_version, + ), + pytest.param( + "datetime64[s, +07:30]", + "datetime64[s]", + id="datetime64[s, +07:30]", + marks=pandas_old_version, + ), + # PyArrow timestamp types can be given in two ways [tz is optional]: + # 1. pd.ArrowDtype(pyarrow.Timestamp(unit, tz=tz)) + # 2. String aliases: "timestamp[unit, tz][pyarrow]" + pytest.param( + "timestamp[s][pyarrow]", + "datetime64[s]", + id="timestamp[s][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[ms][pyarrow]", + "datetime64[ms]", + id="timestamp[ms][pyarrow]", + marks=[skip_if_no(package="pyarrow"), pandas_old_version], + ), + pytest.param( + "timestamp[us][pyarrow]", + "datetime64[us]", + id="timestamp[us][pyarrow]", + marks=[skip_if_no(package="pyarrow"), pandas_old_version], + ), + pytest.param( + "timestamp[ns][pyarrow]", + "datetime64[ns]", + id="timestamp[ns][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[s, UTC][pyarrow]", + "datetime64[s]", + id="timestamp[s, UTC][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[s, America/New_York][pyarrow]", + "datetime64[s]", + id="timestamp[s, America/New_York][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + pytest.param( + "timestamp[s, +08:00][pyarrow]", + "datetime64[s]", + id="timestamp[s, +08:00][pyarrow]", + marks=skip_if_no(package="pyarrow"), + ), + ], +) +def test_to_numpy_pandas_datetime(dtype, expected_dtype): + """ + Test the _to_numpy function with pandas.Series of datetime types. + """ + series = pd.Series( + [pd.Timestamp("2024-01-02T03:04:05"), pd.Timestamp("2024-01-02T03:04:06")], + dtype=dtype, + ) + result = _to_numpy(series) + _check_result(result, np.datetime64) + assert result.dtype == expected_dtype + + # Convert to UTC if the dtype is timezone-aware + if "," in str(dtype): # A hacky way to decide if the dtype is timezone-aware. + # TODO(pandas>=2.1): Simplify the if-else statement. + if Version(pd.__version__) < Version("2.1") and dtype.startswith("timestamp"): + # pandas 2.0 doesn't have the dt.tz_convert method for pyarrow.Timestamp. + series = pd.to_datetime(series, utc=True) + else: + series = series.dt.tz_convert("UTC") + # Remove time zone information and preserve local time. + expected_series = series.dt.tz_localize(tz=None) + npt.assert_array_equal(result, np.array(expected_series, dtype=expected_dtype)) + + ######################################################################################## # Test the _to_numpy function with PyArrow arrays. # @@ -426,6 +631,7 @@ def test_to_numpy_pyarrow_numeric_with_na(dtype, expected_dtype): "large_utf8", # alias for large_string pytest.param( "string_view", + # TODO(pyarrow>=16): Remove the skipif marker for pyarrow<16. marks=pytest.mark.skipif( Version(pa.__version__) < Version("16"), reason="string_view type was added since pyarrow 16", @@ -461,9 +667,9 @@ def test_to_numpy_pyarrow_date(dtype, expected_dtype): Here we explicitly check the dtype and date unit of the result. """ data = [ - date(2024, 1, 1), - datetime(2024, 1, 2), - datetime(2024, 1, 3), + datetime.date(2024, 1, 1), + datetime.datetime(2024, 1, 2), + datetime.datetime(2024, 1, 3), ] array = pa.array(data, type=dtype) result = _to_numpy(array) @@ -507,7 +713,10 @@ def test_to_numpy_pyarrow_timestamp(dtype, expected_dtype): Reference: https://arrow.apache.org/docs/python/generated/pyarrow.timestamp.html """ - data = [datetime(2024, 1, 2, 3, 4, 5), datetime(2024, 1, 2, 3, 4, 6)] + data = [ + datetime.datetime(2024, 1, 2, 3, 4, 5), + datetime.datetime(2024, 1, 2, 3, 4, 6), + ] array = pa.array(data, type=dtype) result = _to_numpy(array) _check_result(result, np.datetime64) diff --git a/pygmt/tests/test_clib_virtualfile_from_stringio.py b/pygmt/tests/test_clib_virtualfile_from_stringio.py index ce6de238a88..62daaa688c8 100644 --- a/pygmt/tests/test_clib_virtualfile_from_stringio.py +++ b/pygmt/tests/test_clib_virtualfile_from_stringio.py @@ -43,14 +43,9 @@ def test_virtualfile_from_stringio(): Test the virtualfile_from_stringio method. """ data = io.StringIO( - "# Comment\n" - "H 24p Legend\n" - "N 2\n" - "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" - ) - expected = ( - ">\n" "H 24p Legend\n" "N 2\n" "S 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" + "# Comment\nH 24p Legend\nN 2\nS 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" ) + expected = ">\nH 24p Legend\nN 2\nS 0.1i c 0.15i p300/12 0.25p 0.3i My circle\n" assert _stringio_to_dataset(data) == expected @@ -66,13 +61,7 @@ def test_one_segment(): "6 7 8 9 FGHIJK LMN OPQ\n" "RSTUVWXYZ\n" ) - expected = ( - "> Segment 1\n" - "1 2 3 ABC\n" - "4 5 DE\n" - "6 7 8 9 FGHIJK LMN OPQ\n" - "RSTUVWXYZ\n" - ) + expected = "> Segment 1\n1 2 3 ABC\n4 5 DE\n6 7 8 9 FGHIJK LMN OPQ\nRSTUVWXYZ\n" assert _stringio_to_dataset(data) == expected diff --git a/pygmt/tests/test_clib_virtualfile_from_vectors.py b/pygmt/tests/test_clib_virtualfile_from_vectors.py index b76a9bfe168..234ba01d7cc 100644 --- a/pygmt/tests/test_clib_virtualfile_from_vectors.py +++ b/pygmt/tests/test_clib_virtualfile_from_vectors.py @@ -192,6 +192,8 @@ def test_virtualfile_from_vectors_arraylike(): assert output == expected +# TODO(PyGMT>=0.16.0): Remove this test in PyGMT v0.16.0 in which the "*args" parameter +# will be removed. def test_virtualfile_from_vectors_args(): """ Test the backward compatibility of the deprecated syntax for passing multiple diff --git a/pygmt/tests/test_clib_virtualfile_in.py b/pygmt/tests/test_clib_virtualfile_in.py index aac8e4af772..8a43c1dc273 100644 --- a/pygmt/tests/test_clib_virtualfile_in.py +++ b/pygmt/tests/test_clib_virtualfile_in.py @@ -105,6 +105,7 @@ def test_virtualfile_in_fail_non_valid_data(data): ) +# TODO(GMT>6.5.0): Remove the xfail marker for GMT<=6.5.0. @pytest.mark.xfail( condition=Version(__gmt_version__) <= Version("6.5.0"), reason="Upstream bug fixed in https://github.com/GenericMappingTools/gmt/pull/8600", @@ -127,33 +128,3 @@ def test_virtualfile_in_matrix_string_dtype(): assert output == "347.5 348.5 -30.5 -30\n" # Should check that lib.virtualfile_from_vectors is called once, # not lib.virtualfile_from_matrix, but it's technically complicated. - - -def test_virtualfile_from_data(): - """ - Test the backwards compatibility of the virtualfile_from_data method. - - This test is the same as test_virtualfile_in_required_z_matrix, but using the - deprecated method. - """ - shape = (5, 3) - dataframe = pd.DataFrame( - data=np.arange(shape[0] * shape[1]).reshape(shape), columns=["x", "y", "z"] - ) - data = np.array(dataframe) - with clib.Session() as lib: - with pytest.warns(FutureWarning, match="virtualfile_from_data"): - with lib.virtualfile_from_data( - data=data, required_z=True, check_kind="vector" - ) as vfile: - with GMTTempFile() as outfile: - lib.call_module("info", [vfile, f"->{outfile.name}"]) - output = outfile.read(keep_tabs=True) - bounds = "\t".join( - [ - f"<{i.min():.0f}/{i.max():.0f}>" - for i in (dataframe.x, dataframe.y, dataframe.z) - ] - ) - expected = f": N = {shape[0]}\t{bounds}\n" - assert output == expected diff --git a/pygmt/tests/test_clib_virtualfiles.py b/pygmt/tests/test_clib_virtualfiles.py index a45a662de71..ba48200deae 100644 --- a/pygmt/tests/test_clib_virtualfiles.py +++ b/pygmt/tests/test_clib_virtualfiles.py @@ -107,33 +107,3 @@ def test_open_virtualfile_bad_direction(): with pytest.raises(GMTInvalidInput): with lib.open_virtualfile(*vfargs): pass - - -def test_open_virtual_file(): - """ - Test the deprecated Session.open_virtual_file method. - - This test is the same as test_open_virtualfile, but using the deprecated method. - """ - shape = (5, 3) - with clib.Session() as lib: - family = "GMT_IS_DATASET|GMT_VIA_MATRIX" - geometry = "GMT_IS_POINT" - dataset = lib.create_data( - family=family, - geometry=geometry, - mode="GMT_CONTAINER_ONLY", - dim=[shape[1], shape[0], 1, 0], # columns, rows, layers, dtype - ) - data = np.arange(shape[0] * shape[1]).reshape(shape) - lib.put_matrix(dataset, matrix=data) - # Add the dataset to a virtual file and pass it along to gmt info - with pytest.warns(FutureWarning, match="open_virtual_file"): - vfargs = (family, geometry, "GMT_IN|GMT_IS_REFERENCE", dataset) - with lib.open_virtual_file(*vfargs) as vfile: - with GMTTempFile() as outfile: - lib.call_module("info", [vfile, f"->{outfile.name}"]) - output = outfile.read(keep_tabs=True) - bounds = "\t".join([f"<{col.min():.0f}/{col.max():.0f}>" for col in data.T]) - expected = f": N = {shape[0]}\t{bounds}\n" - assert output == expected diff --git a/pygmt/tests/test_datasets_earth_deflection.py b/pygmt/tests/test_datasets_earth_deflection.py new file mode 100644 index 00000000000..9118779a379 --- /dev/null +++ b/pygmt/tests/test_datasets_earth_deflection.py @@ -0,0 +1,106 @@ +""" +Test basic functionality for loading IGPP Earth east-west and south-north deflection +datasets. +""" + +import numpy as np +import numpy.testing as npt +from pygmt.datasets import load_earth_deflection + + +def test_earth_edefl_01d(): + """ + Test some properties of the Earth east-west deflection 01d data. + """ + data = load_earth_deflection(resolution="01d") + assert data.name == "z" + assert data.attrs["long_name"] == "edefl (microradians)" + assert data.attrs["description"] == "IGPP Earth east-west deflection" + assert data.attrs["units"] == "micro-radians" + assert data.attrs["horizontal_datum"] == "WGS84" + assert data.shape == (181, 361) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-90, 91, 1)) + npt.assert_allclose(data.lon, np.arange(-180, 181, 1)) + npt.assert_allclose(data.min(), -142.64, atol=0.04) + npt.assert_allclose(data.max(), 178.32, atol=0.04) + + +def test_earth_edefl_01d_with_region(): + """ + Test loading low-resolution Earth east-west deflection with "region". + """ + data = load_earth_deflection(resolution="01d", region=[-10, 10, -5, 5]) + assert data.shape == (11, 21) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-5, 6, 1)) + npt.assert_allclose(data.lon, np.arange(-10, 11, 1)) + npt.assert_allclose(data.min(), -28.92, atol=0.04) + npt.assert_allclose(data.max(), 24.72, atol=0.04) + + +def test_earth_edefl_01m_default_registration(): + """ + Test that the grid returned by default for the 1 arc-minute resolution has a "pixel" + registration. + """ + data = load_earth_deflection(resolution="01m", region=[-10, -9, 3, 5]) + assert data.shape == (120, 60) + assert data.gmt.registration == 1 + npt.assert_allclose(data.coords["lat"].data.min(), 3.008333333) + npt.assert_allclose(data.coords["lat"].data.max(), 4.991666666) + npt.assert_allclose(data.coords["lon"].data.min(), -9.99166666) + npt.assert_allclose(data.coords["lon"].data.max(), -9.00833333) + npt.assert_allclose(data.min(), -62.24, atol=0.04) + npt.assert_allclose(data.max(), 15.52, atol=0.04) + + +def test_earth_ndefl_01d(): + """ + Test some properties of the Earth north-south deflection 01d data. + """ + data = load_earth_deflection(resolution="01d", component="north") + assert data.name == "z" + assert data.attrs["long_name"] == "ndefl (microradians)" + assert data.attrs["description"] == "IGPP Earth north-south deflection" + assert data.attrs["units"] == "micro-radians" + assert data.attrs["horizontal_datum"] == "WGS84" + assert data.shape == (181, 361) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-90, 91, 1)) + npt.assert_allclose(data.lon, np.arange(-180, 181, 1)) + npt.assert_allclose(data.min(), -214.8, atol=0.04) + npt.assert_allclose(data.max(), 163.04, atol=0.04) + + +def test_earth_ndefl_01d_with_region(): + """ + Test loading low-resolution Earth north-south deflection with "region". + """ + data = load_earth_deflection( + resolution="01d", region=[-10, 10, -5, 5], component="north" + ) + assert data.shape == (11, 21) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-5, 6, 1)) + npt.assert_allclose(data.lon, np.arange(-10, 11, 1)) + npt.assert_allclose(data.min(), -48.08, atol=0.04) + npt.assert_allclose(data.max(), 18.92, atol=0.04) + + +def test_earth_ndefl_01m_default_registration(): + """ + Test that the grid returned by default for the 1 arc-minute resolution has a "pixel" + registration. + """ + data = load_earth_deflection( + resolution="01m", region=[-10, -9, 3, 5], component="north" + ) + assert data.shape == (120, 60) + assert data.gmt.registration == 1 + npt.assert_allclose(data.coords["lat"].data.min(), 3.008333333) + npt.assert_allclose(data.coords["lat"].data.max(), 4.991666666) + npt.assert_allclose(data.coords["lon"].data.min(), -9.99166666) + npt.assert_allclose(data.coords["lon"].data.max(), -9.00833333) + npt.assert_allclose(data.min(), -107.04, atol=0.04) + npt.assert_allclose(data.max(), 20.28, atol=0.04) diff --git a/pygmt/tests/test_datasets_earth_dist.py b/pygmt/tests/test_datasets_earth_dist.py new file mode 100644 index 00000000000..a5f61a0b5f2 --- /dev/null +++ b/pygmt/tests/test_datasets_earth_dist.py @@ -0,0 +1,53 @@ +""" +Test basic functionality for loading Earth distance to shoreline datasets. +""" + +import numpy as np +import numpy.testing as npt +from pygmt.datasets import load_earth_dist + + +def test_earth_dist_01d(): + """ + Test some properties of the Earth distance to shoreline 01d data. + """ + data = load_earth_dist(resolution="01d") + assert data.name == "z" + assert data.attrs["description"] == "GSHHG Earth distance to shoreline" + assert data.attrs["units"] == "kilometers" + assert data.attrs["horizontal_datum"] == "WGS84" + assert data.shape == (181, 361) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-90, 91, 1)) + npt.assert_allclose(data.lon, np.arange(-180, 181, 1)) + npt.assert_allclose(data.min(), -2655.7, atol=0.01) + npt.assert_allclose(data.max(), 2463.42, atol=0.01) + + +def test_earth_dist_01d_with_region(): + """ + Test loading low-resolution Earth distance to shoreline with "region". + """ + data = load_earth_dist(resolution="01d", region=[-10, 10, -5, 5]) + assert data.shape == (11, 21) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-5, 6, 1)) + npt.assert_allclose(data.lon, np.arange(-10, 11, 1)) + npt.assert_allclose(data.min(), -1081.94, atol=0.01) + npt.assert_allclose(data.max(), 105.18, atol=0.01) + + +def test_earth_dist_01m_default_registration(): + """ + Test that the grid returned by default for the 1 arc-minute resolution has a + "gridline" registration. + """ + data = load_earth_dist(resolution="01m", region=[-10, -9, 3, 5]) + assert data.shape == (121, 61) + assert data.gmt.registration == 0 + assert data.coords["lat"].data.min() == 3.0 + assert data.coords["lat"].data.max() == 5.0 + assert data.coords["lon"].data.min() == -10.0 + assert data.coords["lon"].data.max() == -9.0 + npt.assert_allclose(data.min(), -243.62, atol=0.01) + npt.assert_allclose(data.max(), 2.94, atol=0.01) diff --git a/pygmt/tests/test_datasets_earth_free_air_anomaly.py b/pygmt/tests/test_datasets_earth_free_air_anomaly.py index 517a1bb9b89..7ce5fc2662c 100644 --- a/pygmt/tests/test_datasets_earth_free_air_anomaly.py +++ b/pygmt/tests/test_datasets_earth_free_air_anomaly.py @@ -52,3 +52,54 @@ def test_earth_faa_01m_default_registration(): npt.assert_allclose(data.coords["lon"].data.max(), -9.00833333) npt.assert_allclose(data.min(), -49.225, atol=0.025) npt.assert_allclose(data.max(), 115.0, atol=0.025) + + +def test_earth_faaerror_01d(): + """ + Test some properties of the free air anomaly error 01d data. + """ + data = load_earth_free_air_anomaly(resolution="01d", uncertainty=True) + assert data.name == "z" + assert data.attrs["long_name"] == "faaerror (mGal)" + assert data.attrs["description"] == "IGPP Earth free-air anomaly errors" + assert data.attrs["units"] == "mGal" + assert data.attrs["horizontal_datum"] == "WGS84" + assert data.shape == (181, 361) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-90, 91, 1)) + npt.assert_allclose(data.lon, np.arange(-180, 181, 1)) + npt.assert_allclose(data.min(), 0.0, atol=0.04) + npt.assert_allclose(data.max(), 49.16, atol=0.04) + + +def test_earth_faaerror_01d_with_region(): + """ + Test loading low-resolution earth free air anomaly error with 'region'. + """ + data = load_earth_free_air_anomaly( + resolution="01d", region=[-10, 10, -5, 5], uncertainty=True + ) + assert data.shape == (11, 21) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-5, 6, 1)) + npt.assert_allclose(data.lon, np.arange(-10, 11, 1)) + npt.assert_allclose(data.min(), 0.72, atol=0.04) + npt.assert_allclose(data.max(), 21.04, atol=0.04) + + +def test_earth_faaerror_01m_default_registration(): + """ + Test that the grid returned by default for the 1 arc-minute resolution has a "pixel" + registration. + """ + data = load_earth_free_air_anomaly( + resolution="01m", region=[-10, -9, 3, 5], uncertainty=True + ) + assert data.shape == (120, 60) + assert data.gmt.registration == 1 + npt.assert_allclose(data.coords["lat"].data.min(), 3.008333333) + npt.assert_allclose(data.coords["lat"].data.max(), 4.991666666) + npt.assert_allclose(data.coords["lon"].data.min(), -9.99166666) + npt.assert_allclose(data.coords["lon"].data.max(), -9.00833333) + npt.assert_allclose(data.min(), 0.40, atol=0.04) + npt.assert_allclose(data.max(), 13.36, atol=0.04) diff --git a/pygmt/tests/test_datasets_earth_geoid.py b/pygmt/tests/test_datasets_earth_geoid.py index 84bfc5d7bf4..af72969b032 100644 --- a/pygmt/tests/test_datasets_earth_geoid.py +++ b/pygmt/tests/test_datasets_earth_geoid.py @@ -15,7 +15,7 @@ def test_earth_geoid_01d(): assert data.name == "z" assert data.attrs["long_name"] == "geoid (m)" assert data.attrs["description"] == "EGM2008 Earth geoid" - assert data.attrs["units"] == "m" + assert data.attrs["units"] == "meters" assert data.attrs["horizontal_datum"] == "WGS84" assert data.shape == (181, 361) assert data.gmt.registration == 0 diff --git a/pygmt/tests/test_datasets_earth_mean_sea_surface.py b/pygmt/tests/test_datasets_earth_mean_sea_surface.py new file mode 100644 index 00000000000..84b2b7123cc --- /dev/null +++ b/pygmt/tests/test_datasets_earth_mean_sea_surface.py @@ -0,0 +1,53 @@ +""" +Test basic functionality for loading Earth mean sea surface datasets. +""" + +import numpy as np +import numpy.testing as npt +from pygmt.datasets import load_earth_mean_sea_surface + + +def test_earth_mss_01d(): + """ + Test some properties of the Earth mean sea surface 01d data. + """ + data = load_earth_mean_sea_surface(resolution="01d") + assert data.name == "z" + assert data.attrs["description"] == "CNES Earth mean sea surface" + assert data.attrs["units"] == "meters" + assert data.attrs["horizontal_datum"] == "WGS84" + assert data.shape == (181, 361) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-90, 91, 1)) + npt.assert_allclose(data.lon, np.arange(-180, 181, 1)) + npt.assert_allclose(data.min(), -104.71, atol=0.01) + npt.assert_allclose(data.max(), 82.38, atol=0.01) + + +def test_earth_mss_01d_with_region(): + """ + Test loading low-resolution Earth mean sea surface with "region". + """ + data = load_earth_mean_sea_surface(resolution="01d", region=[-10, 10, -5, 5]) + assert data.shape == (11, 21) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-5, 6, 1)) + npt.assert_allclose(data.lon, np.arange(-10, 11, 1)) + npt.assert_allclose(data.min(), 6.53, atol=0.01) + npt.assert_allclose(data.max(), 29.31, atol=0.01) + + +def test_earth_mss_01m_default_registration(): + """ + Test that the grid returned by default for the 1 arc-minute resolution has a + "gridline" registration. + """ + data = load_earth_mean_sea_surface(resolution="01m", region=[-10, -9, 3, 5]) + assert data.shape == (121, 61) + assert data.gmt.registration == 0 + assert data.coords["lat"].data.min() == 3.0 + assert data.coords["lat"].data.max() == 5.0 + assert data.coords["lon"].data.min() == -10.0 + assert data.coords["lon"].data.max() == -9.0 + npt.assert_allclose(data.min(), 21.27, atol=0.01) + npt.assert_allclose(data.max(), 31.11, atol=0.01) diff --git a/pygmt/tests/test_datasets_earth_relief.py b/pygmt/tests/test_datasets_earth_relief.py index 44af0e4ff98..b0851430907 100644 --- a/pygmt/tests/test_datasets_earth_relief.py +++ b/pygmt/tests/test_datasets_earth_relief.py @@ -192,6 +192,7 @@ def test_earth_relief_15s_default_registration(): npt.assert_allclose(data.max(), -76.5, atol=0.5) +# TODO(GMT X.Y.Z): Upstream bug which is not fixed yet. @pytest.mark.xfail( condition=Version(__gmt_version__) >= Version("6.5.0"), reason="Upstream bug tracked in https://github.com/GenericMappingTools/pygmt/issues/2511", diff --git a/pygmt/tests/test_datasets_mean_dynamic_topography.py b/pygmt/tests/test_datasets_mean_dynamic_topography.py new file mode 100644 index 00000000000..deae6e90a60 --- /dev/null +++ b/pygmt/tests/test_datasets_mean_dynamic_topography.py @@ -0,0 +1,53 @@ +""" +Test basic functionality for loading Earth mean dynamic topography datasets. +""" + +import numpy as np +import numpy.testing as npt +from pygmt.datasets import load_earth_mean_dynamic_topography + + +def test_earth_mdt_01d(): + """ + Test some properties of the Earth mean dynamic topography 01d data. + """ + data = load_earth_mean_dynamic_topography(resolution="01d") + assert data.name == "z" + assert data.attrs["description"] == "CNES Earth mean dynamic topography" + assert data.attrs["units"] == "meters" + assert data.attrs["horizontal_datum"] == "WGS84" + assert data.shape == (181, 361) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-90, 91, 1)) + npt.assert_allclose(data.lon, np.arange(-180, 181, 1)) + npt.assert_allclose(data.min(), -1.4668, atol=0.0001) + npt.assert_allclose(data.max(), 1.7151, atol=0.0001) + + +def test_earth_mdt_01d_with_region(): + """ + Test loading low-resolution Earth mean dynamic topography with "region". + """ + data = load_earth_mean_dynamic_topography(resolution="01d", region=[-10, 10, -5, 5]) + assert data.shape == (11, 21) + assert data.gmt.registration == 0 + npt.assert_allclose(data.lat, np.arange(-5, 6, 1)) + npt.assert_allclose(data.lon, np.arange(-10, 11, 1)) + npt.assert_allclose(data.min(), 0.346, atol=0.0001) + npt.assert_allclose(data.max(), 0.4839, atol=0.0001) + + +def test_earth_mdt_07m_default_registration(): + """ + Test that the grid returned by default for the 7 arc-minutes resolution has a + "gridline" registration. + """ + data = load_earth_mean_dynamic_topography(resolution="07m", region=[-10, -9, 3, 5]) + assert data.shape == (17, 9) + assert data.gmt.registration == 0 + assert data.coords["lat"].data.min() == 3.0 + assert data.coords["lat"].data.max() == 5.0 + assert data.coords["lon"].data.min() == -10.0 + assert data.coords["lon"].data.max() == -9.0 + npt.assert_allclose(data.min(), 0.4138, atol=0.0001) + npt.assert_allclose(data.max(), 0.4302, atol=0.0001) diff --git a/pygmt/tests/test_grdimage.py b/pygmt/tests/test_grdimage.py index 943b3f12ddc..907886eb0b3 100644 --- a/pygmt/tests/test_grdimage.py +++ b/pygmt/tests/test_grdimage.py @@ -256,6 +256,7 @@ def test_grdimage_imgout_fails(grid): fig.grdimage(grid, A="out.png") +# TODO(GMT>6.5.0): Remove the xfail marker for GMT<=6.5.0. @pytest.mark.xfail( condition=Version(__gmt_version__) <= Version("6.5.0"), reason="Upstream bug fixed in https://github.com/GenericMappingTools/gmt/pull/8554", diff --git a/pygmt/tests/test_grdview.py b/pygmt/tests/test_grdview.py index f73b1150e54..3be4ed7aa42 100644 --- a/pygmt/tests/test_grdview.py +++ b/pygmt/tests/test_grdview.py @@ -161,7 +161,7 @@ def test_grdview_with_perspective_and_zaxis_frame(xrgrid, region): a Transverse Mercator (T) projection. """ fig = Figure() - projection = f"T{(region[0]+region[1])/2}/{abs((region[2]+region[3])/2)}" + projection = f"T{(region[0] + region[1]) / 2}/{abs((region[2] + region[3]) / 2)}" fig.grdview( grid=xrgrid, projection=projection, diff --git a/pygmt/tests/test_hlines.py b/pygmt/tests/test_hlines.py new file mode 100644 index 00000000000..aaaddad4f08 --- /dev/null +++ b/pygmt/tests/test_hlines.py @@ -0,0 +1,121 @@ +""" +Tests for Figure.hlines. +""" + +import pytest +from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput + + +@pytest.mark.mpl_image_compare +def test_hlines_one_line(): + """ + Plot one horizontal line. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.hlines(1) + fig.hlines(2, xmin=1) + fig.hlines(3, xmax=9) + fig.hlines(4, xmin=3, xmax=8) + fig.hlines(5, xmin=4, xmax=8, pen="1p,blue", label="Line at y=5") + fig.hlines(6, xmin=5, xmax=7, pen="1p,red", label="Line at y=6") + fig.legend() + return fig + + +@pytest.mark.mpl_image_compare +def test_hlines_multiple_lines(): + """ + Plot multiple horizontal lines. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 16], projection="X10c/10c", frame=True) + fig.hlines([1, 2]) + fig.hlines([3, 4, 5], xmin=[1, 2, 3]) + fig.hlines([6, 7, 8], xmax=[7, 8, 9]) + fig.hlines([9, 10], xmin=[1, 2], xmax=[9, 10]) + fig.hlines([11, 12], xmin=1, xmax=9, pen="1p,blue", label="Lines at y=11,12") + fig.hlines( + [13, 14], xmin=[3, 4], xmax=[8, 9], pen="1p,red", label="Lines at y=13,14" + ) + fig.legend() + return fig + + +@pytest.mark.mpl_image_compare +def test_hlines_clip(): + """ + Plot horizontal lines with clipping or not. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 4], projection="X10c/4c", frame=True) + fig.hlines(1, xmin=-2, xmax=12) + fig.hlines(2, xmin=-2, xmax=12, no_clip=True) + return fig + + +@pytest.mark.mpl_image_compare +@pytest.mark.parametrize("region", ["g", "d"]) +def test_hlines_geographic_global(region): + """ + Plot horizontal lines in geographic coordinates. + """ + fig = Figure() + fig.basemap(region=region, projection="R15c", frame=True) + # Plot lines with longitude range of 0 to 360. + fig.hlines(10, pen="1p") + fig.hlines(20, xmin=0, xmax=360, pen="1p") + fig.hlines(30, xmin=0, xmax=180, pen="1p") + fig.hlines(40, xmin=180, xmax=360, pen="1p") + fig.hlines(50, xmin=0, xmax=90, pen="1p") + fig.hlines(60, xmin=90, xmax=180, pen="1p") + fig.hlines(70, xmin=180, xmax=270, pen="1p") + fig.hlines(80, xmin=270, xmax=360, pen="1p") + + # Plot lines with longitude range of -180 to 180. + fig.hlines(-10, pen="1p,red") + fig.hlines(-20, xmin=-180, xmax=180, pen="1p,red") + fig.hlines(-30, xmin=-180, xmax=0, pen="1p,red") + fig.hlines(-40, xmin=0, xmax=180, pen="1p,red") + fig.hlines(-50, xmin=-180, xmax=-90, pen="1p,red") + fig.hlines(-60, xmin=-90, xmax=0, pen="1p,red") + fig.hlines(-70, xmin=0, xmax=90, pen="1p,red") + fig.hlines(-80, xmin=90, xmax=180, pen="1p,red") + return fig + + +@pytest.mark.mpl_image_compare +def test_hlines_polar_projection(): + """ + Plot horizontal lines in polar projection. + """ + fig = Figure() + fig.basemap(region=[0, 360, 0, 1], projection="P15c", frame=True) + fig.hlines(0.1, pen="1p") + fig.hlines(0.2, xmin=0, xmax=360, pen="1p") + fig.hlines(0.3, xmin=0, xmax=180, pen="1p") + fig.hlines(0.4, xmin=180, xmax=360, pen="1p") + fig.hlines(0.5, xmin=0, xmax=90, pen="1p") + fig.hlines(0.6, xmin=90, xmax=180, pen="1p") + fig.hlines(0.7, xmin=180, xmax=270, pen="1p") + fig.hlines(0.8, xmin=270, xmax=360, pen="1p") + return fig + + +def test_hlines_invalid_input(): + """ + Test invalid input for hlines. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 6], projection="X10c/6c", frame=True) + with pytest.raises(GMTInvalidInput): + fig.hlines(1, xmin=2, xmax=[3, 4]) + with pytest.raises(GMTInvalidInput): + fig.hlines(1, xmin=[2, 3], xmax=4) + with pytest.raises(GMTInvalidInput): + fig.hlines(1, xmin=[2, 3], xmax=[4, 5]) + with pytest.raises(GMTInvalidInput): + fig.hlines([1, 2], xmin=[2, 3, 4], xmax=3) + with pytest.raises(GMTInvalidInput): + fig.hlines([1, 2], xmin=[2, 3], xmax=[4, 5, 6]) diff --git a/pygmt/tests/test_info.py b/pygmt/tests/test_info.py index 3ac9f27c4e1..d055abb61ec 100644 --- a/pygmt/tests/test_info.py +++ b/pygmt/tests/test_info.py @@ -23,10 +23,7 @@ def test_info(): """ output = info(data=POINTS_DATA) expected_output = ( - f"{POINTS_DATA}: N = 20 " - "<11.5309/61.7074> " - "<-2.9289/7.8648> " - "<0.1412/0.9338>\n" + f"{POINTS_DATA}: N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" ) assert output == expected_output @@ -57,10 +54,7 @@ def test_info_path(table): """ output = info(data=table) expected_output = ( - f"{POINTS_DATA}: N = 20 " - "<11.5309/61.7074> " - "<-2.9289/7.8648> " - "<0.1412/0.9338>\n" + f"{POINTS_DATA}: N = 20 <11.5309/61.7074> <-2.9289/7.8648> <0.1412/0.9338>\n" ) assert output == expected_output diff --git a/pygmt/tests/test_meca.py b/pygmt/tests/test_meca.py index 2e71cc9665d..424486ac408 100644 --- a/pygmt/tests/test_meca.py +++ b/pygmt/tests/test_meca.py @@ -143,6 +143,7 @@ def test_meca_spec_multiple_focalmecha(inputtype): return fig +# TODO(GMT>=6.5.0): Remove the skipif condition for GMT>=6.5.0. @pytest.mark.mpl_image_compare(filename="test_meca_offset.png") @pytest.mark.parametrize( "inputtype", @@ -201,8 +202,9 @@ def test_meca_offset(inputtype): return fig -# Passing event names via pandas doesn't work for GMT<=6.4, thus marked as -# xfail. See https://github.com/GenericMappingTools/pygmt/issues/2524. +# TODO(GMT>=6.5.0): Remove the skipif marker for GMT>=6.5.0. +# Passing event names via pandas doesn't work for GMT<=6.4. +# See https://github.com/GenericMappingTools/pygmt/issues/2524. @pytest.mark.mpl_image_compare(filename="test_meca_eventname.png") @pytest.mark.parametrize( "inputtype", diff --git a/pygmt/tests/test_plot.py b/pygmt/tests/test_plot.py index 721b7841307..c2f2b846724 100644 --- a/pygmt/tests/test_plot.py +++ b/pygmt/tests/test_plot.py @@ -467,9 +467,14 @@ def test_plot_datetime(): fig.plot(x=x, y=y, style="a0.2c", pen="1p") # the Python built-in datetime and date - x = [datetime.date(2018, 1, 1), datetime.datetime(2019, 1, 1)] + x = [datetime.date(2018, 1, 1), datetime.datetime(2019, 1, 1, 0, 0, 0)] y = [8.5, 9.5] fig.plot(x=x, y=y, style="i0.2c", pen="1p") + + # Python sequence of pd.Timestamp + x = [pd.Timestamp("2018-01-01"), pd.Timestamp("2019-01-01")] + y = [5.5, 6.5] + fig.plot(x=x, y=y, style="d0.2c", pen="1p") return fig diff --git a/pygmt/tests/test_vlines.py b/pygmt/tests/test_vlines.py new file mode 100644 index 00000000000..21aff1c06d5 --- /dev/null +++ b/pygmt/tests/test_vlines.py @@ -0,0 +1,102 @@ +""" +Tests for Figure.vlines. +""" + +import pytest +from pygmt import Figure +from pygmt.exceptions import GMTInvalidInput + + +@pytest.mark.mpl_image_compare +def test_vlines_one_line(): + """ + Plot one vertical line. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 10], projection="X10c/10c", frame=True) + fig.vlines(1) + fig.vlines(2, ymin=1) + fig.vlines(3, ymax=9) + fig.vlines(4, ymin=3, ymax=8) + fig.vlines(5, ymin=4, ymax=8, pen="1p,blue", label="Line at x=5") + fig.vlines(6, ymin=5, ymax=7, pen="1p,red", label="Line at x=6") + fig.legend() + return fig + + +@pytest.mark.mpl_image_compare +def test_vlines_multiple_lines(): + """ + Plot multiple vertical lines. + """ + fig = Figure() + fig.basemap(region=[0, 16, 0, 10], projection="X10c/10c", frame=True) + fig.vlines([1, 2]) + fig.vlines([3, 4, 5], ymin=[1, 2, 3]) + fig.vlines([6, 7, 8], ymax=[7, 8, 9]) + fig.vlines([9, 10], ymin=[1, 2], ymax=[9, 10]) + fig.vlines([11, 12], ymin=1, ymax=8, pen="1p,blue", label="Lines at x=11,12") + fig.vlines( + [13, 14], ymin=[3, 4], ymax=[7, 8], pen="1p,red", label="Lines at x=13,14" + ) + fig.legend() + return fig + + +@pytest.mark.mpl_image_compare +def test_vlines_clip(): + """ + Plot vertical lines with clipping or not. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 4], projection="X10c/4c", frame=True) + fig.vlines(1, ymin=-1, ymax=5) + fig.vlines(2, ymin=-1, ymax=5, no_clip=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_vlines_geographic_global(): + """ + Plot vertical lines in geographic coordinates. + """ + fig = Figure() + fig.basemap(region=[-180, 180, -90, 90], projection="R15c", frame="a30g30") + fig.vlines(30, pen="1p") + fig.vlines(90, ymin=-60, pen="1p,blue") + fig.vlines(-90, ymax=60, pen="1p,blue") + fig.vlines(120, ymin=-60, ymax=60, pen="1p,blue") + return fig + + +@pytest.mark.mpl_image_compare +def test_vlines_polar_projection(): + """ + Plot vertical lines in polar projection. + """ + fig = Figure() + fig.basemap(region=[0, 360, 0, 1], projection="P15c", frame=True) + fig.vlines(0, pen="1p") + fig.vlines(30, ymin=0, ymax=1, pen="1p") + fig.vlines(60, ymin=0.5, pen="1p") + fig.vlines(90, ymax=0.5, pen="1p") + fig.vlines(120, ymin=0.25, ymax=0.75, pen="1p") + return fig + + +def test_vlines_invalid_input(): + """ + Test invalid input for vlines. + """ + fig = Figure() + fig.basemap(region=[0, 10, 0, 6], projection="X10c/6c", frame=True) + with pytest.raises(GMTInvalidInput): + fig.vlines(1, ymin=2, ymax=[3, 4]) + with pytest.raises(GMTInvalidInput): + fig.vlines(1, ymin=[2, 3], ymax=4) + with pytest.raises(GMTInvalidInput): + fig.vlines(1, ymin=[2, 3], ymax=[4, 5]) + with pytest.raises(GMTInvalidInput): + fig.vlines([1, 2], ymin=[2, 3, 4], ymax=3) + with pytest.raises(GMTInvalidInput): + fig.vlines([1, 2], ymin=[2, 3], ymax=[4, 5, 6]) diff --git a/pygmt/tests/test_x2sys_cross.py b/pygmt/tests/test_x2sys_cross.py index c72cca04420..70f0c3cf003 100644 --- a/pygmt/tests/test_x2sys_cross.py +++ b/pygmt/tests/test_x2sys_cross.py @@ -37,6 +37,7 @@ def fixture_tracks(): return [dataframe.query(expr="z > -20")] # reduce size of dataset +# TODO(GMT>=6.5.0): Remove the xfail marker for the upstream bug fixed in GMT 6.5.0. @pytest.mark.usefixtures("mock_x2sys_home") @pytest.mark.xfail( condition=Version(__gmt_version__) < Version("6.5.0"), @@ -66,6 +67,7 @@ def test_x2sys_cross_input_file_output_file(): npt.assert_allclose(result["i_1"].max(), 82945.9370, rtol=1.0e-4) +# TODO(GMT>=6.5.0): Remove the xfail marker for the upstream bug fixed in GMT 6.5.0. @pytest.mark.usefixtures("mock_x2sys_home") @pytest.mark.xfail( condition=Version(__gmt_version__) < Version("6.5.0"), @@ -244,6 +246,7 @@ def test_x2sys_cross_invalid_tracks_input_type(tracks): x2sys_cross(tracks=[invalid_tracks]) +# TODO(GMT>=6.5.0): Remove the xfail marker for the upstream bug fixed in GMT 6.5.0. @pytest.mark.usefixtures("mock_x2sys_home") @pytest.mark.xfail( condition=Version(__gmt_version__) < Version("6.5.0"), @@ -279,6 +282,7 @@ def test_x2sys_cross_region_interpolation_numpoints(): npt.assert_allclose(output.z_M.mean(), -2896.875915, rtol=1e-4) +# TODO(GMT>=6.5.0): Remove the xfail marker for the upstream bug fixed in GMT 6.5.0. @pytest.mark.usefixtures("mock_x2sys_home") @pytest.mark.xfail( condition=Version(__gmt_version__) < Version("6.5.0"), diff --git a/pyproject.toml b/pyproject.toml index b4907e6dc64..61c6a541fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,10 +48,11 @@ all = [ ] [project.urls] -homepage = "https://www.pygmt.org" -documentation = "https://www.pygmt.org" -repository = "https://github.com/GenericMappingTools/pygmt" -changelog = "https://www.pygmt.org/latest/changes.html" +"Homepage" = "https://www.pygmt.org" +"Documentation" = "https://www.pygmt.org" +"Source Code" = "https://github.com/GenericMappingTools/pygmt" +"Changelog" = "https://www.pygmt.org/latest/changes.html" +"Issue Tracker" = "https://github.com/GenericMappingTools/pygmt/issues" [tool.setuptools] platforms = ["Any"] @@ -66,7 +67,7 @@ local_scheme = "node-and-date" fallback_version = "999.999.999+unknown" [tool.codespell] -ignore-words-list = "astroid,oints,reenable,tripel,trough" +ignore-words-list = "astroid,oints,reenable,tripel,trough,ND" [tool.coverage.run] omit = ["*/tests/*", "*pygmt/__init__.py"] @@ -119,6 +120,7 @@ select = [ "SIM", # flake8-simplify "T20", # flake8-print "TC", # flake8-type-checking + "TD", # flake8-todos "TID", # flake8-tidy-imports "TRY", # tryceratops "UP", # pyupgrade @@ -147,6 +149,7 @@ ignore = [ "RET504", # Allow variable assignment and return immediately for readability "S603", # Allow method calls that initiate a subprocess without a shell "SIM117", # Allow nested `with` statements + "TD003", # Allow TODO comments without associated issue link ] preview = true explicit-preview-rules = true @@ -157,7 +160,10 @@ known-third-party = ["pygmt"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] # Ignore `F401` (unused-import) in all `__init__.py` files "*/tests/test_*.py" = ["S101"] # Ignore `S101` (use of assert) in all tests files -"examples/**/*.py" = ["T201"] # Allow `print` in examples +"examples/**/*.py" = [ # Ignore rules in examples + "B018", # Allow useless expressions in Jupyter Notebooks + "T201", # Allow `print` statements +] [tool.ruff.lint.pycodestyle] max-doc-length = 88