├── tests ├── __init__.py ├── output │ ├── df │ │ ├── test_bar.png │ │ ├── test_box.png │ │ ├── test_pdf.png │ │ ├── test_contour.png │ │ ├── test_contourf.png │ │ ├── test_scatter.png │ │ ├── test_subplots.png │ │ └── test_windrose_np_plot_and_pd_plot.png │ ├── oo │ │ ├── test_pdf.png │ │ ├── test_filled_with_colormap.png │ │ ├── test_windrose_with_scatter_plot.png │ │ ├── test_filled_with_colormap_contours.png │ │ ├── test_filled_with_colormap_calm_limit.png │ │ ├── test_windrose_stacked_histogram_normed.png │ │ ├── test_without_filled_with_colormap_contours.png │ │ ├── test_filled_with_colormap_contours_calm_limit.png │ │ ├── test_windrose_stacked_histogram_normed_calm_limit.png │ │ ├── test_windrose_stacked_histogram_not_normed_binned.png │ │ ├── test_without_filled_with_colormap_contours_calm_limit.png │ │ └── test_windrose_stacked_histogram_not_normed_binned_calm_limit.png │ ├── func │ │ ├── test_wrbar.png │ │ ├── test_wrbox.png │ │ ├── test_wrpdf.png │ │ ├── test_wrcontour.png │ │ ├── test_wrscatter.png │ │ └── test_wrcontourf.png │ ├── test_bar_from_factory.png │ └── test_pdf_from_factory.png ├── test_windrose_factory.py ├── test_windrose_np_mpl_func.py ├── test_windrose_pandas.py ├── test_windrose.py └── test_windrose_np_mpl_oo.py ├── requirements.txt ├── LICENCE.txt ├── LICENCE_CECILL-B.txt ├── docs ├── api.rst ├── Makefile ├── install.rst ├── make.bat ├── development.rst ├── index.rst └── conf.py ├── paper ├── screenshots │ ├── bar.png │ ├── pdf.png │ ├── overlay.png │ ├── subplots.png │ └── contourf-contour.png ├── paper.md └── paper.bib ├── .binder └── environment.yml ├── MANIFEST.in ├── .github ├── dependabot.yml └── workflows │ ├── pypi.yml │ ├── deploy-docs.yml │ └── tests.yml ├── requirements-dev.txt ├── samples ├── example_pandas.py ├── example_statistical_input.py ├── example_np_mpl_oo.py ├── example_subplots.py ├── amalia_directionally_averaged_speeds.txt ├── example_pdf_by.py ├── example_animate.py └── example_by.py ├── windrose ├── __init__.py └── windrose.py ├── .gitignore ├── LICENCE_BSD-3-Clause.txt ├── release-procedure.md ├── CONTRIBUTORS.md ├── CONTRIBUTING.md ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── pyproject.toml ├── README.md ├── notebooks ├── windrose_sample_poitiers_csv.ipynb └── usage.ipynb └── CHANGELOG.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib >=3 2 | numpy >=1.21 3 | pandas 4 | scipy 5 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | This library is released under dual licensing: 2 | 3 | - CECILL-B 4 | - BSD-3-Clause 5 | -------------------------------------------------------------------------------- /LICENCE_CECILL-B.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/LICENCE_CECILL-B.txt -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. automodule:: windrose 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /paper/screenshots/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/paper/screenshots/bar.png -------------------------------------------------------------------------------- /paper/screenshots/pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/paper/screenshots/pdf.png -------------------------------------------------------------------------------- /paper/screenshots/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/paper/screenshots/overlay.png -------------------------------------------------------------------------------- /tests/output/df/test_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_bar.png -------------------------------------------------------------------------------- /tests/output/df/test_box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_box.png -------------------------------------------------------------------------------- /tests/output/df/test_pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_pdf.png -------------------------------------------------------------------------------- /tests/output/oo/test_pdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_pdf.png -------------------------------------------------------------------------------- /paper/screenshots/subplots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/paper/screenshots/subplots.png -------------------------------------------------------------------------------- /tests/output/df/test_contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_contour.png -------------------------------------------------------------------------------- /tests/output/df/test_contourf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_contourf.png -------------------------------------------------------------------------------- /tests/output/df/test_scatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_scatter.png -------------------------------------------------------------------------------- /tests/output/df/test_subplots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_subplots.png -------------------------------------------------------------------------------- /tests/output/func/test_wrbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/func/test_wrbar.png -------------------------------------------------------------------------------- /tests/output/func/test_wrbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/func/test_wrbox.png -------------------------------------------------------------------------------- /tests/output/func/test_wrpdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/func/test_wrpdf.png -------------------------------------------------------------------------------- /tests/output/func/test_wrcontour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/func/test_wrcontour.png -------------------------------------------------------------------------------- /tests/output/func/test_wrscatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/func/test_wrscatter.png -------------------------------------------------------------------------------- /paper/screenshots/contourf-contour.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/paper/screenshots/contourf-contour.png -------------------------------------------------------------------------------- /tests/output/func/test_wrcontourf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/func/test_wrcontourf.png -------------------------------------------------------------------------------- /tests/output/test_bar_from_factory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/test_bar_from_factory.png -------------------------------------------------------------------------------- /tests/output/test_pdf_from_factory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/test_pdf_from_factory.png -------------------------------------------------------------------------------- /tests/output/oo/test_filled_with_colormap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_filled_with_colormap.png -------------------------------------------------------------------------------- /tests/output/oo/test_windrose_with_scatter_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_windrose_with_scatter_plot.png -------------------------------------------------------------------------------- /tests/output/df/test_windrose_np_plot_and_pd_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/df/test_windrose_np_plot_and_pd_plot.png -------------------------------------------------------------------------------- /tests/output/oo/test_filled_with_colormap_contours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_filled_with_colormap_contours.png -------------------------------------------------------------------------------- /tests/output/oo/test_filled_with_colormap_calm_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_filled_with_colormap_calm_limit.png -------------------------------------------------------------------------------- /tests/output/oo/test_windrose_stacked_histogram_normed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_windrose_stacked_histogram_normed.png -------------------------------------------------------------------------------- /tests/output/oo/test_without_filled_with_colormap_contours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_without_filled_with_colormap_contours.png -------------------------------------------------------------------------------- /tests/output/oo/test_filled_with_colormap_contours_calm_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_filled_with_colormap_contours_calm_limit.png -------------------------------------------------------------------------------- /tests/output/oo/test_windrose_stacked_histogram_normed_calm_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_windrose_stacked_histogram_normed_calm_limit.png -------------------------------------------------------------------------------- /tests/output/oo/test_windrose_stacked_histogram_not_normed_binned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_windrose_stacked_histogram_not_normed_binned.png -------------------------------------------------------------------------------- /tests/output/oo/test_without_filled_with_colormap_contours_calm_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_without_filled_with_colormap_contours_calm_limit.png -------------------------------------------------------------------------------- /tests/output/oo/test_windrose_stacked_histogram_not_normed_binned_calm_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-windrose/windrose/HEAD/tests/output/oo/test_windrose_stacked_histogram_not_normed_binned_calm_limit.png -------------------------------------------------------------------------------- /.binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: WINDROSE 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python=3 6 | - cartopy 7 | - jupyter 8 | - matplotlib-base 9 | - numpy 10 | - pandas 11 | - scipy 12 | - windrose 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.md 3 | include pyproject.toml 4 | 5 | graft windrose 6 | 7 | prune docs 8 | prune tests 9 | prune notebooks 10 | prune paper 11 | prune *.egg-info 12 | prune samples 13 | prune .binder 14 | prune .github 15 | 16 | exclude *.yml 17 | exclude .pre-commit-config.yaml 18 | exclude *.enc 19 | exclude .gitignore 20 | exclude .isort.cfg 21 | exclude windrose/_version.py 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/keeping-your-actions-up-to-date-with-dependabot 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | interval: "daily" 10 | labels: 11 | - "Bot" 12 | groups: 13 | github-actions: 14 | patterns: 15 | - '*' 16 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black 2 | cartopy 3 | check-manifest 4 | coverage 5 | flake8 6 | flake8-builtins 7 | flake8-comprehensions 8 | flake8-mutable 9 | flake8-print 10 | interrogate 11 | isort 12 | jupyter 13 | nbsphinx 14 | pre-commit 15 | pyarrow 16 | pydocstyle 17 | pylint 18 | pytest 19 | pytest-cov 20 | pytest-flake8 21 | pytest-mpl 22 | pytest-sugar 23 | seaborn 24 | setuptools_scm 25 | sphinx 26 | sphinx-copybutton 27 | sphinx_rtd_theme 28 | twine 29 | wheel 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = windrose 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ------- 3 | 4 | Requirements 5 | ~~~~~~~~~~~~ 6 | 7 | - matplotlib https://matplotlib.org/ 8 | - numpy https://numpy.org/ 9 | - and naturally python https://www.python.org/ :-P 10 | 11 | Option libraries: 12 | 13 | - Pandas https://pandas.pydata.org/ (to feed plot functions easily) 14 | - SciPy https://scipy.org/ (to fit data with Weibull distribution) 15 | - ffmpeg https://www.ffmpeg.org/ (to output video) 16 | - click https://click.palletsprojects.com/ (for command line interface tools) 17 | - seaborn https://seaborn.pydata.org/ (for easy subplots) 18 | 19 | Install latest release version via pip 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | A package is available and can be downloaded from PyPi and installed 23 | using: 24 | 25 | .. code:: bash 26 | 27 | $ pip install windrose 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=windrose 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /samples/example_pandas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import matplotlib.cm as cm 4 | import numpy as np 5 | import pandas as pd 6 | from matplotlib import pyplot as plt 7 | 8 | from windrose import plot_windrose 9 | 10 | 11 | def main(): 12 | df = pd.read_csv("samples/sample_wind_poitiers.csv", parse_dates=["Timestamp"]) 13 | # df['Timestamp'] = pd.to_timestamp() 14 | df = df.set_index("Timestamp") 15 | 16 | # N = 500 17 | # ws = np.random.random(N) * 6 18 | # wd = np.random.random(N) * 360 19 | # df = pd.DataFrame({'speed': ws, 'direction': wd}) 20 | 21 | bins = np.arange(0.01, 8, 1) 22 | # bins = np.arange(0, 8, 1)[1:] 23 | plot_windrose(df, kind="contour", bins=bins, cmap=cm.hot, lw=3, rmax=20000) 24 | plt.show() 25 | 26 | bins = np.arange(0, 30 + 1, 1) 27 | bins = bins[1:] 28 | 29 | ax, params = plot_windrose(df, kind="pdf", bins=bins) 30 | # plt.savefig("screenshots/pdf.png") 31 | plt.show() 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /windrose/__init__.py: -------------------------------------------------------------------------------- 1 | """Draw windrose (also known as a polar rose plot)""" 2 | 3 | from matplotlib.projections import register_projection 4 | 5 | from .windrose import ( 6 | D_KIND_PLOT, 7 | DEFAULT_THETA_LABELS, 8 | DPI_DEFAULT, 9 | FIGSIZE_DEFAULT, 10 | WindAxes, 11 | WindAxesFactory, 12 | WindroseAxes, 13 | clean, 14 | clean_df, 15 | plot_windrose, 16 | plot_windrose_df, 17 | plot_windrose_np, 18 | wrbar, 19 | wrbox, 20 | wrcontour, 21 | wrcontourf, 22 | wrpdf, 23 | wrscatter, 24 | ) 25 | 26 | __all__ = [ 27 | "D_KIND_PLOT", 28 | "DEFAULT_THETA_LABELS", 29 | "DPI_DEFAULT", 30 | "FIGSIZE_DEFAULT", 31 | "WindAxes", 32 | "WindAxesFactory", 33 | "WindroseAxes", 34 | "clean", 35 | "clean_df", 36 | "plot_windrose", 37 | "plot_windrose_df", 38 | "plot_windrose_np", 39 | "wrbar", 40 | "wrbox", 41 | "wrcontour", 42 | "wrcontourf", 43 | "wrpdf", 44 | "wrscatter", 45 | ] 46 | register_projection(WindroseAxes) 47 | -------------------------------------------------------------------------------- /tests/test_windrose_factory.py: -------------------------------------------------------------------------------- 1 | # generate the baseline: pytest tests/test_windrose_factory.py --mpl-generate-path=tests/output 2 | 3 | 4 | import matplotlib 5 | import numpy as np 6 | import pandas as pd 7 | import pytest 8 | 9 | from windrose import WindAxesFactory 10 | 11 | matplotlib.use("Agg") # noqa 12 | np.random.seed(0) 13 | # Create wind speed and direction variables 14 | N = 500 15 | ws = np.random.random(N) * 6 16 | wd = np.random.random(N) * 360 17 | bins = np.arange(0.01, 8, 1) 18 | 19 | df = pd.DataFrame({"speed": ws, "direction": wd}) 20 | 21 | 22 | @pytest.mark.mpl_image_compare(baseline_dir="output/") 23 | def test_bar_from_factory(): 24 | ax = WindAxesFactory.create("WindroseAxes") 25 | ax.bar(wd, ws, normed=True, opening=0.8, edgecolor="white") 26 | ax.set_legend() 27 | return ax.figure 28 | 29 | 30 | @pytest.mark.mpl_image_compare(baseline_dir="output/") 31 | def test_pdf_from_factory(): 32 | ax = WindAxesFactory.create("WindAxes") 33 | bins = np.arange(0, 8, 1) 34 | bins = bins[1:] 35 | ax.pdf(ws, bins=bins) 36 | return ax.figure 37 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | ----------- 3 | 4 | You can help to develop this library. 5 | 6 | Issues 7 | ~~~~~~ 8 | 9 | You can submit issues using 10 | https://github.com/python-windrose/windrose/issues 11 | 12 | Clone 13 | ~~~~~ 14 | 15 | You can clone repository to try to fix issues yourself using: 16 | 17 | .. code:: bash 18 | 19 | $ git clone https://github.com/python-windrose/windrose.git 20 | $ cd windrose 21 | 22 | Run unit tests 23 | ~~~~~~~~~~~~~~ 24 | 25 | Run all unit tests 26 | 27 | .. code:: bash 28 | 29 | $ python -m pytest -vv tests 30 | 31 | Run a given test 32 | 33 | .. code:: bash 34 | 35 | $ python -m pytest -vv tests/test_windrose.py::test_windrose_np_plot_and_pd_plot 36 | 37 | Install development version 38 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 39 | 40 | .. code:: bash 41 | 42 | $ python -m pip install . 43 | 44 | or directly from repository 45 | 46 | .. code:: bash 47 | 48 | $ python -m pip install git+https://github.com/python-windrose/windrose.git 49 | 50 | Collaborating 51 | ~~~~~~~~~~~~~ 52 | 53 | - Fork repository 54 | - Create a branch which fix a given issue 55 | - Submit pull requests 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .pytest_cache/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # Mac OS X 61 | .DS_Store 62 | 63 | # IPython 64 | .ipynb_checkpoints 65 | 66 | # PyCharm 67 | .idea/ 68 | 69 | # ignore tmp doc notebook 70 | *-output.ipynb 71 | 72 | # tmp version file should not be pacakged, let setuptool_scm handle this 73 | windrose/_version.py 74 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | release: 9 | types: 10 | - published 11 | 12 | defaults: 13 | run: 14 | shell: bash 15 | 16 | jobs: 17 | packages: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | 22 | - name: Set up Python 23 | uses: actions/setup-python@v6 24 | with: 25 | python-version: "3.x" 26 | 27 | - name: Get tags 28 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* 29 | 30 | - name: Install build tools 31 | run: | 32 | python -m pip install --upgrade pip build 33 | 34 | - name: Build binary wheel 35 | run: python -m build --sdist --wheel . --outdir dist 36 | 37 | - name: CheckFiles 38 | run: > 39 | python -m pip install --upgrade check-manifest 40 | && check-manifest 41 | && ls dist 42 | 43 | - name: Test wheels 44 | run: > 45 | python -m pip install --upgrade pip twine 46 | && cd dist && python -m pip install windrose*.whl 47 | && python -m twine check * 48 | 49 | - name: Publish a Python distribution to PyPI 50 | if: success() && github.event_name == 'release' 51 | uses: pypa/gh-action-pypi-publish@release/v1 52 | with: 53 | user: __token__ 54 | password: ${{ secrets.PYPI_PASSWORD }} 55 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy docs 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | release: 9 | types: 10 | - published 11 | 12 | jobs: 13 | build-docs: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: checkout 18 | uses: actions/checkout@v6 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup Micromamba 23 | uses: mamba-org/setup-micromamba@v2 24 | with: 25 | environment-name: TEST 26 | init-shell: bash 27 | create-args: >- 28 | python=3 pip 29 | --file requirements.txt 30 | --file requirements-dev.txt 31 | --channel conda-forge 32 | 33 | - name: Install windrose 34 | shell: bash -l {0} 35 | run: | 36 | python -m pip install -e . --no-deps --force-reinstall 37 | 38 | - name: Build documentation 39 | shell: bash -l {0} 40 | run: | 41 | set -e 42 | jupyter nbconvert --to notebook --execute notebooks/usage.ipynb --output=usage-output.ipynb 43 | mv notebooks/*output.ipynb docs/ 44 | pushd docs 45 | make clean html linkcheck 46 | popd 47 | 48 | - name: Deploy 49 | if: success() && github.event_name == 'release' 50 | uses: peaceiris/actions-gh-pages@v4 51 | with: 52 | github_token: ${{ secrets.GITHUB_TOKEN }} 53 | publish_dir: docs/_build/html 54 | -------------------------------------------------------------------------------- /LICENCE_BSD-3-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Lionel Roubeyrie / Sébastien Celles 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | run: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12" ] 14 | os: [windows-latest, ubuntu-latest, macos-latest] 15 | fail-fast: false 16 | 17 | steps: 18 | - uses: actions/checkout@v6 19 | 20 | - name: Setup Micromamba for Python ${{ matrix.python-version }} 21 | uses: mamba-org/setup-micromamba@v2 22 | with: 23 | environment-name: TEST 24 | init-shell: bash 25 | create-args: >- 26 | python=${{ matrix.python-version }} pip 27 | --file requirements.txt 28 | --file requirements-dev.txt 29 | --channel conda-forge 30 | 31 | - name: Install windrose 32 | shell: bash -l {0} 33 | run: | 34 | python -m pip install -e . --no-deps --force-reinstall 35 | 36 | - name: Tests 37 | shell: bash -l {0} 38 | run: | 39 | pytest -s -rxs -vv -Werror tests/ --mpl --mpl-generate-summary=html \ 40 | --mpl-results-path="windrose_test_output-${{ matrix.os }}-${{ matrix.python-version }}" 41 | - name: Store mpl-results 42 | uses: actions/upload-artifact@v6 43 | if: failure() 44 | with: 45 | name: "windrose_test_output-${{ matrix.os }}-${{ matrix.python-version }}" 46 | path: "windrose_test_output-${{ matrix.os }}-${{ matrix.python-version }}" 47 | retention-days: 1 48 | -------------------------------------------------------------------------------- /release-procedure.md: -------------------------------------------------------------------------------- 1 | # Release procedure 2 | 3 | ## Python versions 4 | 5 | * ECheck `python_requires = >=3.6` in setup.cfg 6 | 7 | ## CHANGELOG 8 | 9 | * Ensure `CHANGELOG.md` have been updated 10 | 11 | ## Tag 12 | 13 | * Tag commit and push to github 14 | 15 | using Github website 16 | 17 | Go to https://github.com/python-windrose/windrose/releases/new 18 | tag: vx.x.x 19 | 20 | or using cli 21 | 22 | ```bash 23 | git tag -a x.x.x -m 'Version x.x.x' 24 | git push windrose master --tags 25 | ``` 26 | 27 | * Verify on Zenodo 28 | 29 | Go to https://zenodo.org/account/settings/github/repository/python-windrose/windrose 30 | 31 | to ensure that new release have a DOI 32 | 33 | ## Upload to PyPI 34 | 35 | ### Automatic PyPI upload 36 | 37 | PyPI deployment is done via https://github.com/python-windrose/windrose/blob/master/.github/workflows/publish.yml 38 | 39 | When tagging a new release on Github, package should be automatically uploaded on PyPI. 40 | 41 | ### Manual PyPI upload 42 | 43 | Ensure a `~/.pypirc` exists 44 | 45 | ``` 46 | [distutils] # this tells distutils what package indexes you can push to 47 | index-servers = pypi 48 | pypi # the live PyPI 49 | pypitest # test PyPI 50 | 51 | [pypi] 52 | repository:http://pypi.python.org/pypi 53 | username:scls 54 | password:********** 55 | ``` 56 | 57 | Upload 58 | 59 | ``` 60 | git clean -xfd 61 | python -m build --sdist --wheel . --outdir dist 62 | python -m twine check dist/* 63 | python -m twine upload dist/* 64 | ``` 65 | 66 | ## Verify on PyPI 67 | 68 | Go to https://pypi.org/project/windrose/ 69 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # List of python-windrose contributors and notable users (sorted alphabetically) 2 | 3 | - Pete Bachant - Wind Farm Engineer at WindESCo - https://github.com/petebachant 4 | - Brian Blaylock - University of Utah Department of Atmospheric Sciences - https://github.com/blaylockbk 5 | - Ryan Brown - United States Environmental Protection Agency - https://www.epa.gov/ 6 | - daniclaar - Baum lab Applied ecology for impacted oceans - University of Victoria, BC, Canada - https://github.com/daniclaar 7 | - Sébastien Celles - Université de Poitiers - IUT de Poitiers - https://github.com/s-celles - previous project maintainer and co-author 8 | - Filipe Fernandes - Research Software Engineer contractor for SECOORA/IOOS - https://github.com/ocefpaf 9 | - Daniel Garver - United States Environmental Protection Agency - https://www.epa.gov/ 10 | - Fabien Maussion - Research Centre for Climate at the University of Innsbruck - https://github.com/fmaussion 11 | - James McCann - Ramboll - https://github.com/mccannjb 12 | - Ivan Ogasawara - https://github.com/xmnlab 13 | - Julian Quick - National Renewable Energy Laboratory, Golden, CO - https://github.com/kilojoules 14 | - Lionel Roubeyrie - LIMAIR - https://github.com/LionelR - original author 15 | - Bruno Ruas De Pinho - https://github.com/brunorpinho 16 | - Samuël Weber - Institut des Géosciences de l'Environnement, Grenoble, France, https://github.com/weber-s 17 | - 15b3 - https://github.com/15b3 18 | 19 | [Full Github contributors list](https://github.com/python-windrose/windrose/graphs/contributors). 20 | 21 | and maybe more... 22 | -------------------------------------------------------------------------------- /samples/example_statistical_input.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | import numpy as np 5 | from matplotlib import pyplot as plt 6 | 7 | from windrose import WindroseAxes 8 | 9 | FILENAME_DEFAULT = os.path.join( 10 | os.path.dirname(os.path.abspath(__file__)), 11 | "amalia_directionally_averaged_speeds.txt", 12 | ) 13 | 14 | 15 | @click.command() 16 | @click.option("--filename", default=FILENAME_DEFAULT, help="Input filename") 17 | def main(filename): 18 | fig = plt.figure(figsize=(12, 8), dpi=80, facecolor="w", edgecolor="w") 19 | ax = WindroseAxes(fig, [0.1, 0.1, 0.8, 0.8], facecolor="w") 20 | fig.add_axes(ax) 21 | windRose = np.loadtxt(filename) 22 | indexes = np.where(windRose[:, 1] > 0.1) 23 | windDirections = windRose[indexes[0], 0] 24 | windSpeeds = windRose[indexes[0], 1] 25 | # convert from mean wind speed to weibull scale factor 26 | # windSpeeds = windRose[indexes[0], 1] * 2 / np.sqrt(np.pi) 27 | windFrequencies = windRose[indexes[0], 2] 28 | # size = len(windDirections) 29 | ax.box( 30 | windDirections, 31 | windSpeeds, 32 | frequency=windFrequencies, 33 | mean_values=1, 34 | bins=[15, 18, 20, 23, 25], 35 | nsector=72, 36 | ) 37 | # ax.box( 38 | # windDirections, 39 | # [[windSpeeds[i], 2] for i in range(len(windSpeeds))], 40 | # frequency=windFrequencies, 41 | # weibull_factors=1, 42 | # bins=[15, 18, 20, 23, 25], 43 | # nsector=72, 44 | # ) 45 | ax.set_yticklabels([]) 46 | plt.show() 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /tests/test_windrose_np_mpl_func.py: -------------------------------------------------------------------------------- 1 | # generate the baseline: pytest tests/test_windrose_np_mpl_func.py --mpl-generate-path=tests/output/func 2 | 3 | import matplotlib 4 | import numpy as np 5 | import pytest 6 | from matplotlib import cm as cm 7 | 8 | from windrose import wrbar, wrbox, wrcontour, wrcontourf, wrpdf, wrscatter 9 | 10 | np.random.seed(0) 11 | 12 | matplotlib.use("Agg") # noqa 13 | # Create wind speed and direction variables 14 | N = 500 15 | ws = np.random.random(N) * 6 16 | wd = np.random.random(N) * 360 17 | bins = np.arange(0, 8, 1) 18 | 19 | 20 | @pytest.mark.mpl_image_compare(baseline_dir="output/func") 21 | def test_wrscatter(): 22 | ax = wrscatter(wd, ws, alpha=0.2) 23 | return ax.figure 24 | 25 | 26 | @pytest.mark.mpl_image_compare(baseline_dir="output/func", tolerance=15.5) 27 | def test_wrbar(): 28 | ax = wrbar(wd, ws, normed=True, opening=0.8, edgecolor="white") 29 | return ax.figure 30 | 31 | 32 | @pytest.mark.mpl_image_compare(baseline_dir="output/func", tolerance=6.5) 33 | def test_wrbox(): 34 | ax = wrbox(wd, ws, bins=bins) 35 | return ax.figure 36 | 37 | 38 | @pytest.mark.mpl_image_compare(baseline_dir="output/func") 39 | def test_wrcontourf(): 40 | ax = wrcontourf(wd, ws, bins=bins, cmap=cm.hot) 41 | return ax.figure 42 | 43 | 44 | @pytest.mark.mpl_image_compare(baseline_dir="output/func") 45 | def test_wrcontour(): 46 | ax = wrcontour(wd, ws, bins=bins, cmap=cm.hot, lw=3) 47 | return ax.figure 48 | 49 | 50 | @pytest.mark.mpl_image_compare(baseline_dir="output/func") 51 | def test_wrpdf(): 52 | ax, params = wrpdf(ws, bins=bins) 53 | return ax.figure 54 | -------------------------------------------------------------------------------- /samples/example_np_mpl_oo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import matplotlib.cm as cm 4 | import numpy as np 5 | from matplotlib import pyplot as plt 6 | 7 | from windrose import WindAxes, WindroseAxes 8 | 9 | 10 | def main(): 11 | # Create wind speed and direction variables 12 | N = 500 13 | ws = np.random.random(N) * 6 14 | wd = np.random.random(N) * 360 15 | 16 | ax = WindroseAxes.from_ax() 17 | ax.scatter(wd, ws, alpha=0.2) 18 | ax.set_legend() 19 | 20 | # windrose like a stacked histogram with normed (displayed in percent) results 21 | ax = WindroseAxes.from_ax() 22 | ax.bar(wd, ws, normed=True, opening=0.8, edgecolor="white") 23 | ax.set_legend() 24 | 25 | # Another stacked histogram representation, not normed, with bins limits 26 | ax = WindroseAxes.from_ax() 27 | bins = np.arange(0, 8, 1) 28 | ax.box(wd, ws, bins=bins) 29 | ax.set_legend() 30 | 31 | # A windrose in filled representation, with a controleld colormap 32 | ax = WindroseAxes.from_ax() 33 | ax.contourf(wd, ws, bins=bins, cmap=cm.hot) 34 | ax.set_legend() 35 | 36 | # Same as above, but with contours over each filled region... 37 | ax = WindroseAxes.from_ax() 38 | ax.contourf(wd, ws, bins=bins, cmap=cm.hot) 39 | ax.contour(wd, ws, bins=bins, colors="black") 40 | ax.set_legend() 41 | 42 | # ...or without filled regions 43 | ax = WindroseAxes.from_ax() 44 | ax.contour(wd, ws, bins=bins, cmap=cm.hot, lw=3) 45 | ax.set_legend() 46 | 47 | # print ax._info 48 | # plt.show() 49 | 50 | ax = WindAxes.from_ax() 51 | bins = np.arange(0, 6 + 1, 0.5) 52 | bins = bins[1:] 53 | ax.pdf(ws, bins=bins) 54 | plt.show() 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | |Latest Version| |Supported Python versions| |Wheel format| |License| 2 | |Development Status| |Downloads monthly| |Tests| |DOI| |Research software impact| 3 | 4 | .. windrose documentation master file, created by 5 | sphinx-quickstart on Tue May 1 16:51:19 2018. 6 | You can adapt this file completely to your liking, but it should at least 7 | contain the root `toctree` directive. 8 | 9 | Welcome to windrose's documentation! 10 | ==================================== 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | :caption: Contents: 15 | 16 | Install 17 | Usage 18 | Development 19 | API 20 | 21 | A windrose, also known as a polar rose plot, is a special diagram for 22 | representing the distribution of meteorological data, typically wind 23 | speeds by class and direction. This is a simple module for the 24 | matplotlib python library, which requires numpy for internal 25 | computation. 26 | 27 | Original code forked from: - windrose 1.4 by `Lionel 28 | Roubeyrie `__ lionel.roubeyrie@gmail.com 29 | http://youarealegend.blogspot.com/search/label/windrose 30 | 31 | 32 | 33 | https://docs.github.com/en/pull-requests/collaborating-with-pull-requests 34 | 35 | .. |Latest Version| image:: https://img.shields.io/pypi/v/windrose.svg 36 | :target: https://pypi.org/project/windrose/ 37 | .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/windrose.svg 38 | :target: https://pypi.org/project/windrose/ 39 | .. |Wheel format| image:: https://img.shields.io/pypi/wheel/windrose.svg 40 | :target: https://pypi.org/project/windrose/ 41 | .. |License| image:: https://img.shields.io/pypi/l/windrose.svg 42 | :target: https://pypi.org/project/windrose/ 43 | .. |Development Status| image:: https://img.shields.io/pypi/status/windrose.svg 44 | :target: https://pypi.org/project/windrose/ 45 | .. |Downloads monthly| image:: https://img.shields.io/pypi/dm/windrose.svg 46 | :target: https://pypi.org/project/windrose/ 47 | .. |Tests| image:: https://github.com/python-windrose/windrose/actions/workflows/tests.yml/badge.svg 48 | :target: https://github.com/python-windrose/windrose/actions/workflows/tests.yml 49 | .. |DOI| image:: https://zenodo.org/badge/37549137.svg 50 | :target: https://zenodo.org/badge/latestdoi/37549137 51 | 52 | Indices and tables 53 | ================== 54 | 55 | * :ref:`genindex` 56 | * :ref:`modindex` 57 | * :ref:`search` 58 | -------------------------------------------------------------------------------- /tests/test_windrose_pandas.py: -------------------------------------------------------------------------------- 1 | # generate the baseline: pytest tests/test_windrose_pandas.py --mpl-generate-path=tests/output/df/ 2 | 3 | 4 | import matplotlib 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | import pandas as pd 8 | import pytest 9 | from matplotlib import cm as cm 10 | 11 | from windrose import plot_windrose 12 | 13 | matplotlib.use("Agg") # noqa 14 | np.random.seed(0) 15 | # Create wind speed and direction variables 16 | N = 500 17 | ws = np.random.random(N) * 6 18 | wd = np.random.random(N) * 360 19 | bins = np.arange(0, 8, 1) 20 | 21 | df = pd.DataFrame({"speed": ws, "direction": wd}) 22 | 23 | 24 | @pytest.mark.mpl_image_compare(baseline_dir="output/df") 25 | def test_scatter(): 26 | kind = "scatter" 27 | ax = plot_windrose(df, kind=kind, alpha=0.2) 28 | return ax.figure 29 | 30 | 31 | @pytest.mark.mpl_image_compare(baseline_dir="output/df", tolerance=15.5) 32 | def test_bar(): 33 | kind = "bar" 34 | ax = plot_windrose(df, kind=kind, normed=True, opening=0.8, edgecolor="white") 35 | return ax.figure 36 | 37 | 38 | @pytest.mark.mpl_image_compare(baseline_dir="output/df", tolerance=6.5) 39 | def test_box(): 40 | kind = "box" 41 | ax = plot_windrose(df, kind=kind, bins=bins) 42 | return ax.figure 43 | 44 | 45 | @pytest.mark.mpl_image_compare(baseline_dir="output/df") 46 | def test_contourf(): 47 | kind = "contourf" 48 | ax = plot_windrose(df, kind=kind, bins=np.arange(0.01, 8, 1), cmap=cm.hot) 49 | return ax.figure 50 | 51 | 52 | @pytest.mark.mpl_image_compare(baseline_dir="output/df") 53 | def test_contour(): 54 | kind = "contour" 55 | ax = plot_windrose(df, kind=kind, bins=np.arange(0.01, 8, 1), cmap=cm.hot, lw=3) 56 | return ax.figure 57 | 58 | 59 | @pytest.mark.mpl_image_compare(baseline_dir="output/df") 60 | def test_pdf(): 61 | kind = "pdf" 62 | ax, params = plot_windrose(df, kind=kind, bins=np.arange(0.01, 8, 1)) 63 | return ax.figure 64 | 65 | 66 | @pytest.mark.mpl_image_compare(baseline_dir="output/df") 67 | def test_windrose_np_plot_and_pd_plot(): 68 | # Not really pandas but this is an orphan test and fits the plot_windrose tests. 69 | kind = "scatter" 70 | ax = plot_windrose(wd, ws, kind=kind, alpha=0.2) 71 | return ax.figure 72 | 73 | 74 | @pytest.mark.mpl_image_compare(baseline_dir="output/df") 75 | def test_subplots(): 76 | fig, (ax1, ax2) = plt.subplots(ncols=2, subplot_kw={"projection": "windrose"}) 77 | plot_windrose(df, ax=ax1) 78 | plot_windrose(df, ax=ax2) 79 | return fig 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you discover issues, have ideas for improvements or new features, 4 | please report them to the [issue tracker](https://github.com/python-windrose/windrose/issues) of the repository. 5 | After this you can help by fixing it submitting a pull request (PR). 6 | Please, try to follow these guidelines when you 7 | do so. 8 | 9 | ## Issue reporting 10 | 11 | * Check that the issue has not already been reported. 12 | * Check that the issue has not already been fixed in the latest code 13 | (a.k.a. `master`). So be certain that you are using latest `master` code version 14 | (not latest released version). Installing latest development version can be done using: 15 | 16 | ```bash 17 | $ pip install git+https://github.com/python-windrose/windrose 18 | ``` 19 | 20 | or 21 | 22 | ```bash 23 | $ git clone https://github.com/python-windrose/windrose 24 | $ python setup.py install 25 | ``` 26 | 27 | * Be clear, concise and precise in your description of the problem. 28 | * Open an issue with a descriptive title and a summary in grammatically correct, 29 | complete sentences. 30 | * Mention your Python version and operating system. 31 | * Include any relevant code to the issue summary. 32 | A [Minimal Working Example (MWE)](https://en.wikipedia.org/wiki/Minimal_Working_Example) can help. 33 | 34 | ### Reporting bugs 35 | 36 | When reporting bugs it's a good idea to provide stacktrace messages to 37 | the bug report makes it easier to track down bugs. Some steps to reproduce a bug 38 | reliably would also make a huge difference. 39 | 40 | ## Pull requests 41 | 42 | * Read [how to properly contribute to open source projects on Github](http://gun.io/blog/how-to-github-fork-branch-and-pull-request). 43 | * Use a topic branch to easily amend a pull request later, if necessary. 44 | * Use the same coding conventions as the rest of the project. 45 | * Make sure that the unit tests are passing (`py.test`). 46 | * Write [good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 47 | * Mention related tickets in the commit messages (e.g. `[Fix #N] Add command ...`). 48 | * Update the [changelog](https://github.com/python-windrose/windrose/blob/master/CHANGELOG.md). 49 | * [Squash related commits together](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html). 50 | * Open a [pull request](https://help.github.com/articles/using-pull-requests) that relates to *only* one subject with a clear title 51 | and description in grammatically correct, complete sentences. 52 | -------------------------------------------------------------------------------- /samples/example_subplots.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import datetime 4 | 5 | import click 6 | import matplotlib.cm as cm 7 | import numpy as np 8 | import pandas as pd 9 | from matplotlib import pyplot as plt 10 | 11 | import windrose # noqa 12 | 13 | pd.set_option("max_rows", 10) 14 | 15 | 16 | def get_by_func(by=None, by_func=None): 17 | if by is None and by_func is None: 18 | by = "MS" 19 | 20 | if by in ["year", "yearly", "Y"]: 21 | return lambda dt: dt.year 22 | elif by in ["month", "monthly", "MS"]: # MS: month start 23 | return lambda dt: (dt.year, dt.month) 24 | elif by in ["day", "daily", "D"]: 25 | return lambda dt: (dt.year, dt.month, dt.day) 26 | elif by is None and by_func is not None: 27 | return by_func 28 | else: 29 | raise NotImplementedError("'%s' is not an allowed 'by' parameter" % by) 30 | 31 | 32 | def tuple_position(i, nrows, ncols): 33 | i_sheet, sheet_pos = divmod(i, ncols * nrows) 34 | i_row, i_col = divmod(sheet_pos, ncols) 35 | return i_sheet, i_row, i_col 36 | 37 | 38 | @click.command() 39 | @click.option( 40 | "--filename", default="samples/sample_wind_poitiers.csv", help="Input filename" 41 | ) 42 | @click.option("--year", default=2014, help="Year") 43 | def main(filename, year): 44 | df_all = pd.read_csv(filename, parse_dates=["Timestamp"]) 45 | df_all = df_all.set_index("Timestamp") 46 | 47 | f_year = get_by_func("year") 48 | df_all["by_page"] = df_all.index.map(f_year) 49 | f_month = get_by_func("month") 50 | df_all["by"] = df_all.index.map(f_month) 51 | 52 | df_all = df_all.reset_index().set_index(["by_page", "by", "Timestamp"]) 53 | 54 | nrows, ncols = 3, 4 55 | fig = plt.figure() 56 | bins = np.arange(0.01, 8, 1) 57 | 58 | fig.suptitle("Wind speed - %d" % year) 59 | for month in range(1, 13): 60 | ax = fig.add_subplot(nrows, ncols, month, projection="windrose") 61 | title = datetime.datetime(year, month, 1).strftime("%b") 62 | ax.set_title(title) 63 | try: 64 | df = df_all.loc[year].loc[(year, month)] 65 | except KeyError: 66 | continue 67 | direction = df["direction"].values 68 | var = df["speed"].values 69 | # ax.contour(direction, var, bins=bins, colors='black', lw=3) 70 | ax.contourf(direction, var, bins=bins, cmap=cm.hot) 71 | ax.contour(direction, var, bins=bins, colors="black") 72 | 73 | plt.show() 74 | 75 | 76 | if __name__ == "__main__": 77 | main() 78 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'samples' 2 | 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v6.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: check-ast 9 | - id: debug-statements 10 | - id: end-of-file-fixer 11 | - id: check-docstring-first 12 | - id: check-added-large-files 13 | exclude_types: [yaml] 14 | - id: requirements-txt-fixer 15 | - id: file-contents-sorter 16 | files: requirements-dev.txt 17 | 18 | - repo: https://github.com/psf/black-pre-commit-mirror 19 | rev: 25.11.0 20 | hooks: 21 | - id: black 22 | language_version: python3 23 | 24 | - repo: https://github.com/keewis/blackdoc 25 | rev: v0.4.6 26 | hooks: 27 | - id: blackdoc 28 | 29 | - repo: https://github.com/codespell-project/codespell 30 | rev: v2.4.1 31 | hooks: 32 | - id: codespell 33 | exclude: > 34 | (?x)^( 35 | .*\.ipynb 36 | )$ 37 | args: 38 | - --ignore-words-list=celles,hist,therefor 39 | 40 | 41 | - repo: https://github.com/econchick/interrogate 42 | rev: 1.7.0 43 | hooks: 44 | - id: interrogate 45 | exclude: ^(docs|setup.py|tests) 46 | args: [--config=pyproject.toml] 47 | 48 | - repo: https://github.com/asottile/add-trailing-comma 49 | rev: v4.0.0 50 | hooks: 51 | - id: add-trailing-comma 52 | 53 | - repo: https://github.com/astral-sh/ruff-pre-commit 54 | rev: v0.14.7 55 | hooks: 56 | - id: ruff 57 | 58 | - repo: https://github.com/tox-dev/pyproject-fmt 59 | rev: v2.11.1 60 | hooks: 61 | - id: pyproject-fmt 62 | 63 | - repo: https://github.com/nbQA-dev/nbQA 64 | rev: 1.9.1 65 | hooks: 66 | # mdformat works on the CLI but not as pre-commit yet. 67 | # Use `nbqa mdformat jupyterbook --nbqa-md` to run it locally. 68 | # - id: mdformat 69 | - id: nbqa-check-ast 70 | - id: nbqa-black 71 | - id: nbqa-ruff 72 | # We are skipping: 73 | # E402 -> Module level import not at top of file 74 | # T201 -> `print` found 75 | # B018 -> Found useless expression. 76 | args: [--fix, "--extend-ignore=E402,T201,B018"] 77 | 78 | - repo: https://github.com/bdice/nb-strip-paths 79 | rev: v0.1.0 80 | hooks: 81 | - id: nb-strip-paths 82 | 83 | ci: 84 | autofix_commit_msg: | 85 | [pre-commit.ci] auto fixes from pre-commit.com hooks 86 | 87 | for more information, see https://pre-commit.ci 88 | autofix_prs: false 89 | autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' 90 | autoupdate_schedule: monthly 91 | skip: [] 92 | submodules: false 93 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at s.celles@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools>=41.2", 5 | "setuptools-scm", 6 | "wheel", 7 | ] 8 | 9 | [project] 10 | name = "windrose" 11 | description = "Python Matplotlib, Numpy library to manage wind data, draw windrose (also known as a polar rose plot)" 12 | readme = "README.md" 13 | license = { text = "BSD-3-Clause OR BCeCILL-B" } 14 | maintainers = [ 15 | { name = "Sebastien Celles" }, 16 | { name = "Filipe Fernandes", email = "ocefpaf+windrose@gmail.com" }, 17 | ] 18 | authors = [ 19 | { name = "Lionel Roubeyrie", email = "s.celles@gmail.co" }, 20 | ] 21 | requires-python = ">=3.8" 22 | classifiers = [ 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Programming Language :: Python :: 3.14", 31 | ] 32 | dynamic = [ 33 | "version", 34 | ] 35 | dependencies = [ 36 | "matplotlib>=3", 37 | "numpy>=1.21", 38 | ] 39 | optional-dependencies.extras = [ 40 | "pandas", 41 | "scipy", 42 | ] 43 | urls.documentation = "https://python-windrose.github.io/windrose" 44 | urls.homepage = "https://github.com/python-windrose/windrose" 45 | urls.repository = "https://github.com/python-windrose/windrose" 46 | 47 | [tool.setuptools] 48 | packages = [ "windrose" ] 49 | include-package-data = true 50 | license-files = [ 51 | "LICENSE.txt", 52 | "LICENCE_BSD-3-Clause.txt", 53 | "LICENCE_CECILL-B.txt", 54 | ] 55 | [tool.setuptools.dynamic] 56 | dependencies = { file = [ "requirements.txt" ] } 57 | readme = { file = "README.md", content-type = "text/markdown" } 58 | 59 | [tool.setuptools_scm] 60 | write_to = "windrose/_version.py" 61 | write_to_template = "__version__ = '{version}'" 62 | tag_regex = "^(?Pv)?(?P[^\\+]+)(?P.*)?$" 63 | 64 | [tool.ruff] 65 | target-version = "py38" 66 | line-length = 79 67 | 68 | lint.select = [ 69 | "A", # flake8-builtins 70 | "B", # flake8-bugbear 71 | "C4", # flake8-comprehensions 72 | "F", # flakes 73 | "I", # import sorting 74 | "T20", # flake8-print 75 | "UP", # upgrade 76 | ] 77 | lint.per-file-ignores."docs/conf.py" = [ 78 | "A001", 79 | ] 80 | lint.per-file-ignores."notebooks/usage.ipynb" = [ 81 | "T201", 82 | ] 83 | 84 | lint.per-file-ignores."samples/example_by.py" = [ 85 | "T201", 86 | ] 87 | lint.per-file-ignores."samples/example_pdf_by.py" = [ 88 | "T201", 89 | ] 90 | 91 | [tool.check-manifest] 92 | ignore = [ 93 | "*.yaml", 94 | ".coveragerc", 95 | "docs", 96 | "docs/*", 97 | "notebooks", 98 | "notebooks/*", 99 | "tests", 100 | "tests/*", 101 | "paper", 102 | "paper/*", 103 | "*.pyc", 104 | ".binder/", 105 | ".binder/*", 106 | ] 107 | 108 | [tool.interrogate] 109 | ignore-init-method = true 110 | ignore-init-module = false 111 | ignore-magic = false 112 | ignore-semiprivate = false 113 | ignore-private = false 114 | ignore-module = false 115 | fail-under = 70 116 | exclude = [ 117 | "setup.py", 118 | "docs", 119 | "tests", 120 | ] 121 | verbose = 1 122 | quiet = false 123 | color = true 124 | -------------------------------------------------------------------------------- /tests/test_windrose.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | import pandas as pd 5 | import pytest 6 | from numpy.testing import assert_allclose 7 | from pandas.testing import assert_frame_equal 8 | 9 | from windrose import ( 10 | DEFAULT_THETA_LABELS, 11 | WindroseAxes, 12 | clean, 13 | clean_df, 14 | plot_windrose, 15 | ) 16 | 17 | matplotlib.use("Agg") # noqa 18 | # Create wind speed and direction variables 19 | N = 500 20 | ws = np.random.random(N) * 6 21 | wd = np.random.random(N) * 360 22 | 23 | df = pd.DataFrame({"speed": ws, "direction": wd}) 24 | 25 | 26 | def test_windrose_pd_not_default_names(): 27 | kind = "scatter" 28 | df_not_default_names = pd.DataFrame({"wind_speed": ws, "wind_direction": wd}) 29 | plot_windrose( 30 | df_not_default_names, 31 | kind=kind, 32 | alpha=0.2, 33 | var_name="wind_speed", 34 | direction_name="wind_direction", 35 | ) 36 | 37 | 38 | def test_windrose_clean(): 39 | direction = np.array([1.0, 1.0, 1.0, np.nan, np.nan, np.nan]) 40 | var = np.array([2.0, 0.0, np.nan, 2.0, 0.0, np.nan]) 41 | actual_direction, actual_var = clean(direction, var) 42 | expected_direction = np.array([1.0]) 43 | expected_var = np.array([2.0]) 44 | assert_allclose(actual_direction, expected_direction) 45 | assert_allclose(actual_var, expected_var) 46 | 47 | 48 | def test_windrose_clean_df(): 49 | df = pd.DataFrame( 50 | { 51 | "direction": [1.0, 1.0, 1.0, np.nan, np.nan, np.nan], 52 | "speed": [2.0, 0.0, np.nan, 2.0, 0.0, np.nan], 53 | }, 54 | ) 55 | actual_df = clean_df(df) 56 | expected_df = pd.DataFrame( 57 | { 58 | "direction": [1.0], 59 | "speed": [2.0], 60 | }, 61 | ) 62 | assert_frame_equal(actual_df, expected_df) 63 | 64 | 65 | def test_theta_labels(): 66 | # Ensure default theta_labels are correct 67 | ax = WindroseAxes.from_ax() 68 | theta_labels = [t.get_text() for t in ax.get_xticklabels()] 69 | assert theta_labels == DEFAULT_THETA_LABELS 70 | plt.close() 71 | 72 | # Ensure theta_labels are changed when specified 73 | ax = WindroseAxes.from_ax(theta_labels=list("abcdefgh")) 74 | theta_labels = [t.get_text() for t in ax.get_xticklabels()] 75 | assert theta_labels == ["a", "b", "c", "d", "e", "f", "g", "h"] 76 | plt.close() 77 | 78 | 79 | def test_windrose_incorrect_bins_specified(): 80 | ax = WindroseAxes.from_ax() 81 | with pytest.raises(ValueError) as exc_info: 82 | ax.bar(direction=wd, var=ws, bins=[1, 2, 3]) 83 | 84 | (msg,) = exc_info.value.args 85 | assert msg == ( 86 | "the first value provided in bins must be less than or equal to the minimum " 87 | "value of the wind speed data. Did you mean: bins=(0, 1, 2, 3) ? " 88 | "If you want to exclude values below a certain threshold, " 89 | "try setting calm_limit=1." 90 | ) 91 | 92 | 93 | def test_windrose_incorrect_bins_in_combination_with_calm_limit(): 94 | ax = WindroseAxes.from_ax() 95 | with pytest.raises(ValueError) as exc_info: 96 | ax.bar(direction=wd, var=ws, bins=[1, 2, 3], calm_limit=2) 97 | 98 | (msg,) = exc_info.value.args 99 | assert msg == "the lowest value in bins must be >= 2 (=calm_limits)" 100 | -------------------------------------------------------------------------------- /paper/paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Windrose: A Python Matplotlib, Numpy library to manage wind and pollution data, draw windrose' 3 | tags: 4 | - windrose 5 | - windspeed 6 | - wind 7 | - speed 8 | - plot 9 | - python 10 | - matplotlib 11 | - numpy 12 | - pandas 13 | authors: 14 | - name: Lionel Roubeyrie 15 | orcid: 0000-0001-6017-4385 16 | affiliation: 1 17 | - name: Sébastien Celles 18 | orcid: 0000-0001-9987-4338 19 | affiliation: 2 20 | affiliations: 21 | - name: LIMAIR 22 | index: 1 23 | - name: Université de Poitiers - IUT de Poitiers (Poitiers Institute of Technology) 24 | index: 2 25 | date: 11 may 2018 26 | bibliography: paper.bib 27 | nocite: | 28 | @wiki:xxx, @doi:10.1109/MCSE.2007.58, @doi:10.1109/MCSE.2011.36, @Walt:2011:NAS:1957373.1957466, @doi:10.1109/MCSE.2007.55, @mckinney-proc-scipy-2010, @doi:10.1109/MCSE.2007.53, @oliphant2001scipy, @oliphant2006guide, @munn1969pollution, @nrcs, @garver2016, @quick2017optimization, @harris2014parent, @horel2016summer 29 | --- 30 | 31 | # Summary 32 | 33 | A [wind rose](https://en.wikipedia.org/wiki/Wind_rose) is a graphic tool used by meteorologists to give a succinct view of how wind speed and direction are typically distributed at a particular location. It can also be used to describe air quality pollution sources. The wind rose tool uses Matplotlib as a backend. Data can be passed to the package using Numpy arrays or a Pandas DataFrame. 34 | 35 | Windrose is a Python library to manage wind data, draw windroses (also known as polar rose plots), and fit Weibull probability density functions. 36 | 37 | The initial use case of this library was for a technical report concerning pollution exposure and wind distributions analyzes. Data from local pollution measures and meteorologic information from various sources like Meteo-France were used to generate a pollution source wind rose. 38 | 39 | It is also used by some contributors for teaching purpose. 40 | 41 | -![Map overlay](screenshots/overlay.png) 42 | 43 | Some others contributors have used it to make figures for a [wind power plant control optimization study](https://www.nrel.gov/docs/fy17osti/68185.pdf). 44 | 45 | Some academics use it to track lightning strikes during high intensity storms. They are using it to visualize the motion of storms based on the relative position of the lightning from one strike to the next. 46 | 47 | # Examples 48 | 49 | - The bar plot wind rose is the most common plot 50 | 51 | -![Windrose (bar) example](screenshots/bar.png) 52 | 53 | - Contour plots are also possible 54 | 55 | -![Windrose (contourf-contour) example](screenshots/contourf-contour.png) 56 | 57 | - Several windroses can be plotted using subplots to provide a plot per year with for example subplots per month 58 | 59 | -![Windrose subplots](screenshots/subplots.png) 60 | 61 | - Probability density functions (PDF) may be plotted. Fitting Weibull distribution is enabled by Scipy. 62 | The Weibull distribution is used in weather forecasting and the wind power industry to describe wind speed distributions, as the natural distribution of wind speeds often matches the Weibull shape 63 | 64 | -![Probability density function (PDF) example](screenshots/pdf.png) 65 | 66 | # More advanced usages and contributing 67 | 68 | Full documentation of library is available at http://windrose.readthedocs.io/. 69 | 70 | If you discover issues, have ideas for improvements or new features, please report them. 71 | [CONTRIBUTING.md](https://github.com/python-windrose/windrose/blob/master/CONTRIBUTING.md) explains 72 | how to contribute to this project. 73 | 74 | List of contributors and/or notable users can be found at [CONTRIBUTORS.md](https://github.com/python-windrose/windrose/blob/master/CONTRIBUTORS.md). 75 | 76 | # Future 77 | 78 | Windrose is still an evolving library which still need care from its users and developers. 79 | 80 | - Map overlay is a feature that could help some users. 81 | - A better API for video exporting could be an interesting improvement. 82 | - Add the capability to make wind roses based on the Weibull shape and scale factors could be considered. 83 | - Make windroses from an histogram table rather than from two arrays of wind speed and wind direction is also a requested feature. 84 | 85 | # References 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Version](https://img.shields.io/pypi/v/windrose.svg)](https://pypi.python.org/pypi/windrose/) 2 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/windrose.svg)](https://pypi.python.org/pypi/windrose/) 3 | [![Wheel format](https://img.shields.io/pypi/wheel/windrose.svg)](https://pypi.python.org/pypi/windrose/) 4 | [![License](https://img.shields.io/pypi/l/windrose.svg)](https://pypi.python.org/pypi/windrose/) 5 | [![Development Status](https://img.shields.io/pypi/status/windrose.svg)](https://pypi.python.org/pypi/windrose/) 6 | [![Tests](https://github.com/python-windrose/windrose/actions/workflows/tests.yml/badge.svg)](https://github.com/python-windrose/windrose/actions/workflows/tests.yml) 7 | [![DOI](https://zenodo.org/badge/37549137.svg)](https://zenodo.org/badge/latestdoi/37549137) 8 | [![JOSS](https://joss.theoj.org/papers/10.21105/joss.00268/status.svg)](https://joss.theoj.org/papers/10.21105/joss.00268) 9 | 10 | # Windrose 11 | 12 | A [wind rose](https://en.wikipedia.org/wiki/Wind_rose) is a graphic tool used by meteorologists to give a succinct view of how wind speed and direction are typically distributed at a particular location. It can also be used to describe air quality pollution sources. The wind rose tool uses Matplotlib as a backend. Data can be passed to the package using Numpy arrays or a Pandas DataFrame. 13 | 14 | Windrose is a Python library to manage wind data, draw windroses (also known as polar rose plots), and fit Weibull probability density functions. 15 | 16 | The initial use case of this library was for a technical report concerning pollution exposure and wind distributions analyzes. Data from local pollution measures and meteorologic information from various sources like Meteo-France were used to generate a pollution source wind rose. 17 | 18 | It is also used by some contributors for teaching purpose. 19 | 20 | ![Map overlay](https://raw.githubusercontent.com/python-windrose/windrose/main/paper/screenshots/overlay.png) 21 | 22 | Some others contributors have used it to make figures for a [wind power plant control optimization study](https://www.nrel.gov/docs/fy17osti/68185.pdf). 23 | 24 | Some academics use it to track lightning strikes during high intensity storms. They are using it to visualize the motion of storms based on the relative position of the lightning from one strike to the next. 25 | 26 | ## Try windrose on mybinder.org 27 | 28 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/python-windrose/windrose/HEAD?labpath=notebooks) 29 | 30 | ## Install 31 | 32 | ### Requirements 33 | 34 | - matplotlib http://matplotlib.org/ 35 | - numpy http://www.numpy.org/ 36 | - and naturally python https://www.python.org/ :-P 37 | 38 | Optional libraries: 39 | 40 | - Pandas http://pandas.pydata.org/ (to feed plot functions easily) 41 | - Scipy http://www.scipy.org/ (to fit data with Weibull distribution) 42 | - ffmpeg https://www.ffmpeg.org/ (to output video) 43 | - click http://click.pocoo.org/ (for command line interface tools) 44 | - seaborn https://seaborn.pydata.org/ (for easy subplots) 45 | 46 | ### Install latest release version via pip 47 | 48 | A package is available and can be downloaded from PyPi and installed using: 49 | 50 | ```bash 51 | $ pip install windrose 52 | ``` 53 | 54 | ### Install latest development version 55 | 56 | ```bash 57 | $ pip install git+https://github.com/python-windrose/windrose 58 | ``` 59 | 60 | or 61 | 62 | ```bash 63 | $ git clone https://github.com/python-windrose/windrose 64 | $ python setup.py install 65 | ``` 66 | 67 | ## Documentation 68 | Full documentation of library is available at https://python-windrose.github.io/windrose/ 69 | 70 | ## Community guidelines 71 | 72 | You can help to develop this library. 73 | 74 | ### Code of Conduct 75 | 76 | If you are using Python Windrose and want to interact with developers, others users... 77 | we encourage you to follow our [code of conduct](https://github.com/python-windrose/windrose/blob/master/CODE_OF_CONDUCT.md). 78 | 79 | ### Contributing 80 | 81 | If you discover issues, have ideas for improvements or new features, please report them. 82 | [CONTRIBUTING.md](https://github.com/python-windrose/windrose/blob/master/CONTRIBUTING.md) explains 83 | how to contribute to this project. 84 | 85 | ### List of contributors and/or notable users 86 | https://github.com/python-windrose/windrose/blob/main/CONTRIBUTORS.md 87 | -------------------------------------------------------------------------------- /notebooks/windrose_sample_poitiers_csv.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "\n", 11 | "url = \"https://github.com/python-windrose/windrose/raw/main/samples/sample_wind_poitiers.csv\"\n", 12 | "df = pd.read_csv(url, parse_dates=[\"Timestamp\"])\n", 13 | "df = df.set_index(\"Timestamp\")\n", 14 | "df" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import numpy as np\n", 24 | "\n", 25 | "df[\"speed_x\"] = df[\"speed\"] * np.sin(df[\"direction\"] * np.pi / 180.0)\n", 26 | "df[\"speed_y\"] = df[\"speed\"] * np.cos(df[\"direction\"] * np.pi / 180.0)" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "from matplotlib import pyplot as plt\n", 36 | "\n", 37 | "fig, ax = plt.subplots(figsize=(8, 8), dpi=80)\n", 38 | "x0, x1 = ax.get_xlim()\n", 39 | "y0, y1 = ax.get_ylim()\n", 40 | "ax.set_aspect(\"equal\")\n", 41 | "df.plot(kind=\"scatter\", x=\"speed_x\", y=\"speed_y\", alpha=0.05, ax=ax)\n", 42 | "Vw = 80\n", 43 | "ax.set_xlim([-Vw, Vw])\n", 44 | "ax.set_ylim([-Vw, Vw]);" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "import matplotlib.cm as cm\n", 54 | "\n", 55 | "from windrose import WindroseAxes\n", 56 | "\n", 57 | "ax = WindroseAxes.from_ax()\n", 58 | "ax.bar(\n", 59 | " df.direction.values, df.speed.values, bins=np.arange(0.01, 8, 1), cmap=cm.hot, lw=3\n", 60 | ")\n", 61 | "ax.set_legend();" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "from windrose import plot_windrose\n", 71 | "\n", 72 | "plot_windrose(df, kind=\"contour\", bins=np.arange(0.01, 8, 1), cmap=cm.hot, lw=3);" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "bins = np.arange(0, 30 + 1, 1)\n", 82 | "bins = bins[1:]\n", 83 | "bins" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "plot_windrose(df, kind=\"pdf\", bins=np.arange(0.01, 30, 1));" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "data = np.histogram(df[\"speed\"], bins=bins)[0]\n", 102 | "data" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "# Wind rose for a given month" 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "def plot_month(df, t_year_month, *args, **kwargs):\n", 119 | " by = \"year_month\"\n", 120 | " df[by] = df.index.map(lambda dt: (dt.year, dt.month))\n", 121 | " df_month = df[df[by] == t_year_month]\n", 122 | " ax = plot_windrose(df_month, *args, **kwargs)\n", 123 | " return ax\n", 124 | "\n", 125 | "\n", 126 | "plot_month(df, (2014, 7), kind=\"contour\", bins=np.arange(0, 10, 1), cmap=cm.hot);" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "plot_month(df, (2014, 8), kind=\"contour\", bins=np.arange(0, 10, 1), cmap=cm.hot);" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "plot_month(df, (2014, 9), kind=\"contour\", bins=np.arange(0, 10, 1), cmap=cm.hot);" 145 | ] 146 | } 147 | ], 148 | "metadata": { 149 | "kernelspec": { 150 | "display_name": "Python 3 (ipykernel)", 151 | "language": "python", 152 | "name": "python3" 153 | }, 154 | "language_info": { 155 | "codemirror_mode": { 156 | "name": "ipython", 157 | "version": 3 158 | }, 159 | "file_extension": ".py", 160 | "mimetype": "text/x-python", 161 | "name": "python", 162 | "nbconvert_exporter": "python", 163 | "pygments_lexer": "ipython3", 164 | "version": "3.11.4" 165 | } 166 | }, 167 | "nbformat": 4, 168 | "nbformat_minor": 1 169 | } 170 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Python Windrose 4 | 5 | All notable changes to this code base will be documented in this file, 6 | in every released version. 7 | 8 | ### Version 1.x.x (unreleased) 9 | 10 | ## What's Changed 11 | * Fix issue where sometimes the plot sectors showed a straight line instead of a curved one (#137) 12 | 13 | ### Version 1.7.0 14 | 15 | ## What's Changed 16 | * fix typo in docs for map overlay by @weber-s in https://github.com/python-windrose/windrose/pull/144 17 | * Docs simplify map overlay by @weber-s in https://github.com/python-windrose/windrose/pull/145 18 | * Fix variable calling/returning order by @sspagnol in https://github.com/python-windrose/windrose/pull/156 19 | * Fix clean method in case var is nan. by @15b3 in https://github.com/python-windrose/windrose/pull/164 20 | * Fix default behavior of WindroseAxes.from_ax(). by @15b3 in https://github.com/python-windrose/windrose/pull/166 21 | * Update docstring about calm_limit by @15b3 in https://github.com/python-windrose/windrose/pull/162 22 | * fix np.float deprecation warning by @theendlessriver13 in https://github.com/python-windrose/windrose/pull/167 23 | * move to GitHub Actions by @ocefpaf in https://github.com/python-windrose/windrose/pull/173 24 | * Auto-publish on PyPI and test test the tarball by @ocefpaf in https://github.com/python-windrose/windrose/pull/174 25 | * Build and upload docs by @ocefpaf in https://github.com/python-windrose/windrose/pull/175 26 | * Codespell by @ocefpaf in https://github.com/python-windrose/windrose/pull/176 27 | * Fix docs gha by @ocefpaf in https://github.com/python-windrose/windrose/pull/177 28 | * Package metadata by @ocefpaf in https://github.com/python-windrose/windrose/pull/178 29 | * Add pre-commit and many automated fixes by @ocefpaf in https://github.com/python-windrose/windrose/pull/179 30 | * Add binder environment and badge by @ocefpaf in https://github.com/python-windrose/windrose/pull/180 31 | * We no longer support Python 2k, so no universal wheel by @ocefpaf in https://github.com/python-windrose/windrose/pull/181 32 | 33 | ## New Contributors 34 | * @sspagnol made their first contribution in https://github.com/python-windrose/windrose/pull/156 35 | * @15b3 made their first contribution in https://github.com/python-windrose/windrose/pull/164 36 | * @theendlessriver13 made their first contribution in https://github.com/python-windrose/windrose/pull/167 37 | 38 | **Full Changelog**: https://github.com/python-windrose/windrose/compare/v1.6.8...v1.7.0 39 | 40 | ### Version 1.6.8 41 | 42 | - Released: 2020-09-04 43 | - Issues/Enhancements: 44 | - add custom units to the legend #128 45 | - Fix: 46 | - Fix deprecated `_autogen_docstring` for matplotlib >3.1 (#136, #117, #119, #135) 47 | 48 | ### Version 1.6.7 49 | 50 | - Released: 2019-06-07 51 | - Issues/Enhancements: 52 | - Update release procedure for manual Pypi upload 53 | 54 | ### Version 1.6.6 55 | 56 | - Released: 2019-06-07 57 | - Issues/Enhancements: 58 | - Issue #81 #31 (PR #114) Remove use of pylab.poly_between 59 | - Calm conditions 60 | - Update CONTRIBUTORS.md and CONTRIBUTING.md 61 | - PR #107 Code formatting with Black 62 | - PR #104 Fix setup.py 63 | - Autodeploy to PyPI using Travis 64 | - PEP8 65 | 66 | ### Version 1.6.5 67 | 68 | - Released: 2018-08-30 69 | - Issues/Enhancements: 70 | - Issue #99. Fix scatter plot direction 71 | 72 | ### Version 1.6.4 73 | 74 | - Released: 2018-08-22 75 | - Issues/Enhancements: 76 | - Improve doc 77 | 78 | ### Version 1.6.3 79 | 80 | - Released: 2017-08-22 81 | - Issues/Enhancements: 82 | - Issue #69 (PR #70). Dual licensing 83 | - ... 84 | 85 | ### Version 1.6.2 86 | 87 | - Released: 2017-08-02 88 | - Issues/Enhancements: 89 | - Issue #65 (PR #69). Fix inconsistent licence files 90 | - ... 91 | 92 | ### Version 1.6.1 93 | 94 | - Released: 2017-07-30 95 | - Maintainer: Sébastien Celles 96 | - Issues/Enhancements: 97 | - Original code forked from http://youarealegend.blogspot.fr/search/label/windrose 98 | - Create a pip instalable package (registered) 99 | - ... 100 | 101 | ### Version 1.5.0 102 | 103 | - Initial development: 2015-06-16 104 | - Co-Authors: 105 | - Sébastien Celles 106 | - Lionel Roubeyrie 107 | - Maintainer: Sébastien Celles 108 | - Issues/Enhancements: 109 | - Github repository creation 110 | - Create a package 111 | - Add unit tests / continuous integration 112 | - Pandas DataFrames / Series as input (keeping Numpy Array compatibility) 113 | - Python 2.7/3.x support 114 | - PDF output 115 | - Animate windrose (video output) 116 | - Add unit tests / continuous integration 117 | - PEP8 118 | - Register projection windrose 119 | - Weitbul plot 120 | 121 | 122 | ### Version 1.4.0 123 | 124 | - Author: Lionel Roubeyrie 125 | - URL: http://youarealegend.blogspot.fr/search/label/windrose 126 | -------------------------------------------------------------------------------- /tests/test_windrose_np_mpl_oo.py: -------------------------------------------------------------------------------- 1 | # generate the baseline: pytest tests/test_windrose_np_mpl_oo.py --mpl-generate-path=tests/output/oo/ 2 | 3 | import matplotlib 4 | import numpy as np 5 | import pandas as pd 6 | import pytest 7 | from matplotlib import cm as cm 8 | 9 | from windrose import WindAxes, WindroseAxes 10 | 11 | matplotlib.use("Agg") # noqa 12 | np.random.seed(0) 13 | # Create wind speed and direction variables 14 | N = 500 15 | ws = np.random.random(N) * 6 16 | wd = np.random.random(N) * 360 17 | bins = np.arange(0, 8, 1) 18 | 19 | df = pd.DataFrame({"speed": ws, "direction": wd}) 20 | 21 | 22 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 23 | def test_windrose_with_scatter_plot(): 24 | ax = WindroseAxes.from_ax() 25 | ax.scatter(wd, ws, alpha=0.2) 26 | ax.set_legend() 27 | return ax.figure 28 | 29 | 30 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo", tolerance=15.5) 31 | def test_windrose_stacked_histogram_normed(): 32 | # windrose like a stacked histogram with normed (displayed in percent) results 33 | ax = WindroseAxes.from_ax() 34 | ax.bar(wd, ws, normed=True, opening=0.8, edgecolor="white") 35 | ax.set_legend() 36 | return ax.figure 37 | 38 | 39 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo", tolerance=6.5) 40 | def test_windrose_stacked_histogram_not_normed_binned(): 41 | # Another stacked histogram representation, not normed, with bins limits 42 | ax = WindroseAxes.from_ax() 43 | ax.box(wd, ws, bins=bins) 44 | ax.set_legend() 45 | return ax.figure 46 | 47 | 48 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo", tolerance=6.5) 49 | def test_windrose_stacked_histogram_not_normed_binned_calm_limit(): 50 | # Another stacked histogram representation, not normed, with bins limits and a calm limit 51 | ax = WindroseAxes.from_ax() 52 | # the bins most not be below the calm_limit 53 | ax.box(wd, ws, bins=[0.2, 1, 2, 3, 4, 5, 6, 7], calm_limit=0.2) 54 | ax.set_legend() 55 | return ax.figure 56 | 57 | 58 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo", tolerance=15.5) 59 | def test_windrose_stacked_histogram_normed_calm_limit(): 60 | # windrose like a stacked histogram with normed (displayed in percent) results and a calm limit 61 | ax = WindroseAxes.from_ax() 62 | ax.bar(wd, ws, normed=True, opening=0.8, edgecolor="white", calm_limit=0.2) 63 | ax.set_legend() 64 | return ax.figure 65 | 66 | 67 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 68 | def test_filled_with_colormap(): 69 | # A windrose in filled representation, with a controlled colormap 70 | ax = WindroseAxes.from_ax() 71 | ax.contourf(wd, ws, bins=bins, cmap=cm.hot) 72 | ax.set_legend() 73 | return ax.figure 74 | 75 | 76 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 77 | def test_filled_with_colormap_calm_limit(): 78 | # A windrose in filled representation, with a controlled colormap and a calm limit 79 | ax = WindroseAxes.from_ax() 80 | # the bins most not be below the calm_limit 81 | ax.contourf(wd, ws, bins=[0.2, 1, 2, 3, 4, 5, 6, 7], cmap=cm.hot, calm_limit=0.2) 82 | ax.set_legend() 83 | return ax.figure 84 | 85 | 86 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 87 | def test_filled_with_colormap_contours(): 88 | # Same as above, but with contours over each filled region... 89 | ax = WindroseAxes.from_ax() 90 | ax.contourf(wd, ws, bins=bins, cmap=cm.hot) 91 | ax.contour(wd, ws, bins=bins, colors="black") 92 | ax.set_legend() 93 | return ax.figure 94 | 95 | 96 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 97 | def test_filled_with_colormap_contours_calm_limit(): 98 | # Same as above, but with contours over each filled region... 99 | ax = WindroseAxes.from_ax() 100 | # the bins most not be below the calm_limit 101 | bins = [0.2, 1, 2, 3, 4, 5, 6, 7] 102 | ax.contourf(wd, ws, bins=bins, cmap=cm.hot, calm_limit=0.2) 103 | ax.contour(wd, ws, bins=bins, colors="black", calm_limit=0.2) 104 | ax.set_legend() 105 | return ax.figure 106 | 107 | 108 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 109 | def test_without_filled_with_colormap_contours(): 110 | ax = WindroseAxes.from_ax() 111 | ax.contour(wd, ws, bins=bins, cmap=cm.hot, lw=3) 112 | ax.set_legend() 113 | return ax.figure 114 | 115 | 116 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 117 | def test_without_filled_with_colormap_contours_calm_limit(): 118 | ax = WindroseAxes.from_ax() 119 | # the bins most not be below the calm_limit 120 | ax.contour( 121 | wd, 122 | ws, 123 | bins=[0.2, 1, 2, 3, 4, 5, 6, 7], 124 | cmap=cm.hot, 125 | lw=3, 126 | calm_limit=0.2, 127 | ) 128 | ax.set_legend() 129 | return ax.figure 130 | 131 | 132 | @pytest.mark.mpl_image_compare(baseline_dir="output/oo") 133 | def test_pdf(): 134 | ax = WindAxes.from_ax() 135 | bins = np.arange(0, 8, 1) 136 | bins = bins[1:] 137 | ax.pdf(ws, bins=bins) 138 | return ax.figure 139 | -------------------------------------------------------------------------------- /samples/amalia_directionally_averaged_speeds.txt: -------------------------------------------------------------------------------- 1 | # direction, average_speed, probability 2 | 0.000000000000000000e+00 7.443989372765817514e+00 1.178129090726009499e-02 3 | 5.000000000000000000e+00 7.037473467697797247e+00 1.099587151344275440e-02 4 | 1.000000000000000000e+01 7.098204875283014026e+00 9.606283355150539369e-03 5 | 1.500000000000000000e+01 6.975827202579733211e+00 1.212365320712919213e-02 6 | 2.000000000000000000e+01 7.099063879774560881e+00 1.047225858423119459e-02 7 | 2.500000000000000000e+01 6.793840398569997774e+00 1.006947940791461105e-02 8 | 3.000000000000000000e+01 6.613868624428271836e+00 9.686839190413855730e-03 9 | 3.500000000000000000e+01 6.887240030040243433e+00 1.000906253146712291e-02 10 | 4.000000000000000000e+01 7.280119386040773577e+00 1.037156379015205000e-02 11 | 4.500000000000000000e+01 7.361887161929981716e+00 1.121740006041687700e-02 12 | 5.000000000000000000e+01 8.711165432347877768e+00 1.522505286476689111e-02 13 | 5.500000000000000000e+01 8.487120544890457197e+00 1.562783204108347465e-02 14 | 6.000000000000000000e+01 8.292666171934786945e+00 1.574866579397845093e-02 15 | 6.500000000000000000e+01 8.201185831179456542e+00 1.705769811700735133e-02 16 | 7.000000000000000000e+01 8.373684353408945569e+00 1.935353942201188324e-02 17 | 7.500000000000000000e+01 7.581321273563128571e+00 1.419796596515960144e-02 18 | 8.000000000000000000e+01 7.220830604679464138e+00 1.206323633068170399e-02 19 | 8.500000000000000000e+01 6.843751211396981837e+00 1.202295841305004581e-02 20 | 9.000000000000000000e+01 6.971049344118904756e+00 1.321115698318396994e-02 21 | 9.500000000000000000e+01 7.002524339758943839e+00 1.746047729332393661e-02 22 | 1.000000000000000000e+02 6.413119393145527702e+00 1.729936562279730042e-02 23 | 1.050000000000000000e+02 5.275072132632170785e+00 1.439935555331789407e-02 24 | 1.100000000000000000e+02 5.768902039588225783e+00 7.874332896989225464e-03 25 | 1.150000000000000000e+02 0.000000000000000000e+00 0.000000000000000000e+00 26 | 1.200000000000000000e+02 0.000000000000000000e+00 2.013895881582922239e-05 27 | 1.250000000000000000e+02 0.000000000000000000e+00 0.000000000000000000e+00 28 | 1.300000000000000000e+02 6.599693305235294183e+00 3.423622998690967569e-04 29 | 1.350000000000000000e+02 6.616581240762715588e+00 3.564595710401772394e-03 30 | 1.400000000000000000e+02 6.647338295302516187e+00 7.189608297251032058e-03 31 | 1.450000000000000000e+02 7.472358052240274162e+00 8.800725002517370554e-03 32 | 1.500000000000000000e+02 8.618059180445825973e+00 1.135837277212768150e-02 33 | 1.550000000000000000e+02 8.759846978365571246e+00 1.415768804752794326e-02 34 | 1.600000000000000000e+02 9.677066728256948025e+00 1.669519685832242598e-02 35 | 1.650000000000000000e+02 9.323521917193836828e+00 1.631255664082166892e-02 36 | 1.700000000000000000e+02 9.216675349128450989e+00 1.317087906555231176e-02 37 | 1.750000000000000000e+02 8.719007778913285378e+00 1.091531567817943804e-02 38 | 1.800000000000000000e+02 8.817304334084926865e+00 9.485449602255563092e-03 39 | 1.850000000000000000e+02 8.887524347760965782e+00 1.010975732554626923e-02 40 | 1.900000000000000000e+02 9.492215428301701508e+00 1.188198570133924131e-02 41 | 1.950000000000000000e+02 9.934646503111826732e+00 1.260698821870909203e-02 42 | 2.000000000000000000e+02 1.084229158881114330e+01 1.588963850568925543e-02 43 | 2.050000000000000000e+02 1.106679327602730112e+01 1.770214479911388569e-02 44 | 2.100000000000000000e+02 1.061125577114793828e+01 2.042090423925083109e-02 45 | 2.150000000000000000e+02 1.092747238717757163e+01 2.279730137951867935e-02 46 | 2.200000000000000000e+02 1.128913549145671169e+01 2.954385258282146709e-02 47 | 2.250000000000000000e+02 1.060262813772340884e+01 3.028899405900714950e-02 48 | 2.300000000000000000e+02 1.045290065497462351e+01 2.698620481321115788e-02 49 | 2.350000000000000000e+02 9.735850631582730230e+00 2.215285469741214500e-02 50 | 2.400000000000000000e+02 9.058025175439807342e+00 2.124660155069982986e-02 51 | 2.450000000000000000e+02 8.783981653725767558e+00 1.828617460477293191e-02 52 | 2.500000000000000000e+02 8.593467905562416576e+00 1.661464102305910615e-02 53 | 2.550000000000000000e+02 8.736410238545547102e+00 1.901117712214278610e-02 54 | 2.600000000000000000e+02 8.251111368880540198e+00 1.905145503977444255e-02 55 | 2.650000000000000000e+02 7.924095405453314811e+00 1.639311247608498529e-02 56 | 2.700000000000000000e+02 7.742623908089139917e+00 1.762158896385056933e-02 57 | 2.750000000000000000e+02 7.888630724347134304e+00 1.653408518779578978e-02 58 | 2.800000000000000000e+02 7.862056793378839892e+00 1.445977242976538048e-02 59 | 2.850000000000000000e+02 7.909226766898133754e+00 1.403685429463296698e-02 60 | 2.900000000000000000e+02 8.624877191489668249e+00 1.657436310542744970e-02 61 | 2.950000000000000000e+02 8.410354617807987765e+00 1.562783204108347465e-02 62 | 3.000000000000000000e+02 8.220820932359574229e+00 1.534588661766186739e-02 63 | 3.050000000000000000e+02 8.321741539102298191e+00 1.752089416977142128e-02 64 | 3.100000000000000000e+02 8.430233322798232010e+00 1.597019434095257179e-02 65 | 3.150000000000000000e+02 8.580956434400002664e+00 1.510421911187191657e-02 66 | 3.200000000000000000e+02 8.848850793045777152e+00 1.452018930621286862e-02 67 | 3.250000000000000000e+02 8.190563105779933295e+00 1.345282448897392076e-02 68 | 3.300000000000000000e+02 8.266538227899193458e+00 1.478199577081864939e-02 69 | 3.350000000000000000e+02 8.581777873969919312e+00 1.339240761252643262e-02 70 | 3.400000000000000000e+02 8.058018314533699211e+00 1.105628838989024254e-02 71 | 3.450000000000000000e+02 7.791763321868974579e+00 1.045211962541536636e-02 72 | 3.500000000000000000e+02 7.414568224050260170e+00 1.162017923673346054e-02 73 | 3.550000000000000000e+02 7.830039408380693011e+00 1.105628838989024254e-02 74 | -------------------------------------------------------------------------------- /paper/paper.bib: -------------------------------------------------------------------------------- 1 | @misc{wiki:xxx, 2 | author = "{Wikipedia contributors}", 3 | title = "Wind rose --- {Wikipedia}{,} The Free Encyclopedia", 4 | year = "2018", 5 | url = "https://en.wikipedia.org/w/index.php?title=Wind_rose&oldid=839157228", 6 | note = {[Online; accessed ]} 7 | } 8 | 9 | @article{doi:10.1109/MCSE.2007.58, 10 | author = {Travis E. Oliphant}, 11 | title = {Python for Scientific Computing}, 12 | journal = {Computing in Science \& Engineering}, 13 | volume = {9}, 14 | number = {3}, 15 | pages = {10-20}, 16 | year = {2007}, 17 | doi = {10.1109/MCSE.2007.58}, 18 | URL = { 19 | https://aip.scitation.org/doi/abs/10.1109/MCSE.2007.58 20 | }, 21 | eprint = { 22 | https://aip.scitation.org/doi/pdf/10.1109/MCSE.2007.58 23 | } 24 | } 25 | 26 | @article{doi:10.1109/MCSE.2011.36, 27 | author = {K. Jarrod Millman and Michael Aivazis}, 28 | title = {Python for Scientists and Engineers}, 29 | journal = {Computing in Science \& Engineering}, 30 | volume = {13}, 31 | number = {2}, 32 | pages = {9-12}, 33 | year = {2011}, 34 | doi = {10.1109/MCSE.2011.36}, 35 | URL = { 36 | https://aip.scitation.org/doi/abs/10.1109/MCSE.2011.36 37 | }, 38 | eprint = { 39 | https://aip.scitation.org/doi/pdf/10.1109/MCSE.2011.36 40 | } 41 | } 42 | 43 | @article{Walt:2011:NAS:1957373.1957466, 44 | author = {Walt, Stefan van der and Colbert, S. Chris and Varoquaux, Gael}, 45 | title = {The NumPy Array: A Structure for Efficient Numerical Computation}, 46 | journal = {Computing in Science and Engg.}, 47 | issue_date = {March 2011}, 48 | volume = {13}, 49 | number = {2}, 50 | month = mar, 51 | year = {2011}, 52 | issn = {1521-9615}, 53 | pages = {22--30}, 54 | numpages = {9}, 55 | url = {https://doi.org/10.1109/MCSE.2011.37}, 56 | doi = {10.1109/MCSE.2011.37}, 57 | acmid = {1957466}, 58 | publisher = {IEEE Educational Activities Department}, 59 | address = {Piscataway, NJ, USA}, 60 | keywords = {NumPy, Python, Python, NumPy, scientific programming, numerical computations, programming libraries, numerical computations, programming libraries, scientific programming}, 61 | } 62 | 63 | @article{doi:10.1109/MCSE.2007.55, 64 | author = {John D. Hunter}, 65 | title = {Matplotlib: A 2D Graphics Environment}, 66 | journal = {Computing in Science \& Engineering}, 67 | volume = {9}, 68 | number = {3}, 69 | pages = {90-95}, 70 | year = {2007}, 71 | doi = {10.1109/MCSE.2007.55}, 72 | URL = { 73 | https://aip.scitation.org/doi/abs/10.1109/MCSE.2007.55 74 | }, 75 | eprint = { 76 | https://aip.scitation.org/doi/pdf/10.1109/MCSE.2007.55 77 | } 78 | } 79 | 80 | @InProceedings{mckinney-proc-scipy-2010, 81 | author = { Wes McKinney }, 82 | title = { Data Structures for Statistical Computing in Python }, 83 | booktitle = { Proceedings of the 9th Python in Science Conference }, 84 | pages = { 51 - 56 }, 85 | year = { 2010 }, 86 | editor = { St\'efan van der Walt and Jarrod Millman } 87 | } 88 | 89 | @article{doi:10.1109/MCSE.2007.53, 90 | author = {Fernando Pérez and Brian E. Granger}, 91 | title = {IPython: A System for Interactive Scientific Computing}, 92 | journal = {Computing in Science \& Engineering}, 93 | volume = {9}, 94 | number = {3}, 95 | pages = {21-29}, 96 | year = {2007}, 97 | doi = {10.1109/MCSE.2007.53}, 98 | URL = { 99 | https://aip.scitation.org/doi/abs/10.1109/MCSE.2007.53 100 | }, 101 | eprint = { 102 | https://aip.scitation.org/doi/pdf/10.1109/MCSE.2007.53 103 | } 104 | } 105 | 106 | @Misc{oliphant2001scipy, 107 | author = {Eric Jones and Travis Oliphant and Pearu Peterson and others}, 108 | title = {{SciPy}: Open source scientific tools for {Python}}, 109 | year = {2001--}, 110 | url = "http://www.scipy.org/", 111 | note = {[Online; accessed ]} 112 | } 113 | 114 | @book{oliphant2006guide, 115 | title={A guide to NumPy}, 116 | author={Oliphant, Travis E}, 117 | volume={1}, 118 | year={2006}, 119 | publisher={Trelgol Publishing USA} 120 | } 121 | 122 | @article{munn1969pollution, 123 | title={Pollution wind-rose analysis}, 124 | doi = {10.1080/00046973.1969.9676573}, 125 | author={Munn, RE}, 126 | journal={Atmosphere}, 127 | volume={7}, 128 | number={3}, 129 | pages={97--105}, 130 | year={1969}, 131 | publisher={Taylor \& Francis} 132 | } 133 | 134 | @Misc{nrcs, 135 | author = {NRCS National Water and Climate Center}, 136 | title = {NRCS National Water and Climate Center - Climate Products - Climate Data - Wind Rose.}, 137 | url = "http://www.wcc.nrcs.usda.gov/climate/windrose.html", 138 | note = {[Online; accessed ]} 139 | } 140 | 141 | @Misc{garver2016, 142 | author = {Daniel Garver and Ryan Brown}, 143 | title = {Air Quality Data Analysis using Open Source Tools - United States Environmental Protection Agency | US EPA}, 144 | url = "http://slideplayer.com/slide/5922455/", 145 | year = {2016--}, 146 | note = {[Online; accessed ]} 147 | } 148 | 149 | @inproceedings{quick2017optimization, 150 | title={Optimization under uncertainty for wake steering strategies}, 151 | author={Quick, Julian and Annoni, Jennifer and King, Ryan and Dykes, Katherine and Fleming, Paul and Ning, Andrew}, 152 | booktitle={Journal of Physics: Conference Series}, 153 | volume={854}, 154 | number={1}, 155 | pages={012036}, 156 | year={2017}, 157 | organization={IOP Publishing} 158 | } 159 | 160 | @article{harris2014parent, 161 | title={The parent wind speed distribution: Why Weibull?}, 162 | author={Harris, R Ian and Cook, Nicholas J}, 163 | doi = {10.1016/j.jweia.2014.05.005}, 164 | journal={Journal of wind engineering and industrial aerodynamics}, 165 | volume={131}, 166 | pages={72--87}, 167 | year={2014}, 168 | publisher={Elsevier} 169 | } 170 | 171 | @article{horel2016summer, 172 | title={Summer ozone concentrations in the vicinity of the Great Salt Lake}, 173 | doi = {10.1002/asl.680}, 174 | author={Horel, John and Crosman, Erik and Jacques, Alexander and Blaylock, Brian and Arens, Seth and Long, Ansley and Sohl, John and Martin, Randal}, 175 | journal={Atmospheric Science Letters}, 176 | volume={17}, 177 | number={9}, 178 | pages={480--486}, 179 | year={2016}, 180 | publisher={Wiley Online Library} 181 | } 182 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # windrose documentation build configuration file, created by 4 | # sphinx-quickstart on Tue May 1 16:51:19 2018. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | import os 20 | import sys 21 | 22 | sys.path.insert(0, os.path.abspath("../windrose")) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.intersphinx", 37 | "sphinx.ext.napoleon", 38 | "nbsphinx", 39 | "sphinx_copybutton", 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ["_templates"] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # 48 | source_suffix = ".rst" 49 | 50 | # The master toctree document. 51 | master_doc = "index" 52 | 53 | # General information about the project. 54 | project = "windrose" 55 | copyright = "2018, Lionel Roubeyrie & Sebastien Celles" 56 | author = "Lionel Roubeyrie & Sebastien Celles" 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = "" 64 | # The full version, including alpha/beta/rc tags. 65 | release = "" 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = "en" 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = "sphinx" 81 | 82 | # If true, `todo` and `todoList` produce output, else they produce nothing. 83 | todo_include_todos = False 84 | 85 | 86 | # -- Options for HTML output ---------------------------------------------- 87 | 88 | # The theme to use for HTML and HTML Help pages. See the documentation for 89 | # a list of builtin themes. 90 | # 91 | # html_theme = 'alabaster' 92 | html_theme = "sphinx_rtd_theme" 93 | 94 | # Theme options are theme-specific and customize the look and feel of a theme 95 | # further. For a list of options available for each theme, see the 96 | # documentation. 97 | # 98 | # html_theme_options = {} 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | # html_static_path = ["_static"] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # This is required for the alabaster theme 109 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 110 | html_sidebars = { 111 | "**": [ 112 | "about.html", 113 | "navigation.html", 114 | "relations.html", # needs 'show_related': True theme option to display 115 | "searchbox.html", 116 | "donate.html", 117 | ], 118 | } 119 | 120 | 121 | # -- Options for HTMLHelp output ------------------------------------------ 122 | 123 | # Output file base name for HTML help builder. 124 | htmlhelp_basename = "windrosedoc" 125 | 126 | 127 | # -- Options for LaTeX output --------------------------------------------- 128 | 129 | latex_elements = { 130 | # The paper size ('letterpaper' or 'a4paper'). 131 | # 132 | # 'papersize': 'letterpaper', 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | # Latex figure (float) alignment 140 | # 141 | # 'figure_align': 'htbp', 142 | } 143 | 144 | # Grouping the document tree into LaTeX files. List of tuples 145 | # (source start file, target name, title, 146 | # author, documentclass [howto, manual, or own class]). 147 | latex_documents = [ 148 | ( 149 | master_doc, 150 | "windrose.tex", 151 | "windrose Documentation", 152 | "Lionel Roubeyrie \\& Sebastien Celles", 153 | "manual", 154 | ), 155 | ] 156 | 157 | 158 | # -- Options for manual page output --------------------------------------- 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | man_pages = [(master_doc, "windrose", "windrose Documentation", [author], 1)] 163 | 164 | 165 | # -- Options for Texinfo output ------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | ( 172 | master_doc, 173 | "windrose", 174 | "windrose Documentation", 175 | author, 176 | "windrose", 177 | "One line description of project.", 178 | "Miscellaneous", 179 | ), 180 | ] 181 | 182 | 183 | interpshinx_mapping = {"matplotlib": ("http://matplotlib.org", None)} 184 | -------------------------------------------------------------------------------- /samples/example_pdf_by.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Example to create a PDF 5 | Monthly windrose axe 6 | One figure per year 7 | """ 8 | 9 | 10 | import datetime 11 | from math import pi 12 | 13 | import click 14 | 15 | # import matplotlib 16 | import matplotlib.pyplot as plt 17 | import numpy as np 18 | import pandas as pd 19 | 20 | # import matplotlib.animation 21 | from matplotlib.backends.backend_pdf import PdfPages 22 | from numpy import cos, sin 23 | 24 | from windrose import ( # noqa 25 | WindAxes, 26 | WindroseAxes, 27 | clean, 28 | plot_windrose, 29 | wrcontour, 30 | wrcontourf, 31 | wrscatter, 32 | ) 33 | 34 | # import time 35 | 36 | 37 | # import matplotlib.cm as cm 38 | 39 | 40 | FIGSIZE_DEFAULT = (16, 12) 41 | S_FIGSIZE_DEFAULT = ",".join(map(str, FIGSIZE_DEFAULT)) 42 | 43 | DPI_DEFAULT = 40 44 | 45 | 46 | def by_func_yearly(dt): 47 | return dt.year 48 | 49 | 50 | def by_func_monthly(dt): 51 | return dt.year, dt.month 52 | 53 | 54 | def by_func_daily(dt): 55 | return dt.year, dt.month, dt.day 56 | 57 | 58 | @click.command() 59 | @click.option( 60 | "--filename", default="samples/sample_wind_poitiers.csv", help="Input filename" 61 | ) 62 | @click.option("--filename_out", default="windrose.pdf", help="Output filename") 63 | @click.option("--dpi", default=DPI_DEFAULT, help="Dot per inch for plot generation") 64 | @click.option( 65 | "--figsize", 66 | default=S_FIGSIZE_DEFAULT, 67 | help="Figure size x,y - default=%s" % S_FIGSIZE_DEFAULT, 68 | ) 69 | @click.option("--bins_min", default=0.01, help="Bins minimum value") 70 | @click.option("--bins_max", default=20, help="Bins maximum value") 71 | @click.option("--bins_step", default=2, help="Bins step value") 72 | @click.option("--fontname", default="Courier New", help="Font name") 73 | @click.option("--show/--no-show", default=False, help="Show figure") 74 | @click.option("--dt_from", default="", help="Datetime from") 75 | @click.option("--dt_to", default="", help="Datetime to") 76 | @click.option("--offset", default=0, help="Axe figure offset") 77 | @click.option("--ncols", default=4, help="Number of columns per figure") 78 | @click.option("--nrows", default=3, help="Number of rows per figure") 79 | def main( 80 | filename, 81 | dt_from, 82 | dt_to, 83 | dpi, 84 | figsize, 85 | bins_min, 86 | bins_max, 87 | bins_step, 88 | ncols, 89 | nrows, 90 | fontname, 91 | show, 92 | filename_out, 93 | offset, 94 | ): 95 | 96 | # convert figsize (string like "8,9" to a list of float [8.0, 9.0] 97 | figsize = figsize.split(",") 98 | figsize = tuple(map(float, figsize)) 99 | width, height = figsize 100 | 101 | # Read CSV file to a Pandas DataFrame 102 | df_all = pd.read_csv(filename) 103 | df_all["Timestamp"] = pd.to_datetime(df_all["Timestamp"]) 104 | df_all = df_all.set_index("Timestamp") 105 | df_all.index = df_all.index.tz_localize("UTC").tz_convert("UTC") 106 | # df_all = df_all.iloc[-10000:,:] 107 | # df_all = df_all['2011-07-01':'2012-12-31'] 108 | if dt_from == "": 109 | dt_from = df_all.index[0] 110 | if dt_to == "": 111 | dt_to = df_all.index[-1] 112 | df_all = df_all[dt_from:dt_to] 113 | 114 | # Get Numpy arrays from DataFrame 115 | direction_all = df_all["direction"].values 116 | var_all = df_all["speed"].values 117 | # index_all = df_all.index.to_datetime() # Fixed: .values -> to_datetime() 118 | by_all = df_all.index.map(by_func_monthly) 119 | by_unique = np.unique(by_all) 120 | print(by_unique) 121 | 122 | # Define bins 123 | # bins = np.arange(bins_min, bins_max, bins_step) 124 | 125 | with PdfPages(filename_out) as pdf: 126 | 127 | for i, by_value in enumerate(by_unique): 128 | print("processing: %s" % str(by_value)) 129 | 130 | if (i + offset) % (ncols * nrows) == 0 or i == 0: 131 | # Create figure and axes 132 | fig, axs = plt.subplots( 133 | nrows=nrows, 134 | ncols=ncols, 135 | figsize=figsize, 136 | dpi=dpi, 137 | facecolor="w", 138 | edgecolor="w", 139 | ) 140 | print(f"{fig!r}\n{fig.axes!r}\n{axs!r}") 141 | 142 | i_sheet, sheet_pos = divmod(i + offset, ncols * nrows) 143 | i_row, i_col = divmod(sheet_pos, ncols) 144 | 145 | # ax = axs[i_row][i_col] 146 | ax = fig.axes[sheet_pos] 147 | 148 | mask = (pd.Series(by_all) == by_value).values 149 | 150 | # index = index_all[mask] 151 | var = var_all[mask] 152 | direction = direction_all[mask] 153 | 154 | # df = pd.DataFrame([var, direction], index=['Speed', 'Direction'], columns=index).transpose() 155 | # df.index.name = 'DateTime' 156 | # print(df) 157 | 158 | Vx = var * sin(pi / 180 * direction) 159 | Vy = var * cos(pi / 180 * direction) 160 | ax.scatter(Vx, Vy, alpha=0.1) 161 | v = 40 162 | ax.set_xlim(-v, v) 163 | ax.set_ylim(-v, v) 164 | 165 | # rect = [0.1, 0.1, 0.8, 0.8] 166 | # ax = WindroseAxes(fig, rect, facecolor='w') 167 | # wrscatter(direction, var, ax=ax) # ToFix!!!! TypeError: Input must be a 2D array. 168 | 169 | # print(direction) 170 | # print(var) 171 | # print(ax) 172 | # wrcontour(direction, var, ax=ax) # ToFix!!!! TypeError: Input must be a 2D array. 173 | 174 | # Same as above, but with contours over each filled region... 175 | # ToFix!!!! TypeError: Input must be a 2D array. 176 | # ax = WindroseAxes.from_ax(ax) 177 | # rect = [0.1, 0.1, 0.8, 0.8] 178 | # #axs[i_row][i_col] = WindroseAxes(fig, rect, facecolor='w') 179 | # #axs[i_row][i_col] = WindroseAxes.from_ax(fig=fig) 180 | # ax = WindroseAxes(fig, rect, facecolor='w') 181 | # fig.axes[i + offset] = ax 182 | # ax.contourf(direction, var, bins=bins, cmap=cm.hot) 183 | # ax.contour(direction, var, bins=bins, colors='black') 184 | 185 | # dt1 = index[0] 186 | # dt2 = index[-1] 187 | # dt1 = df.index[mask][0] 188 | # dt2 = df.index[mask][-1] 189 | # td = dt2 - dt1 190 | 191 | # title = by_value 192 | # title = "From %s\n to %s" % (dt1, dt2) 193 | # title = "%04d-%02d" % (by_value[0], by_value[1]) 194 | dt = datetime.date(by_value[0], by_value[1], 1) 195 | fmt = "%B" # "%Y %B" # Month 196 | title = dt.strftime(fmt) 197 | ax.set_title(title, fontname=fontname) 198 | 199 | # ax.set_legend() 200 | 201 | fig_title = dt.strftime("%Y") # Year 202 | fig.suptitle(fig_title) 203 | 204 | remaining = (i + offset + 1) % (ncols * nrows) 205 | if remaining == 0: 206 | save_figure(fig, pdf, show, fig_title) 207 | 208 | if remaining != 0: 209 | save_figure(fig, pdf, show, fig_title) 210 | 211 | # time.sleep(10) 212 | 213 | print("Save file to '%s'" % filename_out) 214 | 215 | print("remaining: %d" % remaining) 216 | 217 | 218 | def save_figure(fig, pdf, show, fig_title): 219 | filename = "windrose_%s.png" % fig_title 220 | print("save_figure: %s" % filename) 221 | if show: 222 | plt.show() 223 | fig.savefig(filename) # Save to image 224 | pdf.savefig(fig) 225 | 226 | 227 | if __name__ == "__main__": 228 | main() 229 | -------------------------------------------------------------------------------- /samples/example_animate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | This sample need to be improve to provide 5 | a clean API to output animation 6 | 7 | Monthly 8 | python samples/example_animate.py --by M --exit_at 5 --rmax 1000 9 | 10 | Daily 11 | python samples/example_animate.py --by D --exit_at 5 --rmax 60 12 | 13 | """ 14 | 15 | import datetime 16 | import logging 17 | import time 18 | import traceback 19 | 20 | import click 21 | import matplotlib 22 | import matplotlib.animation 23 | import matplotlib.cm as cm 24 | 25 | # matplotlib.use("Agg") 26 | import matplotlib.pyplot as plt 27 | import numpy as np 28 | import pandas as pd 29 | 30 | from windrose import DPI_DEFAULT, FIGSIZE_DEFAULT, WindroseAxes 31 | 32 | logging.Formatter.converter = time.gmtime 33 | logging.basicConfig(format="%(asctime)s %(levelname)s %(message)s", level=logging.DEBUG) 34 | logger = logging.getLogger(__name__) 35 | 36 | pd.set_option("max_rows", 10) 37 | 38 | S_FIGSIZE_DEFAULT = ",".join(map(str, FIGSIZE_DEFAULT)) 39 | 40 | 41 | def get_by_func(by=None, by_func=None): 42 | if by is None and by_func is None: 43 | by = "MS" 44 | 45 | if by in ["year", "yearly", "Y"]: 46 | return lambda dt: dt.year 47 | elif by in ["month", "monthly", "MS"]: # MS: month start 48 | return lambda dt: (dt.year, dt.month) 49 | elif by in ["day", "daily", "D"]: 50 | return lambda dt: (dt.year, dt.month, dt.day) 51 | elif by is None and by_func is not None: 52 | return by_func 53 | else: 54 | raise NotImplementedError("'%s' is not an allowed 'by' parameter" % by) 55 | 56 | 57 | def generate(df_all, func, copy=True): 58 | if copy: 59 | df_all = df_all.copy() 60 | df_all["by"] = df_all.index.map(func) 61 | df = df_all.reset_index().set_index(["by", df_all.index.name]) 62 | for by_val in df.index.levels[0]: 63 | yield df.loc[by_val] 64 | 65 | 66 | def count(df_all, func): 67 | return len(np.unique(df_all.index.map(func))) 68 | 69 | 70 | @click.command() 71 | @click.option( 72 | "--filename", default="samples/sample_wind_poitiers.csv", help="Input filename" 73 | ) 74 | @click.option("--exit_at", default=0, help="premature exit (int) - must be > 1") 75 | @click.option("--by", default="month", help="Animate by (year, month, day...)") 76 | @click.option("--rmax", default=1000, help="rmax") 77 | @click.option( 78 | "--filename_out", default="windrose_animation.mp4", help="Output filename" 79 | ) 80 | @click.option("--dpi", default=DPI_DEFAULT, help="Dot per inch for plot generation") 81 | @click.option( 82 | "--figsize", 83 | default=S_FIGSIZE_DEFAULT, 84 | help="Figure size x,y - default=%s" % S_FIGSIZE_DEFAULT, 85 | ) 86 | @click.option( 87 | "--fps", default=7, help="Number of frame per seconds for video generation" 88 | ) 89 | @click.option("--bins_min", default=0.01, help="Bins minimum value") 90 | @click.option("--bins_max", default=20, help="Bins maximum value") 91 | @click.option("--bins_step", default=2, help="Bins step value") 92 | @click.option("--fontname", default="Courier New", help="Font name") 93 | def main( 94 | filename, 95 | exit_at, 96 | by, 97 | rmax, 98 | dpi, 99 | figsize, 100 | fps, 101 | bins_min, 102 | bins_max, 103 | bins_step, 104 | fontname, 105 | filename_out, 106 | ): 107 | # convert figsize (string like "8,9" to a list of float [8.0, 9.0] 108 | figsize = figsize.split(",") 109 | figsize = map(float, figsize) 110 | 111 | by_func = get_by_func(by) 112 | 113 | # Read CSV file to a Pandas DataFrame 114 | df_all = pd.read_csv(filename) 115 | df_all["Timestamp"] = pd.to_datetime(df_all["Timestamp"]) 116 | df_all = df_all.set_index("Timestamp") 117 | 118 | df_all.index = df_all.index.tz_localize("UTC").tz_convert("UTC") 119 | 120 | dt_start = df_all.index[0] 121 | dt_end = df_all.index[-1] 122 | 123 | td = dt_end - dt_start 124 | Nslides = count(df_all, by_func) 125 | msg = """Starting 126 | First dt: %s 127 | Last dt: %s 128 | td: %s 129 | Slides: %d""" % ( 130 | dt_start, 131 | dt_end, 132 | td, 133 | Nslides, 134 | ) 135 | logger.info(msg) 136 | 137 | # Define bins 138 | bins = np.arange(bins_min, bins_max, bins_step) 139 | 140 | # Create figure 141 | fig = plt.figure(figsize=figsize, dpi=dpi, facecolor="w", edgecolor="w") 142 | 143 | # Create a video writer (ffmpeg can create MPEG files) 144 | FFMpegWriter = matplotlib.animation.writers["ffmpeg"] 145 | metadata = { 146 | "title": "windrose", 147 | "artist": "windrose", 148 | "comment": """Made with windrose 149 | http://www.github.com/scls19fr/windrose""", 150 | } 151 | writer = FFMpegWriter(fps=fps, metadata=metadata) 152 | 153 | dt_start_process = datetime.datetime.now() 154 | 155 | with writer.saving(fig, filename_out, 100): 156 | try: 157 | for i, df in enumerate(generate(df_all, by_func)): 158 | dt1 = df.index[0] 159 | dt2 = df.index[-1] 160 | td = dt2 - dt1 161 | msg = """ Slide {}/{} 162 | From {} 163 | to {} 164 | td {}""".format( 165 | i + 1, 166 | Nslides, 167 | dt1, 168 | dt2, 169 | td, 170 | ) 171 | logger.info(msg) 172 | remaining = Nslides - (i + 1) 173 | now = datetime.datetime.now() 174 | td_remaining = (now - dt_start_process) / (i + 1) * remaining 175 | logger.info( 176 | """ Expected 177 | time: {} 178 | end at: {} 179 | """.format(td_remaining, now + td_remaining) 180 | ) 181 | 182 | title = f" From {dt1}\n to {dt2}" 183 | 184 | try: 185 | ax = WindroseAxes.from_ax( 186 | fig=fig, rmax=rmax 187 | ) # scatter, bar, box, contour, contourf 188 | 189 | direction = df["direction"].values 190 | var = df["speed"].values 191 | 192 | # ax.scatter(direction, var, alpha=0.2) 193 | # ax.set_xlim([-bins[-1], bins[-1]]) 194 | # ax.set_ylim([-bins[-1], bins[-1]]) 195 | 196 | # ax.bar(direction, var, bins=bins, normed=True, opening=0.8, edgecolor='white') 197 | 198 | # ax.box(direction, var, bins=bins) 199 | 200 | # ax.contour(direction, var, cmap=cm.hot, lw=3, bins=bins) 201 | 202 | ax.contourf(direction, var, bins=bins, cmap=cm.hot) 203 | ax.contour(direction, var, bins=bins, colors="black", lw=3) 204 | 205 | ax.set_legend() 206 | 207 | # ax = WindAxes.from_ax(fig=fig) # pdf: probability density function 208 | # ax.pdf(var, bins=bins) 209 | # ax.set_xlim([0, bins[-1]]) 210 | # ax.set_ylim([0, 0.4]) 211 | 212 | ax.set_title(title, fontname=fontname) 213 | 214 | writer.grab_frame() 215 | except KeyboardInterrupt: 216 | break 217 | except Exception: 218 | logger.error(traceback.format_exc()) 219 | 220 | fig.clf() 221 | if i > exit_at - 1 and exit_at != 0: # exit_at must be > 1 222 | break 223 | except KeyboardInterrupt: 224 | return 225 | except Exception: 226 | logger.error(traceback.format_exc()) 227 | 228 | N = i + 1 229 | logger.info("Number of slides: %d" % N) 230 | 231 | # plt.show() 232 | 233 | logger.info("Save file to '%s'" % filename_out) 234 | 235 | 236 | if __name__ == "__main__": 237 | main() 238 | -------------------------------------------------------------------------------- /samples/example_by.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | sample using "by" keyword 5 | """ 6 | 7 | import click 8 | import matplotlib.cm as cm 9 | 10 | # import matplotlib 11 | # matplotlib.use("Agg") 12 | # import matplotlib as mpl 13 | import matplotlib.pyplot as plt 14 | import numpy as np 15 | import pandas as pd 16 | 17 | from windrose import DPI_DEFAULT, FIGSIZE_DEFAULT, WindroseAxes 18 | 19 | 20 | class AxCollection: 21 | def __init__(self, fig=None, *args, **kwargs): 22 | if fig is None: 23 | self.fig = plt.figure( 24 | figsize=FIGSIZE_DEFAULT, dpi=DPI_DEFAULT, facecolor="w", edgecolor="w" 25 | ) 26 | else: 27 | self.fig = fig 28 | 29 | def animate(self): 30 | pass 31 | 32 | def show(self): 33 | pass 34 | 35 | 36 | class Layout: 37 | """ 38 | Inspired from PdfPages 39 | https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backends/backend_pdf.py - PdfPages 40 | http://matplotlib.org/api/backend_pdf_api.html 41 | http://matplotlib.org/examples/pylab_examples/multipage_pdf.html 42 | 43 | Inspired also from FFMpegWriter 44 | http://matplotlib.org/examples/animation/moviewriter.html 45 | https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/animation.py 46 | MovieWriter 47 | """ 48 | 49 | def __init__(self, ncols=4, nrows=6, nsheets=1): 50 | self.ncols = ncols 51 | self.nrows = nrows 52 | self.nsheets = nsheets 53 | 54 | self._resize() 55 | self._i = 0 56 | 57 | @property 58 | def fig(self): 59 | return self._array_fig 60 | 61 | def _resize(self): 62 | # self._array_ax = np.empty((self.nsheets, self.nrows, self.ncols), dtype=object) 63 | self._array_ax = np.empty(self.nsheets, dtype=object) 64 | # self._array_ax.fill(None) 65 | 66 | self._array_fig = np.empty(self.nsheets, dtype=object) 67 | # self._array_fig.fill(None) 68 | 69 | for i in range(self.nsheets): 70 | fig, axs = plt.subplots(nrows=self.nrows, ncols=self.ncols) 71 | # print(fig, axs) 72 | self._array_fig[i] = fig 73 | self._array_ax[i] = axs 74 | 75 | def __repr__(self): 76 | s = """""".format( 81 | self.ncols, 82 | self.nrows, 83 | self.nsheets, 84 | ) 85 | return s 86 | 87 | def __enter__(self, *args, **kwargs): 88 | print(f"enter {args} {kwargs}") 89 | return self 90 | 91 | def __exit__(self, typ, value, traceback): 92 | # print("exit %s %s" % (args, kwargs)) 93 | print(f"exit {typ} {value} {traceback}") 94 | # print("exit") 95 | self.close() 96 | 97 | def close(self): 98 | print("close") 99 | 100 | def saveax(self): 101 | print("saveax") 102 | self._i += 1 103 | 104 | 105 | class NormalLayout(Layout): 106 | def __init__(self): 107 | super().__init__() 108 | 109 | 110 | S_FIGSIZE_DEFAULT = ",".join(map(str, FIGSIZE_DEFAULT)) 111 | 112 | 113 | def by_func_yearly(dt): 114 | return dt.year 115 | 116 | 117 | def by_func_monthly(dt): 118 | return dt.year, dt.month 119 | 120 | 121 | def by_func_daily(dt): 122 | return dt.year, dt.month, dt.day 123 | 124 | 125 | @click.command() 126 | @click.option( 127 | "--filename", default="samples/sample_wind_poitiers.csv", help="Input filename" 128 | ) 129 | @click.option( 130 | "--filename_out", default="windrose_animation.mp4", help="Output filename" 131 | ) 132 | @click.option("--dpi", default=DPI_DEFAULT, help="Dot per inch for plot generation") 133 | @click.option( 134 | "--figsize", 135 | default=S_FIGSIZE_DEFAULT, 136 | help="Figure size x,y - default=%s" % S_FIGSIZE_DEFAULT, 137 | ) 138 | @click.option( 139 | "--fps", default=7, help="Number of frame per seconds for video generation" 140 | ) 141 | @click.option("--bins_min", default=0.01, help="Bins minimum value") 142 | @click.option("--bins_max", default=20, help="Bins maximum value") 143 | @click.option("--bins_step", default=2, help="Bins step value") 144 | def main(filename, dpi, figsize, fps, bins_min, bins_max, bins_step, filename_out): 145 | # convert figsize (string like "8,9" to a list of float [8.0, 9.0] 146 | figsize = figsize.split(",") 147 | figsize = map(float, figsize) 148 | 149 | # Read CSV file to a Pandas DataFrame 150 | df_all = pd.read_csv(filename) 151 | df_all["Timestamp"] = pd.to_datetime(df_all["Timestamp"]) 152 | df_all = df_all.set_index("Timestamp") 153 | df_all.index = df_all.index.tz_localize("UTC").tz_convert("UTC") 154 | # df_all = df_all.iloc[-10000:,:] 155 | df_all = df_all.ix["2011-07-01":"2011-12-31"] 156 | 157 | # Get Numpy arrays from DataFrame 158 | direction_all = df_all["direction"].values 159 | var_all = df_all["speed"].values 160 | index_all = df_all.index.to_datetime() # Fixed: .values -> to_datetime() 161 | by_all = df_all.index.map(by_func_monthly) 162 | by_unique = np.unique(by_all) 163 | print(by_unique) 164 | 165 | (ncols, nrows, nsheets) = (4, 3, 2) # noqa 166 | # layout = Layout(4, 3, 2) # ncols, nrows, nsheets 167 | # layout = Layout(ncols, nrows, nsheets) 168 | 169 | # layout = Layout(4, 6, 1) 170 | # layout.save(ax) 171 | # layout.to_pdf("filename.pdf") 172 | # layout.to_video("filename.mp4") 173 | 174 | # fig, ax = plt.subplots(nrows=2, ncols=3) 175 | 176 | # with Layout(4, 6, 1) as layout: 177 | # print(layout) 178 | # #layout.save(ax) 179 | 180 | def tuple_position(i, ncols, nrows): 181 | i_sheet, sheet_pos = divmod(i, ncols * nrows) 182 | i_row, i_col = divmod(sheet_pos, ncols) 183 | return i_sheet, i_row, i_col 184 | 185 | def position_from_tuple(t, ncols, nrows): 186 | i_sheet, i_row, i_col = t 187 | return i_sheet * ncols * nrows + i_row * ncols + i_col 188 | 189 | assert tuple_position(0, ncols, nrows) == (0, 0, 0) 190 | assert tuple_position(1, ncols, nrows) == (0, 0, 1) 191 | assert tuple_position(2, ncols, nrows) == (0, 0, 2) 192 | assert tuple_position(3, ncols, nrows) == (0, 0, 3) 193 | assert tuple_position(4, ncols, nrows) == (0, 1, 0) 194 | assert tuple_position(5, ncols, nrows) == (0, 1, 1) 195 | assert tuple_position(6, ncols, nrows) == (0, 1, 2) 196 | assert tuple_position(7, ncols, nrows) == (0, 1, 3) 197 | assert tuple_position(8, ncols, nrows) == (0, 2, 0) 198 | assert tuple_position(9, ncols, nrows) == (0, 2, 1) 199 | assert tuple_position(10, ncols, nrows) == (0, 2, 2) 200 | assert tuple_position(11, ncols, nrows) == (0, 2, 3) 201 | assert tuple_position(12, ncols, nrows) == (1, 0, 0) 202 | assert tuple_position(13, ncols, nrows) == (1, 0, 1) 203 | assert tuple_position(14, ncols, nrows) == (1, 0, 2) 204 | assert tuple_position(15, ncols, nrows) == (1, 0, 3) 205 | assert tuple_position(16, ncols, nrows) == (1, 1, 0) 206 | assert tuple_position(17, ncols, nrows) == (1, 1, 1) 207 | 208 | assert position_from_tuple((0, 0, 0), ncols, nrows) == 0 209 | assert position_from_tuple((1, 0, 0), ncols, nrows) == ncols * nrows 210 | assert position_from_tuple((2, 0, 0), ncols, nrows) == 2 * ncols * nrows 211 | assert position_from_tuple((1, 0, 1), ncols, nrows) == ncols * nrows + 1 212 | assert position_from_tuple((1, 1, 1), ncols, nrows) == ncols * nrows + ncols + 1 213 | assert position_from_tuple((1, 2, 3), ncols, nrows) == ncols * nrows + 2 * ncols + 3 214 | 215 | for i in range(20): 216 | t = tuple_position(i, ncols, nrows) 217 | assert position_from_tuple(t, ncols, nrows) == i 218 | 219 | # layout = NormalLayout() 220 | 221 | # with layout.append() as ax: 222 | # pass 223 | # layout.show() 224 | 225 | # Define bins 226 | bins = np.arange(bins_min, bins_max, bins_step) 227 | 228 | for by_value in by_unique: 229 | # by_value = (2011, 5) 230 | 231 | # mask = (by == by_value).all(axis=1) 232 | # ToFix see 233 | # http://stackoverflow.com/questions/32005403/boolean-indexing-with-numpy-array-and-tuples 234 | 235 | mask = (pd.Series(by_all) == by_value).values 236 | 237 | # print(mask) 238 | 239 | index = index_all[mask] 240 | var = var_all[mask] 241 | direction = direction_all[mask] 242 | 243 | # Create figure 244 | # fig = plt.figure(figsize=figsize, dpi=dpi, facecolor='w', edgecolor='w') 245 | 246 | # Same as above, but with contours over each filled region... 247 | ax = WindroseAxes.from_ax() 248 | ax.contourf(direction, var, bins=bins, cmap=cm.hot) 249 | ax.contour(direction, var, bins=bins, colors="black") 250 | fontname = "Courier" 251 | # title = by_value 252 | dt1 = index[0] 253 | dt2 = index[-1] 254 | # dt1 = df.index[mask][0] 255 | # dt2 = df.index[mask][-1] 256 | # td = dt2 - dt1 257 | title = f"From {dt1}\n to {dt2}" 258 | 259 | ax.set_title(title, fontname=fontname) 260 | ax.set_legend() 261 | 262 | plt.show() 263 | 264 | # time.sleep(10) 265 | 266 | # print("Save file to '%s'" % filename_out) 267 | 268 | 269 | if __name__ == "__main__": 270 | main() 271 | -------------------------------------------------------------------------------- /notebooks/usage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Examples\n", 8 | "\n", 9 | "This example use randoms values for wind speed and direction(ws and wdnotebooks/windrose_sample_poitiers_csv.ipynb\n", 10 | "variables). In situation, these variables are loaded with reals values\n", 11 | "(1-D array), from a database or directly from a text file.\n", 12 | "See [this notebook](https://github.com/python-windrose/windrose/blob/master/notebooks/windrose_sample_poitiers_csv.ipynb) for an example of real data." 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "import numpy as np\n", 22 | "\n", 23 | "N = 500\n", 24 | "ws = np.random.random(N) * 6\n", 25 | "wd = np.random.random(N) * 360" 26 | ] 27 | }, 28 | { 29 | "cell_type": "markdown", 30 | "metadata": {}, 31 | "source": [ 32 | "## A stacked histogram with normed (displayed in percent) results." 33 | ] 34 | }, 35 | { 36 | "cell_type": "code", 37 | "execution_count": null, 38 | "metadata": {}, 39 | "outputs": [], 40 | "source": [ 41 | "from windrose import WindroseAxes\n", 42 | "\n", 43 | "ax = WindroseAxes.from_ax()\n", 44 | "ax.bar(wd, ws, normed=True, opening=0.8, edgecolor=\"white\")\n", 45 | "ax.set_legend()" 46 | ] 47 | }, 48 | { 49 | "cell_type": "markdown", 50 | "metadata": {}, 51 | "source": [ 52 | "## Another stacked histogram representation, not normed, with bins limits." 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": null, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "ax = WindroseAxes.from_ax()\n", 62 | "ax.box(wd, ws, bins=np.arange(0, 8, 1))\n", 63 | "ax.set_legend()" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "metadata": {}, 69 | "source": [ 70 | "## A windrose in filled representation, with a controlled colormap." 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "from matplotlib import cm\n", 80 | "\n", 81 | "ax = WindroseAxes.from_ax()\n", 82 | "ax.contourf(wd, ws, bins=np.arange(0, 8, 1), cmap=cm.hot)\n", 83 | "ax.set_legend()" 84 | ] 85 | }, 86 | { 87 | "cell_type": "markdown", 88 | "metadata": {}, 89 | "source": [ 90 | "## Same as above, but with contours over each filled region..." 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "ax = WindroseAxes.from_ax()\n", 100 | "ax.contourf(wd, ws, bins=np.arange(0, 8, 1), cmap=cm.hot)\n", 101 | "ax.contour(wd, ws, bins=np.arange(0, 8, 1), colors=\"black\")\n", 102 | "ax.set_legend()" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "## ...or without filled regions." 110 | ] 111 | }, 112 | { 113 | "cell_type": "code", 114 | "execution_count": null, 115 | "metadata": {}, 116 | "outputs": [], 117 | "source": [ 118 | "ax = WindroseAxes.from_ax()\n", 119 | "ax.contour(wd, ws, bins=np.arange(0, 8, 1), cmap=cm.hot, lw=3)\n", 120 | "ax.set_legend()" 121 | ] 122 | }, 123 | { 124 | "cell_type": "markdown", 125 | "metadata": {}, 126 | "source": [ 127 | "After that, you can have a look at the computed values used to plot the\n", 128 | "windrose with the `ax._info` dictionary :\n", 129 | "\n", 130 | "- `ax._info['bins']` :\n", 131 | " list of bins (limits) used for wind speeds. If not set in the call, bins\n", 132 | " will be set to 6 parts between wind speed min and max.\n", 133 | "- `ax._info['dir']` : list of directions \"boundaries\" used to compute the\n", 134 | " distribution by wind direction sector. This can be set by the nsector\n", 135 | " parameter (see below).\n", 136 | "- `ax._info['table']` : the resulting table of\n", 137 | " the computation. It's a 2D histogram, where each line represents a wind\n", 138 | " speed class, and each column represents a wind direction class.\n", 139 | "\n", 140 | "So, to know the frequency of each wind direction, for all wind speeds, do:" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": null, 146 | "metadata": {}, 147 | "outputs": [], 148 | "source": [ 149 | "ax.bar(wd, ws, normed=True, nsector=16)\n", 150 | "table = ax._info[\"table\"]\n", 151 | "wd_freq = np.sum(table, axis=0)" 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "and to have a graphical representation of this result:" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "import matplotlib.pyplot as plt\n", 168 | "\n", 169 | "direction = ax._info[\"dir\"]\n", 170 | "wd_freq = np.sum(table, axis=0)\n", 171 | "\n", 172 | "plt.bar(np.arange(16), wd_freq, align=\"center\")\n", 173 | "xlabels = (\n", 174 | " \"N\",\n", 175 | " \"\",\n", 176 | " \"N-E\",\n", 177 | " \"\",\n", 178 | " \"E\",\n", 179 | " \"\",\n", 180 | " \"S-E\",\n", 181 | " \"\",\n", 182 | " \"S\",\n", 183 | " \"\",\n", 184 | " \"S-O\",\n", 185 | " \"\",\n", 186 | " \"O\",\n", 187 | " \"\",\n", 188 | " \"N-O\",\n", 189 | " \"\",\n", 190 | ")\n", 191 | "xticks = np.arange(16)\n", 192 | "plt.gca().set_xticks(xticks)\n", 193 | "plt.gca().set_xticklabels(xlabels)" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "metadata": {}, 199 | "source": [ 200 | "In addition of all the standard pyplot parameters, you can pass special\n", 201 | "parameters to control the windrose production. For the stacked histogram\n", 202 | "windrose, calling help(ax.bar) will give :\n", 203 | "`bar(self, direction, var, **kwargs)` method of\n", 204 | "`windrose.WindroseAxes` instance Plot a windrose in bar mode. For each\n", 205 | "var bins and for each sector, a colored bar will be draw on the axes.\n", 206 | "\n", 207 | "Mandatory:\n", 208 | "\n", 209 | "- `direction` : 1D array - directions the wind blows from, North centred\n", 210 | "- `var` : 1D array - values of the variable to compute. Typically the wind speeds\n", 211 | "\n", 212 | "Optional:\n", 213 | "\n", 214 | "- `nsector` : integer - number of sectors used to compute\n", 215 | " the windrose table. If not set, nsectors=16, then each sector will be\n", 216 | " 360/16=22.5°, and the resulting computed table will be aligned with the\n", 217 | " cardinals points.\n", 218 | "- `bins` : 1D array or integer - number of bins, or a\n", 219 | " sequence of bins variable. If not set, bins=6 between min(var) and\n", 220 | " max(var).\n", 221 | "- `blowto` : bool. If True, the windrose will be pi rotated,\n", 222 | " to show where the wind blow to (useful for pollutant rose).\n", 223 | "- `colors` : string or tuple - one string color (`'k'` or\n", 224 | " `'black'`), in this case all bins will be plotted in this color; a\n", 225 | " tuple of matplotlib color args (string, float, rgb, etc), different\n", 226 | " levels will be plotted in different colors in the order specified.\n", 227 | "- `cmap` : a cm Colormap instance from `matplotlib.cm`. - if\n", 228 | " `cmap == None` and `colors == None`, a default Colormap is used.\n", 229 | "- `edgecolor` : string - The string color each edge bar will be plotted.\n", 230 | " Default : no edgecolor\n", 231 | "- `opening` : float - between 0.0 and 1.0, to\n", 232 | " control the space between each sector (1.0 for no space)\n", 233 | "- `mean_values` : Bool - specify wind speed statistics with\n", 234 | " direction=specific mean wind speeds. If this flag is specified, var is\n", 235 | " expected to be an array of mean wind speeds corresponding to each entry\n", 236 | " in `direction`. These are used to generate a distribution of wind\n", 237 | " speeds assuming the distribution is Weibull with shape factor = 2.\n", 238 | "- `weibull_factors` : Bool - specify wind speed statistics with\n", 239 | " direction=specific weibull scale and shape factors. If this flag is\n", 240 | " specified, var is expected to be of the form \\[\\[7,2\\], ...., \\[7.5,1.9\\]\\]\n", 241 | " where var\\[i\\]\\[0\\] is the weibull scale factor and var\\[i\\]\\[1\\] is the shape\n", 242 | " factor" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "## Probability density function (pdf) and fitting Weibull distribution" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "from windrose import WindAxes\n", 259 | "\n", 260 | "ax = WindAxes.from_ax()\n", 261 | "bins = np.arange(0, 6 + 1, 0.5)\n", 262 | "bins = bins[1:]\n", 263 | "ax, params = ax.pdf(ws, bins=bins)" 264 | ] 265 | }, 266 | { 267 | "cell_type": "markdown", 268 | "metadata": {}, 269 | "source": [ 270 | "Optimal parameters of Weibull distribution can be displayed using" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "print(f\"{params=}\")" 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "metadata": {}, 285 | "source": [ 286 | "## Overlay of a map" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": {}, 292 | "source": [ 293 | "This example illustrate how to set an windrose axe on top of any other axes. Specifically,\n", 294 | "overlaying a map is often useful.\n", 295 | "It rely on matplotlib toolbox inset_axes utilities." 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": null, 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "import cartopy.crs as ccrs\n", 305 | "import cartopy.io.img_tiles as cimgt\n", 306 | "import matplotlib.pyplot as plt\n", 307 | "import numpy as np\n", 308 | "from mpl_toolkits.axes_grid1.inset_locator import inset_axes\n", 309 | "\n", 310 | "import windrose\n", 311 | "\n", 312 | "ws = np.random.random(500) * 6\n", 313 | "wd = np.random.random(500) * 360\n", 314 | "\n", 315 | "minlon, maxlon, minlat, maxlat = (6.5, 7.0, 45.85, 46.05)\n", 316 | "\n", 317 | "proj = ccrs.PlateCarree()\n", 318 | "fig = plt.figure(figsize=(12, 6))\n", 319 | "# Draw main ax on top of which we will add windroses\n", 320 | "main_ax = fig.add_subplot(1, 1, 1, projection=proj)\n", 321 | "main_ax.set_extent([minlon, maxlon, minlat, maxlat], crs=proj)\n", 322 | "main_ax.gridlines(draw_labels=True)\n", 323 | "main_ax.coastlines()\n", 324 | "\n", 325 | "request = cimgt.OSM()\n", 326 | "main_ax.add_image(request, 12)\n", 327 | "\n", 328 | "# Coordinates of the station we were measuring windspeed\n", 329 | "cham_lon, cham_lat = (6.8599, 45.9259)\n", 330 | "passy_lon, passy_lat = (6.7, 45.9159)\n", 331 | "\n", 332 | "# Inset axe it with a fixed size\n", 333 | "wrax_cham = inset_axes(\n", 334 | " main_ax,\n", 335 | " width=1, # size in inches\n", 336 | " height=1, # size in inches\n", 337 | " loc=\"center\", # center bbox at given position\n", 338 | " bbox_to_anchor=(cham_lon, cham_lat), # position of the axe\n", 339 | " bbox_transform=main_ax.transData, # use data coordinate (not axe coordinate)\n", 340 | " axes_class=windrose.WindroseAxes, # specify the class of the axe\n", 341 | ")\n", 342 | "\n", 343 | "# Inset axe with size relative to main axe\n", 344 | "height_deg = 0.1\n", 345 | "wrax_passy = inset_axes(\n", 346 | " main_ax,\n", 347 | " width=\"100%\", # size in % of bbox\n", 348 | " height=\"100%\", # size in % of bbox\n", 349 | " # loc=\"center\", # don\"t know why, but this doesn\"t work.\n", 350 | " # specify the center lon and lat of the plot, and size in degree\n", 351 | " bbox_to_anchor=(\n", 352 | " passy_lon - height_deg / 2,\n", 353 | " passy_lat - height_deg / 2,\n", 354 | " height_deg,\n", 355 | " height_deg,\n", 356 | " ),\n", 357 | " bbox_transform=main_ax.transData,\n", 358 | " axes_class=windrose.WindroseAxes,\n", 359 | ")\n", 360 | "\n", 361 | "wrax_cham.bar(wd, ws)\n", 362 | "wrax_passy.bar(wd, ws)\n", 363 | "for ax in [wrax_cham, wrax_passy]:\n", 364 | " ax.tick_params(labelleft=False, labelbottom=False)" 365 | ] 366 | }, 367 | { 368 | "cell_type": "markdown", 369 | "metadata": {}, 370 | "source": [ 371 | "## Subplots\n", 372 | "\n", 373 | "[seaborn](https://seaborn.pydata.org/index.html) offers an easy way to create subplots per parameter. For example per month or day. You can adapt this to have years as columns and rows as months or vice versa." 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [ 382 | "import numpy as np\n", 383 | "import pandas as pd\n", 384 | "import seaborn as sns\n", 385 | "from matplotlib import pyplot as plt\n", 386 | "\n", 387 | "from windrose import WindroseAxes, plot_windrose\n", 388 | "\n", 389 | "wind_data = pd.DataFrame(\n", 390 | " {\n", 391 | " \"ws\": np.random.random(1200) * 6,\n", 392 | " \"wd\": np.random.random(1200) * 360,\n", 393 | " \"month\": np.repeat(range(1, 13), 100),\n", 394 | " }\n", 395 | ")\n", 396 | "\n", 397 | "\n", 398 | "def plot_windrose_subplots(data, *, direction, var, color=None, **kwargs):\n", 399 | " \"\"\"wrapper function to create subplots per axis\"\"\"\n", 400 | " ax = plt.gca()\n", 401 | " ax = WindroseAxes.from_ax(ax=ax)\n", 402 | " plot_windrose(direction_or_df=data[direction], var=data[var], ax=ax, **kwargs)\n", 403 | "\n", 404 | "\n", 405 | "# this creates the raw subplot structure with a subplot per value in month.\n", 406 | "g = sns.FacetGrid(\n", 407 | " data=wind_data,\n", 408 | " # the column name for each level a subplot should be created\n", 409 | " col=\"month\",\n", 410 | " # place a maximum of 3 plots per row\n", 411 | " col_wrap=3,\n", 412 | " subplot_kws={\"projection\": \"windrose\"},\n", 413 | " sharex=False,\n", 414 | " sharey=False,\n", 415 | " despine=False,\n", 416 | " height=3.5,\n", 417 | ")\n", 418 | "\n", 419 | "g.map_dataframe(\n", 420 | " plot_windrose_subplots,\n", 421 | " direction=\"wd\",\n", 422 | " var=\"ws\",\n", 423 | " normed=True,\n", 424 | " # manually set bins, so they match for each subplot\n", 425 | " bins=(0.1, 1, 2, 3, 4, 5),\n", 426 | " calm_limit=0.1,\n", 427 | " kind=\"bar\",\n", 428 | ")\n", 429 | "\n", 430 | "# make the subplots easier to compare, by having the same y-axis range\n", 431 | "y_ticks = range(0, 17, 4)\n", 432 | "for ax in g.axes:\n", 433 | " ax.set_legend(\n", 434 | " title=r\"$m \\cdot s^{-1}$\", bbox_to_anchor=(1.15, -0.1), loc=\"lower right\"\n", 435 | " )\n", 436 | " ax.set_rgrids(y_ticks, y_ticks)\n", 437 | "\n", 438 | "# adjust the spacing between the subplots to have sufficient space between plots\n", 439 | "plt.subplots_adjust(wspace=-0.2)" 440 | ] 441 | }, 442 | { 443 | "cell_type": "markdown", 444 | "metadata": {}, 445 | "source": [ 446 | "## Functional API" 447 | ] 448 | }, 449 | { 450 | "cell_type": "markdown", 451 | "metadata": {}, 452 | "source": [ 453 | "Instead of using object oriented approach like previously shown, some\n", 454 | "\"shortcut\" functions have been defined: `wrbox`, `wrbar`,\n", 455 | "`wrcontour`, `wrcontourf`, `wrpdf`. See [unit tests](https://github.com/python-windrose/windrose/blob/master/tests/test_windrose.py)." 456 | ] 457 | }, 458 | { 459 | "cell_type": "markdown", 460 | "metadata": {}, 461 | "source": [ 462 | "## Pandas support" 463 | ] 464 | }, 465 | { 466 | "cell_type": "markdown", 467 | "metadata": {}, 468 | "source": [ 469 | "windrose not only supports Numpy arrays. It also supports also Pandas\n", 470 | "DataFrame. `plot_windrose` function provides most of plotting features\n", 471 | "previously shown." 472 | ] 473 | }, 474 | { 475 | "cell_type": "code", 476 | "execution_count": null, 477 | "metadata": {}, 478 | "outputs": [], 479 | "source": [ 480 | "import pandas as pd\n", 481 | "\n", 482 | "N = 500\n", 483 | "ws = np.random.random(N) * 6\n", 484 | "wd = np.random.random(N) * 360\n", 485 | "df = pd.DataFrame({\"speed\": ws, \"direction\": wd})\n", 486 | "plot_windrose(df, kind=\"contour\", bins=np.arange(0, 8, 1), cmap=cm.hot, lw=3)" 487 | ] 488 | }, 489 | { 490 | "cell_type": "markdown", 491 | "metadata": {}, 492 | "source": [ 493 | "Mandatory:\n", 494 | "\n", 495 | "- `df`: Pandas DataFrame with `DateTimeIndex` as index\n", 496 | " and at least 2 columns (`'speed'` and `'direction'`).\n", 497 | "\n", 498 | "Optional:\n", 499 | "\n", 500 | "- `kind` : kind of plot (might be either, `'contour'`, `'contourf'`, `'bar'`, `'box'`, `'pdf'`)\n", 501 | "- `var_name` : name of var column name ; default value is `VAR_DEFAULT='speed'`\n", 502 | "- `direction_name` : name of direction column name ; default value is\n", 503 | " `DIR_DEFAULT='direction'`\n", 504 | "- `clean_flag` : cleanup data flag (remove\n", 505 | " data points with `NaN`, `var=0`) before plotting ; default value is\n", 506 | " `True`." 507 | ] 508 | }, 509 | { 510 | "cell_type": "markdown", 511 | "metadata": {}, 512 | "source": [ 513 | "## Video export\n", 514 | "\n", 515 | "A video of plots can be exported. A playlist of videos is [available here](https://www.youtube.com/playlist?list=PLE9hIvV5BUzsQ4EPBDnJucgmmZ85D_b-W), see:\n", 516 | "\n", 517 | "[![Video1](http://img.youtube.com/vi/0u2RxtGgEFo/0.jpg)](https://www.youtube.com/watch?v=0u2RxtGgEFo)\n", 518 | "\n", 519 | "[![Video2](http://img.youtube.com/vi/3CWpjSEt0so/0.jpg)](https://www.youtube.com/watch?v=3CWpjSEt0so)\n", 520 | "\n", 521 | "[![Video3](http://img.youtube.com/vi/UiGC-3aw9TM/0.jpg)](https://www.youtube.com/watch?v=UiGC-3aw9TM)\n", 522 | "\n", 523 | "[Source code](https://github.com/python-windrose/windrose/blob/master/samples/example_animate.py).\n", 524 | "\n", 525 | "This is just a sample for now. API for video need to be created.\n", 526 | "\n", 527 | "Use:\n", 528 | "\n", 529 | "```bash\n", 530 | "$ python samples/example_animate.py --help\n", 531 | "```\n", 532 | "\n", 533 | "to display command line interface usage." 534 | ] 535 | } 536 | ], 537 | "metadata": { 538 | "kernelspec": { 539 | "display_name": "Python 3 (ipykernel)", 540 | "language": "python", 541 | "name": "python3" 542 | }, 543 | "language_info": { 544 | "codemirror_mode": { 545 | "name": "ipython", 546 | "version": 3 547 | }, 548 | "file_extension": ".py", 549 | "mimetype": "text/x-python", 550 | "name": "python", 551 | "nbconvert_exporter": "python", 552 | "pygments_lexer": "ipython3", 553 | "version": "3.11.4" 554 | } 555 | }, 556 | "nbformat": 4, 557 | "nbformat_minor": 1 558 | } 559 | -------------------------------------------------------------------------------- /windrose/windrose.py: -------------------------------------------------------------------------------- 1 | """Windrose for matplotlib""" 2 | 3 | import locale 4 | import random 5 | 6 | import matplotlib as mpl 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | from matplotlib.projections.polar import PolarAxes 10 | from numpy import histogram2d 11 | 12 | ZBASE = -1000 # The starting zorder for all drawing, negative to have the grid on 13 | VAR_DEFAULT = "speed" 14 | DIR_DEFAULT = "direction" 15 | FIGSIZE_DEFAULT = (8, 8) 16 | DPI_DEFAULT = 80 17 | DEFAULT_THETA_LABELS = ["E", "N-E", "N", "N-W", "W", "S-W", "S", "S-E"] 18 | 19 | 20 | def _copy_docstring(source): 21 | """ 22 | 23 | Copy the docstring from another function. 24 | Implemented according to: https://github.com/matplotlib/matplotlib/blob/b5ac96a8980fdb9e59c9fb649e0714d776e26701/lib/matplotlib/_docstring.py#L86-L92 25 | 26 | """ # noqa: E501 27 | 28 | def inner(target): 29 | if source.__doc__ is not None: 30 | target.__doc__ = source.__doc__ 31 | return target 32 | 33 | return inner 34 | 35 | 36 | class WindAxesFactory: 37 | """ 38 | 39 | Factory class to create WindroseAxes or WindAxes 40 | 41 | """ 42 | 43 | @staticmethod 44 | def create(typ, ax=None, *args, **kwargs): 45 | """ 46 | 47 | Create 48 | 49 | Mandatory: 50 | 51 | Parameters 52 | ---------- 53 | typ : string, 'windroseaxes' or 'windaxes' 54 | Type of axes to create 55 | * windroseaxes : a WindroseAxes axe 56 | * windaxe : a WindAxes axe 57 | 58 | ax : matplotlib.Axes, optional 59 | A matplotlib axe 60 | 61 | """ 62 | typ = typ.lower() 63 | d = {"windroseaxes": WindroseAxes, "windaxes": WindAxes} 64 | if typ in d.keys(): 65 | cls = d[typ] 66 | if isinstance(ax, cls): 67 | return ax 68 | else: 69 | ax = cls.from_ax(ax, *args, **kwargs) 70 | return ax 71 | else: 72 | raise NotImplementedError(f"typ={typ!r} but it might be in {d.keys()}") 73 | 74 | 75 | class WindroseAxes(PolarAxes): 76 | """ 77 | 78 | Create a windrose axes 79 | 80 | """ 81 | 82 | name = "windrose" 83 | 84 | def __init__(self, *args, **kwargs): 85 | """ 86 | See Axes base class for args and kwargs documentation 87 | 88 | Other kwargs are: 89 | 90 | theta_labels : default ["E", "N-E", "N", "N-W", "W", "S-W", "S", "S-E"] 91 | Labels for theta coordinate 92 | """ 93 | 94 | # Uncomment to have the possibility to change the resolution directly 95 | # when the instance is created 96 | # self.RESOLUTION = kwargs.pop('resolution', 100) 97 | self.rmax = kwargs.pop("rmax", None) 98 | self.theta_labels = kwargs.pop("theta_labels", DEFAULT_THETA_LABELS) 99 | 100 | PolarAxes.__init__(self, *args, **kwargs) 101 | self.set_aspect("equal", adjustable="box", anchor="C") 102 | self.radii_angle = 67.5 103 | self.clear() 104 | 105 | @staticmethod 106 | def from_ax( 107 | ax=None, 108 | fig=None, 109 | rmax=None, 110 | figsize=FIGSIZE_DEFAULT, 111 | rect=None, 112 | *args, 113 | **kwargs, 114 | ): 115 | """ 116 | Return a WindroseAxes object for the figure `fig`. 117 | """ 118 | if ax is None: 119 | if fig is None: 120 | fig = plt.figure( 121 | figsize=figsize, 122 | dpi=DPI_DEFAULT, 123 | facecolor="w", 124 | edgecolor="w", 125 | ) 126 | if rect is None: 127 | rect = [0.1, 0.1, 0.8, 0.8] 128 | ax = WindroseAxes(fig, rect, *args, **kwargs) 129 | fig.add_axes(ax) 130 | return ax 131 | else: 132 | return ax 133 | 134 | def clear(self): 135 | """ 136 | Clear the current axes 137 | """ 138 | PolarAxes.clear(self) 139 | 140 | self.theta_angles = np.arange(0, 360, 45) 141 | self.set_thetagrids(angles=self.theta_angles, labels=self.theta_labels) 142 | 143 | self._info = {"dir": [], "bins": [], "table": []} 144 | 145 | self.patches_list = [] 146 | 147 | self.calm_count = None 148 | 149 | def _colors(self, cmap, n): 150 | """ 151 | Returns a list of n colors based on the colormap cmap 152 | 153 | """ 154 | return [cmap(i) for i in np.linspace(0.0, 1.0, n)] 155 | 156 | def set_radii_angle(self, **kwargs): 157 | """ 158 | Set the radii labels angle 159 | """ 160 | 161 | kwargs.pop("labels", None) 162 | angle = kwargs.pop("angle", None) 163 | if angle is None: 164 | angle = self.radii_angle 165 | self.radii_angle = angle 166 | N = 5 167 | rmax = self.get_rmax() 168 | radii = np.linspace(0, rmax, N + 1) 169 | if rmax % N == 0: 170 | fmt = "%d" 171 | else: 172 | fmt = "%.1f" 173 | radii_labels = [fmt % r for r in radii] 174 | # radii_labels[0] = "" # Removing label 0 175 | self.set_rgrids( 176 | radii=radii[1:], 177 | labels=radii_labels[1:], 178 | angle=self.radii_angle, 179 | **kwargs, 180 | ) 181 | 182 | def _update(self): 183 | if not self.rmax: 184 | self.rmax = np.max(np.sum(self._info["table"], axis=0)) 185 | calm_count = self.calm_count or 0 186 | self.set_rmax(rmax=self.rmax + calm_count) 187 | self.set_radii_angle(angle=self.radii_angle) 188 | 189 | def legend(self, loc="lower left", decimal_places=1, units=None, **kwargs): 190 | """ 191 | Sets the legend location and her properties. 192 | 193 | Parameters 194 | ---------- 195 | loc : int, string or pair of floats, default: 'lower left' 196 | see :obj:`matplotlib.pyplot.legend`. 197 | 198 | decimal_places : int, default 1 199 | The decimal places of the formatted legend 200 | 201 | units: str, default None 202 | 203 | Other Parameters 204 | ---------------- 205 | isaxes : boolean, default True 206 | whether this is an axes legend 207 | prop : FontProperties(size='smaller') 208 | the font property 209 | borderpad : float 210 | the fractional whitespace inside the legend border 211 | shadow : boolean 212 | if True, draw a shadow behind legend 213 | labelspacing : float, 0.005 214 | the vertical space between the legend entries 215 | handlelenght : float, 0.05 216 | the length of the legend lines 217 | handletextsep : float, 0.02 218 | the space between the legend line and legend text 219 | borderaxespad : float, 0.02 220 | the border between the axes and legend edge 221 | kwarg 222 | Every other kwarg argument supported by 223 | :obj:`matplotlib.pyplot.legend` 224 | """ 225 | 226 | def get_handles(): 227 | handles = [] 228 | for p in self.patches_list: 229 | if isinstance(p, mpl.patches.Polygon) or isinstance( 230 | p, 231 | mpl.patches.Rectangle, 232 | ): 233 | color = p.get_facecolor() 234 | elif isinstance(p, mpl.lines.Line2D): 235 | color = p.get_color() 236 | else: 237 | raise AttributeError("Can't handle patches") 238 | handles.append( 239 | mpl.patches.Rectangle( 240 | (0, 0), 241 | 0.2, 242 | 0.2, 243 | facecolor=color, 244 | edgecolor="black", 245 | ), 246 | ) 247 | return handles 248 | 249 | def get_labels(decimal_places=1, units=None): 250 | digits = np.copy(self._info["bins"]).tolist() 251 | if not digits: 252 | return "" 253 | digits[-1] = digits[-2] 254 | digits = [f"{label:.{decimal_places}f}" for label in digits] 255 | fmt = "[{} : {}" 256 | if locale.getlocale()[0] in ["fr_FR"]: 257 | fmt += "[" 258 | else: 259 | fmt += ")" 260 | 261 | if units: 262 | fmt += " " + units 263 | 264 | labels = [ 265 | fmt.format(digits[k], digits[k + 1]) for k in range(len(digits) - 1) 266 | ] 267 | if units: 268 | labels[-1] = f">{digits[-1]} " + units 269 | else: 270 | labels[-1] = f">{digits[-1]}" 271 | return labels 272 | 273 | kwargs.pop("labels", None) 274 | kwargs.pop("handles", None) 275 | 276 | handles = get_handles() 277 | labels = get_labels(decimal_places, units) 278 | self.legend_ = mpl.legend.Legend(self, handles, labels, loc=loc, **kwargs) 279 | return self.legend_ 280 | 281 | def set_legend(self, **pyplot_arguments): 282 | if "borderaxespad" not in pyplot_arguments: 283 | pyplot_arguments["borderaxespad"] = -0.10 284 | legend = self.legend(**pyplot_arguments) 285 | plt.setp(legend.get_texts(), fontsize=8) 286 | return legend 287 | 288 | def _init_plot(self, direction, var, **kwargs): 289 | """ 290 | Internal method used by all plotting commands 291 | 292 | Parameters 293 | ---------- 294 | direction : 1D array, 295 | directions the wind blows from, North centred 296 | var : 1D array, 297 | values of the variable to compute. Typically the wind speeds 298 | 299 | Other Parameters 300 | ---------------- 301 | normed : boolean, default False 302 | blowto : boolean, default False 303 | colors : str or list of str, default None 304 | The colors of the plot. 305 | cmap : color map 306 | A :obj:`matplotlib.cm` colormap for the plot. 307 | Warning! It overrides `colors`. 308 | weibull_factors : 309 | mean_values : 310 | frequency : 311 | calm_limit : float, default None 312 | kwarg 313 | Any argument accepted by :obj:`matplotlib.pyplot.plot`. 314 | """ 315 | 316 | normed = kwargs.pop("normed", False) 317 | blowto = kwargs.pop("blowto", False) 318 | 319 | # Calm condition, mask data if needed 320 | calm_limit = kwargs.pop("calm_limit", None) 321 | total = len(var) 322 | if calm_limit is not None: 323 | mask = var > calm_limit 324 | self.calm_count = len(var) - np.count_nonzero(mask) 325 | if normed: 326 | self.calm_count = self.calm_count * 100 / len(var) 327 | var = var[mask] 328 | direction = direction[mask] 329 | 330 | # if weibull factors are entered overwrite direction and var 331 | if "weibull_factors" in kwargs or "mean_values" in kwargs: 332 | if "weibull_factors" in kwargs and "mean_values" in kwargs: 333 | raise TypeError("cannot specify both weibull_factors and mean_values") 334 | statistic_type = "unset" 335 | if "weibull_factors" in kwargs: 336 | statistic_type = "weibull" 337 | val = kwargs.pop("weibull_factors") 338 | elif "mean_values" in kwargs: 339 | statistic_type = "mean" 340 | val = kwargs.pop("mean_values") 341 | if val: 342 | if "frequency" not in kwargs: 343 | raise TypeError( 344 | "specify 'frequency' argument for statistical input", 345 | ) 346 | windFrequencies = kwargs.pop("frequency") 347 | if len(windFrequencies) != len(direction) or len(direction) != len(var): 348 | if len(windFrequencies) != len(direction): 349 | raise TypeError("len(frequency) != len(direction)") 350 | elif len(direction) != len(var): 351 | raise TypeError("len(frequency) != len(direction)") 352 | windSpeeds = [] 353 | windDirections = [] 354 | for dbin in range(len(direction)): 355 | for _ in range(int(windFrequencies[dbin] * 10000)): 356 | if statistic_type == "weibull": 357 | windSpeeds.append( 358 | random.weibullvariate(var[dbin][0], var[dbin][1]), 359 | ) 360 | elif statistic_type == "mean": 361 | windSpeeds.append( 362 | random.weibullvariate( 363 | var[dbin] * 2 / np.sqrt(np.pi), 364 | 2, 365 | ), 366 | ) 367 | windDirections.append(direction[dbin]) 368 | var, direction = windSpeeds, windDirections 369 | 370 | # self.clear() 371 | kwargs.pop("zorder", None) 372 | 373 | # Init of the bins array if not set 374 | bins = kwargs.pop("bins", None) 375 | if bins is None: 376 | bins = np.linspace(np.min(var), np.max(var), 6) 377 | if isinstance(bins, (list, tuple, np.ndarray)): 378 | if len(bins) > 0 and bins[0] > np.min(var) and not calm_limit: 379 | raise ValueError( 380 | f"the first value provided in bins must be less than or equal " 381 | f"to the minimum value of the wind speed data. " 382 | f"Did you mean: bins={(0, *bins)!r} ? " 383 | f"If you want to exclude values below a certain threshold, " 384 | f"try setting calm_limit={min(bins)}.", 385 | ) 386 | elif len(bins) > 0 and calm_limit is not None and min(bins) < calm_limit: 387 | raise ValueError( 388 | f"the lowest value in bins must be >= {calm_limit} (=calm_limits)", 389 | ) 390 | if isinstance(bins, int): 391 | bins = np.linspace(np.min(var), np.max(var), bins) 392 | bins = np.asarray(bins) 393 | nbins = len(bins) 394 | 395 | if np.isnan(bins).any(): 396 | raise ValueError( 397 | "Could not compute the bins due to the presence of NaNs in " 398 | "either the bins provided or the original data.", 399 | ) 400 | 401 | # Number of sectors 402 | nsector = kwargs.pop("nsector", None) 403 | if nsector is None: 404 | nsector = 16 405 | 406 | sector_offset = kwargs.get("sectoroffset", 0) 407 | 408 | # Sets the colors table based on the colormap or the "colors" argument 409 | colors = kwargs.pop("colors", None) 410 | cmap = kwargs.pop("cmap", None) 411 | if colors is not None: 412 | if isinstance(colors, str): 413 | colors = [colors] * nbins 414 | if isinstance(colors, (tuple, list)): 415 | if len(colors) != nbins: 416 | raise ValueError("colors and bins must have same length") 417 | else: 418 | if cmap is None: 419 | cmap = plt.get_cmap() 420 | colors = self._colors(cmap, nbins) 421 | 422 | # Building the angles list 423 | angles = np.arange(0, -2 * np.pi, -2 * np.pi / nsector) + np.pi / 2 424 | 425 | # Set the global information dictionary 426 | self._info["dir"], self._info["bins"], self._info["table"] = histogram( 427 | direction, 428 | var, 429 | bins, 430 | nsector, 431 | total, 432 | sector_offset, 433 | normed, 434 | blowto, 435 | ) 436 | 437 | return bins, nbins, nsector, colors, angles, kwargs 438 | 439 | def _calm_circle(self): 440 | """Draw the calm centered circle""" 441 | if self.calm_count and self.calm_count > 0: 442 | self.set_rorigin(-(np.sqrt(self.calm_count / np.pi))) 443 | 444 | def contour(self, direction, var, **kwargs): 445 | """ 446 | Plot a windrose in linear mode. For each var bins, a line will be 447 | draw on the axes, a segment between each sector (center to center). 448 | Each line can be formatted (color, width, ...) like with standard plot 449 | pylab command. 450 | 451 | Parameters 452 | ---------- 453 | direction : 1D array 454 | directions the wind blows from, North centred 455 | var : 1D array 456 | values of the variable to compute. Typically the wind speeds. 457 | 458 | Other Parameters 459 | ---------------- 460 | nsector : integer, optional 461 | number of sectors used to compute the windrose table. If not set, 462 | nsector=16, then each sector will be 360/16=22.5°, and the 463 | resulting computed table will be aligned with the cardinals points. 464 | bins : 1D array or integer, optional 465 | number of bins, or a sequence of bins variable. If not set, bins=6, 466 | then bins=linspace(min(var), max(var), 6) 467 | blowto : bool, optional 468 | If True, the windrose will be pi rotated, to show where the wind 469 | blow to (useful for pollutant rose). 470 | colors : string or tuple, optional 471 | one string color ('k' or 'black'), in this case all bins will be 472 | plotted in this color; a tuple of matplotlib color args (string, 473 | float, rgb, etc), different levels will be plotted in different 474 | colors in the order specified. 475 | cmap : a cm Colormap instance from :obj:`matplotlib.cm`, optional 476 | if cmap == None and colors == None, a default Colormap is used. 477 | calm_limit : float, optional 478 | Calm limit for the var parameter. If not None, a centered red 479 | circle will be draw for representing the calms occurrences and all 480 | data below this value will be removed from the computation. 481 | 482 | others kwargs 483 | Any supported argument of :obj:`matplotlib.pyplot.plot` 484 | 485 | """ 486 | bins, nbins, nsector, colors, angles, kwargs = self._init_plot( 487 | direction, 488 | var, 489 | **kwargs, 490 | ) 491 | 492 | # closing lines 493 | angles = np.hstack((angles, angles[-1] - 2 * np.pi / nsector)) 494 | vals = np.hstack( 495 | ( 496 | self._info["table"], 497 | np.reshape( 498 | self._info["table"][:, 0], 499 | (self._info["table"].shape[0], 1), 500 | ), 501 | ), 502 | ) 503 | 504 | self._calm_circle() 505 | origin = 0 506 | for i in range(nbins): 507 | val = vals[i, :] + origin 508 | origin += vals[i, :] 509 | zorder = ZBASE + nbins - i 510 | patch = self.plot(angles, val, color=colors[i], zorder=zorder, **kwargs) 511 | self.patches_list.extend(patch) 512 | self._update() 513 | 514 | def contourf(self, direction, var, **kwargs): 515 | """ 516 | Plot a windrose in filled mode. For each var bins, a line will be 517 | draw on the axes, a segment between each sector (center to center). 518 | Each line can be formatted (color, width, ...) like with standard plot 519 | pylab command. 520 | 521 | Parameters 522 | ---------- 523 | direction : 1D array 524 | directions the wind blows from, North centred 525 | var : 1D array 526 | values of the variable to compute. Typically the wind speeds 527 | 528 | Other Parameters 529 | ---------------- 530 | nsector: integer, optional 531 | number of sectors used to compute the windrose table. If not set, 532 | nsector=16, then each sector will be 360/16=22.5°, and the 533 | resulting computed table will be aligned with the cardinals points. 534 | bins : 1D array or integer, optional 535 | number of bins, or a sequence of bins variable. If not set, bins=6, 536 | then bins=linspace(min(`var`), max(`var`), 6) 537 | blowto : bool, optional 538 | If True, the windrose will be pi rotated, to show where the wind 539 | blow to (useful for pollutant rose). 540 | colors : string or tuple, optional 541 | one string color ('k' or 'black'), in this case all bins will be 542 | plotted in this color; a tuple of matplotlib color args (string, 543 | float, rgb, etc), different levels will be plotted in different 544 | colors in the order specified. 545 | cmap : a cm Colormap instance from :obj:`matplotlib.cm`, optional 546 | if cmap == None and colors == None, a default Colormap is used. 547 | calm_limit : float, optional 548 | Calm limit for the var parameter. If not None, a centered red 549 | circle will be draw for representing the calms occurrences and all 550 | data below this value will be removed from the computation. 551 | 552 | others kwargs 553 | Any supported argument of :obj:`matplotlib.pyplot.plot` 554 | """ 555 | 556 | bins, nbins, nsector, colors, angles, kwargs = self._init_plot( 557 | direction, 558 | var, 559 | **kwargs, 560 | ) 561 | kwargs.pop("facecolor", None) 562 | kwargs.pop("edgecolor", None) 563 | 564 | # closing lines 565 | angles = np.hstack((angles, angles[-1] - 2 * np.pi / nsector)) 566 | vals = np.hstack( 567 | ( 568 | self._info["table"], 569 | np.reshape( 570 | self._info["table"][:, 0], 571 | (self._info["table"].shape[0], 1), 572 | ), 573 | ), 574 | ) 575 | self._calm_circle() 576 | origin = 0 577 | for i in range(nbins): 578 | val = vals[i, :] + origin 579 | origin += vals[i, :] 580 | zorder = ZBASE + nbins - i 581 | patch = self.fill( 582 | np.append(angles, 0), 583 | np.append(val, 0), 584 | facecolor=colors[i], 585 | edgecolor=colors[i], 586 | zorder=zorder, 587 | **kwargs, 588 | ) 589 | self.patches_list.extend(patch) 590 | self._update() 591 | 592 | def bar(self, direction, var, **kwargs): 593 | """ 594 | Plot a windrose in bar mode. For each var bins and for each sector, 595 | a colored bar will be draw on the axes. 596 | 597 | Parameters 598 | ---------- 599 | direction : 1D array 600 | directions the wind blows from, North centred 601 | var : 1D array 602 | values of the variable to compute. Typically the wind speeds. 603 | 604 | Other Parameters 605 | ---------------- 606 | nsector : integer, optional 607 | number of sectors used to compute the windrose table. If not set, 608 | nsector=16, then each sector will be 360/16=22.5°, and the 609 | resulting computed table will be aligned with the cardinals points. 610 | sectoroffset: float, optional 611 | the offset for the sectors between [-180/nsector, 180/nsector]. 612 | By default, the offset is zero, and the first sector is 613 | [-360/nsector/2, 360/nsector/2] or [-11.25, 11.25] for nsector=16. 614 | If offset is non-zero, the first sector will be 615 | [-360/nsector + offset, 360/nsector + offset] and etc. 616 | bins : 1D array or integer, optional 617 | number of bins, or a sequence of bins variable. If not set, bins=6 618 | between min(`var`) and max(`var`). 619 | blowto : bool, optional. 620 | if True, the windrose will be pi rotated, to show where the wind 621 | blow to (useful for pollutant rose). 622 | colors : string or tuple, optional 623 | one string color ('k' or 'black'), in this case all bins will be 624 | plotted in this color; a tuple of matplotlib color args (string, 625 | float, rgb, etc), different levels will be plotted 626 | in different colors in the order specified. 627 | cmap : a cm Colormap instance from :obj:`matplotlib.cm`, optional. 628 | if cmap == None and colors == None, a default Colormap is used. 629 | edgecolor : string, optional 630 | The string color each edge box will be plotted. 631 | Default : no edgecolor 632 | opening : float, optional 633 | between 0.0 and 1.0, to control the space between each sector (1.0 634 | for no space) 635 | calm_limit : float, optional 636 | Calm limit for the var parameter. If not None, a centered red 637 | circle will be draw for representing the calms occurrences and all 638 | data below this value will be removed from the computation. 639 | 640 | """ 641 | 642 | bins, nbins, nsector, colors, angles, kwargs = self._init_plot( 643 | direction, 644 | var, 645 | **kwargs, 646 | ) 647 | kwargs.pop("facecolor", None) 648 | edgecolor = kwargs.pop("edgecolor", None) 649 | if edgecolor is not None: 650 | if not isinstance(edgecolor, str): 651 | raise ValueError("edgecolor must be a string color") 652 | opening = kwargs.pop("opening", None) 653 | if opening is None: 654 | opening = 0.8 655 | dtheta = 2 * np.pi / nsector 656 | opening = dtheta * opening 657 | 658 | self._calm_circle() 659 | 660 | # sector offset in radius 661 | sector_offset = kwargs.pop("sectoroffset", 0) / 180 * np.pi 662 | 663 | for j in range(nsector): 664 | origin = 0 665 | for i in range(nbins): 666 | if i > 0: 667 | origin += self._info["table"][i - 1, j] 668 | val = self._info["table"][i, j] 669 | zorder = ZBASE + nbins - i 670 | patch = mpl.patches.Rectangle( 671 | (angles[j] - opening / 2 - sector_offset, origin), 672 | opening, 673 | val, 674 | facecolor=colors[i], 675 | edgecolor=edgecolor, 676 | zorder=zorder, 677 | **kwargs, 678 | ) 679 | # needed so the the line of the rectangle becomes curved 680 | patch.get_path()._interpolation_steps = 100 681 | self.add_patch(patch) 682 | if j == 0: 683 | self.patches_list.append(patch) 684 | self._update() 685 | 686 | def box(self, direction, var, **kwargs): 687 | """ 688 | Plot a windrose in proportional box mode. For each var bins and for 689 | each sector, a colored box will be draw on the axes. 690 | 691 | Parameters 692 | ---------- 693 | direction : 1D array 694 | directions the wind blows from, North centred 695 | var : 1D array 696 | values of the variable to compute. Typically the wind speeds 697 | 698 | Other Parameters 699 | ---------------- 700 | nsector: integer, optional 701 | number of sectors used to compute the windrose table. If not set, 702 | nsector=16, then each sector will be 360/16=22.5°, and the 703 | resulting computed table will be aligned with the cardinals points. 704 | sectoroffset: float, optional 705 | the offset for the sectors. By default, the offsect is zero, and 706 | the first sector is [-360/nsector, 360/nsector] or [-11.25, 11.25] 707 | for nsector=16. If offset is non-zero, the first sector will be 708 | [-360/nsector + offset, 360/nsector + offset] and etc. 709 | bins : 1D array or integer, optional 710 | number of bins, or a sequence of bins variable. If not set, bins=6 711 | between min(`var`) and max(`var`). 712 | blowto : bool, optional 713 | If True, the windrose will be pi rotated, to show where the wind 714 | blow to (useful for pollutant rose). 715 | colors : string or tuple, optional 716 | one string color ('k' or 'black'), in this case all bins will be 717 | plotted in this color; a tuple of matplotlib color args (string, 718 | float, rgb, etc), different levels will be plotted in different 719 | colors in the order specified. 720 | cmap : a cm Colormap instance from :obj:`matplotlib.cm`, optional 721 | if cmap == None and colors == None, a default Colormap is used. 722 | edgecolor : string, optional 723 | The string color each edge bar will be plotted. Default : no 724 | edgecolor 725 | calm_limit : float, optional 726 | Calm limit for the var parameter. If not None, a centered red 727 | circle will be draw for representing the calms occurrences and all 728 | data below this value will be removed from the computation. 729 | 730 | """ 731 | 732 | bins, nbins, nsector, colors, angles, kwargs = self._init_plot( 733 | direction, 734 | var, 735 | **kwargs, 736 | ) 737 | kwargs.pop("facecolor", None) 738 | edgecolor = kwargs.pop("edgecolor", None) 739 | if edgecolor is not None: 740 | if not isinstance(edgecolor, str): 741 | raise ValueError("edgecolor must be a string color") 742 | opening = np.linspace(0.0, np.pi / 16, nbins) 743 | 744 | self._calm_circle() 745 | 746 | # sector offset in radius 747 | sector_offset = kwargs.pop("sectoroffset", 0) / 180 * np.pi 748 | 749 | for j in range(nsector): 750 | origin = 0 751 | for i in range(nbins): 752 | if i > 0: 753 | origin += self._info["table"][i - 1, j] 754 | val = self._info["table"][i, j] 755 | zorder = ZBASE + nbins - i 756 | patch = mpl.patches.Rectangle( 757 | (angles[j] - opening[i] / 2 - sector_offset, origin), 758 | opening[i], 759 | val, 760 | facecolor=colors[i], 761 | edgecolor=edgecolor, 762 | zorder=zorder, 763 | **kwargs, 764 | ) 765 | # needed so the the line of the rectangle becomes curved 766 | patch.get_path()._interpolation_steps = 100 767 | self.add_patch(patch) 768 | if j == 0: 769 | self.patches_list.append(patch) 770 | self._update() 771 | 772 | 773 | class WindAxes(mpl.axes.Subplot): 774 | def __init__(self, *args, **kwargs): 775 | """ 776 | See Axes base class for args and kwargs documentation 777 | """ 778 | super().__init__(*args, **kwargs) 779 | 780 | @staticmethod 781 | def from_ax(ax=None, fig=None, figsize=FIGSIZE_DEFAULT, *args, **kwargs): 782 | if ax is None: 783 | if fig is None: 784 | fig = plt.figure(figsize=figsize, dpi=DPI_DEFAULT) 785 | ax = WindAxes(fig, 1, 1, 1, *args, **kwargs) 786 | fig.add_axes(ax) 787 | return ax 788 | else: 789 | return ax 790 | 791 | def pdf( 792 | self, 793 | var, 794 | bins=None, 795 | Nx=100, 796 | bar_color="b", 797 | plot_color="g", 798 | Nbins=10, 799 | *args, 800 | **kwargs, 801 | ): 802 | """ 803 | Draw probability density function and return Weibull distribution 804 | parameters 805 | """ 806 | import scipy.stats 807 | 808 | if bins is None: 809 | bins = np.linspace(0, np.max(var), Nbins) 810 | hist, bins = np.histogram(var, bins=bins, density=True) 811 | width = 0.7 * (bins[1] - bins[0]) 812 | center = (bins[:-1] + bins[1:]) / 2 813 | self.bar(center, hist, align="center", width=width, color=bar_color) 814 | params = scipy.stats.exponweib.fit(var, floc=0, f0=1) 815 | x = np.linspace(0, bins[-1], Nx) 816 | self.plot(x, scipy.stats.exponweib.pdf(x, *params), color=plot_color) 817 | return (self, params) 818 | 819 | 820 | def histogram( 821 | direction, 822 | var, 823 | bins, 824 | nsector, 825 | total, 826 | sectoroffset=0, 827 | normed=False, 828 | blowto=False, 829 | ): 830 | """ 831 | Returns an array where, for each sector of wind 832 | (centred on the north), we have the number of time the wind comes with a 833 | particular var (speed, pollutant concentration, ...). 834 | 835 | Parameters 836 | ---------- 837 | direction : 1D array 838 | directions the wind blows from, North centred 839 | var : 1D array 840 | values of the variable to compute. Typically the wind speeds 841 | bins : list 842 | list of var category against we're going to compute the table 843 | nsector : integer 844 | number of sectors 845 | 846 | Other Parameters 847 | ---------------- 848 | normed : boolean, default False 849 | The resulting table is normed in percent or not. 850 | blowto : boolean, default False 851 | Normally a windrose is computed with directions as wind blows from. If 852 | true, the table will be reversed (useful for pollutantrose) 853 | """ 854 | 855 | if len(var) != len(direction): 856 | raise ValueError("var and direction must have same length") 857 | 858 | angle = 360.0 / nsector 859 | 860 | dir_bins = np.arange( 861 | -angle / 2 + sectoroffset, 862 | 360.0 + angle + sectoroffset, 863 | angle, 864 | dtype=float, 865 | ) 866 | dir_edges = dir_bins.tolist() 867 | dir_edges.pop(-1) 868 | dir_edges[0] = dir_edges.pop(-1) 869 | 870 | var_bins = bins.tolist() 871 | var_bins.append(np.inf) 872 | 873 | if blowto: 874 | direction = direction + 180.0 875 | direction[direction >= 360.0] = direction[direction >= 360.0] - 360 876 | 877 | table = histogram2d(x=var, y=direction, bins=[var_bins, dir_bins], density=False)[0] 878 | # add the last value to the first to have the table of North winds 879 | table[:, 0] = table[:, 0] + table[:, -1] 880 | # and remove the last col 881 | table = table[:, :-1] 882 | if normed: 883 | table = table * 100 / total 884 | 885 | return dir_edges, var_bins, table 886 | 887 | 888 | @_copy_docstring(WindroseAxes.contour) 889 | def wrcontour(direction, var, ax=None, rmax=None, figsize=FIGSIZE_DEFAULT, **kwargs): 890 | """ 891 | Draw contour probability density function and return Weibull 892 | distribution parameters. 893 | """ 894 | ax = WindroseAxes.from_ax(ax, rmax=rmax, figsize=figsize) 895 | ax.contour(direction, var, **kwargs) 896 | ax.set_legend() 897 | return ax 898 | 899 | 900 | @_copy_docstring(WindroseAxes.contourf) 901 | def wrcontourf(direction, var, ax=None, rmax=None, figsize=FIGSIZE_DEFAULT, **kwargs): 902 | ax = WindroseAxes.from_ax(ax, rmax=rmax, figsize=figsize) 903 | ax.contourf(direction, var, **kwargs) 904 | ax.set_legend() 905 | return ax 906 | 907 | 908 | @_copy_docstring(WindroseAxes.box) 909 | def wrbox(direction, var, ax=None, rmax=None, figsize=FIGSIZE_DEFAULT, **kwargs): 910 | ax = WindroseAxes.from_ax(ax, rmax=rmax, figsize=figsize) 911 | ax.box(direction, var, **kwargs) 912 | ax.set_legend() 913 | return ax 914 | 915 | 916 | @_copy_docstring(WindroseAxes.bar) 917 | def wrbar(direction, var, ax=None, rmax=None, figsize=FIGSIZE_DEFAULT, **kwargs): 918 | ax = WindroseAxes.from_ax(ax, rmax=rmax, figsize=figsize) 919 | ax.bar(direction, var, **kwargs) 920 | ax.set_legend() 921 | return ax 922 | 923 | 924 | @_copy_docstring(WindAxes.pdf) 925 | def wrpdf( 926 | var, 927 | bins=None, 928 | Nx=100, 929 | bar_color="b", 930 | plot_color="g", 931 | Nbins=10, 932 | ax=None, 933 | rmax=None, 934 | figsize=FIGSIZE_DEFAULT, 935 | *args, 936 | **kwargs, 937 | ): 938 | """ 939 | Draw probability density function and return Weitbull distribution 940 | parameters 941 | """ 942 | ax = WindAxes.from_ax(ax, figsize=figsize) 943 | ax, params = ax.pdf(var, bins, Nx, bar_color, plot_color, Nbins, *args, **kwargs) 944 | return (ax, params) 945 | 946 | 947 | def wrscatter( 948 | direction, 949 | var, 950 | ax=None, 951 | rmax=None, 952 | figsize=FIGSIZE_DEFAULT, 953 | *args, 954 | **kwargs, 955 | ): 956 | """ 957 | Draw scatter plot 958 | """ 959 | ax = WindroseAxes.from_ax(ax, rmax=rmax, figsize=figsize) 960 | direction = -np.array(direction) + np.radians(90) 961 | ax.scatter(direction, var, *args, **kwargs) 962 | return ax 963 | 964 | 965 | # def clean(direction, var): 966 | # ''' 967 | # Remove masked values in the two arrays, where if a direction data is masked, 968 | # the var data will also be removed in the cleaning process (and vice-versa) 969 | # ''' 970 | # dirmask = direction.mask==False 971 | # varmask = direction.mask==False 972 | # mask = dirmask*varmask 973 | # return direction[mask], var[mask] 974 | 975 | 976 | def clean_df(df, var=VAR_DEFAULT, direction=DIR_DEFAULT): 977 | """ 978 | Remove nan and var=0 values in the DataFrame 979 | if a var (wind speed) is nan or equal to 0, this row is 980 | removed from DataFrame 981 | if a direction is nan, this row is also removed from DataFrame 982 | """ 983 | return df[df[var].notnull() & (df[var] != 0) & df[direction].notnull()] 984 | 985 | 986 | def clean(direction, var, index=False): 987 | """ 988 | Remove nan and var=0 values in the two arrays 989 | if a var (wind speed) is nan or equal to 0, this data is 990 | removed from var array but also from dir array 991 | if a direction is nan, data is also removed from both array 992 | """ 993 | dirmask = np.isfinite(direction) 994 | varmask = (var != 0) & np.isfinite(var) 995 | mask = dirmask & varmask 996 | if index is None: 997 | index = np.arange(mask.sum()) 998 | return direction[mask], var[mask], index 999 | elif not index: 1000 | return direction[mask], var[mask] 1001 | else: 1002 | index = index[mask] 1003 | return direction[mask], var[mask], index 1004 | 1005 | 1006 | D_KIND_PLOT = { 1007 | "contour": wrcontour, 1008 | "contourf": wrcontourf, 1009 | "box": wrbox, 1010 | "bar": wrbar, 1011 | "pdf": wrpdf, 1012 | "scatter": wrscatter, 1013 | } 1014 | 1015 | 1016 | def plot_windrose( 1017 | direction_or_df, 1018 | var=None, 1019 | kind="contour", 1020 | var_name=VAR_DEFAULT, 1021 | direction_name=DIR_DEFAULT, 1022 | by=None, 1023 | rmax=None, 1024 | ax=None, 1025 | **kwargs, 1026 | ): 1027 | """Plot windrose from a pandas DataFrame or a numpy array.""" 1028 | if var is None: 1029 | # Assuming direction_or_df is a DataFrame 1030 | df = direction_or_df 1031 | var = df[var_name].values 1032 | direction = df[direction_name].values 1033 | else: 1034 | direction = direction_or_df 1035 | return plot_windrose_np( 1036 | direction, 1037 | var, 1038 | kind=kind, 1039 | by=by, 1040 | rmax=rmax, 1041 | ax=ax, 1042 | **kwargs, 1043 | ) 1044 | 1045 | 1046 | def plot_windrose_df( 1047 | df, 1048 | kind="contour", 1049 | var_name=VAR_DEFAULT, 1050 | direction_name=DIR_DEFAULT, 1051 | by=None, 1052 | rmax=None, 1053 | ax=None, 1054 | **kwargs, 1055 | ): 1056 | """Plot windrose from a pandas DataFrame.""" 1057 | var = df[var_name].values 1058 | direction = df[direction_name].values 1059 | return plot_windrose_np( 1060 | direction, 1061 | var, 1062 | kind=kind, 1063 | by=by, 1064 | rmax=rmax, 1065 | ax=ax, 1066 | **kwargs, 1067 | ) 1068 | 1069 | 1070 | def plot_windrose_np( 1071 | direction, 1072 | var, 1073 | kind="contour", 1074 | clean_flag=True, 1075 | by=None, 1076 | rmax=None, 1077 | ax=None, 1078 | **kwargs, 1079 | ): 1080 | """Plot windrose from a numpy array.""" 1081 | if kind in D_KIND_PLOT.keys(): 1082 | f_plot = D_KIND_PLOT[kind] 1083 | else: 1084 | raise Exception(f"kind={kind!r} but it must be in {D_KIND_PLOT.keys()!r}") 1085 | # if f_clean is not None: 1086 | # df = f_clean(df) 1087 | # var = df[var_name].values 1088 | # direction = df[direction_name].values 1089 | if clean_flag: 1090 | direction, var = clean(direction, var) 1091 | if by is None: 1092 | ax = f_plot(direction=direction, var=var, rmax=rmax, ax=ax, **kwargs) 1093 | if kind not in ["pdf"]: 1094 | ax.set_legend() 1095 | return ax 1096 | else: 1097 | raise NotImplementedError( 1098 | "'by' keyword not supported for now " 1099 | "https://github.com/scls19fr/windrose/issues/10", 1100 | ) 1101 | --------------------------------------------------------------------------------