diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4603ea3e..18b06ab5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,10 @@ on: - cron: '0 0 * * *' # Daily “At 00:00” workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: name: Python (${{ matrix.python-version }}, ${{ matrix.os }}) @@ -15,40 +19,24 @@ jobs: defaults: run: shell: bash -l {0} - strategy: fail-fast: false matrix: - os: [ "ubuntu-latest", "macos-latest", "macos-14", "windows-latest"] - python-version: [ "3.9", "3.11", "3.12" ] + os: [ "ubuntu-latest", "macos-latest", "windows-latest"] + python-version: [ "3.9", "3.10", "3.11", "3.12" ] steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.12.1 - with: - access_token: ${{ github.token }} - name: checkout uses: actions/checkout@v4 - with: - token: ${{ github.token }} - - name: conda_setup - uses: conda-incubator/setup-miniconda@v3 - if: matrix.os != 'macos-14' - with: - activate-environment: geocat_viz_build - channel-priority: strict - python-version: ${{ matrix.python-version }} - channels: conda-forge - environment-file: build_envs/environment.yml - - name: conda_setup_m1 + + - name: environment setup uses: conda-incubator/setup-miniconda@v3 - if: matrix.os == 'macos-14' with: - installer-url: https://github.com/conda-forge/miniforge/releases/download/23.11.0-0/Mambaforge-23.11.0-0-MacOSX-arm64.sh activate-environment: geocat_viz_build channel-priority: strict python-version: ${{ matrix.python-version }} channels: conda-forge environment-file: build_envs/environment.yml + - name: Install geocat-viz run: | python -m pip install . --no-deps @@ -57,6 +45,10 @@ jobs: run: | conda list + - name: tests + run: | + python -m pytest + link-check: runs-on: ubuntu-latest defaults: @@ -65,9 +57,8 @@ jobs: steps: - name: checkout uses: actions/checkout@v4 - with: - token: ${{ github.token }} - - name: conda_setup + + - name: environment setup uses: conda-incubator/setup-miniconda@v3 with: activate-environment: gv-docs @@ -75,12 +66,15 @@ jobs: python-version: 3.9 channels: conda-forge environment-file: build_envs/docs.yml + - name: Install geocat-viz run: | python -m pip install . + - name: check conda list run: | conda list + - name: Make docs with linkcheck run: | cd docs diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml deleted file mode 100644 index 8cae21f1..00000000 --- a/.github/workflows/pre-commit.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: pre-commit - -on: - pull_request: - push: - branches: - - main - -jobs: - pre-commit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index ed14d32e..67876b41 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -49,7 +49,7 @@ jobs: python -m twine check dist/* - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.10.3 with: skip-existing: true verbose: true diff --git a/.gitignore b/.gitignore index c7aefe82..700f18dd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ docs/_build/ dist/ .eggs/ *egg-info + +# IDEs +settings.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 81743682..4efe2085 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,18 +1,19 @@ repos: -- repo: https://github.com/pre-commit/mirrors-yapf # To format the code to conform YAPF - rev: v0.31.0 +- repo: https://github.com/google/yapf # To format the code to conform YAPF + rev: v0.40.2 hooks: - id: yapf args: ['--in-place', '--recursive', '--style', 'google'] + require_serial: true -- repo: https://github.com/myint/docformatter # To format the doc strings to conform PEP257 - rev: v1.4 +- repo: https://github.com/PyCQA/docformatter # To format the doc strings to conform PEP257 + rev: v1.7.5 hooks: - id: docformatter args: [--in-place] - repo: https://github.com/pre-commit/pre-commit-hooks # Some common pre-commit hooks - rev: v3.4.0 + rev: v4.6.0 hooks: - id: check-yaml # Checks the syntax of .yaml files. args: [--allow-multiple-documents] @@ -20,3 +21,8 @@ repos: - id: end-of-file-fixer # Makes sure files end with a newline. - id: trailing-whitespace # Checks for any tabs or spaces after the last non-whitespace character on the line. - id: check-docstring-first # Checks that code comes after the docstrings. + - id: check-yaml # Check valid yml file + +ci: + autofix_prs: false + autoupdate_schedule: monthly diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..b149e8ae --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,18 @@ +include LICENSE +include README.md + +recursive-include geocat *.py +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] +exclude .gitignore +exclude .pre-commit-config.yaml +exclude .readthedocs.yml + +include *.md +include *.toml +include *.yaml +include *.txt +prune test* +prune .github* +prune docs* +prune build_envs* diff --git a/README.md b/README.md index 773c6c14..36e1ec3d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # GeoCAT-viz -The GeoCAT-viz repo contains tools to help plot data, including convenience and plotting functions that are used -to facilitate plotting geosciences data with Matplotlib, Cartopy, and possibly other Python ecosystem -plotting packages. GeoCAT-viz functionality is used by -[GeoCAT-examples](https://github.com/NCAR/geocat-examples) and is of possible use for other GeoCAT components. +GeoCAT-viz contains tools to help plot geoscience data, including convenience and plotting functions that are +used to facilitate plotting geosciences data with Matplotlib, Cartopy, and other visualization packages. +GeoCAT-viz functionality is used by +[GeoCAT-examples](https://github.com/NCAR/geocat-examples). -| **Docs** | [![Documentation Status][rtd-badge]][rtd-link] | +| **CI** | [![GitHub Workflow Status][github-ci-badge]][github-ci-link] | |:------------------------:|:--------------------------------------------------------------------------------:| +| **Docs** | [![Documentation Status][rtd-badge]][rtd-link] | | **Package** | [![Conda][conda-badge]][conda-link] [![PyPI][pypi-badge]][pypi-link] | | **License** | [![License][license-badge]][license-badge-link] | | **DOI** | [![DOI][doi-badge]][doi-link] | @@ -31,6 +32,8 @@ https://geocat-viz.readthedocs.io/en/latest/citation.html) page. [comment]: <> (reference links used for badges.) +[github-ci-badge]: https://img.shields.io/github/actions/workflow/status/NCAR/geocat-viz/ci.yml?branch=main&label=CI&style=for-the-badge +[github-ci-link]: https://github.com/NCAR/geocat-viz/actions/workflows/ci.yml [rtd-badge]: https://img.shields.io/readthedocs/geocat-viz/latest.svg?style=for-the-badge [rtd-link]: https://geocat-viz.readthedocs.io/en/latest/?badge=latest [pypi-badge]: https://img.shields.io/pypi/v/geocat-viz?logo=pypix&style=for-the-badge diff --git a/build_envs/environment.yml b/build_envs/environment.yml index b78dfcc9..f06c8eb3 100644 --- a/build_envs/environment.yml +++ b/build_envs/environment.yml @@ -7,13 +7,13 @@ dependencies: - make - matplotlib - sphinx - - pip - cmaps - numpy - xarray - metpy - pint + - pytest + - pytest-mpl - geocat-datafiles - - pip: - - pre-commit - - scikit-learn + - pre-commit + - scikit-learn diff --git a/docs/conf.py b/docs/conf.py index a025d729..4a2eaf4c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -369,6 +369,9 @@ def update_gallery(app: Sphinx): # generate warning for all invalid links #nitpicky = True +# add links to ignore during link checking +linkcheck_ignore = [r'https://stackoverflow.com/*'] + # Allow for changes to be made to the css in the theme_overrides file def setup(app): diff --git a/docs/examples/taylor.ipynb b/docs/examples/taylor.ipynb index 3b696837..1f51d212 100644 --- a/docs/examples/taylor.ipynb +++ b/docs/examples/taylor.ipynb @@ -57,7 +57,7 @@ "\n", "# Draw diagonal dashed lines from origin to correlation values\n", "# Also enforces proper X-Y ratio\n", - "taylor.add_xgrid(np.array([0.6, 0.9]))\n", + "taylor.add_corr_grid(np.array([0.6, 0.9]))\n", "\n", "# Add models to Taylor diagram\n", "taylor.add_model_set(a_sdev,\n", diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 610014b7..fb839e7b 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -5,8 +5,39 @@ Release Notes ============= -v2024.03.0 (Unreleased) ------------------------ +v2024.xx.0 (unreleased) +--------------------------- + +Internal Changes +^^^^^^^^^^^^^^^^ +* Update pre-commit versions and configuration by `Katelyn FitzGerald`_ in (:pr:`247`) +* Remove M1 workaround for CI and tokens that are no longer needed by `Katelyn FitzGerald`_ in (:pr:`232`) + +Testing +^^^^^^^ +* Add basic testing infrastructure by `Katelyn FitzGerald`_ and `Julia Kent`_ in (:pr:`233`) +* Add tests for `viz.taylor` by `Julia Kent`_ in (:pr:`234`) +* Add tests for `viz.util` by `Julia Kent`_ in (:pr:`239`) +* Remove now duplicative macOS runner from CI and add back in Python 3.10 by `Katelyn FitzGerald`_ in (:pr:`240`) + + +v2024.03.0 (March 26, 2024) +--------------------------- +This release deprecates ``TaylorDiagram`` method names ``add_xgrid()`` and +``add_ygrid()`` in favor of the more descriptive ``add_corr_grid()`` and ``add_std_grid()``. + +Documentation +^^^^^^^^^^^^^ +* Document known issue with `suptitle` argument of ``set_titles_and_labels`` and Cartopy plots by `Julia Kent`_ in (:pr:`219`) + +Deprecations +^^^^^^^^^^ +* Pending deprecation warnings added for ``TaylorDiagram`` methods ``add_xgrid`` and ``add_ygrid`` which are changing to the new ``add_corr_grid`` and ``add_std_grid`` by `Julia Kent`_ in (:pr:`219`) + + +v2024.02.1 (February 28, 2024) +------------------------------ +This release changes to implicit namespace packaging and addresses a bug in the Taylor diagram functionality when disabling ``annotate_on``. Bug Fixes ^^^^^^^^^ @@ -15,6 +46,8 @@ Bug Fixes Internal Changes ^^^^^^^^^^^^^^^^ * Switch to PyPI Trusted Publishing by `Orhan Eroglu`_ in (:pr:`208`) +* Add ``linkcheck_ignore`` to ``docs/conf.py`` to address erroneous failures and add CI badge to README by `Katelyn FitzGerald`_ in (:pr:`218`) +* Convert to implicit namespace packaging set up by `Anissa Zacharias`_ in (:pr:`220`) v2024.02.0 (February 6, 2024) ----------------------------- @@ -122,3 +155,4 @@ Documentation .. _`Katelyn Fitzgerald`: https://github.com/kafitzgerald .. _`Simon Rosanka`: https://github.com/srosanka .. _`Orhan Eroglu`: https://github.com/erogluorhan +.. _`Anissa Zacharias`: https://github.com/anissa111 diff --git a/setup.cfg b/setup.cfg index 302e7692..9711735f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,9 +29,7 @@ include_package_data = True python_requires = >=3.9, <3.13 package_dir = =src -packages = - geocat - geocat.viz +packages = find_namespace: setup_requires = setuptools_scm setuptools @@ -45,6 +43,12 @@ install_requires = setuptools scikit-learn metpy +tests_require = + pytest + pytest-mpl + +[options.packages.find] +where = src [options.extras_require] docs = @@ -63,7 +67,7 @@ docs = [tool:pytest] python_files = test_*.py -testpaths = test +testpaths = tests [aliases] test = pytest diff --git a/src/geocat/__init__.py b/src/geocat/__init__.py deleted file mode 100644 index de40ea7c..00000000 --- a/src/geocat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/src/geocat/viz/taylor.py b/src/geocat/viz/taylor.py index 1183b301..0911c2d6 100644 --- a/src/geocat/viz/taylor.py +++ b/src/geocat/viz/taylor.py @@ -1,5 +1,5 @@ """Taylor Diagrams.""" - +import warnings import typing import numpy as np @@ -44,6 +44,11 @@ class TaylorDiagram(object): stdLevel : list Optional list of tick locations for stddev axis + Notes + ----- + Rendering of Taylor Diagrams looks best with a figure that is at least 10x10 inches. If you make your figure size too small, + legends, lagels, and other text items might overlap in undesired ways. + References ---------- - https://validate-climate-model-validation.readthedocs.io/en/latest/_modules/validate/taylor.html @@ -377,13 +382,13 @@ def add_model_set(self, return modelTexts, modelset - def add_xgrid(self, - arr: typing.Union[xr.DataArray, np.ndarray, list, float], - color: str = 'lightgray', - linestyle=(0, (9, 5)), - linewidth: float = 0.5, - **kwargs): - """Add gridlines to the X axis (correlation) specified by array *arr* + def add_corr_grid(self, + arr: typing.Union[xr.DataArray, np.ndarray, list, float], + color: str = 'lightgray', + linestyle=(0, (9, 5)), + linewidth: float = 0.5, + **kwargs): + """Add gridlines to the correlation axis specified by array *arr* Parameters ---------- @@ -411,7 +416,7 @@ def add_xgrid(self, -------- All usage examples are within the GeoCAT-Examples Gallery. To see more usage cases, search the function on the `website `_. - - `NCL_taylor_2.py `_ + - `NCL_taylor_2.py `_ """ for value in arr: @@ -423,14 +428,34 @@ def add_xgrid(self, linewidth=linewidth, **kwargs) - def add_ygrid(self, + def add_xgrid(self, arr: typing.Union[xr.DataArray, np.ndarray, list, float], color: str = 'lightgray', linestyle=(0, (9, 5)), - linewidth: int = 1, + linewidth: float = 0.5, **kwargs): - """Add gridlines (radii) to the Y axis (standard deviation) specified - by array *arr* + """Add gridlines to the correlation axis specified by array *arr*. + + This method will be deprecated in favor of + `TaylorDiagram.add_corr_grid()` + """ + + warnings.warn( + '`TaylorDiagram.add_xgrid` will be deprecated in the future. Please use `TaylorDiagram.add_corr_grid` instead.', + PendingDeprecationWarning) + + return self.add_corr_grid(arr, color, linestyle, linewidth, **kwargs) + + def add_std_grid(self, + arr: typing.Union[xr.DataArray, np.ndarray, list, float], + color: str = 'lightgray', + linestyle=(0, (9, 5)), + linewidth: int = 1, + **kwargs): + """Add radial gridlines to the standard deviation axis specified by + array. + + *arr* Parameters ---------- @@ -458,7 +483,7 @@ def add_ygrid(self, -------- All usage examples are within the GeoCAT-Examples Gallery. To see more usage cases, search the function on the `website `_. - - `NCL_taylor_2.py `_ + - `NCL_taylor_2.py `_ """ t_array = np.linspace(0, np.pi / 2) @@ -471,6 +496,26 @@ def add_ygrid(self, linewidth=linewidth, **kwargs) + def add_ygrid(self, + arr: typing.Union[xr.DataArray, np.ndarray, list, float], + color: str = 'lightgray', + linestyle=(0, (9, 5)), + linewidth: int = 1, + **kwargs): + """Add gridlines to the standard deviation axis specified by array. + + *arr*. + + This method will be deprecated in favor of + `TaylorDiagram.add_std_grid()` + """ + + warnings.warn( + '`TaylorDiagram.add_ygrid` will be deprecated in the future. Please use `TaylorDiagram.add_std_grid` instead.', + PendingDeprecationWarning) + + return self.add_std_grid(arr, color, linestyle, linewidth, **kwargs) + def add_grid(self, *args, **kwargs): """Add a grid. diff --git a/src/geocat/viz/util.py b/src/geocat/viz/util.py index a0e30e2c..d0a3bb13 100644 --- a/src/geocat/viz/util.py +++ b/src/geocat/viz/util.py @@ -556,6 +556,8 @@ def set_titles_and_labels(ax: typing.Union[matplotlib.axes.Axes, >>> | Axes | >>> | | + Be aware that the `suptitle` functionality does not always render well for Cartopy plots. If your main title appears too far above your plot, the recommended fix is to decrease the y-dimension of your figure size. + Examples -------- All usage examples are within the GeoCAT-Examples Gallery. To see more usage cases, search the function on the @@ -571,7 +573,10 @@ def set_titles_and_labels(ax: typing.Union[matplotlib.axes.Axes, if maintitle is not None: if subtitle is not None: fig = ax.get_figure() - fig.suptitle(maintitle, fontsize=maintitlefontsize, y=1.04) + fig.suptitle(maintitle, + fontsize=maintitlefontsize, + y=1.04, + ha='center') elif lefttitle is not None or righttitle is not None: ax.set_title(maintitle, fontsize=maintitlefontsize + 2, y=1.12) else: diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/baseline/test_add_bias_legend.png b/tests/baseline/test_add_bias_legend.png new file mode 100644 index 00000000..d16da635 Binary files /dev/null and b/tests/baseline/test_add_bias_legend.png differ diff --git a/tests/baseline/test_add_contours.png b/tests/baseline/test_add_contours.png new file mode 100644 index 00000000..0657c731 Binary files /dev/null and b/tests/baseline/test_add_contours.png differ diff --git a/tests/baseline/test_add_corr_grid.png b/tests/baseline/test_add_corr_grid.png new file mode 100644 index 00000000..79ac3e2c Binary files /dev/null and b/tests/baseline/test_add_corr_grid.png differ diff --git a/tests/baseline/test_add_height_from_pressure_axis.png b/tests/baseline/test_add_height_from_pressure_axis.png new file mode 100644 index 00000000..1e0aa26c Binary files /dev/null and b/tests/baseline/test_add_height_from_pressure_axis.png differ diff --git a/tests/baseline/test_add_lat_lon_gridlines.png b/tests/baseline/test_add_lat_lon_gridlines.png new file mode 100644 index 00000000..9260372c Binary files /dev/null and b/tests/baseline/test_add_lat_lon_gridlines.png differ diff --git a/tests/baseline/test_add_lat_lon_ticklabels.png b/tests/baseline/test_add_lat_lon_ticklabels.png new file mode 100644 index 00000000..42a58a7f Binary files /dev/null and b/tests/baseline/test_add_lat_lon_ticklabels.png differ diff --git a/tests/baseline/test_add_legend.png b/tests/baseline/test_add_legend.png new file mode 100644 index 00000000..4e6ae13c Binary files /dev/null and b/tests/baseline/test_add_legend.png differ diff --git a/tests/baseline/test_add_major_minor_ticks.png b/tests/baseline/test_add_major_minor_ticks.png new file mode 100644 index 00000000..524a52a6 Binary files /dev/null and b/tests/baseline/test_add_major_minor_ticks.png differ diff --git a/tests/baseline/test_add_model_name.png b/tests/baseline/test_add_model_name.png new file mode 100644 index 00000000..82b68612 Binary files /dev/null and b/tests/baseline/test_add_model_name.png differ diff --git a/tests/baseline/test_add_model_set.png b/tests/baseline/test_add_model_set.png new file mode 100644 index 00000000..7e39a740 Binary files /dev/null and b/tests/baseline/test_add_model_set.png differ diff --git a/tests/baseline/test_add_right_hand_axis.png b/tests/baseline/test_add_right_hand_axis.png new file mode 100644 index 00000000..f3004588 Binary files /dev/null and b/tests/baseline/test_add_right_hand_axis.png differ diff --git a/tests/baseline/test_plot_contour_labels.png b/tests/baseline/test_plot_contour_labels.png new file mode 100644 index 00000000..5ee8411b Binary files /dev/null and b/tests/baseline/test_plot_contour_labels.png differ diff --git a/tests/baseline/test_plot_extrema_labels.png b/tests/baseline/test_plot_extrema_labels.png new file mode 100644 index 00000000..decfbca4 Binary files /dev/null and b/tests/baseline/test_plot_extrema_labels.png differ diff --git a/tests/baseline/test_set_axes_limits_and_ticks.png b/tests/baseline/test_set_axes_limits_and_ticks.png new file mode 100644 index 00000000..2f5a9272 Binary files /dev/null and b/tests/baseline/test_set_axes_limits_and_ticks.png differ diff --git a/tests/baseline/test_set_map_boundary.png b/tests/baseline/test_set_map_boundary.png new file mode 100644 index 00000000..b682084e Binary files /dev/null and b/tests/baseline/test_set_map_boundary.png differ diff --git a/tests/baseline/test_set_tick_direction_spine_visibility.png b/tests/baseline/test_set_tick_direction_spine_visibility.png new file mode 100644 index 00000000..5a50d0ad Binary files /dev/null and b/tests/baseline/test_set_tick_direction_spine_visibility.png differ diff --git a/tests/baseline/test_set_titles_and_labels.png b/tests/baseline/test_set_titles_and_labels.png new file mode 100644 index 00000000..b96376b5 Binary files /dev/null and b/tests/baseline/test_set_titles_and_labels.png differ diff --git a/tests/baseline/test_set_vector_density.png b/tests/baseline/test_set_vector_density.png new file mode 100644 index 00000000..e6e95c7e Binary files /dev/null and b/tests/baseline/test_set_vector_density.png differ diff --git a/tests/baseline/test_xr_add_cyclic_longitudes.png b/tests/baseline/test_xr_add_cyclic_longitudes.png new file mode 100644 index 00000000..8e8a5c83 Binary files /dev/null and b/tests/baseline/test_xr_add_cyclic_longitudes.png differ diff --git a/tests/test_taylor.py b/tests/test_taylor.py new file mode 100644 index 00000000..a76069b5 --- /dev/null +++ b/tests/test_taylor.py @@ -0,0 +1,139 @@ +import pytest +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np + +from geocat.viz.taylor import TaylorDiagram + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_model_set(): + fig = plt.figure(figsize=(10, 10)) + taylor = TaylorDiagram(fig=fig, label='REF') + + taylor.add_model_set([1.230, 0.988, 1.092], [0.958, 0.973, 0.740], + color='red', + label='Model A', + fontsize=16) + + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_legend(): + fig = plt.figure(figsize=(10, 10)) + taylor = TaylorDiagram(fig=fig, label='REF') + + taylor.add_model_set( + [1.230, 0.988, 1.092], + [0.958, 0.973, 0.740], + percent_bias_on= + True, # indicate marker and size to be plotted based on bias_array + bias_array=[2.7, -1.5, 17.31], # specify bias array + color='red', + label='Model A', + fontsize=16) + + taylor.add_model_set([1.129, 0.996, 1.016], [0.963, 0.975, 0.801], + percent_bias_on=True, + bias_array=[1.7, 2.5, -17.31], + color='blue', + label='Model B', + fontsize=16) + + taylor.add_legend(fontsize=16) + + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_bias_legend(): + fig = plt.figure(figsize=(10, 10)) + taylor = TaylorDiagram(fig=fig, label='REF') + + taylor.add_model_set( + [1.230, 0.988, 1.092], + [0.958, 0.973, 0.740], + percent_bias_on= + True, # indicate marker and size to be plotted based on bias_array + bias_array=[2.7, -1.5, 17.31], # specify bias array + color='red', + label='Model A', + fontsize=16) + + taylor.add_bias_legend() + + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_model_name(): + fig = plt.figure(figsize=(10, 10)) + taylor = TaylorDiagram(fig=fig, label='REF') + + taylor.add_model_set( + [1.230, 0.988, 1.092], + [0.958, 0.973, 0.740], + percent_bias_on= + True, # indicate marker and size to be plotted based on bias_array + bias_array=[2.7, -1.5, 17.31], # specify bias array + color='red', + label='Model A', + fontsize=16) + + taylor.add_model_name(['a', 'b', 'c'], fontsize=16) + + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_corr_grid(): + fig = plt.figure(figsize=(10, 10)) + taylor = TaylorDiagram(fig=fig, label='REF') + + taylor.add_corr_grid(np.array([0.6, 0.9])) + + taylor.add_model_set( + [1.230, 0.988, 1.092], + [0.958, 0.973, 0.740], + percent_bias_on= + True, # indicate marker and size to be plotted based on bias_array + bias_array=[2.7, -1.5, 17.31], # specify bias array + color='red', + label='Model A', + fontsize=16) + + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_contours(): + fig = plt.figure(figsize=(10, 10)) + taylor = TaylorDiagram(fig=fig, label='REF') + + taylor.add_model_set( + [1.230, 0.988, 1.092], + [0.958, 0.973, 0.740], + percent_bias_on= + True, # indicate marker and size to be plotted based on bias_array + bias_array=[2.7, -1.5, 17.31], # specify bias array + color='red', + label='Model A', + fontsize=16) + + taylor.add_contours(levels=np.arange(0, 1.1, 0.25), + colors='lightgrey', + linewidths=0.5) + return fig diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 00000000..d43f5243 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,314 @@ +import pytest +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np +import cartopy.crs as ccrs +import cartopy.feature as cfeature +import xarray as xr +import pandas as pd +from metpy.units import units +import metpy.calc as mpcalc +import geocat.datafiles as gdf + +from geocat.viz.util import set_tick_direction_spine_visibility, add_lat_lon_gridlines, add_right_hand_axis, add_height_from_pressure_axis, add_lat_lon_ticklabels, add_major_minor_ticks, set_titles_and_labels, set_axes_limits_and_ticks, truncate_colormap, xr_add_cyclic_longitudes, set_map_boundary, find_local_extrema, plot_contour_labels, plot_extrema_labels, set_vector_density, get_skewt_vars + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_set_tick_direction_spine_visibility(): + fig, ax = plt.subplots(figsize=(6, 6)) + + set_tick_direction_spine_visibility(ax, + tick_direction='in', + top_spine_visible=False, + right_spine_visible=False) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_lat_lon_gridlines(): + fig = plt.figure(figsize=(10, 10)) + + ax = plt.axes(projection=ccrs.NorthPolarStereo(central_longitude=10)) + ax.set_extent([4.25, 15.25, 42.25, 49.25], ccrs.PlateCarree()) + + gl = add_lat_lon_gridlines( + ax, + color='black', + labelsize=14, + xlocator=np.arange(4, 18, 2), # longitudes for gridlines + ylocator=np.arange(43, 50)) # latitudes for gridlines + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_right_hand_axis(): + fig = plt.figure(figsize=(9, 10)) + ax1 = plt.gca() + + add_right_hand_axis(ax1, + label="Right Hand Axis", + ylim=(0, 13), + yticks=np.array([4, 8]), + ticklabelsize=15, + axislabelsize=21) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_height_from_pressure_axis(): + fig = plt.figure(figsize=(8, 8)) + ax = plt.axes() + plt.yscale('log') + ax.invert_yaxis() + + add_height_from_pressure_axis(ax, heights=[4, 8]) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_lat_lon_ticklabels(): + fig = plt.figure(figsize=(12, 6)) + + projection = ccrs.PlateCarree() + ax = plt.axes(projection=projection) + ax.add_feature(cfeature.LAND, color='silver') + + add_lat_lon_ticklabels(ax) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_add_major_minor_ticks(): + fig = plt.figure(figsize=(12, 6)) + ax = plt.axes() + + add_major_minor_ticks(ax, + x_minor_per_major=4, + y_minor_per_major=5, + labelsize="small") + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_set_titles_and_labels(): + fig = fig, ax = plt.subplots() + + set_titles_and_labels(ax, + maintitle="Title", + maintitlefontsize=24, + subtitle="Subtitle", + xlabel="x", + ylabel="y", + labelfontsize=16) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_set_axes_limits_and_ticks(): + fig = fig, ax = plt.subplots() + + set_axes_limits_and_ticks(ax, + xlim=(0, 900), + ylim=(100, 1000), + xticks=range(0, 901, 100), + yticks=range(100, 1001, 100)) + return fig + + +def test_truncate_colormap(): + cmap = mpl.colormaps['terrain'] + truncated_cmap = truncate_colormap(cmap, 0.1, 0.9) + + assert isinstance(truncated_cmap, mpl.colors.LinearSegmentedColormap) + assert truncated_cmap(0.0) == cmap(0.1) + assert truncated_cmap(1.0) == cmap(0.9) + + +def test_xr_add_cyclic_longitudes_length(): + ds = xr.open_dataset(gdf.get("netcdf_files/uv300.nc")).isel(time=1) + U = ds.U + + U = xr_add_cyclic_longitudes(U, 'lon') + + cyclic_lon = [ + -180., -177.1875, -174.375, -171.5625, -168.75, -165.9375, -163.125, + -160.3125, -157.5, -154.6875, -151.875, -149.0625, -146.25, -143.4375, + -140.625, -137.8125, -135., -132.1875, -129.375, -126.5625, -123.75, + -120.9375, -118.125, -115.3125, -112.5, -109.6875, -106.875, -104.0625, + -101.25, -98.4375, -95.625, -92.8125, -90., -87.1875, -84.375, -81.5625, + -78.75, -75.9375, -73.125, -70.3125, -67.5, -64.6875, -61.875, -59.0625, + -56.25, -53.4375, -50.625, -47.8125, -45., -42.1875, -39.375, -36.5625, + -33.75, -30.9375, -28.125, -25.3125, -22.5, -19.6875, -16.875, -14.0625, + -11.25, -8.4375, -5.625, -2.8125, 0., 2.8125, 5.625, 8.4375, 11.25, + 14.0625, 16.875, 19.6875, 22.5, 25.3125, 28.125, 30.9375, 33.75, + 36.5625, 39.375, 42.1875, 45., 47.8125, 50.625, 53.4375, 56.25, 59.0625, + 61.875, 64.6875, 67.5, 70.3125, 73.125, 75.9375, 78.75, 81.5625, 84.375, + 87.1875, 90., 92.8125, 95.625, 98.4375, 101.25, 104.0625, 106.875, + 109.6875, 112.5, 115.3125, 118.125, 120.9375, 123.75, 126.5625, 129.375, + 132.1875, 135., 137.8125, 140.625, 143.4375, 146.25, 149.0625, 151.875, + 154.6875, 157.5, 160.3125, 163.125, 165.9375, 168.75, 171.5625, 174.375, + 177.1875, 180. + ] + + assert len(U.lon) == len(cyclic_lon) + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_xr_add_cyclic_longitudes(): + ds = xr.open_dataset(gdf.get("netcdf_files/slp.1963.nc"), + decode_times=False) + pressure = ds.slp[24, :, :].astype('float64') * 0.01 + wrap_pressure = xr_add_cyclic_longitudes(pressure, "lon") + + fig = plt.figure(figsize=(8, 8)) + + proj = ccrs.Orthographic(central_longitude=270, central_latitude=45) + ax = plt.axes(projection=proj) + ax.set_global() + + p = wrap_pressure.plot.contour(ax=ax, + transform=ccrs.PlateCarree(), + linewidths=0.5, + cmap='black', + add_labels=False) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_set_map_boundary(): + fig = plt.figure(figsize=(8, 8)) + ax = plt.axes(projection=ccrs.NorthPolarStereo()) + ax.add_feature(cfeature.LAND, facecolor='lightgray') + set_map_boundary(ax, [-180, 180], [0, 40], south_pad=1) + + return fig + + +def test_find_local_extrema(): + data = [[1, 4, 5, 6, 8.2], [9, 8.4, 10, 10.6, 9.7], [4.4, 5, 0, 6.6, 1.4], + [4.6, 5.2, 1.5, 7.6, 2.4]] + data = xr.DataArray(data, + dims=["lat", "lon"], + coords=dict(lat=np.arange(4), lon=np.arange(5))) + + lmin = find_local_extrema(data, eType='Low')[0] + + assert lmin == (2, 2) + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_plot_contour_labels(): + ds = xr.open_dataset(gdf.get("netcdf_files/slp.1963.nc"), + decode_times=False) + pressure = ds.slp[24, :, :].astype('float64') * 0.01 + + fig = plt.figure(figsize=(8, 8)) + + proj = ccrs.Orthographic(central_longitude=270, central_latitude=45) + ax = plt.axes(projection=proj) + ax.set_global() + + p = pressure.plot.contour(ax=ax, + transform=ccrs.PlateCarree(), + linewidths=0.5, + cmap='black', + add_labels=False) + + contour_label_locations = [ + (176.4, 34.63), (-150.46, 42.44), (-142.16, 28.5), (-92.49, 25.64), + (-156.05, 84.47), (-17.83, 82.52), (-76.3, 41.99), (-48.89, 41.45), + (-33.43, 37.55) + ] + + plot_contour_labels(ax, + p, + ccrs.Geodetic(), + proj, + clabel_locations=contour_label_locations) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_plot_extrema_labels(): + ds = xr.open_dataset(gdf.get("netcdf_files/slp.1963.nc"), + decode_times=False) + pressure = ds.slp[24, :, :].astype('float64') * 0.01 + + fig = plt.figure(figsize=(8, 8)) + proj = ccrs.Orthographic(central_longitude=270, central_latitude=45) + ax = plt.axes(projection=proj) + ax.set_global() + + lowClevels = [(357.5, 75.0), (302.5, 60.0), (170.0, 52.5), (327.5, -60.0)] + + plot_extrema_labels(pressure, + ccrs.Geodetic(), + proj, + label_locations=lowClevels, + label='L', + show_warnings=False) + return fig + + +@pytest.mark.mpl_image_compare(tolerance=0.02, + remove_text=True, + style='default') +def test_set_vector_density(): + file_in = xr.open_dataset(gdf.get("netcdf_files/uv300.nc")) + ds = file_in.isel(time=1, lon=slice(0, -1, 3), lat=slice(1, -1, 3)) + + fig = plt.figure(figsize=(10, 5.25)) + ax = plt.axes(projection=ccrs.PlateCarree()) + + z = set_vector_density(ds, 10) + + Q = plt.quiver(z['lon'], + z['lat'], + z['U'].data, + z['V'].data, + color='black', + zorder=1, + pivot="middle", + width=0.0007, + headwidth=10) + return fig + + +def test_get_skewt_vars(): + ds = pd.read_csv(gdf.get('ascii_files/sounding.testdata'), + delimiter='\\s+', + header=None) + + p = ds[1].values * units.hPa + tc = (ds[5].values + 2) * units.degC + tdc = ds[9].values * units.degC + + tc0 = tc[0] + tdc0 = tdc[0] + pro = mpcalc.parcel_profile(p, tc0, tdc0) + subtitle = get_skewt_vars(p, tc, tdc, pro) + assert subtitle == 'Plcl= 927 Tlcl[C]= 24 Shox= 3 Pwat[cm]= 5 Cape[J]= 3135'